ttsc 0.31.6 → 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 (261) hide show
  1. package/cmd/ttsc/compiler_argument_cwd_test.go +38 -38
  2. package/cmd/ttscserver/main.go +2 -12
  3. package/cmd/ttscserver/resolve_launch_uses_native_flag_authority_test.go +7 -7
  4. package/driver/lsp.go +4 -43
  5. package/driver/rewrite.go +1 -1
  6. package/internal/e2etrace/completion_hints.go +3 -3
  7. package/internal/e2etrace/completion_hints_test.go +26 -12
  8. package/internal/graph/build.go +5 -6
  9. package/internal/graph/graph.go +3 -3
  10. package/internal/graph/line_model.go +2 -35
  11. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +13 -36
  12. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +9 -3
  13. package/internal/lspserver/lsp_plugin.go +19 -0
  14. package/internal/lspserver/lsp_proxy.go +9 -129
  15. package/internal/lspserver/lsp_server.go +0 -8
  16. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +6 -36
  17. package/internal/sourceprocess/windows_target_owns_suspended_admission_test.go +5 -5
  18. package/lib/TtscCompiler.js +1 -1
  19. package/lib/TtscCompiler.js.map +1 -1
  20. package/lib/compiler/internal/ResidentCheckProcess.d.ts +74 -96
  21. package/lib/compiler/internal/ResidentCheckProcess.js +150 -335
  22. package/lib/compiler/internal/ResidentCheckProcess.js.map +1 -1
  23. package/lib/compiler/internal/ResidentCheckProcessHost.d.ts +127 -0
  24. package/lib/compiler/internal/ResidentCheckProcessHost.js +447 -0
  25. package/lib/compiler/internal/ResidentCheckProcessHost.js.map +1 -0
  26. package/lib/compiler/internal/ResidentCheckProcessProtocol.d.ts +64 -0
  27. package/lib/compiler/internal/ResidentCheckProcessProtocol.js +3 -0
  28. package/lib/compiler/internal/ResidentCheckProcessProtocol.js.map +1 -0
  29. package/lib/compiler/internal/build/BuildExecution.d.ts +103 -1
  30. package/lib/compiler/internal/build/BuildExecution.js +92 -12
  31. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  32. package/lib/compiler/internal/build/ResidentCheckWatchSession.d.ts +16 -11
  33. package/lib/compiler/internal/build/ResidentCheckWatchSession.js +37 -21
  34. package/lib/compiler/internal/build/ResidentCheckWatchSession.js.map +1 -1
  35. package/lib/compiler/internal/build/RunBuildOptions.d.ts +6 -5
  36. package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
  37. package/lib/compiler/internal/build/WatchInputPublication.d.ts +59 -0
  38. package/lib/compiler/internal/build/WatchInputPublication.js +93 -0
  39. package/lib/compiler/internal/build/WatchInputPublication.js.map +1 -0
  40. package/lib/compiler/internal/build/runBuild.d.ts +29 -2
  41. package/lib/compiler/internal/build/runBuild.js +45 -19
  42. package/lib/compiler/internal/build/runBuild.js.map +1 -1
  43. package/lib/compiler/internal/buildNativeCompiler.d.ts +2 -0
  44. package/lib/compiler/internal/buildNativeCompiler.js +1 -0
  45. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  46. package/lib/compiler/internal/compileProjectInMemory.js +4 -2
  47. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  48. package/lib/compiler/internal/project/CompilerProjectSelection.d.ts +9 -8
  49. package/lib/compiler/internal/project/CompilerProjectSelection.js +11 -9
  50. package/lib/compiler/internal/project/CompilerProjectSelection.js.map +1 -1
  51. package/lib/compiler/internal/project/resolveOwningProjectConfig.d.ts +25 -8
  52. package/lib/compiler/internal/project/resolveOwningProjectConfig.js +26 -14
  53. package/lib/compiler/internal/project/resolveOwningProjectConfig.js.map +1 -1
  54. package/lib/compiler/internal/readEffectiveCompilerOptions.js +1 -1
  55. package/lib/compiler/internal/readEffectiveCompilerOptions.js.map +1 -1
  56. package/lib/compiler/internal/resolveBinary.d.ts +1 -0
  57. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  58. package/lib/compiler/internal/resolveTsgo.d.ts +6 -0
  59. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  60. package/lib/compiler/internal/runSingleFileEmit.d.ts +32 -4
  61. package/lib/compiler/internal/runSingleFileEmit.js +124 -89
  62. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  63. package/lib/compiler/internal/startResidentTransform.js +8 -2
  64. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  65. package/lib/compiler/internal/transformProjectInMemory.js +11 -3
  66. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  67. package/lib/compiler/internal/transformProjectInWorker.d.ts +1 -1
  68. package/lib/compiler/internal/transformProjectInWorker.js +3 -2
  69. package/lib/compiler/internal/transformProjectInWorker.js.map +1 -1
  70. package/lib/flags/ParseOptions.d.ts +2 -2
  71. package/lib/internal/E2ETrace.js.map +1 -1
  72. package/lib/internal/OwnedNativeProcess.d.ts +2 -2
  73. package/lib/internal/OwnedNativeProcess.js +2 -2
  74. package/lib/internal/SourceNativeRetirement.d.ts +8 -7
  75. package/lib/internal/SourceNativeRetirement.js +19 -10
  76. package/lib/internal/SourceNativeRetirement.js.map +1 -1
  77. package/lib/internal/TtsxNodeOptions.d.ts +79 -0
  78. package/lib/internal/TtsxNodeOptions.js +121 -0
  79. package/lib/internal/TtsxNodeOptions.js.map +1 -0
  80. package/lib/internal/javascriptRuntimeCapabilities.js +4 -1
  81. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  82. package/lib/internal/parseWindowsDirectoryCaseSensitivity.d.ts +5 -5
  83. package/lib/internal/parseWindowsDirectoryCaseSensitivity.js +5 -5
  84. package/lib/internal/pathIdentity/createCaseSensitivityObservations.d.ts +35 -0
  85. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js +209 -0
  86. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js.map +1 -0
  87. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js +2 -143
  88. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js.map +1 -1
  89. package/lib/internal/projectInputPathIdentity.d.ts +1 -0
  90. package/lib/internal/projectInputPathIdentity.js +1 -0
  91. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  92. package/lib/internal/workerModuleUrl.d.ts +28 -0
  93. package/lib/internal/workerModuleUrl.js +40 -0
  94. package/lib/internal/workerModuleUrl.js.map +1 -0
  95. package/lib/launcher/internal/buildSingleRootProject.js +2 -1
  96. package/lib/launcher/internal/buildSingleRootProject.js.map +1 -1
  97. package/lib/launcher/internal/getCompilerVersionText.js +3 -2
  98. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  99. package/lib/launcher/internal/parseTtsxCLI.d.ts +5 -4
  100. package/lib/launcher/internal/parseTtsxCLI.js +5 -4
  101. package/lib/launcher/internal/parseTtsxCLI.js.map +1 -1
  102. package/lib/launcher/internal/prepareExecution.js +2 -1
  103. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  104. package/lib/launcher/internal/resolveSingleFileOutput.d.ts +8 -9
  105. package/lib/launcher/internal/resolveSingleFileOutput.js +8 -9
  106. package/lib/launcher/internal/resolveSingleFileOutput.js.map +1 -1
  107. package/lib/launcher/internal/runTtsc.js +91 -41
  108. package/lib/launcher/internal/runTtsc.js.map +1 -1
  109. package/lib/launcher/internal/runTtsx.js +5 -2
  110. package/lib/launcher/internal/runTtsx.js.map +1 -1
  111. package/lib/launcher/internal/runtime/CommonJsRuntimeSource.d.ts +10 -6
  112. package/lib/launcher/internal/runtime/CommonJsRuntimeSource.js +11 -9
  113. package/lib/launcher/internal/runtime/CommonJsRuntimeSource.js.map +1 -1
  114. package/lib/launcher/internal/runtime/RuntimeEntrySources.d.ts +40 -0
  115. package/lib/launcher/internal/runtime/RuntimeEntrySources.js +57 -0
  116. package/lib/launcher/internal/runtime/RuntimeEntrySources.js.map +1 -0
  117. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.d.ts +5 -4
  118. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js +8 -6
  119. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js.map +1 -1
  120. package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.d.ts +20 -0
  121. package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.js +27 -0
  122. package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.js.map +1 -0
  123. package/lib/launcher/internal/runtime/commonJsExportNames.d.ts +2 -1
  124. package/lib/launcher/internal/runtime/commonJsExportNames.js +2 -1
  125. package/lib/launcher/internal/runtime/commonJsExportNames.js.map +1 -1
  126. package/lib/launcher/internal/runtime/installRuntimeHooks.d.ts +5 -3
  127. package/lib/launcher/internal/runtime/installRuntimeHooks.js +42 -12
  128. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  129. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js +3 -1
  130. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js.map +1 -1
  131. package/lib/launcher/internal/watch/AsyncWatchTopology.d.ts +113 -0
  132. package/lib/launcher/internal/watch/AsyncWatchTopology.js +232 -0
  133. package/lib/launcher/internal/watch/AsyncWatchTopology.js.map +1 -0
  134. package/lib/launcher/internal/watch/WatchSettlement.d.ts +49 -0
  135. package/lib/launcher/internal/watch/WatchSettlement.js +70 -0
  136. package/lib/launcher/internal/watch/WatchSettlement.js.map +1 -0
  137. package/lib/launcher/internal/watch/WatchTopology.d.ts +103 -13
  138. package/lib/launcher/internal/watch/WatchTopology.js +380 -95
  139. package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
  140. package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.d.ts +121 -0
  141. package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.js +3 -0
  142. package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.js.map +1 -0
  143. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.d.ts +4 -1
  144. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js +3 -2
  145. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js.map +1 -1
  146. package/lib/launcher/internal/watch/serveWatchTopologyWorker.d.ts +19 -0
  147. package/lib/launcher/internal/watch/serveWatchTopologyWorker.js +62 -0
  148. package/lib/launcher/internal/watch/serveWatchTopologyWorker.js.map +1 -0
  149. package/lib/launcher/internal/watch/watchTopologyWorker.d.ts +1 -0
  150. package/lib/launcher/internal/watch/watchTopologyWorker.js +22 -0
  151. package/lib/launcher/internal/watch/watchTopologyWorker.js.map +1 -0
  152. package/lib/plugin/CapabilityPluginResolver.d.ts +1 -1
  153. package/lib/plugin/CapabilityPluginResolver.js +3 -2
  154. package/lib/plugin/CapabilityPluginResolver.js.map +1 -1
  155. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +15 -12
  156. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +13 -11
  157. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
  158. package/lib/plugin/internal/load/PluginPackageResolution.d.ts +2 -2
  159. package/lib/plugin/internal/load/PluginPackageResolution.js +13 -7
  160. package/lib/plugin/internal/load/PluginPackageResolution.js.map +1 -1
  161. package/lib/plugin/internal/load/ProjectPluginEntries.d.ts +23 -7
  162. package/lib/plugin/internal/load/ProjectPluginEntries.js +36 -14
  163. package/lib/plugin/internal/load/ProjectPluginEntries.js.map +1 -1
  164. package/lib/plugin/internal/load/collectProjectHostInputs.d.ts +3 -1
  165. package/lib/plugin/internal/load/collectProjectHostInputs.js +5 -2
  166. package/lib/plugin/internal/load/collectProjectHostInputs.js.map +1 -1
  167. package/lib/plugin/internal/load/hasProjectPluginEntries.d.ts +3 -1
  168. package/lib/plugin/internal/load/hasProjectPluginEntries.js +4 -2
  169. package/lib/plugin/internal/load/hasProjectPluginEntries.js.map +1 -1
  170. package/lib/plugin/internal/load/loadProjectPlugins.d.ts +36 -5
  171. package/lib/plugin/internal/load/loadProjectPlugins.js +477 -421
  172. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  173. package/lib/plugin/internal/load/pluginResolutionFailure.d.ts +18 -0
  174. package/lib/plugin/internal/load/pluginResolutionFailure.js +32 -0
  175. package/lib/plugin/internal/load/pluginResolutionFailure.js.map +1 -0
  176. package/lib/plugin/internal/load/resolveNativeSource.js +2 -3
  177. package/lib/plugin/internal/load/resolveNativeSource.js.map +1 -1
  178. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +1 -1
  179. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +3 -2
  180. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
  181. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.d.ts +4 -1
  182. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js +9 -2
  183. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js.map +1 -1
  184. package/lib/structures/ITtscCompilerContext.d.ts +12 -12
  185. package/lib/structures/ITtscPlugin.d.ts +7 -5
  186. package/lib/structures/ITtscPluginFactoryContext.d.ts +9 -8
  187. package/lib/structures/internal/TtscCommonOptions.d.ts +2 -2
  188. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +12 -6
  189. package/package.json +9 -9
  190. package/src/TtscCompiler.ts +1 -1
  191. package/src/compiler/internal/ResidentCheckProcess.ts +159 -374
  192. package/src/compiler/internal/ResidentCheckProcessHost.ts +502 -0
  193. package/src/compiler/internal/ResidentCheckProcessProtocol.ts +53 -0
  194. package/src/compiler/internal/build/BuildExecution.ts +123 -11
  195. package/src/compiler/internal/build/ResidentCheckWatchSession.ts +41 -24
  196. package/src/compiler/internal/build/RunBuildOptions.ts +9 -5
  197. package/src/compiler/internal/build/TsgoArguments.ts +4 -1
  198. package/src/compiler/internal/build/WatchInputPublication.ts +106 -0
  199. package/src/compiler/internal/build/runBuild.ts +58 -34
  200. package/src/compiler/internal/buildNativeCompiler.ts +4 -0
  201. package/src/compiler/internal/compileProjectInMemory.ts +8 -2
  202. package/src/compiler/internal/project/CompilerProjectSelection.ts +14 -14
  203. package/src/compiler/internal/project/resolveOwningProjectConfig.ts +42 -16
  204. package/src/compiler/internal/readEffectiveCompilerOptions.ts +1 -1
  205. package/src/compiler/internal/resolveBinary.ts +4 -1
  206. package/src/compiler/internal/resolveTsgo.ts +7 -0
  207. package/src/compiler/internal/runSingleFileEmit.ts +145 -101
  208. package/src/compiler/internal/startResidentTransform.ts +9 -2
  209. package/src/compiler/internal/transformProjectInMemory.ts +12 -3
  210. package/src/compiler/internal/transformProjectInWorker.ts +10 -2
  211. package/src/flags/ParseOptions.ts +2 -2
  212. package/src/internal/E2ETrace.ts +3 -1
  213. package/src/internal/OwnedNativeProcess.ts +2 -2
  214. package/src/internal/SourceNativeRetirement.ts +42 -19
  215. package/src/internal/TtsxNodeOptions.ts +124 -0
  216. package/src/internal/javascriptRuntimeCapabilities.ts +6 -1
  217. package/src/internal/parseWindowsDirectoryCaseSensitivity.ts +5 -5
  218. package/src/internal/pathIdentity/createCaseSensitivityObservations.ts +242 -0
  219. package/src/internal/pathIdentity/createFilesystemPathIdentityContext.ts +2 -169
  220. package/src/internal/projectInputPathIdentity.ts +1 -0
  221. package/src/internal/workerModuleUrl.ts +34 -0
  222. package/src/launcher/internal/buildSingleRootProject.ts +2 -0
  223. package/src/launcher/internal/getCompilerVersionText.ts +3 -2
  224. package/src/launcher/internal/parseTtsxCLI.ts +5 -4
  225. package/src/launcher/internal/prepareExecution.ts +6 -1
  226. package/src/launcher/internal/resolveSingleFileOutput.ts +8 -9
  227. package/src/launcher/internal/runTtsc.ts +127 -49
  228. package/src/launcher/internal/runTtsx.ts +17 -8
  229. package/src/launcher/internal/runtime/CommonJsRuntimeSource.ts +11 -9
  230. package/src/launcher/internal/runtime/RuntimeEntrySources.ts +57 -0
  231. package/src/launcher/internal/runtime/RuntimeProjectOwnership.ts +23 -10
  232. package/src/launcher/internal/runtime/advertiseTypeScriptExtensions.ts +25 -0
  233. package/src/launcher/internal/runtime/commonJsExportNames.ts +11 -4
  234. package/src/launcher/internal/runtime/installRuntimeHooks.ts +62 -16
  235. package/src/launcher/internal/runtime/resolveRuntimeCleanTargets.ts +17 -5
  236. package/src/launcher/internal/watch/AsyncWatchTopology.ts +267 -0
  237. package/src/launcher/internal/watch/WatchSettlement.ts +67 -0
  238. package/src/launcher/internal/watch/WatchTopology.ts +461 -102
  239. package/src/launcher/internal/watch/WatchTopologyWorkerProtocol.ts +106 -0
  240. package/src/launcher/internal/watch/projectInputReloadEventShouldNotify.ts +14 -9
  241. package/src/launcher/internal/watch/serveWatchTopologyWorker.ts +75 -0
  242. package/src/launcher/internal/watch/watchTopologyWorker.ts +29 -0
  243. package/src/plugin/CapabilityPluginResolver.ts +9 -2
  244. package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +17 -12
  245. package/src/plugin/internal/load/PluginPackageResolution.ts +14 -9
  246. package/src/plugin/internal/load/ProjectPluginEntries.ts +40 -15
  247. package/src/plugin/internal/load/collectProjectHostInputs.ts +7 -1
  248. package/src/plugin/internal/load/hasProjectPluginEntries.ts +8 -3
  249. package/src/plugin/internal/load/loadProjectPlugins.ts +683 -593
  250. package/src/plugin/internal/load/pluginResolutionFailure.ts +37 -0
  251. package/src/plugin/internal/load/resolveNativeSource.ts +10 -4
  252. package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +8 -2
  253. package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +10 -1
  254. package/src/structures/ITtscCompilerContext.ts +12 -12
  255. package/src/structures/ITtscPlugin.ts +7 -5
  256. package/src/structures/ITtscPluginFactoryContext.ts +9 -8
  257. package/src/structures/internal/TtscCommonOptions.ts +2 -2
  258. package/src/structures/internal/TtscSingleFileEmitOptions.ts +22 -11
  259. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +0 -78
  260. package/internal/graphsymbols/provider.go +0 -650
  261. package/internal/lspserver/lsp_symbols.go +0 -212
