@bobfrankston/npmglobalize 1.0.169 → 1.0.171

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 */
@@ -165,6 +171,24 @@ export declare function checkVersionExists(packageName: string, version: string)
165
171
  export declare function checkPackageExists(packageName: string): boolean;
166
172
  /** Check npm package access level (public/restricted/null if not published) */
167
173
  export declare function checkNpmAccess(packageName: string): 'public' | 'restricted' | null;
174
+ /** Walk `file:` deps transitively from a starting directory and ensure each is set
175
+ * up to be installable from a public consumer: persists `npmVisibility:"public"` in
176
+ * each dep's `.globalize.json5`, and flips npm access from `restricted` to `public`
177
+ * for any dep that's already on npm. Returns blockers for deps that can't be made
178
+ * public (private:true or noPublish:true). Used by single-package `-public-deps`
179
+ * to handle deps that are unchanged locally — those would otherwise be skipped by
180
+ * the cascade and stay private on npm. */
181
+ export declare function cascadePublicVisibility(cwd: string, opts?: {
182
+ dryRun?: boolean;
183
+ verbose?: boolean;
184
+ }): Promise<{
185
+ promoted: string[];
186
+ configWritten: string[];
187
+ blockers: Array<{
188
+ name: string;
189
+ reason: string;
190
+ }>;
191
+ }>;
168
192
  /** Check if public package has private/inaccessible dependencies */
169
193
  export declare function checkPrivateDependencies(pkg: any, verbose?: boolean): {
170
194
  name: string;
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;
@@ -565,6 +565,113 @@ export function checkNpmAccess(packageName) {
565
565
  return null;
566
566
  }
567
567
  }
568
+ /** Walk `file:` deps transitively from a starting directory and ensure each is set
569
+ * up to be installable from a public consumer: persists `npmVisibility:"public"` in
570
+ * each dep's `.globalize.json5`, and flips npm access from `restricted` to `public`
571
+ * for any dep that's already on npm. Returns blockers for deps that can't be made
572
+ * public (private:true or noPublish:true). Used by single-package `-public-deps`
573
+ * to handle deps that are unchanged locally — those would otherwise be skipped by
574
+ * the cascade and stay private on npm. */
575
+ export async function cascadePublicVisibility(cwd, opts = {}) {
576
+ const { dryRun = false, verbose = false } = opts;
577
+ const promoted = [];
578
+ const configWritten = [];
579
+ const blockers = [];
580
+ const visited = new Set();
581
+ const processed = new Set(); // dedupe by package name across diamond deps
582
+ async function walk(dir) {
583
+ let real;
584
+ try {
585
+ real = fs.realpathSync(dir);
586
+ }
587
+ catch {
588
+ return;
589
+ }
590
+ if (visited.has(real))
591
+ return;
592
+ visited.add(real);
593
+ let pkg;
594
+ try {
595
+ pkg = readPackageJson(dir);
596
+ }
597
+ catch {
598
+ return;
599
+ }
600
+ for (const key of DEP_KEYS) {
601
+ // Use the .dependencies backup if present (has original file: refs).
602
+ const deps = pkg['.' + key] || pkg[key];
603
+ if (!deps)
604
+ continue;
605
+ for (const [name, value] of Object.entries(deps)) {
606
+ if (typeof value !== 'string' || !isFileRef(value))
607
+ continue;
608
+ let targetPath;
609
+ try {
610
+ targetPath = resolveFilePath(value, dir);
611
+ }
612
+ catch {
613
+ continue;
614
+ }
615
+ if (!fs.existsSync(path.join(targetPath, 'package.json')))
616
+ continue;
617
+ let targetPkg;
618
+ try {
619
+ targetPkg = readPackageJson(targetPath);
620
+ }
621
+ catch {
622
+ continue;
623
+ }
624
+ const targetName = targetPkg.name || name;
625
+ if (!processed.has(targetName)) {
626
+ processed.add(targetName);
627
+ if (targetPkg.private) {
628
+ blockers.push({ name: targetName, reason: '"private": true in package.json (cannot publish to npm)' });
629
+ }
630
+ else {
631
+ const targetConfig = readConfig(targetPath);
632
+ if (targetConfig.noPublish) {
633
+ blockers.push({ name: targetName, reason: 'noPublish:true in .globalize.json5' });
634
+ }
635
+ else {
636
+ if (targetConfig.npmVisibility !== 'public') {
637
+ if (!dryRun) {
638
+ targetConfig.npmVisibility = 'public';
639
+ writeConfig(targetPath, targetConfig, new Set(['npmVisibility']));
640
+ }
641
+ configWritten.push(targetName);
642
+ if (verbose)
643
+ console.log(colors.dim(` ${targetName}: npmVisibility:"public" written to .globalize.json5`));
644
+ }
645
+ const access = checkNpmAccess(targetName);
646
+ if (access === 'restricted') {
647
+ if (!dryRun) {
648
+ const r = runCommand('npm', ['access', 'set', 'status=public', targetName], { cwd: targetPath, silent: true });
649
+ if (r.success) {
650
+ promoted.push(targetName);
651
+ console.log(colors.green(` ✓ Flipped ${targetName} to PUBLIC on npm`));
652
+ }
653
+ else {
654
+ blockers.push({ name: targetName, reason: `npm access set failed: ${r.stderr || r.output || 'unknown'}` });
655
+ }
656
+ }
657
+ else {
658
+ console.log(colors.dim(` [dry-run] Would flip ${targetName} to PUBLIC on npm`));
659
+ }
660
+ }
661
+ else if (access === 'public' && verbose) {
662
+ console.log(colors.dim(` ${targetName}: already PUBLIC on npm`));
663
+ }
664
+ // access === null: not on npm yet — the cascade publish step handles that.
665
+ }
666
+ }
667
+ }
668
+ await walk(targetPath);
669
+ }
670
+ }
671
+ }
672
+ await walk(cwd);
673
+ return { promoted, configWritten, blockers };
674
+ }
568
675
  /** Check if public package has private/inaccessible dependencies */
