@bobfrankston/npmglobalize 1.0.237 → 1.0.238

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 +1 -1
  2. package/lib.d.ts +18 -10
  3. package/lib.js +95 -27
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -94,7 +94,7 @@ 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 lists every such edge on every run: consumer → dep and range, the `package.json` that holds the range, and — when the same dep is linked as `file:` elsewhere in the graph — the directory of its local source (that package then exists twice). Each finding is repeated as one line in the end-of-run **Issues Summary**, because the prescan block has scrolled away by the time a cascade finishes. Nothing is asked and the run continues; a range can be deliberate. A package with `"files": false` in its `.globalize.json5` keeps npm refs on purpose and is not listed. npmglobalize does not rewrite the range: change it to a `file:` path in the named `package.json`, then run `npmglobalize` in that package. (2026-09-21)
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)
98
98
 
99
99
  Skip the prescan with `-no-prescan` / `-nps`.
100
100
 
package/lib.d.ts CHANGED
@@ -425,7 +425,7 @@ 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: — reported by reportRegistryRefs, nothing asked. */
428
+ * by version range instead of file: — rewritten to file: by fixRegistryRefs (2026-09-23), nothing asked. */
429
429
  kind?: 'unscoped' | 'registryRef';
430
430
  package: string;
431
431
  /** For 'registryRef': the directory of the CONSUMER whose package.json holds the range. */
@@ -448,15 +448,23 @@ export declare function findUndeclaredImports(cwd: string, pkg: any): Undeclared
448
448
  * user resolve all problems before starting the publish cascade instead of
449
449
  * being interrupted mid-run. */
450
450
  export declare function prescanDepGraph(baseDir: string, visited?: Set<string>, verbose?: boolean, fileTargets?: Map<string, string>): PrescanIssue[];
451
- /** Report own-scope packages that some package in the file: graph takes from the registry
452
- * (a version range) instead of from local source. Nothing is asked and the run continues: a
453
- * range can be deliberate, and the fix is an edit to the CONSUMER's package.json, which is not
454
- * npmglobalize's to make. It is printed on every run because the consumer is usually nested
455
- * under other file: deps, where neither the `+` list nor the dependency tree shows it.
456
- * `fileTargets` is the map prescanDepGraph filled: when the same package is linked as file:
457
- * elsewhere in the graph, its directory is named — that package then exists twice, the linked
458
- * source and the registry copy in the consumer's node_modules. */
459
- export declare function reportRegistryRefs(registryRefs: PrescanIssue[], fileTargets: Map<string, string>): void;
451
+ /** Own-scope packages that some package in the file: graph takes from the registry (a version
452
+ * range) instead of from local source get REWRITTEN to a file: path, and npm install runs in
453
+ * that consumer so node_modules holds the link instead of the registry copy. The consumer is
454
+ * usually nested under other file: deps, where neither the `+` list nor the dependency tree
455
+ * shows it, which is why this runs in the prescan, before the cascade reads the consumer.
456
+ * A range whose local source cannot be found (not linked elsewhere in the graph, no sibling
457
+ * directory of that name) is reported as before and left alone. `"files": false` consumers
458
+ * were never collected (see prescanDepGraph). Every outcome is also recorded for the
459
+ * Issues Summary, since the prescan block has scrolled away by the end of a cascade.
460
+ * 2026-09-23 01:40 EDT — Claude Code (Fable 5.1), at Bob's direction: "you should also make
461
+ * sure that package.json for local packages such as httppoly uses file: rather than the npm
462
+ * version". Until now this only reported (v1.0.233), and certsupport kept
463
+ * `miscassists: ^1.0.61` through three runs. Returns false if an npm install failed. */
464
+ export declare function fixRegistryRefs(registryRefs: PrescanIssue[], fileTargets: Map<string, string>, opts?: {
465
+ dryRun?: boolean;
466
+ verbose?: boolean;
467
+ }): Promise<boolean>;
460
468
  /** Report unscoped `file:` deps found by the prescan, without asking anything.
461
469
  *
462
470
  * An unscoped dep with private intent cannot reach npm, and npmglobalize does not
package/lib.js CHANGED
@@ -2001,7 +2001,7 @@ export function findUndeclaredImports(cwd, pkg) {
2001
2001
  * being interrupted mid-run. */
2002
2002
  // 2026-09-21 — Claude Code (Fable 5.1), at Bob's direction ("you need to warn about @installs
2003
2003
  // because they may be nested so I won't see them"). `fileTargets` added: every file: target the
2004
- // walk resolves is recorded (package name → directory) so reportRegistryRefs can say where the
2004
+ // walk resolves is recorded (package name → directory) so fixRegistryRefs can say where the
2005
2005
  // local source of a registry-installed package is. Own-scope deps given as a version range are
2006
2006
  // collected as kind 'registryRef'. Seen in certsupport: `@bobfrankston/miscassists: ^1.0.61`
