ttsc 0.26.2 → 0.28.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/graphbench/main.go +26 -16
- package/cmd/graphdump/main.go +37 -12
- package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
- package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
- package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
- package/cmd/ttsc/api_transform.go +28 -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/dump.go +43 -11
- package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
- package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
- package/cmd/ttscgraph/serve.go +357 -60
- package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
- package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
- package/cmd/ttscgraph/serve_shards.go +31 -9
- package/driver/config_discovery.go +121 -0
- package/driver/emit_plugin.go +77 -11
- 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 +506 -15
- package/driver/program.go +54 -1
- package/driver/resolution_candidates.go +13 -5
- package/driver/rewrite.go +45 -2
- package/driver/transform_dependencies.go +283 -0
- package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
- package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
- package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
- package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
- package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
- package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
- package/internal/graph/artifacts.go +230 -0
- package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
- package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
- package/internal/graph/build.go +35 -0
- package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
- package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
- package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
- package/internal/graph/doc_tags.go +416 -0
- package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
- package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
- package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
- package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
- package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
- package/internal/graph/dump.go +60 -13
- package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
- package/internal/graph/dump_paths.go +2 -2
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
- package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
- package/internal/graph/edges.go +1 -0
- package/internal/graph/graph.go +79 -4
- package/internal/graph/provenance.go +29 -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/project/createNativeProjectContextArgs.d.ts +9 -0
- package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
- package/lib/compiler/internal/project/createNativeProjectContextArgs.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/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.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 +47 -3
- package/lib/launcher/internal/runtimeHooks.js +746 -64
- 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/capabilityResolutionCache.d.ts +76 -0
- package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
- package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
- 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/plugin/resolveCapabilityPlugins.d.ts +55 -0
- package/lib/plugin/resolveCapabilityPlugins.js +118 -0
- package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
- 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 +38 -0
- package/lib/structures/ITtscPlugin.d.ts +33 -0
- package/package.json +16 -9
- package/shim/ast/parent.go +16 -3
- package/shim/checker/enums_gen.go +11 -0
- package/shim/checker/go.mod +9 -0
- package/shim/checker/go.sum +22 -0
- package/shim/checker/shim.go +78 -0
- package/shim/compiler/emithook.go +10 -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/project/createNativeProjectContextArgs.ts +19 -4
- 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/index.ts +1 -0
- 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 +934 -68
- package/src/plugin/internal/buildSourcePlugin.ts +984 -79
- package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
- package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
- package/src/plugin/resolveCapabilityPlugins.ts +171 -0
- package/src/register.ts +47 -0
- package/src/structures/ITtscCompilerContext.ts +6 -3
- package/src/structures/ITtscCompilerTransformation.ts +46 -0
- package/src/structures/ITtscPlugin.ts +36 -0
- package/tools/shim_audit/baseline.json +0 -2
- package/utility/host.go +18 -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:
|
|
@@ -246,10 +274,21 @@ let installed = false;
|
|
|
246
274
|
* `require()` made from inside a CommonJS module that was itself reached
|
|
247
275
|
* through an ESM `import` (the interop translator loads it on the raw CJS
|
|
248
276
|
* path). The ESM graph goes through `registerHooks`; the CommonJS `require`
|
|
249
|
-
* graph goes through `Module._extensions` — the
|
|
250
|
-
*
|
|
277
|
+
* graph goes through `Module._extensions` and `Module._resolveFilename` — the
|
|
278
|
+
* canonical loader extension points `ts-node`/`tsx` use for the same reason.
|
|
279
|
+
*
|
|
280
|
+
* Both halves of that second graph are required, and they answer different
|
|
281
|
+
* questions. `_extensions` decides how a file that was _found_ is compiled, and
|
|
282
|
+
* Node reuses its keys when it probes an extensionless request, so
|
|
283
|
+
* `require("./x")` reaches `x.ts` for free. Nothing there resolves a request
|
|
284
|
+
* that already carries an extension: `require("./x.js")` backed only by `x.ts`
|
|
285
|
+
* is a resolution failure before any compiler is consulted, which is why the
|
|
286
|
+
* rescue is installed on `_resolveFilename` as well (samchon/ttsc#1280).
|
|
251
287
|
*/
|
|
252
|
-
export function installRuntimeHooks(): void {
|
|
288
|
+
export function installRuntimeHooks(options: RuntimeHookOptions = {}): void {
|
|
289
|
+
if (options.prepareEntry !== undefined) {
|
|
290
|
+
prepareRuntimeEntry = options.prepareEntry;
|
|
291
|
+
}
|
|
253
292
|
if (installed) {
|
|
254
293
|
return;
|
|
255
294
|
}
|
|
@@ -264,6 +303,7 @@ export function installRuntimeHooks(): void {
|
|
|
264
303
|
}
|
|
265
304
|
registerHooks({ load, resolve });
|
|
266
305
|
installCommonJsHook();
|
|
306
|
+
installCommonJsResolveHook();
|
|
267
307
|
}
|
|
268
308
|
|
|
269
309
|
/**
|
|
@@ -277,23 +317,137 @@ function installCommonJsHook(): void {
|
|
|
277
317
|
_extensions: Record<
|
|
278
318
|
string,
|
|
279
319
|
(
|
|
280
|
-
module: {
|
|
320
|
+
module: {
|
|
321
|
+
_compile(source: string, filename: string): void;
|
|
322
|
+
parent?: { filename?: string | null } | null;
|
|
323
|
+
},
|
|
281
324
|
filename: string,
|
|
282
325
|
) => void
|
|
283
326
|
>;
|
|
284
327
|
}
|
|
285
328
|
)._extensions;
|
|
286
329
|
const compile = (
|
|
287
|
-
module: {
|
|
330
|
+
module: {
|
|
331
|
+
_compile(source: string, filename: string): void;
|
|
332
|
+
parent?: { filename?: string | null } | null;
|
|
333
|
+
},
|
|
288
334
|
filename: string,
|
|
289
335
|
): void => {
|
|
290
|
-
module.
|
|
336
|
+
const parent = module.parent?.filename;
|
|
337
|
+
module._compile(
|
|
338
|
+
resolveServedSource(
|
|
339
|
+
filename,
|
|
340
|
+
pathToFileURL(filename).href,
|
|
341
|
+
typeof parent !== "string" || !isTypeScriptSource(parent),
|
|
342
|
+
).source,
|
|
343
|
+
filename,
|
|
344
|
+
);
|
|
291
345
|
};
|
|
292
346
|
for (const extension of [".ts", ".tsx", ".cts"]) {
|
|
293
347
|
extensions[extension] = compile;
|
|
294
348
|
}
|
|
295
349
|
}
|
|
296
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Map a JavaScript spelling back to its TypeScript source on the CommonJS
|
|
353
|
+
* resolution path, so `require("./x.js")` finds `x.ts` when nothing emitted
|
|
354
|
+
* `x.js`.
|
|
355
|
+
*
|
|
356
|
+
* TypeScript asks authors to write the emitted `.js` extension in a relative
|
|
357
|
+
* specifier, so this spelling is the documented one rather than a mistake, and
|
|
358
|
+
* the ESM `resolve` hook has always rescued it. The CommonJS graph could not,
|
|
359
|
+
* and on Node 22 that graph is reached by every `require()` inside a CommonJS
|
|
360
|
+
* module the ESM loader evaluated — `module.registerHooks` sees the `import()`
|
|
361
|
+
* of that module and nothing within it. Node 24 routes the same `require`
|
|
362
|
+
* through the hooks, which is why the gap was only ever visible on the oldest
|
|
363
|
+
* runtime ttsx supports (`engines.node` is `>=22.15.0`).
|
|
364
|
+
*
|
|
365
|
+
* The wrapper only ever answers a request Node's own resolver already refused,
|
|
366
|
+
* so a resolution that succeeds is never perturbed, and a request no candidate
|
|
367
|
+
* satisfies rethrows Node's original error with its `MODULE_NOT_FOUND` code and
|
|
368
|
+
* `requireStack` intact. `require.resolve` shares this entry point and is
|
|
369
|
+
* rescued with it, except in its `{ paths }` form, which resolves against the
|
|
370
|
+
* caller's list rather than the parent this reads.
|
|
371
|
+
*/
|
|
372
|
+
function installCommonJsResolveHook(): void {
|
|
373
|
+
const internals = Module as unknown as {
|
|
374
|
+
_resolveFilename(
|
|
375
|
+
request: string,
|
|
376
|
+
parent: { filename?: string | null } | null | undefined,
|
|
377
|
+
isMain: boolean,
|
|
378
|
+
options?: unknown,
|
|
379
|
+
): string;
|
|
380
|
+
};
|
|
381
|
+
const original = internals._resolveFilename;
|
|
382
|
+
internals._resolveFilename = function resolveFilename(
|
|
383
|
+
this: unknown,
|
|
384
|
+
request: string,
|
|
385
|
+
parent: { filename?: string | null } | null | undefined,
|
|
386
|
+
isMain: boolean,
|
|
387
|
+
options?: unknown,
|
|
388
|
+
): string {
|
|
389
|
+
try {
|
|
390
|
+
return original.call(this, request, parent, isMain, options);
|
|
391
|
+
} catch (error) {
|
|
392
|
+
const rescued = rescueCommonJsRequest(request, parent, options);
|
|
393
|
+
if (rescued === null) {
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
return rescued;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The TypeScript source a refused CommonJS request names, or `null`.
|
|
403
|
+
*
|
|
404
|
+
* Only a relative or absolute request can be rescued: a bare specifier is a
|
|
405
|
+
* package lookup, whose own resolver already probed every extension the
|
|
406
|
+
* installed `_extensions` keys declare.
|
|
407
|
+
*
|
|
408
|
+
* Two shapes decline before that. `_resolveFilename` is an internal entry point
|
|
409
|
+
* anything may call, so a non-string request reaches here as readily as a
|
|
410
|
+
* specifier does, and reading it would replace Node's own argument error with a
|
|
411
|
+
* `TypeError` from this file. And `require.resolve(request, { paths })`
|
|
412
|
+
* resolves against the caller's list, which this rescue does not consult, so
|
|
413
|
+
* answering it from the parent's directory would be a different question's
|
|
414
|
+
* answer.
|
|
415
|
+
*/
|
|
416
|
+
function rescueCommonJsRequest(
|
|
417
|
+
request: string,
|
|
418
|
+
parent: { filename?: string | null } | null | undefined,
|
|
419
|
+
options?: unknown,
|
|
420
|
+
): string | null {
|
|
421
|
+
if (typeof request !== "string") {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
if (
|
|
425
|
+
typeof options === "object" &&
|
|
426
|
+
options !== null &&
|
|
427
|
+
Array.isArray((options as { paths?: unknown }).paths)
|
|
428
|
+
) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
let base: string;
|
|
432
|
+
if (path.isAbsolute(request)) {
|
|
433
|
+
base = request;
|
|
434
|
+
} else if (isRelativeSpecifier(request)) {
|
|
435
|
+
const from = parent?.filename;
|
|
436
|
+
if (typeof from !== "string") {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
base = path.resolve(path.dirname(from), request);
|
|
440
|
+
} else {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
for (const candidate of typescriptSourcesForJavaScriptSpecifier(base)) {
|
|
444
|
+
if (isFile(candidate)) {
|
|
445
|
+
return candidate;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
297
451
|
/**
|
|
298
452
|
* The module format of the entry source file, derived from the entry project's
|
|
299
453
|
* compiler options (via the runtime manifest) the same way the served files are
|
|
@@ -301,15 +455,19 @@ function installCommonJsHook(): void {
|
|
|
301
455
|
* `require` or an ESM `import`.
|
|
302
456
|
*/
|
|
303
457
|
export function entryModuleFormat(entryFile: string): "module" | "commonjs" {
|
|
304
|
-
const
|
|
458
|
+
const real = realPath(entryFile);
|
|
459
|
+
const owner = findEntryEmit(real)?.manifest;
|
|
305
460
|
return moduleFormat(
|
|
306
461
|
entryFile,
|
|
307
|
-
|
|
462
|
+
owner === undefined ? null : (owner.moduleOptions ?? {}),
|
|
308
463
|
) === "module"
|
|
309
464
|
? "module"
|
|
310
465
|
: "commonjs";
|
|
311
466
|
}
|
|
312
467
|
|
|
468
|
+
/** TypeScript URLs resolved at a JavaScript-to-TypeScript entry boundary. */
|
|
469
|
+
const runtimeEntryUrls = new Set<string>();
|
|
470
|
+
|
|
313
471
|
/**
|
|
314
472
|
* Rescue an extensionless or directory relative specifier that Node's resolver
|
|
315
473
|
* rejected. Only runs after `nextResolve` throws, so a successful resolution is
|
|
@@ -321,24 +479,394 @@ function resolve(
|
|
|
321
479
|
context: ResolveContext,
|
|
322
480
|
nextResolve: NextResolve,
|
|
323
481
|
): ResolveResult {
|
|
482
|
+
recordPluginDescriptorResolutionCandidates(specifier, context.parentURL);
|
|
324
483
|
try {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
484
|
+
const result = rememberRuntimeEntry(
|
|
485
|
+
rememberCommonJsNamedInterop(
|
|
486
|
+
restoreStrippedNodeBuiltinScheme(
|
|
487
|
+
specifier,
|
|
488
|
+
nextResolve(specifier, context),
|
|
489
|
+
),
|
|
490
|
+
context,
|
|
329
491
|
),
|
|
330
492
|
context,
|
|
331
493
|
);
|
|
494
|
+
recordPluginDescriptorResolution(specifier, context.parentURL, result.url);
|
|
495
|
+
return result;
|
|
332
496
|
} catch (error) {
|
|
333
497
|
const rescued = probeRescuableSpecifier(specifier, context.parentURL);
|
|
334
498
|
if (rescued === null) {
|
|
335
499
|
throw error;
|
|
336
500
|
}
|
|
337
|
-
|
|
338
|
-
|
|
501
|
+
const result = rememberRuntimeEntry(
|
|
502
|
+
rememberCommonJsNamedInterop(
|
|
503
|
+
{ shortCircuit: true, url: rescued },
|
|
504
|
+
context,
|
|
505
|
+
),
|
|
339
506
|
context,
|
|
340
507
|
);
|
|
508
|
+
recordPluginDescriptorResolution(specifier, context.parentURL, result.url);
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Fingerprint every path whose state can redirect one descriptor import.
|
|
515
|
+
*
|
|
516
|
+
* This runs before the real resolver. Reporting candidates only after the
|
|
517
|
+
* descriptor finished would pair an earlier descriptor result with later file
|
|
518
|
+
* state when a higher-priority candidate appeared during evaluation.
|
|
519
|
+
*/
|
|
520
|
+
function recordPluginDescriptorResolutionCandidates(
|
|
521
|
+
specifier: string,
|
|
522
|
+
parentURL: string | undefined,
|
|
523
|
+
): void {
|
|
524
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
525
|
+
if (isBuiltin(specifier) || specifier.startsWith("node:")) return;
|
|
526
|
+
const parent = runtimeFilePath(parentURL);
|
|
527
|
+
if (parent === undefined) return;
|
|
528
|
+
const recorded = new Set<string>();
|
|
529
|
+
const record = (candidate: string): void => {
|
|
530
|
+
const resolved = path.resolve(candidate);
|
|
531
|
+
if (recorded.has(resolved)) return;
|
|
532
|
+
recorded.add(resolved);
|
|
533
|
+
recordPluginDescriptorInput({
|
|
534
|
+
parent,
|
|
535
|
+
resolved,
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
const recordManifestTargets = (
|
|
539
|
+
value: unknown,
|
|
540
|
+
directory: string,
|
|
541
|
+
allowBare: boolean = false,
|
|
542
|
+
): void => {
|
|
543
|
+
if (typeof value === "string") {
|
|
544
|
+
if (
|
|
545
|
+
value !== "" &&
|
|
546
|
+
(allowBare || value.startsWith("./") || value.startsWith("../"))
|
|
547
|
+
) {
|
|
548
|
+
recordBase(path.resolve(directory, value));
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (Array.isArray(value)) {
|
|
553
|
+
for (const item of value) {
|
|
554
|
+
recordManifestTargets(item, directory, allowBare);
|
|
555
|
+
}
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (value !== null && typeof value === "object") {
|
|
559
|
+
for (const item of Object.values(value)) {
|
|
560
|
+
recordManifestTargets(item, directory, allowBare);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
const recordPackageManifests = (file: string): void => {
|
|
565
|
+
for (
|
|
566
|
+
let directory = path.dirname(file);
|
|
567
|
+
;
|
|
568
|
+
directory = path.dirname(directory)
|
|
569
|
+
) {
|
|
570
|
+
const manifest = path.join(directory, "package.json");
|
|
571
|
+
record(manifest);
|
|
572
|
+
if (isFile(manifest)) return;
|
|
573
|
+
const parentDirectory = path.dirname(directory);
|
|
574
|
+
if (parentDirectory === directory) return;
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
const localBases = (value: string): string[] => {
|
|
578
|
+
if (value.startsWith("file:")) return [fileURLToPath(value)];
|
|
579
|
+
const raw = path.resolve(path.dirname(parent), value);
|
|
580
|
+
const suffixStart = value.search(/[?#]/);
|
|
581
|
+
if (suffixStart === -1) return [raw];
|
|
582
|
+
const pathname = value.slice(0, suffixStart);
|
|
583
|
+
return pathname === ""
|
|
584
|
+
? [raw]
|
|
585
|
+
: [...new Set([raw, path.resolve(path.dirname(parent), pathname)])];
|
|
586
|
+
};
|
|
587
|
+
const bases = new Set<string>();
|
|
588
|
+
const recordBase = (base: string): void => {
|
|
589
|
+
const resolvedBase = path.resolve(base);
|
|
590
|
+
if (bases.has(resolvedBase)) return;
|
|
591
|
+
bases.add(resolvedBase);
|
|
592
|
+
record(resolvedBase);
|
|
593
|
+
for (const candidate of typescriptSourcesForJavaScriptSpecifier(
|
|
594
|
+
resolvedBase,
|
|
595
|
+
)) {
|
|
596
|
+
record(candidate);
|
|
597
|
+
}
|
|
598
|
+
for (const extension of DESCRIPTOR_PROBE_EXTENSIONS) {
|
|
599
|
+
record(resolvedBase + extension);
|
|
600
|
+
}
|
|
601
|
+
const manifestFile = path.join(resolvedBase, "package.json");
|
|
602
|
+
record(manifestFile);
|
|
603
|
+
for (const extension of DESCRIPTOR_PROBE_EXTENSIONS) {
|
|
604
|
+
record(path.join(resolvedBase, `index${extension}`));
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
const manifest = JSON.parse(
|
|
608
|
+
fs.readFileSync(manifestFile, "utf8").replace(/^\uFEFF/, ""),
|
|
609
|
+
) as Record<string, unknown>;
|
|
610
|
+
recordManifestTargets(manifest.exports, resolvedBase);
|
|
611
|
+
recordManifestTargets(manifest.module, resolvedBase, true);
|
|
612
|
+
recordManifestTargets(manifest.main, resolvedBase, true);
|
|
613
|
+
} catch {
|
|
614
|
+
// The real resolver owns malformed package diagnostics.
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
if (
|
|
619
|
+
specifier.startsWith(".") ||
|
|
620
|
+
path.isAbsolute(specifier) ||
|
|
621
|
+
specifier.startsWith("file:")
|
|
622
|
+
) {
|
|
623
|
+
try {
|
|
624
|
+
for (const base of localBases(specifier)) {
|
|
625
|
+
recordPackageManifests(base);
|
|
626
|
+
if (isFile(base)) record(base);
|
|
627
|
+
else recordBase(base);
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
// The real resolver owns invalid URL spellings.
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const parts = specifier.split("/");
|
|
636
|
+
const packageParts = parts[0]?.startsWith("@")
|
|
637
|
+
? parts.slice(0, 2)
|
|
638
|
+
: parts.slice(0, 1);
|
|
639
|
+
if (packageParts.some((part) => part === undefined || part === "")) return;
|
|
640
|
+
const packageName = packageParts.join("/");
|
|
641
|
+
const subpath = parts.slice(packageParts.length);
|
|
642
|
+
for (const searchPath of createRequire(parent).resolve.paths(specifier) ??
|
|
643
|
+
[]) {
|
|
644
|
+
const packageDirectory = path.join(searchPath, packageName);
|
|
645
|
+
recordBase(packageDirectory);
|
|
646
|
+
if (subpath.length !== 0) {
|
|
647
|
+
recordBase(path.join(packageDirectory, ...subpath));
|
|
648
|
+
}
|
|
649
|
+
// CommonJS resolution continues past an existing but unusable package
|
|
650
|
+
// directory. Record every search root before the resolver runs so a
|
|
651
|
+
// farther selected package retains evaluation-time hashes for its
|
|
652
|
+
// superseding candidates.
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Report one resolved descriptor edge to the parent loader. The channel is
|
|
658
|
+
* armed by the generated descriptor shim only after ttsx's own bootstrap and
|
|
659
|
+
* imports have loaded, keeping compiler implementation files out of the
|
|
660
|
+
* project's persistent-cache inputs.
|
|
661
|
+
*/
|
|
662
|
+
function recordPluginDescriptorResolution(
|
|
663
|
+
specifier: string,
|
|
664
|
+
parentURL: string | undefined,
|
|
665
|
+
resolvedURL: string,
|
|
666
|
+
): void {
|
|
667
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
668
|
+
const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
|
|
669
|
+
if (out === undefined || out.length === 0) return;
|
|
670
|
+
const resolved = runtimeFilePath(resolvedURL);
|
|
671
|
+
if (resolved === undefined) return;
|
|
672
|
+
const parent = runtimeFilePath(parentURL);
|
|
673
|
+
recordPluginDescriptorInput({
|
|
674
|
+
...(parent === undefined ? {} : { parent }),
|
|
675
|
+
resolved,
|
|
676
|
+
specifier,
|
|
677
|
+
});
|
|
678
|
+
for (
|
|
679
|
+
let directory = path.dirname(resolved);
|
|
680
|
+
;
|
|
681
|
+
directory = path.dirname(directory)
|
|
682
|
+
) {
|
|
683
|
+
const manifest = path.join(directory, "package.json");
|
|
684
|
+
recordPluginDescriptorInput({
|
|
685
|
+
...(parent === undefined ? {} : { parent }),
|
|
686
|
+
resolved: manifest,
|
|
687
|
+
});
|
|
688
|
+
if (isFile(manifest)) break;
|
|
689
|
+
const parentDirectory = path.dirname(directory);
|
|
690
|
+
if (parentDirectory === directory) break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function recordPluginDescriptorInput(record: {
|
|
695
|
+
hash?: string | null;
|
|
696
|
+
parent?: string;
|
|
697
|
+
realpath?: string | null;
|
|
698
|
+
resolved: string;
|
|
699
|
+
signature?: string;
|
|
700
|
+
specifier?: string;
|
|
701
|
+
unstable?: boolean;
|
|
702
|
+
}): void {
|
|
703
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
704
|
+
const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
|
|
705
|
+
if (out === undefined || out.length === 0) return;
|
|
706
|
+
recordPluginDescriptorInputOnce(record);
|
|
707
|
+
const resolved = path.resolve(record.resolved);
|
|
708
|
+
const parsed = path.parse(resolved);
|
|
709
|
+
let current = parsed.root;
|
|
710
|
+
const relative = path.relative(parsed.root, resolved);
|
|
711
|
+
for (const segment of relative.split(path.sep).slice(0, -1)) {
|
|
712
|
+
if (segment === "") continue;
|
|
713
|
+
current = path.join(current, segment);
|
|
714
|
+
try {
|
|
715
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
716
|
+
recordPluginDescriptorInputOnce({ resolved: current });
|
|
717
|
+
}
|
|
718
|
+
} catch {
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Record one path without recursively revisiting its lexical ancestors. */
|
|
725
|
+
function recordPluginDescriptorInputOnce(record: {
|
|
726
|
+
hash?: string | null;
|
|
727
|
+
parent?: string;
|
|
728
|
+
realpath?: string | null;
|
|
729
|
+
resolved: string;
|
|
730
|
+
signature?: string;
|
|
731
|
+
specifier?: string;
|
|
732
|
+
unstable?: boolean;
|
|
733
|
+
}): void {
|
|
734
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
735
|
+
const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
|
|
736
|
+
if (out === undefined || out.length === 0) return;
|
|
737
|
+
try {
|
|
738
|
+
const beforeSignature = pluginDescriptorInputMetadataSignature(
|
|
739
|
+
record.resolved,
|
|
740
|
+
);
|
|
741
|
+
const observedHash = pluginDescriptorInputHash(record.resolved);
|
|
742
|
+
const observedRealpath = pluginDescriptorInputRealpath(record.resolved);
|
|
743
|
+
const afterSignature = pluginDescriptorInputMetadataSignature(
|
|
744
|
+
record.resolved,
|
|
745
|
+
);
|
|
746
|
+
const unstable =
|
|
747
|
+
record.unstable === true ||
|
|
748
|
+
beforeSignature === undefined ||
|
|
749
|
+
afterSignature === undefined ||
|
|
750
|
+
beforeSignature !== afterSignature ||
|
|
751
|
+
(record.hash !== undefined && record.hash !== observedHash) ||
|
|
752
|
+
(record.realpath !== undefined && record.realpath !== observedRealpath) ||
|
|
753
|
+
(record.signature !== undefined && record.signature !== afterSignature);
|
|
754
|
+
fs.appendFileSync(
|
|
755
|
+
out,
|
|
756
|
+
`${JSON.stringify({
|
|
757
|
+
...record,
|
|
758
|
+
hash: observedHash,
|
|
759
|
+
realpath: observedRealpath,
|
|
760
|
+
...(unstable ? { unstable: true } : { signature: afterSignature }),
|
|
761
|
+
})}\n`,
|
|
762
|
+
"utf8",
|
|
763
|
+
);
|
|
764
|
+
} catch {
|
|
765
|
+
// Dependency reporting is advisory to cache reuse; the selected entry is
|
|
766
|
+
// still retained by the parent if this best-effort side channel fails.
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function pluginDescriptorInputRealpath(file: string): string | null {
|
|
771
|
+
try {
|
|
772
|
+
return fs.realpathSync.native(file);
|
|
773
|
+
} catch {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** Metadata identity that exposes content-preserving A-B-A replacement. */
|
|
779
|
+
function pluginDescriptorInputMetadataSignature(
|
|
780
|
+
file: string,
|
|
781
|
+
): string | undefined {
|
|
782
|
+
const requested = path.resolve(file);
|
|
783
|
+
let current = requested;
|
|
784
|
+
for (;;) {
|
|
785
|
+
try {
|
|
786
|
+
const link = fs.lstatSync(current, { bigint: true });
|
|
787
|
+
let target = link;
|
|
788
|
+
if (link.isSymbolicLink()) {
|
|
789
|
+
try {
|
|
790
|
+
target = fs.statSync(current, { bigint: true });
|
|
791
|
+
} catch {
|
|
792
|
+
// A broken link's own metadata cannot expose its target appearing
|
|
793
|
+
// and disappearing during evaluation. Decline cache proof instead.
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return [
|
|
798
|
+
path.relative(current, requested),
|
|
799
|
+
link.dev,
|
|
800
|
+
link.ino,
|
|
801
|
+
link.mode,
|
|
802
|
+
link.size,
|
|
803
|
+
link.mtimeNs,
|
|
804
|
+
link.ctimeNs,
|
|
805
|
+
target.dev,
|
|
806
|
+
target.ino,
|
|
807
|
+
target.mode,
|
|
808
|
+
target.size,
|
|
809
|
+
target.mtimeNs,
|
|
810
|
+
target.ctimeNs,
|
|
811
|
+
].join(":");
|
|
812
|
+
} catch (error) {
|
|
813
|
+
if (!isMissingPathError(error)) return undefined;
|
|
814
|
+
const parent = path.dirname(current);
|
|
815
|
+
if (parent === current) return undefined;
|
|
816
|
+
current = parent;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function pluginDescriptorInputHash(file: string): string | null {
|
|
822
|
+
try {
|
|
823
|
+
if (fs.statSync(file).isDirectory()) {
|
|
824
|
+
return pluginDescriptorDirectoryHash();
|
|
825
|
+
}
|
|
826
|
+
return crypto
|
|
827
|
+
.createHash("sha256")
|
|
828
|
+
.update(fs.readFileSync(file))
|
|
829
|
+
.digest("hex");
|
|
830
|
+
} catch {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** Stable public fingerprint for an existing directory candidate's kind. */
|
|
836
|
+
function pluginDescriptorDirectoryHash(): string {
|
|
837
|
+
return crypto
|
|
838
|
+
.createHash("sha256")
|
|
839
|
+
.update("ttsc:host-input:directory\0")
|
|
840
|
+
.digest("hex");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function runtimeFilePath(value: string | undefined): string | undefined {
|
|
844
|
+
if (value === undefined) return undefined;
|
|
845
|
+
if (value.startsWith("file:")) {
|
|
846
|
+
try {
|
|
847
|
+
return path.resolve(fileURLToPath(value));
|
|
848
|
+
} catch {
|
|
849
|
+
return undefined;
|
|
850
|
+
}
|
|
341
851
|
}
|
|
852
|
+
return path.isAbsolute(value) ? path.resolve(value) : undefined;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Remember an ESM root until its synchronous load hook prepares the project. */
|
|
856
|
+
function rememberRuntimeEntry(
|
|
857
|
+
result: ResolveResult,
|
|
858
|
+
context: ResolveContext,
|
|
859
|
+
): ResolveResult {
|
|
860
|
+
if (
|
|
861
|
+
result.url.startsWith("file:") &&
|
|
862
|
+
isTypeScriptSource(fileURLToPath(result.url)) &&
|
|
863
|
+
(context.parentURL === undefined ||
|
|
864
|
+
!context.parentURL.startsWith("file:") ||
|
|
865
|
+
!isTypeScriptSource(fileURLToPath(context.parentURL)))
|
|
866
|
+
) {
|
|
867
|
+
runtimeEntryUrls.add(result.url);
|
|
868
|
+
}
|
|
869
|
+
return result;
|
|
342
870
|
}
|
|
343
871
|
|
|
344
872
|
/**
|
|
@@ -369,6 +897,12 @@ interface BuiltProject {
|
|
|
369
897
|
moduleOptions: OwningModuleOptions;
|
|
370
898
|
}
|
|
371
899
|
const builtProjects = new Map<string, BuiltProject>();
|
|
900
|
+
// A descriptor evaluator's dependency emit must never be reused by another
|
|
901
|
+
// process. PIDs are eventually recycled while the disk cache persists, so PID
|
|
902
|
+
// alone cannot provide that isolation. One cryptographically random process
|
|
903
|
+
// nonce keeps every evaluator generation distinct while `builtProjects` still
|
|
904
|
+
// shares repeated imports inside this process.
|
|
905
|
+
const descriptorProcessCacheNonce = crypto.randomBytes(16).toString("hex");
|
|
372
906
|
|
|
373
907
|
/** File URLs whose CommonJS source was reached from an ESM parent import. */
|
|
374
908
|
const commonJsNamedInteropUrls = new Set<string>();
|
|
@@ -386,7 +920,11 @@ function load(
|
|
|
386
920
|
if (!isTypeScriptSource(filename)) {
|
|
387
921
|
return nextLoad(url, context);
|
|
388
922
|
}
|
|
389
|
-
const { format, source } = resolveRuntimeSource(
|
|
923
|
+
const { format, source } = resolveRuntimeSource(
|
|
924
|
+
filename,
|
|
925
|
+
url,
|
|
926
|
+
runtimeEntryUrls.delete(url) || isProcessEntry(filename),
|
|
927
|
+
);
|
|
390
928
|
return {
|
|
391
929
|
format,
|
|
392
930
|
shortCircuit: true,
|
|
@@ -397,8 +935,9 @@ function load(
|
|
|
397
935
|
function resolveRuntimeSource(
|
|
398
936
|
filename: string,
|
|
399
937
|
url: string = pathToFileURL(filename).href,
|
|
938
|
+
prepareAsEntry: boolean = false,
|
|
400
939
|
): { format: string; source: string } {
|
|
401
|
-
const served = resolveServedSource(filename, url);
|
|
940
|
+
const served = resolveServedSource(filename, url, prepareAsEntry);
|
|
402
941
|
const format = moduleFormat(filename, served.moduleOptions);
|
|
403
942
|
return {
|
|
404
943
|
format,
|
|
@@ -413,6 +952,16 @@ function resolveRuntimeSource(
|
|
|
413
952
|
};
|
|
414
953
|
}
|
|
415
954
|
|
|
955
|
+
/** Whether `filename` is the TypeScript main module named on Node's argv. */
|
|
956
|
+
function isProcessEntry(filename: string): boolean {
|
|
957
|
+
const entry = process.argv[1];
|
|
958
|
+
return (
|
|
959
|
+
entry !== undefined &&
|
|
960
|
+
isTypeScriptSource(entry) &&
|
|
961
|
+
realPath(path.resolve(entry)) === realPath(filename)
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
416
965
|
function rememberCommonJsNamedInterop(
|
|
417
966
|
result: ResolveResult,
|
|
418
967
|
context: ResolveContext,
|
|
@@ -467,9 +1016,9 @@ function owningModuleOptions(filename: string): OwningModuleOptions | null {
|
|
|
467
1016
|
return null;
|
|
468
1017
|
}
|
|
469
1018
|
const real = realPath(filename);
|
|
470
|
-
const
|
|
471
|
-
if (
|
|
472
|
-
return
|
|
1019
|
+
const owner = findEntryEmit(real)?.manifest;
|
|
1020
|
+
if (owner !== undefined) {
|
|
1021
|
+
return owner.moduleOptions ?? {};
|
|
473
1022
|
}
|
|
474
1023
|
const tsconfig = nearestTsconfig(real);
|
|
475
1024
|
if (tsconfig === null) {
|
|
@@ -481,10 +1030,7 @@ function owningModuleOptions(filename: string): OwningModuleOptions | null {
|
|
|
481
1030
|
}
|
|
482
1031
|
let options: OwningModuleOptions = {};
|
|
483
1032
|
try {
|
|
484
|
-
const project =
|
|
485
|
-
cwd: path.dirname(tsconfig),
|
|
486
|
-
tsconfig,
|
|
487
|
-
});
|
|
1033
|
+
const project = readPluginDescriptorProjectConfig(tsconfig);
|
|
488
1034
|
options = projectModuleOptions(project.compilerOptions);
|
|
489
1035
|
} catch {
|
|
490
1036
|
// The owning project cannot be read, so nothing is known about the format
|
|
@@ -498,6 +1044,147 @@ function owningModuleOptions(filename: string): OwningModuleOptions | null {
|
|
|
498
1044
|
return options;
|
|
499
1045
|
}
|
|
500
1046
|
|
|
1047
|
+
/**
|
|
1048
|
+
* Read one owning config while its discovered chain stays unchanged.
|
|
1049
|
+
*
|
|
1050
|
+
* A preliminary read discovers `extends`; an accepted read is bracketed by
|
|
1051
|
+
* equal fingerprints over that exact chain. If churn never settles, ttsx can
|
|
1052
|
+
* still execute the last project but reports every observed config as unstable,
|
|
1053
|
+
* preventing a later snapshot from certifying the torn result.
|
|
1054
|
+
*/
|
|
1055
|
+
function readPluginDescriptorProjectConfig(
|
|
1056
|
+
tsconfig: string,
|
|
1057
|
+
): ReturnType<typeof readProjectConfig> {
|
|
1058
|
+
const read = () =>
|
|
1059
|
+
readProjectConfig({ cwd: path.dirname(tsconfig), tsconfig });
|
|
1060
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return read();
|
|
1061
|
+
|
|
1062
|
+
const observed = new Set<string>([path.resolve(tsconfig)]);
|
|
1063
|
+
let project: ReturnType<typeof readProjectConfig>;
|
|
1064
|
+
try {
|
|
1065
|
+
project = read();
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
recordPluginDescriptorProjectInputs(observed, true);
|
|
1068
|
+
throw error;
|
|
1069
|
+
}
|
|
1070
|
+
let inputs = normalizedProjectConfigPaths(project, tsconfig);
|
|
1071
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1072
|
+
for (const input of inputs) observed.add(input);
|
|
1073
|
+
const before = pluginDescriptorInputHashes(inputs);
|
|
1074
|
+
const beforeRealpaths = pluginDescriptorInputRealpaths(inputs);
|
|
1075
|
+
const beforeSignatures = pluginDescriptorInputMetadataSignatures(inputs);
|
|
1076
|
+
let candidate: ReturnType<typeof readProjectConfig>;
|
|
1077
|
+
try {
|
|
1078
|
+
candidate = read();
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
recordPluginDescriptorProjectInputs(observed, true);
|
|
1081
|
+
throw error;
|
|
1082
|
+
}
|
|
1083
|
+
const candidateInputs = normalizedProjectConfigPaths(candidate, tsconfig);
|
|
1084
|
+
for (const input of candidateInputs) observed.add(input);
|
|
1085
|
+
const after = pluginDescriptorInputHashes(candidateInputs);
|
|
1086
|
+
const afterRealpaths = pluginDescriptorInputRealpaths(candidateInputs);
|
|
1087
|
+
const afterSignatures =
|
|
1088
|
+
pluginDescriptorInputMetadataSignatures(candidateInputs);
|
|
1089
|
+
if (
|
|
1090
|
+
equalPluginDescriptorInputLists(inputs, candidateInputs) &&
|
|
1091
|
+
equalPluginDescriptorInputHashes(before, after) &&
|
|
1092
|
+
equalPluginDescriptorInputHashes(beforeRealpaths, afterRealpaths) &&
|
|
1093
|
+
beforeSignatures !== undefined &&
|
|
1094
|
+
afterSignatures !== undefined &&
|
|
1095
|
+
equalPluginDescriptorInputHashes(beforeSignatures, afterSignatures)
|
|
1096
|
+
) {
|
|
1097
|
+
for (const input of candidateInputs) {
|
|
1098
|
+
recordPluginDescriptorInput({
|
|
1099
|
+
hash: after[input]!,
|
|
1100
|
+
realpath: afterRealpaths[input]!,
|
|
1101
|
+
resolved: input,
|
|
1102
|
+
signature: afterSignatures[input]!,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
return candidate;
|
|
1106
|
+
}
|
|
1107
|
+
project = candidate;
|
|
1108
|
+
inputs = candidateInputs;
|
|
1109
|
+
}
|
|
1110
|
+
recordPluginDescriptorProjectInputs(observed, true);
|
|
1111
|
+
return project;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function normalizedProjectConfigPaths(
|
|
1115
|
+
project: ReturnType<typeof readProjectConfig>,
|
|
1116
|
+
requestedConfig: string,
|
|
1117
|
+
): string[] {
|
|
1118
|
+
return [
|
|
1119
|
+
...new Set(
|
|
1120
|
+
[requestedConfig, ...project.configPaths].map((file) =>
|
|
1121
|
+
path.resolve(file),
|
|
1122
|
+
),
|
|
1123
|
+
),
|
|
1124
|
+
].sort();
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function pluginDescriptorInputHashes(
|
|
1128
|
+
inputs: readonly string[],
|
|
1129
|
+
): Record<string, string | null> {
|
|
1130
|
+
return Object.fromEntries(
|
|
1131
|
+
inputs.map((input) => [input, pluginDescriptorInputHash(input)]),
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function pluginDescriptorInputRealpaths(
|
|
1136
|
+
inputs: readonly string[],
|
|
1137
|
+
): Record<string, string | null> {
|
|
1138
|
+
return Object.fromEntries(
|
|
1139
|
+
inputs.map((input) => [input, pluginDescriptorInputRealpath(input)]),
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function pluginDescriptorInputMetadataSignatures(
|
|
1144
|
+
inputs: readonly string[],
|
|
1145
|
+
): Record<string, string> | undefined {
|
|
1146
|
+
const output: Record<string, string> = {};
|
|
1147
|
+
for (const input of inputs) {
|
|
1148
|
+
const signature = pluginDescriptorInputMetadataSignature(input);
|
|
1149
|
+
if (signature === undefined) return undefined;
|
|
1150
|
+
output[path.resolve(input)] = signature;
|
|
1151
|
+
}
|
|
1152
|
+
return output;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function equalPluginDescriptorInputLists(
|
|
1156
|
+
first: readonly string[],
|
|
1157
|
+
second: readonly string[],
|
|
1158
|
+
): boolean {
|
|
1159
|
+
return (
|
|
1160
|
+
first.length === second.length &&
|
|
1161
|
+
first.every((input, index) => input === second[index])
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function equalPluginDescriptorInputHashes(
|
|
1166
|
+
first: Readonly<Record<string, string | null>>,
|
|
1167
|
+
second: Readonly<Record<string, string | null>>,
|
|
1168
|
+
): boolean {
|
|
1169
|
+
return (
|
|
1170
|
+
Object.keys(first).length === Object.keys(second).length &&
|
|
1171
|
+
Object.entries(first).every(([input, hash]) => second[input] === hash)
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function recordPluginDescriptorProjectInputs(
|
|
1176
|
+
inputs: Iterable<string>,
|
|
1177
|
+
unstable: boolean,
|
|
1178
|
+
): void {
|
|
1179
|
+
for (const input of inputs) {
|
|
1180
|
+
const resolved = path.resolve(input);
|
|
1181
|
+
recordPluginDescriptorInput({
|
|
1182
|
+
resolved,
|
|
1183
|
+
...(unstable ? { unstable: true } : {}),
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
501
1188
|
/** Narrow a resolved project's compiler options to the emit-format pair. */
|
|
502
1189
|
export function projectModuleOptions(
|
|
503
1190
|
compilerOptions: Record<string, unknown>,
|
|
@@ -522,12 +1209,32 @@ export function projectModuleOptions(
|
|
|
522
1209
|
function resolveServedSource(
|
|
523
1210
|
filename: string,
|
|
524
1211
|
url: string = pathToFileURL(filename).href,
|
|
1212
|
+
prepareAsEntry: boolean = false,
|
|
525
1213
|
): ServedSource {
|
|
526
1214
|
const real = realPath(filename);
|
|
527
|
-
|
|
1215
|
+
// Only the public preload can prepare a newly discovered root. Direct ttsx
|
|
1216
|
+
// has one pre-built manifest and historically relies on trailing-stem
|
|
1217
|
+
// recovery when Windows presents the same source through its short and long
|
|
1218
|
+
// temp-path spellings (the lint TypeScript-config loader is one such case).
|
|
1219
|
+
// Treating that boundary as prepare-only would discard the existing emit and
|
|
1220
|
+
// feed ESM source into CommonJS interop, producing ERR_REQUIRE_CYCLE_MODULE.
|
|
1221
|
+
const prepareEntry = prepareAsEntry ? prepareRuntimeEntry : undefined;
|
|
1222
|
+
// A JavaScript-to-TypeScript boundary is a new checked root unless an
|
|
1223
|
+
// existing manifest proves exact ownership. Trailing-stem recovery is not
|
|
1224
|
+
// ownership evidence: two out-of-include `index.ts` roots can otherwise map
|
|
1225
|
+
// to the first manifest's `index.js`, skipping the second root's diagnostics.
|
|
1226
|
+
let served = serveEntryEmit(real, prepareEntry === undefined);
|
|
528
1227
|
if (served !== null) {
|
|
529
1228
|
return withInlineSourceMap(served);
|
|
530
1229
|
}
|
|
1230
|
+
if (prepareEntry !== undefined) {
|
|
1231
|
+
registeredManifests.push(prepareEntry(real));
|
|
1232
|
+
served = serveEntryEmit(real);
|
|
1233
|
+
if (served === null) {
|
|
1234
|
+
throw new Error(`ttsx: prepared entry emit not found for ${filename}`);
|
|
1235
|
+
}
|
|
1236
|
+
return withInlineSourceMap(served);
|
|
1237
|
+
}
|
|
531
1238
|
const built = serveDependencyEmit(real);
|
|
532
1239
|
if (built !== null) {
|
|
533
1240
|
return withInlineSourceMap(built);
|
|
@@ -610,7 +1317,7 @@ function emitOrphanAsCommonJs(filename: string): string | null {
|
|
|
610
1317
|
return hit;
|
|
611
1318
|
}
|
|
612
1319
|
}
|
|
613
|
-
const outDir =
|
|
1320
|
+
const outDir = createCanonicalTempDirectory("ttsx-orphan-");
|
|
614
1321
|
try {
|
|
615
1322
|
const res = spawnNative(
|
|
616
1323
|
tsgo,
|
|
@@ -673,7 +1380,7 @@ function emitCommonJsForNameScan(filename: string): string | null {
|
|
|
673
1380
|
commonJsNameScanSources.set(real, null);
|
|
674
1381
|
return null;
|
|
675
1382
|
}
|
|
676
|
-
const outDir =
|
|
1383
|
+
const outDir = createCanonicalTempDirectory("ttsx-export-scan-");
|
|
677
1384
|
try {
|
|
678
1385
|
const res = spawnNative(
|
|
679
1386
|
tsgo,
|
|
@@ -1166,45 +1873,95 @@ function isIdentifierName(name: string): boolean {
|
|
|
1166
1873
|
* `rootDir` cannot have a mirrored emit, so it falls through to the dependency
|
|
1167
1874
|
* paths.
|
|
1168
1875
|
*/
|
|
1169
|
-
function serveEntryEmit(
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
if (emitted === null) {
|
|
1876
|
+
function serveEntryEmit(
|
|
1877
|
+
real: string,
|
|
1878
|
+
allowStemFallback: boolean = true,
|
|
1879
|
+
): ServedSource | null {
|
|
1880
|
+
const owner = findEntryEmit(real, allowStemFallback);
|
|
1881
|
+
if (owner === null) {
|
|
1176
1882
|
return null;
|
|
1177
1883
|
}
|
|
1178
|
-
const source = readFileOrNull(
|
|
1884
|
+
const source = readFileOrNull(owner.emittedFile);
|
|
1179
1885
|
return source === null
|
|
1180
1886
|
? null
|
|
1181
1887
|
: {
|
|
1182
|
-
emittedFile:
|
|
1183
|
-
moduleOptions:
|
|
1888
|
+
emittedFile: owner.emittedFile,
|
|
1889
|
+
moduleOptions: owner.manifest.moduleOptions ?? {},
|
|
1184
1890
|
source,
|
|
1185
1891
|
sourceFile: real,
|
|
1186
1892
|
};
|
|
1187
1893
|
}
|
|
1188
1894
|
|
|
1895
|
+
/**
|
|
1896
|
+
* Find the manifest that owns `real`. Explicit prepared roots win, followed by
|
|
1897
|
+
* exact mirrored paths across every manifest. Only then may legacy stem
|
|
1898
|
+
* recovery run, so one manifest's approximate match cannot shadow another's
|
|
1899
|
+
* exact emit.
|
|
1900
|
+
*/
|
|
1901
|
+
function findEntryEmit(
|
|
1902
|
+
real: string,
|
|
1903
|
+
allowStemFallback: boolean = true,
|
|
1904
|
+
): { emittedFile: string; manifest: RuntimeManifest } | null {
|
|
1905
|
+
const manifests = runtimeManifests();
|
|
1906
|
+
for (const candidate of manifests) {
|
|
1907
|
+
if (
|
|
1908
|
+
candidate.entrySource !== undefined &&
|
|
1909
|
+
candidate.entryFile !== undefined &&
|
|
1910
|
+
realPath(candidate.entrySource) === real &&
|
|
1911
|
+
fs.existsSync(candidate.entryFile)
|
|
1912
|
+
) {
|
|
1913
|
+
return { emittedFile: candidate.entryFile, manifest: candidate };
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
for (const candidate of manifests) {
|
|
1917
|
+
const emitted = entryEmitPath(candidate, real, false);
|
|
1918
|
+
if (emitted !== null) {
|
|
1919
|
+
return { emittedFile: emitted, manifest: candidate };
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (!allowStemFallback) {
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
for (const candidate of manifests) {
|
|
1926
|
+
const emitted = entryEmitPath(candidate, real, true);
|
|
1927
|
+
if (emitted !== null) {
|
|
1928
|
+
return { emittedFile: emitted, manifest: candidate };
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1189
1934
|
/**
|
|
1190
1935
|
* The entry project's emitted JavaScript for `real`, or `null` when that
|
|
1191
1936
|
* project did not emit it. Shared with `owningModuleOptions` so "the entry
|
|
1192
1937
|
* project owns this file" means exactly one thing in both places.
|
|
1193
1938
|
*/
|
|
1194
|
-
function entryEmitPath(
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1939
|
+
function entryEmitPath(
|
|
1940
|
+
m: RuntimeManifest,
|
|
1941
|
+
real: string,
|
|
1942
|
+
allowStemFallback: boolean = true,
|
|
1943
|
+
): string | null {
|
|
1944
|
+
const cache = allowStemFallback
|
|
1945
|
+
? entryEmitPathCache
|
|
1946
|
+
: exactEntryEmitPathCache;
|
|
1947
|
+
let manifestCache = cache.get(m);
|
|
1948
|
+
if (manifestCache === undefined) {
|
|
1949
|
+
manifestCache = new Map<string, string | null>();
|
|
1950
|
+
cache.set(m, manifestCache);
|
|
1951
|
+
}
|
|
1952
|
+
if (manifestCache.has(real)) {
|
|
1953
|
+
return manifestCache.get(real) ?? null;
|
|
1198
1954
|
}
|
|
1199
1955
|
const resolved = isWithin(real, m.rootDir)
|
|
1200
1956
|
? resolveEmittedJavaScript({
|
|
1957
|
+
allowStemFallback,
|
|
1201
1958
|
emittedFiles: m.emittedFiles,
|
|
1202
1959
|
outDir: m.emitDir,
|
|
1203
1960
|
projectRoot: m.rootDir,
|
|
1204
1961
|
sourceFile: real,
|
|
1205
1962
|
})
|
|
1206
1963
|
: null;
|
|
1207
|
-
|
|
1964
|
+
manifestCache.set(real, resolved);
|
|
1208
1965
|
return resolved;
|
|
1209
1966
|
}
|
|
1210
1967
|
|
|
@@ -1212,10 +1969,18 @@ function entryEmitPath(m: RuntimeManifest, real: string): string | null {
|
|
|
1212
1969
|
* Memo for `entryEmitPath`, because it is now on the `resolve` hook's path
|
|
1213
1970
|
* through `owningModuleOptions` — once per import specifier — and a miss inside
|
|
1214
1971
|
* `resolveEmittedJavaScript` walks the whole emit tree. The entry emit is
|
|
1215
|
-
* written once before the run starts and never changes under it
|
|
1216
|
-
*
|
|
1972
|
+
* written once before the run starts and never changes under it. Registration
|
|
1973
|
+
* can add several independent entry emits, so the manifest is part of the cache
|
|
1974
|
+
* identity as well as the source path.
|
|
1217
1975
|
*/
|
|
1218
|
-
const entryEmitPathCache = new
|
|
1976
|
+
const entryEmitPathCache = new WeakMap<
|
|
1977
|
+
RuntimeManifest,
|
|
1978
|
+
Map<string, string | null>
|
|
1979
|
+
>();
|
|
1980
|
+
const exactEntryEmitPathCache = new WeakMap<
|
|
1981
|
+
RuntimeManifest,
|
|
1982
|
+
Map<string, string | null>
|
|
1983
|
+
>();
|
|
1219
1984
|
|
|
1220
1985
|
/**
|
|
1221
1986
|
* True when `real` is `directory` itself or sits beneath it. Handles a root
|
|
@@ -1351,11 +2116,7 @@ interface DependencyCachePaths {
|
|
|
1351
2116
|
}
|
|
1352
2117
|
|
|
1353
2118
|
function dependencyCachePaths(tsconfig: string): DependencyCachePaths {
|
|
1354
|
-
const key =
|
|
1355
|
-
.createHash("sha256")
|
|
1356
|
-
.update(tsconfig)
|
|
1357
|
-
.digest("hex")
|
|
1358
|
-
.slice(0, 16);
|
|
2119
|
+
const key = dependencyCacheKey(tsconfig);
|
|
1359
2120
|
const root = dependencyCacheRoot();
|
|
1360
2121
|
return {
|
|
1361
2122
|
cacheDir: path.join(root, key),
|
|
@@ -1365,6 +2126,38 @@ function dependencyCachePaths(tsconfig: string): DependencyCachePaths {
|
|
|
1365
2126
|
};
|
|
1366
2127
|
}
|
|
1367
2128
|
|
|
2129
|
+
/** Derive one dependency cache key; exported for isolation regressions. */
|
|
2130
|
+
export function dependencyCacheKey(
|
|
2131
|
+
tsconfig: string,
|
|
2132
|
+
options: {
|
|
2133
|
+
descriptorLoad?: boolean;
|
|
2134
|
+
descriptorNonce?: string;
|
|
2135
|
+
} = {},
|
|
2136
|
+
): string {
|
|
2137
|
+
const descriptorLoad =
|
|
2138
|
+
options.descriptorLoad ?? process.env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1";
|
|
2139
|
+
return (
|
|
2140
|
+
crypto
|
|
2141
|
+
.createHash("sha256")
|
|
2142
|
+
.update(tsconfig)
|
|
2143
|
+
// Descriptor evaluation promises a result bound to this process's exact
|
|
2144
|
+
// input observations. Reusing an emit another evaluator built can pair
|
|
2145
|
+
// that process's old source/config bytes with this process's later hashes.
|
|
2146
|
+
// Keep ordinary ttsx worker sharing, but isolate descriptor builds with a
|
|
2147
|
+
// non-reusable process nonce; the in-process `builtProjects` map still
|
|
2148
|
+
// compiles each owning project once.
|
|
2149
|
+
.update(
|
|
2150
|
+
descriptorLoad
|
|
2151
|
+
? `\0descriptor-process:${
|
|
2152
|
+
options.descriptorNonce ?? descriptorProcessCacheNonce
|
|
2153
|
+
}`
|
|
2154
|
+
: "",
|
|
2155
|
+
)
|
|
2156
|
+
.digest("hex")
|
|
2157
|
+
.slice(0, 16)
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
1368
2161
|
/** The immutable emit directory of one build generation under `cacheDir`. */
|
|
1369
2162
|
function dependencyGenerationDir(cacheDir: string, generation: string): string {
|
|
1370
2163
|
return path.join(cacheDir, `gen-${generation}`);
|
|
@@ -1517,7 +2310,7 @@ function buildDependency(
|
|
|
1517
2310
|
cacheDir: string,
|
|
1518
2311
|
metaPath: string,
|
|
1519
2312
|
): BuiltProject {
|
|
1520
|
-
const project =
|
|
2313
|
+
const project = readPluginDescriptorProjectConfig(tsconfig);
|
|
1521
2314
|
const generation = newDependencyGeneration();
|
|
1522
2315
|
const emitDir = dependencyGenerationDir(cacheDir, generation);
|
|
1523
2316
|
fs.rmSync(emitDir, { force: true, recursive: true });
|
|
@@ -1612,10 +2405,28 @@ function publishDependencyMeta(
|
|
|
1612
2405
|
}
|
|
1613
2406
|
}
|
|
1614
2407
|
|
|
1615
|
-
function dependencyCacheRoot(
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
2408
|
+
export function dependencyCacheRoot(
|
|
2409
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
2410
|
+
): string {
|
|
2411
|
+
// Descriptor evaluators are disposable and must not leave one isolated emit
|
|
2412
|
+
// generation in the shared temp cache per load. Their result file already
|
|
2413
|
+
// lives in the evaluator-owned directory that the parent removes in
|
|
2414
|
+
// `finally`; put dependency emits beside it so that cleanup owns both.
|
|
2415
|
+
if (
|
|
2416
|
+
env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1" &&
|
|
2417
|
+
typeof env.TTSC_PLUGIN_DESCRIPTOR_OUT === "string" &&
|
|
2418
|
+
path.isAbsolute(env.TTSC_PLUGIN_DESCRIPTOR_OUT)
|
|
2419
|
+
) {
|
|
2420
|
+
return path.join(
|
|
2421
|
+
path.dirname(env.TTSC_PLUGIN_DESCRIPTOR_OUT),
|
|
2422
|
+
"dependency-cache",
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
const owner = runtimeManifests().find(
|
|
2426
|
+
(candidate) => candidate.depCacheDir.length !== 0,
|
|
2427
|
+
);
|
|
2428
|
+
return owner !== undefined
|
|
2429
|
+
? owner.depCacheDir
|
|
1619
2430
|
: path.join(os.tmpdir(), "ttsx-dep");
|
|
1620
2431
|
}
|
|
1621
2432
|
|
|
@@ -1990,7 +2801,12 @@ function formatDuration(ms: number): string {
|
|
|
1990
2801
|
}
|
|
1991
2802
|
|
|
1992
2803
|
/** Owning-tsconfig cache keyed by directory, mirroring `packageTypeCache`. */
|
|
1993
|
-
|
|
2804
|
+
interface ITsconfigLookup {
|
|
2805
|
+
candidates: readonly string[];
|
|
2806
|
+
result: string | null;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
const tsconfigCache = new Map<string, ITsconfigLookup>();
|
|
1994
2810
|
const moduleOptionsCache = new Map<string, OwningModuleOptions | null>();
|
|
1995
2811
|
|
|
1996
2812
|
/**
|
|
@@ -2011,13 +2827,30 @@ function nearestTsconfig(file: string): string | null {
|
|
|
2011
2827
|
for (;;) {
|
|
2012
2828
|
const cached = tsconfigCache.get(directory);
|
|
2013
2829
|
if (cached !== undefined) {
|
|
2014
|
-
|
|
2830
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") {
|
|
2831
|
+
return rememberTsconfig(chain, cached.result, cached.candidates);
|
|
2832
|
+
}
|
|
2833
|
+
// Bracket the cached selection with the same candidate observations the
|
|
2834
|
+
// parent later reconciles. A nearer config created between a liveness
|
|
2835
|
+
// check and reporting must conflict, not be paired with the cached
|
|
2836
|
+
// farther result and certified as current.
|
|
2837
|
+
recordPluginDescriptorTsconfigCandidates(cached.candidates);
|
|
2838
|
+
const current = nearestExistingTsconfig(cached.candidates);
|
|
2839
|
+
recordPluginDescriptorTsconfigCandidates(cached.candidates);
|
|
2840
|
+
if (current === cached.result) {
|
|
2841
|
+
return rememberTsconfig(chain, cached.result, cached.candidates);
|
|
2842
|
+
}
|
|
2843
|
+
// Descriptor factories can deliberately create a config before a lazy
|
|
2844
|
+
// import. A lookup cached while an earlier import ran must not keep
|
|
2845
|
+
// serving the orphan lane after the nearest candidate changed.
|
|
2846
|
+
tsconfigCache.delete(directory);
|
|
2015
2847
|
}
|
|
2016
2848
|
if (path.basename(directory) === "node_modules") {
|
|
2017
2849
|
return rememberTsconfig(chain, null);
|
|
2018
2850
|
}
|
|
2019
2851
|
chain.push(directory);
|
|
2020
2852
|
const candidate = path.join(directory, "tsconfig.json");
|
|
2853
|
+
recordPluginDescriptorTsconfigCandidates([candidate]);
|
|
2021
2854
|
if (isFile(candidate)) {
|
|
2022
2855
|
return rememberTsconfig(chain, candidate);
|
|
2023
2856
|
}
|
|
@@ -2029,16 +2862,40 @@ function nearestTsconfig(file: string): string | null {
|
|
|
2029
2862
|
}
|
|
2030
2863
|
}
|
|
2031
2864
|
|
|
2865
|
+
function nearestExistingTsconfig(candidates: readonly string[]): string | null {
|
|
2866
|
+
for (const candidate of candidates) {
|
|
2867
|
+
if (isFile(candidate)) return path.resolve(candidate);
|
|
2868
|
+
}
|
|
2869
|
+
return null;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2032
2872
|
function rememberTsconfig(
|
|
2033
2873
|
directories: readonly string[],
|
|
2034
2874
|
result: string | null,
|
|
2875
|
+
tailCandidates: readonly string[] = [],
|
|
2035
2876
|
): string | null {
|
|
2036
|
-
|
|
2037
|
-
|
|
2877
|
+
let candidates = [...tailCandidates];
|
|
2878
|
+
for (let index = directories.length - 1; index >= 0; index -= 1) {
|
|
2879
|
+
const directory = directories[index]!;
|
|
2880
|
+
candidates = [path.join(directory, "tsconfig.json"), ...candidates];
|
|
2881
|
+
tsconfigCache.set(directory, { candidates, result });
|
|
2038
2882
|
}
|
|
2039
2883
|
return result;
|
|
2040
2884
|
}
|
|
2041
2885
|
|
|
2886
|
+
/** Report every owning-config candidate observed by the nearest-config walk. */
|
|
2887
|
+
function recordPluginDescriptorTsconfigCandidates(
|
|
2888
|
+
candidates: readonly string[],
|
|
2889
|
+
): void {
|
|
2890
|
+
if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
|
|
2891
|
+
for (const candidate of candidates) {
|
|
2892
|
+
const resolved = path.resolve(candidate);
|
|
2893
|
+
recordPluginDescriptorInput({
|
|
2894
|
+
resolved,
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2042
2899
|
function readFileOrNull(file: string | null): string | null {
|
|
2043
2900
|
if (file === null) {
|
|
2044
2901
|
return null;
|
|
@@ -2143,6 +3000,15 @@ const JS_TO_TS_EXTENSIONS: ReadonlyMap<string, readonly string[]> = new Map([
|
|
|
2143
3000
|
[".cjs", [".cts"]],
|
|
2144
3001
|
]);
|
|
2145
3002
|
|
|
3003
|
+
/** Candidate sources that can satisfy one missing JavaScript spelling. */
|
|
3004
|
+
function typescriptSourcesForJavaScriptSpecifier(file: string): string[] {
|
|
3005
|
+
const extension = path.extname(file).toLowerCase();
|
|
3006
|
+
const substitutions = JS_TO_TS_EXTENSIONS.get(extension);
|
|
3007
|
+
if (substitutions === undefined) return [];
|
|
3008
|
+
const stem = file.slice(0, file.length - extension.length);
|
|
3009
|
+
return substitutions.map((candidate) => stem + candidate);
|
|
3010
|
+
}
|
|
3011
|
+
|
|
2146
3012
|
/**
|
|
2147
3013
|
* Rescue a `specifier` that Node's resolver rejected: map a JavaScript
|
|
2148
3014
|
* extension back to its TypeScript source, or probe candidate extensions /
|