ttsc 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +66 -63
  3. package/cmd/ttscgraph/serve.go +264 -48
  4. package/cmd/ttscgraph/serve_answers_a_project_that_implements_its_own_ambient_declaration_test.go +69 -0
  5. package/cmd/ttscgraph/serve_early_error_encode_failure_is_observable_test.go +40 -0
  6. package/cmd/ttscgraph/serve_graph_snapshot_version_matches_typescript_client_test.go +32 -0
  7. package/cmd/ttscgraph/serve_phase_trace_accounts_for_load_failure_test.go +45 -0
  8. package/cmd/ttscgraph/serve_phase_trace_is_opt_in_and_payload_free_test.go +52 -0
  9. package/cmd/ttscgraph/serve_protocol_negotiates_incremental_shards_test.go +44 -0
  10. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +2 -2
  11. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +46 -20
  12. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +9 -9
  13. package/cmd/ttscgraph/serve_shards.go +1390 -0
  14. package/cmd/ttscgraph/serve_shards_abandoned_generation_converges_on_retry_test.go +92 -0
  15. package/cmd/ttscgraph/serve_shards_delete_removed_source_test.go +57 -0
  16. package/cmd/ttscgraph/serve_shards_external_endpoints_test.go +53 -0
  17. package/cmd/ttscgraph/serve_shards_keep_source_distributed_dependencies_at_the_boundary_test.go +108 -0
  18. package/cmd/ttscgraph/serve_shards_match_full_dump_oracle_test.go +88 -0
  19. package/cmd/ttscgraph/serve_shards_merge_pending_and_new_edits_test.go +50 -0
  20. package/cmd/ttscgraph/serve_shards_prune_unreferenced_external_endpoints_test.go +65 -0
  21. package/cmd/ttscgraph/serve_shards_publish_created_source_test.go +92 -0
  22. package/cmd/ttscgraph/serve_shards_rebuild_declaration_edit_test.go +43 -0
  23. package/cmd/ttscgraph/serve_shards_refresh_cross_file_implementation_test.go +63 -0
  24. package/cmd/ttscgraph/serve_shards_refresh_dependent_diagnostics_test.go +80 -0
  25. package/cmd/ttscgraph/serve_shards_rename_moves_source_identity_test.go +95 -0
  26. package/cmd/ttscgraph/serve_shards_retry_preserves_committed_generation_test.go +48 -0
  27. package/cmd/ttscgraph/serve_shards_reuse_unaffected_sources_test.go +121 -0
  28. package/driver/declaration_shape.go +85 -0
  29. package/driver/program.go +39 -10
  30. package/go.sum +22 -22
  31. package/internal/graph/assigned_implementations_of_declaration_file_symbols_leave_no_edge_source_test.go +84 -0
  32. package/internal/graph/assigned_implementations_of_node_modules_symbols_leave_no_edge_source_test.go +85 -0
  33. package/internal/graph/build.go +62 -10
  34. package/internal/graph/build_files_reuses_committed_cross_file_endpoints_test.go +87 -0
  35. package/internal/graph/build_keeps_source_distributed_dependencies_at_the_external_boundary_test.go +74 -0
  36. package/internal/graph/dump.go +84 -43
  37. package/internal/graph/dump_path_mapper_caches_alias_resolution_test.go +29 -0
  38. package/internal/graph/dump_paths.go +134 -11
  39. package/internal/graph/dump_paths_canonicalize_windows_short_root_test.go +39 -0
  40. package/internal/graph/dump_rejects_provenance_only_path_errors_test.go +32 -0
  41. package/internal/graph/dump_serializes_the_full_graph_test.go +2 -2
  42. package/internal/graph/dump_symlink_project_publishes_matching_base_test.go +82 -0
  43. package/internal/graph/edges.go +73 -17
  44. package/internal/graph/exports.go +9 -2
  45. package/internal/graph/graph.go +47 -7
  46. package/internal/graph/graph_kind_contracts_match_their_producers_test.go +121 -0
  47. package/internal/graph/node_modifiers_emit_union_strings_test.go +1 -1
  48. package/internal/graph/provenance.go +11 -1
  49. package/internal/graph/resolve.go +1 -4
  50. package/internal/graph/source.go +23 -0
  51. package/internal/graph/wire_node_ids_share_alias_resolution_test.go +29 -0
  52. package/internal/lspserver/lsp_native_plugin_source.go +5 -6
  53. package/internal/lspserver/lsp_native_resident.go +17 -24
  54. package/lib/TtscService.d.ts +1 -6
  55. package/lib/TtscService.js +2 -2
  56. package/lib/TtscService.js.map +1 -1
  57. package/lib/compiler/internal/captureProcessOutput.d.ts +26 -0
  58. package/lib/compiler/internal/captureProcessOutput.js +92 -0
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -0
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -7
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/residentCheckProcess.d.ts +0 -2
  63. package/lib/compiler/internal/residentCheckProcess.js +1 -15
  64. package/lib/compiler/internal/residentCheckProcess.js.map +1 -1
  65. package/lib/compiler/internal/residentTransformProcess.d.ts +0 -6
  66. package/lib/compiler/internal/residentTransformProcess.js +0 -25
  67. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  68. package/lib/compiler/internal/spawnNative.d.ts +14 -6
  69. package/lib/compiler/internal/spawnNative.js +32 -11
  70. package/lib/compiler/internal/spawnNative.js.map +1 -1
  71. package/lib/compiler/internal/startResidentTransform.d.ts +1 -5
  72. package/lib/compiler/internal/startResidentTransform.js +1 -3
  73. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  74. package/lib/launcher/internal/prepareExecution.d.ts +2 -1
  75. package/lib/launcher/internal/prepareExecution.js +269 -23
  76. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  77. package/lib/launcher/internal/registerRuntimeHooks.js +6 -1
  78. package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
  79. package/lib/launcher/internal/runTtscserver.js +4 -9
  80. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  81. package/lib/launcher/internal/runTtsx.js +2 -55
  82. package/lib/launcher/internal/runTtsx.js.map +1 -1
  83. package/lib/launcher/internal/runtimeHooks.d.ts +25 -2
  84. package/lib/launcher/internal/runtimeHooks.js +344 -69
  85. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  86. package/lib/plugin/internal/buildSourcePlugin.js +31 -3
  87. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  88. package/lib/plugin/internal/descriptorProcessFailure.d.ts +23 -19
  89. package/lib/plugin/internal/descriptorProcessFailure.js +40 -51
  90. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -1
  91. package/lib/plugin/internal/loadProjectPlugins.d.ts +12 -0
  92. package/lib/plugin/internal/loadProjectPlugins.js +77 -28
  93. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  94. package/package.json +8 -8
  95. package/shim/ast/extra-shim.json +4 -4
  96. package/shim/ast/go.mod +14 -14
  97. package/shim/ast/go.sum +20 -20
  98. package/shim/astnav/go.mod +14 -14
  99. package/shim/astnav/go.sum +20 -20
  100. package/shim/bundled/extra-shim.json +1 -1
  101. package/shim/bundled/go.mod +5 -5
  102. package/shim/checker/extra-shim.json +31 -31
  103. package/shim/checker/go.mod +5 -5
  104. package/shim/compiler/extra-shim.json +4 -4
  105. package/shim/compiler/go.mod +14 -14
  106. package/shim/compiler/go.sum +22 -22
  107. package/shim/core/extra-shim.json +4 -4
  108. package/shim/core/go.mod +5 -5
  109. package/shim/diagnosticwriter/go.mod +5 -5
  110. package/shim/lsp/extra-shim.json +4 -4
  111. package/shim/lsp/go.mod +16 -16
  112. package/shim/lsp/go.sum +26 -26
  113. package/shim/parser/extra-shim.json +4 -4
  114. package/shim/parser/go.mod +5 -5
  115. package/shim/printer/go.mod +14 -14
  116. package/shim/printer/go.sum +22 -22
  117. package/shim/scanner/extra-shim.json +4 -4
  118. package/shim/scanner/go.mod +5 -5
  119. package/shim/transformers/go.mod +14 -14
  120. package/shim/transformers/go.sum +20 -20
  121. package/shim/tsoptions/extra-shim.json +4 -4
  122. package/shim/tsoptions/go.mod +5 -5
  123. package/shim/tspath/extra-shim.json +4 -4
  124. package/shim/tspath/go.mod +5 -5
  125. package/shim/vfs/cachedvfs/extra-shim.json +4 -4
  126. package/shim/vfs/cachedvfs/go.mod +5 -5
  127. package/shim/vfs/extra-shim.json +4 -4
  128. package/shim/vfs/go.mod +5 -5
  129. package/shim/vfs/osvfs/extra-shim.json +4 -4
  130. package/shim/vfs/osvfs/go.mod +5 -5
  131. package/src/TtscService.ts +8 -20
  132. package/src/compiler/internal/captureProcessOutput.ts +96 -0
  133. package/src/compiler/internal/compileProjectInMemory.ts +7 -7
  134. package/src/compiler/internal/residentCheckProcess.ts +1 -21
  135. package/src/compiler/internal/residentTransformProcess.ts +0 -42
  136. package/src/compiler/internal/spawnNative.ts +39 -18
  137. package/src/compiler/internal/startResidentTransform.ts +1 -12
  138. package/src/launcher/internal/prepareExecution.ts +297 -26
  139. package/src/launcher/internal/registerRuntimeHooks.ts +11 -2
  140. package/src/launcher/internal/runTtscserver.ts +4 -9
  141. package/src/launcher/internal/runTtsx.ts +2 -78
  142. package/src/launcher/internal/runtimeHooks.ts +385 -82
  143. package/src/plugin/internal/buildSourcePlugin.ts +36 -3
  144. package/src/plugin/internal/descriptorProcessFailure.ts +34 -65
  145. package/src/plugin/internal/loadProjectPlugins.ts +78 -39
  146. package/tools/gen_shims/go.mod +14 -14
  147. package/tools/gen_shims/go.sum +12 -12
  148. package/tools/shim_audit/go.mod +10 -10
  149. package/tools/shim_audit/go.sum +8 -8
