ttsc 0.32.0 → 0.32.1

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 (164) hide show
  1. package/lib/TtscCompiler.js +1 -1
  2. package/lib/TtscCompiler.js.map +1 -1
  3. package/lib/compiler/internal/ResidentCheckProcess.d.ts +74 -96
  4. package/lib/compiler/internal/ResidentCheckProcess.js +150 -335
  5. package/lib/compiler/internal/ResidentCheckProcess.js.map +1 -1
  6. package/lib/compiler/internal/ResidentCheckProcessHost.d.ts +127 -0
  7. package/lib/compiler/internal/ResidentCheckProcessHost.js +447 -0
  8. package/lib/compiler/internal/ResidentCheckProcessHost.js.map +1 -0
  9. package/lib/compiler/internal/ResidentCheckProcessProtocol.d.ts +64 -0
  10. package/lib/compiler/internal/ResidentCheckProcessProtocol.js +3 -0
  11. package/lib/compiler/internal/ResidentCheckProcessProtocol.js.map +1 -0
  12. package/lib/compiler/internal/build/BuildExecution.d.ts +103 -1
  13. package/lib/compiler/internal/build/BuildExecution.js +91 -11
  14. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  15. package/lib/compiler/internal/build/ResidentCheckWatchSession.d.ts +16 -11
  16. package/lib/compiler/internal/build/ResidentCheckWatchSession.js +37 -21
  17. package/lib/compiler/internal/build/ResidentCheckWatchSession.js.map +1 -1
  18. package/lib/compiler/internal/build/RunBuildOptions.d.ts +6 -5
  19. package/lib/compiler/internal/build/WatchInputPublication.d.ts +59 -0
  20. package/lib/compiler/internal/build/WatchInputPublication.js +93 -0
  21. package/lib/compiler/internal/build/WatchInputPublication.js.map +1 -0
  22. package/lib/compiler/internal/build/runBuild.d.ts +29 -2
  23. package/lib/compiler/internal/build/runBuild.js +45 -19
  24. package/lib/compiler/internal/build/runBuild.js.map +1 -1
  25. package/lib/compiler/internal/buildNativeCompiler.d.ts +2 -0
  26. package/lib/compiler/internal/buildNativeCompiler.js +1 -0
  27. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  28. package/lib/compiler/internal/compileProjectInMemory.js +4 -2
  29. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  30. package/lib/compiler/internal/project/resolveOwningProjectConfig.d.ts +7 -2
  31. package/lib/compiler/internal/project/resolveOwningProjectConfig.js +6 -3
  32. package/lib/compiler/internal/project/resolveOwningProjectConfig.js.map +1 -1
  33. package/lib/compiler/internal/readEffectiveCompilerOptions.js +1 -1
  34. package/lib/compiler/internal/readEffectiveCompilerOptions.js.map +1 -1
  35. package/lib/compiler/internal/resolveBinary.d.ts +1 -0
  36. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  37. package/lib/compiler/internal/resolveTsgo.d.ts +6 -0
  38. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  39. package/lib/compiler/internal/runSingleFileEmit.d.ts +28 -1
  40. package/lib/compiler/internal/runSingleFileEmit.js +120 -86
  41. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  42. package/lib/compiler/internal/startResidentTransform.js +8 -2
  43. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  44. package/lib/compiler/internal/transformProjectInMemory.js +11 -3
  45. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  46. package/lib/compiler/internal/transformProjectInWorker.d.ts +1 -1
  47. package/lib/compiler/internal/transformProjectInWorker.js +3 -2
  48. package/lib/compiler/internal/transformProjectInWorker.js.map +1 -1
  49. package/lib/internal/workerModuleUrl.d.ts +28 -0
  50. package/lib/internal/workerModuleUrl.js +40 -0
  51. package/lib/internal/workerModuleUrl.js.map +1 -0
  52. package/lib/launcher/internal/buildSingleRootProject.js +2 -1
  53. package/lib/launcher/internal/buildSingleRootProject.js.map +1 -1
  54. package/lib/launcher/internal/getCompilerVersionText.js +3 -2
  55. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  56. package/lib/launcher/internal/prepareExecution.js +2 -1
  57. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  58. package/lib/launcher/internal/runTtsc.js +91 -41
  59. package/lib/launcher/internal/runTtsc.js.map +1 -1
  60. package/lib/launcher/internal/runtime/CommonJsRuntimeSource.d.ts +10 -6
  61. package/lib/launcher/internal/runtime/CommonJsRuntimeSource.js +11 -9
  62. package/lib/launcher/internal/runtime/CommonJsRuntimeSource.js.map +1 -1
  63. package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.d.ts +20 -0
  64. package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.js +27 -0
  65. package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.js.map +1 -0
  66. package/lib/launcher/internal/runtime/installRuntimeHooks.d.ts +5 -3
  67. package/lib/launcher/internal/runtime/installRuntimeHooks.js +7 -3
  68. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  69. package/lib/launcher/internal/watch/AsyncWatchTopology.d.ts +113 -0
  70. package/lib/launcher/internal/watch/AsyncWatchTopology.js +232 -0
  71. package/lib/launcher/internal/watch/AsyncWatchTopology.js.map +1 -0
  72. package/lib/launcher/internal/watch/WatchSettlement.d.ts +49 -0
  73. package/lib/launcher/internal/watch/WatchSettlement.js +70 -0
  74. package/lib/launcher/internal/watch/WatchSettlement.js.map +1 -0
  75. package/lib/launcher/internal/watch/WatchTopology.d.ts +29 -1
  76. package/lib/launcher/internal/watch/WatchTopology.js +86 -10
  77. package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
  78. package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.d.ts +121 -0
  79. package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.js +3 -0
  80. package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.js.map +1 -0
  81. package/lib/launcher/internal/watch/serveWatchTopologyWorker.d.ts +19 -0
  82. package/lib/launcher/internal/watch/serveWatchTopologyWorker.js +62 -0
  83. package/lib/launcher/internal/watch/serveWatchTopologyWorker.js.map +1 -0
  84. package/lib/launcher/internal/watch/watchTopologyWorker.d.ts +1 -0
  85. package/lib/launcher/internal/watch/watchTopologyWorker.js +22 -0
  86. package/lib/launcher/internal/watch/watchTopologyWorker.js.map +1 -0
  87. package/lib/plugin/CapabilityPluginResolver.d.ts +1 -1
  88. package/lib/plugin/CapabilityPluginResolver.js +3 -2
  89. package/lib/plugin/CapabilityPluginResolver.js.map +1 -1
  90. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +15 -12
  91. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +13 -11
  92. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
  93. package/lib/plugin/internal/load/PluginPackageResolution.d.ts +2 -2
  94. package/lib/plugin/internal/load/PluginPackageResolution.js +13 -7
  95. package/lib/plugin/internal/load/PluginPackageResolution.js.map +1 -1
  96. package/lib/plugin/internal/load/ProjectPluginEntries.d.ts +23 -7
  97. package/lib/plugin/internal/load/ProjectPluginEntries.js +36 -14
  98. package/lib/plugin/internal/load/ProjectPluginEntries.js.map +1 -1
  99. package/lib/plugin/internal/load/collectProjectHostInputs.d.ts +3 -1
  100. package/lib/plugin/internal/load/collectProjectHostInputs.js +5 -2
  101. package/lib/plugin/internal/load/collectProjectHostInputs.js.map +1 -1
  102. package/lib/plugin/internal/load/hasProjectPluginEntries.d.ts +3 -1
  103. package/lib/plugin/internal/load/hasProjectPluginEntries.js +4 -2
  104. package/lib/plugin/internal/load/hasProjectPluginEntries.js.map +1 -1
  105. package/lib/plugin/internal/load/loadProjectPlugins.d.ts +36 -5
  106. package/lib/plugin/internal/load/loadProjectPlugins.js +466 -416
  107. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  108. package/lib/plugin/internal/load/pluginResolutionFailure.d.ts +18 -0
  109. package/lib/plugin/internal/load/pluginResolutionFailure.js +32 -0
  110. package/lib/plugin/internal/load/pluginResolutionFailure.js.map +1 -0
  111. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +1 -1
  112. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +3 -2
  113. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
  114. package/lib/structures/ITtscCompilerContext.d.ts +12 -12
  115. package/lib/structures/ITtscPlugin.d.ts +7 -5
  116. package/lib/structures/ITtscPluginFactoryContext.d.ts +9 -8
  117. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +12 -6
  118. package/package.json +9 -9
  119. package/src/TtscCompiler.ts +1 -1
  120. package/src/compiler/internal/ResidentCheckProcess.ts +159 -374
  121. package/src/compiler/internal/ResidentCheckProcessHost.ts +502 -0
  122. package/src/compiler/internal/ResidentCheckProcessProtocol.ts +53 -0
  123. package/src/compiler/internal/build/BuildExecution.ts +118 -9
  124. package/src/compiler/internal/build/ResidentCheckWatchSession.ts +41 -24
  125. package/src/compiler/internal/build/RunBuildOptions.ts +9 -5
  126. package/src/compiler/internal/build/WatchInputPublication.ts +106 -0
  127. package/src/compiler/internal/build/runBuild.ts +58 -34
  128. package/src/compiler/internal/buildNativeCompiler.ts +4 -0
  129. package/src/compiler/internal/compileProjectInMemory.ts +8 -2
  130. package/src/compiler/internal/project/resolveOwningProjectConfig.ts +16 -5
  131. package/src/compiler/internal/readEffectiveCompilerOptions.ts +1 -1
  132. package/src/compiler/internal/resolveBinary.ts +4 -1
  133. package/src/compiler/internal/resolveTsgo.ts +7 -0
  134. package/src/compiler/internal/runSingleFileEmit.ts +139 -99
  135. package/src/compiler/internal/startResidentTransform.ts +9 -2
  136. package/src/compiler/internal/transformProjectInMemory.ts +12 -3
  137. package/src/compiler/internal/transformProjectInWorker.ts +10 -2
  138. package/src/internal/workerModuleUrl.ts +34 -0
  139. package/src/launcher/internal/buildSingleRootProject.ts +2 -0
  140. package/src/launcher/internal/getCompilerVersionText.ts +3 -2
  141. package/src/launcher/internal/prepareExecution.ts +2 -0
  142. package/src/launcher/internal/runTtsc.ts +123 -45
  143. package/src/launcher/internal/runtime/CommonJsRuntimeSource.ts +11 -9
  144. package/src/launcher/internal/runtime/advertiseTypeScriptExtensions.ts +25 -0
  145. package/src/launcher/internal/runtime/installRuntimeHooks.ts +7 -3
  146. package/src/launcher/internal/watch/AsyncWatchTopology.ts +267 -0
  147. package/src/launcher/internal/watch/WatchSettlement.ts +67 -0
  148. package/src/launcher/internal/watch/WatchTopology.ts +84 -13
  149. package/src/launcher/internal/watch/WatchTopologyWorkerProtocol.ts +106 -0
  150. package/src/launcher/internal/watch/serveWatchTopologyWorker.ts +75 -0
  151. package/src/launcher/internal/watch/watchTopologyWorker.ts +29 -0
  152. package/src/plugin/CapabilityPluginResolver.ts +9 -2
  153. package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +17 -12
  154. package/src/plugin/internal/load/PluginPackageResolution.ts +14 -9
  155. package/src/plugin/internal/load/ProjectPluginEntries.ts +40 -15
  156. package/src/plugin/internal/load/collectProjectHostInputs.ts +7 -1
  157. package/src/plugin/internal/load/hasProjectPluginEntries.ts +8 -3
  158. package/src/plugin/internal/load/loadProjectPlugins.ts +672 -594
  159. package/src/plugin/internal/load/pluginResolutionFailure.ts +37 -0
  160. package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +8 -2
  161. package/src/structures/ITtscCompilerContext.ts +12 -12
  162. package/src/structures/ITtscPlugin.ts +7 -5
  163. package/src/structures/ITtscPluginFactoryContext.ts +9 -8
  164. package/src/structures/internal/TtscSingleFileEmitOptions.ts +22 -11