569
676
  export function checkPrivateDependencies(pkg, verbose = false) {
570
677
  const privateDeps = [];
@@ -3108,6 +3215,7 @@ async function doLocalInstall(cwd, options) {
3108
3215
  export async function globalize(cwd, options = {}, configOptions = {}) {
3109
3216
  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
3217
  publishDepsYes = false, // -pd: auto-yes to dep-cascade prompts (private only)
3218
+ publicDeps = false, // -public-deps: cascade public visibility to all deps
3111
3219
  noPrescan = false, forcePublish = false, fix = true, fixTags = false, rebase = false, show = false, local = false, freeze = false, usePaths = true, allowTs } = options;
3112
3220
  // Show tool version only for recursive dep calls (CLI already prints it at startup)
3113
3221
  const toolVersion = getToolVersion();
@@ -3843,6 +3951,32 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3843
3951
  console.log(colors.dim(`Would add publishConfig.access: "public" to package.json`));
3844
3952
  }
3845
3953
  }
3954
+ // -public-deps pre-pass: at the top of the user-invoked call, walk file: deps
3955
+ // transitively and flip any that are already on npm but currently `restricted`
3956
+ // to `public`, persisting `npmVisibility:"public"` in each dep's .globalize.json5.
3957
+ // This handles the case where a public consumer's deps are unchanged locally
3958
+ // (so the cascade publish step would skip them) but published as private —
3959
+ // public installers would 404. Only runs once at the top level (suppressed in
3960
+ // recursive cascade and workspace-orchestrated calls, which already handle this).
3961
+ if (publicDeps && !options._fromDep && !options._fromWorkspace) {
3962
+ console.log(colors.italic('-public-deps: walking file: deps to ensure all are PUBLIC on npm...'));
3963
+ const result = await cascadePublicVisibility(cwd, { dryRun, verbose });
3964
+ if (result.promoted.length === 0 && result.configWritten.length === 0 && verbose) {
3965
+ console.log(colors.dim(' (no changes needed)'));
3966
+ }
3967
+ if (result.blockers.length > 0) {
3968
+ console.log('');
3969
+ console.log(colors.red('✗ -public-deps: cannot make every transitive dep public:'));
3970
+ for (const b of result.blockers) {
3971
+ console.log(colors.red(` ${b.name}: ${b.reason}`));
3972
+ }
3973
+ if (!force) {
3974
+ return false;
3975
+ }
3976
+ console.log(colors.yellow('Continuing with --force despite blockers...'));
3977
+ }
3978
+ console.log('');
3979
+ }
3846
3980
  // Check for private dependencies in public packages