@@ -88,9 +88,21 @@ interface LoadResult {
88
88
  shortCircuit?: boolean;
89
89
  }
90
90
 
91
+ /**
92
+ * The compiler options that decide the emit format of a file, as declared by
93
+ * the project that emitted it. Both fields matter: tsgo derives the module kind
94
+ * from `target` whenever `module` is absent, so carrying only `module` cannot
95
+ * reproduce its decision.
96
+ */
97
+ export interface OwningModuleOptions {
98
+ module?: string;
99
+ target?: string;
100
+ }
101
+
91
102
  interface ServedSource {
92
103
  source: string;
93
- moduleOption?: string;
104
+ /** Options of the project that emitted this source; `null` when none did. */
105
+ moduleOptions: OwningModuleOptions | null;
94
106
  emittedFile?: string;
95
107
  sourceFile?: string;
96
108
  }
@@ -114,8 +126,12 @@ interface RuntimeManifest {
114
126
  emitDir: string;
115
127
  /** Emitted file list from the entry build, for source→output matching. */
116
128
  emittedFiles?: readonly string[];
117
- /** The entry tsconfig's `module` option, deciding emit CJS/ESM per file. */
118
- moduleOption?: string;
129
+ /**
130
+ * The entry tsconfig's `module` and `target`, deciding emit CJS/ESM per file.
131
+ * `target` is not decoration: an absent `module` makes tsgo derive the module
132
+ * kind from it.
133
+ */
134
+ moduleOptions?: OwningModuleOptions;
119
135
  /** Root directory for per-dependency build output. */
120
136
  depCacheDir: string;
121
137
  }
