@bobfrankston/npmglobalize 1.0.201 → 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 +162 -60
  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;
@@ -219,6 +229,33 @@ export function readConfig(dir) {
219
229
  return {};
220
230
  }
221
231
  }
232
+ /** Inline comments for boolean settings in .globalize.json5. Value-aware: a
233
+ * negated setting must not read as though it still does the thing (a
234
+ * `"quiet": false` line saying "Suppress npm warnings" describes the
235
+ * opposite of what it does). */
236
+ const BOOL_COMMENTS = {
237
+ install: { on: 'Auto-install globally after publish (from registry)', off: 'Do NOT auto-install globally after publish' },
238
+ link: { on: 'Install globally via symlink (npm install -g .)', off: 'Do NOT install globally via symlink' },
239
+ wsl: { on: 'Also install in WSL', off: 'Do NOT install in WSL' },
240
+ files: { on: 'Restore file: paths after publish', off: 'Leave npm versions in package.json after publish' },
241
+ force: { on: 'Continue despite git errors', off: 'Stop on git errors' },
242
+ quiet: { on: 'Suppress npm warnings', off: 'Show npm warnings' },
243
+ verbose: { on: 'Show detailed output', off: 'Normal output detail' },
244
+ fix: { on: 'Auto-run npm audit fix', off: 'Do NOT run npm audit fix' },
245
+ local: { on: 'Local install only (skip transform/publish)', off: 'Normal transform/publish flow' },
246
+ noPublish: { on: 'Transform but don\'t publish', off: 'Publish normally' },
247
+ freeze: { on: 'Freeze node_modules (replace symlinks with real copies)', off: 'Leave node_modules symlinks as-is' },
248
+ usePaths: { on: 'Resolve file: deps from sibling checkouts', off: 'Resolve file: deps from npm, not siblings (standalone package)' },
249
+ allowTs: { on: 'Keep .ts source (and *.map, tsconfig.json) in the published npm tarball', off: 'Strip .ts source from the published npm tarball' },
250
+ importgen: { on: 'Regenerate browser import maps', off: 'Never ask about importgen here' }
251
+ };
252
+ /** Inline comments for settings whose value isn't a plain boolean. */
253
+ const VALUE_COMMENTS = {
254
+ gitVisibility: 'private (default) or public',
255
+ npmVisibility: 'private (default) or public',
256
+ bump: 'patch (default), minor, or major',
257
+ subProjects: 'Sub-dirs with their own tsconfig that build must compile (false = never ask)'
258
+ };
222
259
  /** Write .globalize.json5 config file */
223
260
  export function writeConfig(dir, config, explicitKeys) {
224
261
  const configPath = path.join(dir, '.globalize.json5');
@@ -284,68 +321,51 @@ export function writeConfig(dir, config, explicitKeys) {
284
321
  }
285
322
  const jsonValue = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
286
323
  const comma = ','; // JSON5 allows trailing commas
287
- // Add inline comment for clarity
324
+ // Add inline comment for clarity, keyed off the actual value so a
325
+ // negated boolean doesn't describe the behaviour it turns off.
288
326
  let comment = '';
289
- if (key === 'install')
290
- comment = ' // Auto-install globally after publish (from registry)';
291
- else if (key === 'link')
292
- comment = ' // Install globally via symlink (npm install -g .)';
293
- else if (key === 'wsl')
294
- comment = ' // Also install in WSL';
295
- else if (key === 'files')
296
- comment = ' // Keep file: paths after publish';
297
- else if (key === 'force')
298
- comment = ' // Continue despite git errors';
299
- else if (key === 'quiet')
300
- comment = ' // Suppress npm warnings';
301
- else if (key === 'verbose')
302
- comment = ' // Show detailed output';
303
- else if (key === 'gitVisibility')
304
- comment = ' // private (default) or public';
305
- else if (key === 'npmVisibility')
306
- comment = ' // private (default) or public';
307
- else if (key === 'bump')
308
- comment = ' // patch (default), minor, or major';
309
- else if (key === 'fix')
310
- comment = ' // Auto-run npm audit fix';
311
- else if (key === 'local')
312
- comment = ' // Local install only (skip transform/publish)';
313
- else if (key === 'noPublish')
314
- comment = ' // Transform but don\'t publish';
315
- else if (key === 'freeze')
316
- comment = ' // Freeze node_modules (replace symlinks with real copies)';
317
- else if (key === 'usePaths')
318
- comment = ' // Resolve file: deps from sibling checkouts (set false for standalone packages)';
319
- else if (key === 'allowTs')
320
- comment = ' // Keep .ts source (and *.map, tsconfig.json) in the published npm tarball';
321
- else if (key === 'importgen')
322
- comment = ' // Regenerate browser import maps (false = never ask about importgen here)';
323
- else if (key === 'subProjects')
324
- comment = ' // Sub-dirs with their own tsconfig that build must compile (false = never ask)';
327
+ const boolComment = BOOL_COMMENTS[key];
328
+ if (boolComment && typeof value === 'boolean') {
329
+ comment = ` // ${value ? boolComment.on : boolComment.off}`;
330
+ }
331
+ else if (VALUE_COMMENTS[key]) {
332
+ comment = ` // ${VALUE_COMMENTS[key]}`;
333
+ }
334
+ else if (boolComment) {
335
+ // Non-boolean value for a normally-boolean key (e.g. subProjects: ["Sw"])
336
+ comment = ` // ${boolComment.on}`;
337
+ }
325
338
  lines.push(` "${key}": ${jsonValue}${comma}${comment}`);
326
339
  });
