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.
Files changed (95) hide show
  1. package/cmd/ttscserver/main.go +79 -3
  2. package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
  3. package/driver/lsp.go +4 -0
  4. package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
  5. package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
  6. package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
  7. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
  8. package/internal/lspserver/lsp_native_plugin_source.go +171 -55
  9. package/internal/lspserver/lsp_native_resident.go +110 -9
  10. package/internal/lspserver/lsp_plugin.go +6 -0
  11. package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
  12. package/internal/lspserver/lsp_project_diagnostics.go +235 -0
  13. package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
  14. package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
  15. package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
  16. package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
  17. package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
  18. package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
  19. package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
  20. package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
  21. package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
  22. package/internal/lspserver/lsp_project_input_registration.go +504 -0
  23. package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
  24. package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
  25. package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
  26. package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
  27. package/internal/lspserver/lsp_project_inputs.go +944 -0
  28. package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
  29. package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
  30. package/internal/lspserver/lsp_proxy.go +591 -28
  31. package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
  32. package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
  33. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
  34. package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
  35. package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
  36. package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
  37. package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
  38. package/lib/compiler/internal/project/readProjectConfig.js +46 -14
  39. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  40. package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
  41. package/lib/compiler/internal/residentCheckProcess.js +227 -0
  42. package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
  43. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
  44. package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
  45. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  46. package/lib/compiler/internal/runBuild.d.ts +64 -1
  47. package/lib/compiler/internal/runBuild.js +484 -7
  48. package/lib/compiler/internal/runBuild.js.map +1 -1
  49. package/lib/compiler/internal/runSingleFileEmit.js +1 -0
  50. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  51. package/lib/flags/parser.js +13 -17
  52. package/lib/flags/parser.js.map +1 -1
  53. package/lib/flags/schema.d.ts +10 -3
  54. package/lib/flags/schema.js +99 -0
  55. package/lib/flags/schema.js.map +1 -1
  56. package/lib/internal/projectInputPathIdentity.d.ts +23 -0
  57. package/lib/internal/projectInputPathIdentity.js +222 -0
  58. package/lib/internal/projectInputPathIdentity.js.map +1 -0
  59. package/lib/launcher/internal/runTtsc.d.ts +17 -0
  60. package/lib/launcher/internal/runTtsc.js +121 -88
  61. package/lib/launcher/internal/runTtsc.js.map +1 -1
  62. package/lib/launcher/internal/runTtscserver.d.ts +13 -1
  63. package/lib/launcher/internal/runTtscserver.js +415 -45
  64. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  65. package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
  66. package/lib/launcher/internal/singleFileOutput.js +103 -0
  67. package/lib/launcher/internal/singleFileOutput.js.map +1 -0
  68. package/lib/launcher/internal/watchTopology.d.ts +214 -2
  69. package/lib/launcher/internal/watchTopology.js +1560 -41
  70. package/lib/launcher/internal/watchTopology.js.map +1 -1
  71. package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
  72. package/lib/plugin/internal/loadProjectPlugins.js +5 -0
  73. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  74. package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
  75. package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
  76. package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
  77. package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
  78. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
  79. package/package.json +8 -8
  80. package/src/compiler/internal/project/readProjectConfig.ts +62 -8
  81. package/src/compiler/internal/residentCheckProcess.ts +279 -0
  82. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
  83. package/src/compiler/internal/runBuild.ts +728 -12
  84. package/src/compiler/internal/runSingleFileEmit.ts +1 -0
  85. package/src/flags/parser.ts +17 -17
  86. package/src/flags/schema.ts +111 -3
  87. package/src/internal/projectInputPathIdentity.ts +273 -0
  88. package/src/launcher/internal/runTtsc.ts +141 -117
  89. package/src/launcher/internal/runTtscserver.ts +561 -54
  90. package/src/launcher/internal/singleFileOutput.ts +133 -0
  91. package/src/launcher/internal/watchTopology.ts +2008 -59
  92. package/src/plugin/internal/loadProjectPlugins.ts +8 -0
  93. package/src/structures/ITtscPluginCapabilities.ts +34 -0
  94. package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
  95. 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(["baseUrl", "declarationDir", "rootDir"]);
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 relative to the config that
39
- * declares them. The `outDir` is resolved to an absolute path. Plugins are
40
- * inherited from the nearest ancestor that declares them.
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(tsconfig);
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(canonical, parsed.extends, seen);
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
- ? resolveAbsolutePath(ownBaseDir, own.outDir)
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] = resolveAbsolutePath(baseDir, value);
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 on disk.
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 = resolveExactEmittedFile(
29
+ const exact = resolveExactEmittedFiles(
29
30
  options.outDir,
30
31
  options.projectRoot,
31
32
  options.sourceFile,
32
33
  );
33
- if (exact && fs.existsSync(exact)) {
34
- return exact;
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 `null` when the source is
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 resolveExactEmittedFile(
98
+ function resolveExactEmittedFiles(
88
99
  outDir: string,
89
100
  projectRoot: string,
90
101
  sourceFile: string,
91
- ): string | null {
102
+ ): string[] {
92
103
  const relative = path.relative(projectRoot, sourceFile);
93
104
  if (relative === "" || isOutsideRelativePath(relative)) {
94
- return null;
105
+ return [];
95
106
  }
96
- return path.resolve(
97
- outDir,
98
- relative.slice(0, relative.length - path.extname(relative).length) +
99
- emittedJavaScriptExtension(sourceFile),
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 TypeScript source extension to its JavaScript output counterpart.
153
- * `.mts` → `.mjs`, `.cts` → `.cjs`, everything else → `.js`.
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 emittedJavaScriptExtension(filename: string): string {
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 `.js`, `.mjs`, or `.cjs` extension. */
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
  }