3847
3981
  const willBePublic = effectiveNpmVisibility === 'public' ||
3848
3982
  currentAccess === 'public' ||
@@ -3985,8 +4119,11 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3985
4119
  if (!dryRun) {
3986
4120
  // Check if package has EVER been published to npm (any version)
3987
4121
  const hasBeenPublished = checkPackageExists(name);
3988
- // Recursively call globalize on the dependency
3989
- // Only pass npmVisibility to truly NEW packages (never published before)
4122
+ // Recursively call globalize on the dependency.
4123
+ // Normally only pass npmVisibility to truly NEW packages (never published
4124
+ // before) — already-published deps keep their own visibility setting.
4125
+ // -public-deps overrides this: propagate npmVisibility regardless so a
4126
+ // public consumer's transitive deps all get promoted to public.
3990
4127
  const depSuccess = await globalize(path, {
3991
4128
  bump: 'patch', // Use existing version, don't bump
3992
4129
  verbose,
@@ -3994,13 +4131,14 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
3994
4131
  force,
3995
4132
  files,
3996
4133
  gitVisibility,
3997
- npmVisibility: hasBeenPublished ? undefined : npmVisibility, // Only for new packages
4134
+ npmVisibility: (hasBeenPublished && !publicDeps) ? undefined : npmVisibility,
3998
4135
  updateDeps,
3999
4136
  updateMajor,
4000
4137
  fix,
4001
4138
  conform, // Propagate conform to dependencies
4002
4139
  publishDeps, // Propagate so transitive deps also get published
4003
4140
  publishDepsYes, // Propagate auto-yes through cascade
4141
+ publicDeps, // Propagate so transitive deps also get promoted to public
4004
4142
  forcePublish, // Propagate so transitive deps get force-published too
4005
4143
  _fromDep: true, // Suppress nested prescan
4006
4144
  rebase, // Propagate so behind-remote deps get rebased automatically
@@ -5364,32 +5502,109 @@ export async function globalizeWorkspace(rootDir, options = {}, configOptions =
5364
5502
  }
5365
5503
  }
5366
5504
  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')
5505
+ // Fixpoint: each promotion can expose new transitive deps (a newly-public B may
5506
+ // itself depend on a private C). Iterate until no further promotions happen so
5507
+ // chains like public A → private B → private C are fully resolved regardless of
5508
+ // packages[] order.
5509
+ {
5510
+ let changed = true;
5511
+ let guard = 0;
5512
+ while (changed && guard++ < 50) {
5513
+ changed = false;
5514
+ for (const pkgInfo of packages) {
5515
+ if (visibilityMap.get(pkgInfo.name) !== 'public')
5516
+ continue;
5517
+ const deps = { ...pkgInfo.pkg.dependencies, ...pkgInfo.pkg.devDependencies };
5518
+ for (const depName of Object.keys(deps || {})) {
5519
+ if (!wsNameSet.has(depName))
5520
+ continue;
5521
+ if (visibilityMap.get(depName) === 'public')
5522
+ continue;
5523
+ const depInfo = packages.find(p => p.name === depName);
5524
+ if (!depInfo)
5525
+ continue;
5526
+ console.log(colors.yellow(`⚠ Public package ${pkgInfo.name} depends on ${depName} which has no public visibility set.`));
5527
+ let makePublic = false;
5528
+ if (options.publicDeps) {
5529
+ console.log(colors.green(` -public-deps: auto-promoting ${depName} to public`));
5530
+ makePublic = true;
5531
+ }
5532
+ else if (options.dryRun) {
5533
+ console.log(colors.dim(` [dry-run] Would ask to make ${depName} public`));
5534
+ }
5535
+ else {
5536
+ makePublic = await confirm(`Make ${depName} public too?`, true);
5537
+ }
5538
+ if (makePublic) {
5539
+ if (!options.dryRun) {
5540
+ const depConfig = readConfig(depInfo.dir);
5541
+ depConfig.npmVisibility = 'public';
5542
+ writeConfig(depInfo.dir, depConfig, new Set(['npmVisibility']));
5543
+ console.log(colors.green(`✓ Set ${depName} to public in .globalize.json5`));
5544
+ }
5545
+ else {
5546
+ console.log(colors.dim(` [dry-run] Would set ${depName} npmVisibility:"public"`));
5547
+ }
5548
+ visibilityMap.set(depName, 'public');
5549
+ changed = true;
5550
+ }
5551
+ }
5552
+ }
5553
+ }
5554
+ }
5555
+ // Fail-loud: refuse to publish a public workspace member whose file: deps point
5556
+ // at non-publishable workspace siblings (private:true, noPublish:true, or still
5557
+ // not marked public after the prompt loop). A registry tarball declaring such
5558
+ // deps would 404 on a clean install. See docs/npmglobalize-transitive-workspace-deps.md.
5559
+ {
5560
+ const allMembers = resolveAllWorkspacePackages(rootDir);
5561
+ const memberByName = new Map();
5562
+ for (const m of allMembers)
5563
+ memberByName.set(m.name, m);
5564
+ const blockers = [];
5565
+ const filteredSet = new Set(filteredOrder);
5566
+ for (const pkgInfo of packages) {
5567
+ if (visibilityMap.get(pkgInfo.name) !== 'public')
5375
5568
  continue;
5376
- const depInfo = packages.find(p => p.name === depName);
5377
- if (!depInfo)
5569
+ // Only check members that will actually be processed this run.
5570
+ if (!filteredSet.has(pkgInfo.name))
5378
5571
  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`));
5572
+ const deps = { ...pkgInfo.pkg.dependencies, ...pkgInfo.pkg.devDependencies };
5573
+ for (const [depName, depVal] of Object.entries(deps)) {
5574
+ if (typeof depVal !== 'string')
5575
+ continue;
5576
+ const member = memberByName.get(depName);
5577
+ if (!member)
5578
+ continue; // External dep, not workspace-internal — out of scope.
5579
+ if (member.pkg.private) {
5580
+ blockers.push({ pkg: pkgInfo.name, dep: depName, reason: `${depName} has "private": true in package.json (cannot publish)` });
5581
+ continue;
5582
+ }
5583
+ const depConfig = readConfig(member.dir);
5584
+ if (depConfig.noPublish) {
5585
+ blockers.push({ pkg: pkgInfo.name, dep: depName, reason: `${depName} has noPublish:true in .globalize.json5` });
5586
+ continue;
5587
+ }
5588
+ if (visibilityMap.get(depName) !== 'public') {
5589
+ 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)` });
5590
+ continue;
5388
5591
  }
5389
5592
  }
5390
- else {
5391
- console.log(colors.dim(` [dry-run] Would ask to make ${depName} public`));
5593
+ }
5594
+ if (blockers.length > 0) {
5595
+ console.log('');
5596
+ console.log(colors.red('✗ Refusing to publish: public workspace members depend on non-publishable siblings.'));
5597
+ console.log(colors.red(' The published tarball would 404 on a clean install.'));
5598
+ console.log('');
5599
+ for (const b of blockers) {
5600
+ console.log(colors.red(` ${b.pkg} → ${b.dep}: ${b.reason}`));
5392
5601
  }
5602
+ console.log('');
5603
+ console.log(colors.dim('Fixes:'));
5604
+ console.log(colors.dim(' - Set npmVisibility:"public" in each offending dep\'s .globalize.json5'));
5605
+ console.log(colors.dim(' - Or rerun with -public-deps to auto-promote all transitive workspace deps'));
5606
+ console.log(colors.dim(' - Or remove npmVisibility:"public" from the consumer (keep it bundled under the workspace root)'));
5607
+ return { success: false, packages: [], publishOrder };
5393
5608
  }
5394
5609
  }
5395
5610
  // 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.171",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",