@bobfrankston/npmglobalize 1.0.202 → 1.0.204
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/lib.js +202 -4
- package/package.json +1 -1
package/lib.js
CHANGED
|
@@ -42,6 +42,16 @@ const _buildIssues = [];
|
|
|
42
42
|
export function recordBuildIssue(module, severity, message) {
|
|
43
43
|
_buildIssues.push({ module, severity, message });
|
|
44
44
|
}
|
|
45
|
+
/** How many issues have been recorded so far — pair with `dropBuildIssuesFrom`
|
|
46
|
+
* to un-record issues for an action that was subsequently undone. */
|
|
47
|
+
function markBuildIssues() {
|
|
48
|
+
return _buildIssues.length;
|
|
49
|
+
}
|
|
50
|
+
/** Drop every issue recorded after `mark` (see `markBuildIssues`). */
|
|
51
|
+
function dropBuildIssuesFrom(mark) {
|
|
52
|
+
if (mark >= 0 && mark < _buildIssues.length)
|
|
53
|
+
_buildIssues.length = mark;
|
|
54
|
+
}
|
|
45
55
|
/** Get all accumulated build issues */
|
|
46
56
|
export function getBuildIssues() {
|
|
47
57
|
return _buildIssues;
|
|
@@ -2323,8 +2333,12 @@ function ensureTsconfigNodeTypes(cwd) {
|
|
|
2323
2333
|
* most-leaf writable config so it wins. This is a *semantic* change: the rebuild
|
|
2324
2334
|
* may now surface genuine resolution errors — that is the point. Those are real
|
|
2325
2335
|
* bugs the `node10` resolver was hiding, not deprecations. Returns true if it
|
|
2326
|
-
* changed any file.
|
|
2327
|
-
|
|
2336
|
+
* changed any file.
|
|
2337
|
+
*
|
|
2338
|
+
* Pass `snapshot` to capture each touched file's original text (path → text)
|
|
2339
|
+
* before it is rewritten, so the caller can put the tsconfigs back if the
|
|
2340
|
+
* migrated build turns out to be worse than the un-migrated one. */
|
|
2341
|
+
function migrateTsconfigDeprecations(cwd, snapshot) {
|
|
2328
2342
|
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
2329
2343
|
if (!fs.existsSync(tsconfigPath))
|
|
2330
2344
|
return false;
|
|
@@ -2358,6 +2372,8 @@ function migrateTsconfigDeprecations(cwd) {
|
|
|
2358
2372
|
return false;
|
|
2359
2373
|
try {
|
|
2360
2374
|
fs.writeFileSync(filePath, patched);
|
|
2375
|
+
if (snapshot && !snapshot.has(filePath))
|
|
2376
|
+
snapshot.set(filePath, text);
|
|
2361
2377
|
return true;
|
|
2362
2378
|
}
|
|
2363
2379
|
catch (error) {
|
|
@@ -2413,6 +2429,131 @@ function migrateTsconfigDeprecations(cwd) {
|
|
|
2413
2429
|
}
|
|
2414
2430
|
return changed;
|
|
2415
2431
|
}
|
|
2432
|
+
/** Ordering of ES language levels as they appear in `target`/`lib`. Anything
|
|
2433
|
+
* unknown ranks 0 so it never looks newer than what the build asked for. */
|
|
2434
|
+
const ES_LEVEL_RANK = {
|
|
2435
|
+
es3: 3, es5: 5, es6: 6, es2015: 6, es7: 7, es2016: 7, es2017: 8, es2018: 9,
|
|
2436
|
+
es2019: 10, es2020: 11, es2021: 12, es2022: 13, es2023: 14, es2024: 15,
|
|
2437
|
+
es2025: 16, esnext: 99,
|
|
2438
|
+
};
|
|
2439
|
+
const esLevelRank = (value) => ES_LEVEL_RANK[value.toLowerCase().replace(/\.full$/, '')] ?? 0;
|
|
2440
|
+
/** tsc says "Try changing the 'lib' compiler option to 'es2023' or later"
|
|
2441
|
+
* (TS2550/TS2583/TS2584) when the code uses a standard-library API newer than
|
|
2442
|
+
* the configured level. That isn't a deprecation — it's a tsconfig the language
|
|
2443
|
+
* moved past — so raise the level to whatever the build asked for and let it
|
|
2444
|
+
* try again, rather than leaving the user to decode `replaceAll` and `toSorted`
|
|
2445
|
+
* errors by hand.
|
|
2446
|
+
*
|
|
2447
|
+
* We raise `target`: its implied `lib` keeps the DOM declarations a bare
|
|
2448
|
+
* `lib: ["es2023"]` would silently drop. An explicit `lib` overrides `target`
|
|
2449
|
+
* though, so when one is set we raise its `es*` entry in place and keep the
|
|
2450
|
+
* rest (dom, webworker, …). Reads the required level from `buildOutput`;
|
|
2451
|
+
* returns true if it changed any file. */
|
|
2452
|
+
function raiseTsconfigLibLevel(cwd, buildOutput, snapshot) {
|
|
2453
|
+
let required = '';
|
|
2454
|
+
for (const m of buildOutput.matchAll(/'lib' compiler option to '([A-Za-z0-9.]+)'/g)) {
|
|
2455
|
+
if (esLevelRank(m[1]) > esLevelRank(required))
|
|
2456
|
+
required = m[1].toLowerCase();
|
|
2457
|
+
}
|
|
2458
|
+
if (!required)
|
|
2459
|
+
return false;
|
|
2460
|
+
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
2461
|
+
if (!fs.existsSync(tsconfigPath))
|
|
2462
|
+
return false;
|
|
2463
|
+
const { chain } = resolveTsconfigChain(tsconfigPath);
|
|
2464
|
+
if (!chain.length)
|
|
2465
|
+
return false;
|
|
2466
|
+
const mostLeafWritable = chain.find(c => c.writable);
|
|
2467
|
+
if (!mostLeafWritable)
|
|
2468
|
+
return false;
|
|
2469
|
+
const name = (() => { try {
|
|
2470
|
+
return readPackageJson(cwd).name;
|
|
2471
|
+
}
|
|
2472
|
+
catch {
|
|
2473
|
+
return null;
|
|
2474
|
+
} })() || path.basename(cwd);
|
|
2475
|
+
const edit = (filePath, fn) => {
|
|
2476
|
+
let text;
|
|
2477
|
+
try {
|
|
2478
|
+
text = fs.readFileSync(filePath, 'utf-8');
|
|
2479
|
+
}
|
|
2480
|
+
catch {
|
|
2481
|
+
return false;
|
|
2482
|
+
}
|
|
2483
|
+
const patched = fn(text);
|
|
2484
|
+
if (patched == null || patched === text)
|
|
2485
|
+
return false;
|
|
2486
|
+
try {
|
|
2487
|
+
fs.writeFileSync(filePath, patched);
|
|
2488
|
+
if (snapshot && !snapshot.has(filePath))
|
|
2489
|
+
snapshot.set(filePath, text);
|
|
2490
|
+
return true;
|
|
2491
|
+
}
|
|
2492
|
+
catch (error) {
|
|
2493
|
+
console.error(colors.yellow(` Could not write tsconfig patch (${filePath}): ${error.message}`));
|
|
2494
|
+
return false;
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
/** The file to edit for an option: where it's defined if that's writable,
|
|
2498
|
+
* else the most-leaf writable config, where an override wins. */
|
|
2499
|
+
const targetFor = (definedIn) => {
|
|
2500
|
+
const def = definedIn ? chain.find(c => c.path === definedIn) : null;
|
|
2501
|
+
return def?.writable ? def : mostLeafWritable;
|
|
2502
|
+
};
|
|
2503
|
+
// An explicit `lib` beats `target`, so it's the one that has to move.
|
|
2504
|
+
const lib = effectiveCompilerOption(chain, 'lib');
|
|
2505
|
+
if (lib && Array.isArray(lib.value)) {
|
|
2506
|
+
const entries = lib.value.filter((e) => typeof e === 'string');
|
|
2507
|
+
if (entries.some(e => esLevelRank(e) >= esLevelRank(required)))
|
|
2508
|
+
return false;
|
|
2509
|
+
const raised = [required, ...entries.filter(e => esLevelRank(e) === 0)];
|
|
2510
|
+
const tgt = targetFor(lib.definedIn);
|
|
2511
|
+
if (!edit(tgt.path, t => upsertCompilerOption(t, 'lib', JSON.stringify(raised).replace(/","/g, '", "'))))
|
|
2512
|
+
return false;
|
|
2513
|
+
console.log(colors.cyan(` Raised lib [${entries.join(', ')}] → [${raised.join(', ')}] in ${path.relative(cwd, tgt.path) || 'tsconfig.json'} (build needs ${required})`));
|
|
2514
|
+
recordBuildIssue(name, 'warning', `Raised "lib" to ${required} — the code uses standard-library APIs newer than the tsconfig allowed.`);
|
|
2515
|
+
return true;
|
|
2516
|
+
}
|
|
2517
|
+
const tg = effectiveCompilerOption(chain, 'target');
|
|
2518
|
+
const current = tg && typeof tg.value === 'string' ? tg.value : 'es5'; // tsc's own default
|
|
2519
|
+
if (esLevelRank(current) >= esLevelRank(required))
|
|
2520
|
+
return false;
|
|
2521
|
+
const tgt = targetFor(tg?.definedIn);
|
|
2522
|
+
if (!edit(tgt.path, t => upsertCompilerOption(t, 'target', `"${required}"`)))
|
|
2523
|
+
return false;
|
|
2524
|
+
console.log(colors.cyan(` Raised target "${current}" → "${required}" in ${path.relative(cwd, tgt.path) || 'tsconfig.json'} (build needs it)`));
|
|
2525
|
+
recordBuildIssue(name, 'warning', `Raised "target" ${current} → ${required} — the code uses standard-library APIs newer than the tsconfig allowed.`);
|
|
2526
|
+
return true;
|
|
2527
|
+
}
|
|
2528
|
+
/** Write back the texts captured in a `migrateTsconfigDeprecations` snapshot
|
|
2529
|
+
* (path → text). Used to undo a migration whose build turned out worse, and to
|
|
2530
|
+
* re-apply it afterwards. Returns false if any write failed — the tree is then
|
|
2531
|
+
* in a mixed state, which the caller reports rather than hides. */
|
|
2532
|
+
function restoreTsconfigTexts(texts) {
|
|
2533
|
+
let ok = true;
|
|
2534
|
+
for (const [filePath, text] of texts) {
|
|
2535
|
+
try {
|
|
2536
|
+
fs.writeFileSync(filePath, text);
|
|
2537
|
+
}
|
|
2538
|
+
catch (error) {
|
|
2539
|
+
ok = false;
|
|
2540
|
+
console.error(colors.yellow(` Could not restore tsconfig (${filePath}): ${error.message}`));
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return ok;
|
|
2544
|
+
}
|
|
2545
|
+
/** Current text of every file in a snapshot, so a migration can be re-applied
|
|
2546
|
+
* after being tried in reverse. */
|
|
2547
|
+
function readTsconfigTexts(paths) {
|
|
2548
|
+
const out = new Map();
|
|
2549
|
+
for (const p of paths) {
|
|
2550
|
+
try {
|
|
2551
|
+
out.set(p, fs.readFileSync(p, 'utf-8'));
|
|
2552
|
+
}
|
|
2553
|
+
catch { /* gone — nothing to re-apply */ }
|
|
2554
|
+
}
|
|
2555
|
+
return out;
|
|
2556
|
+
}
|
|
2416
2557
|
/** Newest mtime of any regular file under `dir` (recursive). Symlinks/junctions
|
|
2417
2558
|
* are not followed — the file: dep webs here link back into sibling checkouts. */
|
|
2418
2559
|
function newestMtimeUnder(dir) {
|
|
@@ -2913,15 +3054,29 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
2913
3054
|
// with no tsconfig at all, and still need its import map regenerated.
|
|
2914
3055
|
await ensureImportgenInBuild(cwd);
|
|
2915
3056
|
let shouldBuild = false;
|
|
3057
|
+
let hasTsconfig = false;
|
|
2916
3058
|
try {
|
|
2917
3059
|
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
2918
3060
|
const content = fs.readFileSync(tsconfigPath, 'utf-8');
|
|
2919
3061
|
const tsconfig = JSON5.parse(content);
|
|
3062
|
+
hasTsconfig = true;
|
|
2920
3063
|
shouldBuild = tsconfig.compilerOptions?.noEmit !== true;
|
|
2921
3064
|
}
|
|
2922
3065
|
catch {
|
|
2923
3066
|
// No tsconfig — skip
|
|
2924
3067
|
}
|
|
3068
|
+
// Fix removed-in-TS7 tsconfig settings up front rather than waiting for a
|
|
3069
|
+
// build to fail on them. A deprecated `moduleResolution` is only a *hard*
|
|
3070
|
+
// error under the tsc that flags it (TS6+, and the editor's bundled
|
|
3071
|
+
// compiler), so a package pinned to an older tsc — or one that never gets
|
|
3072
|
+
// to the build at all (noEmit, no build script, output already fresh) —
|
|
3073
|
+
// would otherwise keep the deprecation until TS7 removes it outright.
|
|
3074
|
+
// Deliberately ahead of every early return below: the tsconfig is worth
|
|
3075
|
+
// fixing whether or not this package builds. Migrating also touches
|
|
3076
|
+
// tsconfig.json, which makes the freshness check correctly read as stale.
|
|
3077
|
+
const tsconfigSnapshot = new Map();
|
|
3078
|
+
const issueMark = markBuildIssues();
|
|
3079
|
+
const migratedUpFront = hasTsconfig && migrateTsconfigDeprecations(cwd, tsconfigSnapshot);
|
|
2925
3080
|
let earlyPkg;
|
|
2926
3081
|
try {
|
|
2927
3082
|
earlyPkg = readPackageJson(cwd);
|
|
@@ -2974,7 +3129,9 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
2974
3129
|
// freshness — one the user declined to wire in isn't this build's business.
|
|
2975
3130
|
const finalBuild = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build : '';
|
|
2976
3131
|
const wiredSubs = detectSubProjects(cwd).filter(s => scriptBuildsSubProject(finalBuild, s.dir));
|
|
2977
|
-
|
|
3132
|
+
// A migration just changed how modules resolve — never report that as fresh;
|
|
3133
|
+
// the rebuild is what validates it (and what the revert path above needs).
|
|
3134
|
+
if (!opts.forceBuild && !migratedUpFront && isBuildUpToDate(cwd) && areSubProjectsUpToDate(cwd, wiredSubs)
|
|
2978
3135
|
&& (!runsImportgen || isImportMapUpToDate(cwd))) {
|
|
2979
3136
|
console.log(colors.dim(`– Build up to date (${pkg.name || path.basename(cwd)})`));
|
|
2980
3137
|
return true;
|
|
@@ -2987,10 +3144,51 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
2987
3144
|
// We don't silence with ignoreDeprecations — the retry may now surface real
|
|
2988
3145
|
// resolution errors, which are genuine bugs to fix, not deprecations.
|
|
2989
3146
|
const out = (buildResult.stderr || '') + (buildResult.output || '');
|
|
2990
|
-
if (/error TS510[17]\b/.test(out) && migrateTsconfigDeprecations(cwd)) {
|
|
3147
|
+
if (/error TS510[17]\b/.test(out) && migrateTsconfigDeprecations(cwd, tsconfigSnapshot)) {
|
|
2991
3148
|
buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
|
|
2992
3149
|
}
|
|
2993
3150
|
}
|
|
3151
|
+
// "Try changing the 'lib' compiler option to 'es2023' or later" — a tsconfig
|
|
3152
|
+
// pinned below the standard-library APIs the code actually uses. Raise it and
|
|
3153
|
+
// rebuild. Looped because one level can uncover the need for a higher one,
|
|
3154
|
+
// and bounded because each pass must raise the level to run again.
|
|
3155
|
+
for (let pass = 0; pass < 3 && !buildResult.success; pass++) {
|
|
3156
|
+
const out = (buildResult.stderr || '') + (buildResult.output || '');
|
|
3157
|
+
if (!raiseTsconfigLibLevel(cwd, out, tsconfigSnapshot))
|
|
3158
|
+
break;
|
|
3159
|
+
buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
|
|
3160
|
+
}
|
|
3161
|
+
if (!buildResult.success && migratedUpFront && tsconfigSnapshot.size) {
|
|
3162
|
+
// The up-front migration is a semantic change: `nodenext` resolution can
|
|
3163
|
+
// surface real errors (missing file extensions on relative imports) that
|
|
3164
|
+
// `node10` was hiding. Those are genuine bugs — but they are not this
|
|
3165
|
+
// publish's business, and silently breaking a build that worked a moment
|
|
3166
|
+
// ago is worse than leaving the deprecation in place. So put the
|
|
3167
|
+
// tsconfigs back, and if that restores a working build, hand the user
|
|
3168
|
+
// the errors plus the command that migrates for real.
|
|
3169
|
+
const migratedOutput = (buildResult.stderr || '') + (buildResult.output || '');
|
|
3170
|
+
const migratedTexts = readTsconfigTexts(tsconfigSnapshot.keys());
|
|
3171
|
+
if (restoreTsconfigTexts(tsconfigSnapshot)) {
|
|
3172
|
+
const retry = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
|
|
3173
|
+
if (retry.success) {
|
|
3174
|
+
dropBuildIssuesFrom(issueMark); // the migration was undone; its warnings no longer apply
|
|
3175
|
+
const firstErr = extractFirstTscError(migratedOutput);
|
|
3176
|
+
const label = pkg.name || path.basename(cwd);
|
|
3177
|
+
console.log(colors.yellow(` Reverted the tsconfig TS7 migration in ${label} — it broke the build:`));
|
|
3178
|
+
if (firstErr)
|
|
3179
|
+
console.log(colors.yellow(` ${firstErr}`));
|
|
3180
|
+
console.log(colors.yellow(` Deprecated settings left in place. Fix with: npmglobalize ${cwd} -tsfix`));
|
|
3181
|
+
recordBuildIssue(label, 'warning', `tsconfig still uses settings TypeScript 7 removes: migrating to "nodenext" broke the build (${firstErr || 'see build output'}). Run \`npmglobalize ${cwd} -tsfix\` and fix the surfaced imports.`);
|
|
3182
|
+
buildResult = retry;
|
|
3183
|
+
}
|
|
3184
|
+
else {
|
|
3185
|
+
// The build was already failing for its own reasons — the migration
|
|
3186
|
+
// isn't what broke it, so keep the migrated (TS7-ready) tsconfigs
|
|
3187
|
+
// and report the migrated build's failure.
|
|
3188
|
+
restoreTsconfigTexts(migratedTexts);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
2994
3192
|
if (buildResult.success) {
|
|
2995
3193
|
console.log(colors.green(`✓ Build succeeded (${pkg.name || path.basename(cwd)})`));
|
|
2996
3194
|
return true;
|