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
@@ -1,19 +1,47 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
 
7
+ import { createNativeProjectContextArgs } from "../../compiler/internal/project/createNativeProjectContextArgs";
6
8
  import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
7
9
  import { resolveBinary } from "../../compiler/internal/resolveBinary";
8
10
  import { resolveTsgo } from "../../compiler/internal/resolveTsgo";
11
+ import { resolveProjectInputPath } from "../../internal/projectInputPathIdentity";
9
12
  import {
10
13
  hasProjectPluginEntries,
11
14
  loadProjectPlugins,
12
15
  } from "../../plugin/internal/loadProjectPlugins";
13
16
  import type { ITtscLoadedNativePlugin } from "../../structures/internal/ITtscLoadedNativePlugin";
17
+ import type { ITtscParsedProjectConfig } from "../../structures/internal/ITtscParsedProjectConfig";
14
18
  import type { ITtscProjectIdentity } from "../../structures/internal/ITtscProjectIdentity";
19
+ import type { ITtscProjectInputSnapshot } from "../../structures/internal/ITtscProjectInputSnapshot";
15
20
  import { resolveTtscserverBinary } from "./resolveTtscserverBinary";
16
21
 
22
+ type InitialLSPProjectInputSnapshot = ITtscProjectInputSnapshot & {
23
+ reloadDirectoryDigests: Readonly<Record<string, string>>;
24
+ reloadFileDigests: Readonly<Record<string, string>>;
25
+ };
26
+
27
+ type LSPExecutionContext = {
28
+ initialProjectInputs: ReadonlyMap<string, InitialLSPProjectInputSnapshot>;
29
+ nativePlugins: readonly ITtscLoadedNativePlugin[];
30
+ projectContext?: ITtscProjectIdentity;
31
+ tsgoBinary: string;
32
+ };
33
+
34
+ type TtscserverEnvironment = {
35
+ /** Flags prepended to the native invocation, ahead of the caller's argv. */
36
+ args: readonly string[];
37
+ dispose(): void;
38
+ env: NodeJS.ProcessEnv;
39
+ };
40
+
41
+ const LSP_SELECTION_STABILITY_ATTEMPTS = 3;
42
+ const LSP_PROJECT_INPUT_TIMEOUT_MS = 30_000;
43
+ const LSP_PROJECT_INPUT_MAX_BUFFER = 4 * 1024 * 1024;
44
+
17
45
  /**
18
46
  * Drive the ttscserver native binary from a node launcher. The launcher is
19
47
  * deliberately thin: argument parsing, version banners, and help text are owned
@@ -22,7 +50,7 @@ import { resolveTtscserverBinary } from "./resolveTtscserverBinary";
22
50
  *
23
51
  * - Resolve the platform binary,
24
52
  * - Resolve the project TypeScript-Go binary for the native wrapper,
25
- * - Resolve the project config and build the LSP plugin manifest environment,
53
+ * - Resolve the project config and materialize the private LSP plugin manifest,
26
54
  * - Inject the Node/ttsx helper paths used by disk-backed LSP sidecars,
27
55
  * - Inject `--stdio` when the first arg is not a meta-command,
28
56
  * - Delegate to the binary with inherited stdio so OS-level signals reach the
@@ -44,20 +72,25 @@ export function runTtscserver(
44
72
  ensureExecutable(binary);
45
73
 
46
74
  const args = needsStdio(argv) ? ["--stdio", ...argv] : [...argv];
47
- let env: NodeJS.ProcessEnv;
75
+ let execution: TtscserverEnvironment;
48
76
  try {
49
- env = resolveTtscserverEnv(args);
77
+ execution = resolveTtscserverEnv(args);
50
78
  } catch (error) {
51
79
  process.stderr.write(
52
80
  `ttscserver: ${stripTtscPrefix(formatError(error))}\n`,
53
81
  );
54
82
  return 1;
55
83
  }
56
- const result = spawnSync(binary, args, {
57
- stdio: "inherit",
58
- env,
59
- windowsHide: true,
60
- });
84
+ let result: ReturnType<typeof spawnSync>;
85
+ try {
86
+ result = spawnSync(binary, [...execution.args, ...args], {
87
+ stdio: "inherit",
88
+ env: execution.env,
89
+ windowsHide: true,
90
+ });
91
+ } finally {
92
+ execution.dispose();
93
+ }
61
94
  if (result.error) {
62
95
  process.stderr.write(`ttscserver: ${result.error.message}\n`);
63
96
  return 1;
@@ -80,51 +113,106 @@ export function runTtscserver(
80
113
  /**
81
114
  * Build the environment for the native binary. In `--stdio` (LSP) mode the Go
82
115
  * binary needs the project tsgo binary plus any LSP-capable plugin sidecars the
83
- * JS loader resolved from config. Inject those paths through environment
84
- * variables so the native host can stay focused on proxying tsgo and
85
- * dispatching sidecar verbs. Skip `TTSC_TSGO_BINARY` injection when the caller
86
- * already provided the variable or passed an explicit `--tsgo` option.
116
+ * JS loader resolved from config. Pass the manifest through a private temporary
117
+ * file and inject canonical helper paths so the native host and every later
118
+ * sidecar refresh use the same launch context.
87
119
  */
