@bobfrankston/npmglobalize 1.0.191 → 1.0.192

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/_synctoken.cjs ADDED
@@ -0,0 +1,28 @@
1
+ // One-shot: copy the current Windows npm authToken into WSL's ~/.npmrc.
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ const winNpmrc = path.join(os.homedir(), '.npmrc');
7
+ const wslNpmrc = '\\\\wsl.localhost\\Ubuntu\\home\\bob\\.npmrc';
8
+ const KEY = '//registry.npmjs.org/:_authToken=';
9
+
10
+ const winText = fs.readFileSync(winNpmrc, 'utf8');
11
+ const m = winText.match(/\/\/registry\.npmjs\.org\/:_authToken=(\S+)/);
12
+ if (!m) { console.error('No Windows token found in', winNpmrc); process.exit(1); }
13
+ const token = m[1];
14
+
15
+ let wslText = '';
16
+ try { wslText = fs.readFileSync(wslNpmrc, 'utf8'); }
17
+ catch (e) { console.error('Cannot read WSL .npmrc:', e.message); process.exit(1); }
18
+
19
+ const line = KEY + token;
20
+ const lines = wslText.split(/\r?\n/);
21
+ let replaced = false;
22
+ for (let i = 0; i < lines.length; i++) {
23
+ if (lines[i].startsWith(KEY)) { lines[i] = line; replaced = true; }
24
+ }
25
+ if (!replaced) lines.push(line);
26
+ const out = lines.filter((l, i) => !(l === '' && i === lines.length - 1)).join('\n') + '\n';
27
+ fs.writeFileSync(wslNpmrc, out);
28
+ console.log(replaced ? 'Replaced stale WSL token' : 'Added token to WSL .npmrc', '(tail ...' + token.slice(-6) + ')');
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=_synctoken.d.cts.map
package/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
  /**
3
3
  * npmglobalize CLI - Transform file: dependencies to npm versions for publishing
4
4
  */
5
- import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically } from './lib.js';
5
+ import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations } from './lib.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
- import { styleText } from 'util';
8
+ import { colors } from './colors.js';
9
9
  // npmglobalize install directory (for --version)
10
10
  const __dirname = import.meta.dirname;
