ttsc 0.29.0 → 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.
@@ -21,8 +21,27 @@ import {
21
21
  type FilesystemPathIdentityContext,
22
22
  createFilesystemPathIdentityContext,
23
23
  } from "../../internal/projectInputPathIdentity";
24
+ import { parseCommonJsExports } from "./commonJsExportMetadata";
25
+ import { runtimeCompilerArgs } from "./runtimeCompilerArgs";
24
26
  import { inlineServedSourceMap } from "./servedSourceMap";
25
27
 
28
+ /**
29
+ * One emit policy for orphan execution and CommonJS export discovery. The
30
+ * compiler ignores the consumer's config, lowers proposal syntax, and checks no
31
+ * types because the entry build owns diagnostics. Isolation prevents imported
32
+ * const-enum inlining and secondary emits, and keeps const enums as runtime
33
+ * exports that the name scanner must also observe.
34
+ */
35
+ const ISOLATED_EMIT_ARGS = [
36
+ "--ignoreConfig",
37
+ "--target",
38
+ "es2022",
39
+ "--noCheck",
40
+ "--skipLibCheck",
41
+ "--noResolve",
42
+ "--isolatedModules",
43
+ ] as const;
44
+
26
45
  /**
27
46
  * Synchronous Node module hooks installed (via `module.registerHooks`) in the
28
47
  * child process `ttsx` spawns to run a TypeScript entry _from source_.
@@ -41,11 +60,9 @@ import { inlineServedSourceMap } from "./servedSourceMap";
41
60
  * type-stripping cannot do cross-file type-only elision — e.g. a
42
61
  * value-shaped import of a type+namespace merge survives stripping and
43
62
  * dangles at runtime.
44
- * 3. No owning tsconfig → transform the lone file by the format it resolves to: a
45
- * CommonJS-classified file (`.cts`, or a `.ts` in a package without `type:
46
- * "module"`) is lowered to CommonJS through a tsgo single-file emit so its
47
- * `export` syntax becomes `module.exports`; any other (ESM) file keeps the
48
- * fast in-process `mode: "transform"` type-strip.
63
+ * 3. No owning tsconfig: use an isolated tsgo emit in the runtime module format,
64
+ * lowering standard decorators and CommonJS imports/exports. Type stripping
65
+ * remains recovery when no compiler emit is available.
49
66
  *
50
67
  * The hooks are synchronous and run on the main thread (not a loader worker):
51
68
  * that is what lets a CommonJS `require("./x")` chain reach them and what makes
@@ -1036,7 +1053,7 @@ function owningModuleOptions(filename: string): OwningModuleOptions | null {
1036
1053
  // The owning project cannot be read, so nothing is known about the format
1037
1054
  // it would have emitted. That is the same state as having no project at
1038
1055
  // all, and it is what the dependency lane will conclude too when its build
1039
- // fails and the file falls through to the orphan type-strip.
1056
+ // fails and the file falls through to the isolated orphan emit.
1040
1057
  moduleOptionsCache.set(tsconfig, null);
1041
1058
  return null;
1042
1059
  }
@@ -1202,9 +1219,8 @@ export function projectModuleOptions(
1202
1219
  /**
1203
1220
  * Resolve the JavaScript to run for a TypeScript source file, in priority
1204
1221
  * order: the entry project's pre-built emit (transform plugins applied), a
1205
- * built raw `.ts` dependency, or — when no tsconfig owns it — a `mode:
1206
- * "transform"` type-strip. Shared by the ESM `load` hook and the CommonJS
1207
- * `require` handler.
1222
+ * built raw `.ts` dependency, or an isolated emit when no tsconfig owns it.
1223
+ * Shared by the ESM `load` hook and the CommonJS `require` handler.
1208
1224
  */
