ttsc 0.23.0 → 0.25.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 (149) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +66 -63
  3. package/cmd/ttscgraph/serve.go +264 -48
  4. package/cmd/ttscgraph/serve_answers_a_project_that_implements_its_own_ambient_declaration_test.go +69 -0
  5. package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
  6. package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
  7. package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
  8. package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
  9. package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
  10. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
  11. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
  12. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
  13. package/cmd/ttscgraph/serve_shards.go +1390 -0
  14. package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
  15. package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
  16. package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
  17. package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
  18. package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
  19. package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
  20. package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
  21. package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
  22. package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
  23. package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
  24. package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
  25. package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
  26. package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
  27. package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
  28. package/driver/declaration_shape.go +85 -0
  29. package/driver/program.go +39 -10
  30. package/go.sum +22 -22
  31. package/internal/graph/assigned_implementations_of_declaration_file_symbols_leave_no_edge_source_test.go +84 -0
  32. package/internal/graph/assigned_implementations_of_node_modules_symbols_leave_no_edge_source_test.go +85 -0
  33. package/internal/graph/build.go +62 -10
  34. package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
  35. package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
  36. package/internal/graph/dump.go +84 -43
  37. package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
  38. package/internal/graph/dump_paths.go +134 -11
  39. package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
  40. package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
  41. package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
  42. package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
  43. package/internal/graph/edges.go +73 -17
  44. package/internal/graph/exports.go +9 -2
  45. package/internal/graph/graph.go +47 -7
  46. package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
  47. package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
  48. package/internal/graph/provenance.go +11 -1
  49. package/internal/graph/resolve.go +1 -4
  50. package/internal/graph/source.go +23 -0
  51. package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
  52. package/internal/lspserver/lsp_native_plugin_source.go +5 -6
  53. package/internal/lspserver/lsp_native_resident.go +17 -24
  54. package/lib/TtscService.d.ts +1 -6
  55. package/lib/TtscService.js +2 -2
  56. package/lib/TtscService.js.map +1 -1
  57. package/lib/compiler/internal/captureProcessOutput.d.ts +26 -0
  58. package/lib/compiler/internal/captureProcessOutput.js +92 -0
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -0
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -7
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/residentCheckProcess.d.ts +0 -2
  63. package/lib/compiler/internal/residentCheckProcess.js +1 -15
  64. package/lib/compiler/internal/residentCheckProcess.js.map +1 -1
  65. package/lib/compiler/internal/residentTransformProcess.d.ts +0 -6
  66. package/lib/compiler/internal/residentTransformProcess.js +0 -25
  67. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  68. package/lib/compiler/internal/spawnNative.d.ts +14 -6
  69. package/lib/compiler/internal/spawnNative.js +32 -11
  70. package/lib/compiler/internal/spawnNative.js.map +1 -1
  71. package/lib/compiler/internal/startResidentTransform.d.ts +1 -5
  72. package/lib/compiler/internal/startResidentTransform.js +1 -3
  73. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  74. package/lib/launcher/internal/prepareExecution.d.ts +2 -1
  75. package/lib/launcher/internal/prepareExecution.js +269 -23
  76. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  77. package/lib/launcher/internal/registerRuntimeHooks.js +6 -1
  78. package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
  79. package/lib/launcher/internal/runTtscserver.js +4 -9
  80. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  81. package/lib/launcher/internal/runTtsx.js +2 -55
  82. package/lib/launcher/internal/runTtsx.js.map +1 -1
  83. package/lib/launcher/internal/runtimeHooks.d.ts +25 -2
  84. package/lib/launcher/internal/runtimeHooks.js +344 -69
  85. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  86. package/lib/plugin/internal/buildSourcePlugin.js +31 -3
  87. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  88. package/lib/plugin/internal/descriptorProcessFailure.d.ts +23 -19
  89. package/lib/plugin/internal/descriptorProcessFailure.js +40 -51
  90. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -1
  91. package/lib/plugin/internal/loadProjectPlugins.d.ts +12 -0
  92. package/lib/plugin/internal/loadProjectPlugins.js +77 -28
  93. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  94. package/package.json +8 -8
  95. package/shim/ast/extra-shim.json +4 -4
  96. package/shim/ast/go.mod +14 -14
  97. package/shim/ast/go.sum +20 -20
  98. package/shim/astnav/go.mod +14 -14
  99. package/shim/astnav/go.sum +20 -20
  100. package/shim/bundled/extra-shim.json +1 -1
  101. package/shim/bundled/go.mod +5 -5
  102. package/shim/checker/extra-shim.json +31 -31
  103. package/shim/checker/go.mod +5 -5
  104. package/shim/compiler/extra-shim.json +4 -4
  105. package/shim/compiler/go.mod +14 -14
  106. package/shim/compiler/go.sum +22 -22
  107. package/shim/core/extra-shim.json +4 -4
  108. package/shim/core/go.mod +5 -5
  109. package/shim/diagnosticwriter/go.mod +5 -5
  110. package/shim/lsp/extra-shim.json +4 -4
  111. package/shim/lsp/go.mod +16 -16
  112. package/shim/lsp/go.sum +26 -26
  113. package/shim/parser/extra-shim.json +4 -4
  114. package/shim/parser/go.mod +5 -5
  115. package/shim/printer/go.mod +14 -14
  116. package/shim/printer/go.sum +22 -22
  117. package/shim/scanner/extra-shim.json +4 -4
  118. package/shim/scanner/go.mod +5 -5
  119. package/shim/transformers/go.mod +14 -14
  120. package/shim/transformers/go.sum +20 -20
  121. package/shim/tsoptions/extra-shim.json +4 -4
  122. package/shim/tsoptions/go.mod +5 -5
  123. package/shim/tspath/extra-shim.json +4 -4
  124. package/shim/tspath/go.mod +5 -5
  125. package/shim/vfs/cachedvfs/extra-shim.json +4 -4
  126. package/shim/vfs/cachedvfs/go.mod +5 -5
  127. package/shim/vfs/extra-shim.json +4 -4
  128. package/shim/vfs/go.mod +5 -5
  129. package/shim/vfs/osvfs/extra-shim.json +4 -4
  130. package/shim/vfs/osvfs/go.mod +5 -5
  131. package/src/TtscService.ts +8 -20
  132. package/src/compiler/internal/captureProcessOutput.ts +96 -0
  133. package/src/compiler/internal/compileProjectInMemory.ts +7 -7
  134. package/src/compiler/internal/residentCheckProcess.ts +1 -21
  135. package/src/compiler/internal/residentTransformProcess.ts +0 -42
  136. package/src/compiler/internal/spawnNative.ts +39 -18
  137. package/src/compiler/internal/startResidentTransform.ts +1 -12
  138. package/src/launcher/internal/prepareExecution.ts +297 -26
  139. package/src/launcher/internal/registerRuntimeHooks.ts +11 -2
  140. package/src/launcher/internal/runTtscserver.ts +4 -9
  141. package/src/launcher/internal/runTtsx.ts +2 -78
  142. package/src/launcher/internal/runtimeHooks.ts +385 -82
  143. package/src/plugin/internal/buildSourcePlugin.ts +36 -3
  144. package/src/plugin/internal/descriptorProcessFailure.ts +34 -65
  145. package/src/plugin/internal/loadProjectPlugins.ts +78 -39
  146. package/tools/gen_shims/go.mod +14 -14
  147. package/tools/gen_shims/go.sum +12 -12
  148. package/tools/shim_audit/go.mod +10 -10
  149. package/tools/shim_audit/go.sum +8 -8
