@bobfrankston/npmglobalize 1.0.169 → 1.0.170

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -37,6 +37,12 @@ Dependency Options:
37
37
  -publish-deps Auto-publish file: dependencies (default)
38
38
  -pd Like -publish-deps, plus auto-yes to dep-cascade prompts (private only)
39
39
  -no-publish-deps, -npd Don't auto-publish file: dependencies (use with caution)
40
+ -public-deps Cascade npmVisibility:"public" to all transitive workspace/file:
41
+ deps. In workspace mode: auto-promotes every reachable workspace
42
+ member without prompting (writes npmVisibility:"public" into each
43
+ dep's .globalize.json5). In single-package mode: also propagates
44
+ public visibility to already-published deps in the cascade.
45
+ Use when a public consumer's deps weren't all marked public.
40
46
  -no-prescan, -nps Skip upfront dep-graph prescan
41
47
  -force-publish Republish dependencies even if version exists
42
48
  -fix Run npm audit fix after transformation
@@ -298,6 +304,9 @@ function parseArgs(args) {
298
304
  case '-npd':
299
305
  options.publishDeps = false; // Disable auto-publishing
300
306
  break;
307
+ case '-public-deps':
308
+ options.publicDeps = true;
309
+ break;
301
310
  case '-no-use-paths':
302
311
  case '-nup':
303
312
  options.usePaths = false; // Skip sibling-path resolution; use latest npm version for file: deps
package/lib/config.js CHANGED
@@ -65,7 +65,7 @@ export function writeConfig(dir, config, explicitKeys) {
65
65
  const existing = readConfig(dir);
66
66
  // Filter out temporary flags and default values (unless explicitly set)
67
67
  const filtered = {};
68
- const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'forcePublish', 'once', 'cleanNestedModules']);
68
+ const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'publicDeps', 'forcePublish', 'once', 'cleanNestedModules']);
69
69
  for (const [key, value] of Object.entries(config)) {
70
70
  if (omitKeys.has(key))
71
71
  continue;
package/lib/types.d.ts CHANGED
@@ -54,6 +54,8 @@ export interface GlobalizeOptions {
54
54
  publishDeps?: boolean;
55
55
  /** Auto-yes to dep-cascade prompts (add scope for private); does NOT auto-yes public prompts */
56
56
  publishDepsYes?: boolean;
57
+ /** Cascade npmVisibility:"public" to all transitive workspace/file: deps without prompting. */
58
+ publicDeps?: boolean;
57
59
  /** Skip the upfront dep-graph prescan */
58
60
  noPrescan?: boolean;
59
61
  /** Force republish dependencies even if version exists on npm */
package/lib.d.ts CHANGED
@@ -73,6 +73,12 @@ export interface GlobalizeOptions {
73
73
  publishDeps?: boolean;
74
74
  /** Auto-yes to dep-cascade prompts (add scope to make private, etc.); does NOT auto-yes public prompts */
75
75
  publishDepsYes?: boolean;
76
+ /** Cascade npmVisibility:"public" to all transitive workspace/file: deps without prompting.
77
+ * In workspace mode: fixpoint-promotes every workspace member reachable from a public consumer.
78
+ * In single-package mode: drops the "first publish only" guard so already-published deps also
79
+ * get the parent's npmVisibility propagated. Use to fix a workspace where a public member's
80
+ * deps weren't all marked public (and the published tarball would 404 on install). */
81
+ publicDeps?: boolean;
76
82
  /** Force republish dependencies even if version exists on npm */
77
83
  forcePublish?: boolean;
78
84
  /** Run npm audit and fix vulnerabilities */
package/lib.js CHANGED
@@ -238,7 +238,7 @@ export function writeConfig(dir, config, explicitKeys) {
238
238
  const existing = readConfig(dir);
239
239
  // Filter out temporary flags and default values (unless explicitly set)
240
240
  const filtered = {};
241
- const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'forcePublish', 'once', 'cleanNestedModules']);
241
+ const omitKeys = new Set(['cleanup', 'init', 'dryRun', 'message', 'conform', 'asis', 'help', 'error', 'updateDeps', 'updateMajor', 'publishDeps', 'publicDeps', 'forcePublish', 'once', 'cleanNestedModules']);
242
242
  for (const [key, value] of Object.entries(config)) {
243
243
  if (omitKeys.has(key))
244
244
  continue;
@@ -3108,6 +3108,7 @@ async function doLocalInstall(cwd, options) {
3108
3108
  export async function globalize(cwd, options = {}, configOptions = {}) {
3109
3109
  const { bump = 'patch', noPublish = false, cleanup = false, install = false, link = false, wsl = false, force = false, files = true, dryRun = false, quiet = true, verbose = false, init = false, gitVisibility = 'private', npmVisibility = 'private', message, conform = false, asis = false, updateDeps = false, updateMajor = false, publishDeps = true, // Default to publishing deps for safety
3110
3110
  publishDepsYes = false, // -pd: auto-yes to dep-cascade prompts (private only)
3111
+ publicDeps = false, // -public-deps: cascade public visibility to all deps
3111
3112
  noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs } = options;
3112
3113
  // Show tool version only for recursive dep calls (CLI already prints it at startup)
3113
3114
  const toolVersion = getToolVersion();
@@ -3985,8 +3986,11 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3985
3986
  if (!dryRun) {
3986
3987
  // Check if package has EVER been published to npm (any version)
3987
3988
  const hasBeenPublished = checkPackageExists(name);
3988
- // Recursively call globalize on the dependency
3989
- // Only pass npmVisibility to truly NEW packages (never published before)
3989
+ // Recursively call globalize on the dependency.
3990
+ // Normally only pass npmVisibility to truly NEW packages (never published
3991
+ // before) — already-published deps keep their own visibility setting.
3992
+ // -public-deps overrides this: propagate npmVisibility regardless so a
3993
+ // public consumer's transitive deps all get promoted to public.
3990
3994
  const depSuccess = await globalize(path, {
3991
3995
  bump: 'patch', // Use existing version, don't bump
3992
3996
  verbose,
@@ -3994,13 +3998,14 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3994
3998
  force,
3995
3999
  files,
3996
4000
  gitVisibility,
3997
- npmVisibility: hasBeenPublished ? undefined : npmVisibility, // Only for new packages
4001
+ npmVisibility: (hasBeenPublished && !publicDeps) ? undefined : npmVisibility,
3998
4002
  updateDeps,
3999
4003
  updateMajor,
4000
4004
  fix,
4001
4005
  conform, // Propagate conform to dependencies
4002
4006
  publishDeps, // Propagate so transitive deps also get published
4003
4007
  publishDepsYes, // Propagate auto-yes through cascade
4008
+ publicDeps, // Propagate so transitive deps also get promoted to public
4004
4009
  forcePublish, // Propagate so transitive deps get force-published too
4005
4010
  _fromDep: true, // Suppress nested prescan
4006
4011
  rebase, // Propagate so behind-remote deps get rebased automatically
@@ -5364,32 +5369,109 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
5364
5369
  }
5365
5370
  }
5366
5371
  const wsNameSet = new Set(packages.map(p => p.name));
5367
- for (const pkgInfo of packages) {
5368
- if (visibilityMap.get(pkgInfo.name) !== 'public')
5369
- continue;
5370
- const deps = { ...pkgInfo.pkg.dependencies, ...pkgInfo.pkg.devDependencies };
5371
- for (const depName of Object.keys(deps || {})) {
5372
- if (!wsNameSet.has(depName))
5373
- continue;
5374
- if (visibilityMap.get(depName) === 'public')
5372
+ // Fixpoint: each promotion can expose new transitive deps (a newly-public B may
5373
+ // itself depend on a private C). Iterate until no further promotions happen so
5374
+ // chains like public A → private B → private C are fully resolved regardless of
5375
+ // packages[] order.
5376
+ {
5377
+ let changed = true;
5378
+ let guard = 0;
5379
+ while (changed && guard++ < 50) {
5380
+ changed = false;
5381
+ for (const pkgInfo of packages) {
5382
+ if (visibilityMap.get(pkgInfo.name) !== 'public')
5383
+ continue;
5384
+ const deps = { ...pkgInfo.pkg.dependencies, ...pkgInfo.pkg.devDependencies };
5385
+ for (const depName of Object.keys(deps || {})) {
5386
+ if (!wsNameSet.has(depName))
5387
+ continue;
5388
+ if (visibilityMap.get(depName) === 'public')
5389
+ continue;
5390
+ const depInfo = packages.find(p => p.name === depName);
5391
+ if (!depInfo)
5392
+ continue;
5393
+ console.log(colors.yellow(`⚠ Public package ${pkgInfo.name} depends on ${depName} which has no public visibility set.`));
5394
+ let makePublic = false;
5395
+ if (options.publicDeps) {
5396
+ console.log(colors.green(` -public-deps: auto-promoting ${depName} to public`));
5397
+ makePublic = true;
5398
+ }
5399
+ else if (options.dryRun) {
5400
+ console.log(colors.dim(` [dry-run] Would ask to make ${depName} public`));
5401
+ }
5402
+ else {
5403
+ makePublic = await confirm(`Make ${depName} public too?`, true);
5404
+ }
5405
+ if (makePublic) {
5406
+ if (!options.dryRun) {
5407
+ const depConfig = readConfig(depInfo.dir);
5408
+ depConfig.npmVisibility = 'public';
5409
+ writeConfig(depInfo.dir, depConfig, new Set(['npmVisibility']));
5410
+ console.log(colors.green(`✓ Set ${depName} to public in .globalize.json5`));
5411
+ }
5412
+ else {
5413
+ console.log(colors.dim(` [dry-run] Would set ${depName} npmVisibility:"public"`));
5414
+ }
5415
+ visibilityMap.set(depName, 'public');
5416
+ changed = true;
5417
+ }
5418
+ }
5419
+ }
5420
+ }
5421
+ }
5422
+ // Fail-loud: refuse to publish a public workspace member whose file: deps point
5423
+ // at non-publishable workspace siblings (private:true, noPublish:true, or still
5424
+ // not marked public after the prompt loop). A registry tarball declaring such
5425
+ // deps would 404 on a clean install. See docs/npmglobalize-transitive-workspace-deps.md.
5426
+ {
5427
+ const allMembers = resolveAllWorkspacePackages(rootDir);
5428
+ const memberByName = new Map();
5429
+ for (const m of allMembers)
5430
+ memberByName.set(m.name, m);
5431
+ const blockers = [];
5432
+ const filteredSet = new Set(filteredOrder);
5433
+ for (const pkgInfo of packages) {
5434
+ if (visibilityMap.get(pkgInfo.name) !== 'public')
5375
5435
  continue;
5376
- const depInfo = packages.find(p => p.name === depName);
5377
- if (!depInfo)
5436
+ // Only check members that will actually be processed this run.
5437
+ if (!filteredSet.has(pkgInfo.name))
5378
5438
  continue;
5379
- console.log(colors.yellow(`⚠ Public package ${pkgInfo.name} depends on ${depName} which has no public visibility set.`));
5380
- if (!options.dryRun) {
5381
- const makePublic = await confirm(`Make ${depName} public too?`, true);
5382
- if (makePublic) {
5383
- const depConfig = readConfig(depInfo.dir);
5384
- depConfig.npmVisibility = 'public';
5385
- writeConfig(depInfo.dir, depConfig, new Set(['npmVisibility']));
5386
- visibilityMap.set(depName, 'public');
5387
- console.log(colors.green(`✓ Set ${depName} to public in .globalize.json5`));
5439
+ const deps = { ...pkgInfo.pkg.dependencies, ...pkgInfo.pkg.devDependencies };
5440
+ for (const [depName, depVal] of Object.entries(deps)) {
5441
+ if (typeof depVal !== 'string')
5442
+ continue;
5443
+ const member = memberByName.get(depName);
5444
+ if (!member)
5445
+ continue; // External dep, not workspace-internal — out of scope.
5446
+ if (member.pkg.private) {
5447
+ blockers.push({ pkg: pkgInfo.name, dep: depName, reason: `${depName} has "private": true in package.json (cannot publish)` });
5448
+ continue;
5449
+ }
5450
+ const depConfig = readConfig(member.dir);
5451
+ if (depConfig.noPublish) {
5452
+ blockers.push({ pkg: pkgInfo.name, dep: depName, reason: `${depName} has noPublish:true in .globalize.json5` });
5453
+ continue;
5454
+ }
5455
+ if (visibilityMap.get(depName) !== 'public') {
5456
+ blockers.push({ pkg: pkgInfo.name, dep: depName, reason: `${depName} is not marked public (set npmVisibility:"public" in its .globalize.json5, or rerun with -public-deps)` });
5457
+ continue;
5388
5458
  }
5389
5459
  }
5390
- else {
5391
- console.log(colors.dim(` [dry-run] Would ask to make ${depName} public`));
5460
+ }
5461
+ if (blockers.length > 0) {
5462
+ console.log('');
5463
+ console.log(colors.red('✗ Refusing to publish: public workspace members depend on non-publishable siblings.'));
5464
+ console.log(colors.red(' The published tarball would 404 on a clean install.'));
5465
+ console.log('');
5466
+ for (const b of blockers) {
5467
+ console.log(colors.red(` ${b.pkg} → ${b.dep}: ${b.reason}`));
5392
5468
  }
5469
+ console.log('');
5470
+ console.log(colors.dim('Fixes:'));
5471
+ console.log(colors.dim(' - Set npmVisibility:"public" in each offending dep\'s .globalize.json5'));
5472
+ console.log(colors.dim(' - Or rerun with -public-deps to auto-promote all transitive workspace deps'));
5473
+ console.log(colors.dim(' - Or remove npmVisibility:"public" from the consumer (keep it bundled under the workspace root)'));
5474
+ return { success: false, packages: [], publishOrder };
5393
5475
  }
5394
5476
  }
5395
5477
  // Sync workspace-root node_modules with member package.json files. Catches
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.169",
3
+ "version": "1.0.170",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",