1209
1225
  function resolveServedSource(
1210
1226
  filename: string,
@@ -1251,7 +1267,8 @@ function resolveServedSource(
1251
1267
  * map's `sources`, so the JavaScript executed under the `.ts` source URL stays
1252
1268
  * self-describing after the per-run emit directory is deleted. Applied to both
1253
1269
  * the entry lane (`serveEntryEmit`) and the dependency lane
1254
- * (`serveBuiltDependency`); the orphan type-strip lane carries no emitted map.
1270
+ * (`serveBuiltDependency`); the orphan lane inlines its own map before
1271
+ * caching.
1255
1272
  */
1256
1273
  function withInlineSourceMap(served: ServedSource): ServedSource {
1257
1274
  const source = inlineServedSourceMap(
@@ -1267,23 +1284,17 @@ function withInlineSourceMap(served: ServedSource): ServedSource {
1267
1284
  * vendored package that ships raw `.ts`/`.cts`/`.mts` straight under
1268
1285
  * `node_modules`), choosing the lowering by the format the file resolves to.
1269
1286
  *
1270
- * Node's in-process `stripTypeScriptTypes` only erases type syntax; it never
1271
- * rewrites ECMAScript `import`/`export` into CommonJS. That is correct for a
1272
- * file Node will load as ESM, but wrong for one classified CommonJS — a `.cts`,
1273
- * or a `.ts` in a package without `type: "module"` — when the author wrote it
1274
- * with module syntax (`export const`, `export namespace`, `export function`).
1275
- * Stripping leaves the `export` in place and Node's CommonJS loader dies with
1276
- * `SyntaxError: Unexpected token 'export'`. So a CommonJS-format orphan is
1277
- * lowered through a real tsgo `--module commonjs` single-file emit (which also
1278
- * handles `export =`), exactly the format decision tsgo would have made for an
1279
- * owning project; an ESM-format orphan keeps the fast in-process strip.
1287
+ * Both module formats need the compiler: Node's type stripping neither lowers
1288
+ * standard decorators nor rewrites ESM exports for CommonJS. Use the existing
1289
+ * single-file emit with the file's own module format and a standard target.
1290
+ * Retain stripping as recovery when no compiler emit is available.
1280
1291
  */
1281
1292
  function transformOrphanSource(filename: string, url: string): string {
1282
- if (moduleFormat(filename, null) === "commonjs") {
1283
- const lowered = emitOrphanAsCommonJs(filename);
1284
- if (lowered !== null) {
1285
- return lowered;
1286
- }
1293
+ const format =
1294
+ moduleFormat(filename, null) === "commonjs" ? "commonjs" : "module";
1295
+ const lowered = emitOrphanSource(filename, format);
1296
+ if (lowered !== null) {
1297
+ return lowered;
1287
1298
  }
1288
1299
  return stripTypeScriptTypes(fs.readFileSync(filename, "utf8"), {
1289
1300
  mode: "transform",
@@ -1292,25 +1303,27 @@ function transformOrphanSource(filename: string, url: string): string {
1292
1303
  }
1293
1304
 
1294
1305
  /**
1295
- * Lower a single CommonJS-format source file to CommonJS JavaScript by running
1296
- * tsgo on the lone file with `--module commonjs`. Emit-only, no diagnostic gate
1297
- * (the entry project's up-front check is the type gate), matching
1298
- * `buildDependency`. Returns `null` when tsgo is unavailable or produced no
1299
- * output, so the caller can fall back to the in-process strip.
1306
+ * Lower a single source file to its runtime module format. Emit-only, no
1307
+ * diagnostic gate (the entry project's up-front check is the type gate),
1308
+ * matching `buildDependency`. Returns `null` when tsgo is unavailable or
1309
+ * produced no output, so the caller can fall back to the in-process strip.
1300
1310
  */
1301
- function emitOrphanAsCommonJs(filename: string): string | null {
1311
+ function emitOrphanSource(
1312
+ filename: string,
1313
+ format: "commonjs" | "module",
1314
+ ): string | null {
1302
1315
  let tsgo: string;
1303
1316
  try {
1304
1317
  tsgo = resolveTsgo({ cwd: path.dirname(filename) }).binary;
1305
1318
  } catch {
1306
1319
  return null;
1307
1320
  }
1308
- // Content-hash cache: a CJS-format orphan ('s tsgo single-file emit) is lowered
1321
+ // Content-hash cache: an orphan's tsgo single-file emit is lowered
1309
1322
  // once and reused by every other process in the run, and across runs. Without
1310
1323
  // it a program that fans out into many processes (the automated test corpus
1311
1324
  // imports the same vendored `.ts` deps from thousands of generated files) would
1312
1325
  // re-spawn tsgo per file per process and crawl.
1313
- const cacheFile = orphanCacheFile(filename, tsgo);
1326
+ const cacheFile = orphanCacheFile(filename, tsgo, format);
1314
1327
  if (cacheFile !== null) {
1315
1328
  const hit = readFileOrNull(cacheFile);
1316
1329
  if (hit !== null) {
@@ -1323,31 +1336,26 @@ function emitOrphanAsCommonJs(filename: string): string | null {
1323
1336
  tsgo,
1324
1337
  [
1325
1338
  filename,
1326
- // The file is named on the command line, so any tsconfig tsgo would
1327
- // discover by walking up (the consumer's own) must be ignored — both
1328
- // because it is not this file's project and because tsgo errors out
1329
- // ("tsconfig.json is present but will not be loaded") otherwise.
1330
- "--ignoreConfig",
1331
1339
  "--module",
1332
- "commonjs",
1333
- "--target",
1334
- "es2022",
1335
- // This is an emit-only lowering: the entry project's up-front build is
1336
- // the type gate, so the single-file pass does not need to type-check.
1337
- // Skipping the check (and the lib check it implies) cuts the per-file
1338
- // cost several-fold, which matters when a program generates and imports
1339
- // thousands of raw `.ts` files at runtime (a fanned-out test corpus) and
1340
- // each one would otherwise pay a full single-file check.
1341
- "--noCheck",
1342
- "--skipLibCheck",
1340
+ format === "commonjs" ? "commonjs" : "esnext",
1341
+ ...ISOLATED_EMIT_ARGS,
1342
+ "--sourceMap",
1343
+ "--inlineSources",
1343
1344
  "--outDir",
1344
1345
  outDir,
1345
1346
  "--listEmittedFiles",
1346
1347
  ],
1347
1348
  { cwd: path.dirname(filename), encoding: "utf8" },
1348
1349
  );
1349
- const emitted = parseFirstEmittedFile(outputText(res.stdout));
1350
- const lowered = emitted === null ? null : readFileOrNull(emitted);
1350
+ const emitted = pickEmittedJavaScript(
1351
+ filename,
1352
+ parseEmittedFiles(outputText(res.stdout)),
1353
+ );
1354
+ const source = emitted === null ? null : readFileOrNull(emitted);
1355
+ const lowered =
1356
+ source === null
1357
+ ? null
1358
+ : inlineServedSourceMap(source, emitted!, filename);
1351
1359
  if (lowered !== null && cacheFile !== null) {
1352
1360
  writeOrphanCache(cacheFile, lowered);
1353
1361
  }
@@ -1360,7 +1368,10 @@ function emitOrphanAsCommonJs(filename: string): string | null {
1360
1368
  }
1361
1369
 
1362
1370
  /**
1363
- * Emit a source file only for CommonJS export-name discovery.
1371
+ * Read actual owned output for CommonJS export-name discovery, falling back to
1372
+ * isolated emission only when no project emitted this source. Project
1373
+ * transforms and const-enum settings decide which names exist at runtime. An
1374
+ * ESM emit is lowered as JavaScript solely to scan its CommonJS names.
1364
1375
  *
1365
1376
  * This intentionally does not read or write the runtime orphan cache. Name
1366
1377
  * discovery may inspect a source dependency without executing it, so sharing
@@ -1373,6 +1384,14 @@ function emitCommonJsForNameScan(filename: string): string | null {
1373
1384
  if (cached !== undefined) {
1374
1385
  return cached;
1375
1386
  }
1387
+ const served = serveEntryEmit(real) ?? serveDependencyEmit(real);
1388
+ if (
1389
+ served !== null &&
1390
+ moduleFormat(real, served.moduleOptions) === "commonjs"
1391
+ ) {
1392
+ commonJsNameScanSources.set(real, served.source);
1393
+ return served.source;
1394
+ }
1376
1395
  let tsgo: string;
1377
1396
  try {
1378
1397
  tsgo = resolveTsgo({ cwd: path.dirname(real) }).binary;
@@ -1382,17 +1401,17 @@ function emitCommonJsForNameScan(filename: string): string | null {
1382
1401
  }
1383
1402
  const outDir = createCanonicalTempDirectory("ttsx-export-scan-");
1384
1403
  try {
1404
+ // Compile the owned JavaScript, not the original TypeScript whose project
1405
+ // may already have erased or transformed declarations. No source executes.
1406
+ const input = served === null ? real : path.join(outDir, "source.cts");
1407
+ if (served !== null) fs.writeFileSync(input, served.source);
1385
1408
  const res = spawnNative(
1386
1409
  tsgo,
1387
1410
  [
1388
- real,
1389
- "--ignoreConfig",
1411
+ input,
1390
1412
  "--module",
1391
1413
  "commonjs",
1392
- "--target",
1393
- "es2022",
1394
- "--noCheck",
1395
- "--skipLibCheck",
1414
+ ...ISOLATED_EMIT_ARGS,
1396
1415
  "--outDir",
1397
1416
  outDir,
1398
1417
  "--listEmittedFiles",
@@ -1400,7 +1419,7 @@ function emitCommonJsForNameScan(filename: string): string | null {
1400
1419
  { cwd: path.dirname(real), encoding: "utf8" },
1401
1420
  );
1402
1421
  const emitted = pickEmittedJavaScript(
1403
- real,
1422
+ input,
1404
1423
  parseEmittedFiles(outputText(res.stdout)),
1405
1424
  );
1406
1425
  const lowered = emitted === null ? null : readFileOrNull(emitted);
@@ -1423,15 +1442,19 @@ function orphanCacheRoot(): string {
1423
1442
  process.env.TTSC_CACHE_DIR && process.env.TTSC_CACHE_DIR.length !== 0
1424
1443
  ? process.env.TTSC_CACHE_DIR
1425
1444
  : path.join(os.tmpdir(), "ttsc-orphan");
1426
- return path.join(base, "ttsx-orphan-cjs");
1445
+ return path.join(base, "ttsx-orphan");
1427
1446
  }
1428
1447
 
1429
1448
  /**
1430
- * Content-addressed cache path for one orphan file's CommonJS lowering, keyed
1431
- * by source bytes and the tsgo binary so a tsgo bump invalidates it. `null`
1432
- * when the source cannot be read.
1449
+ * Content-addressed cache path for isolated orphan lowering, including the
1450
+ * source path because the inlined map identifies that path. `null` when the
1451
+ * source cannot be read.
1433
1452
  */
1434
- function orphanCacheFile(filename: string, tsgo: string): string | null {
1453
+ function orphanCacheFile(
1454
+ filename: string,
1455
+ tsgo: string,
1456
+ format: "commonjs" | "module",
1457
+ ): string | null {
1435
1458
  let source: Buffer;
1436
1459
  try {
1437
1460
  source = fs.readFileSync(filename);
@@ -1441,6 +1464,9 @@ function orphanCacheFile(filename: string, tsgo: string): string | null {
1441
1464
  const key = crypto
1442
1465
  .createHash("sha256")
1443
1466
  .update(tsgo)
1467
+ .update("\0isolated-source-map-v1\0")
1468
+ .update(filename)
1469
+ .update("\0" + format)
1444
1470
  .update("\0")
1445
1471
  .update(source)
1446
1472
  .digest("hex")
@@ -1464,17 +1490,6 @@ function writeOrphanCache(cacheFile: string, lowered: string): void {
1464
1490
  }
1465
1491
  }
1466
1492
 
1467
- /** First `TSFILE:` path tsgo printed under `--listEmittedFiles`, or `null`. */
1468
- function parseFirstEmittedFile(stdout: string): string | null {
1469
- for (const line of stdout.split(/\r?\n/)) {
1470
- const match = line.match(/^TSFILE:\s*(.+)$/);
1471
- if (match?.[1]) {
1472
- return match[1].trim();
1473
- }
1474
- }
1475
- return null;
1476
- }
1477
-
1478
1493
  /** `TSFILE:` paths tsgo printed under `--listEmittedFiles`. */
1479
1494
  function parseEmittedFiles(stdout: string): string[] {
1480
1495
  const files: string[] = [];
@@ -1516,48 +1531,37 @@ function pickEmittedJavaScript(
1516
1531
  * Runtime CommonJS consumers see the getters that helper installs, but Node's
1517
1532
  * ESM linker only exposes named imports it can statically identify from
1518
1533
  * `exports.name = ...` assignments. For relative star re-exports whose emitted
1519
- * target is available, replace the helper call with explicit configurable
1520
- * export placeholders followed by the same `__createBinding` getter install.
1534
+ * target is available, advertise its names through inert assignments that
1535
+ * Node's static lexer recognizes. The original helper still owns every runtime
1536
+ * binding, including values static discovery cannot enumerate.
1521
1537
  */
1522
1538
  function exposeCommonJsStarExports(
1523
1539
  source: string,
1524
1540
  emittedFile: string | undefined,
1525
1541
  sourceFile: string | undefined,
1526
1542
  ): string {
1527
- if (!source.includes("__exportStar(")) {
1528
- return source;
1529
- }
1530
- const reserved = collectStaticCommonJsExportNames(source);
1531
- let index = 0;
1532
- return source.replace(
1533
- /^(\s*)__exportStar\(\s*require\((["'])([^"']+)\2\)\s*,\s*exports\s*\);/gm,
1534
- (statement: string, indent: string, _quote: string, specifier: string) => {
1535
- const names = [
1536
- ...collectStarExportNames(emittedFile, sourceFile, specifier),
1537
- ].filter(
1538
- (name) =>
1539
- name !== "default" &&
1540
- name !== "__esModule" &&
1541
- isIdentifierName(name) &&
1542
- !reserved.has(name),
1543
- );
1544
- if (names.length === 0) {
1545
- return statement;
1546
- }
1547
- for (const name of names) {
1548
- reserved.add(name);
1543
+ const parsed = parseCommonJsExports(source);
1544
+ const reserved = new Set(parsed.exports);
1545
+ const names = new Set<string>();
1546
+ for (const specifier of parsed.reexports) {
1547
+ for (const name of collectStarExportNames(
1548
+ emittedFile,
1549
+ sourceFile,
1550
+ specifier,
1551
+ )) {
1552
+ if (name !== "default" && name !== "__esModule" && !reserved.has(name)) {
1553
+ names.add(name);
1549
1554
  }
1550
- const receiver = `__ttsx_export_star_${index++}`;
1551
- return [
1552
- ...names.map((name) => `${indent}exports.${name} = void 0;`),
1553
- `${indent}var ${receiver} = require(${JSON.stringify(specifier)});`,
1554
- ...names.map(
1555
- (name) =>
1556
- `${indent}__createBinding(exports, ${receiver}, ${JSON.stringify(name)});`,
1557
- ),
1558
- ].join("\n");
1559
- },
1560
- );
1555
+ }
1556
+ }
1557
+ if (names.size === 0) return source;
1558
+ // Node's lexer reads these assignments statically. Nothing executes, and no
1559
+ // existing statement, source position, control-flow body or runtime export
1560
+ // is changed. Keep all specifier/literal parsing in the same lexer Node uses.
1561
+ const hints = [...names]
1562
+ .map((name) => `exports[${JSON.stringify(name)}] = void 0;`)
1563
+ .join(" ");
1564
+ return source + `\nif (false) { ${hints} }\n`;
1561
1565
  }
1562
1566
 
1563
1567
  function collectStarExportNames(
@@ -1593,8 +1597,9 @@ function collectCommonJsExportNames(
1593
1597
  if (source === null) {
1594
1598
  return new Set();
1595
1599
  }
1596
- const names = collectStaticCommonJsExportNames(source);
1597
- for (const specifier of collectExportStarSpecifiers(source)) {
1600
+ const parsed = parseCommonJsExports(source);
1601
+ const names = new Set(parsed.exports);
1602
+ for (const specifier of parsed.reexports) {
1598
1603
  const target = resolveEmittedRequire(real, specifier);
1599
1604
  if (target === null) {
1600
1605
  continue;
@@ -1608,164 +1613,6 @@ function collectCommonJsExportNames(
1608
1613
  return names;
1609
1614
  }
1610
1615
 
1611
- function collectStaticCommonJsExportNames(source: string): Set<string> {
1612
- // Scan executable syntax only. Text that merely resembles an assignment —
1613
- // `exports.x =` inside a comment, string, or template-literal text — must not
1614
- // become an ESM-visible export name, or a named import of it would link to
1615
- // `undefined` for a property the CommonJS module never defines. Masking the
1616
- // inert lexical spans before matching keeps genuine top-level assignments
1617
- // (and executable `${ ... }` template substitutions) while dropping the
1618
- // decoys.
1619
- const scannable = maskCommentsAndStrings(source);
1620
- const names = new Set<string>();
1621
- const pattern =
1622
- /(?:^|[^\w$])(?:exports|module\.exports)\.([A-Za-z_$][\w$]*)\s*=/g;
1623
- let match: RegExpExecArray | null;
1624
- while ((match = pattern.exec(scannable)) !== null) {
1625
- names.add(match[1]!);
1626
- }
1627
- return names;
1628
- }
1629
-
1630
- /**
1631
- * Blank out the interior of line comments, block comments, string literals, and
1632
- * template-literal text in emitted JavaScript, replacing each masked character
1633
- * with a space while preserving newlines, code, and template `${ ... }`
1634
- * substitutions verbatim. Positions and length are preserved so an offset in
1635
- * the masked text maps back to the same offset in the source.
1636
- *
1637
- * The input is tsgo's CommonJS emit (well-formed JavaScript), so a character
1638
- * scanner that tracks the standard comment/string/template states is sufficient
1639
- * to separate executable tokens from inert text. Regular-expression literals
1640
- * are intentionally not masked: distinguishing `/`-division from a regex
1641
- * literal needs full tokenization, and tsgo's CommonJS emit never wraps an
1642
- * `exports.<name> =` assignment inside a regex literal.
1643
- */
1644
- function maskCommentsAndStrings(source: string): string {
1645
- const out = source.split("");
1646
- const n = out.length;
1647
- const blank = (index: number): void => {
1648
- const ch = out[index];
1649
- if (ch !== "\n" && ch !== "\r") {
1650
- out[index] = " ";
1651
- }
1652
- };
1653
- // A stack of lexical contexts. The base is code; each backtick pushes a
1654
- // template context, and each `${` inside a template pushes a nested code
1655
- // context whose `braceDepth` tracks `{}` nesting so an object literal inside
1656
- // the substitution does not end it early.
1657
- interface Context {
1658
- kind: "code" | "template";
1659
- braceDepth: number;
1660
- }
1661
- const stack: Context[] = [{ kind: "code", braceDepth: 0 }];
1662
- let i = 0;
1663
- while (i < n) {
1664
- const top = stack[stack.length - 1]!;
1665
- const ch = out[i]!;
1666
- if (top.kind === "template") {
1667
- if (ch === "\\") {
1668
- blank(i);
1669
- blank(i + 1);
1670
- i += 2;
1671
- continue;
1672
- }
1673
- if (ch === "`") {
1674
- blank(i);
1675
- stack.pop();
1676
- i += 1;
1677
- continue;
1678
- }
1679
- if (ch === "$" && out[i + 1] === "{") {
1680
- // Enter a code substitution: `${` and its contents stay executable.
1681
- stack.push({ kind: "code", braceDepth: 0 });
1682
- i += 2;
1683
- continue;
1684
- }
1685
- blank(i);
1686
- i += 1;
1687
- continue;
1688
- }
1689
- // Code context.
1690
- if (ch === "/" && out[i + 1] === "/") {
1691
- blank(i);
1692
- blank(i + 1);
1693
- i += 2;
1694
- while (i < n && out[i] !== "\n") {
1695
- blank(i);
1696
- i += 1;
1697
- }
1698
- continue;
1699
- }
1700
- if (ch === "/" && out[i + 1] === "*") {
1701
- blank(i);
1702
- blank(i + 1);
1703
- i += 2;
1704
- while (i < n && !(out[i] === "*" && out[i + 1] === "/")) {
1705
- blank(i);
1706
- i += 1;
1707
- }
1708
- if (i < n) {
1709
- blank(i);
1710
- blank(i + 1);
1711
- i += 2;
1712
- }
1713
- continue;
1714
- }
1715
- if (ch === '"' || ch === "'") {
1716
- blank(i);
1717
- i += 1;
1718
- while (i < n && out[i] !== ch) {
1719
- if (out[i] === "\\") {
1720
- blank(i);
1721
- blank(i + 1);
1722
- i += 2;
1723
- continue;
1724
- }
1725
- // A bare newline ends an unterminated string; stop masking so the rest
1726
- // of the line is still scanned as code (defensive — tsgo never emits
1727
- // one).
1728
- if (out[i] === "\n") {
1729
- break;
1730
- }
1731
- blank(i);
1732
- i += 1;
1733
- }
1734
- if (i < n && out[i] === ch) {
1735
- blank(i);
1736
- i += 1;
1737
- }
1738
- continue;
1739
- }
1740
- if (ch === "`") {
1741
- blank(i);
1742
- stack.push({ kind: "template", braceDepth: 0 });
1743
- i += 1;
1744
- continue;
1745
- }
1746
- if (ch === "{") {
1747
- top.braceDepth += 1;
1748
- i += 1;
1749
- continue;
1750
- }
1751
- if (ch === "}") {
1752
- if (top.braceDepth === 0 && stack.length > 1) {
1753
- // Close the enclosing template `${ ... }` and resume template text.
1754
- stack.pop();
1755
- i += 1;
1756
- continue;
1757
- }
1758
- if (top.braceDepth > 0) {
1759
- top.braceDepth -= 1;
1760
- }
1761
- i += 1;
1762
- continue;
1763
- }
1764
- i += 1;
1765
- }
1766
- return out.join("");
1767
- }
1768
-
1769
1616
  function collectSourceCommonJsExportNames(
1770
1617
  sourceFile: string,
1771
1618
  seen: Set<string>,
@@ -1779,8 +1626,9 @@ function collectSourceCommonJsExportNames(
1779
1626
  if (source === null) {
1780
1627
  return new Set();
1781
1628
  }
1782
- const names = collectStaticCommonJsExportNames(source);
1783
- for (const specifier of collectExportStarSpecifiers(source)) {
1629
+ const parsed = parseCommonJsExports(source);
1630
+ const names = new Set(parsed.exports);
1631
+ for (const specifier of parsed.reexports) {
1784
1632
  const target = resolveSourceSpecifier(real, specifier);
1785
1633
  if (target === null) {
1786
1634
  continue;
@@ -1794,17 +1642,6 @@ function collectSourceCommonJsExportNames(
1794
1642
  return names;
1795
1643
  }
1796
1644
 
1797
- function collectExportStarSpecifiers(source: string): string[] {
1798
- const specifiers: string[] = [];
1799
- const pattern =
1800
- /^(\s*)__exportStar\(\s*require\((["'])([^"']+)\2\)\s*,\s*exports\s*\);/gm;
1801
- let match: RegExpExecArray | null;
1802
- while ((match = pattern.exec(source)) !== null) {
1803
- specifiers.push(match[3]!);
1804
- }
1805
- return specifiers;
1806
- }
1807
-
1808
1645
  function resolveEmittedRequire(
1809
1646
  emittedFile: string,
1810
1647
  specifier: string,
@@ -1840,7 +1677,8 @@ function resolveSourceSpecifier(
1840
1677
  }
1841
1678
  const base = path.resolve(path.dirname(sourceFile), specifier);
1842
1679
  if (path.extname(base).length !== 0) {
1843
- return isFile(base) ? base : null;
1680
+ if (isFile(base)) return base;
1681
+ return typescriptSourcesForJavaScriptSpecifier(base).find(isFile) ?? null;
1844
1682
  }
1845
1683
  for (const extension of TYPESCRIPT_EXTENSIONS) {
1846
1684
  const candidate = base + extension;
@@ -1857,10 +1695,6 @@ function resolveSourceSpecifier(
1857
1695
  return null;
1858
1696
  }
1859
1697
 
1860
- function isIdentifierName(name: string): boolean {
1861
- return /^[A-Za-z_$][\w$]*$/.test(name);
1862
- }
1863
-
1864
1698
  /**
1865
1699
  * Serve the entry project's pre-built JavaScript for a source file the build
1866
1700
  * emitted, or `null` when the file is outside the build or its emit is
@@ -2020,7 +1854,7 @@ function serveDependencyEmit(real: string): ServedSource | null {
2020
1854
  try {
2021
1855
  built = ensureProjectBuilt(tsconfig);
2022
1856
  } catch {
2023
- // The owning project produced no emit at all; fall back to type-stripping
1857
+ // The owning project produced no emit at all; fall back to isolated emit of
2024
1858
  // this single file rather than failing the whole run.
2025
1859
  return null;
2026
1860
  }
@@ -2140,6 +1974,7 @@ export function dependencyCacheKey(
2140
1974
  crypto
2141
1975
  .createHash("sha256")
2142
1976
  .update(tsconfig)
1977
+ .update("\0runtime-es2025")
2143
1978
  // Descriptor evaluation promises a result bound to this process's exact
2144
1979
  // input observations. Reusing an emit another evaluator built can pair
2145
1980
  // that process's old source/config bytes with this process's later hashes.
@@ -2317,6 +2152,7 @@ function buildDependency(
2317
2152
  fs.mkdirSync(emitDir, { recursive: true });
2318
2153
  const result = runBuild({
2319
2154
  cwd: project.root,
2155
+ passthrough: runtimeCompilerArgs(project),
2320
2156
  emit: true,
2321
2157
  forceListEmittedFiles: true,
2322
2158
  outDir: emitDir,
@@ -2360,7 +2196,7 @@ function buildDependency(
2360
2196
  // list": a native transform host (typia, @ttsc/banner, …) emits without
2361
2197
  // printing the `--listEmittedFiles` lines, so `result.emittedFiles` is empty
2362
2198
  // even on a clean build. A genuinely empty output directory is the real
2363
- // failure; the caller then falls back to type-stripping the one file.
2199
+ // failure; the caller then falls back to isolated emit of the one file.
2364
2200
  if (!emittedAnything(emitDir)) {
2365
2201
  // Drop the failed generation so its partial directory can never be mistaken
2366
2202
  // for a reusable build.
@@ -2814,8 +2650,8 @@ const moduleOptionsCache = new Map<string, OwningModuleOptions | null>();
2814
2650
  * walk stops at a `node_modules` boundary: a tsconfig above `node_modules`
2815
2651
  * belongs to the consumer, not to the published dependency inside it, so a
2816
2652
  * dependency that ships no tsconfig of its own has no owning project and is
2817
- * type-stripped instead. A pnpm-symlinked workspace package is unaffected
2818
- * because `file` is already its real path (outside `node_modules`).
2653
+ * compiled in isolation instead. A pnpm-symlinked workspace package is
2654
+ * unaffected because `file` is already its real path (outside `node_modules`).
2819
2655
  *
2820
2656
  * The walk is memoised per directory (the whole walked chain shares one
2821
2657
  * answer), so the thousands of files a fanned-out test corpus imports from the