@@ -2,10 +2,13 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
+ import { isOutsideRelativePath } from "../../compiler/internal/paths";
5
6
  import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
6
7
  import { resolveEmittedJavaScript } from "../../compiler/internal/resolveEmittedJavaScript";
7
8
  import { runBuild } from "../../compiler/internal/runBuild";
9
+ import { createFilesystemPathIdentityContext } from "../../internal/projectInputPathIdentity";
8
10
  import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
11
+ import { type OwningModuleOptions, projectModuleOptions } from "./runtimeHooks";
9
12
 
10
13
  /** Subdirectory name that isolates concurrent ttsx processes by PID. */
11
14
  const PROCESS_CACHE_KEY = String(process.pid);
@@ -16,6 +19,12 @@ const PROCESS_CACHE_KEY = String(process.pid);
16
19
  * resolve correctly without reaching an unsafe boundary.
17
20
  */
18
21
  const MAX_VIRTUAL_PARENT_DEPTH = 3;
22
+ /**
23
+ * Emit directory of the entry-only fallback build, a sibling of the virtual
24
+ * layout's volume-label directories so it can never collide with a mirrored
25
+ * project path.
26
+ */
27
+ const ENTRY_PROJECT_EMIT_DIR = "entry-project";
19
28
 
20
29
  /** Build the owning project and locate the emitted JavaScript entry for `ttsx`. */
