@bobfrankston/npmglobalize 1.0.193 → 1.0.195

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 (5) hide show
  1. package/README.md +13 -0
  2. package/cli.js +47 -3
  3. package/lib.d.ts +29 -1
  4. package/lib.js +225 -23
  5. package/package.json +1 -1
package/README.md CHANGED
@@ -454,6 +454,19 @@ Workspace mode is auto-detected when run from a root with `"private": true` and
454
454
  target. Fixes arborist "Cannot read properties of null"
455
455
  crashes caused by sibling file: deps with nested
456
456
  node_modules. Suggested automatically when the error hits.
457
+ -ts7-report, -deprecation-report
458
+ Report-only: scan this package and its file: deps for
459
+ compilerOptions removed in TypeScript 7 and list a migration
460
+ to-do. Writes nothing.
461
+ -tsfix, -ts7-fix
462
+ One-off utility, NOT part of the main flow: apply the TS7
463
+ tsconfig migration to a package (and its file: deps) and exit —
464
+ no build, no commit, no push, no publish. Intended as a
465
+ temporary tool for fixing a local/subdirectory tsconfig in
466
+ place. The normal release flow already applies the same
467
+ migration automatically when a build hits a TS7 deprecation
468
+ error, so day-to-day you never need this flag.
469
+ npmglobalize <path> -tsfix
457
470
  -show Show package.json dependency changes
458
471
  -package, -pkg Update package.json scripts to use npmglobalize (see below)
459
472
  -h, -help Show help
package/cli.js CHANGED
@@ -2,7 +2,7 @@
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, reportTs7Deprecations } from './lib.js';
5
+ import { globalize, globalizeWorkspace, installCleanupHandlers, readConfig, readPackageJson, readUserNpmConfig, writeConfig, writePackageJson, getBuildIssues, clearBuildIssues, ensureFileDepModules, buildProject, buildFileDepsTopologically, reportTs7Deprecations, fixTs7Deprecations } from './lib.js';
6
6
  import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { colors } from './colors.js';
@@ -56,6 +56,9 @@ Dependency Options:
56
56
  public visibility to already-published deps in the cascade.
57
57
  Use when a public consumer's deps weren't all marked public.
58
58
  -no-prescan, -nps Skip upfront dep-graph prescan
59
+ -force-build, -fb Rebuild file: deps even when their output looks up
60
+ to date (freshness check compares .ts/tsconfig
61
+ mtimes against emitted .js/.d.ts)
59
62
  -force-publish Republish dependencies even if version exists
60
63
  -fix Run npm audit fix after transformation
61
64
  -no-use-paths, -nup Declare this package standalone — file: deps shouldn't
@@ -121,6 +124,13 @@ Other Options:
121
124
  compilerOptions removed in TypeScript 7 (deprecated
122
125
  moduleResolution, target es3, deprecated flags). Lists a
123
126
  migration to-do list and exits. Writes nothing.
127
+ -tsfix, -ts7-fix
128
+ One-off utility, not the main flow: apply the TS7 tsconfig
129
+ migration to this package and its file: deps, then exit.
130
+ Only tsconfig files change — no build, no commit, no push,
131
+ no publish. The release flow auto-applies the same migration
132
+ on a TS7 build error; use this only for a temporary in-place
133
+ fix of a local directory: npmglobalize <path> -tsfix
124
134
  -show Show package.json dependency changes
125
135
  -package, -pkg Update package.json scripts to use npmglobalize
126
136
  -h, -help Show this help
@@ -161,6 +171,7 @@ function parseArgs(args) {
161
171
  version: false,
162
172
  error: '',
163
173
  ts7Report: false,
174
+ ts7Fix: false,
164
175
  explicitKeys: new Set()
165
176
  };
166
177
  const unrecognized = [];
