@bobfrankston/npmglobalize 1.0.202 → 1.0.203

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 +96 -4
  2. 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
- function migrateTsconfigDeprecations(cwd) {
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,35 @@ function migrateTsconfigDeprecations(cwd) {
2413
2429
  }
2414
2430
  return changed;
2415
2431
  }
2432
+ /** Write back the texts captured in a `migrateTsconfigDeprecations` snapshot
2433
+ * (path → text). Used to undo a migration whose build turned out worse, and to
2434
+ * re-apply it afterwards. Returns false if any write failed — the tree is then
2435
+ * in a mixed state, which the caller reports rather than hides. */
2436
+ function restoreTsconfigTexts(texts) {
2437
+ let ok = true;
2438
+ for (const [filePath, text] of texts) {
2439
+ try {
2440
+ fs.writeFileSync(filePath, text);
2441
+ }
2442
+ catch (error) {
2443
+ ok = false;
2444
+ console.error(colors.yellow(` Could not restore tsconfig (${filePath}): ${error.message}`));
2445
+ }
2446
+ }
2447
+ return ok;
2448
+ }
2449
+ /** Current text of every file in a snapshot, so a migration can be re-applied
2450
+ * after being tried in reverse. */
2451
+ function readTsconfigTexts(paths) {
2452
+ const out = new Map();
2453
+ for (const p of paths) {
2454
+ try {
2455
+ out.set(p, fs.readFileSync(p, 'utf-8'));
2456
+ }
2457
+ catch { /* gone — nothing to re-apply */ }
2458
+ }
2459
+ return out;
2460
+ }
2416
2461
  /** Newest mtime of any regular file under `dir` (recursive). Symlinks/junctions
2417
2462
  * are not followed — the file: dep webs here link back into sibling checkouts. */
2418
2463
  function newestMtimeUnder(dir) {
@@ -2913,15 +2958,29 @@ export async function buildProject(cwd, opts = {}) {
2913
2958
  // with no tsconfig at all, and still need its import map regenerated.
2914
2959
  await ensureImportgenInBuild(cwd);
2915
2960
  let shouldBuild = false;
2961
+ let hasTsconfig = false;
2916
2962
  try {
2917
2963
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
2918
2964
  const content = fs.readFileSync(tsconfigPath, 'utf-8');
2919
2965
  const tsconfig = JSON5.parse(content);
2966
+ hasTsconfig = true;
2920
2967
  shouldBuild = tsconfig.compilerOptions?.noEmit !== true;
2921
2968
  }
2922
2969
  catch {
2923
2970
  // No tsconfig — skip
2924
2971
  }
2972
+ // Fix removed-in-TS7 tsconfig settings up front rather than waiting for a
2973
+ // build to fail on them. A deprecated `moduleResolution` is only a *hard*
2974
+ // error under the tsc that flags it (TS6+, and the editor's bundled
2975
+ // compiler), so a package pinned to an older tsc — or one that never gets
2976
+ // to the build at all (noEmit, no build script, output already fresh) —
2977
+ // would otherwise keep the deprecation until TS7 removes it outright.
2978
+ // Deliberately ahead of every early return below: the tsconfig is worth
2979
+ // fixing whether or not this package builds. Migrating also touches
2980
+ // tsconfig.json, which makes the freshness check correctly read as stale.
2981
+ const tsconfigSnapshot = new Map();
2982
+ const issueMark = markBuildIssues();
2983
+ const migratedUpFront = hasTsconfig && migrateTsconfigDeprecations(cwd, tsconfigSnapshot);
2925
2984
  let earlyPkg;
2926
2985
  try {
2927
2986
  earlyPkg = readPackageJson(cwd);
@@ -2974,7 +3033,9 @@ export async function buildProject(cwd, opts = {}) {
2974
3033
  // freshness — one the user declined to wire in isn't this build's business.
2975
3034
  const finalBuild = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build : '';
2976
3035
  const wiredSubs = detectSubProjects(cwd).filter(s => scriptBuildsSubProject(finalBuild, s.dir));
2977
- if (!opts.forceBuild && isBuildUpToDate(cwd) && areSubProjectsUpToDate(cwd, wiredSubs)
3036
+ // A migration just changed how modules resolve — never report that as fresh;
3037
+ // the rebuild is what validates it (and what the revert path above needs).
3038
+ if (!opts.forceBuild && !migratedUpFront && isBuildUpToDate(cwd) && areSubProjectsUpToDate(cwd, wiredSubs)
2978
3039
  && (!runsImportgen || isImportMapUpToDate(cwd))) {
2979
3040
  console.log(colors.dim(`– Build up to date (${pkg.name || path.basename(cwd)})`));
2980
3041
  return true;
@@ -2987,10 +3048,41 @@ export async function buildProject(cwd, opts = {}) {
2987
3048
  // We don't silence with ignoreDeprecations — the retry may now surface real
2988
3049
  // resolution errors, which are genuine bugs to fix, not deprecations.
2989
3050
  const out = (buildResult.stderr || '') + (buildResult.output || '');
2990
- if (/error TS510[17]\b/.test(out) && migrateTsconfigDeprecations(cwd)) {
3051
+ if (/error TS510[17]\b/.test(out) && migrateTsconfigDeprecations(cwd, tsconfigSnapshot)) {
2991
3052
  buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
2992
3053
  }
2993
3054
  }
3055
+ if (!buildResult.success && migratedUpFront && tsconfigSnapshot.size) {
3056
+ // The up-front migration is a semantic change: `nodenext` resolution can
3057
+ // surface real errors (missing file extensions on relative imports) that
3058
+ // `node10` was hiding. Those are genuine bugs — but they are not this
3059
+ // publish's business, and silently breaking a build that worked a moment
3060
+ // ago is worse than leaving the deprecation in place. So put the
3061
+ // tsconfigs back, and if that restores a working build, hand the user
3062
+ // the errors plus the command that migrates for real.
3063
+ const migratedOutput = (buildResult.stderr || '') + (buildResult.output || '');
3064
+ const migratedTexts = readTsconfigTexts(tsconfigSnapshot.keys());
3065
+ if (restoreTsconfigTexts(tsconfigSnapshot)) {
3066
+ const retry = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
3067
+ if (retry.success) {
3068
+ dropBuildIssuesFrom(issueMark); // the migration was undone; its warnings no longer apply
3069
+ const firstErr = extractFirstTscError(migratedOutput);
3070
+ const label = pkg.name || path.basename(cwd);
3071
+ console.log(colors.yellow(` Reverted the tsconfig TS7 migration in ${label} — it broke the build:`));
3072
+ if (firstErr)
3073
+ console.log(colors.yellow(` ${firstErr}`));
3074
+ console.log(colors.yellow(` Deprecated settings left in place. Fix with: npmglobalize ${cwd} -tsfix`));
3075
+ 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.`);
3076
+ buildResult = retry;
3077
+ }
3078
+ else {
3079
+ // The build was already failing for its own reasons — the migration
3080
+ // isn't what broke it, so keep the migrated (TS7-ready) tsconfigs
3081
+ // and report the migrated build's failure.
3082
+ restoreTsconfigTexts(migratedTexts);
3083
+ }
3084
+ }
3085
+ }
2994
3086
  if (buildResult.success) {
2995
3087
  console.log(colors.green(`✓ Build succeeded (${pkg.name || path.basename(cwd)})`));
2996
3088
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.202",
3
+ "version": "1.0.203",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",