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