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.
- package/lib/launcher/internal/commonJsExportMetadata.d.ts +7 -0
- package/lib/launcher/internal/commonJsExportMetadata.js +89 -0
- package/lib/launcher/internal/commonJsExportMetadata.js.map +1 -0
- package/lib/launcher/internal/prepareExecution.js +3 -2
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runtimeCompilerArgs.d.ts +9 -0
- package/lib/launcher/internal/runtimeCompilerArgs.js +70 -0
- package/lib/launcher/internal/runtimeCompilerArgs.js.map +1 -0
- package/lib/launcher/internal/runtimeHooks.js +112 -268
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/package.json +12 -8
- package/shim/compiler/resolution.go +16 -0
- package/src/launcher/internal/commonJsExportMetadata.ts +91 -0
- package/src/launcher/internal/prepareExecution.ts +11 -2
- package/src/launcher/internal/runtimeCompilerArgs.ts +77 -0
- package/src/launcher/internal/runtimeHooks.ts +128 -292
|
@@ -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
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
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
|
|
1206
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1271
|
-
* rewrites
|
|
1272
|
-
* file
|
|
1273
|
-
*
|
|
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
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
|
|
1334
|
-
"
|
|
1335
|
-
|
|
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 =
|
|
1350
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1389
|
-
"--ignoreConfig",
|
|
1411
|
+
input,
|
|
1390
1412
|
"--module",
|
|
1391
1413
|
"commonjs",
|
|
1392
|
-
|
|
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
|
-
|
|
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
|
|
1445
|
+
return path.join(base, "ttsx-orphan");
|
|
1427
1446
|
}
|
|
1428
1447
|
|
|
1429
1448
|
/**
|
|
1430
|
-
* Content-addressed cache path for
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
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(
|
|
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,
|
|
1520
|
-
*
|
|
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
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
|
1597
|
-
|
|
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
|
|
1783
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|