@@ -4,11 +4,13 @@ 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";
10
11
  import { E2ETrace } from "../../../internal/E2ETrace";
11
12
  import { OwnedSynchronousProcess } from "../../../internal/OwnedSynchronousProcess";
13
+ import { TtsxNodeOptions } from "../../../internal/TtsxNodeOptions";
12
14
  import { createCanonicalTempDirectory } from "../../../internal/createCanonicalTempDirectory";
13
15
  import { javascriptRuntimeCapabilities } from "../../../internal/javascriptRuntimeCapabilities";
14
16
  import { resolveNodeBinary } from "../../../internal/resolveNodeBinary";
@@ -19,6 +21,7 @@ import type { ITtscPluginFactoryContext } from "../../../structures/ITtscPluginF
19
21
  import type { ITtscProjectPluginConfig } from "../../../structures/ITtscProjectPluginConfig";
20
22
  import type { ITtscLoadedNativePlugin } from "../../../structures/internal/ITtscLoadedNativePlugin";
21
23
  import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
24
+ import type { TtscPluginSourceInputsHandler } from "../../../structures/internal/TtscSingleFileEmitOptions";
22
25
  import { pluginDescriptorFailureReason } from "../pluginDescriptorFailureReason";
23
26
  import { pluginDescriptorProcessFailure } from "../pluginDescriptorProcessFailure";