@@ -280,12 +296,16 @@ function installCommonJsHook(): void {
280
296
 
281
297
  /**
282
298
  * The module format of the entry source file, derived from the entry project's
283
- * `module` option (via the runtime manifest) the same way the served files are
299
+ * compiler options (via the runtime manifest) the same way the served files are
284
300
  * classified. The bootstrap uses it to load the entry through a CommonJS
285
301
  * `require` or an ESM `import`.
286
302
  */
287
303
  export function entryModuleFormat(entryFile: string): "module" | "commonjs" {
288
- return moduleFormat(entryFile, manifest()?.moduleOption) === "module"
304
+ const m = manifest();
305
+ return moduleFormat(
306
+ entryFile,
307
+ m === null ? null : (m.moduleOptions ?? {}),
308
+ ) === "module"
289
309
  ? "module"
290
310
  : "commonjs";
291
311
  }
@@ -346,7 +366,7 @@ interface BuiltProject {
346
366
  emitDir: string;
347
367
  rootDir: string;
348
368
  emittedFiles?: readonly string[];
349
- moduleOption?: string;
369
+ moduleOptions: OwningModuleOptions;
350
370
  }
351
371
  const builtProjects = new Map<string, BuiltProject>();
352
372
 
@@ -379,7 +399,7 @@ function resolveRuntimeSource(
379
399
  url: string = pathToFileURL(filename).href,
380
400
  ): { format: string; source: string } {
381
401
  const served = resolveServedSource(filename, url);
382
- const format = moduleFormat(filename, served.moduleOption);
402
+ const format = moduleFormat(filename, served.moduleOptions);
383
403
  return {
384
404
  format,
385
405
  source:
@@ -403,6 +423,17 @@ function rememberCommonJsNamedInterop(
403
423
  return result;
404
424
  }
405
425
 
426
+ /**
427
+ * Whether a CommonJS-classified TypeScript source reached from an ESM parent
428
+ * needs its nested `export *` names exposed.
429
+ *
430
+ * Only the parent side is decided here. The child's own format is re-checked
431
+ * authoritatively in `resolveRuntimeSource` against the format its served
432
+ * source actually carries, so this predicate deliberately does not repeat that
433
+ * check: doing so would need the child's owning project, which is not known
434
+ * until the source is served, and an answer guessed from the nearest tsconfig
435
+ * silently under-exposes a file that tsconfig does not compile.
436
+ */
406
437
  function shouldExposeCommonJsNamedExports(
407
438
  url: string,
408
439
  parentURL: string | undefined,
@@ -415,49 +446,70 @@ function shouldExposeCommonJsNamedExports(
415
446
  return false;
416
447
  }
417
448
  const parentFile = fileURLToPath(parentURL);
418
- if (
419
- moduleFormat(parentFile, moduleOptionForSource(parentFile)) !== "module"
420
- ) {
449
+ if (moduleFormat(parentFile, owningModuleOptions(parentFile)) !== "module") {
421
450
  return false;
422
451
  }
423
- const filename = fileURLToPath(url);
424
- return (
425
- isTypeScriptSource(filename) &&
426
- moduleFormat(filename, moduleOptionForSource(filename)) === "commonjs"
427
- );
452
+ return isTypeScriptSource(fileURLToPath(url));
428
453
  }
429
454
 
430
- function moduleOptionForSource(filename: string): string | undefined {
455
+ /**
456
+ * The emit-deciding compiler options of the project that owns `filename`, or
457
+ * `null` when none does.
458
+ *
459
+ * The entry project owns a file only when it actually emitted it. Testing
460
+ * `isWithin(rootDir)` alone would claim every file under a wide `rootDir` —
461
+ * including the volume-root `rootDir` a config-loader project uses — and hand
462
+ * them the entry project's options even though the dependency or orphan lane is
463
+ * what serves them.
464
+ */
465
+ function owningModuleOptions(filename: string): OwningModuleOptions | null {
431
466
  if (!isTypeScriptSource(filename)) {
432
- return undefined;
467
+ return null;
433
468
  }
434
469
  const real = realPath(filename);
435
470
  const m = manifest();
436
- if (m !== null && isWithin(real, m.rootDir)) {
437
- return m.moduleOption;
471
+ if (m !== null && entryEmitPath(m, real) !== null) {
472
+ return m.moduleOptions ?? {};
438
473
  }
439
474
  const tsconfig = nearestTsconfig(real);
440
475
  if (tsconfig === null) {
441
- return undefined;
476
+ return null;
442
477
  }
443
- if (moduleOptionCache.has(tsconfig)) {
444
- return moduleOptionCache.get(tsconfig);
478
+ const cached = moduleOptionsCache.get(tsconfig);
479
+ if (cached !== undefined) {
480
+ return cached;
445
481
  }
446
- let moduleOption: string | undefined;
482
+ let options: OwningModuleOptions = {};
447
483
  try {
448
484
  const project = readProjectConfig({
449
485
  cwd: path.dirname(tsconfig),
450
486
  tsconfig,
451
487
  });
452
- moduleOption =
453
- typeof project.compilerOptions.module === "string"
454
- ? project.compilerOptions.module
455
- : undefined;
488
+ options = projectModuleOptions(project.compilerOptions);
456
489
  } catch {
457
- moduleOption = undefined;
490
+ // The owning project cannot be read, so nothing is known about the format
491
+ // it would have emitted. That is the same state as having no project at
492
+ // all, and it is what the dependency lane will conclude too when its build
493
+ // fails and the file falls through to the orphan type-strip.
494
+ moduleOptionsCache.set(tsconfig, null);
495
+ return null;
458
496
  }
459
- moduleOptionCache.set(tsconfig, moduleOption);
460
- return moduleOption;
497
+ moduleOptionsCache.set(tsconfig, options);
498
+ return options;
499
+ }
500
+
501
+ /** Narrow a resolved project's compiler options to the emit-format pair. */
502
+ export function projectModuleOptions(
503
+ compilerOptions: Record<string, unknown>,
504
+ ): OwningModuleOptions {
505
+ return {
506
+ ...(typeof compilerOptions.module === "string"
507
+ ? { module: compilerOptions.module }
508
+ : {}),
509
+ ...(typeof compilerOptions.target === "string"
510
+ ? { target: compilerOptions.target }
511
+ : {}),
512
+ };
461
513
  }
462
514
 
463
515
  /**
@@ -474,17 +526,14 @@ function resolveServedSource(
474
526
  const real = realPath(filename);
475
527
  const served = serveEntryEmit(real);
476
528
  if (served !== null) {
477
- return withInlineSourceMap({
478
- moduleOption: manifest()?.moduleOption,
479
- ...served,
480
- });
529
+ return withInlineSourceMap(served);
481
530
  }
482
531
  const built = serveDependencyEmit(real);
483
532
  if (built !== null) {
484
533
  return withInlineSourceMap(built);
485
534
  }
486
535
  return {
487
- moduleOption: undefined,
536
+ moduleOptions: null,
488
537
  sourceFile: filename,
489
538
  source: transformOrphanSource(filename, url),
490
539
  };
@@ -523,7 +572,7 @@ function withInlineSourceMap(served: ServedSource): ServedSource {
523
572
  * owning project; an ESM-format orphan keeps the fast in-process strip.
524
573
  */
525
574
  function transformOrphanSource(filename: string, url: string): string {
526
- if (moduleFormat(filename, undefined) === "commonjs") {
575
+ if (moduleFormat(filename, null) === "commonjs") {
527
576
  const lowered = emitOrphanAsCommonJs(filename);
528
577
  if (lowered !== null) {
529
578
  return lowered;
@@ -1122,24 +1171,52 @@ function serveEntryEmit(real: string): ServedSource | null {
1122
1171
  if (m === null) {
1123
1172
  return null;
1124
1173
  }
1125
- if (!isWithin(real, m.rootDir)) {
1126
- return null;
1127
- }
1128
- const emitted = resolveEmittedJavaScript({
1129
- emittedFiles: m.emittedFiles,
1130
- outDir: m.emitDir,
1131
- projectRoot: m.rootDir,
1132
- sourceFile: real,
1133
- });
1174
+ const emitted = entryEmitPath(m, real);
1134
1175
  if (emitted === null) {
1135
1176
  return null;
1136
1177
  }
1137
1178
  const source = readFileOrNull(emitted);
1138
1179
  return source === null
1139
1180
  ? null
1140
- : { emittedFile: emitted, source, sourceFile: real };
1181
+ : {
1182
+ emittedFile: emitted,
1183
+ moduleOptions: m.moduleOptions ?? {},
1184
+ source,
1185
+ sourceFile: real,
1186
+ };
1141
1187
  }
1142
1188
 
1189
+ /**
1190
+ * The entry project's emitted JavaScript for `real`, or `null` when that
1191
+ * project did not emit it. Shared with `owningModuleOptions` so "the entry
1192
+ * project owns this file" means exactly one thing in both places.
1193
+ */
1194
+ function entryEmitPath(m: RuntimeManifest, real: string): string | null {
1195
+ const cached = entryEmitPathCache.get(real);
1196
+ if (cached !== undefined) {
1197
+ return cached;
1198
+ }
1199
+ const resolved = isWithin(real, m.rootDir)
1200
+ ? resolveEmittedJavaScript({
1201
+ emittedFiles: m.emittedFiles,
1202
+ outDir: m.emitDir,
1203
+ projectRoot: m.rootDir,
1204
+ sourceFile: real,
1205
+ })
1206
+ : null;
1207
+ entryEmitPathCache.set(real, resolved);
1208
+ return resolved;
1209
+ }
1210
+
1211
+ /**
1212
+ * Memo for `entryEmitPath`, because it is now on the `resolve` hook's path
1213
+ * through `owningModuleOptions` — once per import specifier — and a miss inside
1214
+ * `resolveEmittedJavaScript` walks the whole emit tree. The entry emit is
1215
+ * written once before the run starts and never changes under it, so one answer
1216
+ * per path is the only one there is.
1217
+ */
1218
+ const entryEmitPathCache = new Map<string, string | null>();
1219
+
1143
1220
  /**
1144
1221
  * True when `real` is `directory` itself or sits beneath it. Handles a root
1145
1222
  * `directory` (`/`, `C:\`): naively appending a separator would yield `//`,
@@ -1207,7 +1284,7 @@ function serveBuiltDependency(
1207
1284
  ? null
1208
1285
  : {
1209
1286
  emittedFile: emitted,
1210
- moduleOption: built.moduleOption,
1287
+ moduleOptions: built.moduleOptions,
1211
1288
  source,
1212
1289
  sourceFile: real,
1213
1290
  };
@@ -1227,7 +1304,7 @@ function serveBuiltDependency(
1227
1304
  interface DependencyCacheMeta {
1228
1305
  generation: string;
1229
1306
  rootDir: string;
1230
- moduleOption?: string;
1307
+ moduleOptions?: OwningModuleOptions;
1231
1308
  }
1232
1309
 
1233
1310
  /**
@@ -1328,7 +1405,15 @@ export function readDependencyCache(
1328
1405
  }
1329
1406
  if (
1330
1407
  !isDependencyGeneration(meta.generation) ||
1331
- typeof meta.rootDir !== "string"
1408
+ typeof meta.rootDir !== "string" ||
1409
+ // A marker with no `moduleOptions` object predates this field and cannot
1410
+ // say which format its emit carries. Treating the absence as "no options"
1411
+ // would classify a CommonJS emit as an ES module, so the generation is
1412
+ // rejected and rebuilt instead. Every marker this version writes carries
1413
+ // the object, empty or not.
1414
+ typeof meta.moduleOptions !== "object" ||
1415
+ meta.moduleOptions === null ||
1416
+ Array.isArray(meta.moduleOptions)
1332
1417
  ) {
1333
1418
  return null;
1334
1419
  }
@@ -1339,9 +1424,18 @@ export function readDependencyCache(
1339
1424
  return {
1340
1425
  emitDir,
1341
1426
  emittedFiles: undefined,
1342
- moduleOption:
1343
- typeof meta.moduleOption === "string" ? meta.moduleOption : undefined,
1344
- rootDir: meta.rootDir,
1427
+ moduleOptions: projectModuleOptions(
1428
+ meta.moduleOptions as Record<string, unknown>,
1429
+ ),
1430
+ // Resolved on the way out, not trusted as written. `rootDir` never gated
1431
+ // reuse — the marker's generation, module options, and a non-empty emit do
1432
+ // — so a marker carrying an unresolved spelling was already being reused,
1433
+ // and then served every file of that dependency through the whole-tree stem
1434
+ // rescan. The pass is idempotent and runs only on a hit, which
1435
+ // `ensureProjectBuilt` memoizes per tsconfig. A marker from an earlier ttsc
1436
+ // survives only under the shared `os.tmpdir()/ttsx-dep` fallback root; the
1437
+ // manifest's own `depCacheDir` is per-process and removed with the run.
1438
+ rootDir: resolvePhysicalPath(meta.rootDir),
1345
1439
  };
1346
1440
  }
1347
1441
 
@@ -1480,16 +1574,10 @@ function buildDependency(
1480
1574
  .join("\n"),
1481
1575
  );
1482
1576
  }
1483
- const rootDir =
1484
- typeof project.compilerOptions.rootDir === "string"
1485
- ? project.compilerOptions.rootDir
1486
- : project.root;
1487
- const moduleOption =
1488
- typeof project.compilerOptions.module === "string"
1489
- ? project.compilerOptions.module
1490
- : undefined;
1491
- publishDependencyMeta(metaPath, { generation, moduleOption, rootDir });
1492
- return { emitDir, emittedFiles: undefined, moduleOption, rootDir };
1577
+ const rootDir = resolveDependencySourceRoot(project);
1578
+ const moduleOptions = projectModuleOptions(project.compilerOptions);
1579
+ publishDependencyMeta(metaPath, { generation, moduleOptions, rootDir });
1580
+ return { emitDir, emittedFiles: undefined, moduleOptions, rootDir };
1493
1581
  }
1494
1582
 
1495
1583
  /**
@@ -1896,7 +1984,7 @@ function formatDuration(ms: number): string {
1896
1984
 
1897
1985
  /** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
1898
1986
  const tsconfigCache = new Map<string, string | null>();
1899
- const moduleOptionCache = new Map<string, string | undefined>();
1987
+ const moduleOptionsCache = new Map<string, OwningModuleOptions | null>();
1900
1988
 
1901
1989
  /**
1902
1990
  * The nearest `tsconfig.json` at or above `file`'s directory, or `null`. The
@@ -1955,14 +2043,86 @@ function readFileOrNull(file: string | null): string | null {
1955
2043
  }
1956
2044
  }
1957
2045
 
1958
- function realPath(target: string): string {
2046
+ /**
2047
+ * The physical path, in the same spelling the launcher decided on.
2048
+ *
2049
+ * `realpathSync.native` first, because the plain implementation resolves
2050
+ * reparse points but leaves a Windows 8.3 component alone — and `TEMP` is
2051
+ * `C:\Users\RUNNER~1\...` on a GitHub Windows runner. The launcher resolves the
2052
+ * entry through `fs.realpathSync.native`, so answering here with the short name
2053
+ * would put a `..` in `path.relative(rootDir, real)`, drop the exact-mirror
2054
+ * lane, and leave the entry to whatever the trailing-stem matcher picks.
2055
+ */
2056
+ export function realPath(target: string): string {
1959
2057
  try {
1960
- return fs.realpathSync(target);
2058
+ return fs.realpathSync.native?.(target) ?? fs.realpathSync(target);
1961
2059
  } catch {
1962
2060
  return target;
1963
2061
  }
1964
2062
  }
1965
2063
 
2064
+ /**
2065
+ * The physical spelling of a path, produced the way the served sources it will
2066
+ * be compared against are produced.
2067
+ *
2068
+ * `createFilesystemPathIdentityContext` is the same resolver the entry lane
2069
+ * uses (`prepareExecution.ts::resolveRuntimeSourceRoot`). For a path that
2070
+ * exists it is one `realpathSync.native`. For one that does not it resolves as
2071
+ * far as the filesystem goes and folds the missing tail by the case semantics
2072
+ * of the surviving ancestor, which costs a directory read and, on Windows, can
2073
+ * cost an `fsutil` query — worth knowing, but off the path a real dependency
2074
+ * root takes.
2075
+ *
2076
+ * Total on purpose. `throwOnRealpathError: false` silences a failed realpath,
2077
+ * but the case-sensitivity probe can still fail on its own (an unreadable
2078
+ * ancestor, a denied alternate-case `lstat`), and neither caller has anywhere
2079
+ * to put that: `readDependencyCache` is contracted to answer `null` rather than
2080
+ * throw, and `buildDependency` has already produced its emit. The unresolved
2081
+ * spelling is exactly what both had before this pass, so it is the fallback.
2082
+ */
2083
+ function resolvePhysicalPath(location: string): string {
2084
+ try {
2085
+ return createFilesystemPathIdentityContext({
2086
+ throwOnRealpathError: false,
2087
+ }).resolve(location).path;
2088
+ } catch {
2089
+ return location;
2090
+ }
2091
+ }
2092
+
2093
+ /**
2094
+ * The source-tree root a dependency's emit mirrors, in the spelling
2095
+ * `resolveEmittedJavaScript` compares a served source against.
2096
+ *
2097
+ * Both branches need the pass, for different reasons. A declared `rootDir`
2098
+ * arrives from `readProjectConfig` joined against the config that declared it
2099
+ * but never resolved, so a `rootDir` that is itself a symlinked directory stays
2100
+ * unresolved. `project.root` arrives through plain `fs.realpathSync`, which
2101
+ * follows reparse points but leaves a Windows 8.3 component alone — and
2102
+ * {@link realPath} uses `fs.realpathSync.native`, which expands it. Either way
2103
+ * `path.relative` puts a `..` in front of an in-project source,
2104
+ * `resolveExactEmittedFiles` returns nothing, and every served file of that
2105
+ * dependency falls to the trailing-stem matcher, which rescans the whole emit
2106
+ * tree per file because a dependency build publishes no emitted-file list.
2107
+ *
2108
+ * This is the pass the entry lane settled on for the same mixed pair; the two
2109
+ * lanes now read alike, `path.isAbsolute` guard included. `readProjectConfig`
2110
+ * absolutizes every path option against the config that declared it, so that
2111
+ * guard is a mirror of the entry lane rather than a live branch.
2112
+ */
2113
+ function resolveDependencySourceRoot(
2114
+ project: ReturnType<typeof readProjectConfig>,
2115
+ ): string {
2116
+ const rootDir = project.compilerOptions.rootDir;
2117
+ return resolvePhysicalPath(
2118
+ typeof rootDir !== "string"
2119
+ ? project.root
2120
+ : path.isAbsolute(rootDir)
2121
+ ? rootDir
2122
+ : path.resolve(project.root, rootDir),
2123
+ );
2124
+ }
2125
+
1966
2126
  /**
1967
2127
  * Map the JavaScript extension a relative `specifier` carries to the TypeScript
1968
2128
  * source extensions tsgo would have emitted it from. Running from source, a
@@ -2050,16 +2210,27 @@ function probeRescuableSpecifier(
2050
2210
 
2051
2211
  /**
2052
2212
  * Decide the module format the way Node and tsgo do — from configuration, never
2053
- * by sniffing the emitted text. The file extension is authoritative
2054
- * (`.mts`/`.mjs` → module, `.cts`/`.cjs` → commonjs); otherwise the owning
2055
- * tsconfig's `module` option decides: a fixed CommonJS / ES target maps
2056
- * directly, while the `node*`/`preserve` family (and a file with no owning
2057
- * tsconfig) defers to the nearest `package.json` `type`, exactly as tsgo chose
2058
- * when it emitted.
2213
+ * by sniffing the emitted text.
2214
+ *
2215
+ * The file extension is authoritative first (`.mts`/`.mjs` → module,
2216
+ * `.cts`/`.cjs` → commonjs), exactly as tsgo's
2217
+ * `getImpliedNodeFormatForEmitWorker` checks it ahead of everything else.
2218
+ *
2219
+ * After that the decision belongs to the project that emitted the file, so
2220
+ * `options` is the whole compiler-option pair tsgo consults, not just `module`:
2221
+ * an absent `module` is NOT "ask Node", it is "derive the kind from `target`"
2222
+ * (tsgo's `getEmitModuleKind`), and TypeScript 7 defaults `target` to the
2223
+ * latest standard, which means ES modules. Only the `node*` family defers to
2224
+ * the nearest `package.json` `type`, because only that family makes tsgo
2225
+ * consult it.
2226
+ *
2227
+ * `options` is `null` for a file no tsconfig owns at all — a raw `.ts` shipped
2228
+ * under `node_modules`. Nothing emitted it, so Node's own rule is the only rule
2229
+ * there is, and the package `type` decides.
2059
2230
  */
2060
2231
  function moduleFormat(
2061
2232
  filename: string,
2062
- moduleOption: string | undefined,
2233
+ options: OwningModuleOptions | null,
2063
2234
  ): string {
2064
2235
  if (filename.endsWith(".mts") || filename.endsWith(".mjs")) {
2065
2236
  return "module";
@@ -2067,24 +2238,120 @@ function moduleFormat(
2067
2238
  if (filename.endsWith(".cts") || filename.endsWith(".cjs")) {
2068
2239
  return "commonjs";
2069
2240
  }
2070
- const option = (moduleOption ?? "").toLowerCase();
2071
- if (option === "commonjs" || option === "node10" || option === "none") {
2072
- return "commonjs";
2241
+ if (options === null) {
2242
+ return nearestPackageType(filename);
2243
+ }
2244
+ // A package that states its `type` outright decides for the files inside it
2245
+ // whatever the module kind is. This is where a source-shipping dependency's
2246
+ // own declaration overrides the compiling project's `module`; see
2247
+ // `declaredNodeModulesPackageType` for why `node_modules` bounds it.
2248
+ const declared = declaredNodeModulesPackageType(filename);
2249
+ if (declared !== null) {
2250
+ return declared;
2073
2251
  }
2252
+ const kind = effectiveModuleKind(options);
2074
2253
  if (
2075
- option === "" ||
2076
- option === "node16" ||
2077
- option === "node18" ||
2078
- option === "nodenext" ||
2079
- option === "preserve"
2254
+ kind === "node16" ||
2255
+ kind === "node18" ||
2256
+ kind === "node20" ||
2257
+ kind === "nodenext"
2080
2258
  ) {
2081
2259
  return nearestPackageType(filename);
2082
2260
  }
2083
- // Every remaining `module` target (es2015 … esnext, system, amd, umd) emits
2084
- // ECMAScript modules.
2261
+ if (kind === "commonjs") {
2262
+ return "commonjs";
2263
+ }
2264
+ // Everything that remains (es2015 … esnext, and `preserve`, which keeps the
2265
+ // authored ESM syntax verbatim whatever the package `type` says) is emitted
2266
+ // as ECMAScript modules. `amd`, `umd`, and `system` are not among them:
2267
+ // TypeScript 7 removed all three, so a project declaring one cannot build,
2268
+ // and a file it would have owned reaches the run through the orphan lane
2269
+ // instead.
2085
2270
  return "module";
2086
2271
  }
2087
2272
 
2273
+ /**
2274
+ * The `"type"` a `node_modules` package states for `filename`, or `null`.
2275
+ *
2276
+ * Tsgo's `GetImpliedNodeFormatForEmitWorker` consults a file's
2277
+ * `packageJsonType` for **every** module kind, not just the `node*` family. So
2278
+ * a source-shipping dependency that declares `"type": "commonjs"` is emitted as
2279
+ * CommonJS even while the compiling project asks for `esnext`, and the mirror
2280
+ * has to honour the same override or Node is handed the wrong format.
2281
+ *
2282
+ * Outside `node_modules` that field is empty for every module kind this
2283
+ * override can change. `loadSourceFileMetaData` does fill it elsewhere — for a
2284
+ * file whose extension does not itself state the format (anything but `.mts`,
2285
+ * `.cts`, `.mjs`, `.cjs`) whose project sets `moduleResolution` to the
2286
+ * `node16`…`nodenext` family — but `program.go` rejects that resolution unless
2287
+ * `module` is in the same family, and for a `node*` `module` the package `type`
2288
+ * is already the whole answer, which the caller's own `node*` branch produces.
2289
+ * So answering from a manifest outside `node_modules` could only override the
2290
+ * project's own `module` option, the very confusion this classifier exists to
2291
+ * end.
2292
+ */
2293
+ function declaredNodeModulesPackageType(
2294
+ filename: string,
2295
+ ): "module" | "commonjs" | null {
2296
+ if (!pathHasNodeModulesSegment(filename)) {
2297
+ return null;
2298
+ }
2299
+ return declaredPackageType(filename);
2300
+ }
2301
+
2302
+ /**
2303
+ * Whether any path segment of `filename` is literally `node_modules`.
2304
+ *
2305
+ * Case-sensitive on every platform, because the upstream test this mirrors is a
2306
+ * case-sensitive substring search and Node's own resolver recognises only the
2307
+ * exact spelling. A directory named `Node_Modules` is a package store to
2308
+ * neither of them, and must not become one here.
2309
+ */
2310
+ function pathHasNodeModulesSegment(filename: string): boolean {
2311
+ return filename.split(/[\\/]/).includes("node_modules");
2312
+ }
2313
+
2314
+ /**
2315
+ * The `module` kind tsgo actually emits with, mirroring `getEmitModuleKind`:
2316
+ * the declared `module` when there is one, and otherwise the kind implied by
2317
+ * `target`. `"none"` is the spelling of an unset `module`, so it derives the
2318
+ * same way.
2319
+ */
2320
+ function effectiveModuleKind(options: OwningModuleOptions): string {
2321
+ const declared = (options.module ?? "").toLowerCase();
2322
+ if (declared !== "" && declared !== "none") {
2323
+ return declared;
2324
+ }
2325
+ const target = (options.target ?? "").toLowerCase();
2326
+ if (target === "esnext") {
2327
+ return "esnext";
2328
+ }
2329
+ const year = scriptTargetYear(target);
2330
+ if (year === null) {
2331
+ // An unset `target` is `ScriptTargetLatestStandard` upstream, well above the
2332
+ // ES2022 boundary. Every other spelling lands here too: TypeScript 7 removed
2333
+ // `ES5` and dropped `ES3` entirely, and its only remaining targets are
2334
+ // year-numbered ones, so nothing that reaches emit derives CommonJS.
2335
+ return "es2022";
2336
+ }
2337
+ if (year >= 2022) return "es2022";
2338
+ if (year >= 2020) return "es2020";
2339
+ return "es2015";
2340
+ }
2341
+
2342
+ /**
2343
+ * Year of an `ES<year>` script target, with `es6` normalized to its `es2015`
2344
+ * synonym. Returns `null` when the spelling is not a year-numbered target,
2345
+ * which the caller resolves to the modern default.
2346
+ */
2347
+ function scriptTargetYear(target: string): number | null {
2348
+ if (target === "es6") {
2349
+ return 2015;
2350
+ }
2351
+ const match = /^es(\d{4})$/.exec(target);
2352
+ return match === null ? null : Number(match[1]);
2353
+ }
2354
+
2088
2355
  /** Package-type cache keyed by directory, mirroring Node's own lookup walk. */
2089
2356
  const packageTypeCache = new Map<string, "module" | "commonjs">();
2090
2357
 
@@ -2135,6 +2402,42 @@ function readPackageType(directory: string): "module" | "commonjs" | null {
2135
2402
  }
2136
2403
  }
2137
2404
 
2405
+ /**
2406
+ * The `"type"` the nearest `package.json` states outright, or `null` when the
2407
+ * nearest manifest omits it (or there is none).
2408
+ *
2409
+ * `nearestPackageType` answers "what format would Node use", which defaults a
2410
+ * silent manifest to CommonJS. This answers the narrower question "did a
2411
+ * package actually say", which is what an override has to be built on: a
2412
+ * manifest that says nothing must not out-vote the compiling project's own
2413
+ * `module` option.
2414
+ */
2415
+ function declaredPackageType(filename: string): "module" | "commonjs" | null {
2416
+ let directory = path.dirname(filename);
2417
+ while (true) {
2418
+ const manifestPath = path.join(directory, "package.json");
2419
+ if (isFile(manifestPath)) {
2420
+ try {
2421
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as {
2422
+ type?: unknown;
2423
+ };
2424
+ // The walk stops at the first manifest either way, exactly as Node's
2425
+ // package-scope lookup does; only the answer differs.
2426
+ return parsed.type === "module" || parsed.type === "commonjs"
2427
+ ? parsed.type
2428
+ : null;
2429
+ } catch {
2430
+ return null;
2431
+ }
2432
+ }
2433
+ const parent = path.dirname(directory);
2434
+ if (parent === directory) {
2435
+ return null;
2436
+ }
2437
+ directory = parent;
2438
+ }
2439
+ }
2440
+
2138
2441
  function isRelativeSpecifier(specifier: string): boolean {
2139
2442
  return (
2140
2443
  specifier === "." ||