ttsc 0.24.0 → 0.26.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 (63) hide show
  1. package/README.md +70 -63
  2. package/cmd/ttscgraph/serve_answers_a_project_that_implements_its_own_ambient_declaration_test.go +69 -0
  3. package/internal/graph/assigned_implementations_of_declaration_file_symbols_leave_no_edge_source_test.go +84 -0
  4. package/internal/graph/assigned_implementations_of_node_modules_symbols_leave_no_edge_source_test.go +85 -0
  5. package/internal/graph/edges.go +15 -1
  6. package/internal/lspserver/lsp_native_plugin_source.go +5 -6
  7. package/internal/lspserver/lsp_native_resident.go +17 -24
  8. package/lib/TtscService.d.ts +1 -6
  9. package/lib/TtscService.js +2 -2
  10. package/lib/TtscService.js.map +1 -1
  11. package/lib/compiler/internal/captureProcessOutput.d.ts +26 -0
  12. package/lib/compiler/internal/captureProcessOutput.js +92 -0
  13. package/lib/compiler/internal/captureProcessOutput.js.map +1 -0
  14. package/lib/compiler/internal/compileProjectInMemory.js +4 -7
  15. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  16. package/lib/compiler/internal/residentCheckProcess.d.ts +0 -2
  17. package/lib/compiler/internal/residentCheckProcess.js +1 -15
  18. package/lib/compiler/internal/residentCheckProcess.js.map +1 -1
  19. package/lib/compiler/internal/residentTransformProcess.d.ts +0 -6
  20. package/lib/compiler/internal/residentTransformProcess.js +0 -25
  21. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  22. package/lib/compiler/internal/spawnNative.d.ts +14 -6
  23. package/lib/compiler/internal/spawnNative.js +32 -11
  24. package/lib/compiler/internal/spawnNative.js.map +1 -1
  25. package/lib/compiler/internal/startResidentTransform.d.ts +1 -5
  26. package/lib/compiler/internal/startResidentTransform.js +1 -3
  27. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  28. package/lib/launcher/internal/prepareExecution.d.ts +2 -1
  29. package/lib/launcher/internal/prepareExecution.js +269 -23
  30. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  31. package/lib/launcher/internal/registerRuntimeHooks.js +6 -1
  32. package/lib/launcher/internal/registerRuntimeHooks.js.map +1 -1
  33. package/lib/launcher/internal/runTtscserver.js +4 -9
  34. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  35. package/lib/launcher/internal/runTtsx.js +2 -55
  36. package/lib/launcher/internal/runTtsx.js.map +1 -1
  37. package/lib/launcher/internal/runtimeHooks.d.ts +25 -2
  38. package/lib/launcher/internal/runtimeHooks.js +344 -69
  39. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  40. package/lib/plugin/internal/buildSourcePlugin.js +31 -3
  41. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  42. package/lib/plugin/internal/descriptorProcessFailure.d.ts +23 -19
  43. package/lib/plugin/internal/descriptorProcessFailure.js +40 -51
  44. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -1
  45. package/lib/plugin/internal/loadProjectPlugins.d.ts +12 -0
  46. package/lib/plugin/internal/loadProjectPlugins.js +77 -28
  47. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  48. package/package.json +8 -8
  49. package/src/TtscService.ts +8 -20
  50. package/src/compiler/internal/captureProcessOutput.ts +96 -0
  51. package/src/compiler/internal/compileProjectInMemory.ts +7 -7
  52. package/src/compiler/internal/residentCheckProcess.ts +1 -21
  53. package/src/compiler/internal/residentTransformProcess.ts +0 -42
  54. package/src/compiler/internal/spawnNative.ts +39 -18
  55. package/src/compiler/internal/startResidentTransform.ts +1 -12
  56. package/src/launcher/internal/prepareExecution.ts +297 -26
  57. package/src/launcher/internal/registerRuntimeHooks.ts +11 -2
  58. package/src/launcher/internal/runTtscserver.ts +4 -9
  59. package/src/launcher/internal/runTtsx.ts +2 -78
  60. package/src/launcher/internal/runtimeHooks.ts +385 -82
  61. package/src/plugin/internal/buildSourcePlugin.ts +36 -3
  62. package/src/plugin/internal/descriptorProcessFailure.ts +34 -65
  63. package/src/plugin/internal/loadProjectPlugins.ts +78 -39
