ttsc 0.26.2 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +9 -0
  2. package/cmd/graphbench/main.go +26 -16
  3. package/cmd/graphdump/main.go +37 -12
  4. package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
  5. package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
  6. package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
  7. package/cmd/ttsc/api_transform.go +28 -3
  8. package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
  9. package/cmd/ttscgraph/disk_realpath_other.go +16 -0
  10. package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
  11. package/cmd/ttscgraph/dump.go +43 -11
  12. package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
  13. package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
  14. package/cmd/ttscgraph/serve.go +357 -60
  15. package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
  16. package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
  17. package/cmd/ttscgraph/serve_shards.go +31 -9
  18. package/driver/config_discovery.go +121 -0
  19. package/driver/emit_plugin.go +77 -11
  20. package/driver/graph.go +64 -11
  21. package/driver/input_observation.go +273 -0
  22. package/driver/input_observation_test.go +149 -0
  23. package/driver/plugins.go +506 -15
  24. package/driver/program.go +54 -1
  25. package/driver/resolution_candidates.go +13 -5
  26. package/driver/rewrite.go +45 -2
  27. package/driver/transform_dependencies.go +283 -0
  28. package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
  29. package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
  30. package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
  31. package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
  32. package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
  33. package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
  34. package/internal/graph/artifacts.go +230 -0
  35. package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
  36. package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
  37. package/internal/graph/build.go +35 -0
  38. package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
  39. package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
  40. package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
  41. package/internal/graph/doc_tags.go +416 -0
  42. package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
  43. package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
  44. package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
  45. package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
  46. package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
  47. package/internal/graph/dump.go +60 -13
  48. package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
  49. package/internal/graph/dump_paths.go +2 -2
  50. package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
  51. package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
  52. package/internal/graph/edges.go +1 -0
  53. package/internal/graph/graph.go +79 -4
  54. package/internal/graph/provenance.go +29 -2
  55. package/lib/TtscCompiler.d.ts +12 -11
  56. package/lib/TtscCompiler.js +44 -29
  57. package/lib/TtscCompiler.js.map +1 -1
  58. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -3
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
  63. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
  64. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
  65. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  66. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  67. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  68. package/lib/compiler/internal/runBuild.d.ts +1 -1
  69. package/lib/compiler/internal/runBuild.js +80 -34
  70. package/lib/compiler/internal/runBuild.js.map +1 -1
  71. package/lib/compiler/internal/runSingleFileEmit.js +2 -2
  72. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  73. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  74. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  75. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  76. package/lib/compiler/internal/startResidentTransform.js +12 -4
  77. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  78. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  79. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  80. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  81. package/lib/flags/schema.js +12 -5
  82. package/lib/flags/schema.js.map +1 -1
  83. package/lib/index.d.ts +1 -0
  84. package/lib/index.js +1 -0
  85. package/lib/index.js.map +1 -1
  86. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  87. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  88. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  89. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  90. package/lib/internal/projectInputPathIdentity.js +11 -5
  91. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  92. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  93. package/lib/internal/resolveNodeBinary.js +150 -0
  94. package/lib/internal/resolveNodeBinary.js.map +1 -0
  95. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  96. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  97. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  98. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  99. package/lib/launcher/internal/prepareExecution.js +42 -14
  100. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  101. package/lib/launcher/internal/runTtsc.js +25 -13
  102. package/lib/launcher/internal/runTtsc.js.map +1 -1
  103. package/lib/launcher/internal/runTtscserver.js +18 -3
  104. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  105. package/lib/launcher/internal/runTtsx.js +2 -0
  106. package/lib/launcher/internal/runTtsx.js.map +1 -1
  107. package/lib/launcher/internal/runtimeHooks.d.ts +47 -3
  108. package/lib/launcher/internal/runtimeHooks.js +746 -64
  109. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  110. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  111. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  112. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  113. package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
  114. package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
  115. package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
  116. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  117. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  118. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  119. package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
  120. package/lib/plugin/resolveCapabilityPlugins.js +118 -0
  121. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
  122. package/lib/register.d.ts +1 -0
  123. package/lib/register.js +46 -0
  124. package/lib/register.js.map +1 -0
  125. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  126. package/lib/structures/ITtscCompilerTransformation.d.ts +38 -0
  127. package/lib/structures/ITtscPlugin.d.ts +33 -0
  128. package/package.json +16 -9
  129. package/shim/ast/parent.go +16 -3
  130. package/shim/checker/enums_gen.go +11 -0
  131. package/shim/checker/go.mod +9 -0
  132. package/shim/checker/go.sum +22 -0
  133. package/shim/checker/shim.go +78 -0
  134. package/shim/compiler/emithook.go +10 -0
  135. package/shim/compiler/incremental.go +45 -8
  136. package/src/TtscCompiler.ts +55 -33
  137. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  138. package/src/compiler/internal/compileProjectInMemory.ts +4 -3
  139. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
  140. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  141. package/src/compiler/internal/runBuild.ts +102 -39
  142. package/src/compiler/internal/runSingleFileEmit.ts +2 -2
  143. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  144. package/src/compiler/internal/startResidentTransform.ts +12 -3
  145. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  146. package/src/flags/schema.ts +12 -5
  147. package/src/index.ts +1 -0
  148. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  149. package/src/internal/projectInputPathIdentity.ts +17 -4
  150. package/src/internal/resolveNodeBinary.ts +173 -0
  151. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  152. package/src/launcher/internal/prepareExecution.ts +50 -14
  153. package/src/launcher/internal/runTtsc.ts +24 -13
  154. package/src/launcher/internal/runTtscserver.ts +18 -3
  155. package/src/launcher/internal/runTtsx.ts +2 -0
  156. package/src/launcher/internal/runtimeHooks.ts +934 -68
  157. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  158. package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
  159. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  160. package/src/plugin/resolveCapabilityPlugins.ts +171 -0
  161. package/src/register.ts +47 -0
  162. package/src/structures/ITtscCompilerContext.ts +6 -3
  163. package/src/structures/ITtscCompilerTransformation.ts +46 -0
  164. package/src/structures/ITtscPlugin.ts +36 -0
  165. package/tools/shim_audit/baseline.json +0 -2
  166. package/utility/host.go +18 -3
  167. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  168. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  169. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  170. package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
@@ -10,7 +10,9 @@ exports.entryModuleFormat = entryModuleFormat;
10
10
  exports.restoreStrippedNodeBuiltinScheme = restoreStrippedNodeBuiltinScheme;
11
11
  exports.projectModuleOptions = projectModuleOptions;
12
12
  exports.isWithin = isWithin;
13
+ exports.dependencyCacheKey = dependencyCacheKey;
13
14
  exports.readDependencyCache = readDependencyCache;
15
+ exports.dependencyCacheRoot = dependencyCacheRoot;
14
16
  exports.acquireDependencyBuildLock = acquireDependencyBuildLock;
15
17
  exports.releaseDependencyBuildLock = releaseDependencyBuildLock;
16
18
  exports.reclaimDependencyBuildLock = reclaimDependencyBuildLock;
