ttsc 0.26.2 → 0.27.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 (111) hide show
  1. package/README.md +9 -0
  2. package/cmd/ttsc/api_transform.go +17 -3
  3. package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
  4. package/cmd/ttscgraph/disk_realpath_other.go +16 -0
  5. package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
  6. package/cmd/ttscgraph/serve.go +104 -35
  7. package/driver/emit_plugin.go +20 -3
  8. package/driver/graph.go +64 -11
  9. package/driver/input_observation.go +273 -0
  10. package/driver/input_observation_test.go +149 -0
  11. package/driver/plugins.go +384 -4
  12. package/driver/program.go +54 -1
  13. package/driver/resolution_candidates.go +13 -5
  14. package/driver/rewrite.go +45 -2
  15. package/lib/TtscCompiler.d.ts +12 -11
  16. package/lib/TtscCompiler.js +44 -29
  17. package/lib/TtscCompiler.js.map +1 -1
  18. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  19. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  20. package/lib/compiler/internal/compileProjectInMemory.js +4 -3
  21. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  22. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  23. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  24. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  25. package/lib/compiler/internal/runBuild.d.ts +1 -1
  26. package/lib/compiler/internal/runBuild.js +80 -34
  27. package/lib/compiler/internal/runBuild.js.map +1 -1
  28. package/lib/compiler/internal/runSingleFileEmit.js +2 -2
  29. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  30. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  31. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  32. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  33. package/lib/compiler/internal/startResidentTransform.js +12 -4
  34. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  35. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  36. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  37. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  38. package/lib/flags/schema.js +12 -5
  39. package/lib/flags/schema.js.map +1 -1
  40. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  41. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  42. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  43. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  44. package/lib/internal/projectInputPathIdentity.js +11 -5
  45. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  46. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  47. package/lib/internal/resolveNodeBinary.js +150 -0
  48. package/lib/internal/resolveNodeBinary.js.map +1 -0
  49. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  50. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  51. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  52. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  53. package/lib/launcher/internal/prepareExecution.js +42 -14
  54. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  55. package/lib/launcher/internal/runTtsc.js +25 -13
  56. package/lib/launcher/internal/runTtsc.js.map +1 -1
  57. package/lib/launcher/internal/runTtscserver.js +18 -3
  58. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  59. package/lib/launcher/internal/runTtsx.js +2 -0
  60. package/lib/launcher/internal/runTtsx.js.map +1 -1
  61. package/lib/launcher/internal/runtimeHooks.d.ts +37 -1
  62. package/lib/launcher/internal/runtimeHooks.js +653 -62
  63. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  64. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  65. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  66. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  67. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  68. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  69. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  70. package/lib/register.d.ts +1 -0
  71. package/lib/register.js +46 -0
  72. package/lib/register.js.map +1 -0
  73. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  74. package/lib/structures/ITtscCompilerTransformation.d.ts +31 -0
  75. package/lib/structures/ITtscPlugin.d.ts +33 -0
  76. package/package.json +16 -9
  77. package/shim/checker/go.mod +9 -0
  78. package/shim/checker/go.sum +22 -0
  79. package/shim/checker/shim.go +75 -0
  80. package/shim/compiler/incremental.go +45 -8
  81. package/src/TtscCompiler.ts +55 -33
  82. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  83. package/src/compiler/internal/compileProjectInMemory.ts +4 -3
  84. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  85. package/src/compiler/internal/runBuild.ts +102 -39
  86. package/src/compiler/internal/runSingleFileEmit.ts +2 -2
  87. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  88. package/src/compiler/internal/startResidentTransform.ts +12 -3
  89. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  90. package/src/flags/schema.ts +12 -5
  91. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  92. package/src/internal/projectInputPathIdentity.ts +17 -4
  93. package/src/internal/resolveNodeBinary.ts +173 -0
  94. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  95. package/src/launcher/internal/prepareExecution.ts +50 -14
  96. package/src/launcher/internal/runTtsc.ts +24 -13
  97. package/src/launcher/internal/runTtscserver.ts +18 -3
  98. package/src/launcher/internal/runTtsx.ts +2 -0
  99. package/src/launcher/internal/runtimeHooks.ts +823 -66
  100. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  101. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  102. package/src/register.ts +47 -0
  103. package/src/structures/ITtscCompilerContext.ts +6 -3
  104. package/src/structures/ITtscCompilerTransformation.ts +39 -0
  105. package/src/structures/ITtscPlugin.ts +36 -0
  106. package/tools/shim_audit/baseline.json +0 -1
  107. package/utility/host.go +9 -3
  108. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  109. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  110. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  111. 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`
@@ -167,7 +185,10 @@ let installed = false;
167
185
  * graph goes through `Module._extensions` — the canonical loader extension
168
186
  * point `ts-node`/`tsx` use for the same reason.
169
187
  */
170
- function installRuntimeHooks() {
188
+ function installRuntimeHooks(options = {}) {
189
+ if (options.prepareEntry !== undefined) {
190
+ prepareRuntimeEntry = options.prepareEntry;
191
+ }
171
192
  if (installed) {
172
193
  return;
173
194
  }
@@ -191,7 +212,8 @@ function installRuntimeHooks() {
191
212
  function installCommonJsHook() {
192
213
  const extensions = node_module_1.Module._extensions;
193
214
  const compile = (module, filename) => {
194
- module._compile(resolveServedSource(filename).source, filename);
215
+ const parent = module.parent?.filename;
216
+ module._compile(resolveServedSource(filename, (0, node_url_1.pathToFileURL)(filename).href, typeof parent !== "string" || !isTypeScriptSource(parent)).source, filename);
195
217
  };
196
218
  for (const extension of [".ts", ".tsx", ".cts"]) {
197
219
  extensions[extension] = compile;
@@ -204,11 +226,14 @@ function installCommonJsHook() {
204
226
  * `require` or an ESM `import`.
205
227
  */
206
228
  function entryModuleFormat(entryFile) {
207
- const m = manifest();
208
- return moduleFormat(entryFile, m === null ? null : (m.moduleOptions ?? {})) === "module"
229
+ const real = realPath(entryFile);
230
+ const owner = findEntryEmit(real)?.manifest;
231
+ return moduleFormat(entryFile, owner === undefined ? null : (owner.moduleOptions ?? {})) === "module"
209
232
  ? "module"
210
233
  : "commonjs";
211
234
  }
235
+ /** TypeScript URLs resolved at a JavaScript-to-TypeScript entry boundary. */
236
+ const runtimeEntryUrls = new Set();
212
237
  /**
213
238
  * Rescue an extensionless or directory relative specifier that Node's resolver
214
239
  * rejected. Only runs after `nextResolve` throws, so a successful resolution is
@@ -216,17 +241,344 @@ function entryModuleFormat(entryFile) {
216
241
  * original error is rethrown, preserving `ERR_MODULE_NOT_FOUND`.
217
242
  */
218
243
  function resolve(specifier, context, nextResolve) {
244
+ recordPluginDescriptorResolutionCandidates(specifier, context.parentURL);
219
245
  try {
220
- return rememberCommonJsNamedInterop(restoreStrippedNodeBuiltinScheme(specifier, nextResolve(specifier, context)), context);
246
+ const result = rememberRuntimeEntry(rememberCommonJsNamedInterop(restoreStrippedNodeBuiltinScheme(specifier, nextResolve(specifier, context)), context), context);
247
+ recordPluginDescriptorResolution(specifier, context.parentURL, result.url);
248
+ return result;
221
249
  }
222
250
  catch (error) {
223
251
  const rescued = probeRescuableSpecifier(specifier, context.parentURL);
224
252
  if (rescued === null) {
225
253
  throw error;
226
254
  }
227
- return rememberCommonJsNamedInterop({ shortCircuit: true, url: rescued }, context);
255
+ const result = rememberRuntimeEntry(rememberCommonJsNamedInterop({ shortCircuit: true, url: rescued }, context), context);
256
+ recordPluginDescriptorResolution(specifier, context.parentURL, result.url);
257
+ return result;
228
258
  }
229
259
  }
260
+ /**
261
+ * Fingerprint every path whose state can redirect one descriptor import.
262
+ *
263
+ * This runs before the real resolver. Reporting candidates only after the
264
+ * descriptor finished would pair an earlier descriptor result with later file
265
+ * state when a higher-priority candidate appeared during evaluation.
266
+ */
267
+ function recordPluginDescriptorResolutionCandidates(specifier, parentURL) {
268
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
269
+ return;
270
+ if ((0, node_module_1.isBuiltin)(specifier) || specifier.startsWith("node:"))
271
+ return;
272
+ const parent = runtimeFilePath(parentURL);
273
+ if (parent === undefined)
274
+ return;
275
+ const recorded = new Set();
276
+ const record = (candidate) => {
277
+ const resolved = node_path_1.default.resolve(candidate);
278
+ if (recorded.has(resolved))
279
+ return;
280
+ recorded.add(resolved);
281
+ recordPluginDescriptorInput({
282
+ parent,
283
+ resolved,
284
+ });
285
+ };
286
+ const recordManifestTargets = (value, directory, allowBare = false) => {
287
+ if (typeof value === "string") {
288
+ if (value !== "" &&
289
+ (allowBare || value.startsWith("./") || value.startsWith("../"))) {
290
+ recordBase(node_path_1.default.resolve(directory, value));
291
+ }
292
+ return;
293
+ }
294
+ if (Array.isArray(value)) {
295
+ for (const item of value) {
296
+ recordManifestTargets(item, directory, allowBare);
297
+ }
298
+ return;
299
+ }
300
+ if (value !== null && typeof value === "object") {
301
+ for (const item of Object.values(value)) {
302
+ recordManifestTargets(item, directory, allowBare);
303
+ }
304
+ }
305
+ };
306
+ const recordPackageManifests = (file) => {
307
+ for (let directory = node_path_1.default.dirname(file);; directory = node_path_1.default.dirname(directory)) {
308
+ const manifest = node_path_1.default.join(directory, "package.json");
309
+ record(manifest);
310
+ if (isFile(manifest))
311
+ return;
312
+ const parentDirectory = node_path_1.default.dirname(directory);
313
+ if (parentDirectory === directory)
314
+ return;
315
+ }
316
+ };
317
+ const localBases = (value) => {
318
+ if (value.startsWith("file:"))
319
+ return [(0, node_url_1.fileURLToPath)(value)];
320
+ const raw = node_path_1.default.resolve(node_path_1.default.dirname(parent), value);
321
+ const suffixStart = value.search(/[?#]/);
322
+ if (suffixStart === -1)
323
+ return [raw];
324
+ const pathname = value.slice(0, suffixStart);
325
+ return pathname === ""
326
+ ? [raw]
327
+ : [...new Set([raw, node_path_1.default.resolve(node_path_1.default.dirname(parent), pathname)])];
328
+ };
329
+ const bases = new Set();
330
+ const recordBase = (base) => {
331
+ const resolvedBase = node_path_1.default.resolve(base);
332
+ if (bases.has(resolvedBase))
333
+ return;
334
+ bases.add(resolvedBase);
335
+ record(resolvedBase);
336
+ for (const candidate of typescriptSourcesForJavaScriptSpecifier(resolvedBase)) {
337
+ record(candidate);
338
+ }
339
+ for (const extension of DESCRIPTOR_PROBE_EXTENSIONS) {
340
+ record(resolvedBase + extension);
341
+ }
342
+ const manifestFile = node_path_1.default.join(resolvedBase, "package.json");
343
+ record(manifestFile);
344
+ for (const extension of DESCRIPTOR_PROBE_EXTENSIONS) {
345
+ record(node_path_1.default.join(resolvedBase, `index${extension}`));
346
+ }
347
+ try {
348
+ const manifest = JSON.parse(node_fs_1.default.readFileSync(manifestFile, "utf8").replace(/^\uFEFF/, ""));
349
+ recordManifestTargets(manifest.exports, resolvedBase);
350
+ recordManifestTargets(manifest.module, resolvedBase, true);
351
+ recordManifestTargets(manifest.main, resolvedBase, true);
352
+ }
353
+ catch {
354
+ // The real resolver owns malformed package diagnostics.
355
+ }
356
+ };
357
+ if (specifier.startsWith(".") ||
358
+ node_path_1.default.isAbsolute(specifier) ||
359
+ specifier.startsWith("file:")) {
360
+ try {
361
+ for (const base of localBases(specifier)) {
362
+ recordPackageManifests(base);
363
+ if (isFile(base))
364
+ record(base);
365
+ else
366
+ recordBase(base);
367
+ }
368
+ }
369
+ catch {
370
+ // The real resolver owns invalid URL spellings.
371
+ }
372
+ return;
373
+ }
374
+ const parts = specifier.split("/");
375
+ const packageParts = parts[0]?.startsWith("@")
376
+ ? parts.slice(0, 2)
377
+ : parts.slice(0, 1);
378
+ if (packageParts.some((part) => part === undefined || part === ""))
379
+ return;
380
+ const packageName = packageParts.join("/");
381
+ const subpath = parts.slice(packageParts.length);
382
+ for (const searchPath of (0, node_module_1.createRequire)(parent).resolve.paths(specifier) ??
383
+ []) {
384
+ const packageDirectory = node_path_1.default.join(searchPath, packageName);
385
+ recordBase(packageDirectory);
386
+ if (subpath.length !== 0) {
387
+ recordBase(node_path_1.default.join(packageDirectory, ...subpath));
388
+ }
389
+ // CommonJS resolution continues past an existing but unusable package
390
+ // directory. Record every search root before the resolver runs so a
391
+ // farther selected package retains evaluation-time hashes for its
392
+ // superseding candidates.
393
+ }
394
+ }
395
+ /**
396
+ * Report one resolved descriptor edge to the parent loader. The channel is
397
+ * armed by the generated descriptor shim only after ttsx's own bootstrap and
398
+ * imports have loaded, keeping compiler implementation files out of the
399
+ * project's persistent-cache inputs.
400
+ */
401
+ function recordPluginDescriptorResolution(specifier, parentURL, resolvedURL) {
402
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
403
+ return;
404
+ const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
405
+ if (out === undefined || out.length === 0)
406
+ return;
407
+ const resolved = runtimeFilePath(resolvedURL);
408
+ if (resolved === undefined)
409
+ return;
410
+ const parent = runtimeFilePath(parentURL);
411
+ recordPluginDescriptorInput({
412
+ ...(parent === undefined ? {} : { parent }),
413
+ resolved,
414
+ specifier,
415
+ });
416
+ for (let directory = node_path_1.default.dirname(resolved);; directory = node_path_1.default.dirname(directory)) {
417
+ const manifest = node_path_1.default.join(directory, "package.json");
418
+ recordPluginDescriptorInput({
419
+ ...(parent === undefined ? {} : { parent }),
420
+ resolved: manifest,
421
+ });
422
+ if (isFile(manifest))
423
+ break;
424
+ const parentDirectory = node_path_1.default.dirname(directory);
425
+ if (parentDirectory === directory)
426
+ break;
427
+ }
428
+ }
429
+ function recordPluginDescriptorInput(record) {
430
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
431
+ return;
432
+ const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
433
+ if (out === undefined || out.length === 0)
434
+ return;
435
+ recordPluginDescriptorInputOnce(record);
436
+ const resolved = node_path_1.default.resolve(record.resolved);
437
+ const parsed = node_path_1.default.parse(resolved);
438
+ let current = parsed.root;
439
+ const relative = node_path_1.default.relative(parsed.root, resolved);
440
+ for (const segment of relative.split(node_path_1.default.sep).slice(0, -1)) {
441
+ if (segment === "")
442
+ continue;
443
+ current = node_path_1.default.join(current, segment);
444
+ try {
445
+ if (node_fs_1.default.lstatSync(current).isSymbolicLink()) {
446
+ recordPluginDescriptorInputOnce({ resolved: current });
447
+ }
448
+ }
449
+ catch {
450
+ break;
451
+ }
452
+ }
453
+ }
454
+ /** Record one path without recursively revisiting its lexical ancestors. */
455
+ function recordPluginDescriptorInputOnce(record) {
456
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
457
+ return;
458
+ const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
459
+ if (out === undefined || out.length === 0)
460
+ return;
461
+ try {
462
+ const beforeSignature = pluginDescriptorInputMetadataSignature(record.resolved);
463
+ const observedHash = pluginDescriptorInputHash(record.resolved);
464
+ const observedRealpath = pluginDescriptorInputRealpath(record.resolved);
465
+ const afterSignature = pluginDescriptorInputMetadataSignature(record.resolved);
466
+ const unstable = record.unstable === true ||
467
+ beforeSignature === undefined ||
468
+ afterSignature === undefined ||
469
+ beforeSignature !== afterSignature ||
470
+ (record.hash !== undefined && record.hash !== observedHash) ||
471
+ (record.realpath !== undefined && record.realpath !== observedRealpath) ||
472
+ (record.signature !== undefined && record.signature !== afterSignature);
473
+ node_fs_1.default.appendFileSync(out, `${JSON.stringify({
474
+ ...record,
475
+ hash: observedHash,
476
+ realpath: observedRealpath,
477
+ ...(unstable ? { unstable: true } : { signature: afterSignature }),
478
+ })}\n`, "utf8");
479
+ }
480
+ catch {
481
+ // Dependency reporting is advisory to cache reuse; the selected entry is
482
+ // still retained by the parent if this best-effort side channel fails.
483
+ }
484
+ }
485
+ function pluginDescriptorInputRealpath(file) {
486
+ try {
487
+ return node_fs_1.default.realpathSync.native(file);
488
+ }
489
+ catch {
490
+ return null;
491
+ }
492
+ }
493
+ /** Metadata identity that exposes content-preserving A-B-A replacement. */
494
+ function pluginDescriptorInputMetadataSignature(file) {
495
+ const requested = node_path_1.default.resolve(file);
496
+ let current = requested;
497
+ for (;;) {
498
+ try {
499
+ const link = node_fs_1.default.lstatSync(current, { bigint: true });
500
+ let target = link;
501
+ if (link.isSymbolicLink()) {
502
+ try {
503
+ target = node_fs_1.default.statSync(current, { bigint: true });
504
+ }
505
+ catch {
506
+ // A broken link's own metadata cannot expose its target appearing
507
+ // and disappearing during evaluation. Decline cache proof instead.
508
+ return undefined;
509
+ }
510
+ }
511
+ return [
512
+ node_path_1.default.relative(current, requested),
513
+ link.dev,
514
+ link.ino,
515
+ link.mode,
516
+ link.size,
517
+ link.mtimeNs,
518
+ link.ctimeNs,
519
+ target.dev,
520
+ target.ino,
521
+ target.mode,
522
+ target.size,
523
+ target.mtimeNs,
524
+ target.ctimeNs,
525
+ ].join(":");
526
+ }
527
+ catch (error) {
528
+ if (!isMissingPathError(error))
529
+ return undefined;
530
+ const parent = node_path_1.default.dirname(current);
531
+ if (parent === current)
532
+ return undefined;
533
+ current = parent;
534
+ }
535
+ }
536
+ }
537
+ function pluginDescriptorInputHash(file) {
538
+ try {
539
+ if (node_fs_1.default.statSync(file).isDirectory()) {
540
+ return pluginDescriptorDirectoryHash();
541
+ }
542
+ return node_crypto_1.default
543
+ .createHash("sha256")
544
+ .update(node_fs_1.default.readFileSync(file))
545
+ .digest("hex");
546
+ }
547
+ catch {
548
+ return null;
549
+ }
550
+ }
551
+ /** Stable public fingerprint for an existing directory candidate's kind. */
552
+ function pluginDescriptorDirectoryHash() {
553
+ return node_crypto_1.default
554
+ .createHash("sha256")
555
+ .update("ttsc:host-input:directory\0")
556
+ .digest("hex");
557
+ }
558
+ function runtimeFilePath(value) {
559
+ if (value === undefined)
560
+ return undefined;
561
+ if (value.startsWith("file:")) {
562
+ try {
563
+ return node_path_1.default.resolve((0, node_url_1.fileURLToPath)(value));
564
+ }
565
+ catch {
566
+ return undefined;
567
+ }
568
+ }
569
+ return node_path_1.default.isAbsolute(value) ? node_path_1.default.resolve(value) : undefined;
570
+ }
571
+ /** Remember an ESM root until its synchronous load hook prepares the project. */
572
+ function rememberRuntimeEntry(result, context) {
573
+ if (result.url.startsWith("file:") &&
574
+ isTypeScriptSource((0, node_url_1.fileURLToPath)(result.url)) &&
575
+ (context.parentURL === undefined ||
576
+ !context.parentURL.startsWith("file:") ||
577
+ !isTypeScriptSource((0, node_url_1.fileURLToPath)(context.parentURL)))) {
578
+ runtimeEntryUrls.add(result.url);
579
+ }
580
+ return result;
581
+ }
230
582
  /**
231
583
  * Restore a `node:` builtin URL when affected Node releases return the exact
232
584
  * prefix-stripped spelling from their synchronous CommonJS resolver.
@@ -244,6 +596,12 @@ function restoreStrippedNodeBuiltinScheme(specifier, result) {
244
596
  : result;
245
597
  }
246
598
  const builtProjects = new Map();
599
+ // A descriptor evaluator's dependency emit must never be reused by another
600
+ // process. PIDs are eventually recycled while the disk cache persists, so PID
601
+ // alone cannot provide that isolation. One cryptographically random process
602
+ // nonce keeps every evaluator generation distinct while `builtProjects` still
603
+ // shares repeated imports inside this process.
604
+ const descriptorProcessCacheNonce = node_crypto_1.default.randomBytes(16).toString("hex");
247
605
  /** File URLs whose CommonJS source was reached from an ESM parent import. */
248
606
  const commonJsNamedInteropUrls = new Set();
249
607
  const commonJsNameScanSources = new Map();
@@ -255,15 +613,15 @@ function load(url, context, nextLoad) {
255
613
  if (!isTypeScriptSource(filename)) {
256
614
  return nextLoad(url, context);
257
615
  }
258
- const { format, source } = resolveRuntimeSource(filename, url);
616
+ const { format, source } = resolveRuntimeSource(filename, url, runtimeEntryUrls.delete(url) || isProcessEntry(filename));
259
617
  return {
260
618
  format,
261
619
  shortCircuit: true,
262
620
  source,
263
621
  };
264
622
  }
265
- function resolveRuntimeSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href) {
266
- const served = resolveServedSource(filename, url);
623
+ function resolveRuntimeSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href, prepareAsEntry = false) {
624
+ const served = resolveServedSource(filename, url, prepareAsEntry);
267
625
  const format = moduleFormat(filename, served.moduleOptions);
268
626
  return {
269
627
  format,
@@ -272,6 +630,13 @@ function resolveRuntimeSource(filename, url = (0, node_url_1.pathToFileURL)(file
272
630
  : served.source,
273
631
  };
274
632
  }
633
+ /** Whether `filename` is the TypeScript main module named on Node's argv. */
634
+ function isProcessEntry(filename) {
635
+ const entry = process.argv[1];
636
+ return (entry !== undefined &&
637
+ isTypeScriptSource(entry) &&
638
+ realPath(node_path_1.default.resolve(entry)) === realPath(filename));
639
+ }
275
640
  function rememberCommonJsNamedInterop(result, context) {
276
641
  if (shouldExposeCommonJsNamedExports(result.url, context.parentURL)) {
277
642
  commonJsNamedInteropUrls.add(result.url);
@@ -316,9 +681,9 @@ function owningModuleOptions(filename) {
316
681
  return null;
317
682
  }
318
683
  const real = realPath(filename);
319
- const m = manifest();
320
- if (m !== null && entryEmitPath(m, real) !== null) {
321
- return m.moduleOptions ?? {};
684
+ const owner = findEntryEmit(real)?.manifest;
685
+ if (owner !== undefined) {
686
+ return owner.moduleOptions ?? {};
322
687
  }
323
688
  const tsconfig = nearestTsconfig(real);
324
689
  if (tsconfig === null) {
@@ -330,10 +695,7 @@ function owningModuleOptions(filename) {
330
695
  }
331
696
  let options = {};
332
697
  try {
333
- const project = (0, readProjectConfig_1.readProjectConfig)({
334
- cwd: node_path_1.default.dirname(tsconfig),
335
- tsconfig,
336
- });
698
+ const project = readPluginDescriptorProjectConfig(tsconfig);
337
699
  options = projectModuleOptions(project.compilerOptions);
338
700
  }
339
701
  catch {
@@ -347,6 +709,108 @@ function owningModuleOptions(filename) {
347
709
  moduleOptionsCache.set(tsconfig, options);
348
710
  return options;
349
711
  }
712
+ /**
713
+ * Read one owning config while its discovered chain stays unchanged.
714
+ *
715
+ * A preliminary read discovers `extends`; an accepted read is bracketed by
716
+ * equal fingerprints over that exact chain. If churn never settles, ttsx can
717
+ * still execute the last project but reports every observed config as unstable,
718
+ * preventing a later snapshot from certifying the torn result.
719
+ */
720
+ function readPluginDescriptorProjectConfig(tsconfig) {
721
+ const read = () => (0, readProjectConfig_1.readProjectConfig)({ cwd: node_path_1.default.dirname(tsconfig), tsconfig });
722
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
723
+ return read();
724
+ const observed = new Set([node_path_1.default.resolve(tsconfig)]);
725
+ let project;
726
+ try {
727
+ project = read();
728
+ }
729
+ catch (error) {
730
+ recordPluginDescriptorProjectInputs(observed, true);
731
+ throw error;
732
+ }
733
+ let inputs = normalizedProjectConfigPaths(project, tsconfig);
734
+ for (let attempt = 0; attempt < 3; attempt += 1) {
735
+ for (const input of inputs)
736
+ observed.add(input);
737
+ const before = pluginDescriptorInputHashes(inputs);
738
+ const beforeRealpaths = pluginDescriptorInputRealpaths(inputs);
739
+ const beforeSignatures = pluginDescriptorInputMetadataSignatures(inputs);
740
+ let candidate;
741
+ try {
742
+ candidate = read();
743
+ }
744
+ catch (error) {
745
+ recordPluginDescriptorProjectInputs(observed, true);
746
+ throw error;
747
+ }
748
+ const candidateInputs = normalizedProjectConfigPaths(candidate, tsconfig);
749
+ for (const input of candidateInputs)
750
+ observed.add(input);
751
+ const after = pluginDescriptorInputHashes(candidateInputs);
752
+ const afterRealpaths = pluginDescriptorInputRealpaths(candidateInputs);
753
+ const afterSignatures = pluginDescriptorInputMetadataSignatures(candidateInputs);
754
+ if (equalPluginDescriptorInputLists(inputs, candidateInputs) &&
755
+ equalPluginDescriptorInputHashes(before, after) &&
756
+ equalPluginDescriptorInputHashes(beforeRealpaths, afterRealpaths) &&
757
+ beforeSignatures !== undefined &&
758
+ afterSignatures !== undefined &&
759
+ equalPluginDescriptorInputHashes(beforeSignatures, afterSignatures)) {
760
+ for (const input of candidateInputs) {
761
+ recordPluginDescriptorInput({
762
+ hash: after[input],
763
+ realpath: afterRealpaths[input],
764
+ resolved: input,
765
+ signature: afterSignatures[input],
766
+ });
767
+ }
768
+ return candidate;
769
+ }
770
+ project = candidate;
771
+ inputs = candidateInputs;
772
+ }
773
+ recordPluginDescriptorProjectInputs(observed, true);
774
+ return project;
775
+ }
776
+ function normalizedProjectConfigPaths(project, requestedConfig) {
777
+ return [
778
+ ...new Set([requestedConfig, ...project.configPaths].map((file) => node_path_1.default.resolve(file))),
779
+ ].sort();
780
+ }
781
+ function pluginDescriptorInputHashes(inputs) {
782
+ return Object.fromEntries(inputs.map((input) => [input, pluginDescriptorInputHash(input)]));
783
+ }
784
+ function pluginDescriptorInputRealpaths(inputs) {
785
+ return Object.fromEntries(inputs.map((input) => [input, pluginDescriptorInputRealpath(input)]));
786
+ }
787
+ function pluginDescriptorInputMetadataSignatures(inputs) {
788
+ const output = {};
789
+ for (const input of inputs) {
790
+ const signature = pluginDescriptorInputMetadataSignature(input);
791
+ if (signature === undefined)
792
+ return undefined;
793
+ output[node_path_1.default.resolve(input)] = signature;
794
+ }
795
+ return output;
796
+ }
797
+ function equalPluginDescriptorInputLists(first, second) {
798
+ return (first.length === second.length &&
799
+ first.every((input, index) => input === second[index]));
800
+ }
801
+ function equalPluginDescriptorInputHashes(first, second) {
802
+ return (Object.keys(first).length === Object.keys(second).length &&
803
+ Object.entries(first).every(([input, hash]) => second[input] === hash));
804
+ }
805
+ function recordPluginDescriptorProjectInputs(inputs, unstable) {
806
+ for (const input of inputs) {
807
+ const resolved = node_path_1.default.resolve(input);
808
+ recordPluginDescriptorInput({
809
+ resolved,
810
+ ...(unstable ? { unstable: true } : {}),
811
+ });
812
+ }
813
+ }
350
814
  /** Narrow a resolved project's compiler options to the emit-format pair. */
351
815
  function projectModuleOptions(compilerOptions) {
352
816
  return {
@@ -365,12 +829,31 @@ function projectModuleOptions(compilerOptions) {
365
829
  * "transform"` type-strip. Shared by the ESM `load` hook and the CommonJS