2007
2007
  // committed in its package.json, two levels under pserve, where no tree listing showed it.
@@ -2104,34 +2104,96 @@ export function prescanDepGraph(baseDir, visited = new Set(), verbose = false, f
2104
2104
  }
2105
2105
  return issues;
2106
2106
  }
2107
- /** Report own-scope packages that some package in the file: graph takes from the registry
2108
- * (a version range) instead of from local source. Nothing is asked and the run continues: a
2109
- * range can be deliberate, and the fix is an edit to the CONSUMER's package.json, which is not
2110
- * npmglobalize's to make. It is printed on every run because the consumer is usually nested
2111
- * under other file: deps, where neither the `+` list nor the dependency tree shows it.
2112
- * `fileTargets` is the map prescanDepGraph filled: when the same package is linked as file:
2113
- * elsewhere in the graph, its directory is named — that package then exists twice, the linked
2114
- * source and the registry copy in the consumer's node_modules. */
2115
- export function reportRegistryRefs(registryRefs, fileTargets) {
2107
+ /** Where the local source of an own-scope package is, or null. First the map prescanDepGraph
2108
+ * filled (the package is linked as file: elsewhere in the graph); then the sibling-directory
2109
+ * convention every file: ref in these trees follows (`file:../<name>`): a directory named
2110
+ * after the unscoped package name beside the consumer, accepted only when the package.json
2111
+ * there carries exactly that package name. Nothing else is searched. */
2112
+ function findLocalSource(consumerDir, packageName, fileTargets) {
2113
+ const known = fileTargets.get(packageName);
2114
+ if (known)
2115
+ return known;
2116
+ const unscopedName = packageName.includes('/') ? packageName.slice(packageName.indexOf('/') + 1) : packageName;
2117
+ const sibling = path.join(path.dirname(consumerDir), unscopedName);
2118
+ try {
2119
+ if (readPackageJson(sibling).name === packageName)
2120
+ return sibling;
2121
+ }
2122
+ catch (error) {
2123
+ // Justified swallow: no package.json beside the consumer under that name is the
2124
+ // ordinary "not a sibling" outcome. The caller reports "local source not found" on
2125
+ // null, so the information becomes that message rather than being lost.
2126
+ }
2127
+ return null;
2128
+ }
2129
+ /** Own-scope packages that some package in the file: graph takes from the registry (a version
2130
+ * range) instead of from local source get REWRITTEN to a file: path, and npm install runs in
2131
+ * that consumer so node_modules holds the link instead of the registry copy. The consumer is
2132
+ * usually nested under other file: deps, where neither the `+` list nor the dependency tree
2133
+ * shows it, which is why this runs in the prescan, before the cascade reads the consumer.
2134
+ * A range whose local source cannot be found (not linked elsewhere in the graph, no sibling
2135
+ * directory of that name) is reported as before and left alone. `"files": false` consumers
2136
+ * were never collected (see prescanDepGraph). Every outcome is also recorded for the
2137
+ * Issues Summary, since the prescan block has scrolled away by the end of a cascade.
2138
+ * 2026-09-23 01:40 EDT — Claude Code (Fable 5.1), at Bob's direction: "you should also make
2139
+ * sure that package.json for local packages such as httppoly uses file: rather than the npm
2140
+ * version". Until now this only reported (v1.0.233), and certsupport kept
2141
+ * `miscassists: ^1.0.61` through three runs. Returns false if an npm install failed. */
2142
+ export async function fixRegistryRefs(registryRefs, fileTargets, opts = {}) {
2116
2143
  if (!registryRefs.length)
2117
- return;
2144
+ return true;
2145
+ const { dryRun = false, verbose = false } = opts;
2118
2146
  console.log(colors.yellow(`${registryRefs.length} own-scope dependenc${registryRefs.length === 1 ? 'y is' : 'ies are'} installed from npm, not linked with file: (a registry copy in node_modules, not the local source):`));
2147
+ // One npm install per consumer, after all of its ranges are rewritten.
2148
+ const rewrittenConsumers = new Map(); // consumer dir → consumer name
2149
+ let ok = true;
2119
2150
  for (const ref of registryRefs) {
2151
+ const pkgJsonPath = path.join(ref.path, 'package.json');
2120
2152
  console.log(` ${colors.yellow('@')} ${ref.consumer} → ${ref.package} ${ref.range}`);
2121
- console.log(colors.dim(` in ${path.join(ref.path, 'package.json')}`));
2122
- const localSource = fileTargets.get(ref.package);
2123
- if (localSource) {
2124
- console.log(colors.dim(` local source, linked as file: elsewhere in this graph: ${localSource}`));
2125
- }
2126
- // 2026-09-21 — Claude Code (Fable 5.1), at Bob's direction ("it should summarize the
2127
- // findings at the end"). The block above prints before the cascade and scrolls away, so
2128
- // each finding also goes into the Issues Summary cli.ts prints on every exit path. One
2129
- // self-contained line: it has to make sense with the prescan output long gone.
2130
- recordBuildIssue(ref.consumer, 'warning', `${ref.package} ${ref.range} is installed from npm, not file: — ${path.join(ref.path, 'package.json')}`
2131
- + (localSource ? ` (local source: ${localSource})` : ''));
2132
- }
2133
- console.log(colors.dim(` To link one, set it to a file: path in the package.json named above, then run npmglobalize in that package.`));
2153
+ console.log(colors.dim(` in ${pkgJsonPath}`));
2154
+ const localSource = findLocalSource(ref.path, ref.package, fileTargets);
2155
+ if (!localSource) {
2156
+ console.log(colors.dim(` local source not found (not linked as file: elsewhere in this graph, no sibling directory of that name) — left as is`));
2157
+ recordBuildIssue(ref.consumer, 'warning', `${ref.package} ${ref.range} is installed from npm, not file: — ${pkgJsonPath} (local source not found, left as is)`);
2158
+ continue;
2159
+ }
2160
+ let relative = path.relative(ref.path, localSource).split(path.sep).join('/');
2161
+ if (!relative.startsWith('.'))
2162
+ relative = './' + relative;
2163
+ const fileSpec = `file:${relative}`;
2164
+ if (dryRun) {
2165
+ console.log(colors.dim(` [dry-run] would set to ${fileSpec} (${localSource}) and run npm install in ${ref.consumer}`));
2166
+ continue;
2167
+ }
2168
+ const pkg = readPackageJson(ref.path);
2169
+ let rewrote = false;
2170
+ for (const key of DEP_KEYS) {
2171
+ // A `.dependencies` backup (left by an interrupted transform) is the file: truth;
2172
+ // prescanDepGraph read the range from it, so the rewrite goes there too.
2173
+ for (const depsKey of ['.' + key, key]) {
2174
+ const deps = pkg[depsKey];
2175
+ if (deps && deps[ref.package] === ref.range) {
2176
+ deps[ref.package] = fileSpec;
2177
+ rewrote = true;
2178
+ }
2179
+ }
2180
+ }
2181
+ if (!rewrote) {
2182
+ console.log(colors.dim(` range not found in ${pkgJsonPath} any more — left as is`));
2183
+ continue;
2184
+ }
2185
+ writePackageJson(ref.path, pkg);
2186
+ console.log(colors.green(` ✓ set to ${fileSpec} (${localSource})`));
2187
+ recordBuildIssue(ref.consumer, 'warning', `${ref.package} ${ref.range} rewritten to ${fileSpec} in ${pkgJsonPath}`);
2188
+ rewrittenConsumers.set(ref.path, ref.consumer);
2189
+ }
2190
+ for (const [consumerDir, consumerName] of rewrittenConsumers) {
2191
+ console.log(colors.yellow(`↻ npm install in ${consumerName} (${consumerDir}) to replace the registry copy with the file: link`));
2192
+ if (!await npmInstallWithCleanRetry(consumerDir, verbose, `fixRegistryRefs: ${consumerDir}`))
2193
+ ok = false;
2194
+ }
2134
2195
  console.log('');
2196
+ return ok;
2135
2197
  }
