@bobfrankston/npmglobalize 1.0.230 → 1.0.232

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 +19 -7
  2. package/cli.js +28 -8
  3. package/lib.d.ts +50 -2
  4. package/lib.js +234 -13
  5. package/package.json +1 -1
package/README.md CHANGED
@@ -586,13 +586,25 @@ Workspace mode is auto-detected when run from a root with `"private": true` and
586
586
  migration automatically when a build hits a TS7 deprecation
587
587
  error, so day-to-day you never need this flag.
588
588
  npmglobalize <path> -tsfix
589
- If the automatic migration breaks a build (usually relative
590
- imports missing their .js extension, TS2835), the tsconfig is
591
- put back so the build keeps working and EVERY error from the
592
- nodenext attempt is listed, file(line,col) and the suggested
593
- fix, for fixing by hand. Nothing in the source is rewritten.
594
- The migration reruns on the next build once the imports are
595
- fixed. (2026-09-14)
589
+ Relative imports that NodeNext rejects for lacking a file
590
+ extension (TS2835) are fixed too, in the release flow and
591
+ under -tsfix alike: the extension tsc itself suggests is
592
+ written into the import ("./a" -> "./a.js", "./utils" ->
593
+ "./utils/index.js"), driven only by tsc's diagnostics, never
594
+ by a regex over source, so comments, template strings and
595
+ bare package specifiers are never touched. A noEmit project
596
+ that Node runs directly gets ".ts" instead and
597
+ allowImportingTsExtensions is switched on, since Node's type
598
+ stripping does not map "./a.js" to a.ts. A suggestion that is
599
+ not just an added extension is left alone and reported. Each
600
+ rewritten import is printed and counted in the issues summary
601
+ so the diff can be reviewed. -tsfix type-checks each package
602
+ with tsc --noEmit to get those diagnostics (it still emits
603
+ nothing, commits nothing, publishes nothing). If a build
604
+ still fails after the import fixes, the tsconfig is put back
605
+ so the build keeps working, the import edits stay (a named
606
+ file is right under the old resolver too), and every
607
+ remaining error is listed for fixing by hand. (2026-09-14)
596
608
  -show Show package.json dependency changes
597
609
  -package, -pkg Update package.json scripts to use npmglobalize (see below)
598
610
  -h, -help Show help
package/cli.js CHANGED
@@ -136,11 +136,14 @@ Other Options:
136
136
  migration to-do list and exits. Writes nothing.
137
137
  -tsfix, -ts7-fix
138
138
  One-off utility, not the main flow: apply the TS7 tsconfig
139
- migration to this package and its file: deps, then exit.
140
- Only tsconfig files change no build, no commit, no push,
141
- no publish. The release flow auto-applies the same migration
142
- on a TS7 build error; use this only for a temporary in-place
143
- fix of a local directory: npmglobalize <path> -tsfix
139
+ migration to this package and its file: deps, type-check
140
+ each with tsc --noEmit, add the extension NodeNext requires
141
+ on relative imports tsc flags ("./a" -> "./a.js", a noEmit
142
+ project gets ".ts"), and exit. tsconfig files and flagged
143
+ imports change no emit, no commit, no push, no publish.
144
+ Whatever still fails to type-check is listed. The release
145
+ flow does the same on a TS7 build error; use this for an
146
+ in-place fix of a local directory: npmglobalize <path> -tsfix
144
147
  -show Show package.json dependency changes
145
148
  -package, -pkg Update package.json scripts to use npmglobalize
146
149
  -h, -help Show this help