11
11
  /** Own version, read once. Stamped into the startup banner AND the Issues Summary
@@ -116,6 +116,11 @@ Other Options:
116
116
  Before npm pack, wipe node_modules/ in each file: dep target.
117
117
  Fixes arborist "Cannot read properties of null" crashes when
118
118
  sibling file: deps have their own nested node_modules.
119
+ -ts7-report, -deprecation-report
120
+ Report-only: scan this package and its file: deps for
121
+ compilerOptions removed in TypeScript 7 (deprecated
122
+ moduleResolution, target es3, deprecated flags). Lists a
123
+ migration to-do list and exits. Writes nothing.
119
124
  -show Show package.json dependency changes
120
125
  -package, -pkg Update package.json scripts to use npmglobalize
121
126
  -h, -help Show this help
@@ -155,6 +160,7 @@ function parseArgs(args) {
155
160
  help: false,
156
161
  version: false,
157
162
  error: '',
163
+ ts7Report: false,
158
164
  explicitKeys: new Set()
159
165
  };
160
166
  const unrecognized = [];
@@ -172,6 +178,10 @@ function parseArgs(args) {
172
178
  case '-v':
173
179
  options.version = true;
174
180
  break;
181
+ case '-ts7-report':
182
+ case '-deprecation-report':
183
+ options.ts7Report = true;
184
+ break;
175
185
  case '-patch':
176
186
  options.bump = 'patch';
177
187
  break;
@@ -423,24 +433,55 @@ function parseArgs(args) {
423
433
  }
424
434
  return options;
425
435
  }
426
- /** Print accumulated build issues in warning color */
436
+ /** Print accumulated build issues, colored per severity via the theme module. */
427
437
  function printBuildSummary() {
428
438
  const issues = getBuildIssues();
429
439
  if (issues.length === 0)
430
440
  return;
431
441
  console.log('');
432
- console.log(styleText('yellow', `━━━ Issues Summary (npmglobalize v${NPMGLOBALIZE_VERSION}) ━━━`));
442
+ console.log(colors.warn(`━━━ Issues Summary (npmglobalize v${NPMGLOBALIZE_VERSION}) ━━━`));
433
443
  for (const issue of issues) {
444
+ // Errors in red (readable on any background); warnings in the theme warn color.
445
+ const paint = issue.severity === 'error' ? colors.error : colors.warn;
434
446
  const icon = issue.severity === 'error' ? '✗' : '⚠';
435
- console.log(styleText('yellow', ` ${icon} ${issue.module}: ${issue.message}`));
447
+ console.log(paint(` ${icon} ${issue.module}: ${issue.message}`));
436
448
  }
437
- console.log(styleText('yellow', '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
449
+ console.log(colors.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
450
+ console.log('');
451
+ }
452
+ /** Print the report-only TS7-readiness scan over cwd + its file: dep graph. */
453
+ function printTs7Report(cwd) {
454
+ const findings = reportTs7Deprecations(cwd);
455
+ console.log('');
456
+ console.log(colors.accent('━━━ TypeScript 7 readiness ━━━'));
457
+ if (findings.length === 0) {
458
+ console.log(colors.success(' ✓ No removed-in-TS7 compilerOptions found in this package or its file: deps.'));
459
+ console.log('');
460
+ return;
461
+ }
462
+ // Group by package for a readable migration to-do list.
463
+ const byPkg = new Map();
464
+ for (const f of findings) {
465
+ if (!byPkg.has(f.name))
466
+ byPkg.set(f.name, []);
467
+ byPkg.get(f.name).push(f);
468
+ }
469
+ console.log(colors.warn(` ${findings.length} item(s) across ${byPkg.size} package(s) need migration before TypeScript 7:`));
470
+ for (const [name, items] of byPkg) {
471
+ console.log(colors.accent(`\n ${name}`));
472
+ for (const f of items) {
473
+ console.log(colors.warn(` • ${f.option}: ${JSON.stringify(f.value)} (in ${f.definedIn})`));
474
+ console.log(colors.muted(` → ${f.action}`));
475
+ }
476
+ }
477
+ console.log(colors.muted('\n Note: npmglobalize auto-applies these migrations at build time when a deprecation error fires —'));
478
+ console.log(colors.muted(' it does NOT silence them with "ignoreDeprecations". Any ignoreDeprecations above is itself a flagged bug.'));
438
479
  console.log('');
439
480
  }
440
481
  export async function main() {
441
482
  installCleanupHandlers();
442
483
  // Show version at the very start
443
- console.log(styleText('cyan', `npmglobalize v${NPMGLOBALIZE_VERSION}`));
484
+ console.log(colors.accent(`npmglobalize v${NPMGLOBALIZE_VERSION}`));
444
485
  const args = process.argv.slice(2);
445
486
  const cliOptions = parseArgs(args);
446
487
  if (cliOptions.help) {
@@ -469,6 +510,11 @@ export async function main() {
469
510
  process.exit(1);
470
511
  }
471
512
  }
513
+ // Report-only TS7-readiness scan (no transform / build / publish).
514
+ if (cliOptions.ts7Report) {
515
+ printTs7Report(cwd);
516
+ process.exit(0);
517
+ }
472
518
  // Build file: deps topologically, then the target itself.
473
519
  // Ensures consumers' tsc sees up-to-date `.d.ts` from sibling checkouts
474
520
  // whose source has changed since their last build.
@@ -480,14 +526,14 @@ export async function main() {
480
526
  process.exit(1);
481
527
  }
482
528
  if (!depsOk)
483
- console.log(styleText('yellow', 'Continuing with --force despite dep build failure...'));
529
+ console.log(colors.warn('Continuing with --force despite dep build failure...'));
484
530
  const targetOk = await buildProject(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force });
485
531
  if (!targetOk) {
486
532
  if (!cliOptions.force) {
487
533
  printBuildSummary();
488
534
  process.exit(1);
489
535
  }
490
- console.log(styleText('yellow', 'Continuing with --force...'));
536
+ console.log(colors.warn('Continuing with --force...'));
491
537
  }
492
538
  }
493
539
  // Handle --package: update scripts in target package.json
package/colors.js CHANGED
@@ -18,7 +18,11 @@ function detectTheme() {
18
18
  return 'light';
19
19
  if (process.env.NPMG_THEME === 'dark')
20
20
  return 'dark';
21
- return 'dark';
21
+ // Unknown background: default to the 'light' palette. Its colors (magenta warn,
22
+ // red, green, blue, cyan) are all readable on BOTH light and dark terminals,
23
+ // whereas the 'dark' palette's yellow warn is invisible on a light background.
24
+ // Per global pref: never default to yellow on an unknown background.
25
+ return 'light';
22
26
  }
23
27
  const theme = detectTheme();
24
28
  // Raw ANSI colors per theme — semantic → [dark, light]
package/lib.d.ts CHANGED
@@ -328,6 +328,25 @@ export declare function buildFileDepsTopologically(cwd: string, opts?: {
328
328
  verbose?: boolean;
329
329
  force?: boolean;
330
330
  }, visited?: Set<string>): Promise<boolean>;
331
+ /** A single TS7-readiness problem found in a package's effective tsconfig. */
332
+ export interface Ts7Finding {
333
+ name: string;
334
+ dir: string;
335
+ option: string;
336
+ value: string;
337
+ /** tsconfig (relative to the package dir) where the value is actually set —
338
+ * may be the package's own file or a base it extends. */
339
+ definedIn: string;
340
+ action: string;
341
+ }
342
+ /** Report-only TS7-readiness scan over `cwd` and its `file:` dep graph. For each
343
+ * package with a tsconfig it resolves the *effective* compilerOptions through the
344
+ * `extends` chain and flags removed-in-TS7 settings (deprecated `moduleResolution`,
345
+ * `target: es3`, and the deprecated boolean flags) plus any `ignoreDeprecations`
346
+ * (itself a flagged bug — it silences a real TS7 error). Reads only — never writes.
347
+ * The same migration `migrateTsconfigDeprecations` applies automatically on a build
348
+ * failure; this report is the read-only, whole-graph inventory. */
349
+ export declare function reportTs7Deprecations(cwd: string): Ts7Finding[];
331
350
  /** Ensure the workspace-root `node_modules/` is in sync with every member's
332
351
  * declared deps. Workspaces hoist deps to the root, so a dep added to any
333
352
  * member `package.json` without a follow-up `npm install` at the root leaves
@@ -364,9 +383,6 @@ export declare function runCommandAsync(cmd: string, args: string[], options?: {
364
383
  stderr: string;
365
384
  signal: NodeJS.Signals | null;
366
385
  }>;
367
- /** Install a package globally in WSL, auto-fixing the root-owned /usr/local/lib/node_modules
368
- * EACCES case by switching npm to a user prefix (~/.npm-global) and retrying once.
369
- * Output is captured (so we can scan for the error) and then mirrored to the terminal. */
370
386
  export declare function installInWsl(wslArgs: string[], opts?: {
371
387
  cwd?: string;
372
388
  }): Promise<{
package/lib.js CHANGED
@@ -1975,6 +1975,39 @@ function insertCompilerOption(text, key, valueLiteral) {
1975
1975
  // Inline single-line object: `{ "target": ... }`
1976
1976
  return text.slice(0, braceEnd) + ` ${prop},` + text.slice(braceEnd);
1977
1977
  }
1978
+ /** Replace the value of an existing top-level `compilerOptions.<key>` in `text`,
1979
+ * preserving the file's formatting. The key's value may be a quoted string, a
1980
+ * boolean/number, or any run up to the next comma/brace/newline. Returns the
1981
+ * patched text, or null if the key isn't present as a simple property. */
1982
+ function replaceCompilerOptionValue(text, key, newValueLiteral) {
1983
+ const re = new RegExp(`("${key}"\\s*:\\s*)("(?:[^"\\\\]|\\\\.)*"|[^,}\\n]+)`);
1984
+ const m = text.match(re);
1985
+ if (!m)
1986
+ return null;
1987
+ return text.slice(0, m.index) + m[1] + newValueLiteral + text.slice(m.index + m[0].length);
1988
+ }
1989
+ /** Set `compilerOptions.<key>` to `valueLiteral`: replace it in place if already
1990
+ * present, otherwise insert it. Returns patched text, or null if neither could be
1991
+ * done safely (no locatable `compilerOptions` object). */
1992
+ function upsertCompilerOption(text, key, valueLiteral) {
1993
+ const replaced = replaceCompilerOptionValue(text, key, valueLiteral);
1994
+ if (replaced !== null)
1995
+ return replaced;
1996
+ return insertCompilerOption(text, key, valueLiteral);
1997
+ }
1998
+ /** Remove the whole `compilerOptions.<key>` property line. Works for the usual
1999
+ * one-option-per-line tsconfig formatting; returns null if `<key>` isn't on its
2000
+ * own line (caller then reports it as a manual fix). A trailing comma left on the
2001
+ * preceding line is fine — tsconfig is parsed as JSONC (trailing commas allowed). */
2002
+ function removeCompilerOptionLine(text, key) {
2003
+ const lines = text.split('\n');
2004
+ const re = new RegExp(`^\\s*"${key}"\\s*:`);
2005
+ const idx = lines.findIndex(l => re.test(l));
2006
+ if (idx === -1)
2007
+ return null;
2008
+ lines.splice(idx, 1);
2009
+ return lines.join('\n');
2010
+ }
1978
2011
  /** Insert a `"types": [...]` line as the first property of `compilerOptions`.
1979
2012
  * Only ever called when `compilerOptions.types` is absent. */
1980
2013
  function insertTypesIntoTsconfig(text, typesArr) {
@@ -2107,54 +2140,109 @@ function ensureTsconfigNodeTypes(cwd) {
2107
2140
  }
2108
2141
  recordBuildIssue(chain[0].parsed?.name || path.basename(cwd), 'warning', `TS6+ build may fail: no writable compilerOptions block in the tsconfig chain of ${cwd} to add "types"`);
2109
2142
  }
2110
- /** TS6 turned several long-deprecated compiler options (e.g.
2111
- * `moduleResolution: "node10"`/`"node"`/`"classic"`, old `target`s) into hard
2112
- * errors (`TS5107`/`TS5101`) that will be *removed* in TS7. TypeScript's own
2113
- * sanctioned bridge for the 6→7 window is `"ignoreDeprecations": "<version>"`
2114
- * (the error text names the version, e.g. `"6.0"`). We apply exactly that so the
2115
- * build proceeds, and record a warning so the real migration (to `node16` /
2116
- * `nodenext` / `bundler`) still happens before TS7. We do NOT auto-rewrite
2117
- * `moduleResolution` itself — that changes resolution semantics and must be a
2118
- * human, per-package decision. Returns true if it patched a file. Mirrors
2119
- * `ensureTsconfigNodeTypes`: resolves the `extends` chain, respects an existing
2120
- * `ignoreDeprecations`, and patches the base-most writable config. */
2121
- function ensureTsconfigIgnoreDeprecations(cwd, version) {
2143
+ /** TS6 turned several long-deprecated compiler options (`moduleResolution:
2144
+ * "node10"`/`"node"`/`"classic"`, `target: "es3"`, the dead boolean flags) into
2145
+ * hard errors (`TS5107`/`TS5101`) that TS7 *removes*. We deliberately do NOT
2146
+ * silence them with `"ignoreDeprecations"` — that just defers a real error to the
2147
+ * day TS7 lands. Instead we perform the actual migration in place:
2148
+ * `moduleResolution` `"nodenext"` (and align `module` to `"nodenext"`, since
2149
+ * `nodenext` resolution requires a `nodenext`/`node16`/`preserve` module),
2150
+ *`target: "es3"` `"es2022"`,
2151
+ * delete each dead flag (`importsNotUsedAsValues`, `out`, …).
2152
+ * Each option is edited in the writable file where it's defined; when it's
2153
+ * inherited from a `node_modules` base we instead write an override into the
2154
+ * most-leaf writable config so it wins. This is a *semantic* change: the rebuild
2155
+ * may now surface genuine resolution errors — that is the point. Those are real
2156
+ * bugs the `node10` resolver was hiding, not deprecations. Returns true if it
2157
+ * changed any file. */
2158
+ function migrateTsconfigDeprecations(cwd) {
2122
2159
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
2123
2160
  if (!fs.existsSync(tsconfigPath))
2124
2161
  return false;
2125
- const { chain, truncated } = resolveTsconfigChain(tsconfigPath);
2162
+ const { chain } = resolveTsconfigChain(tsconfigPath);
2126
2163
  if (!chain.length)
2127
2164
  return false;
2128
- // Already set somewhere in the chain (possibly with a different version) — leave it.
2129
- if (chain.some(c => c.parsed?.compilerOptions?.ignoreDeprecations !== undefined))
2130
- return false;
2131
- if (truncated)
2132
- return false;
2133
- const valueLiteral = JSON.stringify(version);
2134
- const candidates = chain.filter(c => c.writable).reverse();
2135
- for (const cand of candidates) {
2165
+ const name = (() => { try {
2166
+ return readPackageJson(cwd).name;
2167
+ }
2168
+ catch {
2169
+ return null;
2170
+ } })()
2171
+ || chain[0].parsed?.name || path.basename(cwd);
2172
+ const mostLeafWritable = chain.find(c => c.writable); // chain is leaf-first
2173
+ /** File to edit for an option defined in `definedIn`: that file if writable,
2174
+ * else the most-leaf writable config (an override there beats a node_modules base). */
2175
+ const targetFor = (definedIn) => {
2176
+ const def = chain.find(c => c.path === definedIn);
2177
+ return def?.writable ? def : mostLeafWritable;
2178
+ };
2179
+ const edit = (filePath, fn) => {
2136
2180
  let text;
2137
2181
  try {
2138
- text = fs.readFileSync(cand.path, 'utf-8');
2182
+ text = fs.readFileSync(filePath, 'utf-8');
2139
2183
  }
2140
2184
  catch {
2141
- continue;
2185
+ return false;
2142
2186
  }
2143
- const patched = insertCompilerOption(text, 'ignoreDeprecations', valueLiteral);
2144
- if (!patched)
2145
- continue;
2187
+ const patched = fn(text);
2188
+ if (patched == null || patched === text)
2189
+ return false;
2146
2190
  try {
2147
- fs.writeFileSync(cand.path, patched);
2148
- const where = path.relative(cwd, cand.path) || 'tsconfig.json';
2149
- console.log(colors.cyan(` Patched ${where}: added "ignoreDeprecations": ${valueLiteral} (silences TS6 removed-in-TS7 deprecation errors)`));
2150
- recordBuildIssue(chain[0].parsed?.name || path.basename(cwd), 'warning', `Deprecated compilerOptions silenced via ignoreDeprecations=${version}. Migrate moduleResolution to "node16"/"nodenext"/"bundler" before TypeScript 7.`);
2191
+ fs.writeFileSync(filePath, patched);
2151
2192
  return true;
2152
2193
  }
2153
2194
  catch (error) {
2154
- console.error(colors.yellow(` Could not write tsconfig patch (${cand.path}): ${error.message}`));
2195
+ console.error(colors.yellow(` Could not write tsconfig patch (${filePath}): ${error.message}`));
2196
+ return false;
2197
+ }
2198
+ };
2199
+ const note = (msg) => console.log(colors.cyan(` ${msg}`));
2200
+ let changed = false;
2201
+ // moduleResolution: node/node10/classic → nodenext (+ align module).
2202
+ const mr = effectiveCompilerOption(chain, 'moduleResolution');
2203
+ if (mr && typeof mr.value === 'string' && DEPRECATED_MODULE_RESOLUTION.has(mr.value.toLowerCase())) {
2204
+ const tgt = targetFor(mr.definedIn);
2205
+ if (tgt && edit(tgt.path, t => upsertCompilerOption(t, 'moduleResolution', '"nodenext"'))) {
2206
+ changed = true;
2207
+ note(`Migrated moduleResolution "${mr.value}" → "nodenext" in ${path.relative(cwd, tgt.path) || 'tsconfig.json'}`);
2208
+ // nodenext resolution requires a matching module setting.
2209
+ const mod = effectiveCompilerOption(chain, 'module');
2210
+ const modOk = mod && typeof mod.value === 'string' && /^(node16|nodenext|preserve)$/i.test(mod.value);
2211
+ if (!modOk) {
2212
+ const modTgt = mod ? targetFor(mod.definedIn) : mostLeafWritable;
2213
+ if (modTgt && edit(modTgt.path, t => upsertCompilerOption(t, 'module', '"nodenext"'))) {
2214
+ note(`Aligned module → "nodenext" (required by moduleResolution "nodenext")`);
2215
+ }
2216
+ }
2217
+ recordBuildIssue(name, 'warning', `Migrated moduleResolution "${mr.value}" → "nodenext". Verify relative imports resolve (NodeNext requires explicit file extensions on relative specifiers).`);
2218
+ }
2219
+ }
2220
+ // target: es3 → es2022 (es3 is removed in TS7).
2221
+ const tg = effectiveCompilerOption(chain, 'target');
2222
+ if (tg && typeof tg.value === 'string' && tg.value.toLowerCase() === 'es3') {
2223
+ const tgt = targetFor(tg.definedIn);
2224
+ if (tgt && edit(tgt.path, t => upsertCompilerOption(t, 'target', '"es2022"'))) {
2225
+ changed = true;
2226
+ note(`Raised target "es3" → "es2022" in ${path.relative(cwd, tgt.path) || 'tsconfig.json'}`);
2227
+ }
2228
+ }
2229
+ // Dead boolean flags: delete where writable; report when inherited from a base.
2230
+ for (const flag of DEPRECATED_TS7_FLAGS) {
2231
+ const f = effectiveCompilerOption(chain, flag);
2232
+ if (!f)
2233
+ continue;
2234
+ const def = chain.find(c => c.path === f.definedIn);
2235
+ if (def?.writable) {
2236
+ if (edit(def.path, t => removeCompilerOptionLine(t, flag))) {
2237
+ changed = true;
2238
+ note(`Removed deprecated flag "${flag}" from ${path.relative(cwd, def.path) || 'tsconfig.json'}`);
2239
+ }
2240
+ }
2241
+ else {
2242
+ recordBuildIssue(name, 'warning', `Deprecated flag "${flag}" is inherited from ${path.relative(cwd, f.definedIn)} (a node_modules base) — remove it there before TypeScript 7.`);
2155
2243
  }
2156
2244
  }
2157
- return false;
2245
+ return changed;
2158
2246
  }
2159
2247
  /** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
2160
2248
  * TypeScript project lacks a build script, run `npm run build`, record
@@ -2192,12 +2280,12 @@ export async function buildProject(cwd, opts = {}) {
2192
2280
  console.log(`Building ${pkg.name || cwd}...`);
2193
2281
  let buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
2194
2282
  if (!buildResult.success) {
2195
- // TS6 deprecation error (removed-in-TS7): apply the version TS itself names
2196
- // in `"ignoreDeprecations": "<v>"` and retry once.
2283
+ // TS6 deprecation error (removed-in-TS7): perform the real migration
2284
+ // (moduleResolution→nodenext, raise target, drop dead flags) and retry once.
2285
+ // We don't silence with ignoreDeprecations — the retry may now surface real
2286
+ // resolution errors, which are genuine bugs to fix, not deprecations.
2197
2287
  const out = (buildResult.stderr || '') + (buildResult.output || '');
2198
- const dep = out.match(/error TS510[17]\b/) &&
2199
- out.match(/['"]ignoreDeprecations['"]\s*:\s*['"]([\d.]+)['"]/);
2200
- if (dep && ensureTsconfigIgnoreDeprecations(cwd, dep[1])) {
2288
+ if (/error TS510[17]\b/.test(out) && migrateTsconfigDeprecations(cwd)) {
2201
2289
  buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
2202
2290
  }
2203
2291
  }
@@ -2259,6 +2347,136 @@ export async function buildFileDepsTopologically(cwd, opts = {}, visited = new S
2259
2347
  }
2260
2348
  return allOk;
2261
2349
  }
2350
+ /** `moduleResolution` values TypeScript 6 deprecated and TypeScript 7 removes. */
2351
+ const DEPRECATED_MODULE_RESOLUTION = new Set(['node', 'node10', 'classic']);
2352
+ /** Compiler flags TypeScript deprecated (TS5101) and will remove in TS7. Their
2353
+ * mere presence — at any value — is a migration item. */
2354
+ const DEPRECATED_TS7_FLAGS = [
2355
+ 'charset', 'importsNotUsedAsValues', 'keyofStringsOnly', 'noImplicitUseStrict',
2356
+ 'noStrictGenericChecks', 'out', 'preserveValueImports',
2357
+ 'suppressExcessPropertyErrors', 'suppressImplicitAnyIndexErrors',
2358
+ ];
2359
+ /** First defined value of `key` across the resolved chain (leaf overrides base),
2360
+ * plus which file in the chain defined it. */
2361
+ function effectiveCompilerOption(chain, key) {
2362
+ for (const c of chain) {
2363
+ const v = c.parsed?.compilerOptions?.[key];
2364
+ if (v !== undefined)
2365
+ return { value: v, definedIn: c.path };
2366
+ }
2367
+ return null;
2368
+ }
2369
+ /** Expand one npm `workspaces` pattern (relative to `baseDir`) to package dirs.
2370
+ * Handles an explicit path and a single-level `<prefix>/*` glob — the common
2371
+ * cases; deep `**` globs aren't expanded. */
2372
+ function expandWorkspacePattern(baseDir, pattern) {
2373
+ if (!pattern.includes('*')) {
2374
+ const d = path.resolve(baseDir, pattern);
2375
+ return fs.existsSync(path.join(d, 'package.json')) ? [d] : [];
2376
+ }
2377
+ const prefix = pattern.slice(0, pattern.indexOf('*')).replace(/[\\/]$/, '');
2378
+ const parent = path.resolve(baseDir, prefix);
2379
+ try {
2380
+ return fs.readdirSync(parent, { withFileTypes: true })
2381
+ .filter(e => e.isDirectory())
2382
+ .map(e => path.join(parent, e.name))
2383
+ .filter(d => fs.existsSync(path.join(d, 'package.json')));
2384
+ }
2385
+ catch {
2386
+ return [];
2387
+ }
2388
+ }
2389
+ /** Collect this package and every package reachable through `file:` deps AND
2390
+ * npm `workspaces` members (the same graph the build cascade walks). Returns a
2391
+ * set of absolute dirs. */
2392
+ function collectFileDepDirs(cwd, visited = new Set()) {
2393
+ const abs = path.resolve(cwd);
2394
+ if (visited.has(abs))
2395
+ return visited;
2396
+ visited.add(abs);
2397
+ let pkg;
2398
+ try {
2399
+ pkg = readPackageJson(cwd);
2400
+ }
2401
+ catch {
2402
+ return visited;
2403
+ }
2404
+ for (const key of ['dependencies', 'devDependencies']) {
2405
+ const deps = pkg?.[key];
2406
+ if (!deps || typeof deps !== 'object')
2407
+ continue;
2408
+ for (const [, spec] of Object.entries(deps)) {
2409
+ if (typeof spec !== 'string' || !spec.startsWith('file:'))
2410
+ continue;
2411
+ const target = path.resolve(cwd, spec.slice('file:'.length));
2412
+ if (!fs.existsSync(path.join(target, 'package.json')))
2413
+ continue;
2414
+ collectFileDepDirs(target, visited);
2415
+ }
2416
+ }
2417
+ // Workspace members (referenced via the `workspaces` field, not file: deps).
2418
+ const ws = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
2419
+ if (Array.isArray(ws)) {
2420
+ for (const pattern of ws) {
2421
+ if (typeof pattern !== 'string')
2422
+ continue;
2423
+ for (const memberDir of expandWorkspacePattern(cwd, pattern))
2424
+ collectFileDepDirs(memberDir, visited);
2425
+ }
2426
+ }
2427
+ return visited;
2428
+ }
2429
+ /** Report-only TS7-readiness scan over `cwd` and its `file:` dep graph. For each
2430
+ * package with a tsconfig it resolves the *effective* compilerOptions through the
2431
+ * `extends` chain and flags removed-in-TS7 settings (deprecated `moduleResolution`,
2432
+ * `target: es3`, and the deprecated boolean flags) plus any `ignoreDeprecations`
2433
+ * (itself a flagged bug — it silences a real TS7 error). Reads only — never writes.
2434
+ * The same migration `migrateTsconfigDeprecations` applies automatically on a build
2435
+ * failure; this report is the read-only, whole-graph inventory. */
2436
+ export function reportTs7Deprecations(cwd) {
2437
+ const dirs = collectFileDepDirs(cwd);
2438
+ const findings = [];
2439
+ for (const dir of dirs) {
2440
+ const tsconfigPath = path.join(dir, 'tsconfig.json');
2441
+ if (!fs.existsSync(tsconfigPath))
2442
+ continue;
2443
+ const { chain } = resolveTsconfigChain(tsconfigPath);
2444
+ if (!chain.length)
2445
+ continue;
2446
+ let name;
2447
+ try {
2448
+ name = readPackageJson(dir).name || path.basename(dir);
2449
+ }
2450
+ catch {
2451
+ name = path.basename(dir);
2452
+ }
2453
+ const rel = (p) => path.relative(dir, p) || 'tsconfig.json';
2454
+ // Any ignoreDeprecations is itself a latent bug: it silences a real,
2455
+ // removed-in-TS7 error rather than fixing it. Flag it, loudly.
2456
+ const igd = effectiveCompilerOption(chain, 'ignoreDeprecations');
2457
+ if (igd !== null) {
2458
+ findings.push({ name, dir, option: 'ignoreDeprecations', value: String(igd.value), definedIn: rel(igd.definedIn),
2459
+ action: 'REMOVE — silences a real TS7-removal error instead of fixing it; a bug waiting to bite at TS7. Fix the underlying option(s) below, then delete this.' });
2460
+ }
2461
+ const mr = effectiveCompilerOption(chain, 'moduleResolution');
2462
+ if (mr && typeof mr.value === 'string' && DEPRECATED_MODULE_RESOLUTION.has(mr.value.toLowerCase())) {
2463
+ findings.push({ name, dir, option: 'moduleResolution', value: mr.value, definedIn: rel(mr.definedIn),
2464
+ action: 'migrate to "node16" / "nodenext" / "bundler" (validate import resolution)' });
2465
+ }
2466
+ const tgt = effectiveCompilerOption(chain, 'target');
2467
+ if (tgt && typeof tgt.value === 'string' && tgt.value.toLowerCase() === 'es3') {
2468
+ findings.push({ name, dir, option: 'target', value: tgt.value, definedIn: rel(tgt.definedIn),
2469
+ action: 'raise target — "es3" is removed in TS7' });
2470
+ }
2471
+ for (const flag of DEPRECATED_TS7_FLAGS) {
2472
+ const f = effectiveCompilerOption(chain, flag);
2473
+ if (f)
2474
+ findings.push({ name, dir, option: flag, value: String(f.value), definedIn: rel(f.definedIn),
2475
+ action: 'remove — deprecated flag, removed in TS7' });
2476
+ }
2477
+ }
2478
+ return findings;
2479
+ }
2262
2480
  /** Ensure the workspace-root `node_modules/` is in sync with every member's
2263
2481
  * declared deps. Workspaces hoist deps to the root, so a dep added to any
2264
2482
  * member `package.json` without a follow-up `npm install` at the root leaves
@@ -2439,36 +2657,133 @@ function dumpPackCtrlcDiagnostics(pkg, cwd, packOutput, packStderr) {
2439
2657
  /** Install a package globally in WSL, auto-fixing the root-owned /usr/local/lib/node_modules
2440
2658
  * EACCES case by switching npm to a user prefix (~/.npm-global) and retrying once.
2441
2659
  * Output is captured (so we can scan for the error) and then mirrored to the terminal. */
2660
+ /** Pull the install spec (e.g. `@scope/name@1.2.3`) out of WSL install args.
2661
+ * Returns null for a local-dir install (`.`) or when no spec is present —
2662
+ * those can't 404 on registry propagation. */
2663
+ function extractWslInstallSpec(wslArgs) {
2664
+ const idx = wslArgs.findIndex(a => a === 'install' || a === 'i');
2665
+ if (idx === -1)
2666
+ return null;
2667
+ for (const a of wslArgs.slice(idx + 1)) {
2668
+ if (a.startsWith('-'))
2669
+ continue; // flag
2670
+ if (a === '.' || a === './')
2671
+ return null; // local dir install
2672
+ return a; // first positional = the spec
2673
+ }
2674
+ return null;
2675
+ }
2676
+ /** Is WSL's npm authenticated to the registry? (npm masks a private-package 401
2677
+ * as a 404, so a WSL install can fail with E404 purely because WSL's token is
2678
+ * stale/missing while Windows' token is fine.) */
2679
+ async function isWslNpmAuthed() {
2680
+ const r = await runCommandAsync('wsl', ['npm', 'whoami'], { silent: true });
2681
+ return r.success && (r.output || '').trim().length > 0;
2682
+ }
2683
+ /** Copy the current Windows npm authToken into WSL's npm config. WSL keeps its
2684
+ * own `~/.npmrc` (separate `/home/<user>`), so a token rotated on Windows leaves
2685
+ * WSL stale → private installs 404. Uses `wsl npm config set` (argv passed
2686
+ * directly via spawn — no shell-quoting hazards). Returns true if a token was
2687
+ * found on Windows and the set command succeeded. */
2688
+ async function syncNpmTokenToWsl() {
2689
+ let token;
2690
+ try {
2691
+ const winNpmrc = path.join(os.homedir(), '.npmrc');
2692
+ token = fs.readFileSync(winNpmrc, 'utf8').match(/\/\/registry\.npmjs\.org\/:_authToken=(\S+)/)?.[1];
2693
+ }
2694
+ catch {
2695
+ return false;
2696
+ }
2697
+ if (!token)
2698
+ return false;
2699
+ const r = await runCommandAsync('wsl', ['npm', 'config', 'set', `//registry.npmjs.org/:_authToken=${token}`], { silent: true });
2700
+ return r.success;
2701
+ }
2702
+ /** Poll the npm registry *from inside WSL* until `spec` resolves there. The
2703
+ * Windows-side `waitForNpmVersion` can report "ready" while WSL's npm still
2704
+ * hits a CDN edge that 404s — so for a WSL install we must confirm visibility
2705
+ * in WSL's own view before retrying. */
2706
+ async function waitForNpmVersionInWsl(spec, maxWaitMs = 180000) {
2707
+ const interval = 10000;
2708
+ const maxAttempts = Math.ceil(maxWaitMs / interval);
2709
+ process.stdout.write(`${timestamp()} Waiting for ${spec} on npm registry (WSL view)`);
2710
+ for (let i = 0; i < maxAttempts; i++) {
2711
+ const r = await runCommandAsync('wsl', ['npm', 'view', spec, 'version'], { silent: true });
2712
+ if (r.success && (r.output || '').trim()) {
2713
+ process.stdout.write(' ready\n');
2714
+ return true;
2715
+ }
2716
+ process.stdout.write('.');
2717
+ await new Promise(resolve => setTimeout(resolve, interval));
2718
+ }
2719
+ process.stdout.write(' timed out\n');
2720
+ return false;
2721
+ }
2442
2722
  export async function installInWsl(wslArgs, opts = {}) {
2443
- console.log(colors.cyan(`> wsl ${wslArgs.join(' ')}`));
2444
- let result = await runCommandAsync('wsl', wslArgs, { cwd: opts.cwd, silent: true });
2445
- if (result.output)
2446
- process.stdout.write(result.output);
2447
- if (result.stderr)
2448
- process.stderr.write(result.stderr);
2723
+ const runOnce = async () => {
2724
+ console.log(colors.cyan(`> wsl ${wslArgs.join(' ')}`));
2725
+ const r = await runCommandAsync('wsl', wslArgs, { cwd: opts.cwd, silent: true });
2726
+ if (r.output)
2727
+ process.stdout.write(r.output);
2728
+ if (r.stderr)
2729
+ process.stderr.write(r.stderr);
2730
+ return r;
2731
+ };
2732
+ let result = await runOnce();
2449
2733
  if (result.success)
2450
2734
  return { success: true, fixed: false };
2451
- const combined = (result.output || '') + '\n' + (result.stderr || '');
2452
- const permIssue = /EACCES/.test(combined) && /\/usr\/(?:local\/)?lib\/node_modules/.test(combined);
2453
- if (!permIssue)
2454
- return { success: false, fixed: false };
2455
- console.log(colors.yellow('Detected root-owned npm prefix in WSL switching to ~/.npm-global and retrying...'));
2456
- const fix = `set -e; npm config set prefix "$HOME/.npm-global"; mkdir -p "$HOME/.npm-global/bin"; if ! grep -q '\\.npm-global/bin' "$HOME/.bashrc" 2>/dev/null; then printf '\\n# npm user-prefix bin (added by npmglobalize)\\nexport PATH="$HOME/.npm-global/bin:$PATH"\\n' >> "$HOME/.bashrc"; fi`;
2457
- const fixResult = await runCommandAsync('wsl', ['bash', '-lc', fix], { silent: true });
2458
- if (!fixResult.success) {
2459
- console.error(colors.red('Failed to apply WSL npm prefix fix:'));
2460
- if (fixResult.stderr)
2461
- process.stderr.write(fixResult.stderr);
2462
- return { success: false, fixed: false };
2463
- }
2464
- console.log(colors.green('✓ WSL npm prefix set to ~/.npm-global; PATH appended to ~/.bashrc'));
2465
- console.log(colors.cyan(`> wsl ${wslArgs.join(' ')}`));
2466
- result = await runCommandAsync('wsl', wslArgs, { cwd: opts.cwd, silent: true });
2467
- if (result.output)
2468
- process.stdout.write(result.output);
2469
- if (result.stderr)
2470
- process.stderr.write(result.stderr);
2471
- return { success: result.success, fixed: result.success };
2735
+ let combined = (result.output || '') + '\n' + (result.stderr || '');
2736
+ // EACCES on a root-owned npm prefix → switch to a user prefix and retry.
2737
+ if (/EACCES/.test(combined) && /\/usr\/(?:local\/)?lib\/node_modules/.test(combined)) {
2738
+ console.log(colors.yellow('Detected root-owned npm prefix in WSL — switching to ~/.npm-global and retrying...'));
2739
+ const fix = `set -e; npm config set prefix "$HOME/.npm-global"; mkdir -p "$HOME/.npm-global/bin"; if ! grep -q '\\.npm-global/bin' "$HOME/.bashrc" 2>/dev/null; then printf '\\n# npm user-prefix bin (added by npmglobalize)\\nexport PATH="$HOME/.npm-global/bin:$PATH"\\n' >> "$HOME/.bashrc"; fi`;
2740
+ const fixResult = await runCommandAsync('wsl', ['bash', '-lc', fix], { silent: true });
2741
+ if (!fixResult.success) {
2742
+ console.error(colors.red('Failed to apply WSL npm prefix fix:'));
2743
+ if (fixResult.stderr)
2744
+ process.stderr.write(fixResult.stderr);
2745
+ return { success: false, fixed: false };
2746
+ }
2747
+ console.log(colors.green('✓ WSL npm prefix set to ~/.npm-global; PATH appended to ~/.bashrc'));
2748
+ result = await runOnce();
2749
+ if (result.success)
2750
+ return { success: true, fixed: true };
2751
+ combined = (result.output || '') + '\n' + (result.stderr || '');
2752
+ }
2753
+ // E404 on a scoped package has TWO causes, and npm gives the same error for
2754
+ // both (it masks a private-package 401 as 404):
2755
+ // (a) WSL's npm token is stale/missing (Windows is authed, WSL isn't), or
2756
+ // (b) genuine registry propagation lag for a just-published version.
2757
+ // Distinguish via `wsl npm whoami` and apply the matching fix.
2758
+ if (/E404|404 Not Found/.test(combined)) {
2759
+ const spec = extractWslInstallSpec(wslArgs);
2760
+ if (!(await isWslNpmAuthed())) {
2761
+ // (a) Auth: sync the Windows token into WSL, then retry.
2762
+ console.log(colors.yellow("WSL's npm isn't authenticated (stale/missing token) — syncing your Windows npm token to WSL..."));
2763
+ if (await syncNpmTokenToWsl() && await isWslNpmAuthed()) {
2764
+ console.log(colors.green('✓ Synced npm token to WSL'));
2765
+ result = await runOnce();
2766
+ if (result.success)
2767
+ return { success: true, fixed: true };
2768
+ }
2769
+ else {
2770
+ console.error(colors.yellow(' Could not authenticate WSL npm. Run `wsl npm login` (or sync your ~/.npmrc token) and retry.'));
2771
+ }
2772
+ }
2773
+ else if (spec) {
2774
+ // (b) Propagation: WSL is authed but the version isn't visible there yet.
2775
+ console.log(colors.yellow(`WSL npm can't see ${spec} yet — registry propagation lag (Windows already saw it). Waiting in WSL...`));
2776
+ if (await waitForNpmVersionInWsl(spec)) {
2777
+ result = await runOnce();
2778
+ if (result.success)
2779
+ return { success: true, fixed: true };
2780
+ }
2781
+ else {
2782
+ console.error(colors.yellow(` ${spec} still not visible to WSL's npm after waiting — try the WSL install again shortly.`));
2783
+ }
2784
+ }
2785
+ }
2786
+ return { success: false, fixed: false };
2472
2787
  }
2473
2788
  /** Diagnose common build/version failure patterns and print actionable hints.
2474
2789
  * Returns true if a diagnosis was printed. */
@@ -2562,12 +2877,14 @@ function diagnoseBuildFailure(errorText, cwd) {
2562
2877
  diagnosed = true;
2563
2878
  }
2564
2879
  // Pattern: TS6 made deprecated options (moduleResolution=node10, ...) hard errors,
2565
- // removed in TS7. (Safety net for when the auto ignoreDeprecations patch+retry didn't fire.)
2880
+ // removed in TS7. (Safety net for when the auto-migration patch+retry didn't fire
2881
+ // e.g. an unresolved chain or a non-writable base.)
2566
2882
  if (!diagnosed && /error TS510[17]\b/.test(errorText)) {
2567
- const v = errorText.match(/['"]ignoreDeprecations['"]\s*:\s*['"]([\d.]+)['"]/);
2568
- console.error(colors.yellow('\n Hint: TypeScript 6 flags long-deprecated options that TS7 will remove.'));
2569
- console.error(colors.yellow(` Bridge: add "ignoreDeprecations": "${v ? v[1] : '6.0'}" to compilerOptions (shared base in a workspace).`));
2570
- console.error(colors.yellow(' Real fix before TS7: migrate moduleResolution to "node16"/"nodenext"/"bundler".'));
2883
+ console.error(colors.yellow('\n Hint: TypeScript 6 flags long-deprecated options that TS7 removes.'));
2884
+ console.error(colors.yellow(' Fix (do NOT use "ignoreDeprecations" that only hides the error until TS7):'));
2885
+ console.error(colors.yellow(' • moduleResolution "node10"/"node"/"classic" "nodenext" (set module to "nodenext" too).'));
2886
+ console.error(colors.yellow(' target "es3" "es2022"; remove dead flags (importsNotUsedAsValues, out, …).'));
2887
+ console.error(colors.yellow(' Run npmglobalize -ts7-report for the full per-package list.'));
2571
2888
  diagnosed = true;
2572
2889
  }
2573
2890
  // Pattern: TypeScript compilation error
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.191",
3
+ "version": "1.0.192",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",