@@ -27,6 +29,7 @@ const resolveEmittedJavaScript_1 = require("../../compiler/internal/resolveEmitt
27
29
  const resolveTsgo_1 = require("../../compiler/internal/resolveTsgo");
28
30
  const runBuild_1 = require("../../compiler/internal/runBuild");
29
31
  const spawnNative_1 = require("../../compiler/internal/spawnNative");
32
+ const createCanonicalTempDirectory_1 = require("../../internal/createCanonicalTempDirectory");
30
33
  const projectInputPathIdentity_1 = require("../../internal/projectInputPathIdentity");
31
34
  const servedSourceMap_1 = require("./servedSourceMap");
32
35
  /**
@@ -67,25 +70,39 @@ const RESOLVABLE_EXTENSIONS = [
67
70
  ".mjs",
68
71
  ".cjs",
69
72
  ];
73
+ /** Union of the ttsx rescue probes and Node's built-in CommonJS probes. */
74
+ const DESCRIPTOR_PROBE_EXTENSIONS = [
75
+ ...RESOLVABLE_EXTENSIONS,
76
+ ".json",
77
+ ".node",
78
+ ];
70
79
  /** TypeScript source extensions these hooks compile. */
71
80
  const TYPESCRIPT_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
72
- let manifestCache;
73
- function manifest() {
74
- if (manifestCache !== undefined) {
75
- return manifestCache;
81
+ let environmentManifestCache;
82
+ const registeredManifests = [];
83
+ function environmentManifest() {
84
+ if (environmentManifestCache !== undefined) {
85
+ return environmentManifestCache;
76
86
  }
77
87
  const file = process.env.TTSX_RUNTIME_MANIFEST;
78
88
  if (file === undefined || file.length === 0) {
79
- manifestCache = null;
80
- return manifestCache;
89
+ environmentManifestCache = null;
90
+ return environmentManifestCache;
81
91
  }
82
92
  try {
83
- manifestCache = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
93
+ environmentManifestCache = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
84
94
  }
85
95
  catch {
86
- manifestCache = null;
96
+ environmentManifestCache = null;
87
97
  }
88
- return manifestCache;
98
+ return environmentManifestCache;
99
+ }
100
+ /** Every checked entry emit available to this runtime, in ownership order. */
101
+ function runtimeManifests() {
102
+ const inherited = environmentManifest();
103
+ return inherited === null
104
+ ? registeredManifests
105
+ : [inherited, ...registeredManifests];
89
106
  }
90
107
  /**
91
108
  * Lowest Node.js the ttsx source runtime supports. The synchronous
@@ -154,6 +171,7 @@ function assertNodeRuntimeSupport() {
154
171
  }
155
172
  }
156
173
  let installed = false;
174
+ let prepareRuntimeEntry;
157
175
  /**
158
176
  * Install the source-loading hooks on the current (main) thread. Idempotent:
159
177
  * the bootstrap installs them for the entry process, and `NODE_OPTIONS`
@@ -164,10 +182,21 @@ let installed = false;
164
182
  * `require()` made from inside a CommonJS module that was itself reached
165
183
  * through an ESM `import` (the interop translator loads it on the raw CJS
166
184
  * path). The ESM graph goes through `registerHooks`; the CommonJS `require`
167
- * graph goes through `Module._extensions` — the canonical loader extension
168
- * point `ts-node`/`tsx` use for the same reason.
185
+ * graph goes through `Module._extensions` and `Module._resolveFilename` — the
186
+ * canonical loader extension points `ts-node`/`tsx` use for the same reason.
187
+ *
188
+ * Both halves of that second graph are required, and they answer different
189
+ * questions. `_extensions` decides how a file that was _found_ is compiled, and
190
+ * Node reuses its keys when it probes an extensionless request, so
191
+ * `require("./x")` reaches `x.ts` for free. Nothing there resolves a request
192
+ * that already carries an extension: `require("./x.js")` backed only by `x.ts`
193
+ * is a resolution failure before any compiler is consulted, which is why the
194
+ * rescue is installed on `_resolveFilename` as well (samchon/ttsc#1280).
169
195
  */
170
- function installRuntimeHooks() {
196
+ function installRuntimeHooks(options = {}) {
197
+ if (options.prepareEntry !== undefined) {
198
+ prepareRuntimeEntry = options.prepareEntry;
199
+ }
171
200
  if (installed) {
172
201
  return;
173
202
  }
@@ -182,6 +211,7 @@ function installRuntimeHooks() {
182
211
  }
183
212
  (0, node_module_1.registerHooks)({ load, resolve });
184
213
  installCommonJsHook();
214
+ installCommonJsResolveHook();
185
215
  }
186
216
  /**
187
217
  * Register a CommonJS `require` handler for each TypeScript source extension so
@@ -191,12 +221,95 @@ function installRuntimeHooks() {
191
221
  function installCommonJsHook() {
192
222
  const extensions = node_module_1.Module._extensions;
193
223
  const compile = (module, filename) => {
194
- module._compile(resolveServedSource(filename).source, filename);
224
+ const parent = module.parent?.filename;
225
+ module._compile(resolveServedSource(filename, (0, node_url_1.pathToFileURL)(filename).href, typeof parent !== "string" || !isTypeScriptSource(parent)).source, filename);
195
226
  };
196
227
  for (const extension of [".ts", ".tsx", ".cts"]) {
197
228
  extensions[extension] = compile;
198
229
  }
199
230
  }
231
+ /**
232
+ * Map a JavaScript spelling back to its TypeScript source on the CommonJS
233
+ * resolution path, so `require("./x.js")` finds `x.ts` when nothing emitted
234
+ * `x.js`.
235
+ *
236
+ * TypeScript asks authors to write the emitted `.js` extension in a relative
237
+ * specifier, so this spelling is the documented one rather than a mistake, and
238
+ * the ESM `resolve` hook has always rescued it. The CommonJS graph could not,
239
+ * and on Node 22 that graph is reached by every `require()` inside a CommonJS
240
+ * module the ESM loader evaluated — `module.registerHooks` sees the `import()`
241
+ * of that module and nothing within it. Node 24 routes the same `require`
242
+ * through the hooks, which is why the gap was only ever visible on the oldest
243
+ * runtime ttsx supports (`engines.node` is `>=22.15.0`).
244
+ *
245
+ * The wrapper only ever answers a request Node's own resolver already refused,
246
+ * so a resolution that succeeds is never perturbed, and a request no candidate
247
+ * satisfies rethrows Node's original error with its `MODULE_NOT_FOUND` code and
248
+ * `requireStack` intact. `require.resolve` shares this entry point and is
249
+ * rescued with it, except in its `{ paths }` form, which resolves against the
250
+ * caller's list rather than the parent this reads.
251
+ */
252
+ function installCommonJsResolveHook() {
253
+ const internals = node_module_1.Module;
254
+ const original = internals._resolveFilename;
255
+ internals._resolveFilename = function resolveFilename(request, parent, isMain, options) {
256
+ try {
257
+ return original.call(this, request, parent, isMain, options);
258
+ }
259
+ catch (error) {
260
+ const rescued = rescueCommonJsRequest(request, parent, options);
261
+ if (rescued === null) {
262
+ throw error;
263
+ }
264
+ return rescued;
265
+ }
266
+ };
267
+ }
268
+ /**
269
+ * The TypeScript source a refused CommonJS request names, or `null`.
270
+ *
271
+ * Only a relative or absolute request can be rescued: a bare specifier is a
272
+ * package lookup, whose own resolver already probed every extension the
273
+ * installed `_extensions` keys declare.
274
+ *
275
+ * Two shapes decline before that. `_resolveFilename` is an internal entry point
276
+ * anything may call, so a non-string request reaches here as readily as a
277
+ * specifier does, and reading it would replace Node's own argument error with a
278
+ * `TypeError` from this file. And `require.resolve(request, { paths })`
279
+ * resolves against the caller's list, which this rescue does not consult, so
280
+ * answering it from the parent's directory would be a different question's
281
+ * answer.
282
+ */
283
+ function rescueCommonJsRequest(request, parent, options) {
284
+ if (typeof request !== "string") {
285
+ return null;
286
+ }
287
+ if (typeof options === "object" &&
288
+ options !== null &&
289
+ Array.isArray(options.paths)) {
290
+ return null;
291
+ }
292
+ let base;
293
+ if (node_path_1.default.isAbsolute(request)) {
294
+ base = request;
295
+ }
296
+ else if (isRelativeSpecifier(request)) {
297
+ const from = parent?.filename;
298
+ if (typeof from !== "string") {
299
+ return null;
300
+ }
301
+ base = node_path_1.default.resolve(node_path_1.default.dirname(from), request);
302
+ }
303
+ else {
304
+ return null;
305
+ }
306
+ for (const candidate of typescriptSourcesForJavaScriptSpecifier(base)) {
307
+ if (isFile(candidate)) {
308
+ return candidate;
309
+ }
310
+ }
311
+ return null;
312
+ }
200
313
  /**
201
314
  * The module format of the entry source file, derived from the entry project's
202
315
  * compiler options (via the runtime manifest) the same way the served files are
@@ -204,11 +317,14 @@ function installCommonJsHook() {
204
317
  * `require` or an ESM `import`.
205
318
  */
206
319
  function entryModuleFormat(entryFile) {
207
- const m = manifest();
208
- return moduleFormat(entryFile, m === null ? null : (m.moduleOptions ?? {})) === "module"
320
+ const real = realPath(entryFile);
321
+ const owner = findEntryEmit(real)?.manifest;
322
+ return moduleFormat(entryFile, owner === undefined ? null : (owner.moduleOptions ?? {})) === "module"
209
323
  ? "module"
210
324
  : "commonjs";
211
325
  }
326
+ /** TypeScript URLs resolved at a JavaScript-to-TypeScript entry boundary. */
327
+ const runtimeEntryUrls = new Set();
212
328
  /**
213
329
  * Rescue an extensionless or directory relative specifier that Node's resolver
214
330
  * rejected. Only runs after `nextResolve` throws, so a successful resolution is
@@ -216,17 +332,344 @@ function entryModuleFormat(entryFile) {
216
332
  * original error is rethrown, preserving `ERR_MODULE_NOT_FOUND`.
217
333
  */
218
334
  function resolve(specifier, context, nextResolve) {
335
+ recordPluginDescriptorResolutionCandidates(specifier, context.parentURL);
219
336
  try {
220
- return rememberCommonJsNamedInterop(restoreStrippedNodeBuiltinScheme(specifier, nextResolve(specifier, context)), context);
337
+ const result = rememberRuntimeEntry(rememberCommonJsNamedInterop(restoreStrippedNodeBuiltinScheme(specifier, nextResolve(specifier, context)), context), context);
338
+ recordPluginDescriptorResolution(specifier, context.parentURL, result.url);
339
+ return result;
221
340
  }
222
341
  catch (error) {
223
342
  const rescued = probeRescuableSpecifier(specifier, context.parentURL);
224
343
  if (rescued === null) {
225
344
  throw error;
226
345
  }
227
- return rememberCommonJsNamedInterop({ shortCircuit: true, url: rescued }, context);
346
+ const result = rememberRuntimeEntry(rememberCommonJsNamedInterop({ shortCircuit: true, url: rescued }, context), context);
347
+ recordPluginDescriptorResolution(specifier, context.parentURL, result.url);
348
+ return result;
228
349
  }
229
350
  }
351
+ /**
352
+ * Fingerprint every path whose state can redirect one descriptor import.
353
+ *
354
+ * This runs before the real resolver. Reporting candidates only after the
355
+ * descriptor finished would pair an earlier descriptor result with later file
356
+ * state when a higher-priority candidate appeared during evaluation.
357
+ */
358
+ function recordPluginDescriptorResolutionCandidates(specifier, parentURL) {
359
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
360
+ return;
361
+ if ((0, node_module_1.isBuiltin)(specifier) || specifier.startsWith("node:"))
362
+ return;
363
+ const parent = runtimeFilePath(parentURL);
364
+ if (parent === undefined)
365
+ return;
366
+ const recorded = new Set();
367
+ const record = (candidate) => {
368
+ const resolved = node_path_1.default.resolve(candidate);
369
+ if (recorded.has(resolved))
370
+ return;
371
+ recorded.add(resolved);
372
+ recordPluginDescriptorInput({
373
+ parent,
374
+ resolved,
375
+ });
376
+ };
377
+ const recordManifestTargets = (value, directory, allowBare = false) => {
378
+ if (typeof value === "string") {
379
+ if (value !== "" &&
380
+ (allowBare || value.startsWith("./") || value.startsWith("../"))) {
381
+ recordBase(node_path_1.default.resolve(directory, value));
382
+ }
383
+ return;
384
+ }
385
+ if (Array.isArray(value)) {
386
+ for (const item of value) {
387
+ recordManifestTargets(item, directory, allowBare);
388
+ }
389
+ return;
390
+ }
391
+ if (value !== null && typeof value === "object") {
392
+ for (const item of Object.values(value)) {
393
+ recordManifestTargets(item, directory, allowBare);
394
+ }
395
+ }
396
+ };
397
+ const recordPackageManifests = (file) => {
398
+ for (let directory = node_path_1.default.dirname(file);; directory = node_path_1.default.dirname(directory)) {
399
+ const manifest = node_path_1.default.join(directory, "package.json");
400
+ record(manifest);
401
+ if (isFile(manifest))
402
+ return;
403
+ const parentDirectory = node_path_1.default.dirname(directory);
404
+ if (parentDirectory === directory)
405
+ return;
406
+ }
407
+ };
408
+ const localBases = (value) => {
409
+ if (value.startsWith("file:"))
410
+ return [(0, node_url_1.fileURLToPath)(value)];
411
+ const raw = node_path_1.default.resolve(node_path_1.default.dirname(parent), value);
412
+ const suffixStart = value.search(/[?#]/);
413
+ if (suffixStart === -1)
414
+ return [raw];
415
+ const pathname = value.slice(0, suffixStart);
416
+ return pathname === ""
417
+ ? [raw]
418
+ : [...new Set([raw, node_path_1.default.resolve(node_path_1.default.dirname(parent), pathname)])];
419
+ };
420
+ const bases = new Set();
421
+ const recordBase = (base) => {
422
+ const resolvedBase = node_path_1.default.resolve(base);
423
+ if (bases.has(resolvedBase))
424
+ return;
425
+ bases.add(resolvedBase);
426
+ record(resolvedBase);
427
+ for (const candidate of typescriptSourcesForJavaScriptSpecifier(resolvedBase)) {
428
+ record(candidate);
429
+ }
430
+ for (const extension of DESCRIPTOR_PROBE_EXTENSIONS) {
431
+ record(resolvedBase + extension);
432
+ }
433
+ const manifestFile = node_path_1.default.join(resolvedBase, "package.json");
434
+ record(manifestFile);
435
+ for (const extension of DESCRIPTOR_PROBE_EXTENSIONS) {
436
+ record(node_path_1.default.join(resolvedBase, `index${extension}`));
437
+ }
438
+ try {
439
+ const manifest = JSON.parse(node_fs_1.default.readFileSync(manifestFile, "utf8").replace(/^\uFEFF/, ""));
440
+ recordManifestTargets(manifest.exports, resolvedBase);
441
+ recordManifestTargets(manifest.module, resolvedBase, true);
442
+ recordManifestTargets(manifest.main, resolvedBase, true);
443
+ }
444
+ catch {
445
+ // The real resolver owns malformed package diagnostics.
446
+ }
447
+ };
448
+ if (specifier.startsWith(".") ||
449
+ node_path_1.default.isAbsolute(specifier) ||
450
+ specifier.startsWith("file:")) {
451
+ try {
452
+ for (const base of localBases(specifier)) {
453
+ recordPackageManifests(base);
454
+ if (isFile(base))
455
+ record(base);
456
+ else
457
+ recordBase(base);
458
+ }
459
+ }
460
+ catch {
461
+ // The real resolver owns invalid URL spellings.
462
+ }
463
+ return;
464
+ }
465
+ const parts = specifier.split("/");
466
+ const packageParts = parts[0]?.startsWith("@")
467
+ ? parts.slice(0, 2)
468
+ : parts.slice(0, 1);
469
+ if (packageParts.some((part) => part === undefined || part === ""))
470
+ return;
471
+ const packageName = packageParts.join("/");
472
+ const subpath = parts.slice(packageParts.length);
473
+ for (const searchPath of (0, node_module_1.createRequire)(parent).resolve.paths(specifier) ??
474
+ []) {
475
+ const packageDirectory = node_path_1.default.join(searchPath, packageName);
476
+ recordBase(packageDirectory);
477
+ if (subpath.length !== 0) {
478
+ recordBase(node_path_1.default.join(packageDirectory, ...subpath));
479
+ }
480
+ // CommonJS resolution continues past an existing but unusable package
481
+ // directory. Record every search root before the resolver runs so a
482
+ // farther selected package retains evaluation-time hashes for its
483
+ // superseding candidates.
484
+ }
485
+ }
486
+ /**
487
+ * Report one resolved descriptor edge to the parent loader. The channel is
488
+ * armed by the generated descriptor shim only after ttsx's own bootstrap and
489
+ * imports have loaded, keeping compiler implementation files out of the
490
+ * project's persistent-cache inputs.
491
+ */
492
+ function recordPluginDescriptorResolution(specifier, parentURL, resolvedURL) {
493
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
494
+ return;
495
+ const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
496
+ if (out === undefined || out.length === 0)
497
+ return;
498
+ const resolved = runtimeFilePath(resolvedURL);
499
+ if (resolved === undefined)
500
+ return;
501
+ const parent = runtimeFilePath(parentURL);
502
+ recordPluginDescriptorInput({
503
+ ...(parent === undefined ? {} : { parent }),
504
+ resolved,
505
+ specifier,
506
+ });
507
+ for (let directory = node_path_1.default.dirname(resolved);; directory = node_path_1.default.dirname(directory)) {
508
+ const manifest = node_path_1.default.join(directory, "package.json");
509
+ recordPluginDescriptorInput({
510
+ ...(parent === undefined ? {} : { parent }),
511
+ resolved: manifest,
512
+ });
513
+ if (isFile(manifest))
514
+ break;
515
+ const parentDirectory = node_path_1.default.dirname(directory);
516
+ if (parentDirectory === directory)
517
+ break;
518
+ }
519
+ }
520
+ function recordPluginDescriptorInput(record) {
521
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
522
+ return;
523
+ const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
524
+ if (out === undefined || out.length === 0)
525
+ return;
526
+ recordPluginDescriptorInputOnce(record);
527
+ const resolved = node_path_1.default.resolve(record.resolved);
528
+ const parsed = node_path_1.default.parse(resolved);
529
+ let current = parsed.root;
530
+ const relative = node_path_1.default.relative(parsed.root, resolved);
531
+ for (const segment of relative.split(node_path_1.default.sep).slice(0, -1)) {
532
+ if (segment === "")
533
+ continue;
534
+ current = node_path_1.default.join(current, segment);
535
+ try {
536
+ if (node_fs_1.default.lstatSync(current).isSymbolicLink()) {
537
+ recordPluginDescriptorInputOnce({ resolved: current });
538
+ }
539
+ }
540
+ catch {
541
+ break;
542
+ }
543
+ }
544
+ }
545
+ /** Record one path without recursively revisiting its lexical ancestors. */
546
+ function recordPluginDescriptorInputOnce(record) {
547
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
548
+ return;
549
+ const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
550
+ if (out === undefined || out.length === 0)
551
+ return;
552
+ try {
553
+ const beforeSignature = pluginDescriptorInputMetadataSignature(record.resolved);
554
+ const observedHash = pluginDescriptorInputHash(record.resolved);
555
+ const observedRealpath = pluginDescriptorInputRealpath(record.resolved);
556
+ const afterSignature = pluginDescriptorInputMetadataSignature(record.resolved);
557
+ const unstable = record.unstable === true ||
558
+ beforeSignature === undefined ||
559
+ afterSignature === undefined ||
560
+ beforeSignature !== afterSignature ||
561
+ (record.hash !== undefined && record.hash !== observedHash) ||
562
+ (record.realpath !== undefined && record.realpath !== observedRealpath) ||
563
+ (record.signature !== undefined && record.signature !== afterSignature);
564
+ node_fs_1.default.appendFileSync(out, `${JSON.stringify({
565
+ ...record,
566
+ hash: observedHash,
567
+ realpath: observedRealpath,
568
+ ...(unstable ? { unstable: true } : { signature: afterSignature }),
569
+ })}\n`, "utf8");
570
+ }
571
+ catch {
572
+ // Dependency reporting is advisory to cache reuse; the selected entry is
573
+ // still retained by the parent if this best-effort side channel fails.
574
+ }
575
+ }
576
+ function pluginDescriptorInputRealpath(file) {
577
+ try {
578
+ return node_fs_1.default.realpathSync.native(file);
579
+ }
580
+ catch {
581
+ return null;
582
+ }
583
+ }
584
+ /** Metadata identity that exposes content-preserving A-B-A replacement. */
585
+ function pluginDescriptorInputMetadataSignature(file) {
586
+ const requested = node_path_1.default.resolve(file);
587
+ let current = requested;
588
+ for (;;) {
589
+ try {
590
+ const link = node_fs_1.default.lstatSync(current, { bigint: true });
591
+ let target = link;
592
+ if (link.isSymbolicLink()) {
593
+ try {
594
+ target = node_fs_1.default.statSync(current, { bigint: true });
595
+ }
596
+ catch {
597
+ // A broken link's own metadata cannot expose its target appearing
598
+ // and disappearing during evaluation. Decline cache proof instead.
599
+ return undefined;
600
+ }
601
+ }
602
+ return [
603
+ node_path_1.default.relative(current, requested),
604
+ link.dev,
605
+ link.ino,
606
+ link.mode,
607
+ link.size,
608
+ link.mtimeNs,
609
+ link.ctimeNs,
610
+ target.dev,
611
+ target.ino,
612
+ target.mode,
613
+ target.size,
614
+ target.mtimeNs,
615
+ target.ctimeNs,
616
+ ].join(":");
617
+ }
618
+ catch (error) {
619
+ if (!isMissingPathError(error))
620
+ return undefined;
621
+ const parent = node_path_1.default.dirname(current);
622
+ if (parent === current)
623
+ return undefined;
624
+ current = parent;
625
+ }
626
+ }
627
+ }
628
+ function pluginDescriptorInputHash(file) {
629
+ try {
630
+ if (node_fs_1.default.statSync(file).isDirectory()) {
631
+ return pluginDescriptorDirectoryHash();
632
+ }
633
+ return node_crypto_1.default
634
+ .createHash("sha256")
635
+ .update(node_fs_1.default.readFileSync(file))
636
+ .digest("hex");
637
+ }
638
+ catch {
639
+ return null;
640
+ }
641
+ }
642
+ /** Stable public fingerprint for an existing directory candidate's kind. */
643
+ function pluginDescriptorDirectoryHash() {
644
+ return node_crypto_1.default
645
+ .createHash("sha256")
646
+ .update("ttsc:host-input:directory\0")
647
+ .digest("hex");
648
+ }
649
+ function runtimeFilePath(value) {
650
+ if (value === undefined)
651
+ return undefined;
652
+ if (value.startsWith("file:")) {
653
+ try {
654
+ return node_path_1.default.resolve((0, node_url_1.fileURLToPath)(value));
655
+ }
656
+ catch {
657
+ return undefined;
658
+ }
659
+ }
660
+ return node_path_1.default.isAbsolute(value) ? node_path_1.default.resolve(value) : undefined;
661
+ }
662
+ /** Remember an ESM root until its synchronous load hook prepares the project. */
663
+ function rememberRuntimeEntry(result, context) {
664
+ if (result.url.startsWith("file:") &&
665
+ isTypeScriptSource((0, node_url_1.fileURLToPath)(result.url)) &&
666
+ (context.parentURL === undefined ||
667
+ !context.parentURL.startsWith("file:") ||
668
+ !isTypeScriptSource((0, node_url_1.fileURLToPath)(context.parentURL)))) {
669
+ runtimeEntryUrls.add(result.url);
670
+ }
671
+ return result;
672
+ }
230
673
  /**
231
674
  * Restore a `node:` builtin URL when affected Node releases return the exact
232
675
  * prefix-stripped spelling from their synchronous CommonJS resolver.
@@ -244,6 +687,12 @@ function restoreStrippedNodeBuiltinScheme(specifier, result) {
244
687
  : result;
245
688
  }
246
689
  const builtProjects = new Map();
690
+ // A descriptor evaluator's dependency emit must never be reused by another
691
+ // process. PIDs are eventually recycled while the disk cache persists, so PID
692
+ // alone cannot provide that isolation. One cryptographically random process
693
+ // nonce keeps every evaluator generation distinct while `builtProjects` still
694
+ // shares repeated imports inside this process.
695
+ const descriptorProcessCacheNonce = node_crypto_1.default.randomBytes(16).toString("hex");
247
696
  /** File URLs whose CommonJS source was reached from an ESM parent import. */
248
697
  const commonJsNamedInteropUrls = new Set();
249
698
  const commonJsNameScanSources = new Map();
@@ -255,15 +704,15 @@ function load(url, context, nextLoad) {
255
704
  if (!isTypeScriptSource(filename)) {
256
705
  return nextLoad(url, context);
257
706
  }
258
- const { format, source } = resolveRuntimeSource(filename, url);
707
+ const { format, source } = resolveRuntimeSource(filename, url, runtimeEntryUrls.delete(url) || isProcessEntry(filename));
259
708
  return {
260
709
  format,
261
710
  shortCircuit: true,
262
711
  source,
263
712
  };
264
713
  }
265
- function resolveRuntimeSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href) {
266
- const served = resolveServedSource(filename, url);
714
+ function resolveRuntimeSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href, prepareAsEntry = false) {
715
+ const served = resolveServedSource(filename, url, prepareAsEntry);
267
716
  const format = moduleFormat(filename, served.moduleOptions);
268
717
  return {
269
718
  format,
@@ -272,6 +721,13 @@ function resolveRuntimeSource(filename, url = (0, node_url_1.pathToFileURL)(file
272
721
  : served.source,
273
722
  };
274
723
  }
724
+ /** Whether `filename` is the TypeScript main module named on Node's argv. */
725
+ function isProcessEntry(filename) {
726
+ const entry = process.argv[1];
727
+ return (entry !== undefined &&
728
+ isTypeScriptSource(entry) &&
729
+ realPath(node_path_1.default.resolve(entry)) === realPath(filename));
730
+ }
275
731
  function rememberCommonJsNamedInterop(result, context) {
276
732
  if (shouldExposeCommonJsNamedExports(result.url, context.parentURL)) {
277
733
  commonJsNamedInteropUrls.add(result.url);
@@ -316,9 +772,9 @@ function owningModuleOptions(filename) {
316
772
  return null;
317
773
  }
318
774
  const real = realPath(filename);
319
- const m = manifest();
320
- if (m !== null && entryEmitPath(m, real) !== null) {
321
- return m.moduleOptions ?? {};
775
+ const owner = findEntryEmit(real)?.manifest;
776
+ if (owner !== undefined) {
777
+ return owner.moduleOptions ?? {};
322
778
  }
323
779
  const tsconfig = nearestTsconfig(real);
324
780
  if (tsconfig === null) {
@@ -330,10 +786,7 @@ function owningModuleOptions(filename) {
330
786
  }
331
787
  let options = {};
332
788
  try {
333
- const project = (0, readProjectConfig_1.readProjectConfig)({
334
- cwd: node_path_1.default.dirname(tsconfig),
335
- tsconfig,
336
- });
789
+ const project = readPluginDescriptorProjectConfig(tsconfig);
337
790
  options = projectModuleOptions(project.compilerOptions);
338
791
  }
339
792
  catch {
@@ -347,6 +800,108 @@ function owningModuleOptions(filename) {
347
800
  moduleOptionsCache.set(tsconfig, options);
348
801
  return options;
349
802
  }
803
+ /**
804
+ * Read one owning config while its discovered chain stays unchanged.
805
+ *
806
+ * A preliminary read discovers `extends`; an accepted read is bracketed by
807
+ * equal fingerprints over that exact chain. If churn never settles, ttsx can
808
+ * still execute the last project but reports every observed config as unstable,
809
+ * preventing a later snapshot from certifying the torn result.
810
+ */
811
+ function readPluginDescriptorProjectConfig(tsconfig) {
812
+ const read = () => (0, readProjectConfig_1.readProjectConfig)({ cwd: node_path_1.default.dirname(tsconfig), tsconfig });
813
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
814
+ return read();
815
+ const observed = new Set([node_path_1.default.resolve(tsconfig)]);
816
+ let project;
817
+ try {
818
+ project = read();
819
+ }
820
+ catch (error) {
821
+ recordPluginDescriptorProjectInputs(observed, true);
822
+ throw error;
823
+ }
824
+ let inputs = normalizedProjectConfigPaths(project, tsconfig);
825
+ for (let attempt = 0; attempt < 3; attempt += 1) {
826
+ for (const input of inputs)
827
+ observed.add(input);
828
+ const before = pluginDescriptorInputHashes(inputs);
829
+ const beforeRealpaths = pluginDescriptorInputRealpaths(inputs);
830
+ const beforeSignatures = pluginDescriptorInputMetadataSignatures(inputs);
831
+ let candidate;
832
+ try {
833
+ candidate = read();
834
+ }
835
+ catch (error) {
836
+ recordPluginDescriptorProjectInputs(observed, true);
837
+ throw error;
838
+ }
839
+ const candidateInputs = normalizedProjectConfigPaths(candidate, tsconfig);
840
+ for (const input of candidateInputs)
841
+ observed.add(input);
842
+ const after = pluginDescriptorInputHashes(candidateInputs);
843
+ const afterRealpaths = pluginDescriptorInputRealpaths(candidateInputs);
844
+ const afterSignatures = pluginDescriptorInputMetadataSignatures(candidateInputs);
845
+ if (equalPluginDescriptorInputLists(inputs, candidateInputs) &&
846
+ equalPluginDescriptorInputHashes(before, after) &&
847
+ equalPluginDescriptorInputHashes(beforeRealpaths, afterRealpaths) &&
848
+ beforeSignatures !== undefined &&
849
+ afterSignatures !== undefined &&
850
+ equalPluginDescriptorInputHashes(beforeSignatures, afterSignatures)) {
851
+ for (const input of candidateInputs) {
852
+ recordPluginDescriptorInput({
853
+ hash: after[input],
854
+ realpath: afterRealpaths[input],
855
+ resolved: input,
856
+ signature: afterSignatures[input],
857
+ });
858
+ }
859
+ return candidate;
860
+ }
861
+ project = candidate;
862
+ inputs = candidateInputs;
863
+ }
864
+ recordPluginDescriptorProjectInputs(observed, true);
865
+ return project;
866
+ }
867
+ function normalizedProjectConfigPaths(project, requestedConfig) {
868
+ return [
869
+ ...new Set([requestedConfig, ...project.configPaths].map((file) => node_path_1.default.resolve(file))),
870
+ ].sort();
871
+ }
872
+ function pluginDescriptorInputHashes(inputs) {
873
+ return Object.fromEntries(inputs.map((input) => [input, pluginDescriptorInputHash(input)]));
874
+ }
875
+ function pluginDescriptorInputRealpaths(inputs) {
876
+ return Object.fromEntries(inputs.map((input) => [input, pluginDescriptorInputRealpath(input)]));
877
+ }
878
+ function pluginDescriptorInputMetadataSignatures(inputs) {
879
+ const output = {};
880
+ for (const input of inputs) {
881
+ const signature = pluginDescriptorInputMetadataSignature(input);
882
+ if (signature === undefined)
883
+ return undefined;
884
+ output[node_path_1.default.resolve(input)] = signature;
885
+ }
886
+ return output;
887
+ }
888
+ function equalPluginDescriptorInputLists(first, second) {
889
+ return (first.length === second.length &&
890
+ first.every((input, index) => input === second[index]));
891
+ }
892
+ function equalPluginDescriptorInputHashes(first, second) {
893
+ return (Object.keys(first).length === Object.keys(second).length &&
894
+ Object.entries(first).every(([input, hash]) => second[input] === hash));
895
+ }
896
+ function recordPluginDescriptorProjectInputs(inputs, unstable) {
897
+ for (const input of inputs) {
898
+ const resolved = node_path_1.default.resolve(input);
899
+ recordPluginDescriptorInput({
900
+ resolved,
901
+ ...(unstable ? { unstable: true } : {}),
902
+ });
903
+ }
904
+ }
350
905
  /** Narrow a resolved project's compiler options to the emit-format pair. */
351
906
  function projectModuleOptions(compilerOptions) {
352
907
  return {
@@ -365,12 +920,31 @@ function projectModuleOptions(compilerOptions) {
365
920
  * "transform"` type-strip. Shared by the ESM `load` hook and the CommonJS
366
921
  * `require` handler.
367
922
  */
368
- function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href) {
923
+ function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href, prepareAsEntry = false) {
369
924
  const real = realPath(filename);
370
- const served = serveEntryEmit(real);
925
+ // Only the public preload can prepare a newly discovered root. Direct ttsx
926
+ // has one pre-built manifest and historically relies on trailing-stem
927
+ // recovery when Windows presents the same source through its short and long
928
+ // temp-path spellings (the lint TypeScript-config loader is one such case).
929
+ // Treating that boundary as prepare-only would discard the existing emit and
930
+ // feed ESM source into CommonJS interop, producing ERR_REQUIRE_CYCLE_MODULE.
931
+ const prepareEntry = prepareAsEntry ? prepareRuntimeEntry : undefined;
932
+ // A JavaScript-to-TypeScript boundary is a new checked root unless an
933
+ // existing manifest proves exact ownership. Trailing-stem recovery is not
934
+ // ownership evidence: two out-of-include `index.ts` roots can otherwise map
935
+ // to the first manifest's `index.js`, skipping the second root's diagnostics.
936
+ let served = serveEntryEmit(real, prepareEntry === undefined);
371
937
  if (served !== null) {
372
938
  return withInlineSourceMap(served);
373
939
  }
940
+ if (prepareEntry !== undefined) {
941
+ registeredManifests.push(prepareEntry(real));
942
+ served = serveEntryEmit(real);
943
+ if (served === null) {
944
+ throw new Error(`ttsx: prepared entry emit not found for ${filename}`);
945
+ }
946
+ return withInlineSourceMap(served);
947
+ }
374
948
  const built = serveDependencyEmit(real);
375
949
  if (built !== null) {
376
950
  return withInlineSourceMap(built);
@@ -447,7 +1021,7 @@ function emitOrphanAsCommonJs(filename) {
447
1021
  return hit;
448
1022
  }
449
1023
  }
450
- const outDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "ttsx-orphan-"));
1024
+ const outDir = (0, createCanonicalTempDirectory_1.createCanonicalTempDirectory)("ttsx-orphan-");
451
1025
  try {
452
1026
  const res = (0, spawnNative_1.spawnNative)(tsgo, [
453
1027
  filename,
@@ -508,7 +1082,7 @@ function emitCommonJsForNameScan(filename) {
508
1082
  commonJsNameScanSources.set(real, null);
509
1083
  return null;
510
1084
  }
511
- const outDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "ttsx-export-scan-"));
1085
+ const outDir = (0, createCanonicalTempDirectory_1.createCanonicalTempDirectory)("ttsx-export-scan-");
512
1086
  try {
513
1087
  const res = (0, spawnNative_1.spawnNative)(tsgo, [
514
1088
  real,
@@ -936,54 +1510,93 @@ function isIdentifierName(name) {
936
1510
  * `rootDir` cannot have a mirrored emit, so it falls through to the dependency
937
1511
  * paths.
938
1512
  */
939
- function serveEntryEmit(real) {
940
- const m = manifest();
941
- if (m === null) {
942
- return null;
943
- }
944
- const emitted = entryEmitPath(m, real);
945
- if (emitted === null) {
1513
+ function serveEntryEmit(real, allowStemFallback = true) {
1514
+ const owner = findEntryEmit(real, allowStemFallback);
1515
+ if (owner === null) {
946
1516
  return null;
947
1517
  }
948
- const source = readFileOrNull(emitted);
1518
+ const source = readFileOrNull(owner.emittedFile);
949
1519
  return source === null
950
1520
  ? null
951
1521
  : {
952
- emittedFile: emitted,
953
- moduleOptions: m.moduleOptions ?? {},
1522
+ emittedFile: owner.emittedFile,
1523
+ moduleOptions: owner.manifest.moduleOptions ?? {},
954
1524
  source,
955
1525
  sourceFile: real,
956
1526
  };
957
1527
  }
1528
+ /**
1529
+ * Find the manifest that owns `real`. Explicit prepared roots win, followed by
1530
+ * exact mirrored paths across every manifest. Only then may legacy stem
1531
+ * recovery run, so one manifest's approximate match cannot shadow another's
1532
+ * exact emit.
1533
+ */
1534
+ function findEntryEmit(real, allowStemFallback = true) {
1535
+ const manifests = runtimeManifests();
1536
+ for (const candidate of manifests) {
1537
+ if (candidate.entrySource !== undefined &&
1538
+ candidate.entryFile !== undefined &&
1539
+ realPath(candidate.entrySource) === real &&
1540
+ node_fs_1.default.existsSync(candidate.entryFile)) {
1541
+ return { emittedFile: candidate.entryFile, manifest: candidate };
1542
+ }
1543
+ }
1544
+ for (const candidate of manifests) {
1545
+ const emitted = entryEmitPath(candidate, real, false);
1546
+ if (emitted !== null) {
1547
+ return { emittedFile: emitted, manifest: candidate };
1548
+ }
1549
+ }
1550
+ if (!allowStemFallback) {
1551
+ return null;
1552
+ }
1553
+ for (const candidate of manifests) {
1554
+ const emitted = entryEmitPath(candidate, real, true);
1555
+ if (emitted !== null) {
1556
+ return { emittedFile: emitted, manifest: candidate };
1557
+ }
1558
+ }
1559
+ return null;
1560
+ }
958
1561
  /**
959
1562
  * The entry project's emitted JavaScript for `real`, or `null` when that
960
1563
  * project did not emit it. Shared with `owningModuleOptions` so "the entry
961
1564
  * project owns this file" means exactly one thing in both places.
962
1565
  */
963
- function entryEmitPath(m, real) {
964
- const cached = entryEmitPathCache.get(real);
965
- if (cached !== undefined) {
966
- return cached;
1566
+ function entryEmitPath(m, real, allowStemFallback = true) {
1567
+ const cache = allowStemFallback
1568
+ ? entryEmitPathCache
1569
+ : exactEntryEmitPathCache;
1570
+ let manifestCache = cache.get(m);
1571
+ if (manifestCache === undefined) {
1572
+ manifestCache = new Map();
1573
+ cache.set(m, manifestCache);
1574
+ }
1575
+ if (manifestCache.has(real)) {
1576
+ return manifestCache.get(real) ?? null;
967
1577
  }
968
1578
  const resolved = isWithin(real, m.rootDir)
969
1579
  ? (0, resolveEmittedJavaScript_1.resolveEmittedJavaScript)({
1580
+ allowStemFallback,
970
1581
  emittedFiles: m.emittedFiles,
971
1582
  outDir: m.emitDir,
972
1583
  projectRoot: m.rootDir,
973
1584
  sourceFile: real,
974
1585
  })
975
1586
  : null;
976
- entryEmitPathCache.set(real, resolved);
1587
+ manifestCache.set(real, resolved);
977
1588
  return resolved;
978
1589
  }
979
1590
  /**
980
1591
  * Memo for `entryEmitPath`, because it is now on the `resolve` hook's path
981
1592
  * through `owningModuleOptions` — once per import specifier — and a miss inside
982
1593
  * `resolveEmittedJavaScript` walks the whole emit tree. The entry emit is
983
- * written once before the run starts and never changes under it, so one answer
984
- * per path is the only one there is.
1594
+ * written once before the run starts and never changes under it. Registration
1595
+ * can add several independent entry emits, so the manifest is part of the cache
1596
+ * identity as well as the source path.
985
1597
  */
986
- const entryEmitPathCache = new Map();
1598
+ const entryEmitPathCache = new WeakMap();
1599
+ const exactEntryEmitPathCache = new WeakMap();
987
1600
  /**
988
1601
  * True when `real` is `directory` itself or sits beneath it. Handles a root
989
1602
  * `directory` (`/`, `C:\`): naively appending a separator would yield `//`,
@@ -1075,11 +1688,7 @@ function ensureProjectBuilt(tsconfig) {
1075
1688
  return built;
1076
1689
  }
1077
1690
  function dependencyCachePaths(tsconfig) {
1078
- const key = node_crypto_1.default
1079
- .createHash("sha256")
1080
- .update(tsconfig)
1081
- .digest("hex")
1082
- .slice(0, 16);
1691
+ const key = dependencyCacheKey(tsconfig);
1083
1692
  const root = dependencyCacheRoot();
1084
1693
  return {
1085
1694
  cacheDir: node_path_1.default.join(root, key),
@@ -1088,6 +1697,24 @@ function dependencyCachePaths(tsconfig) {
1088
1697
  root,
1089
1698
  };
1090
1699
  }
1700
+ /** Derive one dependency cache key; exported for isolation regressions. */
1701
+ function dependencyCacheKey(tsconfig, options = {}) {
1702
+ const descriptorLoad = options.descriptorLoad ?? process.env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1";
1703
+ return (node_crypto_1.default
1704
+ .createHash("sha256")
1705
+ .update(tsconfig)
1706
+ // Descriptor evaluation promises a result bound to this process's exact
1707
+ // input observations. Reusing an emit another evaluator built can pair
1708
+ // that process's old source/config bytes with this process's later hashes.
1709
+ // Keep ordinary ttsx worker sharing, but isolate descriptor builds with a
1710
+ // non-reusable process nonce; the in-process `builtProjects` map still
1711
+ // compiles each owning project once.
1712
+ .update(descriptorLoad
1713
+ ? `\0descriptor-process:${options.descriptorNonce ?? descriptorProcessCacheNonce}`
1714
+ : "")
1715
+ .digest("hex")
1716
+ .slice(0, 16));
1717
+ }
1091
1718
  /** The immutable emit directory of one build generation under `cacheDir`. */
1092
1719
  function dependencyGenerationDir(cacheDir, generation) {
1093
1720
  return node_path_1.default.join(cacheDir, `gen-${generation}`);
@@ -1216,7 +1843,7 @@ function sleepSync(ms) {
1216
1843
  * never be reused.
1217
1844
  */
1218
1845
  function buildDependency(tsconfig, cacheDir, metaPath) {
1219
- const project = (0, readProjectConfig_1.readProjectConfig)({ cwd: node_path_1.default.dirname(tsconfig), tsconfig });
1846
+ const project = readPluginDescriptorProjectConfig(tsconfig);
1220
1847
  const generation = newDependencyGeneration();
1221
1848
  const emitDir = dependencyGenerationDir(cacheDir, generation);
1222
1849
  node_fs_1.default.rmSync(emitDir, { force: true, recursive: true });
@@ -1303,10 +1930,19 @@ function publishDependencyMeta(metaPath, meta) {
1303
1930
  throw error;
1304
1931
  }
1305
1932
  }
1306
- function dependencyCacheRoot() {
1307
- const m = manifest();
1308
- return m !== null && m.depCacheDir.length !== 0
1309
- ? m.depCacheDir
1933
+ function dependencyCacheRoot(env = process.env) {
1934
+ // Descriptor evaluators are disposable and must not leave one isolated emit
1935
+ // generation in the shared temp cache per load. Their result file already
1936
+ // lives in the evaluator-owned directory that the parent removes in
1937
+ // `finally`; put dependency emits beside it so that cleanup owns both.
1938
+ if (env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1" &&
1939
+ typeof env.TTSC_PLUGIN_DESCRIPTOR_OUT === "string" &&
1940
+ node_path_1.default.isAbsolute(env.TTSC_PLUGIN_DESCRIPTOR_OUT)) {
1941
+ return node_path_1.default.join(node_path_1.default.dirname(env.TTSC_PLUGIN_DESCRIPTOR_OUT), "dependency-cache");
1942
+ }
1943
+ const owner = runtimeManifests().find((candidate) => candidate.depCacheDir.length !== 0);
1944
+ return owner !== undefined
1945
+ ? owner.depCacheDir
1310
1946
  : node_path_1.default.join(node_os_1.default.tmpdir(), "ttsx-dep");
1311
1947
  }
1312
1948
  // -----------------------------------------------------------------------------
@@ -1588,7 +2224,6 @@ function formatDuration(ms) {
1588
2224
  const remainder = seconds % 60;
1589
2225
  return minutes === 0 ? `${seconds}s` : `${minutes}m ${remainder}s`;
1590
2226
  }
1591
- /** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
1592
2227
  const tsconfigCache = new Map();
1593
2228
  const moduleOptionsCache = new Map();
1594
2229
  /**
@@ -1609,13 +2244,30 @@ function nearestTsconfig(file) {
1609
2244
  for (;;) {
1610
2245
  const cached = tsconfigCache.get(directory);
1611
2246
  if (cached !== undefined) {
1612
- return rememberTsconfig(chain, cached);
2247
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") {
2248
+ return rememberTsconfig(chain, cached.result, cached.candidates);
2249
+ }
2250
+ // Bracket the cached selection with the same candidate observations the
2251
+ // parent later reconciles. A nearer config created between a liveness
2252
+ // check and reporting must conflict, not be paired with the cached
2253
+ // farther result and certified as current.
2254
+ recordPluginDescriptorTsconfigCandidates(cached.candidates);
2255
+ const current = nearestExistingTsconfig(cached.candidates);
2256
+ recordPluginDescriptorTsconfigCandidates(cached.candidates);
2257
+ if (current === cached.result) {
2258
+ return rememberTsconfig(chain, cached.result, cached.candidates);
2259
+ }
2260
+ // Descriptor factories can deliberately create a config before a lazy
2261
+ // import. A lookup cached while an earlier import ran must not keep
2262
+ // serving the orphan lane after the nearest candidate changed.
2263
+ tsconfigCache.delete(directory);
1613
2264
  }
1614
2265
  if (node_path_1.default.basename(directory) === "node_modules") {
1615
2266
  return rememberTsconfig(chain, null);
1616
2267
  }
1617
2268
  chain.push(directory);
1618
2269
  const candidate = node_path_1.default.join(directory, "tsconfig.json");
2270
+ recordPluginDescriptorTsconfigCandidates([candidate]);
1619
2271
  if (isFile(candidate)) {
1620
2272
  return rememberTsconfig(chain, candidate);
1621
2273
  }
@@ -1626,12 +2278,33 @@ function nearestTsconfig(file) {
1626
2278
  directory = parent;
1627
2279
  }
1628
2280
  }
1629
- function rememberTsconfig(directories, result) {
1630
- for (const directory of directories) {
1631
- tsconfigCache.set(directory, result);
2281
+ function nearestExistingTsconfig(candidates) {
2282
+ for (const candidate of candidates) {
2283
+ if (isFile(candidate))
2284
+ return node_path_1.default.resolve(candidate);
2285
+ }
2286
+ return null;
2287
+ }
2288
+ function rememberTsconfig(directories, result, tailCandidates = []) {
2289
+ let candidates = [...tailCandidates];
2290
+ for (let index = directories.length - 1; index >= 0; index -= 1) {
2291
+ const directory = directories[index];
2292
+ candidates = [node_path_1.default.join(directory, "tsconfig.json"), ...candidates];
2293
+ tsconfigCache.set(directory, { candidates, result });
1632
2294
  }
1633
2295
  return result;
1634
2296
  }
2297
+ /** Report every owning-config candidate observed by the nearest-config walk. */
2298
+ function recordPluginDescriptorTsconfigCandidates(candidates) {
2299
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
2300
+ return;
2301
+ for (const candidate of candidates) {
2302
+ const resolved = node_path_1.default.resolve(candidate);
2303
+ recordPluginDescriptorInput({
2304
+ resolved,
2305
+ });
2306
+ }
2307
+ }
1635
2308
  function readFileOrNull(file) {
1636
2309
  if (file === null) {
1637
2310
  return null;
@@ -1730,6 +2403,15 @@ const JS_TO_TS_EXTENSIONS = new Map([
1730
2403
  [".mjs", [".mts"]],
1731
2404
  [".cjs", [".cts"]],
1732
2405
  ]);
2406
+ /** Candidate sources that can satisfy one missing JavaScript spelling. */
2407
+ function typescriptSourcesForJavaScriptSpecifier(file) {
2408
+ const extension = node_path_1.default.extname(file).toLowerCase();
2409
+ const substitutions = JS_TO_TS_EXTENSIONS.get(extension);
2410
+ if (substitutions === undefined)
2411
+ return [];
2412
+ const stem = file.slice(0, file.length - extension.length);
2413
+ return substitutions.map((candidate) => stem + candidate);
2414
+ }
1733
2415
  /**
1734
2416
  * Rescue a `specifier` that Node's resolver rejected: map a JavaScript
1735
2417
  * extension back to its TypeScript source, or probe candidate extensions /