88
- function resolveTtscserverEnv(argv: readonly string[]): NodeJS.ProcessEnv {
120
+ function resolveTtscserverEnv(argv: readonly string[]): TtscserverEnvironment {
89
121
  if (!argv.includes("--stdio")) {
90
122
  // Non-LSP invocations (--version, --help) do not shell out to tsgo.
91
- return process.env;
123
+ return { args: [], dispose() {}, env: process.env };
92
124
  }
93
125
  const context = resolveLspExecutionContext(argv);
126
+ const env = lspSidecarEnvironment({
127
+ pluginConfigOrigin: context.projectContext?.pluginConfigOrigin,
128
+ tsgoBinary: context.tsgoBinary,
129
+ });
130
+ delete env.TTSC_LSP_PLUGINS_JSON;
131
+ delete env.TTSC_LSP_PLUGINS_FILE;
132
+ const lspPlugins = context.nativePlugins.filter(
133
+ (plugin) => plugin.capabilities?.lsp === true,
134
+ );
135
+ if (lspPlugins.length === 0) {
136
+ // Nothing would be lost by an older native host, so keep it startable.
137
+ return { args: [], dispose() {}, env };
138
+ }
139
+ const transport = materializeLSPPluginManifest({
140
+ initialProjectInputs: Object.fromEntries(context.initialProjectInputs),
141
+ plugins: serializeNativePlugins(context.nativePlugins),
142
+ projectContext: context.projectContext,
143
+ lspPlugins: lspPlugins.map((plugin) => ({
144
+ binary: plugin.binary,
145
+ ...(plugin.capabilities?.projectInputs === true
146
+ ? { initialProjectInputKey: lspPluginTransportKey(plugin) }
147
+ : {}),
148
+ name: plugin.name,
149
+ projectDiagnostics: plugin.capabilities?.projectDiagnostics === true,
150
+ projectInputs: plugin.capabilities?.projectInputs === true,
151
+ projectContextArgs: plugin.capabilities?.projectContextArgs === true,
152
+ stage: plugin.stage,
153
+ })),
154
+ });
155
+ // Deliver the manifest as an explicit flag rather than an inherited variable.
156
+ // A native host that predates the flag rejects the invocation instead of
157
+ // starting without the plugins this project declared, and nothing downstream
158
+ // of the host inherits either a path to the manifest or its payload.
159
+ return {
160
+ args: ["--lsp-plugins-file", transport.path],
161
+ dispose: transport.dispose,
162
+ env,
163
+ };
164
+ }
165
+
166
+ export function materializeLSPPluginManifest(manifest: unknown): {
167
+ dispose(): void;
168
+ path: string;
169
+ } {
170
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-lsp-"));
171
+ const location = path.join(directory, "plugins.json");
172
+ try {
173
+ fs.writeFileSync(location, JSON.stringify(manifest), {
174
+ encoding: "utf8",
175
+ flag: "wx",
176
+ mode: 0o600,
177
+ });
178
+ } catch (error) {
179
+ fs.rmSync(directory, { force: true, recursive: true });
180
+ throw error;
181
+ }
182
+ let disposed = false;
183
+ return {
184
+ dispose(): void {
185
+ if (disposed) return;
186
+ disposed = true;
187
+ fs.rmSync(directory, { force: true, recursive: true });
188
+ },
189
+ path: location,
190
+ };
191
+ }
192
+
193
+ function lspSidecarEnvironment(options: {
194
+ pluginConfigOrigin: string | undefined;
195
+ tsgoBinary: string;
196
+ }): NodeJS.ProcessEnv {
94
197
  const env: NodeJS.ProcessEnv = {
95
198
  ...process.env,
96
199
  TTSC_NODE_BINARY: process.env.TTSC_NODE_BINARY ?? process.execPath,
200
+ TTSC_TSGO_BINARY: options.tsgoBinary,
97
201
  TTSC_TTSX_BINARY:
98
202
  process.env.TTSC_TTSX_BINARY ??
99
203
  path.join(__dirname, "..", "..", "launcher", "ttsx.js"),
100
204
  };
101
- delete env.TTSC_LSP_PLUGINS_JSON;
102
- if (!process.env.TTSC_TSGO_BINARY && !hasTsgoOption(argv)) {
103
- env.TTSC_TSGO_BINARY = context.tsgoBinary;
104
- }
105
- const lspPlugins = context.nativePlugins.filter(
106
- (plugin) => plugin.capabilities?.lsp === true,
107
- );
108
- if (lspPlugins.length > 0) {
109
- env.TTSC_LSP_PLUGINS_JSON = JSON.stringify({
110
- plugins: serializeNativePlugins(context.nativePlugins),
111
- projectContext: context.projectContext,
112
- lspPlugins: lspPlugins.map((plugin) => ({
113
- binary: plugin.binary,
114
- name: plugin.name,
115
- projectContextArgs: plugin.capabilities?.projectContextArgs === true,
116
- stage: plugin.stage,
117
- })),
118
- });
205
+ if (options.pluginConfigOrigin === undefined) {
206
+ delete env.TTSC_PLUGIN_CONFIG_DIR;
207
+ } else {
208
+ env.TTSC_PLUGIN_CONFIG_DIR = options.pluginConfigOrigin;
119
209
  }
120
210
  return env;
121
211
  }
122
212
 
123
- function resolveLspExecutionContext(argv: readonly string[]): {
124
- nativePlugins: readonly ITtscLoadedNativePlugin[];
125
- projectContext?: ITtscProjectIdentity;
126
- tsgoBinary: string;
127
- } {
213
+ function resolveLspExecutionContext(
214
+ argv: readonly string[],
215
+ ): LSPExecutionContext {
128
216
  const cwd = path.resolve(optionValue(argv, "--cwd") ?? process.cwd());
129
217
  const tsconfig = optionValue(argv, "--tsconfig");
130
218
  const pluginConfigOrigin =
@@ -132,9 +220,9 @@ function resolveLspExecutionContext(argv: readonly string[]): {
132
220
  process.env.TTSC_PLUGIN_CONFIG_DIR === ""
133
221
  ? undefined
134
222
  : path.resolve(cwd, process.env.TTSC_PLUGIN_CONFIG_DIR);
135
- let project: ReturnType<typeof readProjectConfig>;
223
+ let initialProject: ReturnType<typeof readProjectConfig>;
136
224
  try {
137
- project = readProjectConfig({ cwd, tsconfig });
225
+ initialProject = readProjectConfig({ cwd, tsconfig });
138
226
  } catch (error) {
139
227
  if (tsconfig) {
140
228
  throw error;
@@ -145,16 +233,82 @@ function resolveLspExecutionContext(argv: readonly string[]): {
145
233
  resolveFrom: __filename,
146
234
  });
147
235
  return {
236
+ initialProjectInputs: new Map(),
148
237
  nativePlugins: [],
149
238
  tsgoBinary: tsgo.binary,
150
239
  };
151
240
  }
152
- const tsgo = resolveTsgo({
153
- binary: optionValue(argv, "--tsgo"),
154
- cwd: project.root,
155
- resolveFrom: __filename,
156
- });
157
- const loaded = hasProjectPluginEntries(project)
241
+ let project = initialProject;
242
+ for (
243
+ let attempt = 1;
244
+ attempt <= LSP_SELECTION_STABILITY_ATTEMPTS;
245
+ attempt++
246
+ ) {
247
+ const loaded = loadLSPProjectPlugins(project, cwd, pluginConfigOrigin);
248
+ const selectedProject = loaded.project;
249
+ const tsgo = resolveTsgo({
250
+ binary: optionValue(argv, "--tsgo"),
251
+ cwd: selectedProject.root,
252
+ resolveFrom: __filename,
253
+ });
254
+ const initialProjectInputs = captureInitialLSPProjectInputs({
255
+ nativePlugins: loaded.nativePlugins,
256
+ pluginConfigOrigin,
257
+ project: selectedProject,
258
+ tsgoBinary: tsgo.binary,
259
+ });
260
+ const confirmationProject = readProjectConfig({ cwd, tsconfig });
261
+ const confirmation = loadLSPProjectPlugins(
262
+ confirmationProject,
263
+ cwd,
264
+ pluginConfigOrigin,
265
+ );
266
+ const confirmedProject = confirmation.project;
267
+ const confirmedTsgo = resolveTsgo({
268
+ binary: optionValue(argv, "--tsgo"),
269
+ cwd: confirmedProject.root,
270
+ resolveFrom: __filename,
271
+ });
272
+ const confirmedProjectInputs = captureInitialLSPProjectInputs({
273
+ nativePlugins: confirmation.nativePlugins,
274
+ pluginConfigOrigin,
275
+ project: confirmedProject,
276
+ tsgoBinary: confirmedTsgo.binary,
277
+ });
278
+ if (
279
+ lspSelectionSignature(selectedProject, loaded.nativePlugins) ===
280
+ lspSelectionSignature(confirmedProject, confirmation.nativePlugins) &&
281
+ initialLSPProjectInputsEqual(
282
+ initialProjectInputs,
283
+ confirmedProjectInputs,
284
+ ) &&
285
+ [...confirmedProjectInputs.values()].every(
286
+ initialLSPProjectInputSnapshotIsCurrent,
287
+ )
288
+ ) {
289
+ return {
290
+ initialProjectInputs: confirmedProjectInputs,
291
+ nativePlugins: confirmation.nativePlugins,
292
+ projectContext: {
293
+ ...confirmedProject.identity,
294
+ ...(pluginConfigOrigin === undefined ? {} : { pluginConfigOrigin }),
295
+ },
296
+ tsgoBinary: confirmedTsgo.binary,
297
+ };
298
+ }
299
+ project = confirmedProject;
300
+ }
301
+ throw new Error(
302
+ `ttscserver: project plugin selection remained unstable across ${LSP_SELECTION_STABILITY_ATTEMPTS} bounded startup attempts`,
303
+ );
304
+ }
305
+
306
+ function loadLSPProjectPlugins(
307
+ project: ITtscParsedProjectConfig,
308
+ cwd: string,
309
+ pluginConfigOrigin: string | undefined,
310
+ ): ReturnType<typeof loadProjectPlugins> {
311
+ return hasProjectPluginEntries(project)
158
312
  ? loadProjectPlugins({
159
313
  binary: resolveBinary() ?? "",
160
314
  cwd,
@@ -162,19 +316,372 @@ function resolveLspExecutionContext(argv: readonly string[]): {
162
316
  projectRoot: project.root,
163
317
  tsconfig: project.path,
164
318
  })
165
- : { nativePlugins: [] };
319
+ : { nativePlugins: [], project };
320
+ }
321
+
322
+ function captureInitialLSPProjectInputs(options: {
323
+ nativePlugins: readonly ITtscLoadedNativePlugin[];
324
+ pluginConfigOrigin: string | undefined;
325
+ project: ITtscParsedProjectConfig;
326
+ tsgoBinary: string;
327
+ }): ReadonlyMap<string, InitialLSPProjectInputSnapshot> {
328
+ const snapshots = new Map<string, InitialLSPProjectInputSnapshot>();
329
+ const pluginsJSON = JSON.stringify(
330
+ serializeNativePlugins(options.nativePlugins),
331
+ );
332
+ for (const plugin of options.nativePlugins) {
333
+ if (
334
+ plugin.capabilities?.lsp !== true ||
335
+ plugin.capabilities.projectInputs !== true
336
+ ) {
337
+ continue;
338
+ }
339
+ const transportKey = lspPluginTransportKey(plugin);
340
+ if (snapshots.has(transportKey)) continue;
341
+ const args = [
342
+ "project-inputs",
343
+ "--tsconfig=" + options.project.path,
344
+ "--plugins-json=" + pluginsJSON,
345
+ "--cwd=" + options.project.root,
346
+ ];
347
+ if (plugin.capabilities.projectContextArgs === true) {
348
+ args.push(
349
+ ...createNativeProjectContextArgs(
350
+ options.project,
351
+ options.pluginConfigOrigin,
352
+ ),
353
+ );
354
+ }
355
+ const env = lspSidecarEnvironment({
356
+ pluginConfigOrigin: options.pluginConfigOrigin,
357
+ tsgoBinary: options.tsgoBinary,
358
+ });
359
+ const result = spawnSync(plugin.binary, args, {
360
+ cwd: options.project.root,
361
+ encoding: "utf8",
362
+ env,
363
+ maxBuffer: LSP_PROJECT_INPUT_MAX_BUFFER,
364
+ timeout: LSP_PROJECT_INPUT_TIMEOUT_MS,
365
+ windowsHide: true,
366
+ });
367
+ if (result.error) {
368
+ throw new Error(
369
+ `ttscserver: ${plugin.name ?? plugin.binary} project-inputs failed: ${result.error.message}`,
370
+ );
371
+ }
372
+ const stdout = result.stdout.trim();
373
+ if (result.status !== 0) {
374
+ const detail = result.stderr.trim() || stdout;
375
+ throw new Error(
376
+ `ttscserver: ${plugin.name ?? plugin.binary} project-inputs failed${detail ? `: ${detail}` : ""}`,
377
+ );
378
+ }
379
+ snapshots.set(
380
+ transportKey,
381
+ fingerprintInitialLSPProjectInputSnapshot(
382
+ parseInitialLSPProjectInputSnapshot(stdout, plugin),
383
+ ),
384
+ );
385
+ }
386
+ return snapshots;
387
+ }
388
+
389
+ function lspPluginTransportKey(plugin: ITtscLoadedNativePlugin): string {
390
+ return (
391
+ plugin.binary +
392
+ "\0" +
393
+ (plugin.capabilities?.projectContextArgs === true ? "1" : "0")
394
+ );
395
+ }
396
+
397
+ function initialLSPProjectInputsEqual(
398
+ left: ReadonlyMap<string, InitialLSPProjectInputSnapshot>,
399
+ right: ReadonlyMap<string, InitialLSPProjectInputSnapshot>,
400
+ ): boolean {
401
+ if (left.size !== right.size) return false;
402
+ for (const [key, leftSnapshot] of left) {
403
+ const rightSnapshot = right.get(key);
404
+ if (
405
+ rightSnapshot === undefined ||
406
+ initialLSPProjectInputSnapshotSignature(leftSnapshot) !==
407
+ initialLSPProjectInputSnapshotSignature(rightSnapshot)
408
+ ) {
409
+ return false;
410
+ }
411
+ }
412
+ return true;
413
+ }
414
+
415
+ function initialLSPProjectInputSnapshotSignature(
416
+ snapshot: InitialLSPProjectInputSnapshot,
417
+ ): string {
418
+ const sorted = (values: readonly string[] | undefined): string[] =>
419
+ [...(values ?? [])].sort();
420
+ const reloadDirectories = sorted(snapshot.reloadDirectories);
421
+ const reloadFiles = sorted(snapshot.reloadFiles);
422
+ return JSON.stringify({
423
+ files: sorted(snapshot.files),
424
+ globs: sorted(snapshot.globs),
425
+ reloadDirectories: reloadDirectories.map((directory) => [
426
+ directory,
427
+ snapshot.reloadDirectoryDigests[directory],
428
+ ]),
429
+ reloadFiles: reloadFiles.map((file) => [
430
+ file,
431
+ snapshot.reloadFileDigests[file],
432
+ ]),
433
+ root: snapshot.root,
434
+ });
435
+ }
436
+
437
+ function parseInitialLSPProjectInputSnapshot(
438
+ text: string,
439
+ plugin: ITtscLoadedNativePlugin,
440
+ ): ITtscProjectInputSnapshot {
441
+ let value: unknown;
442
+ try {
443
+ value = JSON.parse(text);
444
+ } catch (error) {
445
+ throw new Error(
446
+ `ttscserver: ${plugin.name ?? plugin.binary} project-inputs returned invalid JSON: ${
447
+ error instanceof Error ? error.message : String(error)
448
+ }`,
449
+ );
450
+ }
451
+ if (
452
+ value === null ||
453
+ typeof value !== "object" ||
454
+ typeof (value as ITtscProjectInputSnapshot).root !== "string" ||
455
+ !Array.isArray((value as ITtscProjectInputSnapshot).files) ||
456
+ !Array.isArray((value as ITtscProjectInputSnapshot).globs) ||
457
+ ((value as ITtscProjectInputSnapshot).reloadFiles !== undefined &&
458
+ !Array.isArray((value as ITtscProjectInputSnapshot).reloadFiles)) ||
459
+ ((value as ITtscProjectInputSnapshot).reloadDirectories !== undefined &&
460
+ !Array.isArray((value as ITtscProjectInputSnapshot).reloadDirectories))
461
+ ) {
462
+ throw new Error(
463
+ `ttscserver: ${plugin.name ?? plugin.binary} project-inputs returned a malformed snapshot`,
464
+ );
465
+ }
466
+ return value as ITtscProjectInputSnapshot;
467
+ }
468
+
469
+ export function fingerprintInitialLSPProjectInputSnapshot(
470
+ snapshot: ITtscProjectInputSnapshot,
471
+ ): InitialLSPProjectInputSnapshot {
472
+ const reloadDirectoryDigests: Record<string, string> = {};
473
+ const reloadFileDigests: Record<string, string> = {};
474
+ for (const directory of snapshot.reloadDirectories ?? []) {
475
+ reloadDirectoryDigests[directory] = lspProjectInputDirectoryDigest(
476
+ realLSPProjectInputPath(directory),
477
+ );
478
+ }
479
+ for (const file of snapshot.reloadFiles ?? []) {
480
+ reloadFileDigests[file] = lspProjectInputFileDigest(
481
+ realLSPProjectInputEntryPath(file),
482
+ );
483
+ }
166
484
  return {
167
- nativePlugins: loaded.nativePlugins,
168
- projectContext: {
169
- ...project.identity,
170
- ...(pluginConfigOrigin === undefined ? {} : { pluginConfigOrigin }),
171
- },
172
- tsgoBinary: tsgo.binary,
485
+ ...snapshot,
486
+ reloadDirectoryDigests,
487
+ reloadFileDigests,
173
488
  };
174
489
  }
175
490
 
176
- function hasTsgoOption(argv: readonly string[]): boolean {
177
- return argv.some((arg) => arg === "--tsgo" || arg.startsWith("--tsgo="));
491
+ export function initialLSPProjectInputSnapshotIsCurrent(
492
+ snapshot: InitialLSPProjectInputSnapshot,
493
+ ): boolean {
494
+ return (
495
+ (snapshot.reloadDirectories ?? []).every(
496
+ (directory) =>
497
+ snapshot.reloadDirectoryDigests[directory] ===
498
+ lspProjectInputDirectoryDigest(realLSPProjectInputPath(directory)),
499
+ ) &&
500
+ (snapshot.reloadFiles ?? []).every(
501
+ (file) =>
502
+ snapshot.reloadFileDigests[file] ===
503
+ lspProjectInputFileDigest(realLSPProjectInputEntryPath(file)),
504
+ )
505
+ );
506
+ }
507
+
508
+ function lspSelectionSignature(
509
+ project: ITtscParsedProjectConfig,
510
+ plugins: readonly ITtscLoadedNativePlugin[],
511
+ ): string {
512
+ return JSON.stringify({
513
+ identity: project.identity,
514
+ plugins: plugins.map((plugin) => ({
515
+ binary: plugin.binary,
516
+ capabilities: plugin.capabilities,
517
+ config: plugin.config,
518
+ contributors: plugin.contributors,
519
+ kind: plugin.kind,
520
+ name: plugin.name,
521
+ source: plugin.source,
522
+ stage: plugin.stage,
523
+ })),
524
+ });
525
+ }
526
+
527
+ function lspProjectInputDirectoryDigest(location: string): string {
528
+ const entries: Buffer[] = [];
529
+ try {
530
+ if (process.platform === "win32") {
531
+ for (const entry of fs.readdirSync(location, { withFileTypes: true })) {
532
+ let target = Buffer.alloc(0);
533
+ if (entry.isSymbolicLink()) {
534
+ try {
535
+ target = Buffer.from(
536
+ fs.readlinkSync(path.join(location, entry.name)),
537
+ "utf8",
538
+ );
539
+ } catch {
540
+ target = Buffer.from("<unreadable>");
541
+ }
542
+ }
543
+ entries.push(
544
+ lspProjectInputDirectoryRecord(
545
+ Buffer.from(entry.name),
546
+ entry,
547
+ target,
548
+ ),
549
+ );
550
+ }
551
+ } else {
552
+ for (const entry of fs.readdirSync(location, {
553
+ encoding: "buffer",
554
+ withFileTypes: true,
555
+ })) {
556
+ let target = Buffer.alloc(0);
557
+ if (entry.isSymbolicLink()) {
558
+ try {
559
+ target = fs.readlinkSync(
560
+ Buffer.concat([
561
+ Buffer.from(location),
562
+ Buffer.from(path.sep),
563
+ entry.name,
564
+ ]),
565
+ { encoding: "buffer" },
566
+ );
567
+ } catch {
568
+ target = Buffer.from("<unreadable>");
569
+ }
570
+ }
571
+ entries.push(lspProjectInputDirectoryRecord(entry.name, entry, target));
572
+ }
573
+ }
574
+ } catch {
575
+ return createHash("sha256").update("missing\0").digest("hex");
576
+ }
577
+ entries.sort(Buffer.compare);
578
+ const serialized = Buffer.concat(
579
+ entries.flatMap((entry, index) =>
580
+ index === 0 ? [entry] : [Buffer.from([0]), entry],
581
+ ),
582
+ );
583
+ return createHash("sha256").update(serialized).digest("hex");
584
+ }
585
+
586
+ function lspProjectInputDirectoryRecord(
587
+ name: Buffer,
588
+ entry: {
589
+ isDirectory(): boolean;
590
+ isFile(): boolean;
591
+ isSymbolicLink(): boolean;
592
+ },
593
+ target: Buffer,
594
+ ): Buffer {
595
+ const kind = entry.isDirectory()
596
+ ? "directory"
597
+ : entry.isFile()
598
+ ? "file"
599
+ : entry.isSymbolicLink()
600
+ ? "symlink"
601
+ : "other";
602
+ return Buffer.concat([name, Buffer.from("\0" + kind + "\0"), target]);
603
+ }
604
+
605
+ function lspProjectInputFileDigest(location: string): string {
606
+ try {
607
+ const info = fs.lstatSync(location);
608
+ if (info.isSymbolicLink()) {
609
+ let target = Buffer.from("<unreadable>");
610
+ try {
611
+ target = fs.readlinkSync(Buffer.from(location), {
612
+ encoding: "buffer",
613
+ });
614
+ } catch {
615
+ // Preserve the same explicit unreadable state as the Go validator.
616
+ }
617
+ let content = Buffer.from("missing\0");
618
+ try {
619
+ content = Buffer.concat([
620
+ Buffer.from("file\0"),
621
+ fs.readFileSync(location),
622
+ ]);
623
+ } catch {
624
+ // A dangling or unreadable target remains part of the symlink state.
625
+ }
626
+ return createHash("sha256")
627
+ .update(
628
+ Buffer.concat([
629
+ Buffer.from("symlink\0"),
630
+ target,
631
+ Buffer.from([0]),
632
+ content,
633
+ ]),
634
+ )
635
+ .digest("hex");
636
+ }
637
+ if (info.isFile()) {
638
+ return createHash("sha256")
639
+ .update(
640
+ Buffer.concat([Buffer.from("file\0"), fs.readFileSync(location)]),
641
+ )
642
+ .digest("hex");
643
+ }
644
+ return createHash("sha256").update("other\0").digest("hex");
645
+ } catch {
646
+ return createHash("sha256").update("missing\0").digest("hex");
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Physical path of a location whose leaf may not exist yet.
652
+ *
653
+ * Both ends go through the shared spelling rule rather than `path.resolve`
654
+ * alone. Windows hands back extended-length paths from a native realpath, and a
655
+ * record written as `\?\C:\project` never matches a lookup for `C:\project`
656
+ * even though one file is meant — the split identity every other consumer on
657
+ * this branch was taught to avoid.
658
+ */
659
+ function realLSPProjectInputPath(location: string): string {
660
+ const absolute = resolveProjectInputPath(location);
661
+ let probe = absolute;
662
+ const suffix: string[] = [];
663
+ for (;;) {
664
+ try {
665
+ let resolved = resolveProjectInputPath(fs.realpathSync.native(probe));
666
+ for (let index = suffix.length - 1; index >= 0; index--) {
667
+ resolved = path.join(resolved, suffix[index]!);
668
+ }
669
+ return path.normalize(resolved);
670
+ } catch {
671
+ const parent = path.dirname(probe);
672
+ if (parent === probe) return path.normalize(absolute);
673
+ suffix.push(path.basename(probe));
674
+ probe = parent;
675
+ }
676
+ }
677
+ }
678
+
679
+ function realLSPProjectInputEntryPath(location: string): string {
680
+ const absolute = resolveProjectInputPath(location);
681
+ return path.join(
682
+ realLSPProjectInputPath(path.dirname(absolute)),
683
+ path.basename(absolute),
684
+ );
178
685
  }
179
686
 
180
687
  function optionValue(