366
830
  * `require` handler.
367
831
  */
368
- function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href) {
832
+ function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href, prepareAsEntry = false) {
369
833
  const real = realPath(filename);
370
- const served = serveEntryEmit(real);
834
+ // Only the public preload can prepare a newly discovered root. Direct ttsx
835
+ // has one pre-built manifest and historically relies on trailing-stem
836
+ // recovery when Windows presents the same source through its short and long
837
+ // temp-path spellings (the lint TypeScript-config loader is one such case).
838
+ // Treating that boundary as prepare-only would discard the existing emit and
839
+ // feed ESM source into CommonJS interop, producing ERR_REQUIRE_CYCLE_MODULE.
840
+ const prepareEntry = prepareAsEntry ? prepareRuntimeEntry : undefined;
841
+ // A JavaScript-to-TypeScript boundary is a new checked root unless an
842
+ // existing manifest proves exact ownership. Trailing-stem recovery is not
843
+ // ownership evidence: two out-of-include `index.ts` roots can otherwise map
844
+ // to the first manifest's `index.js`, skipping the second root's diagnostics.
845
+ let served = serveEntryEmit(real, prepareEntry === undefined);
371
846
  if (served !== null) {
372
847
  return withInlineSourceMap(served);
373
848
  }
849
+ if (prepareEntry !== undefined) {
850
+ registeredManifests.push(prepareEntry(real));
851
+ served = serveEntryEmit(real);
852
+ if (served === null) {
853
+ throw new Error(`ttsx: prepared entry emit not found for ${filename}`);
854
+ }
855
+ return withInlineSourceMap(served);
856
+ }
374
857
  const built = serveDependencyEmit(real);
375
858
  if (built !== null) {
376
859
  return withInlineSourceMap(built);
@@ -447,7 +930,7 @@ function emitOrphanAsCommonJs(filename) {
447
930
  return hit;
448
931
  }
449
932
  }
450
- const outDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "ttsx-orphan-"));
933
+ const outDir = (0, createCanonicalTempDirectory_1.createCanonicalTempDirectory)("ttsx-orphan-");
451
934
  try {
452
935
  const res = (0, spawnNative_1.spawnNative)(tsgo, [
453
936
  filename,
@@ -508,7 +991,7 @@ function emitCommonJsForNameScan(filename) {
508
991
  commonJsNameScanSources.set(real, null);
509
992
  return null;
510
993
  }
511
- const outDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "ttsx-export-scan-"));
994
+ const outDir = (0, createCanonicalTempDirectory_1.createCanonicalTempDirectory)("ttsx-export-scan-");
512
995
  try {
513
996
  const res = (0, spawnNative_1.spawnNative)(tsgo, [
514
997
  real,
@@ -936,54 +1419,93 @@ function isIdentifierName(name) {
936
1419
  * `rootDir` cannot have a mirrored emit, so it falls through to the dependency
937
1420
  * paths.
938
1421
  */
939
- function serveEntryEmit(real) {
940
- const m = manifest();
941
- if (m === null) {
1422
+ function serveEntryEmit(real, allowStemFallback = true) {
1423
+ const owner = findEntryEmit(real, allowStemFallback);
1424
+ if (owner === null) {
942
1425
  return null;
943
1426
  }
944
- const emitted = entryEmitPath(m, real);
945
- if (emitted === null) {
946
- return null;
947
- }
948
- const source = readFileOrNull(emitted);
1427
+ const source = readFileOrNull(owner.emittedFile);
949
1428
  return source === null
950
1429
  ? null
951
1430
  : {
952
- emittedFile: emitted,
953
- moduleOptions: m.moduleOptions ?? {},
1431
+ emittedFile: owner.emittedFile,
1432
+ moduleOptions: owner.manifest.moduleOptions ?? {},
954
1433
  source,
955
1434
  sourceFile: real,
956
1435
  };
957
1436
  }
1437
+ /**
1438
+ * Find the manifest that owns `real`. Explicit prepared roots win, followed by
1439
+ * exact mirrored paths across every manifest. Only then may legacy stem
1440
+ * recovery run, so one manifest's approximate match cannot shadow another's
1441
+ * exact emit.
1442
+ */
1443
+ function findEntryEmit(real, allowStemFallback = true) {
1444
+ const manifests = runtimeManifests();
1445
+ for (const candidate of manifests) {
1446
+ if (candidate.entrySource !== undefined &&
1447
+ candidate.entryFile !== undefined &&
1448
+ realPath(candidate.entrySource) === real &&
1449
+ node_fs_1.default.existsSync(candidate.entryFile)) {
1450
+ return { emittedFile: candidate.entryFile, manifest: candidate };
1451
+ }
1452
+ }
1453
+ for (const candidate of manifests) {
1454
+ const emitted = entryEmitPath(candidate, real, false);
1455
+ if (emitted !== null) {
1456
+ return { emittedFile: emitted, manifest: candidate };
1457
+ }
1458
+ }
1459
+ if (!allowStemFallback) {
1460
+ return null;
1461
+ }
1462
+ for (const candidate of manifests) {
1463
+ const emitted = entryEmitPath(candidate, real, true);
1464
+ if (emitted !== null) {
1465
+ return { emittedFile: emitted, manifest: candidate };
1466
+ }
1467
+ }
1468
+ return null;
1469
+ }
958
1470
  /**
959
1471
  * The entry project's emitted JavaScript for `real`, or `null` when that
960
1472
  * project did not emit it. Shared with `owningModuleOptions` so "the entry
961
1473
  * project owns this file" means exactly one thing in both places.
962
1474
  */
963
- function entryEmitPath(m, real) {
964
- const cached = entryEmitPathCache.get(real);
965
- if (cached !== undefined) {
966
- return cached;
1475
+ function entryEmitPath(m, real, allowStemFallback = true) {
1476
+ const cache = allowStemFallback
1477
+ ? entryEmitPathCache
1478
+ : exactEntryEmitPathCache;
1479
+ let manifestCache = cache.get(m);
1480
+ if (manifestCache === undefined) {
1481
+ manifestCache = new Map();
1482
+ cache.set(m, manifestCache);
1483
+ }
1484
+ if (manifestCache.has(real)) {
1485
+ return manifestCache.get(real) ?? null;
967
1486
  }
968
1487
  const resolved = isWithin(real, m.rootDir)
969
1488
  ? (0, resolveEmittedJavaScript_1.resolveEmittedJavaScript)({
1489
+ allowStemFallback,
970
1490
  emittedFiles: m.emittedFiles,
971
1491
  outDir: m.emitDir,
972
1492
  projectRoot: m.rootDir,
973
1493
  sourceFile: real,
974
1494
  })
975
1495
  : null;
976
- entryEmitPathCache.set(real, resolved);
1496
+ manifestCache.set(real, resolved);
977
1497
  return resolved;
978
1498
  }
979
1499
  /**
980
1500
  * Memo for `entryEmitPath`, because it is now on the `resolve` hook's path
981
1501
  * through `owningModuleOptions` — once per import specifier — and a miss inside
982
1502
  * `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.
1503
+ * written once before the run starts and never changes under it. Registration
1504
+ * can add several independent entry emits, so the manifest is part of the cache
1505
+ * identity as well as the source path.
985
1506
  */
986
- const entryEmitPathCache = new Map();
1507
+ const entryEmitPathCache = new WeakMap();
1508
+ const exactEntryEmitPathCache = new WeakMap();
987
1509
  /**
988
1510
  * True when `real` is `directory` itself or sits beneath it. Handles a root
989
1511
  * `directory` (`/`, `C:\`): naively appending a separator would yield `//`,
@@ -1075,11 +1597,7 @@ function ensureProjectBuilt(tsconfig) {
1075
1597
  return built;
1076
1598
  }
1077
1599
  function dependencyCachePaths(tsconfig) {
1078
- const key = node_crypto_1.default
1079
- .createHash("sha256")
1080
- .update(tsconfig)
1081
- .digest("hex")
1082
- .slice(0, 16);
1600
+ const key = dependencyCacheKey(tsconfig);
1083
1601
  const root = dependencyCacheRoot();
1084
1602
  return {
1085
1603
  cacheDir: node_path_1.default.join(root, key),
@@ -1088,6 +1606,24 @@ function dependencyCachePaths(tsconfig) {
1088
1606
  root,
1089
1607
  };
1090
1608
  }
1609
+ /** Derive one dependency cache key; exported for isolation regressions. */
1610
+ function dependencyCacheKey(tsconfig, options = {}) {
1611
+ const descriptorLoad = options.descriptorLoad ?? process.env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1";
1612
+ return (node_crypto_1.default
1613
+ .createHash("sha256")
1614
+ .update(tsconfig)
1615
+ // Descriptor evaluation promises a result bound to this process's exact
1616
+ // input observations. Reusing an emit another evaluator built can pair
1617
+ // that process's old source/config bytes with this process's later hashes.
1618
+ // Keep ordinary ttsx worker sharing, but isolate descriptor builds with a
1619
+ // non-reusable process nonce; the in-process `builtProjects` map still
1620
+ // compiles each owning project once.
1621
+ .update(descriptorLoad
1622
+ ? `\0descriptor-process:${options.descriptorNonce ?? descriptorProcessCacheNonce}`
1623
+ : "")
1624
+ .digest("hex")
1625
+ .slice(0, 16));
1626
+ }
1091
1627
  /** The immutable emit directory of one build generation under `cacheDir`. */
1092
1628
  function dependencyGenerationDir(cacheDir, generation) {
1093
1629
  return node_path_1.default.join(cacheDir, `gen-${generation}`);
@@ -1216,7 +1752,7 @@ function sleepSync(ms) {
1216
1752
  * never be reused.
1217
1753
  */
1218
1754
  function buildDependency(tsconfig, cacheDir, metaPath) {
1219
- const project = (0, readProjectConfig_1.readProjectConfig)({ cwd: node_path_1.default.dirname(tsconfig), tsconfig });
1755
+ const project = readPluginDescriptorProjectConfig(tsconfig);
1220
1756
  const generation = newDependencyGeneration();
1221
1757
  const emitDir = dependencyGenerationDir(cacheDir, generation);
1222
1758
  node_fs_1.default.rmSync(emitDir, { force: true, recursive: true });
@@ -1303,10 +1839,19 @@ function publishDependencyMeta(metaPath, meta) {
1303
1839
  throw error;
1304
1840
  }
1305
1841
  }
1306
- function dependencyCacheRoot() {
1307
- const m = manifest();
1308
- return m !== null && m.depCacheDir.length !== 0
1309
- ? m.depCacheDir
1842
+ function dependencyCacheRoot(env = process.env) {
1843
+ // Descriptor evaluators are disposable and must not leave one isolated emit
1844
+ // generation in the shared temp cache per load. Their result file already
1845
+ // lives in the evaluator-owned directory that the parent removes in
1846
+ // `finally`; put dependency emits beside it so that cleanup owns both.
1847
+ if (env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1" &&
1848
+ typeof env.TTSC_PLUGIN_DESCRIPTOR_OUT === "string" &&
1849
+ node_path_1.default.isAbsolute(env.TTSC_PLUGIN_DESCRIPTOR_OUT)) {
1850
+ return node_path_1.default.join(node_path_1.default.dirname(env.TTSC_PLUGIN_DESCRIPTOR_OUT), "dependency-cache");
1851
+ }
1852
+ const owner = runtimeManifests().find((candidate) => candidate.depCacheDir.length !== 0);
1853
+ return owner !== undefined
1854
+ ? owner.depCacheDir
1310
1855
  : node_path_1.default.join(node_os_1.default.tmpdir(), "ttsx-dep");
1311
1856
  }
1312
1857
  // -----------------------------------------------------------------------------
@@ -1588,7 +2133,6 @@ function formatDuration(ms) {
1588
2133
  const remainder = seconds % 60;
1589
2134
  return minutes === 0 ? `${seconds}s` : `${minutes}m ${remainder}s`;
1590
2135
  }
1591
- /** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
1592
2136
  const tsconfigCache = new Map();
1593
2137
  const moduleOptionsCache = new Map();
1594
2138
  /**
@@ -1609,13 +2153,30 @@ function nearestTsconfig(file) {
1609
2153
  for (;;) {
1610
2154
  const cached = tsconfigCache.get(directory);
1611
2155
  if (cached !== undefined) {
1612
- return rememberTsconfig(chain, cached);
2156
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") {
2157
+ return rememberTsconfig(chain, cached.result, cached.candidates);
2158
+ }
2159
+ // Bracket the cached selection with the same candidate observations the
2160
+ // parent later reconciles. A nearer config created between a liveness
2161
+ // check and reporting must conflict, not be paired with the cached
2162
+ // farther result and certified as current.
2163
+ recordPluginDescriptorTsconfigCandidates(cached.candidates);
2164
+ const current = nearestExistingTsconfig(cached.candidates);
2165
+ recordPluginDescriptorTsconfigCandidates(cached.candidates);
2166
+ if (current === cached.result) {
2167
+ return rememberTsconfig(chain, cached.result, cached.candidates);
2168
+ }
2169
+ // Descriptor factories can deliberately create a config before a lazy
2170
+ // import. A lookup cached while an earlier import ran must not keep
2171
+ // serving the orphan lane after the nearest candidate changed.
2172
+ tsconfigCache.delete(directory);
1613
2173
  }
1614
2174
  if (node_path_1.default.basename(directory) === "node_modules") {
1615
2175
  return rememberTsconfig(chain, null);
1616
2176
  }
1617
2177
  chain.push(directory);
1618
2178
  const candidate = node_path_1.default.join(directory, "tsconfig.json");
2179
+ recordPluginDescriptorTsconfigCandidates([candidate]);
1619
2180
  if (isFile(candidate)) {
1620
2181
  return rememberTsconfig(chain, candidate);
1621
2182
  }
@@ -1626,12 +2187,33 @@ function nearestTsconfig(file) {
1626
2187
  directory = parent;
1627
2188
  }
1628
2189
  }
1629
- function rememberTsconfig(directories, result) {
1630
- for (const directory of directories) {
1631
- tsconfigCache.set(directory, result);
2190
+ function nearestExistingTsconfig(candidates) {
2191
+ for (const candidate of candidates) {
2192
+ if (isFile(candidate))
2193
+ return node_path_1.default.resolve(candidate);
2194
+ }
2195
+ return null;
2196
+ }
2197
+ function rememberTsconfig(directories, result, tailCandidates = []) {
2198
+ let candidates = [...tailCandidates];
2199
+ for (let index = directories.length - 1; index >= 0; index -= 1) {
2200
+ const directory = directories[index];
2201
+ candidates = [node_path_1.default.join(directory, "tsconfig.json"), ...candidates];
2202
+ tsconfigCache.set(directory, { candidates, result });
1632
2203
  }
1633
2204
  return result;
1634
2205
  }
2206
+ /** Report every owning-config candidate observed by the nearest-config walk. */
2207
+ function recordPluginDescriptorTsconfigCandidates(candidates) {
2208
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1")
2209
+ return;
2210
+ for (const candidate of candidates) {
2211
+ const resolved = node_path_1.default.resolve(candidate);
2212
+ recordPluginDescriptorInput({
2213
+ resolved,
2214
+ });
2215
+ }
2216
+ }
1635
2217
  function readFileOrNull(file) {
1636
2218
  if (file === null) {
1637
2219
  return null;
@@ -1730,6 +2312,15 @@ const JS_TO_TS_EXTENSIONS = new Map([
1730
2312
  [".mjs", [".mts"]],
1731
2313
  [".cjs", [".cts"]],
1732
2314
  ]);
2315
+ /** Candidate sources that can satisfy one missing JavaScript spelling. */
2316
+ function typescriptSourcesForJavaScriptSpecifier(file) {
2317
+ const extension = node_path_1.default.extname(file).toLowerCase();
2318
+ const substitutions = JS_TO_TS_EXTENSIONS.get(extension);
2319
+ if (substitutions === undefined)
2320
+ return [];
2321
+ const stem = file.slice(0, file.length - extension.length);
2322
+ return substitutions.map((candidate) => stem + candidate);
2323
+ }
1733
2324
  /**
1734
2325
  * Rescue a `specifier` that Node's resolver rejected: map a JavaScript
1735
2326
  * extension back to its TypeScript source, or probe candidate extensions /