ttsc 0.20.1 → 0.22.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/cmd/ttscserver/main.go +79 -3
- package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
- package/driver/lsp.go +4 -0
- package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
- package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
- package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
- package/internal/lspserver/lsp_native_plugin_source.go +171 -55
- package/internal/lspserver/lsp_native_resident.go +110 -9
- package/internal/lspserver/lsp_plugin.go +6 -0
- package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
- package/internal/lspserver/lsp_project_diagnostics.go +235 -0
- package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
- package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
- package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
- package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
- package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
- package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
- package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
- package/internal/lspserver/lsp_project_input_registration.go +504 -0
- package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
- package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
- package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
- package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
- package/internal/lspserver/lsp_project_inputs.go +944 -0
- package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
- package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
- package/internal/lspserver/lsp_proxy.go +591 -28
- package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
- package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
- package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
- package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
- package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
- package/lib/compiler/internal/project/readProjectConfig.js +46 -14
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
- package/lib/compiler/internal/residentCheckProcess.js +227 -0
- package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +64 -1
- package/lib/compiler/internal/runBuild.js +484 -7
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +1 -0
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/flags/parser.js +13 -17
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +10 -3
- package/lib/flags/schema.js +99 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/projectInputPathIdentity.d.ts +23 -0
- package/lib/internal/projectInputPathIdentity.js +222 -0
- package/lib/internal/projectInputPathIdentity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.d.ts +17 -0
- package/lib/launcher/internal/runTtsc.js +121 -88
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +13 -1
- package/lib/launcher/internal/runTtscserver.js +415 -45
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
- package/lib/launcher/internal/singleFileOutput.js +103 -0
- package/lib/launcher/internal/singleFileOutput.js.map +1 -0
- package/lib/launcher/internal/watchTopology.d.ts +214 -2
- package/lib/launcher/internal/watchTopology.js +1560 -41
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +5 -0
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
- package/package.json +8 -8
- package/src/compiler/internal/project/readProjectConfig.ts +62 -8
- package/src/compiler/internal/residentCheckProcess.ts +279 -0
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
- package/src/compiler/internal/runBuild.ts +728 -12
- package/src/compiler/internal/runSingleFileEmit.ts +1 -0
- package/src/flags/parser.ts +17 -17
- package/src/flags/schema.ts +111 -3
- package/src/internal/projectInputPathIdentity.ts +273 -0
- package/src/launcher/internal/runTtsc.ts +141 -117
- package/src/launcher/internal/runTtscserver.ts +561 -54
- package/src/launcher/internal/singleFileOutput.ts +133 -0
- package/src/launcher/internal/watchTopology.ts +2008 -59
- package/src/plugin/internal/loadProjectPlugins.ts +8 -0
- package/src/structures/ITtscPluginCapabilities.ts +34 -0
- package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
|
@@ -12,7 +12,14 @@ import { resolveProjectIdentity } from "./resolveProjectConfig";
|
|
|
12
12
|
* Compiler option keys whose values are file-system paths that must be resolved
|
|
13
13
|
* relative to the tsconfig that declares them, not the project root.
|
|
14
14
|
*/
|
|
15
|
-
const PATH_OPTIONS = new Set([
|
|
15
|
+
const PATH_OPTIONS = new Set([
|
|
16
|
+
"baseUrl",
|
|
17
|
+
"declarationDir",
|
|
18
|
+
"outFile",
|
|
19
|
+
"rootDir",
|
|
20
|
+
"tsBuildInfoFile",
|
|
21
|
+
]);
|
|
22
|
+
const CONFIG_DIR_TEMPLATE = "${configDir}";
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* Intermediate type used during `extends`-chain resolution before the result is
|
|
@@ -35,9 +42,11 @@ type ResolvedCompilerOptions = {
|
|
|
35
42
|
*
|
|
36
43
|
* Follows `extends` chains (including arrays) and merges compiler options with
|
|
37
44
|
* later configs taking precedence. Path-typed options (`baseUrl`,
|
|
38
|
-
* `declarationDir`, `rootDir`) are resolved
|
|
39
|
-
*
|
|
40
|
-
*
|
|
45
|
+
* `declarationDir`, `outFile`, `rootDir`, `tsBuildInfoFile`) are resolved
|
|
46
|
+
* relative to the config that declares them. `${configDir}` paths are resolved
|
|
47
|
+
* against the final consuming config after the extends chain is merged. The
|
|
48
|
+
* `outDir` is resolved to an absolute path. Plugins are inherited from the
|
|
49
|
+
* nearest ancestor that declares them.
|
|
41
50
|
*/
|
|
42
51
|
export function readProjectConfig(
|
|
43
52
|
opts: ITtscProjectLocatorOptions = {},
|
|
@@ -45,7 +54,11 @@ export function readProjectConfig(
|
|
|
45
54
|
const identity = resolveProjectIdentity(opts);
|
|
46
55
|
const tsconfig = identity.physicalConfigPath;
|
|
47
56
|
const root = identity.physicalProjectRoot;
|
|
48
|
-
const compilerOptions = readResolvedCompilerOptions(
|
|
57
|
+
const compilerOptions = readResolvedCompilerOptions(
|
|
58
|
+
tsconfig,
|
|
59
|
+
new Set(),
|
|
60
|
+
path.dirname(tsconfig),
|
|
61
|
+
);
|
|
49
62
|
return {
|
|
50
63
|
configPaths: compilerOptions.configPaths,
|
|
51
64
|
compilerOptions: {
|
|
@@ -97,6 +110,7 @@ function resolveAbsolutePath(cwd: string, target: string): string {
|
|
|
97
110
|
function readResolvedCompilerOptions(
|
|
98
111
|
tsconfig: string,
|
|
99
112
|
seen: Set<string> = new Set(),
|
|
113
|
+
configDir = path.dirname(tsconfig),
|
|
100
114
|
): ResolvedCompilerOptions {
|
|
101
115
|
const canonical = resolveRealPath(tsconfig);
|
|
102
116
|
if (seen.has(canonical)) {
|
|
@@ -112,7 +126,12 @@ function readResolvedCompilerOptions(
|
|
|
112
126
|
};
|
|
113
127
|
};
|
|
114
128
|
const own = parsed.compilerOptions;
|
|
115
|
-
const base = resolveBaseCompilerOptions(
|
|
129
|
+
const base = resolveBaseCompilerOptions(
|
|
130
|
+
canonical,
|
|
131
|
+
parsed.extends,
|
|
132
|
+
seen,
|
|
133
|
+
configDir,
|
|
134
|
+
);
|
|
116
135
|
const ownBaseDir = path.dirname(canonical);
|
|
117
136
|
const ownOptionBaseDirs =
|
|
118
137
|
own === undefined
|
|
@@ -130,6 +149,7 @@ function readResolvedCompilerOptions(
|
|
|
130
149
|
...(own ?? {}),
|
|
131
150
|
},
|
|
132
151
|
optionBaseDirs,
|
|
152
|
+
configDir,
|
|
133
153
|
);
|
|
134
154
|
const ownPlugins = own?.plugins;
|
|
135
155
|
const pluginsDeclared = Array.isArray(ownPlugins);
|
|
@@ -142,7 +162,7 @@ function readResolvedCompilerOptions(
|
|
|
142
162
|
options,
|
|
143
163
|
outDir:
|
|
144
164
|
typeof own?.outDir === "string"
|
|
145
|
-
?
|
|
165
|
+
? resolveCompilerOptionPath(ownBaseDir, configDir, own.outDir)
|
|
146
166
|
: base.outDir,
|
|
147
167
|
pluginBaseDirs: pluginsDeclared
|
|
148
168
|
? plugins.map(() => ownBaseDir)
|
|
@@ -169,11 +189,13 @@ function resolveBaseCompilerOptions(
|
|
|
169
189
|
tsconfig: string,
|
|
170
190
|
extended: unknown,
|
|
171
191
|
seen: Set<string>,
|
|
192
|
+
configDir: string,
|
|
172
193
|
): ResolvedCompilerOptions {
|
|
173
194
|
if (typeof extended === "string") {
|
|
174
195
|
return readResolvedCompilerOptions(
|
|
175
196
|
resolveExtendsConfig(tsconfig, extended),
|
|
176
197
|
seen,
|
|
198
|
+
configDir,
|
|
177
199
|
);
|
|
178
200
|
}
|
|
179
201
|
if (!Array.isArray(extended)) {
|
|
@@ -201,6 +223,7 @@ function resolveBaseCompilerOptions(
|
|
|
201
223
|
const current = readResolvedCompilerOptions(
|
|
202
224
|
resolveExtendsConfig(tsconfig, specifier),
|
|
203
225
|
seen,
|
|
226
|
+
configDir,
|
|
204
227
|
);
|
|
205
228
|
merged = {
|
|
206
229
|
configPaths: uniquePaths([...merged.configPaths, ...current.configPaths]),
|
|
@@ -234,18 +257,49 @@ function uniquePaths(paths: readonly string[]): string[] {
|
|
|
234
257
|
function resolvePathOptions(
|
|
235
258
|
options: Record<string, unknown>,
|
|
236
259
|
baseDirs: Record<string, string>,
|
|
260
|
+
configDir: string,
|
|
237
261
|
): Record<string, unknown> {
|
|
238
262
|
const resolved = { ...options };
|
|
239
263
|
for (const key of PATH_OPTIONS) {
|
|
240
264
|
const value = resolved[key];
|
|
241
265
|
const baseDir = baseDirs[key];
|
|
242
266
|
if (typeof value === "string" && baseDir !== undefined) {
|
|
243
|
-
resolved[key] =
|
|
267
|
+
resolved[key] = resolveCompilerOptionPath(baseDir, configDir, value);
|
|
244
268
|
}
|
|
245
269
|
}
|
|
246
270
|
return resolved;
|
|
247
271
|
}
|
|
248
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Resolve an ordinary path from the config that declares it, but substitute a
|
|
275
|
+
* leading `${configDir}` from the final config consuming the merged preset.
|
|
276
|
+
*/
|
|
277
|
+
function resolveCompilerOptionPath(
|
|
278
|
+
declaringDir: string,
|
|
279
|
+
configDir: string,
|
|
280
|
+
value: string,
|
|
281
|
+
): string {
|
|
282
|
+
if (
|
|
283
|
+
value.slice(0, CONFIG_DIR_TEMPLATE.length).toLowerCase() !==
|
|
284
|
+
CONFIG_DIR_TEMPLATE.toLowerCase()
|
|
285
|
+
) {
|
|
286
|
+
return resolveAbsolutePath(declaringDir, value);
|
|
287
|
+
}
|
|
288
|
+
// The compiler picks the base directory from a case-insensitive prefix but
|
|
289
|
+
// substitutes only the exact spelling, so a mis-cased template still anchors
|
|
290
|
+
// here while staying in the path as written. Both halves are reproduced: this
|
|
291
|
+
// function exists to say where the compiler will put the file, and a value
|
|
292
|
+
// the compiler leaves literal is not ours to interpret.
|
|
293
|
+
//
|
|
294
|
+
// The exact spelling becomes "./" before the result is normalized, which
|
|
295
|
+
// keeps the value relative even when its remainder resembles an absolute
|
|
296
|
+
// Windows path. Either config-file separator is accepted on every host.
|
|
297
|
+
const substituted = value.startsWith(CONFIG_DIR_TEMPLATE)
|
|
298
|
+
? `./${value.slice(CONFIG_DIR_TEMPLATE.length)}`
|
|
299
|
+
: value;
|
|
300
|
+
return resolveAbsolutePath(configDir, substituted.replaceAll("\\", "/"));
|
|
301
|
+
}
|
|
302
|
+
|
|
249
303
|
/**
|
|
250
304
|
* Resolve an `extends` specifier to an absolute path using the same rules as
|
|
251
305
|
* TypeScript's config resolution:
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { type Interface, createInterface } from "node:readline";
|
|
3
|
+
|
|
4
|
+
import type { TtscBuildResult } from "../../structures/internal/TtscBuildResult";
|
|
5
|
+
import { normalizeRequestTimeoutMs } from "./residentTransformProcess";
|
|
6
|
+
|
|
7
|
+
const STDERR_TAIL_LIMIT = 64 * 1024;
|
|
8
|
+
const REPLY_ECHO_LIMIT = 200;
|
|
9
|
+
const TERMINATION_GRACE_MS = 1_000;
|
|
10
|
+
|
|
11
|
+
export interface ResidentCheckProcessOptions {
|
|
12
|
+
args: readonly string[];
|
|
13
|
+
binary: string;
|
|
14
|
+
cwd: string;
|
|
15
|
+
env: NodeJS.ProcessEnv;
|
|
16
|
+
requestTimeoutMs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ResidentCheckRequest {
|
|
20
|
+
changed?: readonly string[];
|
|
21
|
+
external?: readonly string[];
|
|
22
|
+
invalidate?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ResidentCheckTelemetry {
|
|
26
|
+
pid: number;
|
|
27
|
+
programLoads: number;
|
|
28
|
+
programUpdates: number;
|
|
29
|
+
reused: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ResidentCheckResult = TtscBuildResult & {
|
|
33
|
+
telemetry: ResidentCheckTelemetry;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type PendingRequest = {
|
|
37
|
+
reject(reason: Error): void;
|
|
38
|
+
resolve(result: ResidentCheckResult): void;
|
|
39
|
+
timer: NodeJS.Timeout;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* FIFO JSON-line client for a check-stage `check-serve` sidecar.
|
|
44
|
+
*
|
|
45
|
+
* A watch session serializes cycles, but the client still queues replies so a
|
|
46
|
+
* caller cannot accidentally pair a late response with the next request. Any
|
|
47
|
+
* framing failure retires the process; the launcher then falls back to the
|
|
48
|
+
* ordinary one-shot command for that cycle.
|
|
49
|
+
*/
|
|
50
|
+
export class ResidentCheckProcess {
|
|
51
|
+
private readonly child: ChildProcess;
|
|
52
|
+
private readonly pending: PendingRequest[] = [];
|
|
53
|
+
private readonly reader: Interface;
|
|
54
|
+
private readonly requestTimeoutMs: number;
|
|
55
|
+
private failure: Error | undefined;
|
|
56
|
+
private stderr = "";
|
|
57
|
+
|
|
58
|
+
public constructor(options: ResidentCheckProcessOptions) {
|
|
59
|
+
this.requestTimeoutMs = normalizeRequestTimeoutMs(options.requestTimeoutMs);
|
|
60
|
+
this.child = spawn(options.binary, [...options.args], {
|
|
61
|
+
cwd: options.cwd,
|
|
62
|
+
env: options.env,
|
|
63
|
+
windowsHide: true,
|
|
64
|
+
});
|
|
65
|
+
const stdin = this.child.stdin;
|
|
66
|
+
const stdout = this.child.stdout;
|
|
67
|
+
if (stdin === null || stdout === null) {
|
|
68
|
+
this.child.kill();
|
|
69
|
+
throw new Error("ttsc: resident check host has no stdio pipes");
|
|
70
|
+
}
|
|
71
|
+
this.reader = createInterface({ input: stdout });
|
|
72
|
+
this.reader.on("line", (line) => this.onLine(line));
|
|
73
|
+
this.reader.on("close", () => {
|
|
74
|
+
if (this.failure === undefined) this.fail(this.exitError());
|
|
75
|
+
});
|
|
76
|
+
this.child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
77
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
78
|
+
this.stderr = (this.stderr + text).slice(-STDERR_TAIL_LIMIT);
|
|
79
|
+
});
|
|
80
|
+
this.child.on("error", (error) => this.fail(error));
|
|
81
|
+
stdin.on("error", (error) => this.fail(error));
|
|
82
|
+
stdout.on("error", (error) => this.fail(error));
|
|
83
|
+
this.child.stderr?.on("error", () => {});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public request(payload: ResidentCheckRequest): Promise<ResidentCheckResult> {
|
|
87
|
+
if (this.failure !== undefined) return Promise.reject(this.failure);
|
|
88
|
+
const stdin = this.child.stdin;
|
|
89
|
+
if (stdin === null || stdin.destroyed) {
|
|
90
|
+
return Promise.reject(
|
|
91
|
+
new Error("ttsc: resident check host stdin is closed"),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
let line: string;
|
|
95
|
+
try {
|
|
96
|
+
line = `${JSON.stringify(payload)}\n`;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return Promise.reject(asError(error));
|
|
99
|
+
}
|
|
100
|
+
return new Promise<ResidentCheckResult>((resolve, reject) => {
|
|
101
|
+
const pending: PendingRequest = {
|
|
102
|
+
reject,
|
|
103
|
+
resolve,
|
|
104
|
+
timer: setTimeout(() => {
|
|
105
|
+
const detail = stderrSuffix(this.stderr);
|
|
106
|
+
const timeout = new Error(
|
|
107
|
+
`ttsc: resident check request timed out after ${String(
|
|
108
|
+
this.requestTimeoutMs,
|
|
109
|
+
)} ms${detail}`,
|
|
110
|
+
);
|
|
111
|
+
this.settle(pending, timeout);
|
|
112
|
+
this.fail(timeout);
|
|
113
|
+
}, this.requestTimeoutMs),
|
|
114
|
+
};
|
|
115
|
+
pending.timer.unref();
|
|
116
|
+
this.pending.push(pending);
|
|
117
|
+
try {
|
|
118
|
+
stdin.write(line, (error) => {
|
|
119
|
+
if (error === null || error === undefined) return;
|
|
120
|
+
this.settle(pending, error);
|
|
121
|
+
this.fail(error);
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const reason = asError(error);
|
|
125
|
+
this.settle(pending, reason);
|
|
126
|
+
this.fail(reason);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public dispose(): void {
|
|
132
|
+
if (this.failure !== undefined) return;
|
|
133
|
+
this.fail(new Error("ttsc: resident check host disposed"));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private onLine(line: string): void {
|
|
137
|
+
if (this.failure !== undefined || line.trim().length === 0) return;
|
|
138
|
+
const pending = this.pending[0];
|
|
139
|
+
if (pending === undefined) {
|
|
140
|
+
this.fail(
|
|
141
|
+
new Error("ttsc: resident check host sent an unsolicited reply"),
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const result = parseResidentCheckResult(line);
|
|
146
|
+
if (result === undefined) {
|
|
147
|
+
const error = new Error(
|
|
148
|
+
`ttsc: resident check host sent a malformed reply: ${echoLine(line)}`,
|
|
149
|
+
);
|
|
150
|
+
this.settle(pending, error);
|
|
151
|
+
this.fail(error);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.settle(pending, result);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private settle(
|
|
158
|
+
pending: PendingRequest,
|
|
159
|
+
result: Error | ResidentCheckResult,
|
|
160
|
+
): void {
|
|
161
|
+
const index = this.pending.indexOf(pending);
|
|
162
|
+
if (index === -1) return;
|
|
163
|
+
this.pending.splice(index, 1);
|
|
164
|
+
clearTimeout(pending.timer);
|
|
165
|
+
if (result instanceof Error) pending.reject(result);
|
|
166
|
+
else pending.resolve(result);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private fail(error: Error): void {
|
|
170
|
+
if (this.failure !== undefined) return;
|
|
171
|
+
this.failure = error;
|
|
172
|
+
while (this.pending.length !== 0) {
|
|
173
|
+
this.settle(this.pending[0]!, error);
|
|
174
|
+
}
|
|
175
|
+
this.terminate();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private terminate(): void {
|
|
179
|
+
const stdin = this.child.stdin;
|
|
180
|
+
if (stdin !== null && !stdin.destroyed) stdin.destroy();
|
|
181
|
+
if (this.child.exitCode !== null || this.child.signalCode !== null) return;
|
|
182
|
+
try {
|
|
183
|
+
this.child.kill();
|
|
184
|
+
} catch {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const force = setTimeout(() => {
|
|
188
|
+
if (this.child.exitCode !== null || this.child.signalCode !== null)
|
|
189
|
+
return;
|
|
190
|
+
try {
|
|
191
|
+
this.child.kill("SIGKILL");
|
|
192
|
+
} catch {
|
|
193
|
+
// The host exited between the liveness check and forced termination.
|
|
194
|
+
}
|
|
195
|
+
}, TERMINATION_GRACE_MS);
|
|
196
|
+
force.unref();
|
|
197
|
+
this.child.once("exit", () => clearTimeout(force));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private exitError(): Error {
|
|
201
|
+
const detail = this.stderr.trim();
|
|
202
|
+
if (detail.length !== 0) return new Error(detail);
|
|
203
|
+
const signal =
|
|
204
|
+
this.child.signalCode === null ? "" : `, signal ${this.child.signalCode}`;
|
|
205
|
+
return new Error(
|
|
206
|
+
`ttsc: resident check host exited (code ${
|
|
207
|
+
this.child.exitCode ?? "null"
|
|
208
|
+
}${signal})`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseResidentCheckResult(
|
|
214
|
+
line: string,
|
|
215
|
+
): ResidentCheckResult | undefined {
|
|
216
|
+
let value: unknown;
|
|
217
|
+
try {
|
|
218
|
+
value = JSON.parse(line);
|
|
219
|
+
} catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
!isRecord(value) ||
|
|
224
|
+
typeof value.status !== "number" ||
|
|
225
|
+
!Number.isInteger(value.status)
|
|
226
|
+
) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
if (typeof value.stdout !== "string" || typeof value.stderr !== "string") {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
const telemetry = value.telemetry;
|
|
233
|
+
if (
|
|
234
|
+
!isRecord(telemetry) ||
|
|
235
|
+
typeof telemetry.pid !== "number" ||
|
|
236
|
+
!Number.isSafeInteger(telemetry.pid) ||
|
|
237
|
+
typeof telemetry.programLoads !== "number" ||
|
|
238
|
+
!Number.isSafeInteger(telemetry.programLoads) ||
|
|
239
|
+
typeof telemetry.programUpdates !== "number" ||
|
|
240
|
+
!Number.isSafeInteger(telemetry.programUpdates) ||
|
|
241
|
+
typeof telemetry.reused !== "boolean"
|
|
242
|
+
) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
diagnostics: [],
|
|
247
|
+
status: value.status,
|
|
248
|
+
stderr: value.stderr,
|
|
249
|
+
stdout: value.stdout,
|
|
250
|
+
telemetry: {
|
|
251
|
+
pid: telemetry.pid,
|
|
252
|
+
programLoads: telemetry.programLoads,
|
|
253
|
+
programUpdates: telemetry.programUpdates,
|
|
254
|
+
reused: telemetry.reused,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
260
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function echoLine(line: string): string {
|
|
264
|
+
const trimmed = line.trim();
|
|
265
|
+
return JSON.stringify(
|
|
266
|
+
trimmed.length <= REPLY_ECHO_LIMIT
|
|
267
|
+
? trimmed
|
|
268
|
+
: `${trimmed.slice(0, REPLY_ECHO_LIMIT)}…`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function stderrSuffix(stderr: string): string {
|
|
273
|
+
const detail = stderr.trim();
|
|
274
|
+
return detail.length === 0 ? "" : `: ${detail}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function asError(error: unknown): Error {
|
|
278
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
279
|
+
}
|
|
@@ -10,7 +10,8 @@ import { isOutsideRelativePath } from "./paths";
|
|
|
10
10
|
*
|
|
11
11
|
* 1. Try to derive the exact output path by mirroring the source's relative
|
|
12
12
|
* position inside `projectRoot` into `outDir`, applying the correct JS
|
|
13
|
-
* extension (`.js` / `.mjs` / `.cjs`). Use this path if it exists
|
|
13
|
+
* extension (`.js` / `.jsx` / `.mjs` / `.cjs`). Use this path if it exists
|
|
14
|
+
* on disk.
|
|
14
15
|
* 2. Fall back to scoring each candidate in `emittedFiles` (or a recursive
|
|
15
16
|
* directory scan of `outDir`) by the number of trailing path-stem segments
|
|
16
17
|
* shared with the source file name, and pick the highest-scoring existing
|
|
@@ -25,13 +26,23 @@ export function resolveEmittedJavaScript(options: {
|
|
|
25
26
|
projectRoot: string;
|
|
26
27
|
sourceFile: string;
|
|
27
28
|
}): string | null {
|
|
28
|
-
const exact =
|
|
29
|
+
const exact = resolveExactEmittedFiles(
|
|
29
30
|
options.outDir,
|
|
30
31
|
options.projectRoot,
|
|
31
32
|
options.sourceFile,
|
|
32
33
|
);
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
const emitted = new Set(
|
|
35
|
+
options.emittedFiles?.map((file) => emittedPathKey(file)) ?? [],
|
|
36
|
+
);
|
|
37
|
+
for (const candidate of exact) {
|
|
38
|
+
if (emitted.has(emittedPathKey(candidate)) && fs.existsSync(candidate)) {
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const candidate of exact) {
|
|
43
|
+
if (fs.existsSync(candidate)) {
|
|
44
|
+
return candidate;
|
|
45
|
+
}
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
// Score the pre-computed emit list first (cheap). When it yields nothing —
|
|
@@ -81,22 +92,24 @@ function bestStemMatch(
|
|
|
81
92
|
|
|
82
93
|
/**
|
|
83
94
|
* Derive the exact output path for `sourceFile` by mirroring its position
|
|
84
|
-
* relative to `projectRoot` into `outDir`. Returns
|
|
85
|
-
* not inside the project root or when the path cannot be determined.
|
|
95
|
+
* relative to `projectRoot` into `outDir`. Returns no candidates when the
|
|
96
|
+
* source is not inside the project root or when the path cannot be determined.
|
|
86
97
|
*/
|
|
87
|
-
function
|
|
98
|
+
function resolveExactEmittedFiles(
|
|
88
99
|
outDir: string,
|
|
89
100
|
projectRoot: string,
|
|
90
101
|
sourceFile: string,
|
|
91
|
-
): string
|
|
102
|
+
): string[] {
|
|
92
103
|
const relative = path.relative(projectRoot, sourceFile);
|
|
93
104
|
if (relative === "" || isOutsideRelativePath(relative)) {
|
|
94
|
-
return
|
|
105
|
+
return [];
|
|
95
106
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
relative.
|
|
99
|
-
|
|
107
|
+
const stem = relative.slice(
|
|
108
|
+
0,
|
|
109
|
+
relative.length - path.extname(relative).length,
|
|
110
|
+
);
|
|
111
|
+
return emittedJavaScriptExtensions(sourceFile).map((extension) =>
|
|
112
|
+
path.resolve(outDir, stem + extension),
|
|
100
113
|
);
|
|
101
114
|
}
|
|
102
115
|
|
|
@@ -149,21 +162,30 @@ function sharedSourceStemSegments(outPath: string, srcPath: string): number {
|
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
/**
|
|
152
|
-
* Map a
|
|
153
|
-
*
|
|
165
|
+
* Map a source extension to every JavaScript output counterpart tsgo can use.
|
|
166
|
+
* JSX preserve mode writes `.tsx`/`.jsx` inputs as `.jsx`; all other JSX modes
|
|
167
|
+
* write `.js`.
|
|
154
168
|
*/
|
|
155
|
-
function
|
|
169
|
+
function emittedJavaScriptExtensions(filename: string): readonly string[] {
|
|
156
170
|
switch (path.extname(filename).toLowerCase()) {
|
|
157
171
|
case ".mts":
|
|
158
|
-
return ".mjs";
|
|
172
|
+
return [".mjs"];
|
|
159
173
|
case ".cts":
|
|
160
|
-
return ".cjs";
|
|
174
|
+
return [".cjs"];
|
|
175
|
+
case ".tsx":
|
|
176
|
+
case ".jsx":
|
|
177
|
+
return [".js", ".jsx"];
|
|
161
178
|
default:
|
|
162
|
-
return ".js";
|
|
179
|
+
return [".js"];
|
|
163
180
|
}
|
|
164
181
|
}
|
|
165
182
|
|
|
166
|
-
/** Return true when `filename` has a
|
|
183
|
+
/** Return true when `filename` has a JavaScript output extension. */
|
|
167
184
|
function isJavaScriptOutput(filename: string): boolean {
|
|
168
|
-
return /\.(?:[cm]?js)$/i.test(filename);
|
|
185
|
+
return /\.(?:[cm]?js|jsx)$/i.test(filename);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function emittedPathKey(filename: string): string {
|
|
189
|
+
const resolved = path.resolve(filename);
|
|
190
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
169
191
|
}
|