327
340
  lines.push('');
328
341
  }
329
- // Add commented reference for all options
342
+ // Add commented reference for all options. Booleans are phrased "true = ..."
343
+ // so the description reads correctly next to a default of false.
330
344
  lines.push(' // Defaults (omitted above):');
331
- lines.push(' // "bump": "patch" // Version bump: patch, minor, major');
332
- lines.push(' // "install": false // Auto-install globally after publish (from registry)');
333
- lines.push(' // "link": false // Install globally via symlink (npm install -g .)');
334
- lines.push(' // "wsl": false // Also install in WSL');
335
- lines.push(' // "files": true // Keep file: paths after publish');
336
- lines.push(' // "force": false // Continue despite git errors');
337
- lines.push(' // "quiet": true // Suppress npm warnings');
338
- lines.push(' // "verbose": false // Show detailed output');
339
- lines.push(' // "gitVisibility": "private" // Git repo: private or public');
340
- lines.push(' // "npmVisibility": "private" // npm package: private or public');
341
- lines.push(' // "fix": false // Auto-run npm audit fix');
342
- lines.push(' // "local": false // Local install only (skip transform/publish)');
343
- lines.push(' // "noPublish": false // Transform but don\'t publish');
344
- lines.push(' // "freeze": false // Freeze node_modules (replace symlinks with real copies)');
345
- lines.push(' // "usePaths": true // Resolve file: deps from siblings; false = use latest npm version (standalone)');
346
- lines.push(' // "allowTs": false // Include .ts source in npm tarball (auto-true for noEmit projects)');
347
- lines.push(' // "importgen": auto // Detected from .vscode/tasks.json / import map in HTML; false = never ask');
348
- lines.push(' // "subProjects": auto // Sub-dirs with their own tsconfig (e.g. ["Sw"]); detected from .vscode/tasks.json; false = never ask');
345
+ const reference = [
346
+ ['"bump": "patch"', 'Version bump: patch, minor, major'],
347
+ ['"install": false', 'true = auto-install globally after publish (from registry)'],
348
+ ['"link": false', 'true = install globally via symlink (npm install -g .)'],
349
+ ['"wsl": false', 'true = also install in WSL'],
350
+ ['"files": true', 'true = restore file: paths after publish; false = leave npm versions in package.json'],
351
+ ['"force": false', 'true = continue despite git errors'],
352
+ ['"quiet": true', 'true = suppress npm warnings'],
353
+ ['"verbose": false', 'true = show detailed output'],
354
+ ['"gitVisibility": "private"', 'Git repo: private or public'],
355
+ ['"npmVisibility": "private"', 'npm package: private or public'],
356
+ ['"fix": false', 'true = auto-run npm audit fix'],
357
+ ['"local": false', 'true = local install only (skip transform/publish)'],
358
+ ['"noPublish": false', 'true = transform but don\'t publish'],
359
+ ['"freeze": false', 'true = freeze node_modules (replace symlinks with real copies)'],
360
+ ['"usePaths": true', 'true = resolve file: deps from siblings; false = use latest npm version (standalone)'],
361
+ ['"allowTs": false', 'true = include .ts source in npm tarball (auto-true for noEmit projects)'],
362
+ ['"importgen": auto', 'Detected from .vscode/tasks.json / import map in HTML; false = never ask'],
363
+ ['"subProjects": auto', 'Sub-dirs with their own tsconfig (e.g. ["Sw"]); detected from .vscode/tasks.json; false = never ask']
364
+ ];
365
+ const refWidth = Math.max(...reference.map(([decl]) => decl.length));
366
+ for (const [decl, note] of reference) {
367
+ lines.push(` // ${decl.padEnd(refWidth)} // ${note}`);
368
+ }
349
369
  lines.push('}');