@@ -592,7 +595,7 @@ export async function main() {
592
595
  if (cliOptions.ts7Fix) {
593
596
  console.log('');
594
597
  console.log(colors.accent('━━━ TypeScript 7 tsconfig fix ━━━'));
595
- const { changedDirs, remaining } = fixTs7Deprecations(cwd);
598
+ const { changedDirs, remaining, importFixes, typeErrors } = await fixTs7Deprecations(cwd);
596
599
  if (changedDirs.length === 0) {
597
600
  console.log(colors.success(' ✓ Nothing to fix — no removed-in-TS7 compilerOptions in writable tsconfigs.'));
598
601
  }
@@ -600,7 +603,24 @@ export async function main() {
600
603
  console.log(colors.success(` ✓ Patched tsconfig(s) in ${changedDirs.length} package(s):`));
601
604
  for (const dir of changedDirs)
602
605
  console.log(` ${dir}`);
603
- console.log(colors.muted(' Rebuild to validate — NodeNext resolution may surface real import errors the old resolver hid.'));
606
+ }
607
+ // 2026-09-14 — Claude Code (Fable 5.1): -tsfix now also type-checks each package
608
+ // and adds the extension NodeNext requires on relative imports (see
609
+ // fixImportExtensions in lib.ts). What still fails is listed, per package.
610
+ const totalImports = [...importFixes.values()].reduce((a, b) => a + b, 0);
611
+ if (totalImports > 0) {
612
+ console.log(colors.success(` ✓ Added file extensions to ${totalImports} relative import(s) in ${importFixes.size} package(s) — review the diff.`));
613
+ }
614
+ if (typeErrors.size > 0) {
615
+ console.log(colors.warn(` ${typeErrors.size} package(s) still fail to type-check under the migrated settings:`));
616
+ for (const [dir, errs] of typeErrors) {
617
+ console.log(colors.warn(` ${dir}`));
618
+ for (const e of errs)
619
+ console.log(colors.warn(` ${e}`));
620
+ }
621
+ }
622
+ else if (changedDirs.length > 0 || totalImports > 0) {
623
+ console.log(colors.success(' ✓ Every package type-checks under the migrated settings.'));
604
624
  }
605
625
  if (remaining.length > 0) {
606
626
  console.log(colors.warn(` ${remaining.length} item(s) could not be auto-fixed:`));
@@ -611,7 +631,7 @@ export async function main() {
611
631
  }
612
632
  console.log('');
613
633
  printBuildSummary();
614
- process.exit(remaining.length > 0 ? 1 : 0);
634
+ process.exit(remaining.length > 0 || typeErrors.size > 0 ? 1 : 0);
615
635
  }
616
636
  // Build file: deps topologically, then the target itself.
617
637
  // Ensures consumers' tsc sees up-to-date `.d.ts` from sibling checkouts
package/lib.d.ts CHANGED
@@ -30,6 +30,52 @@ export declare function extractFirstTscError(output: string): string | null;
30
30
  * line (the imports a nodenext migration surfaces), as opposed to the one-line summary
31
31
  * extractFirstTscError gives. (2026-09-14) */
32
32
  export declare function extractTscErrors(output: string): string[];
33
+ /** One relative import rewritten by fixImportExtensions. */
34
+ export interface ImportExtensionFix {
35
+ file: string; /** Path as tsc printed it (relative to the build's cwd) */
36
+ line: number;
37
+ col: number;
38
+ from: string; /** Specifier as it was, e.g. "./a" */
39
+ to: string; /** Specifier as written, e.g. "./a.js" */
40
+ }
41
+ /** A TS2835 diagnostic fixImportExtensions left alone, and why. */
42
+ export interface ImportExtensionSkip {
43
+ file: string;
44
+ line: number;
45
+ col: number;
46
+ reason: string;
47
+ }
48
+ /** Add the file extension NodeNext requires on relative imports, driven ONLY by tsc's own
49
+ * TS2835 diagnostics in `output` ("Relative import paths need explicit file extensions …
50
+ * Did you mean './a.js'?"). Each diagnostic carries the file, the exact position of the
51
+ * specifier's opening quote, and tsc's suggested specifier, which already accounts for
52
+ * directory imports ('./utils' → './utils/index.js') and other source kinds ('.mts' →
53
+ * '.mjs'). No regex over source, so comments, template strings, dynamic imports and bare
54
+ * package specifiers are never touched.
55
+ *
56
+ * TS2834 is the same complaint without a suggestion — TypeScript 6 gives it for a
57
+ * directory import ('./utils' → utils/index.ts). For those the suggestion is derived from
58
+ * disk, relative to the importing file, and only when exactly one answer exists:
59
+ * utils.ts/.tsx/.d.ts/.js → './utils.js', .mts → '.mjs', .cts → '.cjs', a directory
60
+ * with an index file → './utils/index.js' (or .mjs/.cjs). Nothing found, or both a
61
+ * file and a directory, is skipped and reported.
62
+ *
63
+ * A fix is applied only when the suggestion is the current specifier plus an extension,
64
+ * or plus '/index' and an extension; anything else is skipped and reported. A noEmit
65
+ * project run directly by Node gets the '.ts' family instead of '.js': Node's type
66
+ * stripping does not map './a.js' to a.ts, so tsc's suggestion would break the run.
67
+ *
68
+ * Edits are applied per file from the bottom up so earlier positions stay valid.
69
+ * 2026-09-14 19:00 EDT — Claude Code (Fable 5.1), at Bob's direction: "Might as well
70
+ * fix the imports too since I just use modules and legacy problems should be rare or
71
+ * nonexistent." Only relative specifiers are ever in scope (Bob: "I presume this is
72
+ * only for relative imports rather than libraries"). */
73
+ export declare function fixImportExtensions(cwd: string, output: string, opts?: {
74
+ useTsExtensions?: boolean;
75
+ }): {
76
+ fixed: ImportExtensionFix[];
77
+ skipped: ImportExtensionSkip[];
78
+ };
33
79
  /** A TS7016 — "Could not find a declaration file for module 'X'" — that tsc blames
34
80
  * on the file being compiled is frequently not that file's fault: the copy of X in
35
81
  * node_modules carries no `.d.ts` whatsoever. That happens when X was published at
@@ -536,10 +582,12 @@ export declare function reportTs7Deprecations(cwd: string): Ts7Finding[];
536
582
  * the next real TS7-removal error). Returns the dirs whose tsconfigs changed
537
583
  * plus the findings that remain (e.g. options inherited from read-only
538
584
  * node_modules bases). */
539
- export declare function fixTs7Deprecations(cwd: string): {
585
+ export declare function fixTs7Deprecations(cwd: string): Promise<{
540
586
  changedDirs: string[];
541
587
  remaining: Ts7Finding[];
542
- };
588
+ importFixes: Map<string, number>;
589
+ typeErrors: Map<string, string[]>;
590
+ }>;
543
591
  /** Ensure the workspace-root `node_modules/` is in sync with every member's
544
592
  * declared deps. Workspaces hoist deps to the root, so a dep added to any
545
593
  * member `package.json` without a follow-up `npm install` at the root leaves
package/lib.js CHANGED
@@ -96,6 +96,179 @@ export function extractTscErrors(output) {
96
96
  }
97
97
  return lines;
98
98
  }
99
+ /** Add the file extension NodeNext requires on relative imports, driven ONLY by tsc's own
100
+ * TS2835 diagnostics in `output` ("Relative import paths need explicit file extensions …
101
+ * Did you mean './a.js'?"). Each diagnostic carries the file, the exact position of the
102
+ * specifier's opening quote, and tsc's suggested specifier, which already accounts for
103
+ * directory imports ('./utils' → './utils/index.js') and other source kinds ('.mts' →
104
+ * '.mjs'). No regex over source, so comments, template strings, dynamic imports and bare
105
+ * package specifiers are never touched.
106
+ *
107
+ * TS2834 is the same complaint without a suggestion — TypeScript 6 gives it for a
108
+ * directory import ('./utils' → utils/index.ts). For those the suggestion is derived from
109
+ * disk, relative to the importing file, and only when exactly one answer exists:
110
+ * utils.ts/.tsx/.d.ts/.js → './utils.js', .mts → '.mjs', .cts → '.cjs', a directory
111
+ * with an index file → './utils/index.js' (or .mjs/.cjs). Nothing found, or both a
112
+ * file and a directory, is skipped and reported.
113
+ *
114
+ * A fix is applied only when the suggestion is the current specifier plus an extension,
115
+ * or plus '/index' and an extension; anything else is skipped and reported. A noEmit
116
+ * project run directly by Node gets the '.ts' family instead of '.js': Node's type
117
+ * stripping does not map './a.js' to a.ts, so tsc's suggestion would break the run.
118
+ *
119
+ * Edits are applied per file from the bottom up so earlier positions stay valid.
120
+ * 2026-09-14 19:00 EDT — Claude Code (Fable 5.1), at Bob's direction: "Might as well
121
+ * fix the imports too since I just use modules and legacy problems should be rare or
122
+ * nonexistent." Only relative specifiers are ever in scope (Bob: "I presume this is
123
+ * only for relative imports rather than libraries"). */
124
+ export function fixImportExtensions(cwd, output, opts = {}) {
125
+ const fixed = [];
126
+ const skipped = [];
127
+ if (!output)
128
+ return { fixed, skipped };
129
+ // file(line,col): error TS2835: Relative import paths need explicit file extensions … Did you mean './a.js'?
130
+ // file(line,col): error TS2834: Relative import paths need explicit file extensions … Consider adding an extension to the import path.
131
+ const re = /^(.+?)\((\d+),(\d+)\): error TS283[45]: [^\n]*?(?:Did you mean '([^']+)'\?)?\s*$/gm;
132
+ const byFile = new Map();
133
+ for (const m of output.matchAll(re)) {
134
+ const list = byFile.get(m[1]) ?? [];
135
+ list.push({ line: Number(m[2]), col: Number(m[3]), suggested: m[4] || '' });
136
+ byFile.set(m[1], list);
137
+ }
138
+ const toTs = (spec) => spec.replace(/\.js$/, '.ts').replace(/\.mjs$/, '.mts').replace(/\.cjs$/, '.cts');
139
+ /** What `from` (as written in `importer`) should become, decided from what is on disk.
140
+ * Returns the single answer, or an explanation of why there is none. */
141
+ const suggestFromDisk = (importer, from) => {
142
+ const target = path.resolve(path.dirname(importer), from);
143
+ const base = from.replace(/\/$/, '');
144
+ // [file on disk, specifier to write]
145
+ const candidates = [
146
+ [`${target}.ts`, `${base}.js`], [`${target}.tsx`, `${base}.js`], [`${target}.d.ts`, `${base}.js`], [`${target}.js`, `${base}.js`],
147
+ [`${target}.mts`, `${base}.mjs`], [`${target}.mjs`, `${base}.mjs`],
148
+ [`${target}.cts`, `${base}.cjs`], [`${target}.cjs`, `${base}.cjs`],
149
+ [path.join(target, 'index.ts'), `${base}/index.js`], [path.join(target, 'index.tsx'), `${base}/index.js`],
150
+ [path.join(target, 'index.d.ts'), `${base}/index.js`], [path.join(target, 'index.js'), `${base}/index.js`],
151
+ [path.join(target, 'index.mts'), `${base}/index.mjs`], [path.join(target, 'index.mjs'), `${base}/index.mjs`],
152
+ [path.join(target, 'index.cts'), `${base}/index.cjs`], [path.join(target, 'index.cjs'), `${base}/index.cjs`],
153
+ ];
154
+ const found = new Set(candidates.filter(([file]) => fs.existsSync(file)).map(([, spec]) => spec));
155
+ if (found.size === 1)
156
+ return { suggested: [...found][0] };
157
+ if (found.size === 0)
158
+ return { reason: `tsc gave no suggestion and nothing on disk matches "${from}" (looked for ${path.basename(target)}.ts/.tsx/.mts/.cts/.js and ${path.basename(target)}/index.*)` };
159
+ return { reason: `tsc gave no suggestion and "${from}" is ambiguous on disk: ${[...found].join(' or ')}` };
160
+ };
161
+ for (const [file, diags] of byFile) {
162
+ const filePath = path.resolve(cwd, file);
163
+ let text;
164
+ try {
165
+ text = fs.readFileSync(filePath, 'utf-8');
166
+ }
167
+ catch (error) {
168
+ for (const d of diags)
169
+ skipped.push({ file, line: d.line, col: d.col, reason: `cannot read ${filePath}: ${error.message}` });
170
+ continue;
171
+ }
172
+ const lines = text.split(/(?<=\n)/); // keep each line's own EOL
173
+ // Bottom-up, and right-to-left within a line, so untouched positions stay valid.
174
+ diags.sort((a, b) => b.line - a.line || b.col - a.col);
175
+ const seen = new Set();
176
+ let changed = false;
177
+ for (const d of diags) {
178
+ const key = `${d.line}:${d.col}`;
179
+ if (seen.has(key))
180
+ continue; // tsc can report the same position twice (e.g. import + re-export builds)
181
+ seen.add(key);
182
+ const lineText = lines[d.line - 1];
183
+ const quote = lineText?.[d.col - 1];
184
+ if (quote !== '"' && quote !== "'") {
185
+ skipped.push({ file, line: d.line, col: d.col, reason: 'no string literal at the reported position (file changed since the build?)' });
186
+ continue;
187
+ }
188
+ const close = lineText.indexOf(quote, d.col);
189
+ if (close < 0) {
190
+ skipped.push({ file, line: d.line, col: d.col, reason: 'unterminated string at the reported position' });
191
+ continue;
192
+ }
193
+ const from = lineText.slice(d.col, close);
194
+ if (!from.startsWith('./') && !from.startsWith('../')) {
195
+ skipped.push({ file, line: d.line, col: d.col, reason: `"${from}" is not a relative specifier` });
196
+ continue;
197
+ }
198
+ if (!d.suggested) {
199
+ const derived = suggestFromDisk(filePath, from);
200
+ if ('reason' in derived) {
201
+ skipped.push({ file, line: d.line, col: d.col, reason: derived.reason });
202
+ continue;
203
+ }
204
+ d.suggested = derived.suggested;
205
+ }
206
+ const base = from.replace(/\/$/, '');
207
+ const isPlainAppend = /^\.(js|mjs|cjs)$/.test(d.suggested.slice(from.length)) && d.suggested.startsWith(from);
208
+ const isIndexAppend = d.suggested.startsWith(base + '/index.') && /^\.(js|mjs|cjs)$/.test(d.suggested.slice(base.length + '/index'.length));
209
+ if (!isPlainAppend && !isIndexAppend) {
210
+ skipped.push({ file, line: d.line, col: d.col, reason: `tsc suggests "${d.suggested}" for "${from}", which is not just an added extension — left for a human` });
211
+ continue;
212
+ }
213
+ const to = opts.useTsExtensions ? toTs(d.suggested) : d.suggested;
214
+ lines[d.line - 1] = lineText.slice(0, d.col) + to + lineText.slice(close);
215
+ fixed.push({ file, line: d.line, col: d.col, from, to });
216
+ changed = true;
217
+ }
218
+ if (changed) {
219
+ try {
220
+ fs.writeFileSync(filePath, lines.join(''));
221
+ }
222
+ catch (error) {
223
+ // Nothing on disk changed for this file, so its fixes did not happen: move them to skipped.
224
+ for (let i = fixed.length - 1; i >= 0; i--) {
225
+ if (fixed[i].file !== file)
226
+ continue;
227
+ skipped.push({ file, line: fixed[i].line, col: fixed[i].col, reason: `cannot write ${filePath}: ${error.message}` });
228
+ fixed.splice(i, 1);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ fixed.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.col - b.col);
234
+ return { fixed, skipped };
235
+ }
236
+ /** Run fixImportExtensions for `dir` against a build's output and print what it did.
237
+ * For a noEmit project the '.ts' extension is written and `allowImportingTsExtensions`
238
+ * is switched on in the most-leaf writable tsconfig, since tsc rejects '.ts' specifiers
239
+ * without it (TS5097). Returns the number of imports rewritten. */
240
+ function applyImportExtensionFixes(dir, output, label) {
241
+ const useTs = isNoEmitProject(dir);
242
+ const { fixed, skipped } = fixImportExtensions(dir, output, { useTsExtensions: useTs });
243
+ for (const f of fixed)
244
+ console.log(colors.cyan(` Fixed import in ${f.file}(${f.line},${f.col}): "${f.from}" → "${f.to}"`));
245
+ for (const s of skipped)
246
+ console.log(colors.yellow(` Left import in ${s.file}(${s.line},${s.col}): ${s.reason}`));
247
+ if (fixed.length && useTs) {
248
+ const tsconfigPath = path.join(dir, 'tsconfig.json');
249
+ const { chain } = resolveTsconfigChain(tsconfigPath);
250
+ const already = effectiveCompilerOption(chain, 'allowImportingTsExtensions')?.value === true;
251
+ const target = chain.find(c => c.writable);
252
+ if (!already && target) {
253
+ try {
254
+ const text = fs.readFileSync(target.path, 'utf-8');
255
+ const patched = upsertCompilerOption(text, 'allowImportingTsExtensions', 'true');
256
+ if (patched && patched !== text) {
257
+ fs.writeFileSync(target.path, patched);
258
+ console.log(colors.cyan(` Set allowImportingTsExtensions: true in ${path.relative(dir, target.path) || 'tsconfig.json'} (noEmit project — imports name the .ts files)`));
259
+ }
260
+ }
261
+ catch (error) {
262
+ console.log(colors.yellow(` Could not set allowImportingTsExtensions in ${target.path}: ${error.message}`));
263
+ }
264
+ }
265
+ }
266
+ if (fixed.length) {
267
+ const files = new Set(fixed.map(f => f.file)).size;
268
+ recordBuildIssue(label, 'warning', `Added the ${useTs ? '.ts' : '.js'} extension to ${fixed.length} relative import(s) in ${files} file(s) for NodeNext — review the diff.`);
269
+ }
270
+ return fixed.length;
271
+ }
99
272
  /** Does `dir` contain any `.d.ts` at all? Bounded, and never descends into a
100
273
  * nested `node_modules` — we are asking about this package's own output. */
101
274
  function hasDeclarationFiles(dir, depth = 3) {
@@ -3747,6 +3920,21 @@ export async function buildProject(cwd, opts = {}) {
3747
3920
  buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
3748
3921
  }
3749
3922
  }
3923
+ // NodeNext surfaced relative imports without extensions (TS2835). Add the
3924
+ // extension tsc suggests and rebuild — two passes at most, since one pass
3925
+ // handles every diagnostic tsc reported and a second only catches what the
3926
+ // first pass's edits uncovered. Kept ahead of the revert below on purpose:
3927
+ // an import that names its file is right under the old resolver too, so
3928
+ // even a build that still fails afterwards keeps these edits.
3929
+ // 2026-09-14 19:00 EDT — Claude Code (Fable 5.1), at Bob's direction.
3930
+ for (let pass = 0; pass < 2 && !buildResult.success; pass++) {
3931
+ const out = (buildResult.stderr || '') + (buildResult.output || '');
3932
+ if (!/error TS283[45]\b/.test(out))
3933
+ break;
3934
+ if (!applyImportExtensionFixes(cwd, out, pkg.name || path.basename(cwd)))
3935
+ break;
3936
+ buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
3937
+ }
3750
3938
  // "Try changing the 'lib' compiler option to 'es2023' or later" — a tsconfig
3751
3939
  // pinned below the standard-library APIs the code actually uses. Raise it and
3752
3940
  // rebuild. Looped because one level can uncover the need for a higher one,
@@ -4031,12 +4219,45 @@ export function reportTs7Deprecations(cwd) {
4031
4219
  * the next real TS7-removal error). Returns the dirs whose tsconfigs changed
4032
4220
  * plus the findings that remain (e.g. options inherited from read-only
4033
4221
  * node_modules bases). */
4034
- export function fixTs7Deprecations(cwd) {
4222
+ export async function fixTs7Deprecations(cwd) {
4035
4223
  const changedDirs = [];
4036
- for (const dir of collectFileDepDirs(cwd)) {
4224
+ const dirs = collectFileDepDirs(cwd);
4225
+ for (const dir of dirs) {
4037
4226
  if (migrateTsconfigDeprecations(dir))
4038
4227
  changedDirs.push(dir);
4039
4228
  }
4229
+ // Then the imports NodeNext rejects. A type-check pass (`tsc --noEmit`, nothing
4230
+ // written by tsc itself) yields the TS2835 diagnostics that drive
4231
+ // fixImportExtensions; a second pass catches what the first one's edits
4232
+ // uncovered, and whatever still fails is handed back for a human.
4233
+ // 2026-09-14 19:00 EDT — Claude Code (Fable 5.1), at Bob's direction.
4234
+ const importFixes = new Map();
4235
+ const typeErrors = new Map();
4236
+ for (const dir of dirs) {
4237
+ if (!fs.existsSync(path.join(dir, 'tsconfig.json')))
4238
+ continue;
4239
+ const label = (() => { try {
4240
+ return readPackageJson(dir).name;
4241
+ }
4242
+ catch {
4243
+ return null;
4244
+ } })() || path.basename(dir);
4245
+ let result = await runCommandAsync('tsc', ['-p', '.', '--noEmit'], { cwd: dir, silent: true });
4246
+ for (let pass = 0; pass < 2 && !result.success; pass++) {
4247
+ const out = (result.stderr || '') + (result.output || '');
4248
+ if (!/error TS283[45]\b/.test(out))
4249
+ break;
4250
+ const fixedCount = applyImportExtensionFixes(dir, out, label);
4251
+ if (!fixedCount)
4252
+ break;
4253
+ importFixes.set(dir, (importFixes.get(dir) ?? 0) + fixedCount);
4254
+ result = await runCommandAsync('tsc', ['-p', '.', '--noEmit'], { cwd: dir, silent: true });
4255
+ }
4256
+ if (!result.success) {
4257
+ const errs = extractTscErrors((result.stderr || '') + (result.output || ''));
4258
+ typeErrors.set(dir, errs.length ? errs : [(result.stderr || result.output || '').trim().split('\n')[0] || 'tsc failed with no output']);
4259
+ }
4260
+ }
4040
4261
  // Drop ignoreDeprecations wherever nothing deprecated remains in that package.
4041
4262
  let remaining = reportTs7Deprecations(cwd);
4042
4263
  const igdDirs = new Set(remaining.filter(f => f.option === 'ignoreDeprecations').map(f => f.dir));
@@ -4068,7 +4289,7 @@ export function fixTs7Deprecations(cwd) {
4068
4289
  }
4069
4290
  if (removedIgd)
4070
4291
  remaining = reportTs7Deprecations(cwd);
4071
- return { changedDirs, remaining };
4292
+ return { changedDirs, remaining, importFixes, typeErrors };
4072
4293
  }
4073
4294
  /** Ensure the workspace-root `node_modules/` is in sync with every member's
4074
4295
  * declared deps. Workspaces hoist deps to the root, so a dep added to any
@@ -4870,7 +5091,9 @@ export function runCommand(cmd, args, options = {}) {
4870
5091
  * child, restore deps, and exit — instead of being queued behind spawnSync. */
4871
5092
  export function runCommandAsync(cmd, args, options = {}) {
4872
5093
  const { silent = false, verbose = false, showCommand = false, cwd } = options;
4873
- const needsShell = cmd === 'npm' || cmd === 'npm.cmd' || cmd === 'gh';
5094
+ // npm, gh and tsc are .cmd shims on Windows, which spawn() cannot find without a shell.
5095
+ // (tsc added 2026-09-14 for the -tsfix import-extension pass.)
5096
+ const needsShell = cmd === 'npm' || cmd === 'npm.cmd' || cmd === 'gh' || cmd === 'tsc';
4874
5097
  if (!silent && (showCommand || verbose)) {
4875
5098
  console.log(colors.cyan(`> ${cmd} ${args.join(' ')}`));
4876
5099
  }
@@ -5783,19 +6006,17 @@ const ALL_GITIGNORE = [...IGNORE_PATTERNS.gitignore.security, ...IGNORE_PATTERNS
5783
6006
  const ALL_NPMIGNORE = [...IGNORE_PATTERNS.npmignore.security, ...IGNORE_PATTERNS.npmignore.recommended];
5784
6007
  /** Patterns that should NOT be in .npmignore for noEmit projects (TS files are the runtime files) */
5785
6008
  const TS_NPMIGNORE_PATTERNS = new Set(['*.ts', '!*.d.ts', '*.map', 'tsconfig.json']);
5786
- /** Check if target project uses noEmit (TS files run directly, no compilation) */
6009
+ /** Check if target project uses noEmit (TS files run directly, no compilation).
6010
+ * Reads the effective value through the `extends` chain, not just the local file.
6011
+ * 2026-09-14 — Claude Code (Fable 5.1): was a bare JSON5 read of tsconfig.json;
6012
+ * now shared with fixImportExtensions, where a wrong answer writes ".js" into a
6013
+ * project Node runs from its .ts and breaks it at runtime. */
5787
6014
  function isNoEmitProject(cwd) {
5788
6015
  const tsconfigPath = path.join(cwd, 'tsconfig.json');
5789
6016
  if (!fs.existsSync(tsconfigPath))
5790
6017
  return false;
5791
- try {
5792
- const content = fs.readFileSync(tsconfigPath, 'utf-8');
5793
- const tsconfig = JSON5.parse(content);
5794
- return tsconfig.compilerOptions?.noEmit === true;
5795
- }
5796
- catch {
5797
- return false;
5798
- }
6018
+ const { chain } = resolveTsconfigChain(tsconfigPath);
6019
+ return effectiveCompilerOption(chain, 'noEmit')?.value === true;
5799
6020
  }
5800
6021
  /** Resolve whether .ts source files should be kept in the published tarball.
5801
6022
  * Explicit allowTs wins; otherwise noEmit projects default to including .ts. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.230",
3
+ "version": "1.0.232",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",