@shipispec/tsfix 0.1.0 → 0.2.0
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/CHANGELOG.md +86 -0
- package/README.md +60 -10
- package/dist/cli.js +724 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +576 -0
- package/dist/types/index.d.ts +103 -0
- package/dist/types/tsLanguageServiceFixer.d.ts +124 -0
- package/dist/types/validatorInProcess.d.ts +64 -0
- package/package.json +18 -16
- package/bin/tsfix.mjs +0 -49
- package/cli/run-stack.ts +0 -195
- package/src/index.ts +0 -202
- package/src/tsLanguageServiceFixer.ts +0 -486
- package/src/validatorInProcess.ts +0 -276
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-process TypeScript validator (Phase 5 — TSC iron-clad).
|
|
3
|
-
*
|
|
4
|
-
* Replaces the `tsc --noEmit` shell exec with `ts.createProgram` +
|
|
5
|
-
* `getPreEmitDiagnostics()`. Three wins over shelling:
|
|
6
|
-
*
|
|
7
|
-
* 1. **Structured diagnostics** — `{file, start, length, code, messageText,
|
|
8
|
-
* category, relatedInformation}` natively, no regex parsing of tsc text
|
|
9
|
-
* output. Feeds Layer 2's symbol tracer directly.
|
|
10
|
-
*
|
|
11
|
-
* 2. **Speed** — long-lived Program per workspace caches lib files,
|
|
12
|
-
* transitive deps, and tsconfig parse. Cold start is comparable; warm
|
|
13
|
-
* validation is ~5-10× faster than spawning a fresh tsc process.
|
|
14
|
-
*
|
|
15
|
-
* 3. **Diagnostic enrichment** — we have the AST in hand, so each error
|
|
16
|
-
* can carry the offending node's source span and aliased symbol info,
|
|
17
|
-
* which the shell tsc doesn't provide.
|
|
18
|
-
*
|
|
19
|
-
* Backwards compatibility: feature-flagged via `SPECTOSHIP_TSC_INPROCESS`
|
|
20
|
-
* env var. Shell tsc remains the default until we measure parity on real
|
|
21
|
-
* projects.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import * as fs from "node:fs";
|
|
25
|
-
import * as path from "node:path";
|
|
26
|
-
import * as ts from "typescript";
|
|
27
|
-
|
|
28
|
-
export interface InProcessTscResult {
|
|
29
|
-
passed: boolean;
|
|
30
|
-
/** Diagnostic messages formatted in the same shape as `tsc` output. */
|
|
31
|
-
output: string;
|
|
32
|
-
/** Structured per-error data — drives Layer 2 cross-file tracer. */
|
|
33
|
-
diagnostics: Array<{
|
|
34
|
-
file: string;
|
|
35
|
-
line: number;
|
|
36
|
-
column: number;
|
|
37
|
-
code: string;
|
|
38
|
-
message: string;
|
|
39
|
-
category: "error" | "warning" | "message" | "suggestion";
|
|
40
|
-
}>;
|
|
41
|
-
/** Number of lines of output for log truncation. */
|
|
42
|
-
lineCount: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface InProcessTscOptions {
|
|
46
|
-
workspaceRoot: string;
|
|
47
|
-
/** Optional list of files to filter diagnostics to (matches shell tsc filter). */
|
|
48
|
-
generatedFiles?: string[];
|
|
49
|
-
logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Long-lived per-workspace Program cache. Speeds warm validation. */
|
|
53
|
-
interface CachedProgram {
|
|
54
|
-
rootFiles: string[];
|
|
55
|
-
options: ts.CompilerOptions;
|
|
56
|
-
host: ts.CompilerHost;
|
|
57
|
-
program: ts.Program;
|
|
58
|
-
configMtime: number;
|
|
59
|
-
}
|
|
60
|
-
const programCache = new Map<string, CachedProgram>();
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Run tsc in-process. Compatible with the shell-based ToolResult shape
|
|
64
|
-
* so the caller can swap implementations transparently.
|
|
65
|
-
*/
|
|
66
|
-
export function runInProcessTsc(opts: InProcessTscOptions): InProcessTscResult {
|
|
67
|
-
const { workspaceRoot, generatedFiles, logger } = opts;
|
|
68
|
-
const tsconfigPath = path.join(workspaceRoot, "tsconfig.json");
|
|
69
|
-
if (!fs.existsSync(tsconfigPath)) {
|
|
70
|
-
return {
|
|
71
|
-
passed: true,
|
|
72
|
-
output: "(no tsconfig.json — skipped)",
|
|
73
|
-
diagnostics: [],
|
|
74
|
-
lineCount: 1,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const program = getOrCreateProgram(workspaceRoot, tsconfigPath, logger);
|
|
79
|
-
if (!program) {
|
|
80
|
-
return {
|
|
81
|
-
passed: true,
|
|
82
|
-
output: "(failed to create program — skipping)",
|
|
83
|
-
diagnostics: [],
|
|
84
|
-
lineCount: 1,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const allDiagnostics = ts.getPreEmitDiagnostics(program);
|
|
89
|
-
const structured = formatDiagnostics(allDiagnostics, workspaceRoot);
|
|
90
|
-
|
|
91
|
-
// Filter to generated files when provided (mirrors shell-tsc filter).
|
|
92
|
-
const filtered = generatedFiles?.length
|
|
93
|
-
? structured.filter((d) =>
|
|
94
|
-
generatedFiles.some((g) => g.replace(/^\.\//, "") === d.file.replace(/^\.\//, "")),
|
|
95
|
-
)
|
|
96
|
-
: structured;
|
|
97
|
-
|
|
98
|
-
const errors = filtered.filter((d) => d.category === "error");
|
|
99
|
-
|
|
100
|
-
const output = errors
|
|
101
|
-
.map((d) => `${d.file}(${d.line},${d.column}): error ${d.code}: ${d.message}`)
|
|
102
|
-
.join("\n");
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
passed: errors.length === 0,
|
|
106
|
-
output: output || "(no errors)",
|
|
107
|
-
diagnostics: filtered,
|
|
108
|
-
lineCount: output.split("\n").length,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function getOrCreateProgram(
|
|
113
|
-
workspaceRoot: string,
|
|
114
|
-
tsconfigPath: string,
|
|
115
|
-
logger: { warn(msg: string): void; error(msg: string): void },
|
|
116
|
-
): ts.Program | null {
|
|
117
|
-
let configMtime = 0;
|
|
118
|
-
try {
|
|
119
|
-
configMtime = fs.statSync(tsconfigPath).mtimeMs;
|
|
120
|
-
} catch {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const cached = programCache.get(workspaceRoot);
|
|
125
|
-
if (cached && cached.configMtime === configMtime) {
|
|
126
|
-
// Refresh source files in case generated code changed on disk —
|
|
127
|
-
// the program reuses the same options/host but reloads source-file content.
|
|
128
|
-
try {
|
|
129
|
-
const refreshed = ts.createProgram({
|
|
130
|
-
rootNames: cached.rootFiles,
|
|
131
|
-
options: cached.options,
|
|
132
|
-
host: cached.host,
|
|
133
|
-
oldProgram: cached.program,
|
|
134
|
-
});
|
|
135
|
-
cached.program = refreshed;
|
|
136
|
-
return refreshed;
|
|
137
|
-
} catch (err) {
|
|
138
|
-
logger.warn(
|
|
139
|
-
`[in-process-tsc] refresh failed; rebuilding: ${err instanceof Error ? err.message : String(err)}`,
|
|
140
|
-
);
|
|
141
|
-
programCache.delete(workspaceRoot);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Cold path — parse tsconfig from scratch.
|
|
146
|
-
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
147
|
-
if (configFile.error) {
|
|
148
|
-
logger.error(
|
|
149
|
-
`[in-process-tsc] tsconfig parse error: ${ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n")}`,
|
|
150
|
-
);
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
const parsed = ts.parseJsonConfigFileContent(
|
|
154
|
-
configFile.config,
|
|
155
|
-
ts.sys,
|
|
156
|
-
path.dirname(tsconfigPath),
|
|
157
|
-
);
|
|
158
|
-
if (parsed.errors.length > 0) {
|
|
159
|
-
const msgs = parsed.errors
|
|
160
|
-
.map((e) => ts.flattenDiagnosticMessageText(e.messageText, "\n"))
|
|
161
|
-
.join("; ");
|
|
162
|
-
logger.warn(`[in-process-tsc] tsconfig parse warnings: ${msgs}`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const host = ts.createCompilerHost(parsed.options);
|
|
166
|
-
// Override the default-lib location to point at the WORKSPACE's typescript
|
|
167
|
-
// install instead of the extension-bundled one. esbuild bundles `typescript`
|
|
168
|
-
// into dist/extension.js, which breaks the bundled module's __dirname-based
|
|
169
|
-
// lib file lookup (`lib.dom.d.ts`, `lib.es2015.d.ts`, etc. aren't shipped
|
|
170
|
-
// inside the bundle). Without this override, every workspace task fails
|
|
171
|
-
// with "Cannot find name 'Promise'" / "'window'" / etc. (test28R, 2026-05-03).
|
|
172
|
-
const workspaceLibDir = path.join(workspaceRoot, "node_modules", "typescript", "lib");
|
|
173
|
-
if (fs.existsSync(workspaceLibDir)) {
|
|
174
|
-
const originalGetDefaultLibFileName = host.getDefaultLibFileName.bind(host);
|
|
175
|
-
host.getDefaultLibLocation = () => workspaceLibDir;
|
|
176
|
-
host.getDefaultLibFileName = (options) => {
|
|
177
|
-
const fileName = path.basename(originalGetDefaultLibFileName(options));
|
|
178
|
-
return path.join(workspaceLibDir, fileName);
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
let program: ts.Program;
|
|
182
|
-
try {
|
|
183
|
-
program = ts.createProgram({
|
|
184
|
-
rootNames: parsed.fileNames,
|
|
185
|
-
options: parsed.options,
|
|
186
|
-
host,
|
|
187
|
-
});
|
|
188
|
-
} catch (err) {
|
|
189
|
-
logger.error(
|
|
190
|
-
`[in-process-tsc] createProgram failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
191
|
-
);
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
programCache.set(workspaceRoot, {
|
|
196
|
-
rootFiles: parsed.fileNames,
|
|
197
|
-
options: parsed.options,
|
|
198
|
-
host,
|
|
199
|
-
program,
|
|
200
|
-
configMtime,
|
|
201
|
-
});
|
|
202
|
-
return program;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function formatDiagnostics(
|
|
206
|
-
diagnostics: readonly ts.Diagnostic[],
|
|
207
|
-
workspaceRoot: string,
|
|
208
|
-
): InProcessTscResult["diagnostics"] {
|
|
209
|
-
const out: InProcessTscResult["diagnostics"] = [];
|
|
210
|
-
for (const d of diagnostics) {
|
|
211
|
-
if (!d.file || d.start === undefined) {
|
|
212
|
-
// Generic non-file errors (rare) — emit with synthetic location.
|
|
213
|
-
out.push({
|
|
214
|
-
file: "(global)",
|
|
215
|
-
line: 0,
|
|
216
|
-
column: 0,
|
|
217
|
-
code: `TS${d.code}`,
|
|
218
|
-
message: ts.flattenDiagnosticMessageText(d.messageText, "\n"),
|
|
219
|
-
category: categoryName(d.category),
|
|
220
|
-
});
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
const fileName = d.file.fileName;
|
|
224
|
-
// Skip lib files and node_modules from diagnostics — they're never
|
|
225
|
-
// the user's bug to fix.
|
|
226
|
-
if (fileName.includes("node_modules")) {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
if (/lib\.[a-z0-9.]+\.d\.ts$/.test(fileName)) {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
|
|
234
|
-
out.push({
|
|
235
|
-
file: path.relative(workspaceRoot, fileName) || fileName,
|
|
236
|
-
line: line + 1,
|
|
237
|
-
column: character + 1,
|
|
238
|
-
code: `TS${d.code}`,
|
|
239
|
-
message: ts.flattenDiagnosticMessageText(d.messageText, "\n"),
|
|
240
|
-
category: categoryName(d.category),
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
return out;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function categoryName(
|
|
247
|
-
c: ts.DiagnosticCategory,
|
|
248
|
-
): InProcessTscResult["diagnostics"][number]["category"] {
|
|
249
|
-
switch (c) {
|
|
250
|
-
case ts.DiagnosticCategory.Error:
|
|
251
|
-
return "error";
|
|
252
|
-
case ts.DiagnosticCategory.Warning:
|
|
253
|
-
return "warning";
|
|
254
|
-
case ts.DiagnosticCategory.Message:
|
|
255
|
-
return "message";
|
|
256
|
-
case ts.DiagnosticCategory.Suggestion:
|
|
257
|
-
return "suggestion";
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/** Reset cache (for tests + when workspace switches). */
|
|
262
|
-
export function resetInProcessTscCache(): void {
|
|
263
|
-
programCache.clear();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Whether the in-process path is enabled. Defaults to ON as of Sprint J
|
|
268
|
-
* (2026-05-03) — shell tsc consistently times out at 60s on Node 23 due to
|
|
269
|
-
* the documented tsc startup-pause bug, blocking every code-gen task.
|
|
270
|
-
* In-process is also 5-10× faster on warm runs because the Program is reused.
|
|
271
|
-
*
|
|
272
|
-
* Set `SPECTOSHIP_TSC_INPROCESS=false` to opt out.
|
|
273
|
-
*/
|
|
274
|
-
export function isInProcessTscEnabled(): boolean {
|
|
275
|
-
return process.env.SPECTOSHIP_TSC_INPROCESS !== "false";
|
|
276
|
-
}
|