@bobfrankston/npmglobalize 1.0.229 → 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 -0
- package/cli.js +20 -3
- package/lib.d.ts +55 -2
- package/lib.js +268 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -586,6 +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
|
+
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)
|
|
589
608
|
-show Show package.json dependency changes
|
|
590
609
|
-package, -pkg Update package.json scripts to use npmglobalize (see below)
|
|
591
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
|
@@ -25,6 +25,57 @@ export declare function clearBuildIssues(): void;
|
|
|
25
25
|
/** Extract the first TypeScript error line from build output for the summary.
|
|
26
26
|
* Returns a short string like "file.ts(42,5): error TS2339: Property 'foo' ..." */
|
|
27
27
|
export declare function extractFirstTscError(output: string): string | null;
|
|
28
|
+
/** Every TypeScript error line in build output — `file.ts(line,col): error TSnnnn: message`
|
|
29
|
+
* — in order, deduplicated, untruncated. For reports where the reader will act on each
|
|
30
|
+
* line (the imports a nodenext migration surfaces), as opposed to the one-line summary
|
|
31
|
+
* extractFirstTscError gives. (2026-09-14) */
|
|
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
|
+
};
|
|
28
79
|
/** A TS7016 — "Could not find a declaration file for module 'X'" — that tsc blames
|
|
29
80
|
* on the file being compiled is frequently not that file's fault: the copy of X in
|
|
30
81
|
* node_modules carries no `.d.ts` whatsoever. That happens when X was published at
|
|
@@ -531,10 +582,12 @@ export declare function reportTs7Deprecations(cwd: string): Ts7Finding[];
|
|
|
531
582
|
* the next real TS7-removal error). Returns the dirs whose tsconfigs changed
|
|
532
583
|
* plus the findings that remain (e.g. options inherited from read-only
|
|
533
584
|
* node_modules bases). */
|
|
534
|
-
export declare function fixTs7Deprecations(cwd: string): {
|
|
585
|
+
export declare function fixTs7Deprecations(cwd: string): Promise<{
|
|
535
586
|
changedDirs: string[];
|
|
536
587
|
remaining: Ts7Finding[];
|
|
537
|
-
|
|
588
|
+
importFixes: Map<string, number>;
|
|
589
|
+
typeErrors: Map<string, string[]>;
|
|
590
|
+
}>;
|
|
538
591
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
539
592
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
540
593
|
* member `package.json` without a follow-up `npm install` at the root leaves
|
package/lib.js
CHANGED
|
@@ -78,6 +78,197 @@ export function extractFirstTscError(output) {
|
|
|
78
78
|
}
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
|
+
/** Every TypeScript error line in build output — `file.ts(line,col): error TSnnnn: message`
|
|
82
|
+
* — in order, deduplicated, untruncated. For reports where the reader will act on each
|
|
83
|
+
* line (the imports a nodenext migration surfaces), as opposed to the one-line summary
|
|
84
|
+
* extractFirstTscError gives. (2026-09-14) */
|
|
85
|
+
export function extractTscErrors(output) {
|
|
86
|
+
if (!output)
|
|
87
|
+
return [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
const lines = [];
|
|
90
|
+
for (const m of output.matchAll(/^(.+?\(\d+,\d+\): error TS\d+: .+)$/gm)) {
|
|
91
|
+
const line = m[1].trimEnd();
|
|
92
|
+
if (seen.has(line))
|
|
93
|
+
continue;
|
|
94
|
+
seen.add(line);
|
|
95
|
+
lines.push(line);
|
|
96
|
+
}
|
|
97
|
+
return lines;
|
|
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
|
+
}
|
|
81
272
|
/** Does `dir` contain any `.d.ts` at all? Bounded, and never descends into a
|
|
82
273
|
* nested `node_modules` — we are asking about this package's own output. */
|
|
83
274
|
function hasDeclarationFiles(dir, depth = 3) {
|
|
@@ -3729,6 +3920,21 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
3729
3920
|
buildResult = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
|
|
3730
3921
|
}
|
|
3731
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
|
+
}
|
|
3732
3938
|
// "Try changing the 'lib' compiler option to 'es2023' or later" — a tsconfig
|
|
3733
3939
|
// pinned below the standard-library APIs the code actually uses. Raise it and
|
|
3734
3940
|
// rebuild. Looped because one level can uncover the need for a higher one,
|
|
@@ -3753,13 +3959,23 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
3753
3959
|
const retry = await runCommandAsync('npm', ['run', 'build'], { cwd, silent: true });
|
|
3754
3960
|
if (retry.success) {
|
|
3755
3961
|
dropBuildIssuesFrom(issueMark); // the migration was undone; its warnings no longer apply
|
|
3756
|
-
const firstErr = extractFirstTscError(migratedOutput);
|
|
3757
3962
|
const label = pkg.name || path.basename(cwd);
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3963
|
+
// List EVERY error the migrated build produced, not just the first: these
|
|
3964
|
+
// are the imports (usually relative specifiers missing their .js extension)
|
|
3965
|
+
// that have to be fixed by hand before nodenext can stay. The tsconfig is
|
|
3966
|
+
// left on the deprecated settings so the build keeps working meanwhile;
|
|
3967
|
+
// the next build after the imports are fixed migrates again on its own.
|
|
3968
|
+
// 2026-09-14 18:30 EDT — Claude Code (Fable 5.1), at Bob's direction:
|
|
3969
|
+
// "Rather than doing the rewrite of the imports, flag them so I can
|
|
3970
|
+
// manually fix them since I expect most cases have already been fixed."
|
|
3971
|
+
const errs = extractTscErrors(migratedOutput);
|
|
3972
|
+
console.log(colors.yellow(` Reverted the tsconfig TS7 migration in ${label} — under "nodenext" the build fails on ${errs.length || 'these'} error(s):`));
|
|
3973
|
+
for (const e of errs)
|
|
3974
|
+
console.log(colors.yellow(` ${e}`));
|
|
3975
|
+
if (!errs.length)
|
|
3976
|
+
console.log(colors.yellow(` ${extractFirstTscError(migratedOutput) || '(no tsc error line found — rerun with -verbose)'}`));
|
|
3977
|
+
console.log(colors.yellow(` Deprecated settings left in place so the build still works. Fix the imports above; the migration reruns on the next build.`));
|
|
3978
|
+
recordBuildIssue(label, 'warning', `tsconfig still uses settings TypeScript 7 removes: under "nodenext" the build fails on ${errs.length} error(s) (first: ${errs[0] || extractFirstTscError(migratedOutput) || 'see build output'}). Fix the listed imports; the migration reruns on the next build.`);
|
|
3763
3979
|
buildResult = retry;
|
|
3764
3980
|
}
|
|
3765
3981
|
else {
|
|
@@ -4003,12 +4219,45 @@ export function reportTs7Deprecations(cwd) {
|
|
|
4003
4219
|
* the next real TS7-removal error). Returns the dirs whose tsconfigs changed
|
|
4004
4220
|
* plus the findings that remain (e.g. options inherited from read-only
|
|
4005
4221
|
* node_modules bases). */
|
|
4006
|
-
export function fixTs7Deprecations(cwd) {
|
|
4222
|
+
export async function fixTs7Deprecations(cwd) {
|
|
4007
4223
|
const changedDirs = [];
|
|
4008
|
-
|
|
4224
|
+
const dirs = collectFileDepDirs(cwd);
|
|
4225
|
+
for (const dir of dirs) {
|
|
4009
4226
|
if (migrateTsconfigDeprecations(dir))
|
|
4010
4227
|
changedDirs.push(dir);
|
|
4011
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
|
+
}
|
|
4012
4261
|
// Drop ignoreDeprecations wherever nothing deprecated remains in that package.
|
|
4013
4262
|
let remaining = reportTs7Deprecations(cwd);
|
|
4014
4263
|
const igdDirs = new Set(remaining.filter(f => f.option === 'ignoreDeprecations').map(f => f.dir));
|
|
@@ -4040,7 +4289,7 @@ export function fixTs7Deprecations(cwd) {
|
|
|
4040
4289
|
}
|
|
4041
4290
|
if (removedIgd)
|
|
4042
4291
|
remaining = reportTs7Deprecations(cwd);
|
|
4043
|
-
return { changedDirs, remaining };
|
|
4292
|
+
return { changedDirs, remaining, importFixes, typeErrors };
|
|
4044
4293
|
}
|
|
4045
4294
|
/** Ensure the workspace-root `node_modules/` is in sync with every member's
|
|
4046
4295
|
* declared deps. Workspaces hoist deps to the root, so a dep added to any
|
|
@@ -4842,7 +5091,9 @@ export function runCommand(cmd, args, options = {}) {
|
|
|
4842
5091
|
* child, restore deps, and exit — instead of being queued behind spawnSync. */
|
|
4843
5092
|
export function runCommandAsync(cmd, args, options = {}) {
|
|
4844
5093
|
const { silent = false, verbose = false, showCommand = false, cwd } = options;
|
|
4845
|
-
|
|
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';
|
|
4846
5097
|
if (!silent && (showCommand || verbose)) {
|
|
4847
5098
|
console.log(colors.cyan(`> ${cmd} ${args.join(' ')}`));
|
|
4848
5099
|
}
|
|
@@ -5755,19 +6006,17 @@ const ALL_GITIGNORE = [...IGNORE_PATTERNS.gitignore.security, ...IGNORE_PATTERNS
|
|
|
5755
6006
|
const ALL_NPMIGNORE = [...IGNORE_PATTERNS.npmignore.security, ...IGNORE_PATTERNS.npmignore.recommended];
|
|
5756
6007
|
/** Patterns that should NOT be in .npmignore for noEmit projects (TS files are the runtime files) */
|
|
5757
6008
|
const TS_NPMIGNORE_PATTERNS = new Set(['*.ts', '!*.d.ts', '*.map', 'tsconfig.json']);
|
|
5758
|
-
/** 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. */
|
|
5759
6014
|
function isNoEmitProject(cwd) {
|
|
5760
6015
|
const tsconfigPath = path.join(cwd, 'tsconfig.json');
|
|
5761
6016
|
if (!fs.existsSync(tsconfigPath))
|
|
5762
6017
|
return false;
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
const tsconfig = JSON5.parse(content);
|
|
5766
|
-
return tsconfig.compilerOptions?.noEmit === true;
|
|
5767
|
-
}
|
|
5768
|
-
catch {
|
|
5769
|
-
return false;
|
|
5770
|
-
}
|
|
6018
|
+
const { chain } = resolveTsconfigChain(tsconfigPath);
|
|
6019
|
+
return effectiveCompilerOption(chain, 'noEmit')?.value === true;
|
|
5771
6020
|
}
|
|
5772
6021
|
/** Resolve whether .ts source files should be kept in the published tarball.
|
|
5773
6022
|
* Explicit allowTs wins; otherwise noEmit projects default to including .ts. */
|