@@ -8,12 +8,14 @@ exports.checkNodeRuntimeSupport = checkNodeRuntimeSupport;
8
8
  exports.installRuntimeHooks = installRuntimeHooks;
9
9
  exports.entryModuleFormat = entryModuleFormat;
10
10
  exports.restoreStrippedNodeBuiltinScheme = restoreStrippedNodeBuiltinScheme;
11
+ exports.projectModuleOptions = projectModuleOptions;
11
12
  exports.isWithin = isWithin;
12
13
  exports.readDependencyCache = readDependencyCache;
13
14
  exports.acquireDependencyBuildLock = acquireDependencyBuildLock;
14
15
  exports.releaseDependencyBuildLock = releaseDependencyBuildLock;
15
16
  exports.reclaimDependencyBuildLock = reclaimDependencyBuildLock;
16
17
  exports.inspectDependencyBuildLock = inspectDependencyBuildLock;
18
+ exports.realPath = realPath;
17
19
  const node_crypto_1 = __importDefault(require("node:crypto"));
18
20
  const node_fs_1 = __importDefault(require("node:fs"));
19
21
  const node_module_1 = require("node:module");
@@ -197,12 +199,13 @@ function installCommonJsHook() {
197
199
  }
198
200
  /**
199
201
  * The module format of the entry source file, derived from the entry project's
200
- * `module` option (via the runtime manifest) the same way the served files are
202
+ * compiler options (via the runtime manifest) the same way the served files are
201
203
  * classified. The bootstrap uses it to load the entry through a CommonJS
202
204
  * `require` or an ESM `import`.
203
205
  */
204
206
  function entryModuleFormat(entryFile) {
205
- return moduleFormat(entryFile, manifest()?.moduleOption) === "module"
207
+ const m = manifest();
208
+ return moduleFormat(entryFile, m === null ? null : (m.moduleOptions ?? {})) === "module"
206
209
  ? "module"
207
210
  : "commonjs";
208
211
  }
@@ -261,7 +264,7 @@ function load(url, context, nextLoad) {
261
264
  }
