@bobfrankston/npmglobalize 1.0.203 → 1.0.205

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 (2) hide show
  1. package/lib.js +145 -6
  2. package/package.json +1 -1
package/lib.js CHANGED
@@ -2383,23 +2383,43 @@ function migrateTsconfigDeprecations(cwd, snapshot) {
2383
2383
  };
2384
2384
  const note = (msg) => console.log(colors.cyan(` ${msg}`));
2385
2385
  let changed = false;
2386
- // moduleResolution: node/node10/classic → nodenext (+ align module).
2386
+ // moduleResolution: node/node10/classic → NodeNext (+ align module).
2387
2387
  const mr = effectiveCompilerOption(chain, 'moduleResolution');
2388
2388
  if (mr && typeof mr.value === 'string' && DEPRECATED_MODULE_RESOLUTION.has(mr.value.toLowerCase())) {
2389
2389
  const tgt = targetFor(mr.definedIn);
2390
- if (tgt && edit(tgt.path, t => upsertCompilerOption(t, 'moduleResolution', '"nodenext"'))) {
2390
+ if (tgt && edit(tgt.path, t => upsertCompilerOption(t, 'moduleResolution', `"${CANONICAL_NODENEXT}"`))) {
2391
2391
  changed = true;
2392
- note(`Migrated moduleResolution "${mr.value}" → "nodenext" in ${path.relative(cwd, tgt.path) || 'tsconfig.json'}`);
2392
+ note(`Migrated moduleResolution "${mr.value}" → "${CANONICAL_NODENEXT}" in ${path.relative(cwd, tgt.path) || 'tsconfig.json'}`);
2393
2393
  // nodenext resolution requires a matching module setting.
2394
2394
  const mod = effectiveCompilerOption(chain, 'module');
2395
2395
  const modOk = mod && typeof mod.value === 'string' && /^(node16|nodenext|preserve)$/i.test(mod.value);
2396
2396
  if (!modOk) {
2397
2397
  const modTgt = mod ? targetFor(mod.definedIn) : mostLeafWritable;
2398
- if (modTgt && edit(modTgt.path, t => upsertCompilerOption(t, 'module', '"nodenext"'))) {
2399
- note(`Aligned module → "nodenext" (required by moduleResolution "nodenext")`);
2398
+ if (modTgt && edit(modTgt.path, t => upsertCompilerOption(t, 'module', `"${CANONICAL_NODENEXT}"`))) {
2399
+ note(`Aligned module → "${CANONICAL_NODENEXT}" (required by moduleResolution "${CANONICAL_NODENEXT}")`);
2400
2400
  }
2401
2401
  }
2402
- recordBuildIssue(name, 'warning', `Migrated moduleResolution "${mr.value}" → "nodenext". Verify relative imports resolve (NodeNext requires explicit file extensions on relative specifiers).`);
2402
+ recordBuildIssue(name, 'warning', `Migrated moduleResolution "${mr.value}" → "${CANONICAL_NODENEXT}". Verify relative imports resolve (NodeNext requires explicit file extensions on relative specifiers).`);
2403
+ }
2404
+ }
2405
+ // Canonical casing for values we write ourselves. TypeScript reads these
2406
+ // case-insensitively, but the JSON schema behind editor linting (webhint,
2407
+ // "Microsoft Edge Tools") only accepts the documented spelling, so an
2408
+ // all-lowercase "nodenext" we wrote earlier shows up as an error in the
2409
+ // editor even though the build is fine.
2410
+ for (const key of ['module', 'moduleResolution']) {
2411
+ const opt = effectiveCompilerOption(chain, key);
2412
+ if (!opt || typeof opt.value !== 'string')
2413
+ continue;
2414
+ const canonical = CANONICAL_CASING[opt.value.toLowerCase()];
2415
+ if (!canonical || canonical === opt.value)
2416
+ continue;
2417
+ const def = chain.find(c => c.path === opt.definedIn);
2418
+ if (!def?.writable)
2419
+ continue; // an override would be noise, not a fix
2420
+ if (edit(def.path, t => upsertCompilerOption(t, key, `"${canonical}"`))) {
2421
+ changed = true;
2422
+ note(`Normalized ${key} "${opt.value}" → "${canonical}" in ${path.relative(cwd, def.path) || 'tsconfig.json'}`);
2403
2423
  }
2404
2424
  }
2405
2425
  // target: es3 → es2022 (es3 is removed in TS7).
@@ -2429,6 +2449,103 @@ function migrateTsconfigDeprecations(cwd, snapshot) {
2429
2449
  }
2430
2450
  return changed;
2431
2451
  }
2452
+ /** Ordering of ES language levels as they appear in `target`/`lib`. Anything
2453
+ * unknown ranks 0 so it never looks newer than what the build asked for. */
2454
+ const ES_LEVEL_RANK = {
2455
+ es3: 3, es5: 5, es6: 6, es2015: 6, es7: 7, es2016: 7, es2017: 8, es2018: 9,
2456
+ es2019: 10, es2020: 11, es2021: 12, es2022: 13, es2023: 14, es2024: 15,
2457
+ es2025: 16, esnext: 99,
2458
+ };
2459
+ const esLevelRank = (value) => ES_LEVEL_RANK[value.toLowerCase().replace(/\.full$/, '')] ?? 0;
2460
+ /** tsc says "Try changing the 'lib' compiler option to 'es2023' or later"
2461
+ * (TS2550/TS2583/TS2584) when the code uses a standard-library API newer than
2462
+ * the configured level. That isn't a deprecation — it's a tsconfig the language
2463
+ * moved past — so raise the level and let the build try again, rather than
2464
+ * leaving the user to decode `replaceAll` and `toSorted` errors by hand. The
2465
+ * level asked for only decides *whether* to raise; what we write is
2466
+ * `RAISED_ES_LEVEL`, so the next new method used doesn't start this over.
2467
+ *
2468
+ * We raise `target`: its implied `lib` keeps the DOM declarations a bare
2469
+ * `lib: ["es2023"]` would silently drop. An explicit `lib` overrides `target`
2470
+ * though, so when one is set we raise its `es*` entry in place and keep the
2471
+ * rest (dom, webworker, …). Reads the required level from `buildOutput`;
2472
+ * returns true if it changed any file. */
2473
+ function raiseTsconfigLibLevel(cwd, buildOutput, snapshot) {
2474
+ let required = '';
2475
+ for (const m of buildOutput.matchAll(/'lib' compiler option to '([A-Za-z0-9.]+)'/g)) {
2476
+ if (esLevelRank(m[1]) > esLevelRank(required))
2477
+ required = m[1].toLowerCase();
2478
+ }
2479
+ if (!required)
2480
+ return false;
2481
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
2482
+ if (!fs.existsSync(tsconfigPath))
2483
+ return false;
2484
+ const { chain } = resolveTsconfigChain(tsconfigPath);
2485
+ if (!chain.length)
2486
+ return false;
2487
+ const mostLeafWritable = chain.find(c => c.writable);
2488
+ if (!mostLeafWritable)
2489
+ return false;
2490
+ const name = (() => { try {
2491
+ return readPackageJson(cwd).name;
2492
+ }
2493
+ catch {
2494
+ return null;
2495
+ } })() || path.basename(cwd);
2496
+ const edit = (filePath, fn) => {
2497
+ let text;
2498
+ try {
2499
+ text = fs.readFileSync(filePath, 'utf-8');
2500
+ }
2501
+ catch {
2502
+ return false;
2503
+ }
2504
+ const patched = fn(text);
2505
+ if (patched == null || patched === text)
2506
+ return false;
2507
+ try {
2508
+ fs.writeFileSync(filePath, patched);
2509
+ if (snapshot && !snapshot.has(filePath))
2510
+ snapshot.set(filePath, text);
2511
+ return true;
2512
+ }
2513
+ catch (error) {
2514
+ console.error(colors.yellow(` Could not write tsconfig patch (${filePath}): ${error.message}`));
2515
+ return false;
2516
+ }
2517
+ };
2518
+ /** The file to edit for an option: where it's defined if that's writable,
2519
+ * else the most-leaf writable config, where an override wins. */
2520
+ const targetFor = (definedIn) => {
2521
+ const def = definedIn ? chain.find(c => c.path === definedIn) : null;
2522
+ return def?.writable ? def : mostLeafWritable;
2523
+ };
2524
+ // An explicit `lib` beats `target`, so it's the one that has to move.
2525
+ const lib = effectiveCompilerOption(chain, 'lib');
2526
+ if (lib && Array.isArray(lib.value)) {
2527
+ const entries = lib.value.filter((e) => typeof e === 'string');
2528
+ if (entries.some(e => esLevelRank(e) >= esLevelRank(required)))
2529
+ return false;
2530
+ const raised = [RAISED_ES_LEVEL, ...entries.filter(e => esLevelRank(e) === 0)];
2531
+ const tgt = targetFor(lib.definedIn);
2532
+ if (!edit(tgt.path, t => upsertCompilerOption(t, 'lib', JSON.stringify(raised).replace(/","/g, '", "'))))
2533
+ return false;
2534
+ console.log(colors.cyan(` Raised lib [${entries.join(', ')}] → [${raised.join(', ')}] in ${path.relative(cwd, tgt.path) || 'tsconfig.json'} (build needs ${required})`));
2535
+ recordBuildIssue(name, 'warning', `Raised "lib" to ${RAISED_ES_LEVEL} — the code uses standard-library APIs newer than the tsconfig allowed (needs ${required}).`);
2536
+ return true;
2537
+ }
2538
+ const tg = effectiveCompilerOption(chain, 'target');
2539
+ const current = tg && typeof tg.value === 'string' ? tg.value : 'es5'; // tsc's own default
2540
+ if (esLevelRank(current) >= esLevelRank(required))
2541
+ return false;
2542
+ const tgt = targetFor(tg?.definedIn);
2543
+ if (!edit(tgt.path, t => upsertCompilerOption(t, 'target', `"${RAISED_ES_LEVEL}"`)))
2544
+ return false;
2545
+ console.log(colors.cyan(` Raised target "${current}" → "${RAISED_ES_LEVEL}" in ${path.relative(cwd, tgt.path) || 'tsconfig.json'} (build needs ${required})`));
2546
+ recordBuildIssue(name, 'warning', `Raised "target" ${current} → ${RAISED_ES_LEVEL} — the code uses standard-library APIs newer than the tsconfig allowed (needs ${required}).`);
2547
+ return true;
2548
+ }
2432
2549
  /** Write back the texts captured in a `migrateTsconfigDeprecations` snapshot
2433
2550
  * (path → text). Used to undo a migration whose build turned out worse, and to
2434
2551
  * re-apply it afterwards. Returns false if any write failed — the tree is then
@@ -3052,6 +3169,16 @@ export async function buildProject(cwd, opts = {}) {
3052
3169
  buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
3053
3170
  }
3054
3171
  }
3172
+ // "Try changing the 'lib' compiler option to 'es2023' or later" — a tsconfig
3173
+ // pinned below the standard-library APIs the code actually uses. Raise it and
3174
+ // rebuild. Looped because one level can uncover the need for a higher one,
3175
+ // and bounded because each pass must raise the level to run again.
3176
+ for (let pass = 0; pass < 3 && !buildResult.success; pass++) {
3177
+ const out = (buildResult.stderr || '') + (buildResult.output || '');
3178
+ if (!raiseTsconfigLibLevel(cwd, out, tsconfigSnapshot))
3179
+ break;
3180
+ buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
3181
+ }
3055
3182
  if (!buildResult.success && migratedUpFront && tsconfigSnapshot.size) {
3056
3183
  // The up-front migration is a semantic change: `nodenext` resolution can
3057
3184
  // surface real errors (missing file extensions on relative imports) that
@@ -3143,6 +3270,18 @@ export async function buildFileDepsTopologically(cwd, opts = {}, visited = new S
3143
3270
  }
3144
3271
  /** `moduleResolution` values TypeScript 6 deprecated and TypeScript 7 removes. */
3145
3272
  const DEPRECATED_MODULE_RESOLUTION = new Set(['node', 'node10', 'classic']);
3273
+ /** Spelling the tsconfig JSON schema — and so the editor linting built on it,
3274
+ * e.g. webhint via "Microsoft Edge Tools" — expects for the values we write.
3275
+ * TypeScript reads them case-insensitively; the schema does not. Deliberately
3276
+ * limited to the values npmglobalize writes itself: re-casing whatever the user
3277
+ * hand-typed would be churn, not a fix. */
3278
+ const CANONICAL_NODENEXT = 'NodeNext';
3279
+ const CANONICAL_CASING = { nodenext: CANONICAL_NODENEXT, node16: 'Node16' };
3280
+ /** Level we raise a too-old `target`/`lib` to. These are Node packages built and
3281
+ * run on the current Node, so tracking the newest language level costs nothing
3282
+ * and saves a re-raise every time the code picks up a newer standard-library
3283
+ * method. */
3284
+ const RAISED_ES_LEVEL = 'ESNext';
3146
3285
  /** Compiler flags TypeScript deprecated (TS5101) and will remove in TS7. Their
3147
3286
  * mere presence — at any value — is a migration item. */
3148
3287
  const DEPRECATED_TS7_FLAGS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.203",
3
+ "version": "1.0.205",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",