24
27
  import { NativeSourcePackages } from "../source/NativeSourcePackages";
@@ -82,6 +85,9 @@ import { visitImportMappedCandidates } from "./visitImportMappedCandidates";
82
85
  * declaration. Sequential content, metadata and physical-path observations are
83
86
  * not an atomic snapshot or detection of every omitted read.
84
87
  *
88
+ * Synchronous callers require synchronous watch-input handlers; use
89
+ * {@link loadProjectPlugins.async} to await a worker's baseline publication.
90
+ *
85
91
  * @param options.binary - Absolute path to the ttsc native helper binary.
86
92
  * @param options.cacheDir - Override the plugin binary cache directory.
87
93
  * @param options.cwd - Working directory for resolving relative paths.
@@ -92,11 +98,12 @@ import { visitImportMappedCandidates } from "./visitImportMappedCandidates";
92
98
  * native environment-name identity. Defaults to `process.env` for CLI
93
99
  * callers, so ambient behavior is unchanged.
94
100
  * @param options.file - Path to the tsconfig/jsconfig file.
95
- * @param options.pluginConfigDir - Caller-declared anchor for plugin
96
- * config-file discovery (see `ITtscPluginFactoryContext.pluginConfigDir`).
97
- * @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.
98
105
  * @param options.tsconfig - Alias for `file`.