262
265
  function resolveRuntimeSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href) {
263
266
  const served = resolveServedSource(filename, url);
264
- const format = moduleFormat(filename, served.moduleOption);
267
+ const format = moduleFormat(filename, served.moduleOptions);
265
268
  return {
266
269
  format,
267
270
  source: format === "commonjs" && commonJsNamedInteropUrls.has(url)
@@ -275,6 +278,17 @@ function rememberCommonJsNamedInterop(result, context) {
275
278
  }
276
279
  return result;
277
280
  }
281
+ /**
282
+ * Whether a CommonJS-classified TypeScript source reached from an ESM parent
283
+ * needs its nested `export *` names exposed.
284
+ *
285
+ * Only the parent side is decided here. The child's own format is re-checked
286
+ * authoritatively in `resolveRuntimeSource` against the format its served
287
+ * source actually carries, so this predicate deliberately does not repeat that
288
+ * check: doing so would need the child's owning project, which is not known
289
+ * until the source is served, and an answer guessed from the nearest tsconfig
290
+ * silently under-exposes a file that tsconfig does not compile.
291
+ */
278
292
  function shouldExposeCommonJsNamedExports(url, parentURL) {
279
293
  if (parentURL === undefined ||
280
294
  !url.startsWith("file:") ||
@@ -282,45 +296,67 @@ function shouldExposeCommonJsNamedExports(url, parentURL) {
282
296
  return false;
283
297
  }
284
298
  const parentFile = (0, node_url_1.fileURLToPath)(parentURL);
285
- if (moduleFormat(parentFile, moduleOptionForSource(parentFile)) !== "module") {
299
+ if (moduleFormat(parentFile, owningModuleOptions(parentFile)) !== "module") {
286
300
  return false;
287
301
  }
288
- const filename = (0, node_url_1.fileURLToPath)(url);
289
- return (isTypeScriptSource(filename) &&
290
- moduleFormat(filename, moduleOptionForSource(filename)) === "commonjs");
302
+ return isTypeScriptSource((0, node_url_1.fileURLToPath)(url));
291
303
  }
292
- function moduleOptionForSource(filename) {
304
+ /**
305
+ * The emit-deciding compiler options of the project that owns `filename`, or
306
+ * `null` when none does.
307
+ *
308
+ * The entry project owns a file only when it actually emitted it. Testing
309
+ * `isWithin(rootDir)` alone would claim every file under a wide `rootDir` —
310
+ * including the volume-root `rootDir` a config-loader project uses — and hand
311
+ * them the entry project's options even though the dependency or orphan lane is
312
+ * what serves them.
313
+ */
314
+ function owningModuleOptions(filename) {
293
315
  if (!isTypeScriptSource(filename)) {
294
- return undefined;
316
+ return null;
295
317
  }
296
318
  const real = realPath(filename);
297
319
  const m = manifest();
298
- if (m !== null && isWithin(real, m.rootDir)) {
299
- return m.moduleOption;
320
+ if (m !== null && entryEmitPath(m, real) !== null) {
321
+ return m.moduleOptions ?? {};
300
322
  }
301
323
  const tsconfig = nearestTsconfig(real);
302
324
  if (tsconfig === null) {
303
- return undefined;
325
+ return null;
304
326
  }
305
- if (moduleOptionCache.has(tsconfig)) {
306
- return moduleOptionCache.get(tsconfig);
327
+ const cached = moduleOptionsCache.get(tsconfig);
328
+ if (cached !== undefined) {
329
+ return cached;
307
330
  }
308
- let moduleOption;
331
+ let options = {};
309
332
  try {
310
333
  const project = (0, readProjectConfig_1.readProjectConfig)({
311
334
  cwd: node_path_1.default.dirname(tsconfig),
312
335
  tsconfig,
313
336
  });
314
- moduleOption =
315
- typeof project.compilerOptions.module === "string"
316
- ? project.compilerOptions.module
317
- : undefined;
337
+ options = projectModuleOptions(project.compilerOptions);
318
338
  }
319
339
  catch {
320
- moduleOption = undefined;
340
+ // The owning project cannot be read, so nothing is known about the format
341
+ // it would have emitted. That is the same state as having no project at
342
+ // all, and it is what the dependency lane will conclude too when its build
343
+ // fails and the file falls through to the orphan type-strip.
344
+ moduleOptionsCache.set(tsconfig, null);
345
+ return null;
321
346
  }
322
- moduleOptionCache.set(tsconfig, moduleOption);
323
- return moduleOption;
347
+ moduleOptionsCache.set(tsconfig, options);
348
+ return options;
349
+ }
350
+ /** Narrow a resolved project's compiler options to the emit-format pair. */
351
+ function projectModuleOptions(compilerOptions) {
352
+ return {
353
+ ...(typeof compilerOptions.module === "string"
354
+ ? { module: compilerOptions.module }
355
+ : {}),
356
+ ...(typeof compilerOptions.target === "string"
357
+ ? { target: compilerOptions.target }
358
+ : {}),
359
+ };
324
360
  }
325
361
  /**
326
362
  * Resolve the JavaScript to run for a TypeScript source file, in priority
@@ -333,17 +369,14 @@ function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filen
333
369
  const real = realPath(filename);
334
370
  const served = serveEntryEmit(real);
335
371
  if (served !== null) {
336
- return withInlineSourceMap({
337
- moduleOption: manifest()?.moduleOption,
338
- ...served,
339
- });
372
+ return withInlineSourceMap(served);
340
373
  }
341
374
  const built = serveDependencyEmit(real);
342
375
  if (built !== null) {
343
376
  return withInlineSourceMap(built);
344
377
  }
345
378
  return {
346
- moduleOption: undefined,
379
+ moduleOptions: null,
347
380
  sourceFile: filename,
348
381
  source: transformOrphanSource(filename, url),
349
382
  };
@@ -376,7 +409,7 @@ function withInlineSourceMap(served) {
376
409
  * owning project; an ESM-format orphan keeps the fast in-process strip.
377
410
  */
378
411
  function transformOrphanSource(filename, url) {
379
- if (moduleFormat(filename, undefined) === "commonjs") {
412
+ if (moduleFormat(filename, null) === "commonjs") {
380
413
  const lowered = emitOrphanAsCommonJs(filename);
381
414
  if (lowered !== null) {
382
415
  return lowered;
@@ -908,23 +941,49 @@ function serveEntryEmit(real) {
908
941
  if (m === null) {
909
942
  return null;
910
943
  }
911
- if (!isWithin(real, m.rootDir)) {
912
- return null;
913
- }
914
- const emitted = (0, resolveEmittedJavaScript_1.resolveEmittedJavaScript)({
915
- emittedFiles: m.emittedFiles,
916
- outDir: m.emitDir,
917
- projectRoot: m.rootDir,
918
- sourceFile: real,
919
- });
944
+ const emitted = entryEmitPath(m, real);
920
945
  if (emitted === null) {
921
946
  return null;
922
947
  }
923
948
  const source = readFileOrNull(emitted);
924
949
  return source === null
925
950
  ? null
926
- : { emittedFile: emitted, source, sourceFile: real };
951
+ : {
952
+ emittedFile: emitted,
953
+ moduleOptions: m.moduleOptions ?? {},
954
+ source,
955
+ sourceFile: real,
956
+ };
957
+ }
958
+ /**
959
+ * The entry project's emitted JavaScript for `real`, or `null` when that
960
+ * project did not emit it. Shared with `owningModuleOptions` so "the entry
961
+ * project owns this file" means exactly one thing in both places.
962
+ */
963
+ function entryEmitPath(m, real) {
964
+ const cached = entryEmitPathCache.get(real);
965
+ if (cached !== undefined) {
966
+ return cached;
967
+ }
968
+ const resolved = isWithin(real, m.rootDir)
969
+ ? (0, resolveEmittedJavaScript_1.resolveEmittedJavaScript)({
970
+ emittedFiles: m.emittedFiles,
971
+ outDir: m.emitDir,
972
+ projectRoot: m.rootDir,
973
+ sourceFile: real,
974
+ })
975
+ : null;
976
+ entryEmitPathCache.set(real, resolved);
977
+ return resolved;
927
978
  }
979
+ /**
980
+ * Memo for `entryEmitPath`, because it is now on the `resolve` hook's path
981
+ * through `owningModuleOptions` — once per import specifier — and a miss inside
982
+ * `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.
985
+ */
986
+ const entryEmitPathCache = new Map();
928
987
  /**
929
988
  * True when `real` is `directory` itself or sits beneath it. Handles a root
930
989
  * `directory` (`/`, `C:\`): naively appending a separator would yield `//`,
@@ -982,7 +1041,7 @@ function serveBuiltDependency(built, real) {
982
1041
  ? null
983
1042
  : {
984
1043
  emittedFile: emitted,
985
- moduleOption: built.moduleOption,
1044
+ moduleOptions: built.moduleOptions,
986
1045
  source,
987
1046
  sourceFile: real,
988
1047
  };
@@ -1063,7 +1122,15 @@ function readDependencyCache(cacheDir, metaPath) {
1063
1122
  return null;
1064
1123
  }
1065
1124
  if (!isDependencyGeneration(meta.generation) ||
1066
- typeof meta.rootDir !== "string") {
1125
+ typeof meta.rootDir !== "string" ||
1126
+ // A marker with no `moduleOptions` object predates this field and cannot
1127
+ // say which format its emit carries. Treating the absence as "no options"
1128
+ // would classify a CommonJS emit as an ES module, so the generation is
1129
+ // rejected and rebuilt instead. Every marker this version writes carries
1130
+ // the object, empty or not.
1131
+ typeof meta.moduleOptions !== "object" ||
1132
+ meta.moduleOptions === null ||
1133
+ Array.isArray(meta.moduleOptions)) {
1067
1134
  return null;
1068
1135
  }
1069
1136
  const emitDir = dependencyGenerationDir(cacheDir, meta.generation);
@@ -1073,8 +1140,16 @@ function readDependencyCache(cacheDir, metaPath) {
1073
1140
  return {
1074
1141
  emitDir,
1075
1142
  emittedFiles: undefined,
1076
- moduleOption: typeof meta.moduleOption === "string" ? meta.moduleOption : undefined,
1077
- rootDir: meta.rootDir,
1143
+ moduleOptions: projectModuleOptions(meta.moduleOptions),
1144
+ // Resolved on the way out, not trusted as written. `rootDir` never gated
1145
+ // reuse — the marker's generation, module options, and a non-empty emit do
1146
+ // — so a marker carrying an unresolved spelling was already being reused,
1147
+ // and then served every file of that dependency through the whole-tree stem
1148
+ // rescan. The pass is idempotent and runs only on a hit, which
1149
+ // `ensureProjectBuilt` memoizes per tsconfig. A marker from an earlier ttsc
1150
+ // survives only under the shared `os.tmpdir()/ttsx-dep` fallback root; the
1151
+ // manifest's own `depCacheDir` is per-process and removed with the run.
1152
+ rootDir: resolvePhysicalPath(meta.rootDir),
1078
1153
  };
1079
1154
  }
1080
1155
  /**
@@ -1194,14 +1269,10 @@ function buildDependency(tsconfig, cacheDir, metaPath) {
1194
1269
  .filter((line) => line.trim().length !== 0)
1195
1270
  .join("\n"));
1196
1271
  }
1197
- const rootDir = typeof project.compilerOptions.rootDir === "string"
1198
- ? project.compilerOptions.rootDir
1199
- : project.root;
1200
- const moduleOption = typeof project.compilerOptions.module === "string"
1201
- ? project.compilerOptions.module
1202
- : undefined;
1203
- publishDependencyMeta(metaPath, { generation, moduleOption, rootDir });
1204
- return { emitDir, emittedFiles: undefined, moduleOption, rootDir };
1272
+ const rootDir = resolveDependencySourceRoot(project);
1273
+ const moduleOptions = projectModuleOptions(project.compilerOptions);
1274
+ publishDependencyMeta(metaPath, { generation, moduleOptions, rootDir });
1275
+ return { emitDir, emittedFiles: undefined, moduleOptions, rootDir };
1205
1276
  }
1206
1277
  /**
1207
1278
  * Publish the completion marker atomically: write it to a private temp name in
@@ -1512,7 +1583,7 @@ function formatDuration(ms) {
1512
1583
  }
1513
1584
  /** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
1514
1585
  const tsconfigCache = new Map();
1515
- const moduleOptionCache = new Map();
1586
+ const moduleOptionsCache = new Map();
1516
1587
  /**
1517
1588
  * The nearest `tsconfig.json` at or above `file`'s directory, or `null`. The
1518
1589
  * walk stops at a `node_modules` boundary: a tsconfig above `node_modules`
@@ -1565,14 +1636,81 @@ function readFileOrNull(file) {
1565
1636
  return null;
1566
1637
  }
1567
1638
  }
1639
+ /**
1640
+ * The physical path, in the same spelling the launcher decided on.
1641
+ *
1642
+ * `realpathSync.native` first, because the plain implementation resolves
1643
+ * reparse points but leaves a Windows 8.3 component alone — and `TEMP` is
1644
+ * `C:\Users\RUNNER~1\...` on a GitHub Windows runner. The launcher resolves the
1645
+ * entry through `fs.realpathSync.native`, so answering here with the short name
1646
+ * would put a `..` in `path.relative(rootDir, real)`, drop the exact-mirror
1647
+ * lane, and leave the entry to whatever the trailing-stem matcher picks.
1648
+ */
1568
1649
  function realPath(target) {
1569
1650
  try {
1570
- return node_fs_1.default.realpathSync(target);
1651
+ return node_fs_1.default.realpathSync.native?.(target) ?? node_fs_1.default.realpathSync(target);
1571
1652
  }
1572
1653
  catch {
1573
1654
  return target;
1574
1655
  }
1575
1656
  }
1657
+ /**
1658
+ * The physical spelling of a path, produced the way the served sources it will
1659
+ * be compared against are produced.
1660
+ *
1661
+ * `createFilesystemPathIdentityContext` is the same resolver the entry lane
1662
+ * uses (`prepareExecution.ts::resolveRuntimeSourceRoot`). For a path that
1663
+ * exists it is one `realpathSync.native`. For one that does not it resolves as
1664
+ * far as the filesystem goes and folds the missing tail by the case semantics
1665
+ * of the surviving ancestor, which costs a directory read and, on Windows, can
1666
+ * cost an `fsutil` query — worth knowing, but off the path a real dependency
1667
+ * root takes.
1668
+ *
1669
+ * Total on purpose. `throwOnRealpathError: false` silences a failed realpath,
1670
+ * but the case-sensitivity probe can still fail on its own (an unreadable
1671
+ * ancestor, a denied alternate-case `lstat`), and neither caller has anywhere
1672
+ * to put that: `readDependencyCache` is contracted to answer `null` rather than
1673
+ * throw, and `buildDependency` has already produced its emit. The unresolved
1674
+ * spelling is exactly what both had before this pass, so it is the fallback.
1675
+ */
1676
+ function resolvePhysicalPath(location) {
1677
+ try {
1678
+ return (0, projectInputPathIdentity_1.createFilesystemPathIdentityContext)({
1679
+ throwOnRealpathError: false,
1680
+ }).resolve(location).path;
1681
+ }
1682
+ catch {
1683
+ return location;
1684
+ }
1685
+ }
1686
+ /**
1687
+ * The source-tree root a dependency's emit mirrors, in the spelling
1688
+ * `resolveEmittedJavaScript` compares a served source against.
1689
+ *
1690
+ * Both branches need the pass, for different reasons. A declared `rootDir`
1691
+ * arrives from `readProjectConfig` joined against the config that declared it
1692
+ * but never resolved, so a `rootDir` that is itself a symlinked directory stays
1693
+ * unresolved. `project.root` arrives through plain `fs.realpathSync`, which
1694
+ * follows reparse points but leaves a Windows 8.3 component alone — and
1695
+ * {@link realPath} uses `fs.realpathSync.native`, which expands it. Either way
1696
+ * `path.relative` puts a `..` in front of an in-project source,
1697
+ * `resolveExactEmittedFiles` returns nothing, and every served file of that
1698
+ * dependency falls to the trailing-stem matcher, which rescans the whole emit
1699
+ * tree per file because a dependency build publishes no emitted-file list.
1700
+ *
1701
+ * This is the pass the entry lane settled on for the same mixed pair; the two
1702
+ * lanes now read alike, `path.isAbsolute` guard included. `readProjectConfig`
1703
+ * absolutizes every path option against the config that declared it, so that
1704
+ * guard is a mirror of the entry lane rather than a live branch.
1705
+ */
1706
+ function resolveDependencySourceRoot(project) {
1707
+ const rootDir = project.compilerOptions.rootDir;
1708
+ return resolvePhysicalPath(typeof rootDir !== "string"
1709
+ ? project.root
1710
+ : node_path_1.default.isAbsolute(rootDir)
1711
+ ? rootDir
1712
+ : node_path_1.default.resolve(project.root, rootDir));
1713
+ }
1576
1714
  /**
1577
1715
  * Map the JavaScript extension a relative `specifier` carries to the TypeScript
1578
1716
  * source extensions tsgo would have emitted it from. Running from source, a
@@ -1654,35 +1792,138 @@ function probeRescuableSpecifier(specifier, parentURL) {
1654
1792
  }
1655
1793
  /**
1656
1794
  * Decide the module format the way Node and tsgo do — from configuration, never
1657
- * by sniffing the emitted text. The file extension is authoritative
1658
- * (`.mts`/`.mjs` → module, `.cts`/`.cjs` → commonjs); otherwise the owning
1659
- * tsconfig's `module` option decides: a fixed CommonJS / ES target maps
1660
- * directly, while the `node*`/`preserve` family (and a file with no owning
1661
- * tsconfig) defers to the nearest `package.json` `type`, exactly as tsgo chose
1662
- * when it emitted.
1795
+ * by sniffing the emitted text.
1796
+ *
1797
+ * The file extension is authoritative first (`.mts`/`.mjs` → module,
1798
+ * `.cts`/`.cjs` → commonjs), exactly as tsgo's
1799
+ * `getImpliedNodeFormatForEmitWorker` checks it ahead of everything else.
1800
+ *
1801
+ * After that the decision belongs to the project that emitted the file, so
1802
+ * `options` is the whole compiler-option pair tsgo consults, not just `module`:
1803
+ * an absent `module` is NOT "ask Node", it is "derive the kind from `target`"
1804
+ * (tsgo's `getEmitModuleKind`), and TypeScript 7 defaults `target` to the
1805
+ * latest standard, which means ES modules. Only the `node*` family defers to
1806
+ * the nearest `package.json` `type`, because only that family makes tsgo
1807
+ * consult it.
1808
+ *
1809
+ * `options` is `null` for a file no tsconfig owns at all — a raw `.ts` shipped
1810
+ * under `node_modules`. Nothing emitted it, so Node's own rule is the only rule
1811
+ * there is, and the package `type` decides.
1663
1812
  */
1664
- function moduleFormat(filename, moduleOption) {
1813
+ function moduleFormat(filename, options) {
1665
1814
  if (filename.endsWith(".mts") || filename.endsWith(".mjs")) {
1666
1815
  return "module";
1667
1816
  }
1668
1817
  if (filename.endsWith(".cts") || filename.endsWith(".cjs")) {
1669
1818
  return "commonjs";
1670
1819
  }
1671
- const option = (moduleOption ?? "").toLowerCase();
1672
- if (option === "commonjs" || option === "node10" || option === "none") {
1673
- return "commonjs";
1820
+ if (options === null) {
1821
+ return nearestPackageType(filename);
1674
1822
  }
1675
- if (option === "" ||
1676
- option === "node16" ||
1677
- option === "node18" ||
1678
- option === "nodenext" ||
1679
- option === "preserve") {
1823
+ // A package that states its `type` outright decides for the files inside it
1824
+ // whatever the module kind is. This is where a source-shipping dependency's
1825
+ // own declaration overrides the compiling project's `module`; see
1826
+ // `declaredNodeModulesPackageType` for why `node_modules` bounds it.
1827
+ const declared = declaredNodeModulesPackageType(filename);
1828
+ if (declared !== null) {
1829
+ return declared;
1830
+ }
1831
+ const kind = effectiveModuleKind(options);
1832
+ if (kind === "node16" ||
1833
+ kind === "node18" ||
1834
+ kind === "node20" ||
1835
+ kind === "nodenext") {
1680
1836
  return nearestPackageType(filename);
1681
1837
  }
1682
- // Every remaining `module` target (es2015 … esnext, system, amd, umd) emits
1683
- // ECMAScript modules.
1838
+ if (kind === "commonjs") {
1839
+ return "commonjs";
1840
+ }
1841
+ // Everything that remains (es2015 … esnext, and `preserve`, which keeps the
1842
+ // authored ESM syntax verbatim whatever the package `type` says) is emitted
1843
+ // as ECMAScript modules. `amd`, `umd`, and `system` are not among them:
1844
+ // TypeScript 7 removed all three, so a project declaring one cannot build,
1845
+ // and a file it would have owned reaches the run through the orphan lane
1846
+ // instead.
1684
1847
  return "module";
1685
1848
  }
1849
+ /**
1850
+ * The `"type"` a `node_modules` package states for `filename`, or `null`.
1851
+ *
1852
+ * Tsgo's `GetImpliedNodeFormatForEmitWorker` consults a file's
1853
+ * `packageJsonType` for **every** module kind, not just the `node*` family. So
1854
+ * a source-shipping dependency that declares `"type": "commonjs"` is emitted as
1855
+ * CommonJS even while the compiling project asks for `esnext`, and the mirror
1856
+ * has to honour the same override or Node is handed the wrong format.
1857
+ *
1858
+ * Outside `node_modules` that field is empty for every module kind this
1859
+ * override can change. `loadSourceFileMetaData` does fill it elsewhere — for a
1860
+ * file whose extension does not itself state the format (anything but `.mts`,
1861
+ * `.cts`, `.mjs`, `.cjs`) whose project sets `moduleResolution` to the
1862
+ * `node16`…`nodenext` family — but `program.go` rejects that resolution unless
1863
+ * `module` is in the same family, and for a `node*` `module` the package `type`
1864
+ * is already the whole answer, which the caller's own `node*` branch produces.
1865
+ * So answering from a manifest outside `node_modules` could only override the
1866
+ * project's own `module` option, the very confusion this classifier exists to
1867
+ * end.
1868
+ */
1869
+ function declaredNodeModulesPackageType(filename) {
1870
+ if (!pathHasNodeModulesSegment(filename)) {
1871
+ return null;
1872
+ }
1873
+ return declaredPackageType(filename);
1874
+ }
1875
+ /**
1876
+ * Whether any path segment of `filename` is literally `node_modules`.
1877
+ *
1878
+ * Case-sensitive on every platform, because the upstream test this mirrors is a
1879
+ * case-sensitive substring search and Node's own resolver recognises only the
1880
+ * exact spelling. A directory named `Node_Modules` is a package store to
1881
+ * neither of them, and must not become one here.
1882
+ */
1883
+ function pathHasNodeModulesSegment(filename) {
1884
+ return filename.split(/[\\/]/).includes("node_modules");
1885
+ }
1886
+ /**
1887
+ * The `module` kind tsgo actually emits with, mirroring `getEmitModuleKind`:
1888
+ * the declared `module` when there is one, and otherwise the kind implied by
1889
+ * `target`. `"none"` is the spelling of an unset `module`, so it derives the
1890
+ * same way.
1891
+ */
1892
+ function effectiveModuleKind(options) {
1893
+ const declared = (options.module ?? "").toLowerCase();
1894
+ if (declared !== "" && declared !== "none") {
1895
+ return declared;
1896
+ }
1897
+ const target = (options.target ?? "").toLowerCase();
1898
+ if (target === "esnext") {
1899
+ return "esnext";
1900
+ }
1901
+ const year = scriptTargetYear(target);
1902
+ if (year === null) {
1903
+ // An unset `target` is `ScriptTargetLatestStandard` upstream, well above the
1904
+ // ES2022 boundary. Every other spelling lands here too: TypeScript 7 removed
1905
+ // `ES5` and dropped `ES3` entirely, and its only remaining targets are
1906
+ // year-numbered ones, so nothing that reaches emit derives CommonJS.
1907
+ return "es2022";
1908
+ }
1909
+ if (year >= 2022)
1910
+ return "es2022";
1911
+ if (year >= 2020)
1912
+ return "es2020";
1913
+ return "es2015";
1914
+ }
1915
+ /**
1916
+ * Year of an `ES<year>` script target, with `es6` normalized to its `es2015`
1917
+ * synonym. Returns `null` when the spelling is not a year-numbered target,
1918
+ * which the caller resolves to the modern default.
1919
+ */
1920
+ function scriptTargetYear(target) {
1921
+ if (target === "es6") {
1922
+ return 2015;
1923
+ }
1924
+ const match = /^es(\d{4})$/.exec(target);
1925
+ return match === null ? null : Number(match[1]);
1926
+ }
1686
1927
  /** Package-type cache keyed by directory, mirroring Node's own lookup walk. */
1687
1928
  const packageTypeCache = new Map();
1688
1929
  function nearestPackageType(filename) {
@@ -1725,6 +1966,40 @@ function readPackageType(directory) {
1725
1966
  return "commonjs";
1726
1967
  }
1727
1968
  }
1969
+ /**
1970
+ * The `"type"` the nearest `package.json` states outright, or `null` when the
1971
+ * nearest manifest omits it (or there is none).
1972
+ *
1973
+ * `nearestPackageType` answers "what format would Node use", which defaults a
1974
+ * silent manifest to CommonJS. This answers the narrower question "did a
1975
+ * package actually say", which is what an override has to be built on: a
1976
+ * manifest that says nothing must not out-vote the compiling project's own
1977
+ * `module` option.
1978
+ */
1979
+ function declaredPackageType(filename) {
1980
+ let directory = node_path_1.default.dirname(filename);
1981
+ while (true) {
1982
+ const manifestPath = node_path_1.default.join(directory, "package.json");
1983
+ if (isFile(manifestPath)) {
1984
+ try {
1985
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(manifestPath, "utf8"));
1986
+ // The walk stops at the first manifest either way, exactly as Node's
1987
+ // package-scope lookup does; only the answer differs.
1988
+ return parsed.type === "module" || parsed.type === "commonjs"
1989
+ ? parsed.type
1990
+ : null;
1991
+ }
1992
+ catch {
1993
+ return null;
1994
+ }
1995
+ }
1996
+ const parent = node_path_1.default.dirname(directory);
1997
+ if (parent === directory) {
1998
+ return null;
1999
+ }
2000
+ directory = parent;
2001
+ }
2002
+ }
1728
2003
  function isRelativeSpecifier(specifier) {
1729
2004
  return (specifier === "." ||
1730
2005
  specifier === ".." ||