ttsc 0.28.6 → 0.30.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.
@@ -31,7 +31,25 @@ const runBuild_1 = require("../../compiler/internal/runBuild");
31
31
  const spawnNative_1 = require("../../compiler/internal/spawnNative");
32
32
  const createCanonicalTempDirectory_1 = require("../../internal/createCanonicalTempDirectory");
33
33
  const projectInputPathIdentity_1 = require("../../internal/projectInputPathIdentity");
34
+ const commonJsExportMetadata_1 = require("./commonJsExportMetadata");
35
+ const runtimeCompilerArgs_1 = require("./runtimeCompilerArgs");
34
36
  const servedSourceMap_1 = require("./servedSourceMap");
37
+ /**
38
+ * One emit policy for orphan execution and CommonJS export discovery. The
39
+ * compiler ignores the consumer's config, lowers proposal syntax, and checks no
40
+ * types because the entry build owns diagnostics. Isolation prevents imported
41
+ * const-enum inlining and secondary emits, and keeps const enums as runtime
42
+ * exports that the name scanner must also observe.
43
+ */
44
+ const ISOLATED_EMIT_ARGS = [
45
+ "--ignoreConfig",
46
+ "--target",
47
+ "es2022",
48
+ "--noCheck",
49
+ "--skipLibCheck",
50
+ "--noResolve",
51
+ "--isolatedModules",
52
+ ];
35
53
  /**
36
54
  * Synchronous Node module hooks installed (via `module.registerHooks`) in the
37
55
  * child process `ttsx` spawns to run a TypeScript entry _from source_.
@@ -50,11 +68,9 @@ const servedSourceMap_1 = require("./servedSourceMap");
50
68
  * type-stripping cannot do cross-file type-only elision — e.g. a
51
69
  * value-shaped import of a type+namespace merge survives stripping and
52
70
  * dangles at runtime.
53
- * 3. No owning tsconfig → transform the lone file by the format it resolves to: a
54
- * CommonJS-classified file (`.cts`, or a `.ts` in a package without `type:
55
- * "module"`) is lowered to CommonJS through a tsgo single-file emit so its
56
- * `export` syntax becomes `module.exports`; any other (ESM) file keeps the
57
- * fast in-process `mode: "transform"` type-strip.
71
+ * 3. No owning tsconfig: use an isolated tsgo emit in the runtime module format,
72
+ * lowering standard decorators and CommonJS imports/exports. Type stripping
73
+ * remains recovery when no compiler emit is available.
58
74
  *
59
75
  * The hooks are synchronous and run on the main thread (not a loader worker):
60
76
  * that is what lets a CommonJS `require("./x")` chain reach them and what makes
@@ -793,7 +809,7 @@ function owningModuleOptions(filename) {
793
809
  // The owning project cannot be read, so nothing is known about the format
794
810
  // it would have emitted. That is the same state as having no project at
795
811
  // all, and it is what the dependency lane will conclude too when its build
796
- // fails and the file falls through to the orphan type-strip.
812
+ // fails and the file falls through to the isolated orphan emit.
797
813
  moduleOptionsCache.set(tsconfig, null);
798
814
  return null;
799
815
  }
@@ -916,9 +932,8 @@ function projectModuleOptions(compilerOptions) {
916
932
  /**
917
933
  * Resolve the JavaScript to run for a TypeScript source file, in priority
918
934
  * order: the entry project's pre-built emit (transform plugins applied), a
919
- * built raw `.ts` dependency, or — when no tsconfig owns it — a `mode:
920
- * "transform"` type-strip. Shared by the ESM `load` hook and the CommonJS
921
- * `require` handler.
935
+ * built raw `.ts` dependency, or an isolated emit when no tsconfig owns it.
936
+ * Shared by the ESM `load` hook and the CommonJS `require` handler.
922
937
  */
