ttsc 0.15.1 → 0.15.3
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/driver/emit_plugin.go +34 -5
- package/driver/sourcemap_preamble.go +375 -0
- package/lib/TtscCompiler.js +4 -1
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/project/readProjectConfig.js +4 -1
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.d.ts +1 -0
- package/lib/compiler/internal/transformProjectInMemory.js +36 -0
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +14 -0
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.js +369 -24
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.js +7 -7
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +18 -0
- package/package.json +9 -9
- package/shim/compiler/emit_sourcemap.go +176 -0
- package/src/TtscCompiler.ts +5 -1
- package/src/compiler/internal/project/readProjectConfig.ts +5 -1
- package/src/compiler/internal/transformProjectInMemory.ts +46 -0
- package/src/launcher/internal/prepareExecution.ts +16 -0
- package/src/launcher/internal/runtimeHooks.ts +475 -33
- package/src/plugin/internal/buildSourcePlugin.ts +11 -8
- package/src/structures/ITtscCompilerTransformation.ts +20 -0
- package/utility/host.go +30 -7
|
@@ -78,6 +78,13 @@ interface LoadResult {
|
|
|
78
78
|
shortCircuit?: boolean;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
interface ServedSource {
|
|
82
|
+
source: string;
|
|
83
|
+
moduleOption?: string;
|
|
84
|
+
emittedFile?: string;
|
|
85
|
+
sourceFile?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
type NextResolve = (
|
|
82
89
|
specifier: string,
|
|
83
90
|
context: ResolveContext,
|
|
@@ -200,13 +207,19 @@ function resolve(
|
|
|
200
207
|
nextResolve: NextResolve,
|
|
201
208
|
): ResolveResult {
|
|
202
209
|
try {
|
|
203
|
-
return
|
|
210
|
+
return rememberCommonJsNamedInterop(
|
|
211
|
+
nextResolve(specifier, context),
|
|
212
|
+
context,
|
|
213
|
+
);
|
|
204
214
|
} catch (error) {
|
|
205
215
|
const rescued = probeRescuableSpecifier(specifier, context.parentURL);
|
|
206
216
|
if (rescued === null) {
|
|
207
217
|
throw error;
|
|
208
218
|
}
|
|
209
|
-
return
|
|
219
|
+
return rememberCommonJsNamedInterop(
|
|
220
|
+
{ shortCircuit: true, url: rescued },
|
|
221
|
+
context,
|
|
222
|
+
);
|
|
210
223
|
}
|
|
211
224
|
}
|
|
212
225
|
|
|
@@ -219,6 +232,10 @@ interface BuiltProject {
|
|
|
219
232
|
}
|
|
220
233
|
const builtProjects = new Map<string, BuiltProject>();
|
|
221
234
|
|
|
235
|
+
/** File URLs whose CommonJS source was reached from an ESM parent import. */
|
|
236
|
+
const commonJsNamedInteropUrls = new Set<string>();
|
|
237
|
+
const commonJsNameScanSources = new Map<string, string | null>();
|
|
238
|
+
|
|
222
239
|
function load(
|
|
223
240
|
url: string,
|
|
224
241
|
context: LoadContext,
|
|
@@ -231,14 +248,95 @@ function load(
|
|
|
231
248
|
if (!isTypeScriptSource(filename)) {
|
|
232
249
|
return nextLoad(url, context);
|
|
233
250
|
}
|
|
234
|
-
const {
|
|
251
|
+
const { format, source } = resolveRuntimeSource(filename, url);
|
|
235
252
|
return {
|
|
236
|
-
format
|
|
253
|
+
format,
|
|
237
254
|
shortCircuit: true,
|
|
238
255
|
source,
|
|
239
256
|
};
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
function resolveRuntimeSource(
|
|
260
|
+
filename: string,
|
|
261
|
+
url: string = pathToFileURL(filename).href,
|
|
262
|
+
): { format: string; source: string } {
|
|
263
|
+
const served = resolveServedSource(filename, url);
|
|
264
|
+
const format = moduleFormat(filename, served.moduleOption);
|
|
265
|
+
return {
|
|
266
|
+
format,
|
|
267
|
+
source:
|
|
268
|
+
format === "commonjs" && commonJsNamedInteropUrls.has(url)
|
|
269
|
+
? exposeCommonJsStarExports(
|
|
270
|
+
served.source,
|
|
271
|
+
served.emittedFile,
|
|
272
|
+
served.sourceFile,
|
|
273
|
+
)
|
|
274
|
+
: served.source,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function rememberCommonJsNamedInterop(
|
|
279
|
+
result: ResolveResult,
|
|
280
|
+
context: ResolveContext,
|
|
281
|
+
): ResolveResult {
|
|
282
|
+
if (shouldExposeCommonJsNamedExports(result.url, context.parentURL)) {
|
|
283
|
+
commonJsNamedInteropUrls.add(result.url);
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function shouldExposeCommonJsNamedExports(
|
|
289
|
+
url: string,
|
|
290
|
+
parentURL: string | undefined,
|
|
291
|
+
): boolean {
|
|
292
|
+
if (
|
|
293
|
+
parentURL === undefined ||
|
|
294
|
+
!url.startsWith("file:") ||
|
|
295
|
+
!parentURL.startsWith("file:")
|
|
296
|
+
) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const parentFile = fileURLToPath(parentURL);
|
|
300
|
+
if (moduleFormat(parentFile, moduleOptionForSource(parentFile)) !== "module") {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const filename = fileURLToPath(url);
|
|
304
|
+
return (
|
|
305
|
+
isTypeScriptSource(filename) &&
|
|
306
|
+
moduleFormat(filename, moduleOptionForSource(filename)) === "commonjs"
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function moduleOptionForSource(filename: string): string | undefined {
|
|
311
|
+
if (!isTypeScriptSource(filename)) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
const real = realPath(filename);
|
|
315
|
+
const m = manifest();
|
|
316
|
+
if (m !== null && isWithin(real, m.rootDir)) {
|
|
317
|
+
return m.moduleOption;
|
|
318
|
+
}
|
|
319
|
+
const tsconfig = nearestTsconfig(real);
|
|
320
|
+
if (tsconfig === null) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
if (moduleOptionCache.has(tsconfig)) {
|
|
324
|
+
return moduleOptionCache.get(tsconfig);
|
|
325
|
+
}
|
|
326
|
+
let moduleOption: string | undefined;
|
|
327
|
+
try {
|
|
328
|
+
const project = readProjectConfig({ cwd: path.dirname(tsconfig), tsconfig });
|
|
329
|
+
moduleOption =
|
|
330
|
+
typeof project.compilerOptions.module === "string"
|
|
331
|
+
? project.compilerOptions.module
|
|
332
|
+
: undefined;
|
|
333
|
+
} catch {
|
|
334
|
+
moduleOption = undefined;
|
|
335
|
+
}
|
|
336
|
+
moduleOptionCache.set(tsconfig, moduleOption);
|
|
337
|
+
return moduleOption;
|
|
338
|
+
}
|
|
339
|
+
|
|
242
340
|
/**
|
|
243
341
|
* Resolve the JavaScript to run for a TypeScript source file, in priority
|
|
244
342
|
* order: the entry project's pre-built emit (transform plugins applied), a
|
|
@@ -249,11 +347,11 @@ function load(
|
|
|
249
347
|
function resolveServedSource(
|
|
250
348
|
filename: string,
|
|
251
349
|
url: string = pathToFileURL(filename).href,
|
|
252
|
-
):
|
|
350
|
+
): ServedSource {
|
|
253
351
|
const real = realPath(filename);
|
|
254
352
|
const served = serveEntryEmit(real);
|
|
255
353
|
if (served !== null) {
|
|
256
|
-
return { moduleOption: manifest()?.moduleOption,
|
|
354
|
+
return { moduleOption: manifest()?.moduleOption, ...served };
|
|
257
355
|
}
|
|
258
356
|
const built = serveDependencyEmit(real);
|
|
259
357
|
if (built !== null) {
|
|
@@ -261,6 +359,7 @@ function resolveServedSource(
|
|
|
261
359
|
}
|
|
262
360
|
return {
|
|
263
361
|
moduleOption: undefined,
|
|
362
|
+
sourceFile: filename,
|
|
264
363
|
source: transformOrphanSource(filename, url),
|
|
265
364
|
};
|
|
266
365
|
}
|
|
@@ -362,8 +461,65 @@ function emitOrphanAsCommonJs(filename: string): string | null {
|
|
|
362
461
|
}
|
|
363
462
|
}
|
|
364
463
|
|
|
365
|
-
/**
|
|
366
|
-
*
|
|
464
|
+
/**
|
|
465
|
+
* Emit a source file only for CommonJS export-name discovery.
|
|
466
|
+
*
|
|
467
|
+
* This intentionally does not read or write the runtime orphan cache. Name
|
|
468
|
+
* discovery may inspect a source dependency without executing it, so sharing
|
|
469
|
+
* that output with the runtime fallback would let a speculative scan affect a
|
|
470
|
+
* later load path.
|
|
471
|
+
*/
|
|
472
|
+
function emitCommonJsForNameScan(filename: string): string | null {
|
|
473
|
+
const real = realPath(filename);
|
|
474
|
+
const cached = commonJsNameScanSources.get(real);
|
|
475
|
+
if (cached !== undefined) {
|
|
476
|
+
return cached;
|
|
477
|
+
}
|
|
478
|
+
let tsgo: string;
|
|
479
|
+
try {
|
|
480
|
+
tsgo = resolveTsgo({ cwd: path.dirname(real) }).binary;
|
|
481
|
+
} catch {
|
|
482
|
+
commonJsNameScanSources.set(real, null);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ttsx-export-scan-"));
|
|
486
|
+
try {
|
|
487
|
+
const res = spawnNative(
|
|
488
|
+
tsgo,
|
|
489
|
+
[
|
|
490
|
+
real,
|
|
491
|
+
"--ignoreConfig",
|
|
492
|
+
"--module",
|
|
493
|
+
"commonjs",
|
|
494
|
+
"--target",
|
|
495
|
+
"es2022",
|
|
496
|
+
"--noCheck",
|
|
497
|
+
"--skipLibCheck",
|
|
498
|
+
"--outDir",
|
|
499
|
+
outDir,
|
|
500
|
+
"--listEmittedFiles",
|
|
501
|
+
],
|
|
502
|
+
{ cwd: path.dirname(real), encoding: "utf8" },
|
|
503
|
+
);
|
|
504
|
+
const emitted = pickEmittedJavaScript(
|
|
505
|
+
real,
|
|
506
|
+
parseEmittedFiles(outputText(res.stdout)),
|
|
507
|
+
);
|
|
508
|
+
const lowered = emitted === null ? null : readFileOrNull(emitted);
|
|
509
|
+
commonJsNameScanSources.set(real, lowered);
|
|
510
|
+
return lowered;
|
|
511
|
+
} catch {
|
|
512
|
+
commonJsNameScanSources.set(real, null);
|
|
513
|
+
return null;
|
|
514
|
+
} finally {
|
|
515
|
+
fs.rmSync(outDir, { force: true, recursive: true });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Cache root for lowered orphan sources, shared per run (and across runs when
|
|
521
|
+
* `TTSC_CACHE_DIR` points at a persisted directory).
|
|
522
|
+
*/
|
|
367
523
|
function orphanCacheRoot(): string {
|
|
368
524
|
const base =
|
|
369
525
|
process.env.TTSC_CACHE_DIR && process.env.TTSC_CACHE_DIR.length !== 0
|
|
@@ -372,9 +528,11 @@ function orphanCacheRoot(): string {
|
|
|
372
528
|
return path.join(base, "ttsx-orphan-cjs");
|
|
373
529
|
}
|
|
374
530
|
|
|
375
|
-
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
531
|
+
/**
|
|
532
|
+
* Content-addressed cache path for one orphan file's CommonJS lowering, keyed
|
|
533
|
+
* by source bytes and the tsgo binary so a tsgo bump invalidates it. `null`
|
|
534
|
+
* when the source cannot be read.
|
|
535
|
+
*/
|
|
378
536
|
function orphanCacheFile(filename: string, tsgo: string): string | null {
|
|
379
537
|
let source: Buffer;
|
|
380
538
|
try {
|
|
@@ -392,9 +550,11 @@ function orphanCacheFile(filename: string, tsgo: string): string | null {
|
|
|
392
550
|
return path.join(orphanCacheRoot(), `${key}.js`);
|
|
393
551
|
}
|
|
394
552
|
|
|
395
|
-
/**
|
|
553
|
+
/**
|
|
554
|
+
* Write the lowered source to its cache path atomically (temp + rename), so a
|
|
396
555
|
* concurrent reader never sees a half-written file. Best-effort: a failure just
|
|
397
|
-
* means the next process re-lowers.
|
|
556
|
+
* means the next process re-lowers.
|
|
557
|
+
*/
|
|
398
558
|
function writeOrphanCache(cacheFile: string, lowered: string): void {
|
|
399
559
|
try {
|
|
400
560
|
fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
|
|
@@ -417,6 +577,245 @@ function parseFirstEmittedFile(stdout: string): string | null {
|
|
|
417
577
|
return null;
|
|
418
578
|
}
|
|
419
579
|
|
|
580
|
+
/** `TSFILE:` paths tsgo printed under `--listEmittedFiles`. */
|
|
581
|
+
function parseEmittedFiles(stdout: string): string[] {
|
|
582
|
+
const files: string[] = [];
|
|
583
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
584
|
+
const match = line.match(/^TSFILE:\s*(.+)$/);
|
|
585
|
+
if (match?.[1]) {
|
|
586
|
+
files.push(match[1].trim());
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return files;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Pick the emitted JavaScript corresponding to the source file requested. */
|
|
593
|
+
function pickEmittedJavaScript(
|
|
594
|
+
filename: string,
|
|
595
|
+
emittedFiles: readonly string[],
|
|
596
|
+
): string | null {
|
|
597
|
+
const stem = path
|
|
598
|
+
.basename(filename)
|
|
599
|
+
.replace(/\.[cm]?tsx?$/i, "")
|
|
600
|
+
.toLowerCase();
|
|
601
|
+
const candidates = emittedFiles.filter((file) => {
|
|
602
|
+
const parsed = path.parse(file);
|
|
603
|
+
return (
|
|
604
|
+
parsed.name.toLowerCase() === stem && /\.(?:[cm]?js)$/i.test(parsed.base)
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
if (candidates.length === 1) {
|
|
608
|
+
return candidates[0]!;
|
|
609
|
+
}
|
|
610
|
+
return emittedFiles.find((file) => /\.(?:[cm]?js)$/i.test(file)) ?? null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Make TypeScript-Go's CommonJS `export *` output visible to Node's
|
|
615
|
+
* ESM-from-CJS named export scanner.
|
|
616
|
+
*
|
|
617
|
+
* Tsgo lowers star re-exports to `__exportStar(require("./x"), exports)`.
|
|
618
|
+
* Runtime CommonJS consumers see the getters that helper installs, but Node's
|
|
619
|
+
* ESM linker only exposes named imports it can statically identify from
|
|
620
|
+
* `exports.name = ...` assignments. For relative star re-exports whose emitted
|
|
621
|
+
* target is available, replace the helper call with explicit configurable
|
|
622
|
+
* export placeholders followed by the same `__createBinding` getter install.
|
|
623
|
+
*/
|
|
624
|
+
function exposeCommonJsStarExports(
|
|
625
|
+
source: string,
|
|
626
|
+
emittedFile: string | undefined,
|
|
627
|
+
sourceFile: string | undefined,
|
|
628
|
+
): string {
|
|
629
|
+
if (!source.includes("__exportStar(")) {
|
|
630
|
+
return source;
|
|
631
|
+
}
|
|
632
|
+
const reserved = collectStaticCommonJsExportNames(source);
|
|
633
|
+
let index = 0;
|
|
634
|
+
return source.replace(
|
|
635
|
+
/^(\s*)__exportStar\(\s*require\((["'])([^"']+)\2\)\s*,\s*exports\s*\);/gm,
|
|
636
|
+
(statement: string, indent: string, _quote: string, specifier: string) => {
|
|
637
|
+
const names = [
|
|
638
|
+
...collectStarExportNames(emittedFile, sourceFile, specifier),
|
|
639
|
+
].filter(
|
|
640
|
+
(name) =>
|
|
641
|
+
name !== "default" &&
|
|
642
|
+
name !== "__esModule" &&
|
|
643
|
+
isIdentifierName(name) &&
|
|
644
|
+
!reserved.has(name),
|
|
645
|
+
);
|
|
646
|
+
if (names.length === 0) {
|
|
647
|
+
return statement;
|
|
648
|
+
}
|
|
649
|
+
for (const name of names) {
|
|
650
|
+
reserved.add(name);
|
|
651
|
+
}
|
|
652
|
+
const receiver = `__ttsx_export_star_${index++}`;
|
|
653
|
+
return [
|
|
654
|
+
...names.map((name) => `${indent}exports.${name} = void 0;`),
|
|
655
|
+
`${indent}var ${receiver} = require(${JSON.stringify(specifier)});`,
|
|
656
|
+
...names.map(
|
|
657
|
+
(name) =>
|
|
658
|
+
`${indent}__createBinding(exports, ${receiver}, ${JSON.stringify(name)});`,
|
|
659
|
+
),
|
|
660
|
+
].join("\n");
|
|
661
|
+
},
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function collectStarExportNames(
|
|
666
|
+
emittedFile: string | undefined,
|
|
667
|
+
sourceFile: string | undefined,
|
|
668
|
+
specifier: string,
|
|
669
|
+
): Set<string> {
|
|
670
|
+
if (emittedFile !== undefined) {
|
|
671
|
+
const emittedTarget = resolveEmittedRequire(emittedFile, specifier);
|
|
672
|
+
if (emittedTarget !== null) {
|
|
673
|
+
return collectCommonJsExportNames(emittedTarget, new Set());
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (sourceFile !== undefined) {
|
|
677
|
+
const sourceTarget = resolveSourceSpecifier(sourceFile, specifier);
|
|
678
|
+
if (sourceTarget !== null) {
|
|
679
|
+
return collectSourceCommonJsExportNames(sourceTarget, new Set());
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return new Set();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function collectCommonJsExportNames(
|
|
686
|
+
emittedFile: string,
|
|
687
|
+
seen: Set<string>,
|
|
688
|
+
): Set<string> {
|
|
689
|
+
const real = realPath(emittedFile);
|
|
690
|
+
if (seen.has(real)) {
|
|
691
|
+
return new Set();
|
|
692
|
+
}
|
|
693
|
+
seen.add(real);
|
|
694
|
+
const source = readFileOrNull(real);
|
|
695
|
+
if (source === null) {
|
|
696
|
+
return new Set();
|
|
697
|
+
}
|
|
698
|
+
const names = collectStaticCommonJsExportNames(source);
|
|
699
|
+
for (const specifier of collectExportStarSpecifiers(source)) {
|
|
700
|
+
const target = resolveEmittedRequire(real, specifier);
|
|
701
|
+
if (target === null) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
for (const name of collectCommonJsExportNames(target, seen)) {
|
|
705
|
+
if (name !== "default" && name !== "__esModule" && !names.has(name)) {
|
|
706
|
+
names.add(name);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return names;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function collectStaticCommonJsExportNames(source: string): Set<string> {
|
|
714
|
+
const names = new Set<string>();
|
|
715
|
+
const pattern =
|
|
716
|
+
/(?:^|[^\w$])(?:exports|module\.exports)\.([A-Za-z_$][\w$]*)\s*=/g;
|
|
717
|
+
let match: RegExpExecArray | null;
|
|
718
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
719
|
+
names.add(match[1]!);
|
|
720
|
+
}
|
|
721
|
+
return names;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function collectSourceCommonJsExportNames(
|
|
725
|
+
sourceFile: string,
|
|
726
|
+
seen: Set<string>,
|
|
727
|
+
): Set<string> {
|
|
728
|
+
const real = realPath(sourceFile);
|
|
729
|
+
if (seen.has(real)) {
|
|
730
|
+
return new Set();
|
|
731
|
+
}
|
|
732
|
+
seen.add(real);
|
|
733
|
+
const source = emitCommonJsForNameScan(real);
|
|
734
|
+
if (source === null) {
|
|
735
|
+
return new Set();
|
|
736
|
+
}
|
|
737
|
+
const names = collectStaticCommonJsExportNames(source);
|
|
738
|
+
for (const specifier of collectExportStarSpecifiers(source)) {
|
|
739
|
+
const target = resolveSourceSpecifier(real, specifier);
|
|
740
|
+
if (target === null) {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
for (const name of collectSourceCommonJsExportNames(target, seen)) {
|
|
744
|
+
if (name !== "default" && name !== "__esModule" && !names.has(name)) {
|
|
745
|
+
names.add(name);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return names;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function collectExportStarSpecifiers(source: string): string[] {
|
|
753
|
+
const specifiers: string[] = [];
|
|
754
|
+
const pattern =
|
|
755
|
+
/^(\s*)__exportStar\(\s*require\((["'])([^"']+)\2\)\s*,\s*exports\s*\);/gm;
|
|
756
|
+
let match: RegExpExecArray | null;
|
|
757
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
758
|
+
specifiers.push(match[3]!);
|
|
759
|
+
}
|
|
760
|
+
return specifiers;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function resolveEmittedRequire(
|
|
764
|
+
emittedFile: string,
|
|
765
|
+
specifier: string,
|
|
766
|
+
): string | null {
|
|
767
|
+
if (!isRelativeSpecifier(specifier)) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const base = path.resolve(path.dirname(emittedFile), specifier);
|
|
771
|
+
if (path.extname(base).length !== 0) {
|
|
772
|
+
return isFile(base) ? base : null;
|
|
773
|
+
}
|
|
774
|
+
for (const extension of [".js", ".cjs", ".mjs"] as const) {
|
|
775
|
+
const candidate = base + extension;
|
|
776
|
+
if (isFile(candidate)) {
|
|
777
|
+
return candidate;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
for (const extension of [".js", ".cjs", ".mjs"] as const) {
|
|
781
|
+
const candidate = path.join(base, `index${extension}`);
|
|
782
|
+
if (isFile(candidate)) {
|
|
783
|
+
return candidate;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function resolveSourceSpecifier(
|
|
790
|
+
sourceFile: string,
|
|
791
|
+
specifier: string,
|
|
792
|
+
): string | null {
|
|
793
|
+
if (!isRelativeSpecifier(specifier)) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
const base = path.resolve(path.dirname(sourceFile), specifier);
|
|
797
|
+
if (path.extname(base).length !== 0) {
|
|
798
|
+
return isFile(base) ? base : null;
|
|
799
|
+
}
|
|
800
|
+
for (const extension of TYPESCRIPT_EXTENSIONS) {
|
|
801
|
+
const candidate = base + extension;
|
|
802
|
+
if (isFile(candidate)) {
|
|
803
|
+
return candidate;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
for (const extension of TYPESCRIPT_EXTENSIONS) {
|
|
807
|
+
const candidate = path.join(base, `index${extension}`);
|
|
808
|
+
if (isFile(candidate)) {
|
|
809
|
+
return candidate;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function isIdentifierName(name: string): boolean {
|
|
816
|
+
return /^[A-Za-z_$][\w$]*$/.test(name);
|
|
817
|
+
}
|
|
818
|
+
|
|
420
819
|
/**
|
|
421
820
|
* Serve the entry project's pre-built JavaScript for a source file the build
|
|
422
821
|
* emitted, or `null` when the file is outside the build or its emit is
|
|
@@ -429,7 +828,7 @@ function parseFirstEmittedFile(stdout: string): string | null {
|
|
|
429
828
|
* `rootDir` cannot have a mirrored emit, so it falls through to the dependency
|
|
430
829
|
* paths.
|
|
431
830
|
*/
|
|
432
|
-
function serveEntryEmit(real: string):
|
|
831
|
+
function serveEntryEmit(real: string): ServedSource | null {
|
|
433
832
|
const m = manifest();
|
|
434
833
|
if (m === null) {
|
|
435
834
|
return null;
|
|
@@ -443,7 +842,13 @@ function serveEntryEmit(real: string): string | null {
|
|
|
443
842
|
projectRoot: m.rootDir,
|
|
444
843
|
sourceFile: real,
|
|
445
844
|
});
|
|
446
|
-
|
|
845
|
+
if (emitted === null) {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
const source = readFileOrNull(emitted);
|
|
849
|
+
return source === null
|
|
850
|
+
? null
|
|
851
|
+
: { emittedFile: emitted, source, sourceFile: real };
|
|
447
852
|
}
|
|
448
853
|
|
|
449
854
|
/**
|
|
@@ -468,9 +873,7 @@ function isWithin(real: string, directory: string): boolean {
|
|
|
468
873
|
* tsconfig (transform plugins included), so a source-shipping package that
|
|
469
874
|
* needs a transform behaves correctly at runtime.
|
|
470
875
|
*/
|
|
471
|
-
function serveDependencyEmit(
|
|
472
|
-
real: string,
|
|
473
|
-
): { source: string; moduleOption?: string } | null {
|
|
876
|
+
function serveDependencyEmit(real: string): ServedSource | null {
|
|
474
877
|
const tsconfig = nearestTsconfig(real);
|
|
475
878
|
if (tsconfig === null) {
|
|
476
879
|
return null;
|
|
@@ -483,14 +886,35 @@ function serveDependencyEmit(
|
|
|
483
886
|
// this single file rather than failing the whole run.
|
|
484
887
|
return null;
|
|
485
888
|
}
|
|
889
|
+
const served = serveBuiltDependency(built, real);
|
|
890
|
+
if (served !== null) {
|
|
891
|
+
return served;
|
|
892
|
+
}
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function serveBuiltDependency(
|
|
897
|
+
built: BuiltProject,
|
|
898
|
+
real: string,
|
|
899
|
+
): ServedSource | null {
|
|
486
900
|
const emitted = resolveEmittedJavaScript({
|
|
487
901
|
emittedFiles: built.emittedFiles,
|
|
488
902
|
outDir: built.emitDir,
|
|
489
903
|
projectRoot: built.rootDir,
|
|
490
904
|
sourceFile: real,
|
|
491
905
|
});
|
|
906
|
+
if (emitted === null) {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
492
909
|
const source = readFileOrNull(emitted);
|
|
493
|
-
return source === null
|
|
910
|
+
return source === null
|
|
911
|
+
? null
|
|
912
|
+
: {
|
|
913
|
+
emittedFile: emitted,
|
|
914
|
+
moduleOption: built.moduleOption,
|
|
915
|
+
source,
|
|
916
|
+
sourceFile: real,
|
|
917
|
+
};
|
|
494
918
|
}
|
|
495
919
|
|
|
496
920
|
/** On-disk completion marker for a built dependency, shared across processes. */
|
|
@@ -516,15 +940,7 @@ function ensureProjectBuilt(tsconfig: string): BuiltProject {
|
|
|
516
940
|
if (cached !== undefined) {
|
|
517
941
|
return cached;
|
|
518
942
|
}
|
|
519
|
-
const
|
|
520
|
-
.createHash("sha256")
|
|
521
|
-
.update(tsconfig)
|
|
522
|
-
.digest("hex")
|
|
523
|
-
.slice(0, 16);
|
|
524
|
-
const root = dependencyCacheRoot();
|
|
525
|
-
const emitDir = path.join(root, key);
|
|
526
|
-
const metaPath = path.join(root, `${key}.json`);
|
|
527
|
-
const lockDir = path.join(root, `${key}.lock`);
|
|
943
|
+
const { emitDir, lockDir, metaPath, root } = dependencyCachePaths(tsconfig);
|
|
528
944
|
|
|
529
945
|
const reuse = readDependencyCache(emitDir, metaPath);
|
|
530
946
|
if (reuse !== null) {
|
|
@@ -540,6 +956,28 @@ function ensureProjectBuilt(tsconfig: string): BuiltProject {
|
|
|
540
956
|
return built;
|
|
541
957
|
}
|
|
542
958
|
|
|
959
|
+
interface DependencyCachePaths {
|
|
960
|
+
emitDir: string;
|
|
961
|
+
lockDir: string;
|
|
962
|
+
metaPath: string;
|
|
963
|
+
root: string;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function dependencyCachePaths(tsconfig: string): DependencyCachePaths {
|
|
967
|
+
const key = crypto
|
|
968
|
+
.createHash("sha256")
|
|
969
|
+
.update(tsconfig)
|
|
970
|
+
.digest("hex")
|
|
971
|
+
.slice(0, 16);
|
|
972
|
+
const root = dependencyCacheRoot();
|
|
973
|
+
return {
|
|
974
|
+
emitDir: path.join(root, key),
|
|
975
|
+
lockDir: path.join(root, `${key}.lock`),
|
|
976
|
+
metaPath: path.join(root, `${key}.json`),
|
|
977
|
+
root,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
543
981
|
/** Reuse a dependency another process (or an earlier import) already built. */
|
|
544
982
|
function readDependencyCache(
|
|
545
983
|
emitDir: string,
|
|
@@ -671,14 +1109,17 @@ function buildDependency(
|
|
|
671
1109
|
typeof project.compilerOptions.module === "string"
|
|
672
1110
|
? project.compilerOptions.module
|
|
673
1111
|
: undefined;
|
|
674
|
-
|
|
675
|
-
metaPath,
|
|
676
|
-
JSON.stringify({ moduleOption, rootDir } satisfies DependencyCacheMeta),
|
|
677
|
-
"utf8",
|
|
678
|
-
);
|
|
1112
|
+
writeDependencyMeta(metaPath, { moduleOption, rootDir });
|
|
679
1113
|
return { emitDir, emittedFiles: undefined, moduleOption, rootDir };
|
|
680
1114
|
}
|
|
681
1115
|
|
|
1116
|
+
function writeDependencyMeta(
|
|
1117
|
+
metaPath: string,
|
|
1118
|
+
meta: DependencyCacheMeta,
|
|
1119
|
+
): void {
|
|
1120
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta), "utf8");
|
|
1121
|
+
}
|
|
1122
|
+
|
|
682
1123
|
function dependencyCacheRoot(): string {
|
|
683
1124
|
const m = manifest();
|
|
684
1125
|
return m !== null && m.depCacheDir.length !== 0
|
|
@@ -688,6 +1129,7 @@ function dependencyCacheRoot(): string {
|
|
|
688
1129
|
|
|
689
1130
|
/** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
|
|
690
1131
|
const tsconfigCache = new Map<string, string | null>();
|
|
1132
|
+
const moduleOptionCache = new Map<string, string | undefined>();
|
|
691
1133
|
|
|
692
1134
|
/**
|
|
693
1135
|
* The nearest `tsconfig.json` at or above `file`'s directory, or `null`. The
|
|
@@ -240,15 +240,15 @@ function compileSourcePlugin(opts: {
|
|
|
240
240
|
*
|
|
241
241
|
* The lock is an atomic `mkdir` on `<cacheDir>.lock`. The winner builds and
|
|
242
242
|
* publishes the binary; every loser polls for that binary to appear and reuses
|
|
243
|
-
* it. A loser that waits past `PLUGIN_BUILD_LOCK_STEAL_MS` (the builder
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
243
|
+
* it. A loser that waits past `PLUGIN_BUILD_LOCK_STEAL_MS` (the builder crashed
|
|
244
|
+
* without publishing) steals the abandoned lock and retries. This is the same
|
|
245
|
+
* shape as `withBuildLock` in the runtime hooks, keyed on the plugin binary's
|
|
246
|
+
* existence instead of a JSON meta marker.
|
|
247
247
|
*
|
|
248
248
|
* Correctness still rests on `publishBuiltBinary`'s atomic rename: the lock is
|
|
249
|
-
* an optimisation to avoid duplicate work, not the only guard against a
|
|
250
|
-
*
|
|
251
|
-
*
|
|
249
|
+
* an optimisation to avoid duplicate work, not the only guard against a corrupt
|
|
250
|
+
* binary, so a stolen lock that races a slow builder cannot ship a half-written
|
|
251
|
+
* file.
|
|
252
252
|
*/
|
|
253
253
|
function buildUnderPluginLock(
|
|
254
254
|
cacheDir: string,
|
|
@@ -274,7 +274,10 @@ function buildUnderPluginLock(
|
|
|
274
274
|
// unlocked build — correctness is preserved by the atomic publish.
|
|
275
275
|
return build();
|
|
276
276
|
}
|
|
277
|
-
const waited = waitForPluginBinary(
|
|
277
|
+
const waited = waitForPluginBinary(
|
|
278
|
+
binaryPath,
|
|
279
|
+
PLUGIN_BUILD_LOCK_STEAL_MS,
|
|
280
|
+
);
|
|
278
281
|
if (waited) {
|
|
279
282
|
touchCacheEntry(cacheDir);
|
|
280
283
|
return binaryPath;
|