99
- * @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.
100
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.
101
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.
102
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.
@@ -134,10 +141,7 @@ export function loadProjectPlugins(options: {
134
141
  * Observe plugin build roots before building, including failed builds, with
135
142
  * the record store that proves their states (#1728).
136
143
  */
137
- onWatchInputs?: (
138
- inputs: readonly string[],
139
- identities?: PluginContentIdentities.Store,
140
- ) => void;
144
+ onWatchInputs?: TtscPluginSourceInputsHandler;
141
145
 
142
146
  /** Caller-selected anchor for plugin-owned configuration discovery. */
143
147
  pluginConfigDir?: string;
@@ -198,615 +202,676 @@ export function loadProjectPlugins(options: {
198
202
  /** Resolved project values and logical/physical configuration identity. */
199
203
  project: ITtscParsedProjectConfig;
200
204
  } {
201
- // Snapshot the caller environment before `withPluginLoaderEnv` injects
202
- // host-owned Node/ttsx locators into process.env. Under a Bun parent the
203
- // direct descriptor evaluator must remain Bun unless the caller explicitly
204
- // selected another runtime.
205
- const effectiveEnv = SidecarEnvironment.merge(options.env ?? process.env);
206
- const projectSnapshot = readProjectHostInputSnapshot({
207
- cwd: options.cwd,
208
- file: options.file,
209
- includePluginDiscovery: options.entries === undefined,
210
- projectRoot: options.projectRoot,
211
- tsconfig: options.tsconfig,
212
- });
213
- const { project } = projectSnapshot;
214
- const projectHostInputs = projectSnapshot.hostInputs;
215
- const projectHostInputHashes = projectSnapshot.hostInputHashes;
216
- const projectHostInputRealpaths = projectSnapshot.hostInputRealpaths;
217
- const entries: ProjectPluginEntries.ProjectPluginEntry[] =
218
- options.entries === false
219
- ? []
220
- : 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(
221
268
  project,
222
- options.entries,
223
- ).filter((entry) => entry.config.enabled !== false);
224
- if (entries.length === 0) {
225
- options.onWatchInputs?.([]);
226
- return {
227
- ...collectHostInputSnapshot(
228
- project,
229
- {},
230
- [],
231
- projectHostInputs,
232
- revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
233
- revalidateHostInputRealpaths(
234
- projectHostInputRealpaths,
269
+ {},
270
+ [],
235
271
  projectHostInputs,
272
+ revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
273
+ revalidateHostInputRealpaths(
274
+ projectHostInputRealpaths,
275
+ projectHostInputs,
276
+ ),
236
277
  ),
237
- ),
238
- descriptorReadsDeclared: true,
239
- discoveryInputsComplete: project.configInputsComplete === true,
240
- nativePlugins: [],
241
- pluginSources: {},
242
- project,
243
- };
244
- }
278
+ descriptorReadsDeclared: true,
279
+ discoveryInputsComplete: project.configInputsComplete === true,
280
+ nativePlugins: [],
281
+ pluginSources: {},
282
+ project,
283
+ };
284
+ }
245
285
 
246
- const cwd = path.resolve(options.cwd ?? process.cwd());
247
- const context = {
248
- binary: options.binary,
249
- cwd,
250
- ...(options.pluginConfigDir === undefined || options.pluginConfigDir === ""
251
- ? {}
252
- : { pluginConfigDir: path.resolve(cwd, options.pluginConfigDir) }),
253
- projectRoot: project.root,
254
- tsconfig: project.path,
255
- };
256
- // Where isolated descriptor evaluations keep their answers across launches
257
- // (`PluginDescriptorEvaluationCache`).
258
- // WARNING (#1721, #1722, #1723): a warm load must prove its unchanged inputs
259
- // from this store, not re-read them. Every launch is a new process, so any
260
- // module-level memo of this load's work dies with it; a 27 s warm typia
261
- // launch whose native build took 0.28 s is what that looked like.
262
- const identities =
263
- options.identities ??
264
- 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) }),
265
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 = {
266
311
  cacheDir: options.cacheDir,
267
- env: effectiveEnv,
268
- });
269
- const descriptorCache: DescriptorCacheOptions = {
270
- cacheDir: options.cacheDir,
271
- identities,
272
- version: pluginBuildVersions(project.root).ttsc,
273
- };
274
- const loadedEntries = withPluginLoaderEnv(
275
- () =>
276
- entries.map((entry) => {
277
- const specifier = entry.config.transform;
278
- if (typeof specifier !== "string" || specifier.length === 0) {
279
- throw new Error(
280
- `ttsc: plugin entry is missing a string "transform" field`,
281
- );
282
- }
283
- const entryParent = path.join(entry.baseDir, "package.json");
284
- const probedCandidates = collectModuleResolutionCandidates(
285
- specifier,
286
- entryParent,
287
- undefined,
288
- );
289
- // Capture every candidate before resolution chooses the descriptor entry.
290
- // A post-resolution snapshot could bless a higher-priority file created
291
- // after the resolver had already selected the old entry.
292
- const probedCandidateHashes = hashHostInputPaths(probedCandidates);
293
- const probedCandidateRealpaths =
294
- realpathHostInputPaths(probedCandidates);
295
- const request = PluginPackageResolution.resolvePluginRequest(
296
- specifier,
297
- entry.baseDir,
298
- );
299
- // Keep the candidates of the search roots up to the one the entry
300
- // resolved in: the lookup never read the roots after it
301
- // (`moduleResolutionBaseSelects`).
302
- const read = new Set(
303
- 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(
304
327
  specifier,
305
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
+ },
306
375
  request,
307
- ).map((candidate) => path.resolve(candidate)),
308
- );
309
- const entryCandidates = probedCandidates.filter((candidate) =>
310
- read.has(path.resolve(candidate)),
311
- );
312
- const entryCandidateHashes = pickHostInputEntries(
313
- probedCandidateHashes,
314
- entryCandidates,
315
- );
316
- const entryCandidateRealpaths = pickHostInputEntries(
317
- probedCandidateRealpaths,
318
- entryCandidates,
319
- );
320
- const loaded = loadPluginEntry(
321
- entry.config,
322
- { ...context, plugin: entry.config },
323
- request,
324
- effectiveEnv,
325
- descriptorCache,
326
- );
327
- const loadedHostInputHashes = mergeObservedHostInputHashes(
328
- loaded.hostInputHashes,
329
- hashHostInputPaths(Object.keys(loaded.hostInputHashes)),
330
- );
331
- const loadedHostInputRealpaths = mergeObservedHostInputRealpaths(
332
- loaded.hostInputRealpaths,
333
- realpathHostInputPaths(Object.keys(loaded.hostInputRealpaths)),
334
- );
335
- const hostInputHashes = mergeObservedHostInputHashes(
336
- entryCandidateHashes,
337
- loadedHostInputHashes,
338
- );
339
- const hostInputRealpaths = mergeObservedHostInputRealpaths(
340
- entryCandidateRealpaths,
341
- loadedHostInputRealpaths,
342
- );
343
- for (const input of loaded.hostInputs) {
344
- const absolute = path.resolve(input);
345
- if (
346
- !Object.prototype.hasOwnProperty.call(
347
- loadedHostInputHashes,
348
- absolute,
349
- )
350
- ) {
351
- 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
+ }
352
405
  }
353
- }
354
- return {
355
- ...loaded,
356
- hostInputHashes,
357
- hostInputRealpaths,
358
- hostInputs: [...loaded.hostInputs, ...entryCandidates],
359
- request,
360
- };
361
- }),
362
- identities,
363
- );
364
- const plugins = composePluginSources(
365
- entries,
366
- loadedEntries.map((entry) => entry.plugin),
367
- );
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
+ );
368
420
 
