@bobfrankston/npmglobalize 1.0.239 → 1.0.241

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.
Files changed (4) hide show
  1. package/README.md +3 -1
  2. package/lib.d.ts +18 -2
  3. package/lib.js +110 -2
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -94,7 +94,9 @@ Errors abort (unless `--force`); warnings prompt to continue.
94
94
 
95
95
  An unscoped dependency with private intent is reported differently: it is not a question. npmglobalize never renames a dependency to add a scope (the name is what every consumer imports by, and changing it is the dep owner's own project), so the dep cannot reach npm and neither can the package that depends on it. The first run says so in full, names the dep and its path, records it under `unscopedDepsNoted` in the consumer's `.globalize.json5`, and stops. Later runs print one line per dep and stop. Nothing is asked, because no answer could change the outcome. Fix it in the dep: scope the name in its own `package.json`, or set `npmVisibility: "public"` or `noprefix: true` in its `.globalize.json5`; the note clears itself on the next run. If the cascade reaches such a dep anyway (`-no-prescan`), it stops there with the same message. The same holds when you run npmglobalize *in* an unscoped package: it used to ask "Add scope to package name?", and now it warns and assumes no, since a rename changes what every consumer imports. Publishing an unscoped package PUBLIC on purpose takes an explicit `-npm public` or `noprefix: true`. (2026-09-14)
96
96
 
97
- **Own-scope packages installed from npm instead of linked.** When a package in the `file:` graph depends on one of its own-scope packages by version range (`"@bobfrankston/miscassists": "^1.0.61"`) rather than `file:`, npm puts a registry copy in that package's `node_modules` instead of a link to the local source. The consumer is usually nested under other `file:` deps, where neither the `+` list nor the dependency tree shows it, so the prescan finds every such edge. Since 2026-09-23 it **fixes** them: the range is rewritten to a `file:` path in the consumer's `package.json` and `npm install` runs in that consumer, so `node_modules` holds the link. The local source is the directory the same package is linked from elsewhere in the graph, or else the sibling directory named after the package (the `file:../<name>` convention), accepted only when its `package.json` carries that exact name. A range whose local source cannot be found either way is reported and left alone. Nothing is asked. A package with `"files": false` in its `.globalize.json5` keeps npm refs on purpose and is not touched. Each outcome (rewritten, or not found) is repeated as one line in the end-of-run **Issues Summary**. A failed `npm install` stops the run unless `-force`. `-dry-run` prints what would be rewritten. (2026-09-21, fixer 2026-09-23)
97
+ **Own-scope packages installed from npm instead of linked.** When a package in the `file:` graph depends on one of its own-scope packages by version range (`"@bobfrankston/miscassists": "^1.0.61"`) rather than `file:`, npm puts a registry copy in that package's `node_modules` instead of a link to the local source. The consumer is usually nested under other `file:` deps, where neither the `+` list (`-verbose`) nor the dependency tree shows it, so the prescan finds every such edge. Since 2026-09-23 it **fixes** them: the range is rewritten to a `file:` path in the consumer's `package.json` and `npm install` runs in that consumer, so `node_modules` holds the link. The local source is the directory the same package is linked from elsewhere in the graph, or else the sibling directory named after the package (the `file:../<name>` convention), accepted only when its `package.json` carries that exact name. A range whose local source cannot be found either way is reported and left alone. Nothing is asked. A package with `"files": false` in its `.globalize.json5` keeps npm refs on purpose and is not touched. Each outcome (rewritten, or not found) is repeated as one line in the end-of-run **Issues Summary**. A failed `npm install` stops the run unless `-force`. `-dry-run` prints what would be rewritten. (2026-09-21, fixer 2026-09-23)
98
+
99
+ **A `file:` dependency declared under the wrong key.** `"ubntdefs": "file:../ubntdefs"` links fine locally even when that package's own `package.json` says `@bobfrankston/ubntdefs`, because npm links a `file:` dep under whatever key you wrote. But the published `package.json` carries the version range under that key, so the registry copy of the consumer says `"ubntdefs": "^1.0.5"`, a name that is not on npm, and every registry install of it or of anything depending on it dies with E404 (dbx10 → setcolor, 2026-09-24). The prescan compares every `file:` key with the target's own name and **fixes** a mismatch: the key is renamed to the real name in the consumer's `package.json` (the `file:` path is kept), `npm uninstall <old key>` drops the link under the old name, and `npm install` links it under the real one. Nothing is asked; `-dry-run` prints what would change; each rename and any failed uninstall is repeated in the **Issues Summary**; a failed install stops the run unless `-force`. Imports in the consumer's code are not touched, so if anything imported by the old key, fix that by hand. A run with `-no-prescan` that reaches such a key stops in the transform with the same explanation rather than publishing a broken `package.json`. (2026-09-24)
98
100
 
99
101
  Skip the prescan with `-no-prescan` / `-nps`.
100
102
 
package/lib.d.ts CHANGED
@@ -425,9 +425,13 @@ export interface PrescanIssue {
425
425
  /** 'unscoped': an unscoped file: dep with private intent. Not an error and not a
426
426
  * question — globalize reports it once, remembers it, and stops.
427
427
  * 'registryRef': a package in the file: graph depends on one of its own-scope packages
428
- * by version range instead of file: — rewritten to file: by fixRegistryRefs (2026-09-23), nothing asked. */
429
- kind?: 'unscoped' | 'registryRef';
428
+ * by version range instead of file: — rewritten to file: by fixRegistryRefs (2026-09-23), nothing asked.
429
+ * 'nameMismatch': a file: dep declared under a key that is not the target's package.json name —
430
+ * key renamed, old name uninstalled, reinstalled by fixDepKeyMismatches (2026-09-24), nothing asked. */
431
+ kind?: 'unscoped' | 'registryRef' | 'nameMismatch';
430
432
  package: string;
433
+ /** 'nameMismatch' only: the key as written in the consumer's package.json (`ubntdefs`). */
434
+ key?: string;
431
435
  /** For 'registryRef': the directory of the CONSUMER whose package.json holds the range. */
432
436
  path: string;
433
437
  /** 'registryRef' only: name of the package whose package.json declares the dep. */
@@ -465,6 +469,18 @@ export declare function fixRegistryRefs(registryRefs: PrescanIssue[], fileTarget
465
469
  dryRun?: boolean;
466
470
  verbose?: boolean;
467
471
  }): Promise<boolean>;
472
+ /** Rename file: dependency keys that differ from the target package's own name, then
473
+ * `npm uninstall` the old name and `npm install` so node_modules holds the link under the
474
+ * real name. The published package.json carries the version range under the KEY, so a
475
+ * wrong key publishes a dependency nobody can install (dbx10 → `"ubntdefs": "^1.0.5"`,
476
+ * setcolor's install died with E404 on 2026-09-24). Nothing is asked; -dry-run prints what
477
+ * would change; each outcome lands in the Issues Summary.
478
+ * 2026-09-24 16:20 EDT — Claude Code (Fable 5.1), at Bob's direction: "A check would be to
479
+ * look at the file; to uninstall from the old name and reinstall with the new. This is safe." */
480
+ export declare function fixDepKeyMismatches(mismatches: PrescanIssue[], opts?: {
481
+ dryRun?: boolean;
482
+ verbose?: boolean;
483
+ }): Promise<boolean>;
468
484
  /** Report unscoped `file:` deps found by the prescan, without asking anything.
469
485
  *
470
486
  * An unscoped dep with private intent cannot reach npm, and npmglobalize does not
package/lib.js CHANGED
@@ -1735,6 +1735,7 @@ export function verifyDepPathCase(cwd, pkg) {
1735
1735
  export function transformDeps(pkg, baseDir, verbose = false, forcePublish = false) {
1736
1736
  let transformed = false;
1737
1737
  const unpublished = [];
1738
+ let fileRefCount = 0; // 2026-09-24: for the one-line summary below
1738
1739
  // If re-running on a previously-transformed pkg, merge-restore first.
1739
1740
  // This preserves any external modifications (AI tools, npm install, manual edits)
1740
1741
  // made since the last transform, then proceeds to re-transform from a clean slate.
@@ -1754,9 +1755,23 @@ export function transformDeps(pkg, baseDir, verbose = false, forcePublish = fals
1754
1755
  for (const [name, value] of Object.entries(pkg[key])) {
1755
1756
  if (isFileRef(value)) {
1756
1757
  const targetPath = resolveFilePath(value, baseDir);
1757
- console.log(colors.blue(` + ${name} → ${value}`));
1758
+ fileRefCount++;
1759
+ // 2026-09-24 16:35 EDT — Claude Code (Fable 5.1), at Bob's direction: the blue
1760
+ // "+ name → file:…" line per dep is -verbose only now. Bob: "when the packages
1761
+ // are fine no need to list them — screen output is clutter so it's hard to see
1762
+ // all that is happening when things are working." Deps that need work still get
1763
+ // their ⟳ line; the rest are one summary line after the loop.
1764
+ if (verbose)
1765
+ console.log(colors.blue(` + ${name} → ${value}`));
1758
1766
  try {
1759
1767
  const targetPkg = readPackageJson(targetPath);
1768
+ // 2026-09-24 — Claude Code (Fable 5.1), at Bob's direction: the range below goes
1769
+ // under the KEY, so a key that is not the target's name would publish a dep nobody
1770
+ // can install (dbx10's "ubntdefs": "^1.0.5"). The prescan renames such keys; this
1771
+ // is the stop for a run that skipped it (-no-prescan).
1772
+ if (targetPkg.name && targetPkg.name !== name) {
1773
+ throw new Error(`dependency key "${name}" names a package whose package.json says ${targetPkg.name}; published as "${name}" it could not be installed. Rerun without -no-prescan to have the key renamed, or rename it by hand`);
1774
+ }
1760
1775
  const targetVersion = targetPkg.version;
1761
1776
  const npmVersion = '^' + targetVersion;
1762
1777
  // Check if this version exists on npm (or if force publish)
@@ -1800,6 +1815,10 @@ export function transformDeps(pkg, baseDir, verbose = false, forcePublish = fals
1800
1815
  }
1801
1816
  }
1802
1817
  }
1818
+ if (fileRefCount && !verbose) {
1819
+ const fine = fileRefCount - unpublished.length;
1820
+ console.log(colors.dim(` ${fileRefCount} file: dependenc${fileRefCount === 1 ? "y" : "ies"}${unpublished.length ? `: ${unpublished.length} to publish (⟳ above), ${fine} on npm` : ", all on npm"}`));
1821
+ }
1803
1822
  // Record snapshot of transform output so restore can distinguish untouched
1804
1823
  // entries from those modified externally since the transform ran.
1805
1824
  if (transformed) {
@@ -2075,6 +2094,21 @@ export function prescanDepGraph(baseDir, visited = new Set(), verbose = false, f
2075
2094
  }
2076
2095
  const depName = targetPkg.name || name;
2077
2096
  fileTargets.set(depName, targetPath);
2097
+ // 2026-09-24 16:20 EDT — Claude Code (Fable 5.1), at Bob's direction. A file: dep
2098
+ // declared under a key that is not the target's package.json name ("ubntdefs":
2099
+ // file:… whose package.json says "@bobfrankston/ubntdefs") links fine locally, but
2100
+ // transformDeps writes the version range under the KEY, so the published dbx10 said
2101
+ // `"ubntdefs": "^1.0.5"` — a name that is not on npm — and every registry install of
2102
+ // a consumer (setcolor) died with E404. Flagged here; fixDepKeyMismatches renames the
2103
+ // key, uninstalls the old name and reinstalls. Bob: "uninstall from the old name and
2104
+ // reinstall with the new. This is safe."
2105
+ if (targetPkg.name && targetPkg.name !== name) {
2106
+ issues.push({
2107
+ severity: 'warning', kind: 'nameMismatch',
2108
+ package: targetPkg.name, key: name, path: baseDir, consumer: consumerName, range: value,
2109
+ message: `${consumerName} declares ${targetPkg.name} under the key "${name}" (${value})`
2110
+ });
2111
+ }
2078
2112
  // Unscoped + private-intent. This used to be a WARNING whose "continue" led
2079
2113
  // into the cascade prompting to rename the dep by adding a scope. The cascade
2080
2114
  // never renames a dependency now (see the _fromDep check in globalize), so it
@@ -2197,6 +2231,72 @@ export async function fixRegistryRefs(registryRefs, fileTargets, opts = {}) {
2197
2231
  console.log('');
2198
2232
  return ok;
2199
2233
  }
2234
+ /** Rename file: dependency keys that differ from the target package's own name, then
2235
+ * `npm uninstall` the old name and `npm install` so node_modules holds the link under the
2236
+ * real name. The published package.json carries the version range under the KEY, so a
2237
+ * wrong key publishes a dependency nobody can install (dbx10 → `"ubntdefs": "^1.0.5"`,
2238
+ * setcolor's install died with E404 on 2026-09-24). Nothing is asked; -dry-run prints what
2239
+ * would change; each outcome lands in the Issues Summary.
2240
+ * 2026-09-24 16:20 EDT — Claude Code (Fable 5.1), at Bob's direction: "A check would be to
2241
+ * look at the file; to uninstall from the old name and reinstall with the new. This is safe." */
2242
+ export async function fixDepKeyMismatches(mismatches, opts = {}) {
2243
+ if (!mismatches.length)
2244
+ return true;
2245
+ const { dryRun = false, verbose = false } = opts;
2246
+ console.log(colors.yellow(`${mismatches.length} file: dependenc${mismatches.length === 1 ? 'y is' : 'ies are'} declared under a key that is not the package's own name (published that way, npm would look for a name that does not exist):`));
2247
+ // One uninstall + install per consumer, after all of its keys are renamed.
2248
+ const touched = new Map();
2249
+ for (const m of mismatches) {
2250
+ const pkgJsonPath = path.join(m.path, 'package.json');
2251
+ console.log(` ${colors.yellow('≠')} ${m.consumer}: "${m.key}" → ${m.package} (${m.range})`);
2252
+ console.log(colors.dim(` in ${pkgJsonPath}`));
2253
+ if (dryRun) {
2254
+ console.log(colors.dim(` [dry-run] would rename the key to ${m.package}, then npm uninstall ${m.key} and npm install in ${m.consumer}`));
2255
+ continue;
2256
+ }
2257
+ const pkg = readPackageJson(m.path);
2258
+ let rewrote = false;
2259
+ for (const key of DEP_KEYS) {
2260
+ // A `.dependencies` backup (left by an interrupted transform) is the file: truth;
2261
+ // prescanDepGraph read the key from it, so the rename goes there too.
2262
+ for (const depsKey of ['.' + key, key]) {
2263
+ const deps = pkg[depsKey];
2264
+ if (!deps || deps[m.key] !== m.range)
2265
+ continue;
2266
+ delete deps[m.key];
2267
+ deps[m.package] = m.range;
2268
+ rewrote = true;
2269
+ }
2270
+ }
2271
+ if (!rewrote) {
2272
+ console.log(colors.dim(` key not found in ${pkgJsonPath} any more — left as is`));
2273
+ continue;
2274
+ }
2275
+ writePackageJson(m.path, pkg);
2276
+ console.log(colors.green(` ✓ renamed to ${m.package}`));
2277
+ recordBuildIssue(m.consumer, 'warning', `dependency key "${m.key}" renamed to ${m.package} (the file: target's own name) in ${pkgJsonPath}`);
2278
+ const entry = touched.get(m.path) ?? { consumer: m.consumer, oldKeys: [], newNames: [] };
2279
+ entry.oldKeys.push(m.key);
2280
+ entry.newNames.push(m.package);
2281
+ touched.set(m.path, entry);
2282
+ }
2283
+ let ok = true;
2284
+ for (const [dir, { consumer, oldKeys, newNames }] of touched) {
2285
+ console.log(colors.yellow(`↻ npm uninstall ${oldKeys.join(' ')} in ${consumer} (${dir}) — drops the link under the old name`));
2286
+ const uninstall = await runCommandAsync('npm', ['uninstall', ...oldKeys], { cwd: dir, silent: !verbose });
2287
+ if (!uninstall.success) {
2288
+ // Not fatal: the install below links the real name; a leftover link under the old
2289
+ // name is inert (nothing imports it) and is named so it can be removed by hand.
2290
+ console.log(colors.yellow(` npm uninstall failed in ${dir}; node_modules/${oldKeys.join(', node_modules/')} may remain — continuing with npm install`));
2291
+ recordBuildIssue(consumer, 'warning', `npm uninstall ${oldKeys.join(' ')} failed in ${dir}; the old link may remain in node_modules`);
2292
+ }
2293
+ console.log(colors.yellow(`↻ npm install in ${consumer} (${dir}) to link ${newNames.join(', ')} under the real name`));
2294
+ if (!await npmInstallWithCleanRetry(dir, verbose, `fixDepKeyMismatches: ${dir}`))
2295
+ ok = false;
2296
+ }
2297
+ console.log('');
2298
+ return ok;
2299
+ }
2200
2300
  /** Report unscoped `file:` deps found by the prescan, without asking anything.
2201
2301
  *
2202
2302
  * An unscoped dep with private intent cannot reach npm, and npmglobalize does not
@@ -7617,7 +7717,15 @@ async function globalizeUnderLock(cwd, options = {}, configOptions = {}) {
7617
7717
  console.error(colors.red('npm install failed while linking own-scope deps as file: (see above). Stopping; use -force to continue anyway.'));
7618
7718
  return false;
7619
7719
  }
7620
- const allIssues = scanned.filter(i => i.kind !== 'registryRef');
7720
+ // 2026-09-24 — Claude Code (Fable 5.1), at Bob's direction: file: deps declared under a
7721
+ // key that is not the target's name are renamed + reinstalled the same way (see
7722
+ // fixDepKeyMismatches); a failed install stops the run unless -force.
7723
+ const mismatchOk = await fixDepKeyMismatches(scanned.filter(i => i.kind === 'nameMismatch'), { dryRun, verbose });
7724
+ if (!mismatchOk && !force) {
7725
+ console.error(colors.red('npm install failed after renaming a dependency key to the package\'s own name (see above). Stopping; use -force to continue anyway.'));
7726
+ return false;
7727
+ }
7728
+ const allIssues = scanned.filter(i => i.kind !== 'registryRef' && i.kind !== 'nameMismatch');
7621
7729
  // Unscoped deps are handled apart from the error/warning prompt below: nothing the
7622
7730
  // user answers could change the outcome, so nothing is asked. Report, remember,
7623
7731
  // stop. See reportUnscopedDeps.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.239",
3
+ "version": "1.0.241",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",