21
30
  export function prepareExecution(
@@ -29,23 +38,35 @@ export function prepareExecution(
29
38
  emitDir: string;
30
39
  emittedFiles?: readonly string[];
31
40
  entryFile: string;
32
- moduleOption?: string;
41
+ moduleOptions: OwningModuleOptions;
33
42
  projectRoot: string;
34
43
  rootDir: string;
35
44
  } {
45
+ // Two paths, because two different questions are being asked.
46
+ //
47
+ // *Which project compiles this?* is answered from the path the user named.
48
+ // Project discovery walks up from it, so resolving a symlinked entry first
49
+ // would start that walk in the target's tree — finding another project's
50
+ // tsconfig, or none at all.
51
+ //
52
+ // *Where does the compiler put the output, and what does the runtime load?*
53
+ // is answered from the physical path, because that is the spelling Node
54
+ // forces. See `resolveEntrySpelling`.
55
+ const cwd = path.resolve(options.cwd ?? process.cwd());
56
+ const entry = resolveEntrySpelling(cwd, entryFile);
36
57
  const context = createProjectContext(
37
- path.resolve(options.cwd ?? process.cwd()),
38
- entryFile,
58
+ cwd,
59
+ path.resolve(cwd, entryFile),
60
+ entry,
39
61
  options,
40
62
  );
41
63
  try {
42
64
  buildProject(context, options);
43
- const emittedEntry = resolveEmittedJavaScript({
44
- emittedFiles: context.emittedFiles ?? undefined,
45
- outDir: context.emitDir,
46
- projectRoot: context.root,
47
- sourceFile: entryFile,
48
- });
65
+ let emittedEntry = emittedEntryOf(context, entry);
66
+ if (emittedEntry === null) {
67
+ buildEntryProject(context, options, entry);
68
+ emittedEntry = emittedEntryOf(context, entry);
69
+ }
49
70
  if (emittedEntry === null) {
50
71
  throw new Error(`ttsx: emitted entry not found for ${entryFile}`);
51
72
  }
@@ -54,7 +75,7 @@ export function prepareExecution(
54
75
  emitDir: context.emitDir,
55
76
  emittedFiles: context.emittedFiles ?? undefined,
56
77
  entryFile: emittedEntry,
57
- moduleOption: context.moduleOption,
78
+ moduleOptions: context.moduleOptions,
58
79
  projectRoot: context.root,
59
80
  rootDir: context.runtimeRootDir,
60
81
  };
@@ -64,9 +85,79 @@ export function prepareExecution(
64
85
  }
65
86
  }
66
87
 
88
+ /**
89
+ * The JavaScript this build emitted for `entry`, or `null` when it emitted none
90
+ * — which is the signal that the entry sits outside the project's file set.
91
+ *
92
+ * The guard is what makes this an ownership answer rather than a guess. tsgo
93
+ * strips `runtimeRootDir` from every output path, so a file outside that root
94
+ * cannot have an output under `outDir` at all; without the guard the lookup
95
+ * falls through to `resolveEmittedJavaScript`'s trailing-stem matcher, and a
96
+ * `build/release.ts` would happily match the `release.js` emitted for an
97
+ * unrelated `src/release.ts` — running the wrong file instead of compiling the
98
+ * requested one.
99
+ *
100
+ * It is lexical on purpose, and it is the same test `resolveEmittedJavaScript`
101
+ * then applies — including its rejection of an entry that _is_ the root. That
102
+ * only holds because both sides are _produced_ physically: the entry by
103
+ * `resolveEntrySpelling`, the root by `resolveRuntimeSourceRoot`. Folding here
104
+ * instead would paper over a mixed pair and disagree with the mirror that runs
105
+ * immediately after; keeping the pair honest is what makes folding
106
+ * unnecessary.
107
+ */
108
+ function emittedEntryOf(
109
+ context: ReturnType<typeof createProjectContext>,
110
+ entry: string,
111
+ ): string | null {
112
+ const relative = path.relative(context.runtimeRootDir, entry);
113
+ if (relative === "" || isOutsideRelativePath(relative)) {
114
+ return null;
115
+ }
116
+ return resolveEmittedJavaScript({
117
+ emittedFiles: context.emittedFiles ?? undefined,
118
+ outDir: context.emitDir,
119
+ projectRoot: context.runtimeRootDir,
120
+ sourceFile: entry,
121
+ });
122
+ }
123
+
124
+ /**
125
+ * The entry in the filesystem's own spelling, symlinked file included.
126
+ *
127
+ * Node is what forces the choice. Without `--preserve-symlinks` it keys a
128
+ * module by its real path, so the runtime hooks identify a served file that way
129
+ * too — and the emit has to be findable under the same name. tsgo does not
130
+ * force anything: it takes `files` verbatim and never resolves them, which is
131
+ * exactly why it must be handed the spelling Node will use rather than a
132
+ * different one.
133
+ *
134
+ * An entry spelled any other way is not a nicer name for the same file. The
135
+ * gate would claim to own an emit the runtime then refuses to serve, and the
136
+ * entry would run through the orphan type-strip lane with the project's
137
+ * transform plugins, `target`, `paths`, and source map all silently dropped —
138
+ * from a run that still prints and still exits zero.
139
+ *
140
+ * Resolving the link widens `rootDir` to the ancestor the two trees share,
141
+ * which is not a cost but the requirement: the file genuinely lives outside the
142
+ * project, and no root that excludes it can compile it.
143
+ */
144
+ function resolveEntrySpelling(cwd: string, entryFile: string): string {
145
+ const identities = createFilesystemPathIdentityContext({
146
+ throwOnRealpathError: false,
147
+ });
148
+ return identities.resolve(path.resolve(cwd, entryFile)).path;
149
+ }
150
+
151
+ /**
152
+ * @param discoveryFile - The entry as the user named it. Project discovery
153
+ * walks up from here, so it must not be retargeted through a symlink.
154
+ * @param entry - The entry in its physical spelling, which is what the emit
155
+ * layout and the runtime hooks both speak.
156
+ */
67
157
  function createProjectContext(
68
158
  cwd: string,
69
- filename: string,
159
+ discoveryFile: string,
160
+ entry: string,
70
161
  options: NonNullable<Parameters<typeof prepareExecution>[1]>,
71
162
  ) {
72
163
  const project = readProjectConfig(
@@ -76,7 +167,7 @@ function createProjectContext(
76
167
  projectRoot: options.projectRoot,
77
168
  tsconfig: path.resolve(cwd, options.project),
78
169
  }
79
- : { cwd, file: filename, projectRoot: options.projectRoot },
170
+ : { cwd, file: discoveryFile, projectRoot: options.projectRoot },
80
171
  );
81
172
  const tsconfig = project.path;
82
173
  const root = project.root;
@@ -86,6 +177,9 @@ function createProjectContext(
86
177
  path.join(root, "node_modules", ".cache", "ttsc", "ttsx");
87
178
  const processDir = path.join(cacheDir, "project", PROCESS_CACHE_KEY);
88
179
  const virtualRoot = path.join(processDir, "fs");
180
+ // Resolved once: it now costs a realpath (and, for a missing directory on
181
+ // Windows, a case-sensitivity probe) rather than a string join.
182
+ const runtimeRootDir = resolveRuntimeSourceRoot(project, entry);
89
183
  return {
90
184
  project,
91
185
  tsconfig,
@@ -96,17 +190,17 @@ function createProjectContext(
96
190
  virtualRoot,
97
191
  emitDir: project.compilerOptions.outDir
98
192
  ? virtualPath(virtualRoot, project.compilerOptions.outDir)
99
- : virtualPath(virtualRoot, resolveRuntimeSourceRoot(project, filename)),
193
+ : virtualPath(virtualRoot, runtimeRootDir),
100
194
  // The source-tree root the emit mirrors (tsgo strips this prefix). Used to
101
195
  // map a source `.ts` back to its emitted `.js` when the runtime hooks serve
102
196
  // the built entry under its source URL.
103
- runtimeRootDir: resolveRuntimeSourceRoot(project, filename),
104
- // The tsconfig `module` option, so the runtime hooks classify each served
105
- // file's format the same way tsgo chose when emitting it.
106
- moduleOption:
107
- typeof project.compilerOptions.module === "string"
108
- ? project.compilerOptions.module
109
- : undefined,
197
+ runtimeRootDir,
198
+ // The tsconfig options that decide the emit format, so the runtime hooks
199
+ // classify each served file the same way tsgo chose when emitting it.
200
+ // `target` belongs here as much as `module` does: with `module` absent tsgo
201
+ // derives the module kind from `target`, so publishing only `module` makes
202
+ // the hooks guess.
203
+ moduleOptions: projectModuleOptions(project.compilerOptions),
110
204
  // Force a source map on the transient runtime emit only when the project
111
205
  // configures none — when it already emits `sourceMap` or `inlineSourceMap`,
112
206
  // the serve path inlines/absolutizes that map, so no override is needed
@@ -119,17 +213,37 @@ function createProjectContext(
119
213
  };
120
214
  }
121
215
 
216
+ /**
217
+ * The source-tree root the emit mirrors, in the same physical spelling as the
218
+ * entry it will be compared against.
219
+ *
220
+ * Resolving it is the other half of `resolveEntrySpelling`, and skipping it
221
+ * leaves the comparison mixed rather than merely imprecise. `project.root`
222
+ * arrives through plain `fs.realpathSync`, which resolves reparse points but
223
+ * leaves a Windows 8.3 component alone, while the entry arrives through
224
+ * `fs.realpathSync.native`, which expands it — and `path.relative` folds case
225
+ * but not 8.3. A declared `rootDir` is worse still: it is joined verbatim, so a
226
+ * `rootDir` that is itself a symlinked directory never resolves at all. Either
227
+ * way the gate reads an in-project entry as outside its own root, pays a second
228
+ * whole build for it, and publishes a wider root than the project has.
229
+ *
230
+ * `path.dirname(entry)` is already physical, so only the declared branch needs
231
+ * the pass.
232
+ */
122
233
  function resolveRuntimeSourceRoot(
123
234
  project: ReturnType<typeof readProjectConfig>,
124
- filename: string,
235
+ entry: string,
125
236
  ): string {
126
237
  const rootDir = project.compilerOptions.rootDir;
127
- if (typeof rootDir === "string") {
128
- return path.isAbsolute(rootDir)
129
- ? rootDir
130
- : path.resolve(project.root, rootDir);
238
+ if (typeof rootDir !== "string") {
239
+ return path.dirname(entry);
131
240
  }
132
- return path.dirname(filename);
241
+ const identities = createFilesystemPathIdentityContext({
242
+ throwOnRealpathError: false,
243
+ });
244
+ return identities.resolve(
245
+ path.isAbsolute(rootDir) ? rootDir : path.resolve(project.root, rootDir),
246
+ ).path;
133
247
  }
134
248
 
135
249
  function buildProject(
@@ -184,6 +298,163 @@ function buildProject(
184
298
  throw new Error(detail);
185
299
  }
186
300
 
301
+ /**
302
+ * Build an entry the owning project's file set does not contain.
303
+ *
304
+ * `ttsc` selects a _file set_: a project whose `include` is `src` must emit
305
+ * only `src` into `outDir`, and a `clear.ts`, a `build/release.ts`, or a
306
+ * `lint.config.ts` beside the tsconfig has no business in `lib`. `ttsx` selects
307
+ * an _entry_: it needs that same project's compiler options, not its file list.
308
+ * Those two requirements are not in conflict, but the whole-project build
309
+ * cannot satisfy the second one, so an entry it did not emit is compiled here
310
+ * through a project that inherits every option and declares only the entry.
311
+ *
312
+ * The synthesized tsconfig is written beside the real one on purpose. `extends`
313
+ * with an absolute path would resolve from anywhere, but `${configDir}` and
314
+ * `paths` are anchored to the directory of the config that consumes them, so
315
+ * any other location silently retargets them. It is removed as soon as the
316
+ * build returns.
317
+ *
318
+ * `rootDir` widens to the nearest directory holding both the project root and
319
+ * the entry — for the layout this exists for, the project root itself; for an
320
+ * entry that is a symlink out of the tree, the ancestor the two trees share. It
321
+ * has to widen at least that far, because the inherited `rootDir` (`src`) does
322
+ * not contain the entry and tsgo emits an input outside `rootDir` to its own
323
+ * source path.
324
+ *
325
+ * Widening costs precision, not safety. The manifest's `rootDir` bounds which
326
+ * files the runtime hooks will try to serve from this emit, so a wide one
327
+ * admits more sources to the lookup — but the lookup only ever answers with a
328
+ * file from this build's own emit directory, whether from `emittedFiles` or a
329
+ * scan of `outDir` (`resolveEmittedJavaScript`). What a wide root risks is the
330
+ * exact mirror missing and the trailing-stem matcher picking the wrong output
331
+ * _of this build_; it cannot reach a raw source on disk.
332
+ */
333
+ function buildEntryProject(
334
+ context: ReturnType<typeof createProjectContext>,
335
+ options: NonNullable<Parameters<typeof prepareExecution>[1]>,
336
+ entry: string,
337
+ ): void {
338
+ // `entry` already carries the one spelling `prepareExecution` decided on.
339
+ // tsgo compares it against `rootDir` textually — `GetCommonSourceDirectory`
340
+ // takes `rootDir` verbatim and `ContainsPath` is lexical — so a mismatch here
341
+ // is not a near miss: the entry counts as outside `rootDir`, and tsgo emits
342
+ // it to its own source path with the extension changed instead of under
343
+ // `outDir`, writing a `.js` and its map beside the user's `.ts` where nothing
344
+ // cleans them up.
345
+ const rootDir = commonAncestorDirectory(path.dirname(entry), context.root);
346
+ const tsconfig = path.join(
347
+ context.root,
348
+ `.ttsx-entry.${PROCESS_CACHE_KEY}.tsconfig.json`,
349
+ );
350
+ fs.writeFileSync(
351
+ tsconfig,
352
+ JSON.stringify(
353
+ {
354
+ extends: context.tsconfig.replace(/\\/g, "/"),
355
+ compilerOptions: { rootDir: rootDir.replace(/\\/g, "/") },
356
+ // `files` alone does not displace an inherited `include`, and an
357
+ // inherited `exclude` could drop the entry back out of the program, so
358
+ // both are overridden explicitly.
359
+ files: [entry.replace(/\\/g, "/")],
360
+ include: [],
361
+ exclude: [],
362
+ },
363
+ null,
364
+ 2,
365
+ ),
366
+ "utf8",
367
+ );
368
+ try {
369
+ const project = readProjectConfig({
370
+ cwd: context.root,
371
+ projectRoot: options.projectRoot,
372
+ tsconfig,
373
+ });
374
+ const emitDir = path.join(context.virtualRoot, ENTRY_PROJECT_EMIT_DIR);
375
+ fs.mkdirSync(emitDir, { recursive: true });
376
+ const result = runBuild({
377
+ binary: options.binary,
378
+ checkers: options.checkers,
379
+ cwd: context.root,
380
+ emit: true,
381
+ env: options.env,
382
+ forceListEmittedFiles: true,
383
+ cacheDir: context.pluginCacheDir,
384
+ outDir: emitDir,
385
+ passthrough: options.passthrough,
386
+ forceRuntimeSourceMap: context.forceRuntimeSourceMap,
387
+ pluginConfigDir: options.pluginConfigDir,
388
+ plugins: options.plugins,
389
+ quiet: true,
390
+ resolvedProject: project,
391
+ singleThreaded: options.singleThreaded,
392
+ tsconfig,
393
+ });
394
+ if (result.status !== 0) {
395
+ removeRuntimeOutput(context.processDir);
396
+ throw new Error(
397
+ [
398
+ `ttsx: entry check failed for ${entry}`,
399
+ result.stderr || result.stdout,
400
+ ]
401
+ .filter((line) => line.trim().length !== 0)
402
+ .join("\n"),
403
+ );
404
+ }
405
+ context.emitDir = emitDir;
406
+ context.runtimeRootDir = rootDir;
407
+ context.moduleOptions = projectModuleOptions(project.compilerOptions);
408
+ context.emittedFiles =
409
+ result.emittedFiles && result.emittedFiles.length !== 0
410
+ ? result.emittedFiles
411
+ : undefined;
412
+ } finally {
413
+ try {
414
+ fs.rmSync(tsconfig, { force: true });
415
+ } catch {
416
+ // Best effort: a leftover synthesized tsconfig must not mask a build
417
+ // failure, and it is PID-scoped so it can never be mistaken for a real
418
+ // project config.
419
+ }
420
+ }
421
+ }
422
+
423
+ /**
424
+ * The nearest directory containing both `left` and `right`, in the physical
425
+ * spelling both of them share.
426
+ *
427
+ * Containment is asked through the same filesystem-identity predicate the
428
+ * runtime hooks use, and the answer is resolved through it too. Its caller now
429
+ * passes an already-resolved directory, so the two spellings agree before the
430
+ * walk starts; the predicate stays because this returns a `rootDir` that tsgo
431
+ * takes verbatim, and answering in anything but the physical spelling would
432
+ * leave tsgo unable to place a sibling source under it.
433
+ *
434
+ * Falls back to the entry's directory when there genuinely is no shared
435
+ * ancestor, as on two different Windows volumes: the entry still has to
436
+ * compile, and a root that contains it is the closest thing to correct
437
+ * available.
438
+ */
439
+ function commonAncestorDirectory(left: string, right: string): string {
440
+ const identities = createFilesystemPathIdentityContext({
441
+ throwOnRealpathError: false,
442
+ });
443
+ const from = identities.resolve(path.resolve(left)).path;
444
+ const target = identities.resolve(path.resolve(right)).path;
445
+ let current = from;
446
+ for (;;) {
447
+ if (identities.isWithin(current, target)) {
448
+ return current;
449
+ }
450
+ const parent = path.dirname(current);
451
+ if (parent === current) {
452
+ return from;
453
+ }
454
+ current = parent;
455
+ }
456
+ }
457
+
187
458
  function removeRuntimeOutput(directory: string): void {
188
459
  try {
189
460
  fs.rmSync(directory, { recursive: true, force: true });
@@ -1,7 +1,11 @@
1
1
  import path from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
 
4
- import { entryModuleFormat, installRuntimeHooks } from "./runtimeHooks";
4
+ import {
5
+ entryModuleFormat,
6
+ installRuntimeHooks,
7
+ realPath,
8
+ } from "./runtimeHooks";
5
9
 
6
10
  /**
7
11
  * Bootstrap `ttsx` spawns as the child's main module: it installs the runtime
@@ -29,7 +33,12 @@ process.argv = [process.argv[0]!, entry, ...forwarded];
29
33
 
30
34
  installRuntimeHooks();
31
35
 
32
- const kind = entryModuleFormat(path.resolve(entry));
36
+ // The physical path, because the format question is a runtime question: the
37
+ // hooks serve the entry under its real path, and `moduleFormat` walks from the
38
+ // path it is handed to find the `package.json` that decides. Asking from a
39
+ // symlink's own directory reads the wrong package scope and hands an ESM emit
40
+ // to `require`.
41
+ const kind = entryModuleFormat(realPath(path.resolve(entry)));
33
42
 
34
43
  function fail(error: unknown): never {
35
44
  // Match Node's own uncaught-exception surfacing for a thrown entry error.
@@ -8,6 +8,7 @@ import { createNativeProjectContextArgs } from "../../compiler/internal/project/
8
8
  import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
9
9
  import { resolveBinary } from "../../compiler/internal/resolveBinary";
10
10
  import { resolveTsgo } from "../../compiler/internal/resolveTsgo";
11
+ import { outputText, spawnNative } from "../../compiler/internal/spawnNative";
11
12
  import {
12
13
  type ProjectInputPathIdentityContext,
13
14
  createProjectInputPathIdentityContext,
@@ -43,8 +44,6 @@ type TtscserverEnvironment = {
43
44
  };
44
45
 
45
46
  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
47
 
49
48
  /**
50
49
  * Drive the ttscserver native binary from a node launcher. The launcher is
@@ -359,22 +358,18 @@ function captureInitialLSPProjectInputs(options: {
359
358
  pluginConfigOrigin: options.pluginConfigOrigin,
360
359
  tsgoBinary: options.tsgoBinary,
361
360
  });
362
- const result = spawnSync(plugin.binary, args, {
361
+ const result = spawnNative(plugin.binary, args, {
363
362
  cwd: options.project.root,
364
- encoding: "utf8",
365
363
  env,
366
- maxBuffer: LSP_PROJECT_INPUT_MAX_BUFFER,
367
- timeout: LSP_PROJECT_INPUT_TIMEOUT_MS,
368
- windowsHide: true,
369
364
  });
370
365
  if (result.error) {
371
366
  throw new Error(
372
367
  `ttscserver: ${plugin.name ?? plugin.binary} project-inputs failed: ${result.error.message}`,
373
368
  );
374
369
  }
375
- const stdout = result.stdout.trim();
370
+ const stdout = outputText(result.stdout).trim();
376
371
  if (result.status !== 0) {
377
- const detail = result.stderr.trim() || stdout;
372
+ const detail = outputText(result.stderr).trim() || stdout;
378
373
  throw new Error(
379
374
  `ttscserver: ${plugin.name ?? plugin.binary} project-inputs failed${detail ? `: ${detail}` : ""}`,
380
375
  );
@@ -276,7 +276,7 @@ function runPreparedEntry(
276
276
  depCacheDir,
277
277
  emitDir: execution.emitDir,
278
278
  emittedFiles: execution.emittedFiles,
279
- moduleOption: execution.moduleOption,
279
+ moduleOptions: execution.moduleOptions,
280
280
  projectRoot: execution.projectRoot,
281
281
  rootDir: execution.rootDir,
282
282
  }),
@@ -299,7 +299,6 @@ function runPreparedEntry(
299
299
  sourceEntry,
300
300
  ...parsed.passthrough,
301
301
  ];
302
- const evaluatorBoundary = readEvaluatorBoundary(process.env);
303
302
  const runtimeEnv: NodeJS.ProcessEnv = {
304
303
  ...process.env,
305
304
  NODE_OPTIONS: appendNodeOption(
@@ -309,42 +308,12 @@ function runPreparedEntry(
309
308
  TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? tsgo,
310
309
  TTSX_RUNTIME_MANIFEST: manifestPath,
311
310
  };
312
- delete runtimeEnv[TTSC_TTSX_EVALUATOR_DEADLINE_ENV];
313
- delete runtimeEnv[TTSC_TTSX_EVALUATOR_MAX_BUFFER_ENV];
314
- delete runtimeEnv[TTSC_TTSX_EVALUATOR_STATUS_FD_ENV];
315
-
316
311
  const result = spawnSync(process.execPath, args, {
317
312
  cwd,
318
313
  env: runtimeEnv,
319
- ...(evaluatorBoundary
320
- ? {
321
- killSignal: "SIGKILL" as const,
322
- timeout: Math.max(
323
- 1,
324
- Math.min(
325
- MAX_CHILD_PROCESS_TIMEOUT_MS,
326
- evaluatorBoundary.deadlineMs - Date.now(),
327
- ),
328
- ),
329
- maxBuffer: evaluatorBoundary.maxBuffer,
330
- stdio: ["inherit", "pipe", "pipe"] as const,
331
- }
332
- : { stdio: "inherit" as const }),
314
+ stdio: "inherit",
333
315
  windowsHide: true,
334
316
  });
335
- const nestedCode = (result.error as NodeJS.ErrnoException | undefined)
336
- ?.code;
337
- if (
338
- evaluatorBoundary &&
339
- (nestedCode === "ETIMEDOUT" || nestedCode === "ENOBUFS")
340
- ) {
341
- reportEvaluatorFailure(evaluatorBoundary.statusFd, nestedCode);
342
- return 1;
343
- }
344
- if (evaluatorBoundary) {
345
- if (result.stdout) process.stdout.write(result.stdout);
346
- if (result.stderr) process.stderr.write(result.stderr);
347
- }
348
317
  if (result.error) {
349
318
  process.stderr.write(`${result.error.message}\n`);
350
319
  return 1;
@@ -355,51 +324,6 @@ function runPreparedEntry(
355
324
  }
356
325
  }
357
326
 
358
- interface TtsxEvaluatorBoundary {
359
- deadlineMs: number;
360
- maxBuffer: number;
361
- statusFd: number;
362
- }
363
-
364
- const TTSC_TTSX_EVALUATOR_DEADLINE_ENV = "TTSC_TTSX_EVALUATOR_DEADLINE_MS";
365
- const TTSC_TTSX_EVALUATOR_MAX_BUFFER_ENV =
366
- "TTSC_TTSX_EVALUATOR_MAX_BUFFER_BYTES";
367
- const TTSC_TTSX_EVALUATOR_STATUS_FD_ENV = "TTSC_TTSX_EVALUATOR_STATUS_FD";
368
- const TTSC_TTSX_EVALUATOR_STATUS_FD = 3;
369
- const MAX_CHILD_PROCESS_TIMEOUT_MS = 2_147_483_647;
370
-
371
- function readEvaluatorBoundary(
372
- env: NodeJS.ProcessEnv,
373
- ): TtsxEvaluatorBoundary | undefined {
374
- const deadlineMs = Number(env[TTSC_TTSX_EVALUATOR_DEADLINE_ENV]);
375
- const maxBuffer = Number(env[TTSC_TTSX_EVALUATOR_MAX_BUFFER_ENV]);
376
- const statusFd = Number(env[TTSC_TTSX_EVALUATOR_STATUS_FD_ENV]);
377
- if (
378
- !Number.isSafeInteger(deadlineMs) ||
379
- deadlineMs <= 0 ||
380
- !Number.isSafeInteger(maxBuffer) ||
381
- maxBuffer <= 0 ||
382
- maxBuffer > MAX_CHILD_PROCESS_TIMEOUT_MS ||
383
- statusFd !== TTSC_TTSX_EVALUATOR_STATUS_FD
384
- ) {
385
- return undefined;
386
- }
387
- return { deadlineMs, maxBuffer, statusFd };
388
- }
389
-
390
- function reportEvaluatorFailure(
391
- statusFd: number,
392
- code: "ENOBUFS" | "ETIMEDOUT",
393
- ): void {
394
- try {
395
- fs.writeSync(statusFd, code);
396
- } catch {
397
- // The boundary is an internal parent/child protocol. If a standalone ttsx
398
- // caller supplied only the environment variables, preserve the ordinary
399
- // non-zero bounded-process result instead of replacing it with an fd error.
400
- }
401
- }
402
-
403
327
  function removeRuntimeOutput(directory: string): void {
404
328
  try {
405
329
  fs.rmSync(directory, { force: true, recursive: true });