@@ -4,6 +4,7 @@ import { createRequire } from "node:module";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
+ import { WatchInputPublication } from "../../../compiler/internal/build/WatchInputPublication";
7
8
  import { readJsonFile } from "../../../compiler/internal/project/readJsonFile";
8
9
  import { readProjectConfig } from "../../../compiler/internal/project/readProjectConfig";
9
10
  import { SidecarEnvironment } from "../../../compiler/internal/sharedHost/SidecarEnvironment";
@@ -20,6 +21,7 @@ import type { ITtscPluginFactoryContext } from "../../../structures/ITtscPluginF
20
21
  import type { ITtscProjectPluginConfig } from "../../../structures/ITtscProjectPluginConfig";
21
22
  import type { ITtscLoadedNativePlugin } from "../../../structures/internal/ITtscLoadedNativePlugin";
22
23
  import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
24
+ import type { TtscPluginSourceInputsHandler } from "../../../structures/internal/TtscSingleFileEmitOptions";
23
25
  import { pluginDescriptorFailureReason } from "../pluginDescriptorFailureReason";
24
26
  import { pluginDescriptorProcessFailure } from "../pluginDescriptorProcessFailure";
25
27
  import { NativeSourcePackages } from "../source/NativeSourcePackages";
@@ -83,6 +85,9 @@ import { visitImportMappedCandidates } from "./visitImportMappedCandidates";
83
85
  * declaration. Sequential content, metadata and physical-path observations are
84
86
  * not an atomic snapshot or detection of every omitted read.
85
87
  *
88
+ * Synchronous callers require synchronous watch-input handlers; use
89
+ * {@link loadProjectPlugins.async} to await a worker's baseline publication.
90
+ *
86
91
  * @param options.binary - Absolute path to the ttsc native helper binary.
87
92
  * @param options.cacheDir - Override the plugin binary cache directory.
88
93
  * @param options.cwd - Working directory for resolving relative paths.
@@ -93,11 +98,12 @@ import { visitImportMappedCandidates } from "./visitImportMappedCandidates";
93
98
  * native environment-name identity. Defaults to `process.env` for CLI
94
99
  * callers, so ambient behavior is unchanged.
95
100
  * @param options.file - Path to the tsconfig/jsconfig file.
96
- * @param options.pluginConfigDir - Caller-declared anchor for plugin
97
- * config-file discovery (see `ITtscPluginFactoryContext.pluginConfigDir`).
98
- * @param options.projectRoot - Override the project root directory.
101
+ * @param options.pluginConfigDir - Caller-declared consumer directory for
102
+ * plugin packages, factory contexts and config-file discovery.
103
+ * @param options.projectRoot - Override the source root, native working
104
+ * directory, cache anchor and returned relative Go-source base.
99
105
  * @param options.tsconfig - Alias for `file`.
100
- * @evidence contracts/common.md#principled-implementation The loader brackets project discovery observations, evaluates descriptors in isolated processes and validates declarations before publishing conservative source repair interests. Metadata refusal remains terminal; valid records narrow watch interests before final admission/build, while contradictory content or physical observations are omitted from proof rather than retroactively blessed.
106
+ * @evidence contracts/common.md#principled-implementation The loader brackets discovery at the selected config or explicit pluginConfigDir and factories receive that consumer anchor, independently of the source root retained for descriptor/native process cwd, caches and relative Go sources. It validates declarations before publishing conservative source repair interests. Metadata refusal remains terminal; valid records narrow watch interests before final admission/build, while contradictory content or physical observations are omitted from proof rather than retroactively blessed.
101
107
  * @evidence contracts/common.md#clear-and-simple-design The operation owns one ordered load generation; private helpers separate discovery, evaluation, validation, composition and proof merging, while package resolution and source building remain their own modules.
102
108
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Descriptor module caches are isolated rather than deleting application singletons. ttsx retry occurs only for explicit supported TypeScript loader incompatibility, with plugins disabled to avoid recursive self-hosting; arbitrary descriptor failures are not retried into false success.
103
109
  * @evidence contracts/common.md#meaningful-documentation Native JSDoc explains result provenance, environment and path options; helper comments explain input races, conservative proof omission, fallback authority and cleanup. Member/tag spacing and separated concepts follow the documentation skill.
