ttsc 0.26.2 → 0.27.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/README.md +9 -0
- package/cmd/ttsc/api_transform.go +17 -3
- package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
- package/cmd/ttscgraph/disk_realpath_other.go +16 -0
- package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
- package/cmd/ttscgraph/serve.go +104 -35
- package/driver/emit_plugin.go +20 -3
- package/driver/graph.go +64 -11
- package/driver/input_observation.go +273 -0
- package/driver/input_observation_test.go +149 -0
- package/driver/plugins.go +384 -4
- package/driver/program.go +54 -1
- package/driver/resolution_candidates.go +13 -5
- package/driver/rewrite.go +45 -2
- package/lib/TtscCompiler.d.ts +12 -11
- package/lib/TtscCompiler.js +44 -29
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/captureProcessOutput.js +2 -2
- package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +4 -3
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +1 -1
- package/lib/compiler/internal/runBuild.js +80 -34
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +2 -2
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
- package/lib/compiler/internal/sharedHostHelpers.js +39 -0
- package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +12 -4
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
- package/lib/compiler/internal/transformProjectInMemory.js +239 -35
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/schema.js +12 -5
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
- package/lib/internal/createCanonicalTempDirectory.js +42 -0
- package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
- package/lib/internal/projectInputPathIdentity.d.ts +3 -0
- package/lib/internal/projectInputPathIdentity.js +11 -5
- package/lib/internal/projectInputPathIdentity.js.map +1 -1
- package/lib/internal/resolveNodeBinary.d.ts +14 -0
- package/lib/internal/resolveNodeBinary.js +150 -0
- package/lib/internal/resolveNodeBinary.js.map +1 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
- package/lib/launcher/internal/prepareExecution.d.ts +3 -0
- package/lib/launcher/internal/prepareExecution.js +42 -14
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +25 -13
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.js +18 -3
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +2 -0
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +37 -1
- package/lib/launcher/internal/runtimeHooks.js +653 -62
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
- package/lib/plugin/internal/buildSourcePlugin.js +754 -72
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
- package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/register.d.ts +1 -0
- package/lib/register.js +46 -0
- package/lib/register.js.map +1 -0
- package/lib/structures/ITtscCompilerContext.d.ts +6 -3
- package/lib/structures/ITtscCompilerTransformation.d.ts +31 -0
- package/lib/structures/ITtscPlugin.d.ts +33 -0
- package/package.json +16 -9
- package/shim/checker/go.mod +9 -0
- package/shim/checker/go.sum +22 -0
- package/shim/checker/shim.go +75 -0
- package/shim/compiler/incremental.go +45 -8
- package/src/TtscCompiler.ts +55 -33
- package/src/compiler/internal/captureProcessOutput.ts +3 -2
- package/src/compiler/internal/compileProjectInMemory.ts +4 -3
- package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
- package/src/compiler/internal/runBuild.ts +102 -39
- package/src/compiler/internal/runSingleFileEmit.ts +2 -2
- package/src/compiler/internal/sharedHostHelpers.ts +44 -0
- package/src/compiler/internal/startResidentTransform.ts +12 -3
- package/src/compiler/internal/transformProjectInMemory.ts +410 -39
- package/src/flags/schema.ts +12 -5
- package/src/internal/createCanonicalTempDirectory.ts +53 -0
- package/src/internal/projectInputPathIdentity.ts +17 -4
- package/src/internal/resolveNodeBinary.ts +173 -0
- package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
- package/src/launcher/internal/prepareExecution.ts +50 -14
- package/src/launcher/internal/runTtsc.ts +24 -13
- package/src/launcher/internal/runTtscserver.ts +18 -3
- package/src/launcher/internal/runTtsx.ts +2 -0
- package/src/launcher/internal/runtimeHooks.ts +823 -66
- package/src/plugin/internal/buildSourcePlugin.ts +984 -79
- package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
- package/src/register.ts +47 -0
- package/src/structures/ITtscCompilerContext.ts +6 -3
- package/src/structures/ITtscCompilerTransformation.ts +39 -0
- package/src/structures/ITtscPlugin.ts +36 -0
- package/tools/shim_audit/baseline.json +0 -1
- package/utility/host.go +9 -3
- package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
- package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
- package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
- package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import {
|
|
4
4
|
Module,
|
|
5
|
+
createRequire,
|
|
5
6
|
isBuiltin,
|
|
6
7
|
registerHooks,
|
|
7
8
|
stripTypeScriptTypes,
|
|
@@ -15,6 +16,7 @@ import { resolveEmittedJavaScript } from "../../compiler/internal/resolveEmitted
|
|
|
15
16
|
import { resolveTsgo } from "../../compiler/internal/resolveTsgo";
|
|
16
17
|
import { runBuild } from "../../compiler/internal/runBuild";
|
|
17
18
|
import { outputText, spawnNative } from "../../compiler/internal/spawnNative";
|
|
19
|
+
import { createCanonicalTempDirectory } from "../../internal/createCanonicalTempDirectory";
|
|
18
20
|
import {
|
|
19
21
|
type FilesystemPathIdentityContext,
|
|
20
22
|
createFilesystemPathIdentityContext,
|
|
@@ -61,6 +63,13 @@ const RESOLVABLE_EXTENSIONS = [
|
|
|
61
63
|
".cjs",
|
|
62
64
|
] as const;
|
|
63
65
|
|
|
66
|
+
/** Union of the ttsx rescue probes and Node's built-in CommonJS probes. */
|
|
67
|
+
const DESCRIPTOR_PROBE_EXTENSIONS = [
|
|
68
|
+
...RESOLVABLE_EXTENSIONS,
|
|
69
|
+
".json",
|
|
70
|
+
".node",
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
64
73
|
/** TypeScript source extensions these hooks compile. */
|
|
65
74
|
const TYPESCRIPT_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"] as const;
|
|
66
75
|
|
|
@@ -117,7 +126,7 @@ type NextLoad = (url: string, context: LoadContext) => LoadResult;
|
|
|
117
126
|
* Runtime manifest written by `runTtsx` (the parent) and read once here. It
|
|
118
127
|
* describes the already-built entry project so the hooks can serve its emit.
|
|
119
128
|
*/
|
|
120
|
-
interface RuntimeManifest {
|
|
129
|
+
export interface RuntimeManifest {
|
|
121
130
|
/** Project root of the entry's owning tsconfig. */
|
|
122
131
|
projectRoot: string;
|
|
123
132
|
/** Source-tree root the emit mirrors (tsgo strips this prefix). */
|
|
@@ -126,6 +135,10 @@ interface RuntimeManifest {
|
|
|
126
135
|
emitDir: string;
|
|
127
136
|
/** Emitted file list from the entry build, for source→output matching. */
|
|
128
137
|
emittedFiles?: readonly string[];
|
|
138
|
+
/** Physical TypeScript root whose checked preparation created this manifest. */
|
|
139
|
+
entrySource?: string;
|
|
140
|
+
/** Exact JavaScript emitted for `entrySource`. */
|
|
141
|
+
entryFile?: string;
|
|
129
142
|
/**
|
|
130
143
|
* The entry tsconfig's `module` and `target`, deciding emit CJS/ESM per file.
|
|
131
144
|
* `target` is not decoration: an absent `module` makes tsgo derive the module
|
|
@@ -136,25 +149,34 @@ interface RuntimeManifest {
|
|
|
136
149
|
depCacheDir: string;
|
|
137
150
|
}
|
|
138
151
|
|
|
139
|
-
let
|
|
152
|
+
let environmentManifestCache: RuntimeManifest | null | undefined;
|
|
153
|
+
const registeredManifests: RuntimeManifest[] = [];
|
|
140
154
|
|
|
141
|
-
function
|
|
142
|
-
if (
|
|
143
|
-
return
|
|
155
|
+
function environmentManifest(): RuntimeManifest | null {
|
|
156
|
+
if (environmentManifestCache !== undefined) {
|
|
157
|
+
return environmentManifestCache;
|
|
144
158
|
}
|
|
145
159
|
const file = process.env.TTSX_RUNTIME_MANIFEST;
|
|
146
160
|
if (file === undefined || file.length === 0) {
|
|
147
|
-
|
|
148
|
-
return
|
|
161
|
+
environmentManifestCache = null;
|
|
162
|
+
return environmentManifestCache;
|
|
149
163
|
}
|
|
150
164
|
try {
|
|
151
|
-
|
|
165
|
+
environmentManifestCache = JSON.parse(
|
|
152
166
|
fs.readFileSync(file, "utf8"),
|
|
153
167
|
) as RuntimeManifest;
|
|
154
168
|
} catch {
|
|
155
|
-
|
|
169
|
+
environmentManifestCache = null;
|
|
156
170
|
}
|
|
157
|
-
return
|
|
171
|
+
return environmentManifestCache;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Every checked entry emit available to this runtime, in ownership order. */
|
|
175
|
+
function runtimeManifests(): readonly RuntimeManifest[] {
|
|
176
|
+
const inherited = environmentManifest();
|
|
177
|
+
return inherited === null
|
|
178
|
+
? registeredManifests
|
|
179
|
+
: [inherited, ...registeredManifests];
|
|
158
180
|
}
|
|
159
181
|
|
|
160
182
|
/**
|
|
@@ -235,6 +257,12 @@ function assertNodeRuntimeSupport(): void {
|
|
|
235
257
|
}
|
|
236
258
|
|
|
237
259
|
let installed = false;
|
|
260
|
+
let prepareRuntimeEntry: ((filename: string) => RuntimeManifest) | undefined;
|
|
261
|
+
|
|
262
|
+
export interface RuntimeHookOptions {
|
|
263
|
+
/** Prepare and type-check a TypeScript root discovered after registration. */
|
|
264
|
+
prepareEntry?: (filename: string) => RuntimeManifest;
|
|
265
|
+
}
|
|
238
266
|
|
|
239
267
|
/**
|
|
240
268
|
* Install the source-loading hooks on the current (main) thread. Idempotent:
|
|
@@ -249,7 +277,10 @@ let installed = false;
|
|
|
249
277
|
* graph goes through `Module._extensions` — the canonical loader extension
|
|
250
278
|
* point `ts-node`/`tsx` use for the same reason.
|
|
251
279
|
*/
|
|
252
|
-
export function installRuntimeHooks(): void {
|
|
280
|
+
export function installRuntimeHooks(options: RuntimeHookOptions = {}): void {
|
|
281
|
+
if (options.prepareEntry !== undefined) {
|
|
282
|
+
prepareRuntimeEntry = options.prepareEntry;
|
|
283
|
+
}
|
|
253
284
|
if (installed) {
|
|
254
285
|
return;
|
|
255
286
|
}
|
|
@@ -277,17 +308,31 @@ function installCommonJsHook(): void {
|
|
|
277
308
|
_extensions: Record<
|
|
278
309
|
string,
|
|
279
310
|
(
|
|
280
|
-
module: {
|
|
311
|
+
module: {
|
|
312
|
+
_compile(source: string, filename: string): void;
|
|
313
|
+
parent?: { filename?: string | null } | null;
|
|
314
|
+
},
|
|
281
315
|
filename: string,
|
|
282
316
|
) => void
|
|
283
317
|
>;
|
|
284
318
|
}
|
|
285
319
|
)._extensions;
|
|
286
320
|
const compile = (
|
|
287
|
-
module: {
|
|
321
|
+
module: {
|
|
322
|
+
_compile(source: string, filename: string): void;
|
|
323
|
+
parent?: { filename?: string | null } | null;
|
|
324
|
+
},
|
|
288
325
|
filename: string,
|
|
289
326
|
): void => {
|
|
290
|
-
module.
|
|
327
|
+
const parent = module.parent?.filename;
|
|
328
|
+
module._compile(
|
|
329
|
+
resolveServedSource(
|
|
330
|
+
filename,
|
|
331
|
+
pathToFileURL(filename).href,
|
|
332
|
+
typeof parent !== "string" || !isTypeScriptSource(parent),
|
|
333
|
+
).source,
|
|
334
|
+
filename,
|
|
335
|
+
);
|
|
291
336
|
};
|
|
292
337
|
for (const extension of [".ts", ".tsx", ".cts"]) {
|
|
293
338
|
extensions[extension] = compile;
|
|
@@ -301,15 +346,19 @@ function installCommonJsHook(): void {
|
|
|
301
346
|
* `require` or an ESM `import`.
|
|
302
347
|
*/
|
|
303
348
|
export function entryModuleFormat(entryFile: string): "module" | "commonjs" {
|
|
304
|
-
const
|
|
349
|
+
const real = realPath(entryFile);
|
|
350
|
+
const owner = findEntryEmit(real)?.manifest;
|
|
305
351
|
return moduleFormat(
|
|
306
352
|
entryFile,
|
|
307
|
-
|
|
353
|
+
owner === undefined ? null : (owner.moduleOptions ?? {}),
|
|
308
354
|
) === "module"
|
|
309
355
|
? "module"
|
|
310
356
|
: "commonjs";
|
|
311
357
|
}
|
|
312
358
|
|
|
359
|
+
/** TypeScript URLs resolved at a JavaScript-to-TypeScript entry boundary. */
|
|
360
|
+
const runtimeEntryUrls = new Set<string>();
|
|
361
|
+
|
|
313
362
|
/**
|
|
314
363
|
* Rescue an extensionless or directory relative specifier that Node's resolver
|
|
315
364
|
* rejected. Only runs after `nextResolve` throws, so a successful resolution is
|
|
@@ -321,24 +370,394 @@ function resolve(
|
|
|
321
370
|
context: ResolveContext,
|
|
322
371
|
nextResolve: NextResolve,
|
|
323
372
|
): ResolveResult {
|
|
373
|
+
recordPluginDescriptorResolutionCandidates(specifier, context.parentURL);
|
|
324
374
|
try {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
375
|
+
const result = rememberRuntimeEntry(
|
|
376
|
+
rememberCommonJsNamedInterop(
|
|
377
|
+
restoreStrippedNodeBuiltinScheme(
|
|
378
|
+
specifier,
|
|
379
|
+
nextResolve(specifier, context),
|
|
380
|
+
),
|
|
381
|
+
context,
|
|
329
382
|
),
|
|
330
383
|
context,
|
|
331
384
|
);
|
|
385
|
+
recordPluginDescriptorResolution(specifier, context.parentURL, result.url);
|
|
386
|
+
return result;
|
|
332
387
|
} catch (error) {
|
|
333
388
|
const rescued = probeRescuableSpecifier(specifier, context.parentURL);
|
|
334
389
|
if (rescued === null) {
|
|
335
390
|
throw error;
|
|
336
391
|
}
|
|
337
|
-
|
|
338
|
-
|
|
392
|
+
const result = rememberRuntimeEntry(
|
|
393
|
+
rememberCommonJsNamedInterop(
|
|
394
|
+
{ shortCircuit: true, url: rescued },
|
|
395
|
+
context,
|
|
396
|
+
),
|
|
339
397
|
context,
|
|
340
398
|
);
|
|
399
|
+
recordPluginDescriptorResolution(specifier, context.parentURL, result.url);
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Fingerprint every path whose state can redirect one descriptor import.
|
|
406
|
+
*
|
|
407
|
+
* This runs before the real resolver. Reporting candidates only after the
|
|
408
|
+
* descriptor finished would pair an earlier descriptor result with later file
|
|
409
|
+
* state when a higher-priority candidate appeared during evaluation.
|
|
410
|
+
*/
|
|
411
|
+
function recordPluginDescriptorResolutionCandidates(
|
|
412
|
+
specifier: string,
|
|
413
|
+
parentURL: string | undefined,
|
|
414
|
+
): void {
|
|
415
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
416
|
+
if (isBuiltin(specifier) || specifier.startsWith("node:")) return;
|
|
417
|
+
const parent = runtimeFilePath(parentURL);
|
|
418
|
+
if (parent === undefined) return;
|
|
419
|
+
const recorded = new Set<string>();
|
|
420
|
+
const record = (candidate: string): void => {
|
|
421
|
+
const resolved = path.resolve(candidate);
|
|
422
|
+
if (recorded.has(resolved)) return;
|
|
423
|
+
recorded.add(resolved);
|
|
424
|
+
recordPluginDescriptorInput({
|
|
425
|
+
parent,
|
|
426
|
+
resolved,
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
const recordManifestTargets = (
|
|
430
|
+
value: unknown,
|
|
431
|
+
directory: string,
|
|
432
|
+
allowBare: boolean = false,
|
|
433
|
+
): void => {
|
|
434
|
+
if (typeof value === "string") {
|
|
435
|
+
if (
|
|
436
|
+
value !== "" &&
|
|
437
|
+
(allowBare || value.startsWith("./") || value.startsWith("../"))
|
|
438
|
+
) {
|
|
439
|
+
recordBase(path.resolve(directory, value));
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (Array.isArray(value)) {
|
|
444
|
+
for (const item of value) {
|
|
445
|
+
recordManifestTargets(item, directory, allowBare);
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (value !== null && typeof value === "object") {
|
|
450
|
+
for (const item of Object.values(value)) {
|
|
451
|
+
recordManifestTargets(item, directory, allowBare);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
const recordPackageManifests = (file: string): void => {
|
|
456
|
+
for (
|
|
457
|
+
let directory = path.dirname(file);
|
|
458
|
+
;
|
|
459
|
+
directory = path.dirname(directory)
|
|
460
|
+
) {
|
|
461
|
+
const manifest = path.join(directory, "package.json");
|
|
462
|
+
record(manifest);
|
|
463
|
+
if (isFile(manifest)) return;
|
|
464
|
+
const parentDirectory = path.dirname(directory);
|
|
465
|
+
if (parentDirectory === directory) return;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const localBases = (value: string): string[] => {
|
|
469
|
+
if (value.startsWith("file:")) return [fileURLToPath(value)];
|
|
470
|
+
const raw = path.resolve(path.dirname(parent), value);
|
|
471
|
+
const suffixStart = value.search(/[?#]/);
|
|
472
|
+
if (suffixStart === -1) return [raw];
|
|
473
|
+
const pathname = value.slice(0, suffixStart);
|
|
474
|
+
return pathname === ""
|
|
475
|
+
? [raw]
|
|
476
|
+
: [...new Set([raw, path.resolve(path.dirname(parent), pathname)])];
|
|
477
|
+
};
|
|
478
|
+
const bases = new Set<string>();
|
|
479
|
+
const recordBase = (base: string): void => {
|
|
480
|
+
const resolvedBase = path.resolve(base);
|
|
481
|
+
if (bases.has(resolvedBase)) return;
|
|
482
|
+
bases.add(resolvedBase);
|
|
483
|
+
record(resolvedBase);
|
|
484
|
+
for (const candidate of typescriptSourcesForJavaScriptSpecifier(
|
|
485
|
+
resolvedBase,
|
|
486
|
+
)) {
|
|
487
|
+
record(candidate);
|
|
488
|
+
}
|
|
489
|
+
for (const extension of DESCRIPTOR_PROBE_EXTENSIONS) {
|
|
490
|
+
record(resolvedBase + extension);
|
|
491
|
+
}
|
|
492
|
+
const manifestFile = path.join(resolvedBase, "package.json");
|
|
493
|
+
record(manifestFile);
|
|
494
|
+
for (const extension of DESCRIPTOR_PROBE_EXTENSIONS) {
|
|
495
|
+
record(path.join(resolvedBase, `index${extension}`));
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
const manifest = JSON.parse(
|
|
499
|
+
fs.readFileSync(manifestFile, "utf8").replace(/^\uFEFF/, ""),
|
|
500
|
+
) as Record<string, unknown>;
|
|
501
|
+
recordManifestTargets(manifest.exports, resolvedBase);
|
|
502
|
+
recordManifestTargets(manifest.module, resolvedBase, true);
|
|
503
|
+
recordManifestTargets(manifest.main, resolvedBase, true);
|
|
504
|
+
} catch {
|
|
505
|
+
// The real resolver owns malformed package diagnostics.
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
if (
|
|
510
|
+
specifier.startsWith(".") ||
|
|
511
|
+
path.isAbsolute(specifier) ||
|
|
512
|
+
specifier.startsWith("file:")
|
|
513
|
+
) {
|
|
514
|
+
try {
|
|
515
|
+
for (const base of localBases(specifier)) {
|
|
516
|
+
recordPackageManifests(base);
|
|
517
|
+
if (isFile(base)) record(base);
|
|
518
|
+
else recordBase(base);
|
|
519
|
+
}
|
|
520
|
+
} catch {
|
|
521
|
+
// The real resolver owns invalid URL spellings.
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const parts = specifier.split("/");
|
|
527
|
+
const packageParts = parts[0]?.startsWith("@")
|
|
528
|
+
? parts.slice(0, 2)
|
|
529
|
+
: parts.slice(0, 1);
|
|
530
|
+
if (packageParts.some((part) => part === undefined || part === "")) return;
|
|
531
|
+
const packageName = packageParts.join("/");
|
|
532
|
+
const subpath = parts.slice(packageParts.length);
|
|
533
|
+
for (const searchPath of createRequire(parent).resolve.paths(specifier) ??
|
|
534
|
+
[]) {
|
|
535
|
+
const packageDirectory = path.join(searchPath, packageName);
|
|
536
|
+
recordBase(packageDirectory);
|
|
537
|
+
if (subpath.length !== 0) {
|
|
538
|
+
recordBase(path.join(packageDirectory, ...subpath));
|
|
539
|
+
}
|
|
540
|
+
// CommonJS resolution continues past an existing but unusable package
|
|
541
|
+
// directory. Record every search root before the resolver runs so a
|
|
542
|
+
// farther selected package retains evaluation-time hashes for its
|
|
543
|
+
// superseding candidates.
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Report one resolved descriptor edge to the parent loader. The channel is
|
|
549
|
+
* armed by the generated descriptor shim only after ttsx's own bootstrap and
|
|
550
|
+
* imports have loaded, keeping compiler implementation files out of the
|
|
551
|
+
* project's persistent-cache inputs.
|
|
552
|
+
*/
|
|
553
|
+
function recordPluginDescriptorResolution(
|
|
554
|
+
specifier: string,
|
|
555
|
+
parentURL: string | undefined,
|
|
556
|
+
resolvedURL: string,
|
|
557
|
+
): void {
|
|
558
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
559
|
+
const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
|
|
560
|
+
if (out === undefined || out.length === 0) return;
|
|
561
|
+
const resolved = runtimeFilePath(resolvedURL);
|
|
562
|
+
if (resolved === undefined) return;
|
|
563
|
+
const parent = runtimeFilePath(parentURL);
|
|
564
|
+
recordPluginDescriptorInput({
|
|
565
|
+
...(parent === undefined ? {} : { parent }),
|
|
566
|
+
resolved,
|
|
567
|
+
specifier,
|
|
568
|
+
});
|
|
569
|
+
for (
|
|
570
|
+
let directory = path.dirname(resolved);
|
|
571
|
+
;
|
|
572
|
+
directory = path.dirname(directory)
|
|
573
|
+
) {
|
|
574
|
+
const manifest = path.join(directory, "package.json");
|
|
575
|
+
recordPluginDescriptorInput({
|
|
576
|
+
...(parent === undefined ? {} : { parent }),
|
|
577
|
+
resolved: manifest,
|
|
578
|
+
});
|
|
579
|
+
if (isFile(manifest)) break;
|
|
580
|
+
const parentDirectory = path.dirname(directory);
|
|
581
|
+
if (parentDirectory === directory) break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function recordPluginDescriptorInput(record: {
|
|
586
|
+
hash?: string | null;
|
|
587
|
+
parent?: string;
|
|
588
|
+
realpath?: string | null;
|
|
589
|
+
resolved: string;
|
|
590
|
+
signature?: string;
|
|
591
|
+
specifier?: string;
|
|
592
|
+
unstable?: boolean;
|
|
593
|
+
}): void {
|
|
594
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
595
|
+
const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
|
|
596
|
+
if (out === undefined || out.length === 0) return;
|
|
597
|
+
recordPluginDescriptorInputOnce(record);
|
|
598
|
+
const resolved = path.resolve(record.resolved);
|
|
599
|
+
const parsed = path.parse(resolved);
|
|
600
|
+
let current = parsed.root;
|
|
601
|
+
const relative = path.relative(parsed.root, resolved);
|
|
602
|
+
for (const segment of relative.split(path.sep).slice(0, -1)) {
|
|
603
|
+
if (segment === "") continue;
|
|
604
|
+
current = path.join(current, segment);
|
|
605
|
+
try {
|
|
606
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
607
|
+
recordPluginDescriptorInputOnce({ resolved: current });
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Record one path without recursively revisiting its lexical ancestors. */
|
|
616
|
+
function recordPluginDescriptorInputOnce(record: {
|
|
617
|
+
hash?: string | null;
|
|
618
|
+
parent?: string;
|
|
619
|
+
realpath?: string | null;
|
|
620
|
+
resolved: string;
|
|
621
|
+
signature?: string;
|
|
622
|
+
specifier?: string;
|
|
623
|
+
unstable?: boolean;
|
|
624
|
+
}): void {
|
|
625
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
626
|
+
const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
|
|
627
|
+
if (out === undefined || out.length === 0) return;
|
|
628
|
+
try {
|
|
629
|
+
const beforeSignature = pluginDescriptorInputMetadataSignature(
|
|
630
|
+
record.resolved,
|
|
631
|
+
);
|
|
632
|
+
const observedHash = pluginDescriptorInputHash(record.resolved);
|
|
633
|
+
const observedRealpath = pluginDescriptorInputRealpath(record.resolved);
|
|
634
|
+
const afterSignature = pluginDescriptorInputMetadataSignature(
|
|
635
|
+
record.resolved,
|
|
636
|
+
);
|
|
637
|
+
const unstable =
|
|
638
|
+
record.unstable === true ||
|
|
639
|
+
beforeSignature === undefined ||
|
|
640
|
+
afterSignature === undefined ||
|
|
641
|
+
beforeSignature !== afterSignature ||
|
|
642
|
+
(record.hash !== undefined && record.hash !== observedHash) ||
|
|
643
|
+
(record.realpath !== undefined && record.realpath !== observedRealpath) ||
|
|
644
|
+
(record.signature !== undefined && record.signature !== afterSignature);
|
|
645
|
+
fs.appendFileSync(
|
|
646
|
+
out,
|
|
647
|
+
`${JSON.stringify({
|
|
648
|
+
...record,
|
|
649
|
+
hash: observedHash,
|
|
650
|
+
realpath: observedRealpath,
|
|
651
|
+
...(unstable ? { unstable: true } : { signature: afterSignature }),
|
|
652
|
+
})}\n`,
|
|
653
|
+
"utf8",
|
|
654
|
+
);
|
|
655
|
+
} catch {
|
|
656
|
+
// Dependency reporting is advisory to cache reuse; the selected entry is
|
|
657
|
+
// still retained by the parent if this best-effort side channel fails.
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function pluginDescriptorInputRealpath(file: string): string | null {
|
|
662
|
+
try {
|
|
663
|
+
return fs.realpathSync.native(file);
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Metadata identity that exposes content-preserving A-B-A replacement. */
|
|
670
|
+
function pluginDescriptorInputMetadataSignature(
|
|
671
|
+
file: string,
|
|
672
|
+
): string | undefined {
|
|
673
|
+
const requested = path.resolve(file);
|
|
674
|
+
let current = requested;
|
|
675
|
+
for (;;) {
|
|
676
|
+
try {
|
|
677
|
+
const link = fs.lstatSync(current, { bigint: true });
|
|
678
|
+
let target = link;
|
|
679
|
+
if (link.isSymbolicLink()) {
|
|
680
|
+
try {
|
|
681
|
+
target = fs.statSync(current, { bigint: true });
|
|
682
|
+
} catch {
|
|
683
|
+
// A broken link's own metadata cannot expose its target appearing
|
|
684
|
+
// and disappearing during evaluation. Decline cache proof instead.
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return [
|
|
689
|
+
path.relative(current, requested),
|
|
690
|
+
link.dev,
|
|
691
|
+
link.ino,
|
|
692
|
+
link.mode,
|
|
693
|
+
link.size,
|
|
694
|
+
link.mtimeNs,
|
|
695
|
+
link.ctimeNs,
|
|
696
|
+
target.dev,
|
|
697
|
+
target.ino,
|
|
698
|
+
target.mode,
|
|
699
|
+
target.size,
|
|
700
|
+
target.mtimeNs,
|
|
701
|
+
target.ctimeNs,
|
|
702
|
+
].join(":");
|
|
703
|
+
} catch (error) {
|
|
704
|
+
if (!isMissingPathError(error)) return undefined;
|
|
705
|
+
const parent = path.dirname(current);
|
|
706
|
+
if (parent === current) return undefined;
|
|
707
|
+
current = parent;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function pluginDescriptorInputHash(file: string): string | null {
|
|
713
|
+
try {
|
|
714
|
+
if (fs.statSync(file).isDirectory()) {
|
|
715
|
+
return pluginDescriptorDirectoryHash();
|
|
716
|
+
}
|
|
717
|
+
return crypto
|
|
718
|
+
.createHash("sha256")
|
|
719
|
+
.update(fs.readFileSync(file))
|
|
720
|
+
.digest("hex");
|
|
721
|
+
} catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** Stable public fingerprint for an existing directory candidate's kind. */
|
|
727
|
+
function pluginDescriptorDirectoryHash(): string {
|
|
728
|
+
return crypto
|
|
729
|
+
.createHash("sha256")
|
|
730
|
+
.update("ttsc:host-input:directory\0")
|
|
731
|
+
.digest("hex");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function runtimeFilePath(value: string | undefined): string | undefined {
|
|
735
|
+
if (value === undefined) return undefined;
|
|
736
|
+
if (value.startsWith("file:")) {
|
|
737
|
+
try {
|
|
738
|
+
return path.resolve(fileURLToPath(value));
|
|
739
|
+
} catch {
|
|
740
|
+
return undefined;
|
|
741
|
+
}
|
|
341
742
|
}
|
|
743
|
+
return path.isAbsolute(value) ? path.resolve(value) : undefined;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Remember an ESM root until its synchronous load hook prepares the project. */
|
|
747
|
+
function rememberRuntimeEntry(
|
|
748
|
+
result: ResolveResult,
|
|
749
|
+
context: ResolveContext,
|
|
750
|
+
): ResolveResult {
|
|
751
|
+
if (
|
|
752
|
+
result.url.startsWith("file:") &&
|
|
753
|
+
isTypeScriptSource(fileURLToPath(result.url)) &&
|
|
754
|
+
(context.parentURL === undefined ||
|
|
755
|
+
!context.parentURL.startsWith("file:") ||
|
|
756
|
+
!isTypeScriptSource(fileURLToPath(context.parentURL)))
|
|
757
|
+
) {
|
|
758
|
+
runtimeEntryUrls.add(result.url);
|
|
759
|
+
}
|
|
760
|
+
return result;
|
|
342
761
|
}
|
|
343
762
|
|
|
344
763
|
/**
|
|
@@ -369,6 +788,12 @@ interface BuiltProject {
|
|
|
369
788
|
moduleOptions: OwningModuleOptions;
|
|
370
789
|
}
|
|
371
790
|
const builtProjects = new Map<string, BuiltProject>();
|
|
791
|
+
// A descriptor evaluator's dependency emit must never be reused by another
|
|
792
|
+
// process. PIDs are eventually recycled while the disk cache persists, so PID
|
|
793
|
+
// alone cannot provide that isolation. One cryptographically random process
|
|
794
|
+
// nonce keeps every evaluator generation distinct while `builtProjects` still
|
|
795
|
+
// shares repeated imports inside this process.
|
|
796
|
+
const descriptorProcessCacheNonce = crypto.randomBytes(16).toString("hex");
|
|
372
797
|
|
|
373
798
|
/** File URLs whose CommonJS source was reached from an ESM parent import. */
|
|
374
799
|
const commonJsNamedInteropUrls = new Set<string>();
|
|
@@ -386,7 +811,11 @@ function load(
|
|
|
386
811
|
if (!isTypeScriptSource(filename)) {
|
|
387
812
|
return nextLoad(url, context);
|
|
388
813
|
}
|
|
389
|
-
const { format, source } = resolveRuntimeSource(
|
|
814
|
+
const { format, source } = resolveRuntimeSource(
|
|
815
|
+
filename,
|
|
816
|
+
url,
|
|
817
|
+
runtimeEntryUrls.delete(url) || isProcessEntry(filename),
|
|
818
|
+
);
|
|
390
819
|
return {
|
|
391
820
|
format,
|
|
392
821
|
shortCircuit: true,
|
|
@@ -397,8 +826,9 @@ function load(
|
|
|
397
826
|
function resolveRuntimeSource(
|
|
398
827
|
filename: string,
|
|
399
828
|
url: string = pathToFileURL(filename).href,
|
|
829
|
+
prepareAsEntry: boolean = false,
|
|
400
830
|
): { format: string; source: string } {
|
|
401
|
-
const served = resolveServedSource(filename, url);
|
|
831
|
+
const served = resolveServedSource(filename, url, prepareAsEntry);
|
|
402
832
|
const format = moduleFormat(filename, served.moduleOptions);
|
|
403
833
|
return {
|
|
404
834
|
format,
|
|
@@ -413,6 +843,16 @@ function resolveRuntimeSource(
|
|
|
413
843
|
};
|
|
414
844
|
}
|
|
415
845
|
|
|
846
|
+
/** Whether `filename` is the TypeScript main module named on Node's argv. */
|
|
847
|
+
function isProcessEntry(filename: string): boolean {
|
|
848
|
+
const entry = process.argv[1];
|
|
849
|
+
return (
|
|
850
|
+
entry !== undefined &&
|
|
851
|
+
isTypeScriptSource(entry) &&
|
|
852
|
+
realPath(path.resolve(entry)) === realPath(filename)
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
416
856
|
function rememberCommonJsNamedInterop(
|
|
417
857
|
result: ResolveResult,
|
|
418
858
|
context: ResolveContext,
|
|
@@ -467,9 +907,9 @@ function owningModuleOptions(filename: string): OwningModuleOptions | null {
|
|
|
467
907
|
return null;
|
|
468
908
|
}
|
|
469
909
|
const real = realPath(filename);
|
|
470
|
-
const
|
|
471
|
-
if (
|
|
472
|
-
return
|
|
910
|
+
const owner = findEntryEmit(real)?.manifest;
|
|
911
|
+
if (owner !== undefined) {
|
|
912
|
+
return owner.moduleOptions ?? {};
|
|
473
913
|
}
|
|
474
914
|
const tsconfig = nearestTsconfig(real);
|
|
475
915
|
if (tsconfig === null) {
|
|
@@ -481,10 +921,7 @@ function owningModuleOptions(filename: string): OwningModuleOptions | null {
|
|
|
481
921
|
}
|
|
482
922
|
let options: OwningModuleOptions = {};
|
|
483
923
|
try {
|
|
484
|
-
const project =
|
|
485
|
-
cwd: path.dirname(tsconfig),
|
|
486
|
-
tsconfig,
|
|
487
|
-
});
|
|
924
|
+
const project = readPluginDescriptorProjectConfig(tsconfig);
|
|
488
925
|
options = projectModuleOptions(project.compilerOptions);
|
|
489
926
|
} catch {
|
|
490
927
|
// The owning project cannot be read, so nothing is known about the format
|
|
@@ -498,6 +935,147 @@ function owningModuleOptions(filename: string): OwningModuleOptions | null {
|
|
|
498
935
|
return options;
|
|
499
936
|
}
|
|
500
937
|
|
|
938
|
+
/**
|
|
939
|
+
* Read one owning config while its discovered chain stays unchanged.
|
|
940
|
+
*
|
|
941
|
+
* A preliminary read discovers `extends`; an accepted read is bracketed by
|
|
942
|
+
* equal fingerprints over that exact chain. If churn never settles, ttsx can
|
|
943
|
+
* still execute the last project but reports every observed config as unstable,
|
|
944
|
+
* preventing a later snapshot from certifying the torn result.
|
|
945
|
+
*/
|
|
946
|
+
function readPluginDescriptorProjectConfig(
|
|
947
|
+
tsconfig: string,
|
|
948
|
+
): ReturnType<typeof readProjectConfig> {
|
|
949
|
+
const read = () =>
|
|
950
|
+
readProjectConfig({ cwd: path.dirname(tsconfig), tsconfig });
|
|
951
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return read();
|
|
952
|
+
|
|
953
|
+
const observed = new Set<string>([path.resolve(tsconfig)]);
|
|
954
|
+
let project: ReturnType<typeof readProjectConfig>;
|
|
955
|
+
try {
|
|
956
|
+
project = read();
|
|
957
|
+
} catch (error) {
|
|
958
|
+
recordPluginDescriptorProjectInputs(observed, true);
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
961
|
+
let inputs = normalizedProjectConfigPaths(project, tsconfig);
|
|
962
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
963
|
+
for (const input of inputs) observed.add(input);
|
|
964
|
+
const before = pluginDescriptorInputHashes(inputs);
|
|
965
|
+
const beforeRealpaths = pluginDescriptorInputRealpaths(inputs);
|
|
966
|
+
const beforeSignatures = pluginDescriptorInputMetadataSignatures(inputs);
|
|
967
|
+
let candidate: ReturnType<typeof readProjectConfig>;
|
|
968
|
+
try {
|
|
969
|
+
candidate = read();
|
|
970
|
+
} catch (error) {
|
|
971
|
+
recordPluginDescriptorProjectInputs(observed, true);
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
974
|
+
const candidateInputs = normalizedProjectConfigPaths(candidate, tsconfig);
|
|
975
|
+
for (const input of candidateInputs) observed.add(input);
|
|
976
|
+
const after = pluginDescriptorInputHashes(candidateInputs);
|
|
977
|
+
const afterRealpaths = pluginDescriptorInputRealpaths(candidateInputs);
|
|
978
|
+
const afterSignatures =
|
|
979
|
+
pluginDescriptorInputMetadataSignatures(candidateInputs);
|
|
980
|
+
if (
|
|
981
|
+
equalPluginDescriptorInputLists(inputs, candidateInputs) &&
|
|
982
|
+
equalPluginDescriptorInputHashes(before, after) &&
|
|
983
|
+
equalPluginDescriptorInputHashes(beforeRealpaths, afterRealpaths) &&
|
|
984
|
+
beforeSignatures !== undefined &&
|
|
985
|
+
afterSignatures !== undefined &&
|
|
986
|
+
equalPluginDescriptorInputHashes(beforeSignatures, afterSignatures)
|
|
987
|
+
) {
|
|
988
|
+
for (const input of candidateInputs) {
|
|
989
|
+
recordPluginDescriptorInput({
|
|
990
|
+
hash: after[input]!,
|
|
991
|
+
realpath: afterRealpaths[input]!,
|
|
992
|
+
resolved: input,
|
|
993
|
+
signature: afterSignatures[input]!,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
return candidate;
|
|
997
|
+
}
|
|
998
|
+
project = candidate;
|
|
999
|
+
inputs = candidateInputs;
|
|
1000
|
+
}
|
|
1001
|
+
recordPluginDescriptorProjectInputs(observed, true);
|
|
1002
|
+
return project;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function normalizedProjectConfigPaths(
|
|
1006
|
+
project: ReturnType<typeof readProjectConfig>,
|
|
1007
|
+
requestedConfig: string,
|
|
1008
|
+
): string[] {
|
|
1009
|
+
return [
|
|
1010
|
+
...new Set(
|
|
1011
|
+
[requestedConfig, ...project.configPaths].map((file) =>
|
|
1012
|
+
path.resolve(file),
|
|
1013
|
+
),
|
|
1014
|
+
),
|
|
1015
|
+
].sort();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function pluginDescriptorInputHashes(
|
|
1019
|
+
inputs: readonly string[],
|
|
1020
|
+
): Record<string, string | null> {
|
|
1021
|
+
return Object.fromEntries(
|
|
1022
|
+
inputs.map((input) => [input, pluginDescriptorInputHash(input)]),
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function pluginDescriptorInputRealpaths(
|
|
1027
|
+
inputs: readonly string[],
|
|
1028
|
+
): Record<string, string | null> {
|
|
1029
|
+
return Object.fromEntries(
|
|
1030
|
+
inputs.map((input) => [input, pluginDescriptorInputRealpath(input)]),
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function pluginDescriptorInputMetadataSignatures(
|
|
1035
|
+
inputs: readonly string[],
|
|
1036
|
+
): Record<string, string> | undefined {
|
|
1037
|
+
const output: Record<string, string> = {};
|
|
1038
|
+
for (const input of inputs) {
|
|
1039
|
+
const signature = pluginDescriptorInputMetadataSignature(input);
|
|
1040
|
+
if (signature === undefined) return undefined;
|
|
1041
|
+
output[path.resolve(input)] = signature;
|
|
1042
|
+
}
|
|
1043
|
+
return output;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function equalPluginDescriptorInputLists(
|
|
1047
|
+
first: readonly string[],
|
|
1048
|
+
second: readonly string[],
|
|
1049
|
+
): boolean {
|
|
1050
|
+
return (
|
|
1051
|
+
first.length === second.length &&
|
|
1052
|
+
first.every((input, index) => input === second[index])
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function equalPluginDescriptorInputHashes(
|
|
1057
|
+
first: Readonly<Record<string, string | null>>,
|
|
1058
|
+
second: Readonly<Record<string, string | null>>,
|
|
1059
|
+
): boolean {
|
|
1060
|
+
return (
|
|
1061
|
+
Object.keys(first).length === Object.keys(second).length &&
|
|
1062
|
+
Object.entries(first).every(([input, hash]) => second[input] === hash)
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function recordPluginDescriptorProjectInputs(
|
|
1067
|
+
inputs: Iterable<string>,
|
|
1068
|
+
unstable: boolean,
|
|
1069
|
+
): void {
|
|
1070
|
+
for (const input of inputs) {
|
|
1071
|
+
const resolved = path.resolve(input);
|
|
1072
|
+
recordPluginDescriptorInput({
|
|
1073
|
+
resolved,
|
|
1074
|
+
...(unstable ? { unstable: true } : {}),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
501
1079
|
/** Narrow a resolved project's compiler options to the emit-format pair. */
|
|
502
1080
|
export function projectModuleOptions(
|
|
503
1081
|
compilerOptions: Record<string, unknown>,
|
|
@@ -522,12 +1100,32 @@ export function projectModuleOptions(
|
|
|
522
1100
|
function resolveServedSource(
|
|
523
1101
|
filename: string,
|
|
524
1102
|
url: string = pathToFileURL(filename).href,
|
|
1103
|
+
prepareAsEntry: boolean = false,
|
|
525
1104
|
): ServedSource {
|
|
526
1105
|
const real = realPath(filename);
|
|
527
|
-
|
|
1106
|
+
// Only the public preload can prepare a newly discovered root. Direct ttsx
|
|
1107
|
+
// has one pre-built manifest and historically relies on trailing-stem
|
|
1108
|
+
// recovery when Windows presents the same source through its short and long
|
|
1109
|
+
// temp-path spellings (the lint TypeScript-config loader is one such case).
|
|
1110
|
+
// Treating that boundary as prepare-only would discard the existing emit and
|
|
1111
|
+
// feed ESM source into CommonJS interop, producing ERR_REQUIRE_CYCLE_MODULE.
|
|
1112
|
+
const prepareEntry = prepareAsEntry ? prepareRuntimeEntry : undefined;
|
|
1113
|
+
// A JavaScript-to-TypeScript boundary is a new checked root unless an
|
|
1114
|
+
// existing manifest proves exact ownership. Trailing-stem recovery is not
|
|
1115
|
+
// ownership evidence: two out-of-include `index.ts` roots can otherwise map
|
|
1116
|
+
// to the first manifest's `index.js`, skipping the second root's diagnostics.
|
|
1117
|
+
let served = serveEntryEmit(real, prepareEntry === undefined);
|
|
528
1118
|
if (served !== null) {
|
|
529
1119
|
return withInlineSourceMap(served);
|
|
530
1120
|
}
|
|
1121
|
+
if (prepareEntry !== undefined) {
|
|
1122
|
+
registeredManifests.push(prepareEntry(real));
|
|
1123
|
+
served = serveEntryEmit(real);
|
|
1124
|
+
if (served === null) {
|
|
1125
|
+
throw new Error(`ttsx: prepared entry emit not found for ${filename}`);
|
|
1126
|
+
}
|
|
1127
|
+
return withInlineSourceMap(served);
|
|
1128
|
+
}
|
|
531
1129
|
const built = serveDependencyEmit(real);
|
|
532
1130
|
if (built !== null) {
|
|
533
1131
|
return withInlineSourceMap(built);
|
|
@@ -610,7 +1208,7 @@ function emitOrphanAsCommonJs(filename: string): string | null {
|
|
|
610
1208
|
return hit;
|
|
611
1209
|
}
|
|
612
1210
|
}
|
|
613
|
-
const outDir =
|
|
1211
|
+
const outDir = createCanonicalTempDirectory("ttsx-orphan-");
|
|
614
1212
|
try {
|
|
615
1213
|
const res = spawnNative(
|
|
616
1214
|
tsgo,
|
|
@@ -673,7 +1271,7 @@ function emitCommonJsForNameScan(filename: string): string | null {
|
|
|
673
1271
|
commonJsNameScanSources.set(real, null);
|
|
674
1272
|
return null;
|
|
675
1273
|
}
|
|
676
|
-
const outDir =
|
|
1274
|
+
const outDir = createCanonicalTempDirectory("ttsx-export-scan-");
|
|
677
1275
|
try {
|
|
678
1276
|
const res = spawnNative(
|
|
679
1277
|
tsgo,
|
|
@@ -1166,45 +1764,95 @@ function isIdentifierName(name: string): boolean {
|
|
|
1166
1764
|
* `rootDir` cannot have a mirrored emit, so it falls through to the dependency
|
|
1167
1765
|
* paths.
|
|
1168
1766
|
*/
|
|
1169
|
-
function serveEntryEmit(
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
if (emitted === null) {
|
|
1767
|
+
function serveEntryEmit(
|
|
1768
|
+
real: string,
|
|
1769
|
+
allowStemFallback: boolean = true,
|
|
1770
|
+
): ServedSource | null {
|
|
1771
|
+
const owner = findEntryEmit(real, allowStemFallback);
|
|
1772
|
+
if (owner === null) {
|
|
1176
1773
|
return null;
|
|
1177
1774
|
}
|
|
1178
|
-
const source = readFileOrNull(
|
|
1775
|
+
const source = readFileOrNull(owner.emittedFile);
|
|
1179
1776
|
return source === null
|
|
1180
1777
|
? null
|
|
1181
1778
|
: {
|
|
1182
|
-
emittedFile:
|
|
1183
|
-
moduleOptions:
|
|
1779
|
+
emittedFile: owner.emittedFile,
|
|
1780
|
+
moduleOptions: owner.manifest.moduleOptions ?? {},
|
|
1184
1781
|
source,
|
|
1185
1782
|
sourceFile: real,
|
|
1186
1783
|
};
|
|
1187
1784
|
}
|
|
1188
1785
|
|
|
1786
|
+
/**
|
|
1787
|
+
* Find the manifest that owns `real`. Explicit prepared roots win, followed by
|
|
1788
|
+
* exact mirrored paths across every manifest. Only then may legacy stem
|
|
1789
|
+
* recovery run, so one manifest's approximate match cannot shadow another's
|
|
1790
|
+
* exact emit.
|
|
1791
|
+
*/
|
|
1792
|
+
function findEntryEmit(
|
|
1793
|
+
real: string,
|
|
1794
|
+
allowStemFallback: boolean = true,
|
|
1795
|
+
): { emittedFile: string; manifest: RuntimeManifest } | null {
|
|
1796
|
+
const manifests = runtimeManifests();
|
|
1797
|
+
for (const candidate of manifests) {
|
|
1798
|
+
if (
|
|
1799
|
+
candidate.entrySource !== undefined &&
|
|
1800
|
+
candidate.entryFile !== undefined &&
|
|
1801
|
+
realPath(candidate.entrySource) === real &&
|
|
1802
|
+
fs.existsSync(candidate.entryFile)
|
|
1803
|
+
) {
|
|
1804
|
+
return { emittedFile: candidate.entryFile, manifest: candidate };
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
for (const candidate of manifests) {
|
|
1808
|
+
const emitted = entryEmitPath(candidate, real, false);
|
|
1809
|
+
if (emitted !== null) {
|
|
1810
|
+
return { emittedFile: emitted, manifest: candidate };
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
if (!allowStemFallback) {
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
for (const candidate of manifests) {
|
|
1817
|
+
const emitted = entryEmitPath(candidate, real, true);
|
|
1818
|
+
if (emitted !== null) {
|
|
1819
|
+
return { emittedFile: emitted, manifest: candidate };
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1189
1825
|
/**
|
|
1190
1826
|
* The entry project's emitted JavaScript for `real`, or `null` when that
|
|
1191
1827
|
* project did not emit it. Shared with `owningModuleOptions` so "the entry
|
|
1192
1828
|
* project owns this file" means exactly one thing in both places.
|
|
1193
1829
|
*/
|
|
1194
|
-
function entryEmitPath(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1830
|
+
function entryEmitPath(
|
|
1831
|
+
m: RuntimeManifest,
|
|
1832
|
+
real: string,
|
|
1833
|
+
allowStemFallback: boolean = true,
|
|
1834
|
+
): string | null {
|
|
1835
|
+
const cache = allowStemFallback
|
|
1836
|
+
? entryEmitPathCache
|
|
1837
|
+
: exactEntryEmitPathCache;
|
|
1838
|
+
let manifestCache = cache.get(m);
|
|
1839
|
+
if (manifestCache === undefined) {
|
|
1840
|
+
manifestCache = new Map<string, string | null>();
|
|
1841
|
+
cache.set(m, manifestCache);
|
|
1842
|
+
}
|
|
1843
|
+
if (manifestCache.has(real)) {
|
|
1844
|
+
return manifestCache.get(real) ?? null;
|
|
1198
1845
|
}
|
|
1199
1846
|
const resolved = isWithin(real, m.rootDir)
|
|
1200
1847
|
? resolveEmittedJavaScript({
|
|
1848
|
+
allowStemFallback,
|
|
1201
1849
|
emittedFiles: m.emittedFiles,
|
|
1202
1850
|
outDir: m.emitDir,
|
|
1203
1851
|
projectRoot: m.rootDir,
|
|
1204
1852
|
sourceFile: real,
|
|
1205
1853
|
})
|
|
1206
1854
|
: null;
|
|
1207
|
-
|
|
1855
|
+
manifestCache.set(real, resolved);
|
|
1208
1856
|
return resolved;
|
|
1209
1857
|
}
|
|
1210
1858
|
|
|
@@ -1212,10 +1860,18 @@ function entryEmitPath(m: RuntimeManifest, real: string): string | null {
|
|
|
1212
1860
|
* Memo for `entryEmitPath`, because it is now on the `resolve` hook's path
|
|
1213
1861
|
* through `owningModuleOptions` — once per import specifier — and a miss inside
|
|
1214
1862
|
* `resolveEmittedJavaScript` walks the whole emit tree. The entry emit is
|
|
1215
|
-
* written once before the run starts and never changes under it
|
|
1216
|
-
*
|
|
1863
|
+
* written once before the run starts and never changes under it. Registration
|
|
1864
|
+
* can add several independent entry emits, so the manifest is part of the cache
|
|
1865
|
+
* identity as well as the source path.
|
|
1217
1866
|
*/
|
|
1218
|
-
const entryEmitPathCache = new
|
|
1867
|
+
const entryEmitPathCache = new WeakMap<
|
|
1868
|
+
RuntimeManifest,
|
|
1869
|
+
Map<string, string | null>
|
|
1870
|
+
>();
|
|
1871
|
+
const exactEntryEmitPathCache = new WeakMap<
|
|
1872
|
+
RuntimeManifest,
|
|
1873
|
+
Map<string, string | null>
|
|
1874
|
+
>();
|
|
1219
1875
|
|
|
1220
1876
|
/**
|
|
1221
1877
|
* True when `real` is `directory` itself or sits beneath it. Handles a root
|
|
@@ -1351,11 +2007,7 @@ interface DependencyCachePaths {
|
|
|
1351
2007
|
}
|
|
1352
2008
|
|
|
1353
2009
|
function dependencyCachePaths(tsconfig: string): DependencyCachePaths {
|
|
1354
|
-
const key =
|
|
1355
|
-
.createHash("sha256")
|
|
1356
|
-
.update(tsconfig)
|
|
1357
|
-
.digest("hex")
|
|
1358
|
-
.slice(0, 16);
|
|
2010
|
+
const key = dependencyCacheKey(tsconfig);
|
|
1359
2011
|
const root = dependencyCacheRoot();
|
|
1360
2012
|
return {
|
|
1361
2013
|
cacheDir: path.join(root, key),
|
|
@@ -1365,6 +2017,38 @@ function dependencyCachePaths(tsconfig: string): DependencyCachePaths {
|
|
|
1365
2017
|
};
|
|
1366
2018
|
}
|
|
1367
2019
|
|
|
2020
|
+
/** Derive one dependency cache key; exported for isolation regressions. */
|
|
2021
|
+
export function dependencyCacheKey(
|
|
2022
|
+
tsconfig: string,
|
|
2023
|
+
options: {
|
|
2024
|
+
descriptorLoad?: boolean;
|
|
2025
|
+
descriptorNonce?: string;
|
|
2026
|
+
} = {},
|
|
2027
|
+
): string {
|
|
2028
|
+
const descriptorLoad =
|
|
2029
|
+
options.descriptorLoad ?? process.env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1";
|
|
2030
|
+
return (
|
|
2031
|
+
crypto
|
|
2032
|
+
.createHash("sha256")
|
|
2033
|
+
.update(tsconfig)
|
|
2034
|
+
// Descriptor evaluation promises a result bound to this process's exact
|
|
2035
|
+
// input observations. Reusing an emit another evaluator built can pair
|
|
2036
|
+
// that process's old source/config bytes with this process's later hashes.
|
|
2037
|
+
// Keep ordinary ttsx worker sharing, but isolate descriptor builds with a
|
|
2038
|
+
// non-reusable process nonce; the in-process `builtProjects` map still
|
|
2039
|
+
// compiles each owning project once.
|
|
2040
|
+
.update(
|
|
2041
|
+
descriptorLoad
|
|
2042
|
+
? `\0descriptor-process:${
|
|
2043
|
+
options.descriptorNonce ?? descriptorProcessCacheNonce
|
|
2044
|
+
}`
|
|
2045
|
+
: "",
|
|
2046
|
+
)
|
|
2047
|
+
.digest("hex")
|
|
2048
|
+
.slice(0, 16)
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
1368
2052
|
/** The immutable emit directory of one build generation under `cacheDir`. */
|
|
1369
2053
|
function dependencyGenerationDir(cacheDir: string, generation: string): string {
|
|
1370
2054
|
return path.join(cacheDir, `gen-${generation}`);
|
|
@@ -1517,7 +2201,7 @@ function buildDependency(
|
|
|
1517
2201
|
cacheDir: string,
|
|
1518
2202
|
metaPath: string,
|
|
1519
2203
|
): BuiltProject {
|
|
1520
|
-
const project =
|
|
2204
|
+
const project = readPluginDescriptorProjectConfig(tsconfig);
|
|
1521
2205
|
const generation = newDependencyGeneration();
|
|
1522
2206
|
const emitDir = dependencyGenerationDir(cacheDir, generation);
|
|
1523
2207
|
fs.rmSync(emitDir, { force: true, recursive: true });
|
|
@@ -1612,10 +2296,28 @@ function publishDependencyMeta(
|
|
|
1612
2296
|
}
|
|
1613
2297
|
}
|
|
1614
2298
|
|
|
1615
|
-
function dependencyCacheRoot(
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2299
|
+
export function dependencyCacheRoot(
|
|
2300
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
2301
|
+
): string {
|
|
2302
|
+
// Descriptor evaluators are disposable and must not leave one isolated emit
|
|
2303
|
+
// generation in the shared temp cache per load. Their result file already
|
|
2304
|
+
// lives in the evaluator-owned directory that the parent removes in
|
|
2305
|
+
// `finally`; put dependency emits beside it so that cleanup owns both.
|
|
2306
|
+
if (
|
|
2307
|
+
env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1" &&
|
|
2308
|
+
typeof env.TTSC_PLUGIN_DESCRIPTOR_OUT === "string" &&
|
|
2309
|
+
path.isAbsolute(env.TTSC_PLUGIN_DESCRIPTOR_OUT)
|
|
2310
|
+
) {
|
|
2311
|
+
return path.join(
|
|
2312
|
+
path.dirname(env.TTSC_PLUGIN_DESCRIPTOR_OUT),
|
|
2313
|
+
"dependency-cache",
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
const owner = runtimeManifests().find(
|
|
2317
|
+
(candidate) => candidate.depCacheDir.length !== 0,
|
|
2318
|
+
);
|
|
2319
|
+
return owner !== undefined
|
|
2320
|
+
? owner.depCacheDir
|
|
1619
2321
|
: path.join(os.tmpdir(), "ttsx-dep");
|
|
1620
2322
|
}
|
|
1621
2323
|
|
|
@@ -1990,7 +2692,12 @@ function formatDuration(ms: number): string {
|
|
|
1990
2692
|
}
|
|
1991
2693
|
|
|
1992
2694
|
/** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
|
|
1993
|
-
|
|
2695
|
+
interface ITsconfigLookup {
|
|
2696
|
+
candidates: readonly string[];
|
|
2697
|
+
result: string | null;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
const tsconfigCache = new Map<string, ITsconfigLookup>();
|
|
1994
2701
|
const moduleOptionsCache = new Map<string, OwningModuleOptions | null>();
|
|
1995
2702
|
|
|
1996
2703
|
/**
|
|
@@ -2011,13 +2718,30 @@ function nearestTsconfig(file: string): string | null {
|
|
|
2011
2718
|
for (;;) {
|
|
2012
2719
|
const cached = tsconfigCache.get(directory);
|
|
2013
2720
|
if (cached !== undefined) {
|
|
2014
|
-
|
|
2721
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") {
|
|
2722
|
+
return rememberTsconfig(chain, cached.result, cached.candidates);
|
|
2723
|
+
}
|
|
2724
|
+
// Bracket the cached selection with the same candidate observations the
|
|
2725
|
+
// parent later reconciles. A nearer config created between a liveness
|
|
2726
|
+
// check and reporting must conflict, not be paired with the cached
|
|
2727
|
+
// farther result and certified as current.
|
|
2728
|
+
recordPluginDescriptorTsconfigCandidates(cached.candidates);
|
|
2729
|
+
const current = nearestExistingTsconfig(cached.candidates);
|
|
2730
|
+
recordPluginDescriptorTsconfigCandidates(cached.candidates);
|
|
2731
|
+
if (current === cached.result) {
|
|
2732
|
+
return rememberTsconfig(chain, cached.result, cached.candidates);
|
|
2733
|
+
}
|
|
2734
|
+
// Descriptor factories can deliberately create a config before a lazy
|
|
2735
|
+
// import. A lookup cached while an earlier import ran must not keep
|
|
2736
|
+
// serving the orphan lane after the nearest candidate changed.
|
|
2737
|
+
tsconfigCache.delete(directory);
|
|
2015
2738
|
}
|
|
2016
2739
|
if (path.basename(directory) === "node_modules") {
|
|
2017
2740
|
return rememberTsconfig(chain, null);
|
|
2018
2741
|
}
|
|
2019
2742
|
chain.push(directory);
|
|
2020
2743
|
const candidate = path.join(directory, "tsconfig.json");
|
|
2744
|
+
recordPluginDescriptorTsconfigCandidates([candidate]);
|
|
2021
2745
|
if (isFile(candidate)) {
|
|
2022
2746
|
return rememberTsconfig(chain, candidate);
|
|
2023
2747
|
}
|
|
@@ -2029,16 +2753,40 @@ function nearestTsconfig(file: string): string | null {
|
|
|
2029
2753
|
}
|
|
2030
2754
|
}
|
|
2031
2755
|
|
|
2756
|
+
function nearestExistingTsconfig(candidates: readonly string[]): string | null {
|
|
2757
|
+
for (const candidate of candidates) {
|
|
2758
|
+
if (isFile(candidate)) return path.resolve(candidate);
|
|
2759
|
+
}
|
|
2760
|
+
return null;
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2032
2763
|
function rememberTsconfig(
|
|
2033
2764
|
directories: readonly string[],
|
|
2034
2765
|
result: string | null,
|
|
2766
|
+
tailCandidates: readonly string[] = [],
|
|
2035
2767
|
): string | null {
|
|
2036
|
-
|
|
2037
|
-
|
|
2768
|
+
let candidates = [...tailCandidates];
|
|
2769
|
+
for (let index = directories.length - 1; index >= 0; index -= 1) {
|
|
2770
|
+
const directory = directories[index]!;
|
|
2771
|
+
candidates = [path.join(directory, "tsconfig.json"), ...candidates];
|
|
2772
|
+
tsconfigCache.set(directory, { candidates, result });
|
|
2038
2773
|
}
|
|
2039
2774
|
return result;
|
|
2040
2775
|
}
|
|
2041
2776
|
|
|
2777
|
+
/** Report every owning-config candidate observed by the nearest-config walk. */
|
|
2778
|
+
function recordPluginDescriptorTsconfigCandidates(
|
|
2779
|
+
candidates: readonly string[],
|
|
2780
|
+
): void {
|
|
2781
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
2782
|
+
for (const candidate of candidates) {
|
|
2783
|
+
const resolved = path.resolve(candidate);
|
|
2784
|
+
recordPluginDescriptorInput({
|
|
2785
|
+
resolved,
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2042
2790
|
function readFileOrNull(file: string | null): string | null {
|
|
2043
2791
|
if (file === null) {
|
|
2044
2792
|
return null;
|
|
@@ -2143,6 +2891,15 @@ const JS_TO_TS_EXTENSIONS: ReadonlyMap<string, readonly string[]> = new Map([
|
|
|
2143
2891
|
[".cjs", [".cts"]],
|
|
2144
2892
|
]);
|
|
2145
2893
|
|
|
2894
|
+
/** Candidate sources that can satisfy one missing JavaScript spelling. */
|
|
2895
|
+
function typescriptSourcesForJavaScriptSpecifier(file: string): string[] {
|
|
2896
|
+
const extension = path.extname(file).toLowerCase();
|
|
2897
|
+
const substitutions = JS_TO_TS_EXTENSIONS.get(extension);
|
|
2898
|
+
if (substitutions === undefined) return [];
|
|
2899
|
+
const stem = file.slice(0, file.length - extension.length);
|
|
2900
|
+
return substitutions.map((candidate) => stem + candidate);
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2146
2903
|
/**
|
|
2147
2904
|
* Rescue a `specifier` that Node's resolver rejected: map a JavaScript
|
|
2148
2905
|
* extension back to its TypeScript source, or probe candidate extensions /
|