2136
2198
  /** Report unscoped `file:` deps found by the prescan, without asking anything.
2137
2199
  *
@@ -7366,9 +7428,15 @@ export async function globalize(cwd, options = {}, configOptions = {}) {
7366
7428
  const fileTargets = new Map();
7367
7429
  const scanned = prescanDepGraph(cwd, new Set(), verbose, fileTargets);
7368
7430
  // 2026-09-21 — Claude Code (Fable 5.1), at Bob's direction: registry refs to own-scope
7369
- // packages are reported apart from the prompt below, like unscoped deps — a warning
7370
- // with no question, since no answer would change what this run does.
7371
- reportRegistryRefs(scanned.filter(i => i.kind === 'registryRef'), fileTargets);
7431
+ // packages are handled apart from the prompt below, like unscoped deps — no question.
7432
+ // 2026-09-23 — Claude Code (Fable 5.1), at Bob's direction: they are now FIXED (rewritten
7433
+ // to file: and npm-installed), not just reported. A failed install stops the run like
7434
+ // ensureFileDepModules does, unless -force.
7435
+ const refsOk = await fixRegistryRefs(scanned.filter(i => i.kind === 'registryRef'), fileTargets, { dryRun, verbose });
7436
+ if (!refsOk && !force) {
7437
+ console.error(colors.red('npm install failed while linking own-scope deps as file: (see above). Stopping; use -force to continue anyway.'));
7438
+ return false;
7439
+ }
7372
7440
  const allIssues = scanned.filter(i => i.kind !== 'registryRef');
7373
7441
  // Unscoped deps are handled apart from the error/warning prompt below: nothing the
7374
7442
  // user answers could change the outcome, so nothing is asked. Report, remember,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.237",
3
+ "version": "1.0.238",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",