@@ -182,6 +193,10 @@ function parseArgs(args) {
182
193
  case '-deprecation-report':
183
194
  options.ts7Report = true;
184
195
  break;
196
+ case '-tsfix':
197
+ case '-ts7-fix':
198
+ options.ts7Fix = true;
199
+ break;
185
200
  case '-patch':
186
201
  options.bump = 'patch';
187
202
  break;
@@ -233,6 +248,10 @@ function parseArgs(args) {
233
248
  case '-force':
234
249
  options.force = true;
235
250
  break;
251
+ case '-force-build':
252
+ case '-fb':
253
+ options.forceBuild = true;
254
+ break;
236
255
  case '-files':
237
256
  options.files = true;
238
257
  options.explicitKeys.add('files');
@@ -515,19 +534,44 @@ export async function main() {
515
534
  printTs7Report(cwd);
516
535
  process.exit(0);
517
536
  }
537
+ // Fix-only TS7 tsconfig migration (no build / commit / push / publish).
538
+ if (cliOptions.ts7Fix) {
539
+ console.log('');
540
+ console.log(colors.accent('━━━ TypeScript 7 tsconfig fix ━━━'));
541
+ const { changedDirs, remaining } = fixTs7Deprecations(cwd);
542
+ if (changedDirs.length === 0) {
543
+ console.log(colors.success(' ✓ Nothing to fix — no removed-in-TS7 compilerOptions in writable tsconfigs.'));
544
+ }
545
+ else {
546
+ console.log(colors.success(` ✓ Patched tsconfig(s) in ${changedDirs.length} package(s):`));
547
+ for (const dir of changedDirs)
548
+ console.log(` ${dir}`);
549
+ console.log(colors.muted(' Rebuild to validate — NodeNext resolution may surface real import errors the old resolver hid.'));
550
+ }
551
+ if (remaining.length > 0) {
552
+ console.log(colors.warn(` ${remaining.length} item(s) could not be auto-fixed:`));
553
+ for (const f of remaining) {
554
+ console.log(colors.warn(` • ${f.name}: ${f.option}=${JSON.stringify(f.value)} (in ${f.definedIn})`));
555
+ console.log(colors.muted(` → ${f.action}`));
556
+ }
557
+ }
558
+ console.log('');
559
+ printBuildSummary();
560
+ process.exit(remaining.length > 0 ? 1 : 0);
561
+ }
518
562
  // Build file: deps topologically, then the target itself.
519
563
  // Ensures consumers' tsc sees up-to-date `.d.ts` from sibling checkouts
520
564
  // whose source has changed since their last build.
521
565
  if (!cliOptions.cleanup) {
522
566
  await ensureFileDepModules(cwd, !!cliOptions.verbose);
523
- const depsOk = await buildFileDepsTopologically(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force });
567
+ const depsOk = await buildFileDepsTopologically(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force, forceBuild: !!cliOptions.forceBuild });
524
568
  if (!depsOk && !cliOptions.force) {
525
569
  printBuildSummary();
526
570
  process.exit(1);
527
571
  }
528
572
  if (!depsOk)
529
573
  console.log(colors.warn('Continuing with --force despite dep build failure...'));
530
- const targetOk = await buildProject(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force });
574
+ const targetOk = await buildProject(cwd, { verbose: !!cliOptions.verbose, force: !!cliOptions.force, forceBuild: !!cliOptions.forceBuild });
531
575
  if (!targetOk) {
532
576
  if (!cliOptions.force) {
533
577
  printBuildSummary();
package/lib.d.ts CHANGED
@@ -108,6 +108,8 @@ export interface GlobalizeOptions {
108
108
  usePaths?: boolean;
109
109
  /** Local install only — skip transform/publish, just npm install -g . */
110
110
  local?: boolean;
111
+ /** Build every file: dep even when outputs look up to date (skips the freshness check) */
112
+ forceBuild?: boolean;
111
113
  /** Freeze node_modules: replace symlinks/junctions with real copies for network share use */
112
114
  freeze?: boolean;
113
115
  /** Before `npm pack`, delete `node_modules/` inside each `file:` dep target.
@@ -311,13 +313,26 @@ export declare function missingDeps(pkgDir: string, pkg: any): string[];
311
313
  * re-run" (partial-sync) cases. Also covers `cwd` itself on the first call.
312
314
  * Cycle-safe via the shared `visited` set. */
313
315
  export declare function ensureFileDepModules(cwd: string, verbose?: boolean, visited?: Set<string>): Promise<void>;
316
+ /** Cheap freshness check so the build cascade can skip packages whose output is
317
+ * already current. Returns true only when provably up to date:
318
+ * - outDir projects: newest source (.ts/.tsx/.mts/.cts + tsconfig*.json) must
319
+ * not be newer than the newest file under outDir.
320
+ * - side-by-side projects: every source must have an emitted sibling
321
+ * (.js/.jsx/.mjs/.cjs or declaration) at least as new.
322
+ * Conservative: project references, allowJs, missing outputs, or unreadable
323
+ * tsconfig all report stale (→ build). package.json mtime is deliberately
324
+ * ignored — npmglobalize itself rewrites it around every publish, which would
325
+ * otherwise force a rebuild on every run. */
326
+ export declare function isBuildUpToDate(cwd: string): boolean;
314
327
  /** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
315
328
  * TypeScript project lacks a build script, run `npm run build`, record
316
329
  * failures. Returns true if build succeeded (or was skipped because no
317
- * tsconfig / noEmit / no build script after declining the prompt). */
330
+ * tsconfig / noEmit / no build script after declining the prompt / output
331
+ * already up to date). Pass `forceBuild` to skip the freshness check. */
318
332
  export declare function buildProject(cwd: string, opts?: {
319
333
  verbose?: boolean;
320
334
  force?: boolean;
335
+ forceBuild?: boolean;
321
336
  }): Promise<boolean>;
322
337
  /** Walk `file:` deps depth-first (deps before consumers) and build each one
323
338
  * that has a tsconfig. Mirrors `ensureFileDepModules`'s traversal but invokes
@@ -327,6 +342,7 @@ export declare function buildProject(cwd: string, opts?: {
327
342
  export declare function buildFileDepsTopologically(cwd: string, opts?: {
328
343
  verbose?: boolean;
329
344
  force?: boolean;
345
+ forceBuild?: boolean;
330
346
  }, visited?: Set<string>): Promise<boolean>;
331
347
  /** A single TS7-readiness problem found in a package's effective tsconfig. */
332
348
  export interface Ts7Finding {
@@ -347,6 +363,18 @@ export interface Ts7Finding {
347
363
  * The same migration `migrateTsconfigDeprecations` applies automatically on a build
348
364
  * failure; this report is the read-only, whole-graph inventory. */
349
365
  export declare function reportTs7Deprecations(cwd: string): Ts7Finding[];
366
+ /** Standalone fix mode (`-tsfix`): apply `migrateTsconfigDeprecations` to `cwd`
367
+ * and every package reachable through its `file:` dep graph — the write
368
+ * counterpart of `reportTs7Deprecations`. No git, no version bump, no publish;
369
+ * only tsconfig files change. After migrating, any `ignoreDeprecations` whose
370
+ * package has nothing deprecated left is deleted too (kept, it would silence
371
+ * the next real TS7-removal error). Returns the dirs whose tsconfigs changed
372
+ * plus the findings that remain (e.g. options inherited from read-only
373
+ * node_modules bases). */
374
+ export declare function fixTs7Deprecations(cwd: string): {
375
+ changedDirs: string[];
376
+ remaining: Ts7Finding[];
377
+ };
350
378
  /** Ensure the workspace-root `node_modules/` is in sync with every member's
351
379
  * declared deps. Workspaces hoist deps to the root, so a dep added to any
352
380
  * member `package.json` without a follow-up `npm install` at the root leaves
package/lib.js CHANGED
@@ -1720,6 +1720,41 @@ export function printPnpmSuggestionSummary() {
1720
1720
  console.error(l);
1721
1721
  console.error('');
1722
1722
  }
1723
+ /** Run `npm install` in `dir`; on failure, offer to delete `node_modules/`
1724
+ * and `package-lock.json` and retry once — the standard fix for arborist's
1725
+ * null-`package` crash on stale lockfile / junction state — before falling
1726
+ * back to the pnpm suggestion banner. Returns true if install succeeded.
1727
+ * `rmSync` unlinks junctions without descending, so file: sibling targets
1728
+ * are never touched. */
1729
+ async function npmInstallWithCleanRetry(dir, verbose, context) {
1730
+ let r = await runCommandAsync('npm', ['install'], { cwd: dir, silent: !verbose });
1731
+ if (r.success)
1732
+ return true;
1733
+ console.error(colors.red(` ✗ npm install failed in ${dir}`));
1734
+ if (r.stderr)
1735
+ console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
1736
+ if (await confirm(` Delete node_modules/ and package-lock.json in ${dir} and retry npm install?`, true)) {
1737
+ try {
1738
+ fs.rmSync(path.join(dir, 'node_modules'), { recursive: true, force: true });
1739
+ }
1740
+ catch { /* best-effort */ }
1741
+ try {
1742
+ fs.rmSync(path.join(dir, 'package-lock.json'), { force: true });
1743
+ }
1744
+ catch { /* best-effort */ }
1745
+ console.log(colors.yellow(`↻ clean reinstall in ${dir}`));
1746
+ r = await runCommandAsync('npm', ['install'], { cwd: dir, silent: !verbose });
1747
+ if (r.success) {
1748
+ console.log(colors.green(` ✓ clean reinstall succeeded in ${dir}`));
1749
+ return true;
1750
+ }
1751
+ console.error(colors.red(` ✗ npm install still failing in ${dir} after clean reinstall`));
1752
+ if (r.stderr)
1753
+ console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
1754
+ }
1755
+ await suggestPnpmOnInstallFailure(context);
1756
+ return false;
1757
+ }
1723
1758
  /** Wait for a package version to appear on the npm registry.
1724
1759
  * First-time publishes (brand-new package name) take much longer to
1725
1760
  * propagate than version bumps — npm has no cached metadata to update,
@@ -1877,13 +1912,7 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
1877
1912
  catch { /* best-effort */ }
1878
1913
  }
1879
1914
  console.log(colors.yellow(`↻ installing node_modules in ${pkg?.name || abs} (${formatMissingReason(cwdMissing)})`));
1880
- const r = await runCommandAsync('npm', ['install'], { cwd: abs, silent: !verbose });
1881
- if (!r.success) {
1882
- console.error(colors.red(` ✗ npm install failed in ${abs}`));
1883
- if (r.stderr)
1884
- console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
1885
- await suggestPnpmOnInstallFailure(`ensureFileDepModules (cwd): ${abs}`);
1886
- }
1915
+ await npmInstallWithCleanRetry(abs, verbose, `ensureFileDepModules (cwd): ${abs}`);
1887
1916
  }
1888
1917
  for (const key of ['dependencies', 'devDependencies']) {
1889
1918
  const deps = pkg?.[key];
@@ -1915,13 +1944,7 @@ export async function ensureFileDepModules(cwd, verbose = false, visited = new S
1915
1944
  catch { /* best-effort */ }
1916
1945
  }
1917
1946
  console.log(colors.yellow(`↻ restoring node_modules in ${name} (${target}) (${formatMissingReason(targetMissing)})`));
1918
- const r = await runCommandAsync('npm', ['install'], { cwd: target, silent: !verbose });
1919
- if (!r.success) {
1920
- console.error(colors.red(` ✗ npm install failed in ${target}`));
1921
- if (r.stderr)
1922
- console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
1923
- await suggestPnpmOnInstallFailure(`ensureFileDepModules (file: dep ${name}): ${target}`);
1924
- }
1947
+ await npmInstallWithCleanRetry(target, verbose, `ensureFileDepModules (file: dep ${name}): ${target}`);
1925
1948
  }
1926
1949
  await ensureFileDepModules(target, verbose, visited);
1927
1950
  }