923
938
  function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filename).href, prepareAsEntry = false) {
924
939
  const real = realPath(filename);
@@ -960,7 +975,8 @@ function resolveServedSource(filename, url = (0, node_url_1.pathToFileURL)(filen
960
975
  * map's `sources`, so the JavaScript executed under the `.ts` source URL stays
961
976
  * self-describing after the per-run emit directory is deleted. Applied to both
962
977
  * the entry lane (`serveEntryEmit`) and the dependency lane
963
- * (`serveBuiltDependency`); the orphan type-strip lane carries no emitted map.
978
+ * (`serveBuiltDependency`); the orphan lane inlines its own map before
979
+ * caching.
964
980
  */
965
981
  function withInlineSourceMap(served) {
966
982
  const source = (0, servedSourceMap_1.inlineServedSourceMap)(served.source, served.emittedFile, served.sourceFile);
@@ -971,23 +987,16 @@ function withInlineSourceMap(served) {
971
987
  * vendored package that ships raw `.ts`/`.cts`/`.mts` straight under
972
988
  * `node_modules`), choosing the lowering by the format the file resolves to.
973
989
  *
974
- * Node's in-process `stripTypeScriptTypes` only erases type syntax; it never
975
- * rewrites ECMAScript `import`/`export` into CommonJS. That is correct for a
976
- * file Node will load as ESM, but wrong for one classified CommonJS — a `.cts`,
977
- * or a `.ts` in a package without `type: "module"` — when the author wrote it
978
- * with module syntax (`export const`, `export namespace`, `export function`).
979
- * Stripping leaves the `export` in place and Node's CommonJS loader dies with
980
- * `SyntaxError: Unexpected token 'export'`. So a CommonJS-format orphan is
981
- * lowered through a real tsgo `--module commonjs` single-file emit (which also
982
- * handles `export =`), exactly the format decision tsgo would have made for an
983
- * owning project; an ESM-format orphan keeps the fast in-process strip.
990
+ * Both module formats need the compiler: Node's type stripping neither lowers
991
+ * standard decorators nor rewrites ESM exports for CommonJS. Use the existing
992
+ * single-file emit with the file's own module format and a standard target.
993
+ * Retain stripping as recovery when no compiler emit is available.
984
994
  */
985
995
  function transformOrphanSource(filename, url) {
986
- if (moduleFormat(filename, null) === "commonjs") {
987
- const lowered = emitOrphanAsCommonJs(filename);
988
- if (lowered !== null) {
989
- return lowered;
990
- }
996
+ const format = moduleFormat(filename, null) === "commonjs" ? "commonjs" : "module";
997
+ const lowered = emitOrphanSource(filename, format);
998
+ if (lowered !== null) {
999
+ return lowered;
991
1000
  }
992
1001
  return (0, node_module_1.stripTypeScriptTypes)(node_fs_1.default.readFileSync(filename, "utf8"), {
993
1002
  mode: "transform",
@@ -995,13 +1004,12 @@ function transformOrphanSource(filename, url) {
995
1004
  });
996
1005
  }
997
1006
  /**
998
- * Lower a single CommonJS-format source file to CommonJS JavaScript by running
999
- * tsgo on the lone file with `--module commonjs`. Emit-only, no diagnostic gate
1000
- * (the entry project's up-front check is the type gate), matching
1001
- * `buildDependency`. Returns `null` when tsgo is unavailable or produced no
1002
- * output, so the caller can fall back to the in-process strip.
1007
+ * Lower a single source file to its runtime module format. Emit-only, no
1008
+ * diagnostic gate (the entry project's up-front check is the type gate),
1009
+ * matching `buildDependency`. Returns `null` when tsgo is unavailable or
1010
+ * produced no output, so the caller can fall back to the in-process strip.
1003
1011
  */
1004
- function emitOrphanAsCommonJs(filename) {
1012
+ function emitOrphanSource(filename, format) {
1005
1013
  let tsgo;
1006
1014
  try {
1007
1015
  tsgo = (0, resolveTsgo_1.resolveTsgo)({ cwd: node_path_1.default.dirname(filename) }).binary;
@@ -1009,12 +1017,12 @@ function emitOrphanAsCommonJs(filename) {
1009
1017
  catch {
1010
1018
  return null;
1011
1019
  }
1012
- // Content-hash cache: a CJS-format orphan ('s tsgo single-file emit) is lowered
1020
+ // Content-hash cache: an orphan's tsgo single-file emit is lowered
1013
1021
  // once and reused by every other process in the run, and across runs. Without
1014
1022
  // it a program that fans out into many processes (the automated test corpus
1015
1023
  // imports the same vendored `.ts` deps from thousands of generated files) would
1016
1024
  // re-spawn tsgo per file per process and crawl.
1017
- const cacheFile = orphanCacheFile(filename, tsgo);
1025
+ const cacheFile = orphanCacheFile(filename, tsgo, format);
1018
1026
  if (cacheFile !== null) {
1019
1027
  const hit = readFileOrNull(cacheFile);
1020
1028
  if (hit !== null) {
@@ -1025,29 +1033,20 @@ function emitOrphanAsCommonJs(filename) {
1025
1033
  try {
1026
1034
  const res = (0, spawnNative_1.spawnNative)(tsgo, [
1027
1035
  filename,
1028
- // The file is named on the command line, so any tsconfig tsgo would
1029
- // discover by walking up (the consumer's own) must be ignored — both
1030
- // because it is not this file's project and because tsgo errors out
1031
- // ("tsconfig.json is present but will not be loaded") otherwise.
1032
- "--ignoreConfig",
1033
1036
  "--module",
1034
- "commonjs",
1035
- "--target",
1036
- "es2022",
1037
- // This is an emit-only lowering: the entry project's up-front build is
1038
- // the type gate, so the single-file pass does not need to type-check.
1039
- // Skipping the check (and the lib check it implies) cuts the per-file
1040
- // cost several-fold, which matters when a program generates and imports
1041
- // thousands of raw `.ts` files at runtime (a fanned-out test corpus) and
1042
- // each one would otherwise pay a full single-file check.
1043
- "--noCheck",
1044
- "--skipLibCheck",
1037
+ format === "commonjs" ? "commonjs" : "esnext",
1038
+ ...ISOLATED_EMIT_ARGS,
1039
+ "--sourceMap",
1040
+ "--inlineSources",
1045
1041
  "--outDir",
1046
1042
  outDir,
1047
1043
  "--listEmittedFiles",
1048
1044
  ], { cwd: node_path_1.default.dirname(filename), encoding: "utf8" });
1049
- const emitted = parseFirstEmittedFile((0, spawnNative_1.outputText)(res.stdout));
1050
- const lowered = emitted === null ? null : readFileOrNull(emitted);
1045
+ const emitted = pickEmittedJavaScript(filename, parseEmittedFiles((0, spawnNative_1.outputText)(res.stdout)));
1046
+ const source = emitted === null ? null : readFileOrNull(emitted);
1047
+ const lowered = source === null
1048
+ ? null
1049
+ : (0, servedSourceMap_1.inlineServedSourceMap)(source, emitted, filename);
1051
1050
  if (lowered !== null && cacheFile !== null) {
1052
1051
  writeOrphanCache(cacheFile, lowered);
1053
1052
  }
@@ -1061,7 +1060,10 @@ function emitOrphanAsCommonJs(filename) {
1061
1060
  }
1062
1061
  }
1063
1062
  /**
1064
- * Emit a source file only for CommonJS export-name discovery.
1063
+ * Read actual owned output for CommonJS export-name discovery, falling back to
1064
+ * isolated emission only when no project emitted this source. Project
1065
+ * transforms and const-enum settings decide which names exist at runtime. An
1066
+ * ESM emit is lowered as JavaScript solely to scan its CommonJS names.
1065
1067
  *
1066
1068
  * This intentionally does not read or write the runtime orphan cache. Name
1067
1069
  * discovery may inspect a source dependency without executing it, so sharing
@@ -1074,6 +1076,12 @@ function emitCommonJsForNameScan(filename) {
1074
1076
  if (cached !== undefined) {
1075
1077
  return cached;
1076
1078
  }
1079
+ const served = serveEntryEmit(real) ?? serveDependencyEmit(real);
1080
+ if (served !== null &&
1081
+ moduleFormat(real, served.moduleOptions) === "commonjs") {
1082
+ commonJsNameScanSources.set(real, served.source);
1083
+ return served.source;
1084
+ }
1077
1085
  let tsgo;
1078
1086
  try {
1079
1087
  tsgo = (0, resolveTsgo_1.resolveTsgo)({ cwd: node_path_1.default.dirname(real) }).binary;
@@ -1084,20 +1092,21 @@ function emitCommonJsForNameScan(filename) {
1084
1092
  }
1085
1093
  const outDir = (0, createCanonicalTempDirectory_1.createCanonicalTempDirectory)("ttsx-export-scan-");
1086
1094
  try {
1095
+ // Compile the owned JavaScript, not the original TypeScript whose project
1096
+ // may already have erased or transformed declarations. No source executes.
1097
+ const input = served === null ? real : node_path_1.default.join(outDir, "source.cts");
1098
+ if (served !== null)
1099
+ node_fs_1.default.writeFileSync(input, served.source);
1087
1100
  const res = (0, spawnNative_1.spawnNative)(tsgo, [
1088
- real,
1089
- "--ignoreConfig",
1101
+ input,
1090
1102
  "--module",
1091
1103
  "commonjs",
1092
- "--target",
1093
- "es2022",
1094
- "--noCheck",
1095
- "--skipLibCheck",
1104
+ ...ISOLATED_EMIT_ARGS,
1096
1105
  "--outDir",
1097
1106
  outDir,
1098
1107
  "--listEmittedFiles",
1099
1108
  ], { cwd: node_path_1.default.dirname(real), encoding: "utf8" });
1100
- const emitted = pickEmittedJavaScript(real, parseEmittedFiles((0, spawnNative_1.outputText)(res.stdout)));
1109
+ const emitted = pickEmittedJavaScript(input, parseEmittedFiles((0, spawnNative_1.outputText)(res.stdout)));
1101
1110
  const lowered = emitted === null ? null : readFileOrNull(emitted);
1102
1111
  commonJsNameScanSources.set(real, lowered);
1103
1112
  return lowered;
@@ -1118,14 +1127,14 @@ function orphanCacheRoot() {
1118
1127
  const base = process.env.TTSC_CACHE_DIR && process.env.TTSC_CACHE_DIR.length !== 0
1119
1128
  ? process.env.TTSC_CACHE_DIR
1120
1129
  : node_path_1.default.join(node_os_1.default.tmpdir(), "ttsc-orphan");
1121
- return node_path_1.default.join(base, "ttsx-orphan-cjs");
1130
+ return node_path_1.default.join(base, "ttsx-orphan");
1122
1131
  }
1123
1132
  /**
1124
- * Content-addressed cache path for one orphan file's CommonJS lowering, keyed
1125
- * by source bytes and the tsgo binary so a tsgo bump invalidates it. `null`
1126
- * when the source cannot be read.
1133
+ * Content-addressed cache path for isolated orphan lowering, including the
1134
+ * source path because the inlined map identifies that path. `null` when the
1135
+ * source cannot be read.
1127
1136
  */
1128
- function orphanCacheFile(filename, tsgo) {
1137
+ function orphanCacheFile(filename, tsgo, format) {
1129
1138
  let source;
1130
1139
  try {
1131
1140
  source = node_fs_1.default.readFileSync(filename);
@@ -1136,6 +1145,9 @@ function orphanCacheFile(filename, tsgo) {
1136
1145
  const key = node_crypto_1.default
1137
1146
  .createHash("sha256")
1138
1147
  .update(tsgo)
1148
+ .update("\0isolated-source-map-v1\0")
1149
+ .update(filename)
1150
+ .update("\0" + format)
1139
1151
  .update("\0")
1140
1152
  .update(source)
1141
1153
  .digest("hex")
@@ -1158,16 +1170,6 @@ function writeOrphanCache(cacheFile, lowered) {
1158
1170
  // ignore — caching is an optimization, correctness does not depend on it
1159
1171
  }
1160
1172
  }
1161
- /** First `TSFILE:` path tsgo printed under `--listEmittedFiles`, or `null`. */
1162
- function parseFirstEmittedFile(stdout) {
1163
- for (const line of stdout.split(/\r?\n/)) {
1164
- const match = line.match(/^TSFILE:\s*(.+)$/);
1165
- if (match?.[1]) {
1166
- return match[1].trim();
1167
- }
1168
- }
1169
- return null;
1170
- }
1171
1173
  /** `TSFILE:` paths tsgo printed under `--listEmittedFiles`. */
1172
1174
  function parseEmittedFiles(stdout) {
1173
1175
  const files = [];
@@ -1202,35 +1204,30 @@ function pickEmittedJavaScript(filename, emittedFiles) {
1202
1204
  * Runtime CommonJS consumers see the getters that helper installs, but Node's
1203
1205
  * ESM linker only exposes named imports it can statically identify from
1204
1206
  * `exports.name = ...` assignments. For relative star re-exports whose emitted
1205
- * target is available, replace the helper call with explicit configurable
1206
- * export placeholders followed by the same `__createBinding` getter install.
1207
+ * target is available, advertise its names through inert assignments that
1208
+ * Node's static lexer recognizes. The original helper still owns every runtime
1209
+ * binding, including values static discovery cannot enumerate.
1207
1210
  */
1208
1211
  function exposeCommonJsStarExports(source, emittedFile, sourceFile) {
1209
- if (!source.includes("__exportStar(")) {
1210
- return source;
1212
+ const parsed = (0, commonJsExportMetadata_1.parseCommonJsExports)(source);
1213
+ const reserved = new Set(parsed.exports);
1214
+ const names = new Set();
1215
+ for (const specifier of parsed.reexports) {
1216
+ for (const name of collectStarExportNames(emittedFile, sourceFile, specifier)) {
1217
+ if (name !== "default" && name !== "__esModule" && !reserved.has(name)) {
1218
+ names.add(name);
1219
+ }
1220
+ }
1211
1221
  }
1212
- const reserved = collectStaticCommonJsExportNames(source);
1213
- let index = 0;
1214
- return source.replace(/^(\s*)__exportStar\(\s*require\((["'])([^"']+)\2\)\s*,\s*exports\s*\);/gm, (statement, indent, _quote, specifier) => {
1215
- const names = [
1216
- ...collectStarExportNames(emittedFile, sourceFile, specifier),
1217
- ].filter((name) => name !== "default" &&
1218
- name !== "__esModule" &&
1219
- isIdentifierName(name) &&
1220
- !reserved.has(name));
1221
- if (names.length === 0) {
1222
- return statement;
1223
- }
1224
- for (const name of names) {
1225
- reserved.add(name);
1226
- }
1227
- const receiver = `__ttsx_export_star_${index++}`;
1228
- return [
1229
- ...names.map((name) => `${indent}exports.${name} = void 0;`),
1230
- `${indent}var ${receiver} = require(${JSON.stringify(specifier)});`,
1231
- ...names.map((name) => `${indent}__createBinding(exports, ${receiver}, ${JSON.stringify(name)});`),
1232
- ].join("\n");
1233
- });
1222
+ if (names.size === 0)
1223
+ return source;
1224
+ // Node's lexer reads these assignments statically. Nothing executes, and no
1225
+ // existing statement, source position, control-flow body or runtime export
1226
+ // is changed. Keep all specifier/literal parsing in the same lexer Node uses.
1227
+ const hints = [...names]
1228
+ .map((name) => `exports[${JSON.stringify(name)}] = void 0;`)
1229
+ .join(" ");
1230
+ return source + `\nif (false) { ${hints} }\n`;
1234
1231
  }
1235
1232
  function collectStarExportNames(emittedFile, sourceFile, specifier) {
1236
1233
  if (emittedFile !== undefined) {
@@ -1257,8 +1254,9 @@ function collectCommonJsExportNames(emittedFile, seen) {
1257
1254
  if (source === null) {
1258
1255
  return new Set();
1259
1256
  }
1260
- const names = collectStaticCommonJsExportNames(source);
1261
- for (const specifier of collectExportStarSpecifiers(source)) {
1257
+ const parsed = (0, commonJsExportMetadata_1.parseCommonJsExports)(source);
1258
+ const names = new Set(parsed.exports);
1259
+ for (const specifier of parsed.reexports) {
1262
1260
  const target = resolveEmittedRequire(real, specifier);
1263
1261
  if (target === null) {
1264
1262
  continue;
@@ -1271,153 +1269,6 @@ function collectCommonJsExportNames(emittedFile, seen) {
1271
1269
  }
1272
1270
  return names;
1273
1271
  }
1274
- function collectStaticCommonJsExportNames(source) {
1275
- // Scan executable syntax only. Text that merely resembles an assignment —
1276
- // `exports.x =` inside a comment, string, or template-literal text — must not
1277
- // become an ESM-visible export name, or a named import of it would link to
1278
- // `undefined` for a property the CommonJS module never defines. Masking the
1279
- // inert lexical spans before matching keeps genuine top-level assignments
1280
- // (and executable `${ ... }` template substitutions) while dropping the
1281
- // decoys.
1282
- const scannable = maskCommentsAndStrings(source);
1283
- const names = new Set();
1284
- const pattern = /(?:^|[^\w$])(?:exports|module\.exports)\.([A-Za-z_$][\w$]*)\s*=/g;
1285
- let match;
1286
- while ((match = pattern.exec(scannable)) !== null) {
1287
- names.add(match[1]);
1288
- }
1289
- return names;
1290
- }
1291
- /**
1292
- * Blank out the interior of line comments, block comments, string literals, and
1293
- * template-literal text in emitted JavaScript, replacing each masked character
1294
- * with a space while preserving newlines, code, and template `${ ... }`
1295
- * substitutions verbatim. Positions and length are preserved so an offset in
1296
- * the masked text maps back to the same offset in the source.
1297
- *
1298
- * The input is tsgo's CommonJS emit (well-formed JavaScript), so a character
1299
- * scanner that tracks the standard comment/string/template states is sufficient
1300
- * to separate executable tokens from inert text. Regular-expression literals
1301
- * are intentionally not masked: distinguishing `/`-division from a regex
1302
- * literal needs full tokenization, and tsgo's CommonJS emit never wraps an
1303
- * `exports.<name> =` assignment inside a regex literal.
1304
- */
1305
- function maskCommentsAndStrings(source) {
1306
- const out = source.split("");
1307
- const n = out.length;
1308
- const blank = (index) => {
1309
- const ch = out[index];
1310
- if (ch !== "\n" && ch !== "\r") {
1311
- out[index] = " ";
1312
- }
1313
- };
1314
- const stack = [{ kind: "code", braceDepth: 0 }];
1315
- let i = 0;
1316
- while (i < n) {
1317
- const top = stack[stack.length - 1];
1318
- const ch = out[i];
1319
- if (top.kind === "template") {
1320
- if (ch === "\\") {
1321
- blank(i);
1322
- blank(i + 1);
1323
- i += 2;
1324
- continue;
1325
- }
1326
- if (ch === "`") {
1327
- blank(i);
1328
- stack.pop();
1329
- i += 1;
1330
- continue;
1331
- }
1332
- if (ch === "$" && out[i + 1] === "{") {
1333
- // Enter a code substitution: `${` and its contents stay executable.
1334
- stack.push({ kind: "code", braceDepth: 0 });
1335
- i += 2;
1336
- continue;
1337
- }
1338
- blank(i);
1339
- i += 1;
1340
- continue;
1341
- }
1342
- // Code context.
1343
- if (ch === "/" && out[i + 1] === "/") {
1344
- blank(i);
1345
- blank(i + 1);
1346
- i += 2;
1347
- while (i < n && out[i] !== "\n") {
1348
- blank(i);
1349
- i += 1;
1350
- }
1351
- continue;
1352
- }
1353
- if (ch === "/" && out[i + 1] === "*") {
1354
- blank(i);
1355
- blank(i + 1);
1356
- i += 2;
1357
- while (i < n && !(out[i] === "*" && out[i + 1] === "/")) {
1358
- blank(i);
1359
- i += 1;
1360
- }
1361
- if (i < n) {
1362
- blank(i);
1363
- blank(i + 1);
1364
- i += 2;
1365
- }
1366
- continue;
1367
- }
1368
- if (ch === '"' || ch === "'") {
1369
- blank(i);
1370
- i += 1;
1371
- while (i < n && out[i] !== ch) {
1372
- if (out[i] === "\\") {
1373
- blank(i);
1374
- blank(i + 1);
1375
- i += 2;
1376
- continue;
1377
- }
1378
- // A bare newline ends an unterminated string; stop masking so the rest
1379
- // of the line is still scanned as code (defensive — tsgo never emits
1380
- // one).
1381
- if (out[i] === "\n") {
1382
- break;
1383
- }
1384
- blank(i);
1385
- i += 1;
1386
- }
1387
- if (i < n && out[i] === ch) {
1388
- blank(i);
1389
- i += 1;
1390
- }
1391
- continue;
1392
- }
1393
- if (ch === "`") {
1394
- blank(i);
1395
- stack.push({ kind: "template", braceDepth: 0 });
1396
- i += 1;
1397
- continue;
1398
- }
1399
- if (ch === "{") {
1400
- top.braceDepth += 1;
1401
- i += 1;
1402
- continue;
1403
- }
1404
- if (ch === "}") {
1405
- if (top.braceDepth === 0 && stack.length > 1) {
1406
- // Close the enclosing template `${ ... }` and resume template text.
1407
- stack.pop();
1408
- i += 1;
1409
- continue;
1410
- }
1411
- if (top.braceDepth > 0) {
1412
- top.braceDepth -= 1;
1413
- }
1414
- i += 1;
1415
- continue;
1416
- }
1417
- i += 1;
1418
- }
1419
- return out.join("");
1420
- }
1421
1272
  function collectSourceCommonJsExportNames(sourceFile, seen) {
1422
1273
  const real = realPath(sourceFile);
1423
1274
  if (seen.has(real)) {
@@ -1428,8 +1279,9 @@ function collectSourceCommonJsExportNames(sourceFile, seen) {
1428
1279
  if (source === null) {
1429
1280
  return new Set();
1430
1281
  }
1431
- const names = collectStaticCommonJsExportNames(source);
1432
- for (const specifier of collectExportStarSpecifiers(source)) {
1282
+ const parsed = (0, commonJsExportMetadata_1.parseCommonJsExports)(source);
1283
+ const names = new Set(parsed.exports);
1284
+ for (const specifier of parsed.reexports) {
1433
1285
  const target = resolveSourceSpecifier(real, specifier);
1434
1286
  if (target === null) {
1435
1287
  continue;
@@ -1442,15 +1294,6 @@ function collectSourceCommonJsExportNames(sourceFile, seen) {
1442
1294
  }
1443
1295
  return names;
1444
1296
  }
1445
- function collectExportStarSpecifiers(source) {
1446
- const specifiers = [];
1447
- const pattern = /^(\s*)__exportStar\(\s*require\((["'])([^"']+)\2\)\s*,\s*exports\s*\);/gm;
1448
- let match;
1449
- while ((match = pattern.exec(source)) !== null) {
1450
- specifiers.push(match[3]);
1451
- }
1452
- return specifiers;
1453
- }
1454
1297
  function resolveEmittedRequire(emittedFile, specifier) {
1455
1298
  if (!isRelativeSpecifier(specifier)) {
1456
1299
  return null;
@@ -1479,7 +1322,9 @@ function resolveSourceSpecifier(sourceFile, specifier) {
1479
1322
  }
1480
1323
  const base = node_path_1.default.resolve(node_path_1.default.dirname(sourceFile), specifier);
1481
1324
  if (node_path_1.default.extname(base).length !== 0) {
1482
- return isFile(base) ? base : null;
1325
+ if (isFile(base))
1326
+ return base;
1327
+ return typescriptSourcesForJavaScriptSpecifier(base).find(isFile) ?? null;
1483
1328
  }
1484
1329
  for (const extension of TYPESCRIPT_EXTENSIONS) {
1485
1330
  const candidate = base + extension;
@@ -1495,9 +1340,6 @@ function resolveSourceSpecifier(sourceFile, specifier) {
1495
1340
  }
1496
1341
  return null;
1497
1342
  }
1498
- function isIdentifierName(name) {
1499
- return /^[A-Za-z_$][\w$]*$/.test(name);
1500
- }
1501
1343
  /**
1502
1344
  * Serve the entry project's pre-built JavaScript for a source file the build
1503
1345
  * emitted, or `null` when the file is outside the build or its emit is
@@ -1629,7 +1471,7 @@ function serveDependencyEmit(real) {
1629
1471
  built = ensureProjectBuilt(tsconfig);
1630
1472
  }
1631
1473
  catch {
1632
- // The owning project produced no emit at all; fall back to type-stripping
1474
+ // The owning project produced no emit at all; fall back to isolated emit of
1633
1475
  // this single file rather than failing the whole run.
1634
1476
  return null;
1635
1477
  }
@@ -1703,6 +1545,7 @@ function dependencyCacheKey(tsconfig, options = {}) {
1703
1545
  return (node_crypto_1.default
1704
1546
  .createHash("sha256")
1705
1547
  .update(tsconfig)
1548
+ .update("\0runtime-es2025")
1706
1549
  // Descriptor evaluation promises a result bound to this process's exact
1707
1550
  // input observations. Reusing an emit another evaluator built can pair
1708
1551
  // that process's old source/config bytes with this process's later hashes.
@@ -1850,6 +1693,7 @@ function buildDependency(tsconfig, cacheDir, metaPath) {
1850
1693
  node_fs_1.default.mkdirSync(emitDir, { recursive: true });
1851
1694
  const result = (0, runBuild_1.runBuild)({
1852
1695
  cwd: project.root,
1696
+ passthrough: (0, runtimeCompilerArgs_1.runtimeCompilerArgs)(project),
1853
1697
  emit: true,
1854
1698
  forceListEmittedFiles: true,
1855
1699
  outDir: emitDir,
@@ -1891,7 +1735,7 @@ function buildDependency(tsconfig, cacheDir, metaPath) {
1891
1735
  // list": a native transform host (typia, @ttsc/banner, …) emits without
1892
1736
  // printing the `--listEmittedFiles` lines, so `result.emittedFiles` is empty
1893
1737
  // even on a clean build. A genuinely empty output directory is the real
1894
- // failure; the caller then falls back to type-stripping the one file.
1738
+ // failure; the caller then falls back to isolated emit of the one file.
1895
1739
  if (!emittedAnything(emitDir)) {
1896
1740
  // Drop the failed generation so its partial directory can never be mistaken
1897
1741
  // for a reusable build.
@@ -2231,8 +2075,8 @@ const moduleOptionsCache = new Map();
2231
2075
  * walk stops at a `node_modules` boundary: a tsconfig above `node_modules`
2232
2076
  * belongs to the consumer, not to the published dependency inside it, so a
2233
2077
  * dependency that ships no tsconfig of its own has no owning project and is
2234
- * type-stripped instead. A pnpm-symlinked workspace package is unaffected
2235
- * because `file` is already its real path (outside `node_modules`).
2078
+ * compiled in isolation instead. A pnpm-symlinked workspace package is
2079
+ * unaffected because `file` is already its real path (outside `node_modules`).
2236
2080
  *
2237
2081
  * The walk is memoised per directory (the whole walked chain shares one
2238
2082
  * answer), so the thousands of files a fanned-out test corpus imports from the