369
- const { ttsc: ttscVersion, tsgo: tsgoVersion } = pluginBuildVersions(
370
- context.projectRoot,
371
- );
372
- const candidates = plugins
373
- .map((plugin, index) => {
374
- const stage = PluginDescriptorAdmission.stage(plugin);
375
- validatePluginSource(plugin);
376
- const contributors = validatePluginContributors(plugin);
377
- const source = resolvePluginSource(plugin.source, context.projectRoot);
378
- const label = pluginLabel(plugin, entries[index]!.config, index);
379
- requirePluginSource(source, label);
380
- return { plugin, index, stage, contributors, source, label };
381
- })
382
- .map((candidate) => ({
383
- ...candidate,
384
- ...resolvePluginGoModule(candidate.source, candidate.label),
385
- }));
386
- // One reading of each source directory, shared by package selection and
387
- // every build below and reported as the state the binaries were keyed on.
388
- const sourceDigests = new Map<string, string>();
389
- // And one reading of the environment each build directory is keyed on, which
390
- // a plugin module root's state reports as it is.
391
- const environmentDigests = new Map<string, string>();
392
- // With the witness of each such reading, so a build of a directory package
393
- // selection already read keys on that reading instead of probing Go again.
394
- const environmentWitnesses = new Map<
395
- string,
396
- PluginBuildEnvironmentWitness.Record
397
- >();
398
- const readings = {
399
- sources: sourceDigests,
400
- environments: environmentDigests,
401
- witnesses: environmentWitnesses,
402
- };
403
- // Records describing sources must never land inside one, so the store is
404
- // admitted for source proofs only once every source of the load is known.
405
- const sourceIdentities = PluginContentIdentities.outside(identities, [
406
- ...candidates.flatMap((candidate) => [
407
- candidate.moduleRoot,
408
- candidate.packageDir,
409
- ...(candidate.contributors ?? []).map((input) => input.source),
410
- ]),
411
- ...SourcePluginWorkspace.findTtscOverlayDirs(),
412
- ]);
413
- // One fixed-effective-environment load owns manifest syntax sharing from the
414
- // first watch query through metadata and builds. Do not allocate this after
415
- // watch projection: that silently repeats selected-Go work (#1712).
416
- const packageReaders = new Map<string, SourcePluginWorkspace.GoModReader>();
417
- const watchReplacements =
418
- options.onWatchInputs === undefined
419
- ? undefined
420
- : new Map<string, readonly string[]>();
421
- if (options.onWatchInputs !== undefined) {
422
- // The prospective context reads the admitted module layout and anchors its
423
- // tool there before kind exists. Keep both possible final-root baselines:
424
- // a linked package keeps packageDir, and an executable keeps moduleRoot.
425
- const interests = new Set(
426
- candidates
427
- .flatMap((candidate) => [
428
- candidate.moduleRoot,
429
- candidate.packageDir,
430
- ...(candidate.contributors ?? []).map((input) => input.source),
431
- ])
432
- .filter(reportsPluginSource),
421
+ const { ttsc: ttscVersion, tsgo: tsgoVersion } = pluginBuildVersions(
422
+ context.projectRoot,
433
423
  );
434
- // WARNING (#1728): hand the watch owner this load's store with the roots.
435
- // It baselines each root before any build reads it; without the store it
436
- // read every source the load proves from records a moment later.
437
- options.onWatchInputs([...interests].sort(), sourceIdentities);
438
- const initialSize = interests.size;
439
- for (const candidate of candidates) {
440
- // Nested source-only proposals replace the outer root manifests. Do not
441
- // activate their irrelevant module graph just to discover watch inputs.
442
- if (candidate.stage === "transform" && candidate.entry !== ".") continue;
443
- 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) => [
444
459
  candidate.moduleRoot,
445
- effectiveEnv,
446
- watchReplacements,
447
- packageReaders,
448
- ))
449
- if (reportsPluginSource(directory)) interests.add(directory);
450
- }
451
- if (interests.size !== initialSize)
452
- options.onWatchInputs([...interests].sort(), sourceIdentities);
453
- }
454
- const proposals = NativeSourcePackages.propose(
455
- candidates.map((candidate) => ({
456
- ...candidate,
457
- ownModule: candidate.stage !== "transform",
458
- })),
459
- effectiveEnv,
460
- packageReaders,
461
- sourceIdentities,
462
- readings,
463
- );
464
- const records = candidates.map(
465
- ({ plugin, index, stage, contributors, source, packageDir }) => {
466
- const { kind, moduleRoot } = resolveNativeSource(
467
- source,
468
- plugin,
469
- entries[index]!.config,
470
- index,
471
- {
472
- env: effectiveEnv,
473
- observation: proposals[index]!.observation,
474
- },
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),
475
485
  );
476
- if (kind === "linked" && stage !== "transform") {
477
- throw new Error(
478
- `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`,
479
- );
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);
480
506
  }
481
- const linkedContributorName =
482
- kind === "linked"
483
- ? `linked_${String(index).padStart(6, "0")}`
484
- : undefined;
485
- const hostInputs = validatePluginHostInputs(plugin, index);
486
- const pluginHostInputHashes = validatePluginHostInputHashes(
487
- plugin,
488
- index,
489
- hostInputs,
490
- );
491
- const pluginHostInputRealpaths = validatePluginHostInputRealpaths(
492
- plugin,
493
- index,
494
- hostInputs,
495
- );
496
- return {
497
- capabilities: plugin.capabilities,
498
- contributors,
499
- config: entries[index]!.config,
500
- kind,
501
- label: pluginLabel(plugin, entries[index]!.config, index),
502
- linkedContributorName,
503
- name: plugin.name,
504
- reportsTypeScriptDiagnostics:
505
- plugin.reportsTypeScriptDiagnostics === true,
506
- request: loadedEntries[index]!.request,
507
- hostInputHashes: mergePluginHostInputHashes(
508
- loadedEntries[index]!.hostInputHashes,
509
- pluginHostInputHashes,
510
- 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,
511
545
  hostInputs,
512
- ),
513
- hostInputRealpaths: mergePluginHostInputHashes(
514
- loadedEntries[index]!.hostInputRealpaths,
515
- pluginHostInputRealpaths,
516
- loadedEntries[index]!.hostInputs,
546
+ );
547
+ const pluginHostInputRealpaths = validatePluginHostInputRealpaths(
548
+ plugin,
549
+ index,
517
550
  hostInputs,
518
- ),
519
- hostInputs: [...loadedEntries[index]!.hostInputs, ...hostInputs],
520
- moduleRoot,
521
- packageDir,
522
- source,
523
- stage,
524
- };
525
- },
526
- );
527
- const executableCandidates = candidates.filter(
528
- (candidate) =>
529
- records[candidate.index]!.kind === "executable" &&
530
- !proposals[candidate.index]!.ownModule,
531
- );
532
- const buildDirectories = pluginBuildDirectories(
533
- records,
534
- effectiveEnv,
535
- watchReplacements,
536
- packageReaders,
537
- );
538
- // Admission can now narrow the provisional interests without restamping the
539
- // retained package/module baselines. Final host errors still keep these roots.
540
- options.onWatchInputs?.(buildDirectories, sourceIdentities);
541
- const keyedDirectories = new Set(buildDirectories);
542
- const linkedContributors = records
543
- .filter((record) => record.stage === "transform")
544
- .flatMap((record) =>
545
- record.kind === "linked"
546
- ? [{ name: record.linkedContributorName!, source: record.source }]
547
- : [],
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
+ },
548
582
  );