@@ -2244,10 +2267,144 @@ function migrateTsconfigDeprecations(cwd) {
2244
2267
  }
2245
2268
  return changed;
2246
2269
  }
2270
+ /** Newest mtime of any regular file under `dir` (recursive). Symlinks/junctions
2271
+ * are not followed — the file: dep webs here link back into sibling checkouts. */
2272
+ function newestMtimeUnder(dir) {
2273
+ let newest = 0;
2274
+ let entries;
2275
+ try {
2276
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2277
+ }
2278
+ catch {
2279
+ return 0;
2280
+ }
2281
+ for (const e of entries) {
2282
+ if (e.isSymbolicLink())
2283
+ continue;
2284
+ const full = path.join(dir, e.name);
2285
+ if (e.isDirectory()) {
2286
+ newest = Math.max(newest, newestMtimeUnder(full));
2287
+ }
2288
+ else if (e.isFile()) {
2289
+ try {
2290
+ newest = Math.max(newest, fs.statSync(full).mtimeMs);
2291
+ }
2292
+ catch { /* ignore */ }
2293
+ }
2294
+ }
2295
+ return newest;
2296
+ }
2297
+ /** Cheap freshness check so the build cascade can skip packages whose output is
2298
+ * already current. Returns true only when provably up to date:
2299
+ * - outDir projects: newest source (.ts/.tsx/.mts/.cts + tsconfig*.json) must
2300
+ * not be newer than the newest file under outDir.
2301
+ * - side-by-side projects: every source must have an emitted sibling
2302
+ * (.js/.jsx/.mjs/.cjs or declaration) at least as new.
2303
+ * Conservative: project references, allowJs, missing outputs, or unreadable
2304
+ * tsconfig all report stale (→ build). package.json mtime is deliberately
2305
+ * ignored — npmglobalize itself rewrites it around every publish, which would
2306
+ * otherwise force a rebuild on every run. */
2307
+ export function isBuildUpToDate(cwd) {
2308
+ let tsconfig;
2309
+ try {
2310
+ tsconfig = JSON5.parse(fs.readFileSync(path.join(cwd, 'tsconfig.json'), 'utf-8'));
2311
+ }
2312
+ catch {
2313
+ return false;
2314
+ }
2315
+ const co = tsconfig.compilerOptions || {};
2316
+ if (tsconfig.references || co.allowJs || co.checkJs)
2317
+ return false;
2318
+ const outDir = typeof co.outDir === 'string' ? path.resolve(cwd, co.outDir) : null;
2319
+ // tsconfig "exclude" dirs aren't compiled, so their .ts files never get
2320
+ // outputs — don't count them as sources. Match on the literal path prefix
2321
+ // (glob tails like "tests/**" reduce to "tests").
2322
+ const excludes = (Array.isArray(tsconfig.exclude) ? tsconfig.exclude : [])
2323
+ .filter((x) => typeof x === 'string')
2324
+ .map((x) => x.replace(/^\.\//, '').replace(/[\\/]?\*.*$/, '').replace(/[\\/]+$/, '').toLowerCase())
2325
+ .filter((x) => x.length > 0 && !x.includes('*'));
2326
+ const sources = [];
2327
+ const collect = (dir, rel) => {
2328
+ let entries;
2329
+ try {
2330
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2331
+ }
2332
+ catch {
2333
+ return;
2334
+ }
2335
+ for (const e of entries) {
2336
+ if (e.isSymbolicLink())
2337
+ continue;
2338
+ const full = path.join(dir, e.name);
2339
+ const relPath = rel ? `${rel}/${e.name.toLowerCase()}` : e.name.toLowerCase();
2340
+ if (e.isDirectory()) {
2341
+ const lower = e.name.toLowerCase();
2342
+ if (lower === 'node_modules' || lower === 'prev' || e.name.startsWith('.'))
2343
+ continue;
2344
+ if (outDir && path.resolve(full) === outDir)
2345
+ continue;
2346
+ if (excludes.includes(relPath))
2347
+ continue;
2348
+ collect(full, relPath);
2349
+ }
2350
+ else if (/\.(ts|tsx|mts|cts)$/.test(e.name) && !/\.d\.(ts|mts|cts)$/.test(e.name)) {
2351
+ if (excludes.includes(relPath))
2352
+ continue;
2353
+ sources.push(full);
2354
+ }
2355
+ }
2356
+ };
2357
+ collect(cwd, '');
2358
+ if (sources.length === 0)
2359
+ return true;
2360
+ const mtime = (p) => {
2361
+ try {
2362
+ return fs.statSync(p).mtimeMs;
2363
+ }
2364
+ catch {
2365
+ return -1;
2366
+ }
2367
+ };
2368
+ // Any tsconfig*.json edit (own or extended base in the package) invalidates the build.
2369
+ let configTime = 0;
2370
+ try {
2371
+ for (const name of fs.readdirSync(cwd)) {
2372
+ if (/^tsconfig.*\.json$/.test(name))
2373
+ configTime = Math.max(configTime, mtime(path.join(cwd, name)));
2374
+ }
2375
+ }
2376
+ catch { /* ignore */ }
2377
+ if (outDir) {
2378
+ const newestOut = newestMtimeUnder(outDir);
2379
+ if (newestOut === 0)
2380
+ return false;
2381
+ const newestSrc = Math.max(configTime, ...sources.map(mtime));
2382
+ return newestSrc <= newestOut;
2383
+ }
2384
+ // Side-by-side emit: each source needs an output sibling at least as new.
2385
+ let newestOut = 0;
2386
+ for (const src of sources) {
2387
+ const m = src.match(/\.(ts|tsx|mts|cts)$/);
2388
+ const base = src.slice(0, -m[0].length);
2389
+ const candidates = m[1] === 'mts' ? ['.mjs', '.d.mts']
2390
+ : m[1] === 'cts' ? ['.cjs', '.d.cts']
2391
+ : ['.js', '.jsx', '.d.ts'];
2392
+ let outTime = -1;
2393
+ for (const ext of candidates)
2394
+ outTime = Math.max(outTime, mtime(base + ext));
2395
+ if (outTime < 0)
2396
+ return false;
2397
+ if (mtime(src) > outTime)
2398
+ return false;
2399
+ newestOut = Math.max(newestOut, outTime);
2400
+ }
2401
+ return configTime <= newestOut;
2402
+ }
2247
2403
  /** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
2248
2404
  * TypeScript project lacks a build script, run `npm run build`, record
2249
2405
  * failures. Returns true if build succeeded (or was skipped because no
2250
- * tsconfig / noEmit / no build script after declining the prompt). */
2406
+ * tsconfig / noEmit / no build script after declining the prompt / output
2407
+ * already up to date). Pass `forceBuild` to skip the freshness check. */
2251
2408
  export async function buildProject(cwd, opts = {}) {
2252
2409
  let shouldBuild = false;
2253
2410
  try {
@@ -2277,6 +2434,10 @@ export async function buildProject(cwd, opts = {}) {
2277
2434
  }
2278
2435
  }
2279
2436
  ensureTsconfigNodeTypes(cwd);
2437
+ if (!opts.forceBuild && isBuildUpToDate(cwd)) {
2438
+ console.log(colors.dim(`– Build up to date (${pkg.name || path.basename(cwd)})`));
2439
+ return true;
2440
+ }
2280
2441
  console.log(`Building ${pkg.name || cwd}...`);
2281
2442
  let buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
2282
2443
  if (!buildResult.success) {
@@ -2477,6 +2638,53 @@ export function reportTs7Deprecations(cwd) {
2477
2638
  }
2478
2639
  return findings;
2479
2640
  }
2641
+ /** Standalone fix mode (`-tsfix`): apply `migrateTsconfigDeprecations` to `cwd`
2642
+ * and every package reachable through its `file:` dep graph — the write
2643
+ * counterpart of `reportTs7Deprecations`. No git, no version bump, no publish;
2644
+ * only tsconfig files change. After migrating, any `ignoreDeprecations` whose
2645
+ * package has nothing deprecated left is deleted too (kept, it would silence
2646
+ * the next real TS7-removal error). Returns the dirs whose tsconfigs changed
2647
+ * plus the findings that remain (e.g. options inherited from read-only
2648
+ * node_modules bases). */
2649
+ export function fixTs7Deprecations(cwd) {
2650
+ const changedDirs = [];
2651
+ for (const dir of collectFileDepDirs(cwd)) {
2652
+ if (migrateTsconfigDeprecations(dir))
2653
+ changedDirs.push(dir);
2654
+ }
2655
+ // Drop ignoreDeprecations wherever nothing deprecated remains in that package.
2656
+ let remaining = reportTs7Deprecations(cwd);
2657
+ const igdDirs = new Set(remaining.filter(f => f.option === 'ignoreDeprecations').map(f => f.dir));
2658
+ let removedIgd = false;
2659
+ for (const dir of igdDirs) {
2660
+ if (remaining.some(f => f.dir === dir && f.option !== 'ignoreDeprecations'))
2661
+ continue;
2662
+ const { chain } = resolveTsconfigChain(path.join(dir, 'tsconfig.json'));
2663
+ const igd = effectiveCompilerOption(chain, 'ignoreDeprecations');
2664
+ if (!igd)
2665
+ continue;
2666
+ const def = chain.find(c => c.path === igd.definedIn);
2667
+ if (!def?.writable)
2668
+ continue;
2669
+ try {
2670
+ const text = fs.readFileSync(def.path, 'utf-8');
2671
+ const patched = removeCompilerOptionLine(text, 'ignoreDeprecations');
2672
+ if (patched != null && patched !== text) {
2673
+ fs.writeFileSync(def.path, patched);
2674
+ removedIgd = true;
2675
+ if (!changedDirs.includes(dir))
2676
+ changedDirs.push(dir);
2677
+ console.log(colors.cyan(` Removed "ignoreDeprecations" from ${path.relative(dir, def.path) || 'tsconfig.json'} (${path.basename(dir)})`));
2678
+ }
2679
+ }
2680
+ catch (error) {
2681
+ console.error(colors.yellow(` Could not remove ignoreDeprecations (${def.path}): ${error.message}`));
2682
+ }
2683
+ }
2684
+ if (removedIgd)
2685
+ remaining = reportTs7Deprecations(cwd);
2686
+ return { changedDirs, remaining };
2687
+ }
2480
2688
  /** Ensure the workspace-root `node_modules/` is in sync with every member's
2481
2689
  * declared deps. Workspaces hoist deps to the root, so a dep added to any
2482
2690
  * member `package.json` without a follow-up `npm install` at the root leaves
@@ -2502,13 +2710,7 @@ export async function ensureWorkspaceDepModules(rootDir, members, verbose = fals
2502
2710
  return;
2503
2711
  const list = [...allMissing];
2504
2712
  console.log(colors.yellow(`↻ installing workspace node_modules in ${rootPkg?.name || path.basename(root)} (${formatMissingReason(list)})`));
2505
- const r = await runCommandAsync('npm', ['install'], { cwd: root, silent: !verbose });
2506
- if (!r.success) {
2507
- console.error(colors.red(` ✗ npm install failed in ${root}`));
2508
- if (r.stderr)
2509
- console.error(colors.dim(r.stderr.split('\n').slice(0, 5).join('\n')));
2510
- await suggestPnpmOnInstallFailure(`ensureWorkspaceDepModules: ${root}`);
2511
- }
2713
+ await npmInstallWithCleanRetry(root, verbose, `ensureWorkspaceDepModules: ${root}`);
2512
2714
  }
2513
2715
  /** Extract the lines a human needs from a failed npm command's output: the
2514
2716
  * `npm error` lines minus stack frames and brace-only object-dump fragments,
@@ -2704,7 +2906,7 @@ async function installGlobalWithRetry(pkgSpec, cwd, isNewPackage = false, maxRet
2704
2906
  const combined = `${result.output}\n${result.stderr}`;
2705
2907
  fullLog += `===== attempt ${attempt + 1}: npm install -g ${pkgSpec} =====\n${combined}\n`;
2706
2908
  if (result.success) {
2707
- const added = combined.match(/^added \d+ packages?.*$/m);
2909
+ const added = combined.match(/^(?:added|changed|removed) \d+ packages?.*$/m);
2708
2910
  if (added)
2709
2911
  console.log(colors.dim(` ${added[0]}`));
2710
2912
  warnSkippedScripts(combined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.193",
3
+ "version": "1.0.195",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",