@@ -135,10 +141,7 @@ export function loadProjectPlugins(options: {
135
141
  * Observe plugin build roots before building, including failed builds, with
136
142
  * the record store that proves their states (#1728).
137
143
  */
138
- onWatchInputs?: (
139
- inputs: readonly string[],
140
- identities?: PluginContentIdentities.Store,
141
- ) => void;
144
+ onWatchInputs?: TtscPluginSourceInputsHandler;
142
145
 
143
146
  /** Caller-selected anchor for plugin-owned configuration discovery. */
144
147
  pluginConfigDir?: string;
@@ -199,617 +202,676 @@ export function loadProjectPlugins(options: {
199
202
  /** Resolved project values and logical/physical configuration identity. */
200
203
  project: ITtscParsedProjectConfig;
201
204
  } {
202
- // Snapshot the caller environment before `withPluginLoaderEnv` injects
203
- // host-owned Node/ttsx locators into process.env. Under a Bun parent the
204
- // direct descriptor evaluator must remain Bun unless the caller explicitly
205
- // selected another runtime.
206
- const effectiveEnv = SidecarEnvironment.merge(options.env ?? process.env);
207
- const projectSnapshot = readProjectHostInputSnapshot({
208
- cwd: options.cwd,
209
- file: options.file,
210
- includePluginDiscovery: options.entries === undefined,
211
- projectRoot: options.projectRoot,
212
- tsconfig: options.tsconfig,
213
- });
214
- const { project } = projectSnapshot;
215
- const projectHostInputs = projectSnapshot.hostInputs;
216
- const projectHostInputHashes = projectSnapshot.hostInputHashes;
217
- const projectHostInputRealpaths = projectSnapshot.hostInputRealpaths;
218
- const entries: ProjectPluginEntries.ProjectPluginEntry[] =
219
- options.entries === false
220
- ? []
221
- : ProjectPluginEntries.resolvePluginEntries(
205
+ return WatchInputPublication.run(loadProjectPlugins.steps(options));
206
+ }
207
+
208
+ /**
209
+ * Keep synchronous loading and awaited watch handoffs on one load sequence.
210
+ *
211
+ * @evidence contracts/common.md#principled-implementation Both completion modes advance one ordered discovery, admission, publication and build sequence. Awaited stepping preserves baseline-before-read handoffs before package selection and source builds, including empty selection and conservative repair roots.
212
+ * @evidence contracts/common.md#clear-and-simple-design The namespace adds an awaited runner beside synchronous loading and exposes one generator to build orchestration; existing helpers retain discovery, validation and compilation responsibilities.
213
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Publication suspension retains failed-build interests and source proof rather than rebuilding a baseline after compilation. Pending publication cannot establish observation; rejection enters the real load failure path without replacing admission or native builds.
214
+ * @evidence contracts/common.md#meaningful-documentation Entry prose distinguishes completion modes, baseline-before-read ordering and internal sequence completion/rejection duties; the primary loader documents input and result provenance.
215
+ * @evidence contracts/portability.md#os-neutral-implementation The shared sequence retains existing native resolution, paths, environment and argument-array handling; the namespace adds no independent boundary.
216
+ * @evidence contracts/performance.md#efficient-algorithms Discovery, source hashing, Go metadata, validation and builds retain their original input costs; each publication adds one suspension without repeating those phases.
217
+ * @evidence contracts/performance.md#reuse-equivalent-work Validated descriptor/source records and load-local digest/reader sharing remain available across suspension; the completion wrapper retains no reusable load or in-flight cache.
218
+ * @evidence contracts/performance.md#bound-retention-and-release-resources One load generation stays live while its current handoff settles; evaluator cleanup and returned-generation ownership remain with the sequence, and handler rejection enters generator cleanup.
219
+ */
220
+ export namespace loadProjectPlugins {
221
+ /**
222
+ * Load plugins after each watch owner confirms its reported input baseline. A
223
+ * rejected handoff fails loading before the next source read or build.
224
+ */
225
+ export function async(
226
+ options: Parameters<typeof loadProjectPlugins>[0],
227
+ ): Promise<ReturnType<typeof loadProjectPlugins>> {
228
+ return WatchInputPublication.runAsync(steps(options));
229
+ }
230
+
231
+ /**
232
+ * Produce one load, suspending at each watch-input handoff before continuing.
233
+ * Internal build orchestration consumes the whole sequence with its chosen
234
+ * completion runner; it must deliver rejection back into the sequence.
235
+ */
236
+ export function* steps(
237
+ options: Parameters<typeof loadProjectPlugins>[0],
238
+ ): WatchInputPublication.Sequence<ReturnType<typeof loadProjectPlugins>> {
239
+ // Snapshot the caller environment before `withPluginLoaderEnv` injects
240
+ // host-owned Node/ttsx locators into process.env. Under a Bun parent the
241
+ // direct descriptor evaluator must remain Bun unless the caller explicitly
242
+ // selected another runtime.
243
+ const effectiveEnv = SidecarEnvironment.merge(options.env ?? process.env);
244
+ const projectSnapshot = readProjectHostInputSnapshot({
245
+ cwd: options.cwd,
246
+ file: options.file,
247
+ includePluginDiscovery: options.entries === undefined,
248
+ pluginConfigDir: options.pluginConfigDir,
249
+ projectRoot: options.projectRoot,
250
+ tsconfig: options.tsconfig,
251
+ });
252
+ const { project } = projectSnapshot;
253
+ const projectHostInputs = projectSnapshot.hostInputs;
254
+ const projectHostInputHashes = projectSnapshot.hostInputHashes;
255
+ const projectHostInputRealpaths = projectSnapshot.hostInputRealpaths;
256
+ const entries: ProjectPluginEntries.ProjectPluginEntry[] =
257
+ options.entries === false
258
+ ? []
259
+ : ProjectPluginEntries.resolvePluginEntries(
260
+ project,
261
+ options.entries,
262
+ options.pluginConfigDir,
263
+ ).filter((entry) => entry.config.enabled !== false);
264
+ if (entries.length === 0) {
265
+ yield options.onWatchInputs?.([]);
266
+ return {
267
+ ...collectHostInputSnapshot(
222
268
  project,
223
- options.entries,
224
- ).filter((entry) => entry.config.enabled !== false);
225
- if (entries.length === 0) {
226
- options.onWatchInputs?.([]);
227
- return {
228
- ...collectHostInputSnapshot(
229
- project,
230
- {},
231
- [],
232
- projectHostInputs,
233
- revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
234
- revalidateHostInputRealpaths(
235
- projectHostInputRealpaths,
269
+ {},
270
+ [],
236
271
  projectHostInputs,
272
+ revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
273
+ revalidateHostInputRealpaths(
274
+ projectHostInputRealpaths,
275
+ projectHostInputs,
276
+ ),
237
277
  ),
238
- ),
239
- descriptorReadsDeclared: true,
240
- discoveryInputsComplete: project.configInputsComplete === true,
241
- nativePlugins: [],
242
- pluginSources: {},
243
- project,
244
- };
245
- }
278
+ descriptorReadsDeclared: true,
279
+ discoveryInputsComplete: project.configInputsComplete === true,
280
+ nativePlugins: [],
281
+ pluginSources: {},
282
+ project,
283
+ };
284
+ }
246
285
 
247
- const cwd = path.resolve(options.cwd ?? process.cwd());
248
- const context = {
249
- binary: options.binary,
250
- cwd,
251
- ...(options.pluginConfigDir === undefined || options.pluginConfigDir === ""
252
- ? {}
253
- : { pluginConfigDir: path.resolve(cwd, options.pluginConfigDir) }),
254
- projectRoot: project.root,
255
- tsconfig: project.path,
256
- };
257
- // Where isolated descriptor evaluations keep their answers across launches
258
- // (`PluginDescriptorEvaluationCache`).
259
- // WARNING (#1721, #1722, #1723): a warm load must prove its unchanged inputs
260
- // from this store, not re-read them. Every launch is a new process, so any
261
- // module-level memo of this load's work dies with it; a 27 s warm typia
262
- // launch whose native build took 0.28 s is what that looked like.
263
- const identities =
264
- options.identities ??
265
- PluginContentIdentities.open({
286
+ const cwd = path.resolve(options.cwd ?? process.cwd());
287
+ const context = {
288
+ binary: options.binary,
289
+ cwd,
290
+ ...(options.pluginConfigDir === undefined ||
291
+ options.pluginConfigDir === ""
292
+ ? {}
293
+ : { pluginConfigDir: path.resolve(cwd, options.pluginConfigDir) }),
266
294
  projectRoot: project.root,
295
+ tsconfig: project.path,
296
+ };
297
+ // Where isolated descriptor evaluations keep their answers across launches
298
+ // (`PluginDescriptorEvaluationCache`).
299
+ // WARNING (#1721, #1722, #1723): a warm load must prove its unchanged inputs
300
+ // from this store, not re-read them. Every launch is a new process, so any
301
+ // module-level memo of this load's work dies with it; a 27 s warm typia
302
+ // launch whose native build took 0.28 s is what that looked like.
303
+ const identities =
304
+ options.identities ??
305
+ PluginContentIdentities.open({
306
+ projectRoot: project.root,
307
+ cacheDir: options.cacheDir,
308
+ env: effectiveEnv,
309
+ });
310
+ const descriptorCache: DescriptorCacheOptions = {
267
311
  cacheDir: options.cacheDir,
268
- env: effectiveEnv,
269
- });
270
- const descriptorCache: DescriptorCacheOptions = {
271
- cacheDir: options.cacheDir,
272
- identities,
273
- version: pluginBuildVersions(project.root).ttsc,
274
- };
275
- const loadedEntries = withPluginLoaderEnv(
276
- () =>
277
- entries.map((entry) => {
278
- const specifier = entry.config.transform;
279
- if (typeof specifier !== "string" || specifier.length === 0) {
280
- throw new Error(
281
- `ttsc: plugin entry is missing a string "transform" field`,
282
- );
283
- }
284
- const entryParent = path.join(entry.baseDir, "package.json");
285
- const probedCandidates = collectModuleResolutionCandidates(
286
- specifier,
287
- entryParent,
288
- undefined,
289
- );
290
- // Capture every candidate before resolution chooses the descriptor entry.
291
- // A post-resolution snapshot could bless a higher-priority file created
292
- // after the resolver had already selected the old entry.
293
- const probedCandidateHashes = hashHostInputPaths(probedCandidates);
294
- const probedCandidateRealpaths =
295
- realpathHostInputPaths(probedCandidates);
296
- const request = PluginPackageResolution.resolvePluginRequest(
297
- specifier,
298
- entry.baseDir,
299
- );
300
- // Keep the candidates of the search roots up to the one the entry
301
- // resolved in: the lookup never read the roots after it
302
- // (`moduleResolutionBaseSelects`).
303
- const read = new Set(
304
- collectModuleResolutionCandidates(
312
+ identities,
313
+ projectRoot: project.root,
314
+ version: pluginBuildVersions(project.root).ttsc,
315
+ };
316
+ const loadedEntries = withPluginLoaderEnv(
317
+ () =>
318
+ entries.map((entry) => {
319
+ const specifier = entry.config.transform;
320
+ if (typeof specifier !== "string" || specifier.length === 0) {
321
+ throw new Error(
322
+ `ttsc: plugin entry is missing a string "transform" field`,
323
+ );
324
+ }
325
+ const entryParent = path.join(entry.baseDir, "package.json");
326
+ const probedCandidates = collectModuleResolutionCandidates(
305
327
  specifier,
306
328
  entryParent,
329
+ undefined,
330
+ );
331
+ // Capture every candidate before resolution chooses the descriptor entry.
332
+ // A post-resolution snapshot could bless a higher-priority file created
333
+ // after the resolver had already selected the old entry.
334
+ const probedCandidateHashes = hashHostInputPaths(probedCandidates);
335
+ const probedCandidateRealpaths =
336
+ realpathHostInputPaths(probedCandidates);
337
+ const request = PluginPackageResolution.resolvePluginRequest(
338
+ specifier,
339
+ entry.baseDir,
340
+ );
341
+ // Keep the candidates of the search roots up to the one the entry
342
+ // resolved in: the lookup never read the roots after it
343
+ // (`moduleResolutionBaseSelects`).
344
+ const read = new Set(
345
+ collectModuleResolutionCandidates(
346
+ specifier,
347
+ entryParent,
348
+ request,
349
+ ).map((candidate) => path.resolve(candidate)),
350
+ );
351
+ const entryCandidates = probedCandidates.filter((candidate) =>
352
+ read.has(path.resolve(candidate)),
353
+ );
354
+ const entryCandidateHashes = pickHostInputEntries(
355
+ probedCandidateHashes,
356
+ entryCandidates,
357
+ );
358
+ const entryCandidateRealpaths = pickHostInputEntries(
359
+ probedCandidateRealpaths,
360
+ entryCandidates,
361
+ );
362
+ const loaded = loadPluginEntry(
363
+ entry.config,
364
+ {
365
+ ...context,
366
+ plugin: entry.config,
367
+ // Package factories such as typia resolve their own package from
368
+ // this consumer anchor. Keep the compiler source root below for
369
+ // native cwd, cache ownership and returned relative Go sources.
370
+ projectRoot: ProjectPluginEntries.resolvePluginBaseDir(
371
+ project,
372
+ options.pluginConfigDir,
373
+ ),
374
+ },
307
375
  request,
308
- ).map((candidate) => path.resolve(candidate)),
309
- );
310
- const entryCandidates = probedCandidates.filter((candidate) =>
311
- read.has(path.resolve(candidate)),
312
- );
313
- const entryCandidateHashes = pickHostInputEntries(
314
- probedCandidateHashes,
315
- entryCandidates,
316
- );
317
- const entryCandidateRealpaths = pickHostInputEntries(
318
- probedCandidateRealpaths,
319
- entryCandidates,
320
- );
321
- const loaded = loadPluginEntry(
322
- entry.config,
323
- { ...context, plugin: entry.config },
324
- request,
325
- effectiveEnv,
326
- descriptorCache,
327
- );
328
- const loadedHostInputHashes = mergeObservedHostInputHashes(
329
- loaded.hostInputHashes,
330
- hashHostInputPaths(Object.keys(loaded.hostInputHashes)),
331
- );
332
- const loadedHostInputRealpaths = mergeObservedHostInputRealpaths(
333
- loaded.hostInputRealpaths,
334
- realpathHostInputPaths(Object.keys(loaded.hostInputRealpaths)),
335
- );
336
- const hostInputHashes = mergeObservedHostInputHashes(
337
- entryCandidateHashes,
338
- loadedHostInputHashes,
339
- );
340
- const hostInputRealpaths = mergeObservedHostInputRealpaths(
341
- entryCandidateRealpaths,
342
- loadedHostInputRealpaths,
343
- );
344
- for (const input of loaded.hostInputs) {
345
- const absolute = path.resolve(input);
346
- if (
347
- !Object.prototype.hasOwnProperty.call(
348
- loadedHostInputHashes,
349
- absolute,
350
- )
351
- ) {
352
- delete hostInputHashes[absolute];
376
+ effectiveEnv,
377
+ descriptorCache,
378
+ );
379
+ const loadedHostInputHashes = mergeObservedHostInputHashes(
380
+ loaded.hostInputHashes,
381
+ hashHostInputPaths(Object.keys(loaded.hostInputHashes)),
382
+ );
383
+ const loadedHostInputRealpaths = mergeObservedHostInputRealpaths(
384
+ loaded.hostInputRealpaths,
385
+ realpathHostInputPaths(Object.keys(loaded.hostInputRealpaths)),
386
+ );
387
+ const hostInputHashes = mergeObservedHostInputHashes(
388
+ entryCandidateHashes,
389
+ loadedHostInputHashes,
390
+ );
391
+ const hostInputRealpaths = mergeObservedHostInputRealpaths(
392
+ entryCandidateRealpaths,
393
+ loadedHostInputRealpaths,
394
+ );
395
+ for (const input of loaded.hostInputs) {
396
+ const absolute = path.resolve(input);
397
+ if (
398
+ !Object.prototype.hasOwnProperty.call(
399
+ loadedHostInputHashes,
400
+ absolute,
401
+ )
402
+ ) {
403
+ delete hostInputHashes[absolute];
404
+ }
353
405
  }
354
- }
355
- return {
356
- ...loaded,
357
- hostInputHashes,
358
- hostInputRealpaths,
359
- hostInputs: [...loaded.hostInputs, ...entryCandidates],
360
- request,
361
- };
362
- }),
363
- identities,
364
- );
365
- const plugins = composePluginSources(
366
- entries,
367
- loadedEntries.map((entry) => entry.plugin),
368
- );
406
+ return {
407
+ ...loaded,
408
+ hostInputHashes,
409
+ hostInputRealpaths,
410
+ hostInputs: [...loaded.hostInputs, ...entryCandidates],
411
+ request,
412
+ };
413
+ }),
414
+ identities,
415
+ );
416
+ const plugins = composePluginSources(
417
+ entries,
418
+ loadedEntries.map((entry) => entry.plugin),
419
+ );
369
420
 
370
- const { ttsc: ttscVersion, tsgo: tsgoVersion } = pluginBuildVersions(
371
- context.projectRoot,
372
- );
373
- const candidates = plugins
374
- .map((plugin, index) => {
375
- const stage = PluginDescriptorAdmission.stage(plugin);
376
- validatePluginSource(plugin);
377
- const contributors = validatePluginContributors(plugin);
378
- const source = resolvePluginSource(plugin.source, context.projectRoot);
379
- const label = pluginLabel(plugin, entries[index]!.config, index);
380
- requirePluginSource(source, label);
381
- return { plugin, index, stage, contributors, source, label };
382
- })
383
- .map((candidate) => ({
384
- ...candidate,
385
- ...resolvePluginGoModule(candidate.source, candidate.label),
386
- }));
387
- // One reading of each source directory, shared by package selection and
388
- // every build below and reported as the state the binaries were keyed on.
389
- const sourceDigests = new Map<string, string>();
390
- // And one reading of the environment each build directory is keyed on, which
391
- // a plugin module root's state reports as it is.
392
- const environmentDigests = new Map<string, string>();
393
- // With the witness of each such reading, so a build of a directory package
394
- // selection already read keys on that reading instead of probing Go again.
395
- const environmentWitnesses = new Map<
396
- string,
397
- PluginBuildEnvironmentWitness.Record
398
- >();
399
- const readings = {
400
- sources: sourceDigests,
401
- environments: environmentDigests,
402
- witnesses: environmentWitnesses,
403
- };
404
- // Records describing sources must never land inside one, so the store is
405
- // admitted for source proofs only once every source of the load is known.
406
- const sourceIdentities = PluginContentIdentities.outside(identities, [
407
- ...candidates.flatMap((candidate) => [
408
- candidate.moduleRoot,
409
- candidate.packageDir,
410
- ...(candidate.contributors ?? []).map((input) => input.source),
411
- ]),
412
- ...SourcePluginWorkspace.findTtscOverlayDirs(),
413
- ]);
414
- // One fixed-effective-environment load owns manifest syntax sharing from the
415
- // first watch query through metadata and builds. Do not allocate this after
416
- // watch projection: that silently repeats selected-Go work (#1712).
417
- const packageReaders = new Map<string, SourcePluginWorkspace.GoModReader>();
418
- const watchReplacements =
419
- options.onWatchInputs === undefined
420
- ? undefined
421
- : new Map<string, readonly string[]>();
422
- if (options.onWatchInputs !== undefined) {
423
- // The prospective context reads the admitted module layout and anchors its
424
- // tool there before kind exists. Keep both possible final-root baselines:
425
- // a linked package keeps packageDir, and an executable keeps moduleRoot.
426
- const interests = new Set(
427
- candidates
428
- .flatMap((candidate) => [
429
- candidate.moduleRoot,
430
- candidate.packageDir,
431
- ...(candidate.contributors ?? []).map((input) => input.source),
432
- ])
433
- .filter(reportsPluginSource),
421
+ const { ttsc: ttscVersion, tsgo: tsgoVersion } = pluginBuildVersions(
422
+ context.projectRoot,
434
423
  );
435
- // WARNING (#1728): hand the watch owner this load's store with the roots.
436
- // It baselines each root before any build reads it; without the store it
437
- // read every source the load proves from records a moment later.
438
- options.onWatchInputs([...interests].sort(), sourceIdentities);
439
- const initialSize = interests.size;
440
- for (const candidate of candidates) {
441
- // Nested source-only proposals replace the outer root manifests. Do not
442
- // activate their irrelevant module graph just to discover watch inputs.
443
- if (candidate.stage === "transform" && candidate.entry !== ".") continue;
444
- for (const directory of pluginReplacementWatchDirectories(
424
+ const candidates = plugins
425
+ .map((plugin, index) => {
426
+ const stage = PluginDescriptorAdmission.stage(plugin);
427
+ validatePluginSource(plugin);
428
+ const contributors = validatePluginContributors(plugin);
429
+ const source = resolvePluginSource(plugin.source, context.projectRoot);
430
+ const label = pluginLabel(plugin, entries[index]!.config, index);
431
+ requirePluginSource(source, label);
432
+ return { plugin, index, stage, contributors, source, label };
433
+ })
434
+ .map((candidate) => ({
435
+ ...candidate,
436
+ ...resolvePluginGoModule(candidate.source, candidate.label),
437
+ }));
438
+ // One reading of each source directory, shared by package selection and
439
+ // every build below and reported as the state the binaries were keyed on.
440
+ const sourceDigests = new Map<string, string>();
441
+ // And one reading of the environment each build directory is keyed on, which
442
+ // a plugin module root's state reports as it is.
443
+ const environmentDigests = new Map<string, string>();
444
+ // With the witness of each such reading, so a build of a directory package
445
+ // selection already read keys on that reading instead of probing Go again.
446
+ const environmentWitnesses = new Map<
447
+ string,
448
+ PluginBuildEnvironmentWitness.Record
449
+ >();
450
+ const readings = {
451
+ sources: sourceDigests,
452
+ environments: environmentDigests,
453
+ witnesses: environmentWitnesses,
454
+ };
455
+ // Records describing sources must never land inside one, so the store is
456
+ // admitted for source proofs only once every source of the load is known.
457
+ const sourceIdentities = PluginContentIdentities.outside(identities, [
458
+ ...candidates.flatMap((candidate) => [
445
459
  candidate.moduleRoot,
446
- effectiveEnv,
447
- watchReplacements,
448
- packageReaders,
449
- sourceIdentities,
450
- ))
451
- if (reportsPluginSource(directory)) interests.add(directory);
452
- }
453
- if (interests.size !== initialSize)
454
- options.onWatchInputs([...interests].sort(), sourceIdentities);
455
- }
456
- const proposals = NativeSourcePackages.propose(
457
- candidates.map((candidate) => ({
458
- ...candidate,
459
- ownModule: candidate.stage !== "transform",
460
- })),
461
- effectiveEnv,
462
- packageReaders,
463
- sourceIdentities,
464
- readings,
465
- );
466
- const records = candidates.map(
467
- ({ plugin, index, stage, contributors, source, packageDir }) => {
468
- const { kind, moduleRoot } = resolveNativeSource(
469
- source,
470
- plugin,
471
- entries[index]!.config,
472
- index,
473
- {
474
- env: effectiveEnv,
475
- observation: proposals[index]!.observation,
476
- },
460
+ candidate.packageDir,
461
+ ...(candidate.contributors ?? []).map((input) => input.source),
462
+ ]),
463
+ ...SourcePluginWorkspace.findTtscOverlayDirs(),
464
+ ]);
465
+ // One fixed-effective-environment load owns manifest syntax sharing from the
466
+ // first watch query through metadata and builds. Do not allocate this after
467
+ // watch projection: that silently repeats selected-Go work (#1712).
468
+ const packageReaders = new Map<string, SourcePluginWorkspace.GoModReader>();
469
+ const watchReplacements =
470
+ options.onWatchInputs === undefined
471
+ ? undefined
472
+ : new Map<string, readonly string[]>();
473
+ if (options.onWatchInputs !== undefined) {
474
+ // The prospective context reads the admitted module layout and anchors its
475
+ // tool there before kind exists. Keep both possible final-root baselines:
476
+ // a linked package keeps packageDir, and an executable keeps moduleRoot.
477
+ const interests = new Set(
478
+ candidates
479
+ .flatMap((candidate) => [
480
+ candidate.moduleRoot,
481
+ candidate.packageDir,
482
+ ...(candidate.contributors ?? []).map((input) => input.source),
483
+ ])
484
+ .filter(reportsPluginSource),
477
485
  );
478
- if (kind === "linked" && stage !== "transform") {
479
- throw new Error(
480
- `ttsc: plugin "${pluginLabel(plugin, entries[index]!.config, index)}" source is a linked Go package, but only transform-stage plugins can be linked into a compiler host`,
481
- );
486
+ // WARNING (#1728): hand the watch owner this load's store with the roots.
487
+ // It baselines each root before any build reads it; without the store it
488
+ // read every source the load proves from records a moment later.
489
+ // A worker owns the watch baseline now. Complete this handoff before Go
490
+ // metadata or source proof consumes the roots, preserving #1728 (#1758).
491
+ yield options.onWatchInputs([...interests].sort(), sourceIdentities);
492
+ const initialSize = interests.size;
493
+ for (const candidate of candidates) {
494
+ // Nested source-only proposals replace the outer root manifests. Do not
495
+ // activate their irrelevant module graph just to discover watch inputs.
496
+ if (candidate.stage === "transform" && candidate.entry !== ".")
497
+ continue;
498
+ for (const directory of pluginReplacementWatchDirectories(
499
+ candidate.moduleRoot,
500
+ effectiveEnv,
501
+ watchReplacements,
502
+ packageReaders,
503
+ sourceIdentities,
504
+ ))
505
+ if (reportsPluginSource(directory)) interests.add(directory);
482
506
  }
483
- const linkedContributorName =
484
- kind === "linked"
485
- ? `linked_${String(index).padStart(6, "0")}`
486
- : undefined;
487
- const hostInputs = validatePluginHostInputs(plugin, index);
488
- const pluginHostInputHashes = validatePluginHostInputHashes(
489
- plugin,
490
- index,
491
- hostInputs,
492
- );
493
- const pluginHostInputRealpaths = validatePluginHostInputRealpaths(
494
- plugin,
495
- index,
496
- hostInputs,
497
- );
498
- return {
499
- capabilities: plugin.capabilities,
500
- contributors,
501
- config: entries[index]!.config,
502
- kind,
503
- label: pluginLabel(plugin, entries[index]!.config, index),
504
- linkedContributorName,
505
- name: plugin.name,
506
- reportsTypeScriptDiagnostics:
507
- plugin.reportsTypeScriptDiagnostics === true,
508
- request: loadedEntries[index]!.request,
509
- hostInputHashes: mergePluginHostInputHashes(
510
- loadedEntries[index]!.hostInputHashes,
511
- pluginHostInputHashes,
512
- loadedEntries[index]!.hostInputs,
507
+ if (interests.size !== initialSize)
508
+ yield options.onWatchInputs([...interests].sort(), sourceIdentities);
509
+ }
510
+ const proposals = NativeSourcePackages.propose(
511
+ candidates.map((candidate) => ({
512
+ ...candidate,
513
+ ownModule: candidate.stage !== "transform",
514
+ })),
515
+ effectiveEnv,
516
+ packageReaders,
517
+ sourceIdentities,
518
+ readings,
519
+ );
520
+ const records = candidates.map(
521
+ ({ plugin, index, stage, contributors, source, packageDir }) => {
522
+ const { kind, moduleRoot } = resolveNativeSource(
523
+ source,
524
+ plugin,
525
+ entries[index]!.config,
526
+ index,
527
+ {
528
+ env: effectiveEnv,
529
+ observation: proposals[index]!.observation,
530
+ },
531
+ );
532
+ if (kind === "linked" && stage !== "transform") {
533
+ throw new Error(
534
+ `ttsc: plugin "${pluginLabel(plugin, entries[index]!.config, index)}" source is a linked Go package, but only transform-stage plugins can be linked into a compiler host`,
535
+ );
536
+ }
537
+ const linkedContributorName =
538
+ kind === "linked"
539
+ ? `linked_${String(index).padStart(6, "0")}`
540
+ : undefined;
541
+ const hostInputs = validatePluginHostInputs(plugin, index);
542
+ const pluginHostInputHashes = validatePluginHostInputHashes(
543
+ plugin,
544
+ index,
513
545
  hostInputs,
514
- ),
515
- hostInputRealpaths: mergePluginHostInputHashes(
516
- loadedEntries[index]!.hostInputRealpaths,
517
- pluginHostInputRealpaths,
518
- loadedEntries[index]!.hostInputs,
546
+ );
547
+ const pluginHostInputRealpaths = validatePluginHostInputRealpaths(
548
+ plugin,
549
+ index,
519
550
  hostInputs,
520
- ),
521
- hostInputs: [...loadedEntries[index]!.hostInputs, ...hostInputs],
522
- moduleRoot,
523
- packageDir,
524
- source,
525
- stage,
526
- };
527
- },
528
- );
529
- const executableCandidates = candidates.filter(
530
- (candidate) =>
531
- records[candidate.index]!.kind === "executable" &&
532
- !proposals[candidate.index]!.ownModule,
533
- );
534
- const buildDirectories = pluginBuildDirectories(
535
- records,
536
- effectiveEnv,
537
- watchReplacements,
538
- packageReaders,
539
- sourceIdentities,
540
- );
541
- // Admission can now narrow the provisional interests without restamping the
542
- // retained package/module baselines. Final host errors still keep these roots.
543
- options.onWatchInputs?.(buildDirectories, sourceIdentities);
544
- const keyedDirectories = new Set(buildDirectories);
545
- const linkedContributors = records
546
- .filter((record) => record.stage === "transform")
547
- .flatMap((record) =>
548
- record.kind === "linked"
549
- ? [{ name: record.linkedContributorName!, source: record.source }]
550
- : [],
551
+ );
552
+ return {
553
+ capabilities: plugin.capabilities,
554
+ contributors,
555
+ config: entries[index]!.config,
556
+ kind,
557
+ label: pluginLabel(plugin, entries[index]!.config, index),
558
+ linkedContributorName,
559
+ name: plugin.name,
560
+ reportsTypeScriptDiagnostics:
561
+ plugin.reportsTypeScriptDiagnostics === true,
562
+ request: loadedEntries[index]!.request,
563
+ hostInputHashes: mergePluginHostInputHashes(
564
+ loadedEntries[index]!.hostInputHashes,
565
+ pluginHostInputHashes,
566
+ loadedEntries[index]!.hostInputs,
567
+ hostInputs,
568
+ ),
569
+ hostInputRealpaths: mergePluginHostInputHashes(
570
+ loadedEntries[index]!.hostInputRealpaths,
571
+ pluginHostInputRealpaths,
572
+ loadedEntries[index]!.hostInputs,
573
+ hostInputs,
574
+ ),
575
+ hostInputs: [...loadedEntries[index]!.hostInputs, ...hostInputs],
576
+ moduleRoot,
577
+ packageDir,
578
+ source,
579
+ stage,
580
+ };
581
+ },
551
582
  );
552
- const transformHosts = records.filter(
553
- (record) => record.stage === "transform" && record.kind === "executable",
554
- );
555
- const hostContributors =
556
- linkedContributors.length === 0 ? undefined : linkedContributors;
557
- // Every host compiles the linked sources under its own module/tool context.
558
- // Equivalent entries in one module share this metadata command and reader;
559
- // a second executable host can select a genuinely different Go context.
560
- const admittedExecutables = new Set<string>();
561
- if (linkedContributors.length !== 0) {
562
- const hosts =
563
- transformHosts.length === 0
564
- ? [
565
- {
566
- source: path.join(ttscPackageRoot(), "cmd", "utility-host"),
567
- label: "linked-plugin-host",
568
- },
569
- ]
570
- : transformHosts;
571
- const contexts = new Map<
572
- string,
573
- { source: string; label: string; records: typeof transformHosts }
574
- >();
575
- for (const host of hosts) {
576
- const moduleRoot = resolvePluginGoModule(
577
- host.source,
578
- host.label,
579
- ).moduleRoot;
580
- let context = contexts.get(moduleRoot);
581
- if (context === undefined) {
582
- context = { source: host.source, label: host.label, records: [] };
583
- contexts.set(moduleRoot, context);
583
+ const executableCandidates = candidates.filter(
584
+ (candidate) =>
585
+ records[candidate.index]!.kind === "executable" &&
586
+ !proposals[candidate.index]!.ownModule,
587
+ );
588
+ const buildDirectories = pluginBuildDirectories(
589
+ records,
590
+ effectiveEnv,
591
+ watchReplacements,
592
+ packageReaders,
593
+ sourceIdentities,
594
+ );
595
+ // Admission can now narrow the provisional interests without restamping the
596
+ // retained package/module baselines. Final host errors still keep these roots.
597
+ yield options.onWatchInputs?.(buildDirectories, sourceIdentities);
598
+ const keyedDirectories = new Set(buildDirectories);
599
+ const linkedContributors = records
600
+ .filter((record) => record.stage === "transform")
601
+ .flatMap((record) =>
602
+ record.kind === "linked"
603
+ ? [{ name: record.linkedContributorName!, source: record.source }]
604
+ : [],
605
+ );
606
+ const transformHosts = records.filter(
607
+ (record) => record.stage === "transform" && record.kind === "executable",
608
+ );
609
+ const hostContributors =
610
+ linkedContributors.length === 0 ? undefined : linkedContributors;
611
+ // Every host compiles the linked sources under its own module/tool context.
612
+ // Equivalent entries in one module share this metadata command and reader;
613
+ // a second executable host can select a genuinely different Go context.
614
+ const admittedExecutables = new Set<string>();
615
+ if (linkedContributors.length !== 0) {
616
+ const hosts =
617
+ transformHosts.length === 0
618
+ ? [
619
+ {
620
+ source: path.join(ttscPackageRoot(), "cmd", "utility-host"),
621
+ label: "linked-plugin-host",
622
+ },
623
+ ]
624
+ : transformHosts;
625
+ const contexts = new Map<
626
+ string,
627
+ { source: string; label: string; records: typeof transformHosts }
628
+ >();
629
+ for (const host of hosts) {
630
+ const moduleRoot = resolvePluginGoModule(
631
+ host.source,
632
+ host.label,
633
+ ).moduleRoot;
634
+ let context = contexts.get(moduleRoot);
635
+ if (context === undefined) {
636
+ context = { source: host.source, label: host.label, records: [] };
637
+ contexts.set(moduleRoot, context);
638
+ }
639
+ }
640
+ for (const record of transformHosts)
641
+ contexts.get(record.moduleRoot)!.records.push(record);
642
+ for (const host of contexts.values()) {
643
+ const moduleHosts = host.records;
644
+ const hostEntries = moduleHosts.map((record) => ({
645
+ entry: resolvePluginGoModule(record.source, record.label).entry,
646
+ }));
647
+ const observations = NativeSourcePackages.inspect({
648
+ source: host.source,
649
+ pluginName: host.label,
650
+ env: effectiveEnv,
651
+ readers: packageReaders,
652
+ identities: sourceIdentities,
653
+ readings,
654
+ packages: [
655
+ ...hostEntries,
656
+ ...linkedContributors.map((input) => ({
657
+ ...input,
658
+ entry: `./contrib/${input.name}`,
659
+ })),
660
+ ],
661
+ });
662
+ moduleHosts.forEach((record, index) => {
663
+ NativeSourcePackages.kind(
664
+ observations[index]!,
665
+ record.label,
666
+ "executable",
667
+ );
668
+ admittedExecutables.add(record.source);
669
+ });
670
+ linkedContributors.forEach((input, index) => {
671
+ NativeSourcePackages.kind(
672
+ observations[hostEntries.length + index]!,
673
+ `${input.name} inside host ${host.label}`,
674
+ "linked",
675
+ );
676
+ });
584
677
  }
585
678
  }
586
- for (const record of transformHosts)
587
- contexts.get(record.moduleRoot)!.records.push(record);
588
- for (const host of contexts.values()) {
589
- const moduleHosts = host.records;
590
- const hostEntries = moduleHosts.map((record) => ({
591
- entry: resolvePluginGoModule(record.source, record.label).entry,
592
- }));
593
- const observations = NativeSourcePackages.inspect({
594
- source: host.source,
595
- pluginName: host.label,
596
- env: effectiveEnv,
597
- readers: packageReaders,
598
- identities: sourceIdentities,
599
- readings,
600
- packages: [
601
- ...hostEntries,
602
- ...linkedContributors.map((input) => ({
603
- ...input,
604
- entry: `./contrib/${input.name}`,
605
- })),
606
- ],
607
- });
608
- moduleHosts.forEach((record, index) => {
609
- NativeSourcePackages.kind(
610
- observations[index]!,
611
- record.label,
612
- "executable",
613
- );
614
- admittedExecutables.add(record.source);
615
- });
616
- linkedContributors.forEach((input, index) => {
617
- NativeSourcePackages.kind(
618
- observations[hostEntries.length + index]!,
619
- `${input.name} inside host ${host.label}`,
620
- "linked",
621
- );
622
- });
623
- }
624
- }
625
- // A nested main proposal still needs its actual owning manifests. Hosts
626
- // with linked contributors were admitted together above; other equivalent
627
- // entries share one standalone observation, with no repeated host copy.
628
- const standaloneCandidates = executableCandidates.filter(
629
- (candidate) => !admittedExecutables.has(candidate.source),
630
- );
631
- const executablePackages = NativeSourcePackages.ownPackages(
632
- standaloneCandidates,
633
- effectiveEnv,
634
- packageReaders,
635
- sourceIdentities,
636
- readings,
637
- );
638
- standaloneCandidates.forEach((candidate, index) => {
639
- NativeSourcePackages.kind(
640
- executablePackages[index]!,
641
- candidate.label,
642
- "executable",
679
+ // A nested main proposal still needs its actual owning manifests. Hosts
680
+ // with linked contributors were admitted together above; other equivalent
681
+ // entries share one standalone observation, with no repeated host copy.
682
+ const standaloneCandidates = executableCandidates.filter(
683
+ (candidate) => !admittedExecutables.has(candidate.source),
643
684
  );
644
- });
645
- const packageOwnership = (source: string, label: string, linked: boolean) => [
646
- {
647
- entry: resolvePluginGoModule(source, label).entry,
648
- kind: "executable" as const,
649
- },
650
- ...(linked
651
- ? linkedContributors.map((input) => ({
652
- entry: `./contrib/${input.name}`,
653
- kind: "linked" as const,
654
- }))
655
- : []),
656
- ];
657
- const builtTransformHosts = new Map<object, string>();
658
- for (const record of transformHosts) {
659
- builtTransformHosts.set(
660
- record,
661
- buildSourcePlugin({
662
- baseDir: context.projectRoot,
663
- cacheDir: options.cacheDir,
664
- contributors: mergeContributors(record.contributors, hostContributors),
665
- env: effectiveEnv,
666
- goModReaders: packageReaders,
667
- identities: sourceIdentities,
668
- pluginName: record.label,
669
- packageOwnership: packageOwnership(record.source, record.label, true),
670
- source: record.source,
671
- environmentDigests,
672
- environmentWitnesses,
673
- sourceDigests,
674
- ttscVersion,
675
- tsgoVersion,
676
- }),
685
+ const executablePackages = NativeSourcePackages.ownPackages(
686
+ standaloneCandidates,
687
+ effectiveEnv,
688
+ packageReaders,
689
+ sourceIdentities,
690
+ readings,
677
691
  );
678
- }
679
- const fallbackDriverHost =
680
- transformHosts.length === 0 && linkedContributors.length !== 0
681
- ? buildSourcePlugin({
692
+ standaloneCandidates.forEach((candidate, index) => {
693
+ NativeSourcePackages.kind(
694
+ executablePackages[index]!,
695
+ candidate.label,
696
+ "executable",
697
+ );
698
+ });
699
+ const packageOwnership = (
700
+ source: string,
701
+ label: string,
702
+ linked: boolean,
703
+ ) => [
704
+ {
705
+ entry: resolvePluginGoModule(source, label).entry,
706
+ kind: "executable" as const,
707
+ },
708
+ ...(linked
709
+ ? linkedContributors.map((input) => ({
710
+ entry: `./contrib/${input.name}`,
711
+ kind: "linked" as const,
712
+ }))
713
+ : []),
714
+ ];
715
+ const builtTransformHosts = new Map<object, string>();
716
+ for (const record of transformHosts) {
717
+ builtTransformHosts.set(
718
+ record,
719
+ buildSourcePlugin({
682
720
  baseDir: context.projectRoot,
683
721
  cacheDir: options.cacheDir,
684
- contributors: linkedContributors,
722
+ contributors: mergeContributors(
723
+ record.contributors,
724
+ hostContributors,
725
+ ),
685
726
  env: effectiveEnv,
686
727
  goModReaders: packageReaders,
687
728
  identities: sourceIdentities,
688
- label: "linked plugin host",
689
- pluginName: "linked-plugin-host",
690
- source: path.join(ttscPackageRoot(), "cmd", "utility-host"),
691
- packageOwnership: packageOwnership(
692
- path.join(ttscPackageRoot(), "cmd", "utility-host"),
693
- "linked-plugin-host",
694
- true,
695
- ),
729
+ pluginName: record.label,
730
+ packageOwnership: packageOwnership(record.source, record.label, true),
731
+ source: record.source,
696
732
  environmentDigests,
697
733
  environmentWitnesses,
698
734
  sourceDigests,
699
735
  ttscVersion,
700
736
  tsgoVersion,
701
- })
702
- : undefined;
703
- const selectedTransformHost =
704
- transformHosts.length === 0
705
- ? fallbackDriverHost
706
- : builtTransformHosts.get(transformHosts[0]!);
707
- const nativePlugins: ITtscLoadedNativePlugin[] = records.map((record) => {
708
- const binary =
709
- record.stage === "transform" && record.kind === "linked"
710
- ? selectedTransformHost
711
- : record.stage === "transform"
712
- ? builtTransformHosts.get(record)
713
- : buildSourcePlugin({
714
- baseDir: context.projectRoot,
715
- cacheDir: options.cacheDir,
716
- contributors: record.contributors,
717
- env: effectiveEnv,
718
- goModReaders: packageReaders,
719
- identities: sourceIdentities,
720
- pluginName: record.label,
721
- packageOwnership: packageOwnership(
722
- record.source,
723
- record.label,
724
- false,
725
- ),
726
- source: record.source,
727
- environmentDigests,
728
- environmentWitnesses,
729
- sourceDigests,
730
- ttscVersion,
731
- tsgoVersion,
732
- });
733
- if (binary === undefined) {
734
- throw new Error(
735
- `ttsc: plugin "${record.label}" is a linked Go package, but no compiler host is available`,
737
+ }),
736
738
  );
737
739
  }
740
+ const fallbackDriverHost =
741
+ transformHosts.length === 0 && linkedContributors.length !== 0
742
+ ? buildSourcePlugin({
743
+ baseDir: context.projectRoot,
744
+ cacheDir: options.cacheDir,
745
+ contributors: linkedContributors,
746
+ env: effectiveEnv,
747
+ goModReaders: packageReaders,
748
+ identities: sourceIdentities,
749
+ label: "linked plugin host",
750
+ pluginName: "linked-plugin-host",
751
+ source: path.join(ttscPackageRoot(), "cmd", "utility-host"),
752
+ packageOwnership: packageOwnership(
753
+ path.join(ttscPackageRoot(), "cmd", "utility-host"),
754
+ "linked-plugin-host",
755
+ true,
756
+ ),
757
+ environmentDigests,
758
+ environmentWitnesses,
759
+ sourceDigests,
760
+ ttscVersion,
761
+ tsgoVersion,
762
+ })
763
+ : undefined;
764
+ const selectedTransformHost =
765
+ transformHosts.length === 0
766
+ ? fallbackDriverHost
767
+ : builtTransformHosts.get(transformHosts[0]!);
768
+ const nativePlugins: ITtscLoadedNativePlugin[] = records.map((record) => {
769
+ const binary =
770
+ record.stage === "transform" && record.kind === "linked"
771
+ ? selectedTransformHost
772
+ : record.stage === "transform"
773
+ ? builtTransformHosts.get(record)
774
+ : buildSourcePlugin({
775
+ baseDir: context.projectRoot,
776
+ cacheDir: options.cacheDir,
777
+ contributors: record.contributors,
778
+ env: effectiveEnv,
779
+ goModReaders: packageReaders,
780
+ identities: sourceIdentities,
781
+ pluginName: record.label,
782
+ packageOwnership: packageOwnership(
783
+ record.source,
784
+ record.label,
785
+ false,
786
+ ),
787
+ source: record.source,
788
+ environmentDigests,
789
+ environmentWitnesses,
790
+ sourceDigests,
791
+ ttscVersion,
792
+ tsgoVersion,
793
+ });
794
+ if (binary === undefined) {
795
+ throw new Error(
796
+ `ttsc: plugin "${record.label}" is a linked Go package, but no compiler host is available`,
797
+ );
798
+ }
799
+ return {
800
+ binary,
801
+ // This fallback is compiled from our utility-host, whose actual writer
802
+ // publishes provenance and supports a separate compiler argument cwd.
803
+ // A separately selected executable host must opt in itself; a linked
804
+ // library cannot certify that foreign host. An undeclared capability
805
+ // only withholds the optional protocol: build execution still runs such
806
+ // a host and observes it from outside, never rejecting it.
807
+ capabilities:
808
+ record.stage === "transform" &&
809
+ record.kind === "linked" &&
810
+ fallbackDriverHost !== undefined
811
+ ? {
812
+ ...record.capabilities,
813
+ compilerArgsCwd: true,
814
+ emitProvenance: true,
815
+ }
816
+ : record.capabilities,
817
+ config: record.config,
818
+ contributors: record.contributors,
819
+ kind: record.kind,
820
+ name: record.name,
821
+ reportsTypeScriptDiagnostics: record.reportsTypeScriptDiagnostics,
822
+ source: record.source,
823
+ stage: record.stage,
824
+ };
825
+ });
738
826
  return {
739
- binary,
740
- // This fallback is compiled from our utility-host, whose actual writer
741
- // publishes provenance and supports a separate compiler argument cwd.
742
- // A separately selected executable host must opt in itself; a linked
743
- // library cannot certify that foreign host. An undeclared capability
744
- // only withholds the optional protocol: build execution still runs such
745
- // a host and observes it from outside, never rejecting it.
746
- capabilities:
747
- record.stage === "transform" &&
748
- record.kind === "linked" &&
749
- fallbackDriverHost !== undefined
750
- ? {
751
- ...record.capabilities,
752
- compilerArgsCwd: true,
753
- emitProvenance: true,
754
- }
755
- : record.capabilities,
756
- config: record.config,
757
- contributors: record.contributors,
758
- kind: record.kind,
759
- name: record.name,
760
- reportsTypeScriptDiagnostics: record.reportsTypeScriptDiagnostics,
761
- source: record.source,
762
- stage: record.stage,
763
- };
764
- });
765
- return {
766
- ...collectHostInputSnapshot(
767
- project,
768
- context,
769
- records,
770
- projectHostInputs,
771
- revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
772
- revalidateHostInputRealpaths(
773
- projectHostInputRealpaths,
827
+ ...collectHostInputSnapshot(
828
+ project,
829
+ context,
830
+ records,
774
831
  projectHostInputs,
832
+ revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
833
+ revalidateHostInputRealpaths(
834
+ projectHostInputRealpaths,
835
+ projectHostInputs,
836
+ ),
775
837
  ),
776
- ),
777
- descriptorReadsDeclared: loadedEntries.every(
778
- (entry) =>
779
- entry.observationsComplete && declaresHostInputReads(entry.plugin),
780
- ),
781
- discoveryInputsComplete: project.configInputsComplete === true,
782
- ...(identities === undefined ? {} : { identities }),
783
- nativePlugins: orderNativePlugins(nativePlugins),
784
- ...(loadedEntries.some((entry) => !entry.observationsComplete)
785
- ? { observationsComplete: false as const }
786
- : {}),
787
- // The directories the watch inputs named before the builds, as the builds
788
- // read them. Package selection shares these readings and also reads a
789
- // linked plugin's module root, which no build keys on, so only the build
790
- // directories are reported.
791
- pluginSources: Object.fromEntries(
792
- [...sourceDigests]
793
- .filter(([directory]) => keyedDirectories.has(directory))
794
- .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
795
- .map(([directory, sourceDigest]) => [
796
- directory,
797
- pluginSourceState(directory, {
798
- env: effectiveEnv,
799
- sourceDigest,
800
- environment:
801
- environmentDigests.get(directory) ??
802
- pluginBuildEnvironment(
803
- directory,
804
- effectiveEnv,
805
- undefined,
806
- sourceIdentities,
807
- ),
808
- }),
809
- ]),
810
- ),
811
- project,
812
- };
838
+ descriptorReadsDeclared: loadedEntries.every(
839
+ (entry) =>
840
+ entry.observationsComplete && declaresHostInputReads(entry.plugin),
841
+ ),
842
+ discoveryInputsComplete: project.configInputsComplete === true,
843
+ ...(identities === undefined ? {} : { identities }),
844
+ nativePlugins: orderNativePlugins(nativePlugins),
845
+ ...(loadedEntries.some((entry) => !entry.observationsComplete)
846
+ ? { observationsComplete: false as const }
847
+ : {}),
848
+ // The directories the watch inputs named before the builds, as the builds
849
+ // read them. Package selection shares these readings and also reads a
850
+ // linked plugin's module root, which no build keys on, so only the build
851
+ // directories are reported.
852
+ pluginSources: Object.fromEntries(
853
+ [...sourceDigests]
854
+ .filter(([directory]) => keyedDirectories.has(directory))
855
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
856
+ .map(([directory, sourceDigest]) => [
857
+ directory,
858
+ pluginSourceState(directory, {
859
+ env: effectiveEnv,
860
+ sourceDigest,
861
+ environment:
862
+ environmentDigests.get(directory) ??
863
+ pluginBuildEnvironment(
864
+ directory,
865
+ effectiveEnv,
866
+ undefined,
867
+ sourceIdentities,
868
+ ),
869
+ }),
870
+ ]),
871
+ ),
872
+ project,
873
+ };
874
+ }
813
875
  }
814
876
 
815
877
  type ProjectHostInputSnapshot = {
@@ -948,12 +1010,18 @@ function readProjectHostInputSnapshot(options: {
948
1010
  cwd?: string;
949
1011
  file?: string;
950
1012
  includePluginDiscovery: boolean;
1013
+ pluginConfigDir?: string;
951
1014
  projectRoot?: string;
952
1015
  tsconfig?: string;
953
1016
  }): ProjectHostInputSnapshot {
954
- const { includePluginDiscovery, ...projectOptions } = options;
1017
+ const { includePluginDiscovery, pluginConfigDir, ...projectOptions } =
1018
+ options;
955
1019
  let project = readProjectConfig(projectOptions);
956
- let hostInputs = collectProjectHostInputs(project, includePluginDiscovery);
1020
+ let hostInputs = collectProjectHostInputs(
1021
+ project,
1022
+ includePluginDiscovery,
1023
+ pluginConfigDir,
1024
+ );
957
1025
  const observedInputs = new Set(hostInputs);
958
1026
  for (let attempt = 0; attempt < 3; attempt++) {
959
1027
  const before = hashHostInputPaths(hostInputs);
@@ -963,6 +1031,7 @@ function readProjectHostInputSnapshot(options: {
963
1031
  const candidateInputs = collectProjectHostInputs(
964
1032
  candidateProject,
965
1033
  includePluginDiscovery,
1034
+ pluginConfigDir,
966
1035
  );
967
1036
  for (const input of candidateInputs) observedInputs.add(input);
968
1037
  const after = hashHostInputPaths(candidateInputs);
@@ -1384,7 +1453,7 @@ function loadPluginDescriptor(
1384
1453
  request,
1385
1454
  context,
1386
1455
  effectiveEnv,
1387
- descriptorCache.identities,
1456
+ descriptorCache,
1388
1457
  );
1389
1458
  if (loaded === undefined) {
1390
1459
  throw error;
@@ -1401,6 +1470,9 @@ interface DescriptorCacheOptions {
1401
1470
  /** Record store that proves the evaluating runtimes from metadata (#1723). */
1402
1471
  identities: PluginContentIdentities.Store | undefined;
1403
1472
 
1473
+ /** Source-project cache anchor, independent of descriptor consumer context. */
1474
+ projectRoot: string;
1475
+
1404
1476
  /** This ttsc build's version. */
1405
1477
  version: string;
1406
1478
  }
@@ -1453,7 +1525,7 @@ function loadCommonJsDescriptor(
1453
1525
  const runtimeCapabilities = javascriptRuntimeCapabilities(
1454
1526
  runtime,
1455
1527
  effectiveEnv,
1456
- context.projectRoot,
1528
+ descriptorCache.projectRoot,
1457
1529
  descriptorCache.identities,
1458
1530
  );
1459
1531
  const node =
@@ -1463,7 +1535,7 @@ function loadCommonJsDescriptor(
1463
1535
  ? runtimeCapabilities.executable
1464
1536
  : resolveNodeBinary(
1465
1537
  effectiveEnv,
1466
- context.projectRoot,
1538
+ descriptorCache.projectRoot,
1467
1539
  descriptorCache.identities,
1468
1540
  );
1469
1541
  const cacheAuthority = {
@@ -1471,6 +1543,8 @@ function loadCommonJsDescriptor(
1471
1543
  cacheDir: descriptorCache.cacheDir,
1472
1544
  identities: descriptorCache.identities,
1473
1545
  context,
1546
+ // Factory context anchors the consumer; the evaluator runs at the source root.
1547
+ cwd: descriptorCache.projectRoot,
1474
1548
  // Everything the child receives besides the per-evaluation output paths.
1475
1549
  env: {
1476
1550
  ...effectiveEnv,
@@ -1478,7 +1552,7 @@ function loadCommonJsDescriptor(
1478
1552
  ...descriptorRecordRoot(descriptorCache.identities),
1479
1553
  TTSC_TTSX_BINARY: ttsx,
1480
1554
  },
1481
- projectRoot: context.projectRoot,
1555
+ projectRoot: descriptorCache.projectRoot,
1482
1556
  request,
1483
1557
  runtime,
1484
1558
  version: descriptorCache.version,
@@ -1559,7 +1633,7 @@ function loadCommonJsDescriptor(
1559
1633
  shim,
1560
1634
  ],
1561
1635
  {
1562
- cwd: context.projectRoot,
1636
+ cwd: descriptorCache.projectRoot,
1563
1637
  env: {
1564
1638
  ...effectiveEnv,
1565
1639
  // The direct evaluator may be Bun, but ttsx and native config
@@ -1950,12 +2024,16 @@ function loadDescriptorViaTtsx(
1950
2024
  request: string,
1951
2025
  context: ITtscPluginFactoryContext,
1952
2026
  effectiveEnv: NodeJS.ProcessEnv,
1953
- identities: PluginContentIdentities.Store | undefined,
2027
+ descriptorCache: DescriptorCacheOptions,
1954
2028
  ): IsolatedPluginDescriptor | undefined {
1955
2029
  // Binary discovery prefers the instance environment, then the ambient
1956
2030
  // process.env (where `withPluginLoaderEnv` injects ttsc's own node/ttsx paths
1957
2031
  // just before this runs), then the running interpreter.
1958
- const node = resolveNodeBinary(effectiveEnv, context.projectRoot, identities);
2032
+ const node = resolveNodeBinary(
2033
+ effectiveEnv,
2034
+ descriptorCache.projectRoot,
2035
+ descriptorCache.identities,
2036
+ );
1959
2037
  const ttsx = effectiveEnv.TTSC_TTSX_BINARY ?? process.env.TTSC_TTSX_BINARY;
1960
2038
  if (node === undefined || ttsx === undefined || ttsx.length === 0) {
1961
2039
  return undefined;
@@ -1982,11 +2060,11 @@ function loadDescriptorViaTtsx(
1982
2060
  const trace = E2ETrace.begin(
1983
2061
  node,
1984
2062
  [ttsx, "--no-plugins", shim],
1985
- { cwd: context.projectRoot },
2063
+ { cwd: descriptorCache.projectRoot },
1986
2064
  "plugin-descriptor",
1987
2065
  );
1988
2066
  const result = spawnOwnedDescriptor(node, [ttsx, "--no-plugins", shim], {
1989
- cwd: context.projectRoot,
2067
+ cwd: descriptorCache.projectRoot,
1990
2068
  encoding: "utf8",
1991
2069
  env: {
1992
2070
  ...effectiveEnv,
@@ -1994,7 +2072,7 @@ function loadDescriptorViaTtsx(
1994
2072
  // may recurse into further descriptor loads) finds them even when the
1995
2073
  // instance-env snapshot predates `withPluginLoaderEnv`.
1996
2074
  TTSC_NODE_BINARY: node,
1997
- ...descriptorRecordRoot(identities),
2075
+ ...descriptorRecordRoot(descriptorCache.identities),
1998
2076
  TTSC_TTSX_BINARY: ttsx,
1999
2077
  TTSC_PLUGIN_CONTEXT: JSON.stringify({
2000
2078
  binary: context.binary,