549
- const transformHosts = records.filter(
550
- (record) => record.stage === "transform" && record.kind === "executable",
551
- );
552
- const hostContributors =
553
- linkedContributors.length === 0 ? undefined : linkedContributors;
554
- // Every host compiles the linked sources under its own module/tool context.
555
- // Equivalent entries in one module share this metadata command and reader;
556
- // a second executable host can select a genuinely different Go context.
557
- const admittedExecutables = new Set<string>();
558
- if (linkedContributors.length !== 0) {
559
- const hosts =
560
- transformHosts.length === 0
561
- ? [
562
- {
563
- source: path.join(ttscPackageRoot(), "cmd", "utility-host"),
564
- label: "linked-plugin-host",
565
- },
566
- ]
567
- : transformHosts;
568
- const contexts = new Map<
569
- string,
570
- { source: string; label: string; records: typeof transformHosts }
571
- >();
572
- for (const host of hosts) {
573
- const moduleRoot = resolvePluginGoModule(
574
- host.source,
575
- host.label,
576
- ).moduleRoot;
577
- let context = contexts.get(moduleRoot);
578
- if (context === undefined) {
579
- context = { source: host.source, label: host.label, records: [] };
580
- 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
+ });
581
677
  }
582
678
  }
583
- for (const record of transformHosts)
584
- contexts.get(record.moduleRoot)!.records.push(record);
585
- for (const host of contexts.values()) {
586
- const moduleHosts = host.records;
587
- const hostEntries = moduleHosts.map((record) => ({
588
- entry: resolvePluginGoModule(record.source, record.label).entry,
589
- }));
590
- const observations = NativeSourcePackages.inspect({
591
- source: host.source,
592
- pluginName: host.label,
593
- env: effectiveEnv,
594
- readers: packageReaders,
595
- identities: sourceIdentities,
596
- readings,
597
- packages: [
598
- ...hostEntries,
599
- ...linkedContributors.map((input) => ({
600
- ...input,
601
- entry: `./contrib/${input.name}`,
602
- })),
603
- ],
604
- });
605
- moduleHosts.forEach((record, index) => {
606
- NativeSourcePackages.kind(
607
- observations[index]!,
608
- record.label,
609
- "executable",
610
- );
611
- admittedExecutables.add(record.source);
612
- });
613
- linkedContributors.forEach((input, index) => {
614
- NativeSourcePackages.kind(
615
- observations[hostEntries.length + index]!,
616
- `${input.name} inside host ${host.label}`,
617
- "linked",
618
- );
619
- });
620
- }
621
- }
622
- // A nested main proposal still needs its actual owning manifests. Hosts
623
- // with linked contributors were admitted together above; other equivalent
624
- // entries share one standalone observation, with no repeated host copy.
625
- const standaloneCandidates = executableCandidates.filter(
626
- (candidate) => !admittedExecutables.has(candidate.source),
627
- );
628
- const executablePackages = NativeSourcePackages.ownPackages(
629
- standaloneCandidates,
630
- effectiveEnv,
631
- packageReaders,
632
- sourceIdentities,
633
- readings,
634
- );
635
- standaloneCandidates.forEach((candidate, index) => {
636
- NativeSourcePackages.kind(
637
- executablePackages[index]!,
638
- candidate.label,
639
- "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),
640
684
  );
641
- });
642
- const packageOwnership = (source: string, label: string, linked: boolean) => [
643
- {
644
- entry: resolvePluginGoModule(source, label).entry,
645
- kind: "executable" as const,
646
- },
647
- ...(linked
648
- ? linkedContributors.map((input) => ({
649
- entry: `./contrib/${input.name}`,
650
- kind: "linked" as const,
651
- }))
652
- : []),
653
- ];
654
- const builtTransformHosts = new Map<object, string>();
655
- for (const record of transformHosts) {
656
- builtTransformHosts.set(
657
- record,
658
- buildSourcePlugin({
659
- baseDir: context.projectRoot,
660
- cacheDir: options.cacheDir,
661
- contributors: mergeContributors(record.contributors, hostContributors),
662
- env: effectiveEnv,
663
- goModReaders: packageReaders,
664
- identities: sourceIdentities,
665
- pluginName: record.label,
666
- packageOwnership: packageOwnership(record.source, record.label, true),
667
- source: record.source,
668
- environmentDigests,
669
- environmentWitnesses,
670
- sourceDigests,
671
- ttscVersion,
672
- tsgoVersion,
673
- }),
685
+ const executablePackages = NativeSourcePackages.ownPackages(
686
+ standaloneCandidates,
687
+ effectiveEnv,
688
+ packageReaders,
689
+ sourceIdentities,
690
+ readings,
674
691
  );
675
- }
676
- const fallbackDriverHost =
677
- transformHosts.length === 0 && linkedContributors.length !== 0
678
- ? 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({
679
720
  baseDir: context.projectRoot,
680
721
  cacheDir: options.cacheDir,
681
- contributors: linkedContributors,
722
+ contributors: mergeContributors(
723
+ record.contributors,
724
+ hostContributors,
725
+ ),
682
726
  env: effectiveEnv,
683
727
  goModReaders: packageReaders,
684
728
  identities: sourceIdentities,
685
- label: "linked plugin host",
686
- pluginName: "linked-plugin-host",
687
- source: path.join(ttscPackageRoot(), "cmd", "utility-host"),
688
- packageOwnership: packageOwnership(
689
- path.join(ttscPackageRoot(), "cmd", "utility-host"),
690
- "linked-plugin-host",
691
- true,
692
- ),
729
+ pluginName: record.label,
730
+ packageOwnership: packageOwnership(record.source, record.label, true),
731
+ source: record.source,
693
732
  environmentDigests,
694
733
  environmentWitnesses,
695
734
  sourceDigests,
696
735
  ttscVersion,
697
736
  tsgoVersion,
698
- })
699
- : undefined;
700
- const selectedTransformHost =
701
- transformHosts.length === 0
702
- ? fallbackDriverHost
703
- : builtTransformHosts.get(transformHosts[0]!);
704
- const nativePlugins: ITtscLoadedNativePlugin[] = records.map((record) => {
705
- const binary =
706
- record.stage === "transform" && record.kind === "linked"
707
- ? selectedTransformHost
708
- : record.stage === "transform"
709
- ? builtTransformHosts.get(record)
710
- : buildSourcePlugin({
711
- baseDir: context.projectRoot,
712
- cacheDir: options.cacheDir,
713
- contributors: record.contributors,
714
- env: effectiveEnv,
715
- goModReaders: packageReaders,
716
- identities: sourceIdentities,
717
- pluginName: record.label,
718
- packageOwnership: packageOwnership(
719
- record.source,
720
- record.label,
721
- false,
722
- ),
723
- source: record.source,
724
- environmentDigests,
725
- environmentWitnesses,
726
- sourceDigests,
727
- ttscVersion,
728
- tsgoVersion,
729
- });
730
- if (binary === undefined) {
731
- throw new Error(
732
- `ttsc: plugin "${record.label}" is a linked Go package, but no compiler host is available`,
737
+ }),
733
738
  );
734
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
+ });
735
826
  return {
736
- binary,
737
- // This fallback is compiled from our utility-host, whose actual writer
738
- // publishes provenance and supports a separate compiler argument cwd.
739
- // A separately selected executable host must opt in itself; a linked
740
- // library cannot certify that foreign host. An undeclared capability
741
- // only withholds the optional protocol: build execution still runs such
742
- // a host and observes it from outside, never rejecting it.
743
- capabilities:
744
- record.stage === "transform" &&
745
- record.kind === "linked" &&
746
- fallbackDriverHost !== undefined
747
- ? {
748
- ...record.capabilities,
749
- compilerArgsCwd: true,
750
- emitProvenance: true,
751
- }
752
- : record.capabilities,
753
- config: record.config,
754
- contributors: record.contributors,
755
- kind: record.kind,
756
- name: record.name,
757
- reportsTypeScriptDiagnostics: record.reportsTypeScriptDiagnostics,
758
- source: record.source,
759
- stage: record.stage,
760
- };
761
- });
762
- return {
763
- ...collectHostInputSnapshot(
764
- project,
765
- context,
766
- records,
767
- projectHostInputs,
768
- revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
769
- revalidateHostInputRealpaths(
770
- projectHostInputRealpaths,
827
+ ...collectHostInputSnapshot(
828
+ project,
829
+ context,
830
+ records,
771
831
  projectHostInputs,
832
+ revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
833
+ revalidateHostInputRealpaths(
834
+ projectHostInputRealpaths,
835
+ projectHostInputs,
836
+ ),
772
837
  ),
773
- ),
774
- descriptorReadsDeclared: loadedEntries.every(
775
- (entry) =>
776
- entry.observationsComplete && declaresHostInputReads(entry.plugin),
777
- ),
778
- discoveryInputsComplete: project.configInputsComplete === true,
779
- ...(identities === undefined ? {} : { identities }),
780
- nativePlugins: orderNativePlugins(nativePlugins),
781
- ...(loadedEntries.some((entry) => !entry.observationsComplete)
782
- ? { observationsComplete: false as const }
783
- : {}),
784
- // The directories the watch inputs named before the builds, as the builds
785
- // read them. Package selection shares these readings and also reads a
786
- // linked plugin's module root, which no build keys on, so only the build
787
- // directories are reported.
788
- pluginSources: Object.fromEntries(
789
- [...sourceDigests]
790
- .filter(([directory]) => keyedDirectories.has(directory))
791
- .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
792
- .map(([directory, sourceDigest]) => [
793
- directory,
794
- pluginSourceState(directory, {
795
- env: effectiveEnv,
796
- sourceDigest,
797
- environment:
798
- environmentDigests.get(directory) ??
799
- pluginBuildEnvironment(
800
- directory,
801
- effectiveEnv,
802
- undefined,
803
- sourceIdentities,
804
- ),
805
- }),
806
- ]),
807
- ),
808
- project,
809
- };
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
+ }
810
875
  }
