@bobfrankston/npmglobalize 1.0.230 → 1.0.231
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/README.md +19 -7
- package/cli.js +20 -3
- package/lib.d.ts +50 -2
- package/lib.js +234 -13
- 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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
@@ -592,7 +592,7 @@ export async function main() {
|
|
|
592
592
|
if (cliOptions.ts7Fix) {
|
|
593
593
|
console.log('');
|
|
594
594
|
console.log(colors.accent('━━━ TypeScript 7 tsconfig fix ━━━'));
|
|
595
|
-
const { changedDirs, remaining } = fixTs7Deprecations(cwd);
|
|
595
|
+
const { changedDirs, remaining, importFixes, typeErrors } = await fixTs7Deprecations(cwd);
|
|
596
596
|
if (changedDirs.length === 0) {
|
|
597
597
|
console.log(colors.success(' ✓ Nothing to fix — no removed-in-TS7 compilerOptions in writable tsconfigs.'));
|
|
598
598
|
}
|
|
@@ -600,7 +600,24 @@ export async function main() {
|
|
|
600
600
|
console.log(colors.success(` ✓ Patched tsconfig(s) in ${changedDirs.length} package(s):`));
|
|
601
601
|
for (const dir of changedDirs)
|
|
602
602
|
console.log(` ${dir}`);
|
|
603
|
-
|
|
603
|
+
}
|
|
604
|
+
// 2026-09-14 — Claude Code (Fable 5.1): -tsfix now also type-checks each package
|
|
605
|
+
// and adds the extension NodeNext requires on relative imports (see
|
|
606
|
+
// fixImportExtensions in lib.ts). What still fails is listed, per package.
|
|
607
|
+
const totalImports = [...importFixes.values()].reduce((a, b) => a + b, 0);
|
|
608
|
+
if (totalImports > 0) {
|
|
609
|
+
console.log(colors.success(` ✓ Added file extensions to ${totalImports} relative import(s) in ${importFixes.size} package(s) — review the diff.`));
|
|
610
|
+
}
|
|
611
|
+
if (typeErrors.size > 0) {
|
|
612
|
+
console.log(colors.warn(` ${typeErrors.size} package(s) still fail to type-check under the migrated settings:`));
|
|
613
|
+
for (const [dir, errs] of typeErrors) {
|
|
614
|
+
console.log(colors.warn(` ${dir}`));
|
|
615
|
+
for (const e of errs)
|
|
616
|
+
console.log(colors.warn(` ${e}`));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else if (changedDirs.length > 0 || totalImports > 0) {
|
|
620
|
+
console.log(colors.success(' ✓ Every package type-checks under the migrated settings.'));
|
|
604
621
|
}
|
|
605
622
|
if (remaining.length > 0) {
|
|
606
623
|
console.log(colors.warn(` ${remaining.length} item(s) could not be auto-fixed:`));
|
|
@@ -611,7 +628,7 @@ export async function main() {
|
|
|
611
628
|
}
|
|
612
629
|
console.log('');
|
|
613
630
|
printBuildSummary();
|
|
614
|
-
process.exit(remaining.length > 0 ? 1 : 0);
|
|
631
|
+
process.exit(remaining.length > 0 || typeErrors.size > 0 ? 1 : 0);
|
|
615
632
|
}
|
|
616
633
|
// Build file: deps topologically, then the target itself.
|
|
617
634
|
// 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5792
|
-
|
|
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. */
|