350
370
  fs.writeFileSync(configPath, lines.join('\n') + '\n');
351
371
  }
@@ -2313,8 +2333,12 @@ function ensureTsconfigNodeTypes(cwd) {
2313
2333
  * most-leaf writable config so it wins. This is a *semantic* change: the rebuild
2314
2334
  * may now surface genuine resolution errors — that is the point. Those are real
2315
2335
  * bugs the `node10` resolver was hiding, not deprecations. Returns true if it
2316
- * changed any file. */
2317
- 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) {
2318
2342
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
2319
2343
  if (!fs.existsSync(tsconfigPath))
2320
2344
  return false;
@@ -2348,6 +2372,8 @@ function migrateTsconfigDeprecations(cwd) {
2348
2372
  return false;
2349
2373
  try {
2350
2374
  fs.writeFileSync(filePath, patched);
2375
+ if (snapshot && !snapshot.has(filePath))
2376
+ snapshot.set(filePath, text);
2351
2377
  return true;
2352
2378
  }
2353
2379
  catch (error) {
@@ -2403,6 +2429,35 @@ function migrateTsconfigDeprecations(cwd) {
2403
2429
  }
2404
2430
  return changed;
2405
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
+ }
2406
2461
  /** Newest mtime of any regular file under `dir` (recursive). Symlinks/junctions
2407
2462
  * are not followed — the file: dep webs here link back into sibling checkouts. */
2408
2463
  function newestMtimeUnder(dir) {
@@ -2903,15 +2958,29 @@ export async function buildProject(cwd, opts = {}) {
2903
2958
  // with no tsconfig at all, and still need its import map regenerated.
2904
2959
  await ensureImportgenInBuild(cwd);
2905
2960
  let shouldBuild = false;
2961
+ let hasTsconfig = false;
2906
2962
  try {
2907
2963
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
2908
2964
  const content = fs.readFileSync(tsconfigPath, 'utf-8');
2909
2965
  const tsconfig = JSON5.parse(content);
2966
+ hasTsconfig = true;
2910
2967
  shouldBuild = tsconfig.compilerOptions?.noEmit !== true;
2911
2968
  }
2912
2969
  catch {
2913
2970
  // No tsconfig — skip
2914
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);
2915
2984
  let earlyPkg;
2916
2985
  try {
2917
2986
  earlyPkg = readPackageJson(cwd);
@@ -2964,7 +3033,9 @@ export async function buildProject(cwd, opts = {}) {
2964
3033
  // freshness — one the user declined to wire in isn't this build's business.
2965
3034
  const finalBuild = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build : '';
2966
3035
  const wiredSubs = detectSubProjects(cwd).filter(s => scriptBuildsSubProject(finalBuild, s.dir));
2967
- 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)
2968
3039
  && (!runsImportgen || isImportMapUpToDate(cwd))) {
2969
3040
  console.log(colors.dim(`– Build up to date (${pkg.name || path.basename(cwd)})`));
2970
3041
  return true;
@@ -2977,10 +3048,41 @@ export async function buildProject(cwd, opts = {}) {
2977
3048
  // We don't silence with ignoreDeprecations — the retry may now surface real
2978
3049
  // resolution errors, which are genuine bugs to fix, not deprecations.
2979
3050
  const out = (buildResult.stderr || '') + (buildResult.output || '');
2980
- if (/error TS510[17]\b/.test(out) && migrateTsconfigDeprecations(cwd)) {
3051
+ if (/error TS510[17]\b/.test(out) && migrateTsconfigDeprecations(cwd, tsconfigSnapshot)) {
2981
3052
  buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
2982
3053
  }
2983
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
+ }
2984
3086
  if (buildResult.success) {
2985
3087
  console.log(colors.green(`✓ Build succeeded (${pkg.name || path.basename(cwd)})`));
2986
3088
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.201",
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",