811
876
 
812
877
  type ProjectHostInputSnapshot = {
@@ -945,12 +1010,18 @@ function readProjectHostInputSnapshot(options: {
945
1010
  cwd?: string;
946
1011
  file?: string;
947
1012
  includePluginDiscovery: boolean;
1013
+ pluginConfigDir?: string;
948
1014
  projectRoot?: string;
949
1015
  tsconfig?: string;
950
1016
  }): ProjectHostInputSnapshot {
951
- const { includePluginDiscovery, ...projectOptions } = options;
1017
+ const { includePluginDiscovery, pluginConfigDir, ...projectOptions } =
1018
+ options;
952
1019
  let project = readProjectConfig(projectOptions);
953
- let hostInputs = collectProjectHostInputs(project, includePluginDiscovery);
1020
+ let hostInputs = collectProjectHostInputs(
1021
+ project,
1022
+ includePluginDiscovery,
1023
+ pluginConfigDir,
1024
+ );
954
1025
  const observedInputs = new Set(hostInputs);
955
1026
  for (let attempt = 0; attempt < 3; attempt++) {
956
1027
  const before = hashHostInputPaths(hostInputs);
@@ -960,6 +1031,7 @@ function readProjectHostInputSnapshot(options: {
960
1031
  const candidateInputs = collectProjectHostInputs(
961
1032
  candidateProject,
962
1033
  includePluginDiscovery,
1034
+ pluginConfigDir,
963
1035
  );
964
1036
  for (const input of candidateInputs) observedInputs.add(input);
965
1037
  const after = hashHostInputPaths(candidateInputs);
@@ -1381,7 +1453,7 @@ function loadPluginDescriptor(
1381
1453
  request,
1382
1454
  context,
1383
1455
  effectiveEnv,
1384
- descriptorCache.identities,
1456
+ descriptorCache,
1385
1457
  );
1386
1458
  if (loaded === undefined) {
1387
1459
  throw error;
@@ -1398,6 +1470,9 @@ interface DescriptorCacheOptions {
1398
1470
  /** Record store that proves the evaluating runtimes from metadata (#1723). */
1399
1471
  identities: PluginContentIdentities.Store | undefined;
1400
1472
 
1473
+ /** Source-project cache anchor, independent of descriptor consumer context. */
1474
+ projectRoot: string;
1475
+
1401
1476
  /** This ttsc build's version. */
1402
1477
  version: string;
1403
1478
  }
@@ -1438,15 +1513,19 @@ class CommonJsDescriptorLoadError extends Error {
1438
1513
  function loadCommonJsDescriptor(
1439
1514
  request: string,
1440
1515
  context: ITtscPluginFactoryContext,
1441
- effectiveEnv: NodeJS.ProcessEnv,
1516
+ loadEnv: NodeJS.ProcessEnv,
1442
1517
  descriptorCache: DescriptorCacheOptions,
1443
1518
  ): IsolatedPluginDescriptor {
1519
+ // The evaluator is ttsc's own helper: inside a ttsx run it gets the user's
1520
+ // NODE_OPTIONS, not ttsx's preloads, which would refuse the descriptor cache
1521
+ // and run in every evaluation (#1740). The runtime probe below does the same.
1522
+ const effectiveEnv = TtsxNodeOptions.forHelpers(loadEnv);
1444
1523
  const runtime = pluginDescriptorRuntimeBinary(effectiveEnv);
1445
1524
  const ttsx = effectiveEnv.TTSC_TTSX_BINARY ?? process.env.TTSC_TTSX_BINARY;
1446
1525
  const runtimeCapabilities = javascriptRuntimeCapabilities(
1447
1526
  runtime,
1448
1527
  effectiveEnv,
1449
- context.projectRoot,
1528
+ descriptorCache.projectRoot,
1450
1529
  descriptorCache.identities,
1451
1530
  );
1452
1531
  const node =
@@ -1456,7 +1535,7 @@ function loadCommonJsDescriptor(
1456
1535
  ? runtimeCapabilities.executable
1457
1536
  : resolveNodeBinary(
1458
1537
  effectiveEnv,
1459
- context.projectRoot,
1538
+ descriptorCache.projectRoot,
1460
1539
  descriptorCache.identities,
1461
1540
  );
1462
1541
  const cacheAuthority = {
@@ -1464,6 +1543,8 @@ function loadCommonJsDescriptor(
1464
1543
  cacheDir: descriptorCache.cacheDir,
1465
1544
  identities: descriptorCache.identities,
1466
1545
  context,
1546
+ // Factory context anchors the consumer; the evaluator runs at the source root.
1547
+ cwd: descriptorCache.projectRoot,
1467
1548
  // Everything the child receives besides the per-evaluation output paths.
1468
1549
  env: {
1469
1550
  ...effectiveEnv,
@@ -1471,7 +1552,7 @@ function loadCommonJsDescriptor(
1471
1552
  ...descriptorRecordRoot(descriptorCache.identities),
1472
1553
  TTSC_TTSX_BINARY: ttsx,
1473
1554
  },
1474
- projectRoot: context.projectRoot,
1555
+ projectRoot: descriptorCache.projectRoot,
1475
1556
  request,
1476
1557
  runtime,
1477
1558
  version: descriptorCache.version,
@@ -1552,7 +1633,7 @@ function loadCommonJsDescriptor(
1552
1633
  shim,
1553
1634
  ],
1554
1635
  {
1555
- cwd: context.projectRoot,
1636
+ cwd: descriptorCache.projectRoot,
1556
1637
  env: {
1557
1638
  ...effectiveEnv,
1558
1639
  // The direct evaluator may be Bun, but ttsx and native config
@@ -1943,12 +2024,16 @@ function loadDescriptorViaTtsx(
1943
2024
  request: string,
1944
2025
  context: ITtscPluginFactoryContext,
1945
2026
  effectiveEnv: NodeJS.ProcessEnv,
1946
- identities: PluginContentIdentities.Store | undefined,
2027
+ descriptorCache: DescriptorCacheOptions,
1947
2028
  ): IsolatedPluginDescriptor | undefined {
1948
2029
  // Binary discovery prefers the instance environment, then the ambient
1949
2030
  // process.env (where `withPluginLoaderEnv` injects ttsc's own node/ttsx paths
1950
2031
  // just before this runs), then the running interpreter.
1951
- const node = resolveNodeBinary(effectiveEnv, context.projectRoot, identities);
2032
+ const node = resolveNodeBinary(
2033
+ effectiveEnv,
2034
+ descriptorCache.projectRoot,
2035
+ descriptorCache.identities,
2036
+ );
1952
2037
  const ttsx = effectiveEnv.TTSC_TTSX_BINARY ?? process.env.TTSC_TTSX_BINARY;
1953
2038
  if (node === undefined || ttsx === undefined || ttsx.length === 0) {
1954
2039
  return undefined;
@@ -1975,11 +2060,11 @@ function loadDescriptorViaTtsx(
1975
2060
  const trace = E2ETrace.begin(
1976
2061
  node,
1977
2062
  [ttsx, "--no-plugins", shim],
1978
- { cwd: context.projectRoot },
2063
+ { cwd: descriptorCache.projectRoot },
1979
2064
  "plugin-descriptor",
1980
2065
  );
1981
2066
  const result = spawnOwnedDescriptor(node, [ttsx, "--no-plugins", shim], {
1982
- cwd: context.projectRoot,
2067
+ cwd: descriptorCache.projectRoot,
1983
2068
  encoding: "utf8",
1984
2069
  env: {
1985
2070
  ...effectiveEnv,
@@ -1987,7 +2072,7 @@ function loadDescriptorViaTtsx(
1987
2072
  // may recurse into further descriptor loads) finds them even when the
1988
2073
  // instance-env snapshot predates `withPluginLoaderEnv`.
1989
2074
  TTSC_NODE_BINARY: node,
1990
- ...descriptorRecordRoot(identities),
2075
+ ...descriptorRecordRoot(descriptorCache.identities),
1991
2076
  TTSC_TTSX_BINARY: ttsx,
1992
2077
  TTSC_PLUGIN_CONTEXT: JSON.stringify({
1993
2078
  binary: context.binary,
@@ -2349,6 +2434,7 @@ function pluginBuildDirectories(
2349
2434
  env: NodeJS.ProcessEnv,
2350
2435
  replacements?: Map<string, readonly string[]>,
2351
2436
  readers?: Map<string, SourcePluginWorkspace.GoModReader>,
2437
+ identities?: PluginContentIdentities.Store,
2352
2438
  ): string[] {
2353
2439
  const directories = new Set<string>();
2354
2440
  for (const record of records) {
@@ -2363,6 +2449,7 @@ function pluginBuildDirectories(
2363
2449
  env,
2364
2450
  replacements,
2365
2451
  readers,
2452
+ identities,
2366
2453
  ))
2367
2454
  directories.add(directory);
2368
2455
  }
@@ -2382,17 +2469,20 @@ function pluginReplacementWatchDirectories(
2382
2469
  env: NodeJS.ProcessEnv,
2383
2470
  observed?: Map<string, readonly string[]>,
2384
2471
  readers?: Map<string, SourcePluginWorkspace.GoModReader>,
2472
+ identities?: PluginContentIdentities.Store,
2385
2473
  ): readonly string[] {
2386
2474
  const previous = observed?.get(moduleRoot);
2387
2475
  if (previous !== undefined) return previous;
2388
2476
  let directories: readonly string[];
2389
2477
  try {
2478
+ // The load's store goes with the reader this registers first (#1739).
2390
2479
  directories = pluginModuleReplaceDirectories(
2391
2480
  moduleRoot,
2392
2481
  env,
2393
2482
  undefined,
2394
2483
  undefined,
2395
2484
  readers,
2485
+ identities,
2396
2486
  ).map((replacement) => replacement.directory);
2397
2487
  } catch {
2398
2488
  // An unreadable manifest/tool is not valid metadata. The already observed