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
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import childProcess from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
|
-
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
7
8
|
import { findNearestGoMod } from "../../compiler/internal/paths";
|
|
8
9
|
import { readJsonFile } from "../../compiler/internal/project/readConfigJson";
|
|
9
10
|
import { readProjectConfig } from "../../compiler/internal/project/readProjectConfig";
|
|
11
|
+
import { createCanonicalTempDirectory } from "../../internal/createCanonicalTempDirectory";
|
|
12
|
+
import {
|
|
13
|
+
javascriptRuntimeCapabilities,
|
|
14
|
+
resolveNodeBinary,
|
|
15
|
+
} from "../../internal/resolveNodeBinary";
|
|
10
16
|
import type { ITtscPlugin } from "../../structures/ITtscPlugin";
|
|
11
17
|
import type { ITtscPluginContributor } from "../../structures/ITtscPluginContributor";
|
|
12
18
|
import type { ITtscPluginFactoryContext } from "../../structures/ITtscPluginFactoryContext";
|
|
@@ -22,10 +28,6 @@ import {
|
|
|
22
28
|
|
|
23
29
|
const GO_MOD_SEARCH_MAX_DEPTH = 3;
|
|
24
30
|
|
|
25
|
-
type TtscPluginFactory<T = ITtscProjectPluginConfig> = (
|
|
26
|
-
context: ITtscPluginFactoryContext<T>,
|
|
27
|
-
) => ITtscPlugin;
|
|
28
|
-
|
|
29
31
|
type ProjectPluginEntry = {
|
|
30
32
|
baseDir: string;
|
|
31
33
|
config: ITtscProjectPluginConfig;
|
|
@@ -35,27 +37,37 @@ type PackageManifest = {
|
|
|
35
37
|
dependencies?: Record<string, unknown>;
|
|
36
38
|
devDependencies?: Record<string, unknown>;
|
|
37
39
|
exports?: unknown;
|
|
40
|
+
main?: unknown;
|
|
41
|
+
module?: unknown;
|
|
38
42
|
name?: unknown;
|
|
39
43
|
ttsc?: unknown;
|
|
40
44
|
};
|
|
41
45
|
|
|
46
|
+
type ProjectHostInputSnapshot = {
|
|
47
|
+
hostInputHashes: Record<string, string | null>;
|
|
48
|
+
hostInputRealpaths: Record<string, string | null>;
|
|
49
|
+
hostInputs: string[];
|
|
50
|
+
project: ITtscParsedProjectConfig;
|
|
51
|
+
};
|
|
52
|
+
|
|
42
53
|
/**
|
|
43
54
|
* Resolve, load, and build all native plugin sidecars for a TypeScript project.
|
|
44
55
|
*
|
|
45
56
|
* Reads the project config, discovers plugin entries (from tsconfig and package
|
|
46
57
|
* auto-discovery), validates and composes their descriptors, then invokes
|
|
47
58
|
* `buildSourcePlugin` to compile each Go source package into a cached binary.
|
|
48
|
-
* Returns the ordered
|
|
49
|
-
*
|
|
59
|
+
* Returns the ordered native plugins, parsed project config, and exact
|
|
60
|
+
* JavaScript-host files that universally influence the loaded selection.
|
|
50
61
|
*
|
|
51
62
|
* @param options.binary - Absolute path to the ttsc native helper binary.
|
|
52
63
|
* @param options.cacheDir - Override the plugin binary cache directory.
|
|
53
64
|
* @param options.cwd - Working directory for resolving relative paths.
|
|
54
65
|
* @param options.entries - Explicit plugin entries; `false` disables all
|
|
55
66
|
* plugins (skips both tsconfig entries and package auto-discovery).
|
|
56
|
-
* @param options.env - Effective environment for source-plugin builds and
|
|
57
|
-
*
|
|
58
|
-
*
|
|
67
|
+
* @param options.env - Effective environment for source-plugin builds and
|
|
68
|
+
* isolated descriptor evaluators, including the `ttsx` fallback (`{
|
|
69
|
+
* ...process.env, ...context.env }`). Defaults to `process.env` for CLI
|
|
70
|
+
* callers, so ambient behavior is unchanged.
|
|
59
71
|
* @param options.file - Path to the tsconfig/jsconfig file.
|
|
60
72
|
* @param options.pluginConfigDir - Caller-declared anchor for plugin
|
|
61
73
|
* config-file discovery (see `ITtscPluginFactoryContext.pluginConfigDir`).
|
|
@@ -74,16 +86,28 @@ export function loadProjectPlugins(options: {
|
|
|
74
86
|
projectRoot?: string;
|
|
75
87
|
tsconfig?: string;
|
|
76
88
|
}): {
|
|
89
|
+
hostInputHashes: Record<string, string | null>;
|
|
90
|
+
hostInputRealpaths: Record<string, string | null>;
|
|
91
|
+
hostInputs: string[];
|
|
77
92
|
nativePlugins: ITtscLoadedNativePlugin[];
|
|
78
93
|
project: ITtscParsedProjectConfig;
|
|
79
94
|
} {
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
// Snapshot the caller environment before `withPluginLoaderEnv` injects
|
|
96
|
+
// host-owned Node/ttsx locators into process.env. Under a Bun parent the
|
|
97
|
+
// direct descriptor evaluator must remain Bun unless the caller explicitly
|
|
98
|
+
// selected another runtime.
|
|
99
|
+
const effectiveEnv = { ...(options.env ?? process.env) };
|
|
100
|
+
const projectSnapshot = readProjectHostInputSnapshot({
|
|
82
101
|
cwd: options.cwd,
|
|
83
102
|
file: options.file,
|
|
103
|
+
includePluginDiscovery: options.entries === undefined,
|
|
84
104
|
projectRoot: options.projectRoot,
|
|
85
105
|
tsconfig: options.tsconfig,
|
|
86
106
|
});
|
|
107
|
+
const { project } = projectSnapshot;
|
|
108
|
+
const projectHostInputs = projectSnapshot.hostInputs;
|
|
109
|
+
const projectHostInputHashes = projectSnapshot.hostInputHashes;
|
|
110
|
+
const projectHostInputRealpaths = projectSnapshot.hostInputRealpaths;
|
|
87
111
|
const entries: ProjectPluginEntry[] =
|
|
88
112
|
options.entries === false
|
|
89
113
|
? []
|
|
@@ -93,6 +117,17 @@ export function loadProjectPlugins(options: {
|
|
|
93
117
|
if (entries.length === 0) {
|
|
94
118
|
options.onWatchInputs?.([]);
|
|
95
119
|
return {
|
|
120
|
+
...collectHostInputSnapshot(
|
|
121
|
+
project,
|
|
122
|
+
{},
|
|
123
|
+
[],
|
|
124
|
+
projectHostInputs,
|
|
125
|
+
revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
|
|
126
|
+
revalidateHostInputRealpaths(
|
|
127
|
+
projectHostInputRealpaths,
|
|
128
|
+
projectHostInputs,
|
|
129
|
+
),
|
|
130
|
+
),
|
|
96
131
|
nativePlugins: [],
|
|
97
132
|
project,
|
|
98
133
|
};
|
|
@@ -108,16 +143,67 @@ export function loadProjectPlugins(options: {
|
|
|
108
143
|
projectRoot: project.root,
|
|
109
144
|
tsconfig: project.path,
|
|
110
145
|
};
|
|
111
|
-
const
|
|
112
|
-
entries
|
|
113
|
-
|
|
114
|
-
|
|
146
|
+
const loadedEntries = withPluginLoaderEnv(() =>
|
|
147
|
+
entries.map((entry) => {
|
|
148
|
+
const specifier = entry.config.transform;
|
|
149
|
+
if (typeof specifier !== "string" || specifier.length === 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`ttsc: plugin entry is missing a string "transform" field`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const entryCandidates = collectModuleResolutionCandidates(
|
|
155
|
+
specifier,
|
|
156
|
+
path.join(entry.baseDir, "package.json"),
|
|
157
|
+
undefined,
|
|
158
|
+
);
|
|
159
|
+
// Capture every candidate before resolution chooses the descriptor entry.
|
|
160
|
+
// A post-resolution snapshot could bless a higher-priority file created
|
|
161
|
+
// after the resolver had already selected the old entry.
|
|
162
|
+
const entryCandidateHashes = hashHostInputPaths(entryCandidates);
|
|
163
|
+
const entryCandidateRealpaths = realpathHostInputPaths(entryCandidates);
|
|
164
|
+
const request = resolvePluginRequest(specifier, entry.baseDir);
|
|
165
|
+
const loaded = loadPluginEntry(
|
|
115
166
|
entry.config,
|
|
116
167
|
{ ...context, plugin: entry.config },
|
|
117
|
-
|
|
168
|
+
request,
|
|
118
169
|
effectiveEnv,
|
|
119
|
-
)
|
|
120
|
-
|
|
170
|
+
);
|
|
171
|
+
const loadedHostInputHashes = mergeObservedHostInputHashes(
|
|
172
|
+
loaded.hostInputHashes,
|
|
173
|
+
hashHostInputPaths(Object.keys(loaded.hostInputHashes)),
|
|
174
|
+
);
|
|
175
|
+
const loadedHostInputRealpaths = mergeObservedHostInputRealpaths(
|
|
176
|
+
loaded.hostInputRealpaths,
|
|
177
|
+
realpathHostInputPaths(Object.keys(loaded.hostInputRealpaths)),
|
|
178
|
+
);
|
|
179
|
+
const hostInputHashes = mergeObservedHostInputHashes(
|
|
180
|
+
entryCandidateHashes,
|
|
181
|
+
loadedHostInputHashes,
|
|
182
|
+
);
|
|
183
|
+
const hostInputRealpaths = mergeObservedHostInputRealpaths(
|
|
184
|
+
entryCandidateRealpaths,
|
|
185
|
+
loadedHostInputRealpaths,
|
|
186
|
+
);
|
|
187
|
+
for (const input of loaded.hostInputs) {
|
|
188
|
+
const absolute = path.resolve(input);
|
|
189
|
+
if (
|
|
190
|
+
!Object.prototype.hasOwnProperty.call(loadedHostInputHashes, absolute)
|
|
191
|
+
) {
|
|
192
|
+
delete hostInputHashes[absolute];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
...loaded,
|
|
197
|
+
hostInputHashes,
|
|
198
|
+
hostInputRealpaths,
|
|
199
|
+
hostInputs: [...loaded.hostInputs, ...entryCandidates],
|
|
200
|
+
request,
|
|
201
|
+
};
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
const plugins = composePluginSources(
|
|
205
|
+
entries,
|
|
206
|
+
loadedEntries.map((entry) => entry.plugin),
|
|
121
207
|
);
|
|
122
208
|
|
|
123
209
|
const ttscVersion = readTtscVersion();
|
|
@@ -142,6 +228,17 @@ export function loadProjectPlugins(options: {
|
|
|
142
228
|
kind === "linked"
|
|
143
229
|
? `linked_${String(index).padStart(6, "0")}`
|
|
144
230
|
: undefined;
|
|
231
|
+
const hostInputs = validatePluginHostInputs(plugin, index);
|
|
232
|
+
const pluginHostInputHashes = validatePluginHostInputHashes(
|
|
233
|
+
plugin,
|
|
234
|
+
index,
|
|
235
|
+
hostInputs,
|
|
236
|
+
);
|
|
237
|
+
const pluginHostInputRealpaths = validatePluginHostInputRealpaths(
|
|
238
|
+
plugin,
|
|
239
|
+
index,
|
|
240
|
+
hostInputs,
|
|
241
|
+
);
|
|
145
242
|
return {
|
|
146
243
|
capabilities: plugin.capabilities,
|
|
147
244
|
contributors,
|
|
@@ -152,6 +249,20 @@ export function loadProjectPlugins(options: {
|
|
|
152
249
|
name: plugin.name,
|
|
153
250
|
reportsTypeScriptDiagnostics:
|
|
154
251
|
plugin.reportsTypeScriptDiagnostics === true,
|
|
252
|
+
request: loadedEntries[index]!.request,
|
|
253
|
+
hostInputHashes: mergePluginHostInputHashes(
|
|
254
|
+
loadedEntries[index]!.hostInputHashes,
|
|
255
|
+
pluginHostInputHashes,
|
|
256
|
+
loadedEntries[index]!.hostInputs,
|
|
257
|
+
hostInputs,
|
|
258
|
+
),
|
|
259
|
+
hostInputRealpaths: mergePluginHostInputHashes(
|
|
260
|
+
loadedEntries[index]!.hostInputRealpaths,
|
|
261
|
+
pluginHostInputRealpaths,
|
|
262
|
+
loadedEntries[index]!.hostInputs,
|
|
263
|
+
hostInputs,
|
|
264
|
+
),
|
|
265
|
+
hostInputs: [...loadedEntries[index]!.hostInputs, ...hostInputs],
|
|
155
266
|
source,
|
|
156
267
|
stage,
|
|
157
268
|
};
|
|
@@ -242,11 +353,586 @@ export function loadProjectPlugins(options: {
|
|
|
242
353
|
};
|
|
243
354
|
});
|
|
244
355
|
return {
|
|
356
|
+
...collectHostInputSnapshot(
|
|
357
|
+
project,
|
|
358
|
+
context,
|
|
359
|
+
records,
|
|
360
|
+
projectHostInputs,
|
|
361
|
+
revalidateHostInputHashes(projectHostInputHashes, projectHostInputs),
|
|
362
|
+
revalidateHostInputRealpaths(
|
|
363
|
+
projectHostInputRealpaths,
|
|
364
|
+
projectHostInputs,
|
|
365
|
+
),
|
|
366
|
+
),
|
|
245
367
|
nativePlugins: orderNativePlugins(nativePlugins),
|
|
246
368
|
project,
|
|
247
369
|
};
|
|
248
370
|
}
|
|
249
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Collect exact JavaScript-host files that universally influence the loaded
|
|
374
|
+
* transform. Program files belong to the native reference graph; this list is
|
|
375
|
+
* deliberately limited to config ancestry, descriptor entries, the project
|
|
376
|
+
* manifest controlling auto-discovery, and explicit plugin config files.
|
|
377
|
+
*/
|
|
378
|
+
function collectHostInputSnapshot(
|
|
379
|
+
project: ITtscParsedProjectConfig,
|
|
380
|
+
context: { pluginConfigDir?: string },
|
|
381
|
+
records: readonly {
|
|
382
|
+
config: ITtscProjectPluginConfig;
|
|
383
|
+
hostInputHashes: Readonly<Record<string, string | null>>;
|
|
384
|
+
hostInputRealpaths: Readonly<Record<string, string | null>>;
|
|
385
|
+
hostInputs: readonly string[];
|
|
386
|
+
request: string;
|
|
387
|
+
}[],
|
|
388
|
+
baselineInputs: readonly string[],
|
|
389
|
+
baselineHashes: Readonly<Record<string, string | null>>,
|
|
390
|
+
baselineRealpaths: Readonly<Record<string, string | null>>,
|
|
391
|
+
): {
|
|
392
|
+
hostInputHashes: Record<string, string | null>;
|
|
393
|
+
hostInputRealpaths: Record<string, string | null>;
|
|
394
|
+
hostInputs: string[];
|
|
395
|
+
} {
|
|
396
|
+
const inputs = new Set<string>(
|
|
397
|
+
baselineInputs.map((file) => path.resolve(file)),
|
|
398
|
+
);
|
|
399
|
+
const configBase = context.pluginConfigDir ?? path.dirname(project.path);
|
|
400
|
+
for (const record of records) {
|
|
401
|
+
inputs.add(path.resolve(record.request));
|
|
402
|
+
for (const hostInput of record.hostInputs) {
|
|
403
|
+
inputs.add(path.resolve(hostInput));
|
|
404
|
+
}
|
|
405
|
+
const descriptorManifest = findNearestPackageJson(record.request);
|
|
406
|
+
if (descriptorManifest !== undefined) {
|
|
407
|
+
inputs.add(path.resolve(descriptorManifest));
|
|
408
|
+
}
|
|
409
|
+
const configFile = record.config.configFile;
|
|
410
|
+
if (typeof configFile === "string" && configFile.trim() !== "") {
|
|
411
|
+
inputs.add(
|
|
412
|
+
path.isAbsolute(configFile)
|
|
413
|
+
? path.resolve(configFile)
|
|
414
|
+
: path.resolve(configBase, configFile),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const hostInputs = [...inputs].sort();
|
|
419
|
+
const evaluationHashes = mergeObservedHostInputHashes(
|
|
420
|
+
baselineHashes,
|
|
421
|
+
...records.map((record) => record.hostInputHashes),
|
|
422
|
+
);
|
|
423
|
+
const evaluationRealpaths = mergeObservedHostInputRealpaths(
|
|
424
|
+
baselineRealpaths,
|
|
425
|
+
...records.map((record) => record.hostInputRealpaths),
|
|
426
|
+
);
|
|
427
|
+
for (const record of records) {
|
|
428
|
+
for (const input of record.hostInputs) {
|
|
429
|
+
const absolute = path.resolve(input);
|
|
430
|
+
if (
|
|
431
|
+
!Object.prototype.hasOwnProperty.call(record.hostInputHashes, absolute)
|
|
432
|
+
) {
|
|
433
|
+
delete evaluationHashes[absolute];
|
|
434
|
+
}
|
|
435
|
+
if (
|
|
436
|
+
!Object.prototype.hasOwnProperty.call(
|
|
437
|
+
record.hostInputRealpaths,
|
|
438
|
+
absolute,
|
|
439
|
+
)
|
|
440
|
+
) {
|
|
441
|
+
delete evaluationRealpaths[absolute];
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const hostInputHashes = Object.fromEntries(
|
|
446
|
+
hostInputs.flatMap((file) =>
|
|
447
|
+
Object.prototype.hasOwnProperty.call(evaluationHashes, file)
|
|
448
|
+
? ([[file, evaluationHashes[file]!]] as const)
|
|
449
|
+
: [],
|
|
450
|
+
),
|
|
451
|
+
);
|
|
452
|
+
const hostInputRealpaths = Object.fromEntries(
|
|
453
|
+
hostInputs.flatMap((file) =>
|
|
454
|
+
Object.prototype.hasOwnProperty.call(evaluationRealpaths, file)
|
|
455
|
+
? ([[file, evaluationRealpaths[file]!]] as const)
|
|
456
|
+
: [],
|
|
457
|
+
),
|
|
458
|
+
);
|
|
459
|
+
return { hostInputHashes, hostInputRealpaths, hostInputs };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Read one project configuration while its complete discovery surface remains
|
|
464
|
+
* unchanged. The preliminary read discovers the input set; the accepted read is
|
|
465
|
+
* bracketed by equal fingerprints for that same set. A repeatedly changing
|
|
466
|
+
* project is still returned so compilation can make progress, but without any
|
|
467
|
+
* cache proof for the ambiguous snapshot.
|
|
468
|
+
*/
|
|
469
|
+
function readProjectHostInputSnapshot(options: {
|
|
470
|
+
cwd?: string;
|
|
471
|
+
file?: string;
|
|
472
|
+
includePluginDiscovery: boolean;
|
|
473
|
+
projectRoot?: string;
|
|
474
|
+
tsconfig?: string;
|
|
475
|
+
}): ProjectHostInputSnapshot {
|
|
476
|
+
const { includePluginDiscovery, ...projectOptions } = options;
|
|
477
|
+
let project = readProjectConfig(projectOptions);
|
|
478
|
+
let hostInputs = collectProjectHostInputs(project, includePluginDiscovery);
|
|
479
|
+
const observedInputs = new Set(hostInputs);
|
|
480
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
481
|
+
const before = hashHostInputPaths(hostInputs);
|
|
482
|
+
const beforeRealpaths = realpathHostInputPaths(hostInputs);
|
|
483
|
+
const beforeSignatures = hostInputMetadataSignatures(hostInputs);
|
|
484
|
+
const candidateProject = readProjectConfig(projectOptions);
|
|
485
|
+
const candidateInputs = collectProjectHostInputs(
|
|
486
|
+
candidateProject,
|
|
487
|
+
includePluginDiscovery,
|
|
488
|
+
);
|
|
489
|
+
for (const input of candidateInputs) observedInputs.add(input);
|
|
490
|
+
const after = hashHostInputPaths(candidateInputs);
|
|
491
|
+
const afterRealpaths = realpathHostInputPaths(candidateInputs);
|
|
492
|
+
const afterSignatures = hostInputMetadataSignatures(candidateInputs);
|
|
493
|
+
if (
|
|
494
|
+
equalHostInputLists(hostInputs, candidateInputs) &&
|
|
495
|
+
equalHostInputHashes(before, after) &&
|
|
496
|
+
equalHostInputHashes(beforeRealpaths, afterRealpaths) &&
|
|
497
|
+
beforeSignatures !== undefined &&
|
|
498
|
+
afterSignatures !== undefined &&
|
|
499
|
+
equalHostInputHashes(beforeSignatures, afterSignatures)
|
|
500
|
+
) {
|
|
501
|
+
return {
|
|
502
|
+
hostInputHashes: after,
|
|
503
|
+
hostInputRealpaths: afterRealpaths,
|
|
504
|
+
hostInputs: candidateInputs,
|
|
505
|
+
project: candidateProject,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
project = candidateProject;
|
|
509
|
+
hostInputs = candidateInputs;
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
hostInputHashes: {},
|
|
513
|
+
hostInputRealpaths: {},
|
|
514
|
+
hostInputs: [...observedInputs].sort(),
|
|
515
|
+
project,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function equalHostInputLists(
|
|
520
|
+
first: readonly string[],
|
|
521
|
+
second: readonly string[],
|
|
522
|
+
): boolean {
|
|
523
|
+
return (
|
|
524
|
+
first.length === second.length &&
|
|
525
|
+
first.every(
|
|
526
|
+
(file, index) => path.resolve(file) === path.resolve(second[index]!),
|
|
527
|
+
)
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function equalHostInputHashes(
|
|
532
|
+
first: Readonly<Record<string, string | null>>,
|
|
533
|
+
second: Readonly<Record<string, string | null>>,
|
|
534
|
+
): boolean {
|
|
535
|
+
const files = Object.keys(first);
|
|
536
|
+
return (
|
|
537
|
+
files.length === Object.keys(second).length &&
|
|
538
|
+
files.every(
|
|
539
|
+
(file) =>
|
|
540
|
+
Object.prototype.hasOwnProperty.call(second, file) &&
|
|
541
|
+
first[file] === second[file],
|
|
542
|
+
)
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Retain proof only for inputs whose initial and final observations agree. */
|
|
547
|
+
function revalidateHostInputHashes(
|
|
548
|
+
initial: Readonly<Record<string, string | null>>,
|
|
549
|
+
inputs: readonly string[],
|
|
550
|
+
): Record<string, string | null> {
|
|
551
|
+
const current = hashHostInputPaths(inputs);
|
|
552
|
+
const output: Record<string, string | null> = {};
|
|
553
|
+
for (const input of inputs) {
|
|
554
|
+
const absolute = path.resolve(input);
|
|
555
|
+
if (
|
|
556
|
+
Object.prototype.hasOwnProperty.call(initial, absolute) &&
|
|
557
|
+
initial[absolute] === current[absolute]
|
|
558
|
+
) {
|
|
559
|
+
output[absolute] = current[absolute]!;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return output;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Keep physical-identity proof only while the same path resolves identically. */
|
|
566
|
+
function revalidateHostInputRealpaths(
|
|
567
|
+
initial: Readonly<Record<string, string | null>>,
|
|
568
|
+
inputs: readonly string[],
|
|
569
|
+
): Record<string, string | null> {
|
|
570
|
+
const current = realpathHostInputPaths(inputs);
|
|
571
|
+
return Object.fromEntries(
|
|
572
|
+
inputs.flatMap((input) => {
|
|
573
|
+
const absolute = path.resolve(input);
|
|
574
|
+
return Object.prototype.hasOwnProperty.call(initial, absolute) &&
|
|
575
|
+
initial[absolute] === current[absolute]
|
|
576
|
+
? ([[absolute, current[absolute]!]] as const)
|
|
577
|
+
: [];
|
|
578
|
+
}),
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function hashHostInput(file: string): string | null {
|
|
583
|
+
try {
|
|
584
|
+
if (fs.statSync(file).isDirectory()) {
|
|
585
|
+
return crypto
|
|
586
|
+
.createHash("sha256")
|
|
587
|
+
.update("ttsc:host-input:directory\0")
|
|
588
|
+
.digest("hex");
|
|
589
|
+
}
|
|
590
|
+
return crypto
|
|
591
|
+
.createHash("sha256")
|
|
592
|
+
.update(fs.readFileSync(file))
|
|
593
|
+
.digest("hex");
|
|
594
|
+
} catch {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
export function hashHostInputPaths(
|
|
600
|
+
files: readonly string[],
|
|
601
|
+
): Record<string, string | null> {
|
|
602
|
+
return Object.fromEntries(
|
|
603
|
+
files.map((file) => [path.resolve(file), hashHostInput(file)]),
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Physical path selected by a host input, or null while it is unresolved. */
|
|
608
|
+
export function realpathHostInput(file: string): string | null {
|
|
609
|
+
try {
|
|
610
|
+
return fs.realpathSync.native(file);
|
|
611
|
+
} catch {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function realpathHostInputPaths(
|
|
617
|
+
files: readonly string[],
|
|
618
|
+
): Record<string, string | null> {
|
|
619
|
+
return Object.fromEntries(
|
|
620
|
+
files.map((file) => [path.resolve(file), realpathHostInput(file)]),
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Metadata identity that survives content-preserving reads but exposes A-B-A
|
|
626
|
+
* replacement. Missing paths are tied to the nearest existing ancestor whose
|
|
627
|
+
* directory metadata changes when the missing branch appears or disappears.
|
|
628
|
+
*/
|
|
629
|
+
function hostInputMetadataSignature(file: string): string | undefined {
|
|
630
|
+
const requested = path.resolve(file);
|
|
631
|
+
let current = requested;
|
|
632
|
+
for (;;) {
|
|
633
|
+
try {
|
|
634
|
+
const link = fs.lstatSync(current, { bigint: true });
|
|
635
|
+
let target = link;
|
|
636
|
+
if (link.isSymbolicLink()) {
|
|
637
|
+
try {
|
|
638
|
+
target = fs.statSync(current, { bigint: true });
|
|
639
|
+
} catch {
|
|
640
|
+
// A broken link's own metadata cannot expose its target appearing
|
|
641
|
+
// and disappearing during evaluation. Decline cache proof instead.
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return [
|
|
646
|
+
path.relative(current, requested),
|
|
647
|
+
link.dev,
|
|
648
|
+
link.ino,
|
|
649
|
+
link.mode,
|
|
650
|
+
link.size,
|
|
651
|
+
link.mtimeNs,
|
|
652
|
+
link.ctimeNs,
|
|
653
|
+
target.dev,
|
|
654
|
+
target.ino,
|
|
655
|
+
target.mode,
|
|
656
|
+
target.size,
|
|
657
|
+
target.mtimeNs,
|
|
658
|
+
target.ctimeNs,
|
|
659
|
+
].join(":");
|
|
660
|
+
} catch (error) {
|
|
661
|
+
if (!isMissingPathError(error)) return undefined;
|
|
662
|
+
const parent = path.dirname(current);
|
|
663
|
+
if (parent === current) return undefined;
|
|
664
|
+
current = parent;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function hostInputMetadataSignatures(
|
|
670
|
+
files: readonly string[],
|
|
671
|
+
): Record<string, string> | undefined {
|
|
672
|
+
const output: Record<string, string> = {};
|
|
673
|
+
for (const file of files) {
|
|
674
|
+
const signature = hostInputMetadataSignature(file);
|
|
675
|
+
if (signature === undefined) return undefined;
|
|
676
|
+
output[path.resolve(file)] = signature;
|
|
677
|
+
}
|
|
678
|
+
return output;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function isMissingPathError(error: unknown): boolean {
|
|
682
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
683
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Return config ancestry and the manifest controlling package discovery. */
|
|
687
|
+
export function collectProjectHostInputs(
|
|
688
|
+
project: ITtscParsedProjectConfig,
|
|
689
|
+
includePluginDiscovery: boolean = true,
|
|
690
|
+
): string[] {
|
|
691
|
+
const inputs = new Set<string>(
|
|
692
|
+
project.configPaths.map((file) => path.resolve(file)),
|
|
693
|
+
);
|
|
694
|
+
if (!includePluginDiscovery) return [...inputs].sort();
|
|
695
|
+
const manifestCandidates = collectNearestPackageJsonCandidates(project.root);
|
|
696
|
+
for (const candidate of manifestCandidates) inputs.add(candidate);
|
|
697
|
+
const manifest = manifestCandidates.find(existingFile);
|
|
698
|
+
if (manifest !== undefined) {
|
|
699
|
+
const projectManifest = readPackageManifest(manifest);
|
|
700
|
+
if (projectManifest !== undefined) {
|
|
701
|
+
const projectRoot = path.dirname(manifest);
|
|
702
|
+
for (const dependency of directDependencyNames(projectManifest)) {
|
|
703
|
+
const dependencyManifest = resolveDependencyPackageJson(
|
|
704
|
+
dependency,
|
|
705
|
+
projectRoot,
|
|
706
|
+
);
|
|
707
|
+
for (const candidate of collectDependencyManifestCandidates(
|
|
708
|
+
dependency,
|
|
709
|
+
manifest,
|
|
710
|
+
dependencyManifest,
|
|
711
|
+
)) {
|
|
712
|
+
inputs.add(candidate);
|
|
713
|
+
}
|
|
714
|
+
if (dependencyManifest !== undefined) {
|
|
715
|
+
inputs.add(path.resolve(dependencyManifest));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return [...inputs].sort();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// The direct evaluator preloads ttsx's supported Node hook, whose
|
|
724
|
+
// extensionless rescue order includes TypeScript and ESM/CJS spellings in
|
|
725
|
+
// addition to Node's ordinary CommonJS probes.
|
|
726
|
+
const MODULE_PROBE_EXTENSIONS = [
|
|
727
|
+
".ts",
|
|
728
|
+
".tsx",
|
|
729
|
+
".mts",
|
|
730
|
+
".cts",
|
|
731
|
+
".js",
|
|
732
|
+
".mjs",
|
|
733
|
+
".cjs",
|
|
734
|
+
".json",
|
|
735
|
+
".node",
|
|
736
|
+
] as const;
|
|
737
|
+
|
|
738
|
+
/** Record every file whose later appearance can change one module resolution. */
|
|
739
|
+
function collectModuleResolutionCandidates(
|
|
740
|
+
specifier: string,
|
|
741
|
+
parentFile: string,
|
|
742
|
+
resolvedFile: string | undefined,
|
|
743
|
+
): string[] {
|
|
744
|
+
const inputs = new Set<string>();
|
|
745
|
+
const recordedBases = new Set<string>();
|
|
746
|
+
const candidates = (base: string): string[] => [
|
|
747
|
+
base,
|
|
748
|
+
...MODULE_PROBE_EXTENSIONS.map((extension) => base + extension),
|
|
749
|
+
path.join(base, "package.json"),
|
|
750
|
+
...MODULE_PROBE_EXTENSIONS.map((extension) =>
|
|
751
|
+
path.join(base, `index${extension}`),
|
|
752
|
+
),
|
|
753
|
+
];
|
|
754
|
+
const recordManifestTargets = (
|
|
755
|
+
value: unknown,
|
|
756
|
+
directory: string,
|
|
757
|
+
allowBare: boolean = false,
|
|
758
|
+
): void => {
|
|
759
|
+
if (typeof value === "string") {
|
|
760
|
+
if (
|
|
761
|
+
value !== "" &&
|
|
762
|
+
(allowBare || value.startsWith("./") || value.startsWith("../"))
|
|
763
|
+
) {
|
|
764
|
+
recordBase(path.resolve(directory, value));
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (Array.isArray(value)) {
|
|
769
|
+
for (const item of value) {
|
|
770
|
+
recordManifestTargets(item, directory, allowBare);
|
|
771
|
+
}
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (isRecord(value)) {
|
|
775
|
+
for (const item of Object.values(value)) {
|
|
776
|
+
recordManifestTargets(item, directory, allowBare);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
const recordBase = (base: string): void => {
|
|
781
|
+
const resolvedBase = path.resolve(base);
|
|
782
|
+
if (recordedBases.has(resolvedBase)) return;
|
|
783
|
+
recordedBases.add(resolvedBase);
|
|
784
|
+
for (const candidate of candidates(resolvedBase)) {
|
|
785
|
+
inputs.add(path.resolve(candidate));
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
const manifest = readJsonFile(path.join(resolvedBase, "package.json"));
|
|
789
|
+
if (isRecord(manifest)) {
|
|
790
|
+
recordManifestTargets(manifest.exports, resolvedBase);
|
|
791
|
+
recordManifestTargets(manifest.module, resolvedBase, true);
|
|
792
|
+
recordManifestTargets(manifest.main, resolvedBase, true);
|
|
793
|
+
}
|
|
794
|
+
} catch {
|
|
795
|
+
// A malformed selected manifest is reported by normal resolution/load.
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
const selectedBy = (base: string): boolean => {
|
|
799
|
+
if (resolvedFile === undefined) return false;
|
|
800
|
+
let selected: string;
|
|
801
|
+
try {
|
|
802
|
+
selected = fs.realpathSync.native(resolvedFile);
|
|
803
|
+
} catch {
|
|
804
|
+
selected = path.resolve(resolvedFile);
|
|
805
|
+
}
|
|
806
|
+
for (const candidate of candidates(base)) {
|
|
807
|
+
try {
|
|
808
|
+
const canonical = fs.realpathSync.native(candidate);
|
|
809
|
+
const relative = path.relative(canonical, selected);
|
|
810
|
+
if (
|
|
811
|
+
relative === "" ||
|
|
812
|
+
(fs.statSync(canonical).isDirectory() &&
|
|
813
|
+
relative !== ".." &&
|
|
814
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
815
|
+
!path.isAbsolute(relative))
|
|
816
|
+
) {
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
// Missing candidates are the inputs this function intentionally keeps.
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return false;
|
|
824
|
+
};
|
|
825
|
+
const localBases = (): string[] => {
|
|
826
|
+
if (specifier.startsWith("file:")) return [fileURLToPath(specifier)];
|
|
827
|
+
const directory = path.dirname(parentFile);
|
|
828
|
+
const raw = path.resolve(directory, specifier);
|
|
829
|
+
const suffixStart = specifier.search(/[?#]/);
|
|
830
|
+
if (suffixStart === -1) return [raw];
|
|
831
|
+
const pathname = specifier.slice(0, suffixStart);
|
|
832
|
+
return pathname === ""
|
|
833
|
+
? [raw]
|
|
834
|
+
: [...new Set([raw, path.resolve(directory, pathname)])];
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
if (
|
|
838
|
+
specifier.startsWith(".") ||
|
|
839
|
+
path.isAbsolute(specifier) ||
|
|
840
|
+
specifier.startsWith("file:")
|
|
841
|
+
) {
|
|
842
|
+
try {
|
|
843
|
+
for (const base of localBases()) {
|
|
844
|
+
// An existing exact file wins before extension and directory probes.
|
|
845
|
+
// Its own recorded identity is therefore sufficient; siblings cannot
|
|
846
|
+
// supersede it while it exists.
|
|
847
|
+
if (
|
|
848
|
+
selectedByExactFile(base, resolvedFile) ||
|
|
849
|
+
(resolvedFile === undefined && existingFile(base))
|
|
850
|
+
) {
|
|
851
|
+
inputs.add(path.resolve(base));
|
|
852
|
+
} else {
|
|
853
|
+
recordBase(base);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
} catch {
|
|
857
|
+
// Invalid URL spellings are diagnosed by the real resolver.
|
|
858
|
+
}
|
|
859
|
+
return [...inputs];
|
|
860
|
+
}
|
|
861
|
+
const parts = specifier.split("/");
|
|
862
|
+
const packageParts = parts[0]?.startsWith("@")
|
|
863
|
+
? parts.slice(0, 2)
|
|
864
|
+
: parts.slice(0, 1);
|
|
865
|
+
if (packageParts.some((part) => part === undefined || part === "")) {
|
|
866
|
+
return [...inputs];
|
|
867
|
+
}
|
|
868
|
+
const packageName = packageParts.join("/");
|
|
869
|
+
const subpath = parts.slice(packageParts.length);
|
|
870
|
+
const searchPaths = createRequire(parentFile).resolve.paths(specifier) ?? [];
|
|
871
|
+
for (const searchPath of searchPaths) {
|
|
872
|
+
const packageDirectory = path.join(searchPath, packageName);
|
|
873
|
+
recordBase(packageDirectory);
|
|
874
|
+
if (subpath.length !== 0) {
|
|
875
|
+
recordBase(path.join(packageDirectory, ...subpath));
|
|
876
|
+
}
|
|
877
|
+
if (selectedBy(packageDirectory)) break;
|
|
878
|
+
}
|
|
879
|
+
return [...inputs];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function existingFile(file: string): boolean {
|
|
883
|
+
try {
|
|
884
|
+
return fs.statSync(file).isFile();
|
|
885
|
+
} catch {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function selectedByExactFile(
|
|
891
|
+
candidate: string,
|
|
892
|
+
selected: string | undefined,
|
|
893
|
+
): boolean {
|
|
894
|
+
if (selected === undefined) return false;
|
|
895
|
+
try {
|
|
896
|
+
return (
|
|
897
|
+
fs.realpathSync.native(candidate) === fs.realpathSync.native(selected)
|
|
898
|
+
);
|
|
899
|
+
} catch {
|
|
900
|
+
return path.resolve(candidate) === path.resolve(selected);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Record package manifests whose later appearance can redirect discovery. */
|
|
905
|
+
function collectDependencyManifestCandidates(
|
|
906
|
+
packageName: string,
|
|
907
|
+
parentFile: string,
|
|
908
|
+
selectedManifest: string | undefined,
|
|
909
|
+
): string[] {
|
|
910
|
+
const out: string[] = [];
|
|
911
|
+
let selectedDirectory: string | undefined;
|
|
912
|
+
if (selectedManifest !== undefined) {
|
|
913
|
+
try {
|
|
914
|
+
selectedDirectory = fs.realpathSync.native(
|
|
915
|
+
path.dirname(selectedManifest),
|
|
916
|
+
);
|
|
917
|
+
} catch {
|
|
918
|
+
selectedDirectory = path.resolve(path.dirname(selectedManifest));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
for (const searchPath of createRequire(parentFile).resolve.paths(
|
|
922
|
+
packageName,
|
|
923
|
+
) ?? []) {
|
|
924
|
+
const packageDirectory = path.join(searchPath, ...packageName.split("/"));
|
|
925
|
+
out.push(path.join(packageDirectory, "package.json"));
|
|
926
|
+
if (selectedDirectory === undefined) continue;
|
|
927
|
+
try {
|
|
928
|
+
if (fs.realpathSync.native(packageDirectory) === selectedDirectory) break;
|
|
929
|
+
} catch {
|
|
930
|
+
// Keep searching until the selected package directory is reached.
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return out;
|
|
934
|
+
}
|
|
935
|
+
|
|
250
936
|
function composePluginSources(
|
|
251
937
|
entries: readonly ProjectPluginEntry[],
|
|
252
938
|
plugins: readonly ITtscPlugin[],
|
|
@@ -521,7 +1207,7 @@ function resolveDependencyPackageJson(
|
|
|
521
1207
|
): string | undefined {
|
|
522
1208
|
const direct = path.join(projectRoot, "node_modules", ...name.split("/"));
|
|
523
1209
|
const directManifest = path.join(direct, "package.json");
|
|
524
|
-
if (
|
|
1210
|
+
if (existingFile(directManifest)) {
|
|
525
1211
|
return resolveRealPath(directManifest);
|
|
526
1212
|
}
|
|
527
1213
|
const projectPackage = path.join(projectRoot, "package.json");
|
|
@@ -538,18 +1224,23 @@ function resolveDependencyPackageJson(
|
|
|
538
1224
|
}
|
|
539
1225
|
|
|
540
1226
|
function findNearestPackageJson(location: string): string | undefined {
|
|
1227
|
+
const selected =
|
|
1228
|
+
collectNearestPackageJsonCandidates(location).find(existingFile);
|
|
1229
|
+
return selected === undefined ? undefined : resolveRealPath(selected);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** Every package-scope candidate through the first regular manifest file. */
|
|
1233
|
+
function collectNearestPackageJsonCandidates(location: string): string[] {
|
|
541
1234
|
let current = fs.statSync(location).isDirectory()
|
|
542
1235
|
? location
|
|
543
1236
|
: path.dirname(location);
|
|
1237
|
+
const candidates: string[] = [];
|
|
544
1238
|
while (true) {
|
|
545
|
-
const manifest = path.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
1239
|
+
const manifest = path.resolve(current, "package.json");
|
|
1240
|
+
candidates.push(manifest);
|
|
1241
|
+
if (existingFile(manifest)) return candidates;
|
|
549
1242
|
const parent = path.dirname(current);
|
|
550
|
-
if (parent === current)
|
|
551
|
-
return undefined;
|
|
552
|
-
}
|
|
1243
|
+
if (parent === current) return candidates;
|
|
553
1244
|
current = parent;
|
|
554
1245
|
}
|
|
555
1246
|
}
|
|
@@ -561,7 +1252,7 @@ function findNearestPackageJson(location: string): string | undefined {
|
|
|
561
1252
|
* message worse here than anywhere else.
|
|
562
1253
|
*/
|
|
563
1254
|
function readPackageManifest(file: string): PackageManifest | undefined {
|
|
564
|
-
if (!
|
|
1255
|
+
if (!existingFile(file)) {
|
|
565
1256
|
return undefined;
|
|
566
1257
|
}
|
|
567
1258
|
const parsed = readJsonFile(file);
|
|
@@ -606,55 +1297,42 @@ function orderNativePlugins(
|
|
|
606
1297
|
function loadPluginEntry(
|
|
607
1298
|
entry: ITtscProjectPluginConfig,
|
|
608
1299
|
base: Omit<ITtscPluginFactoryContext, "dirname" | "filename">,
|
|
609
|
-
|
|
1300
|
+
request: string,
|
|
610
1301
|
effectiveEnv: NodeJS.ProcessEnv,
|
|
611
|
-
):
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1302
|
+
): {
|
|
1303
|
+
hostInputHashes: Record<string, string | null>;
|
|
1304
|
+
hostInputRealpaths: Record<string, string | null>;
|
|
1305
|
+
hostInputs: string[];
|
|
1306
|
+
plugin: ITtscPlugin;
|
|
1307
|
+
} {
|
|
1308
|
+
const specifier = entry.transform;
|
|
1309
|
+
if (typeof specifier !== "string" || specifier.length === 0) {
|
|
1310
|
+
throw new Error(`ttsc: plugin entry is missing a string "transform" field`);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// `dirname`/`filename` are per-entry: each plugin entry resolves to its own
|
|
1314
|
+
// descriptor module, so they are derived here from the resolved `request`
|
|
1315
|
+
// rather than carried on the shared base context. They give factories a
|
|
1316
|
+
// load-mode-independent stand-in for `__dirname`/`__filename`, which are
|
|
1317
|
+
// undefined when a descriptor loads through ttsx or as ESM.
|
|
1318
|
+
const context: ITtscPluginFactoryContext = {
|
|
1319
|
+
...base,
|
|
1320
|
+
dirname: path.dirname(request),
|
|
1321
|
+
filename: request,
|
|
1322
|
+
};
|
|
1323
|
+
const loaded = loadPluginDescriptor(request, context, effectiveEnv);
|
|
1324
|
+
if (isTtscPlugin(loaded.descriptor)) {
|
|
1325
|
+
rejectJsTransformFunctions(specifier, loaded.descriptor);
|
|
1326
|
+
return {
|
|
1327
|
+
hostInputHashes: loaded.hostInputHashes,
|
|
1328
|
+
hostInputRealpaths: loaded.hostInputRealpaths,
|
|
1329
|
+
hostInputs: loaded.inputs,
|
|
1330
|
+
plugin: loaded.descriptor,
|
|
630
1331
|
};
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const candidate =
|
|
636
|
-
mod.createTtscPlugin ??
|
|
637
|
-
mod.default ??
|
|
638
|
-
mod.plugin ??
|
|
639
|
-
(mod as unknown as ITtscPlugin | TtscPluginFactory);
|
|
640
|
-
if (typeof candidate === "function") {
|
|
641
|
-
const plugin = candidate(context);
|
|
642
|
-
if (!isTtscPlugin(plugin)) {
|
|
643
|
-
throw new Error(
|
|
644
|
-
`ttsc: plugin "${specifier}" does not export a valid ttsc plugin`,
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
rejectJsTransformFunctions(specifier, plugin);
|
|
648
|
-
return plugin;
|
|
649
|
-
}
|
|
650
|
-
if (isTtscPlugin(candidate)) {
|
|
651
|
-
rejectJsTransformFunctions(specifier, candidate);
|
|
652
|
-
return candidate;
|
|
653
|
-
}
|
|
654
|
-
throw new Error(
|
|
655
|
-
`ttsc: plugin "${specifier}" does not export a valid ttsc plugin`,
|
|
656
|
-
);
|
|
657
|
-
});
|
|
1332
|
+
}
|
|
1333
|
+
throw new Error(
|
|
1334
|
+
`ttsc: plugin "${specifier}" does not export a valid ttsc plugin`,
|
|
1335
|
+
);
|
|
658
1336
|
}
|
|
659
1337
|
|
|
660
1338
|
/**
|
|
@@ -672,25 +1350,761 @@ function loadPluginEntry(
|
|
|
672
1350
|
* never runs and cannot deadlock. A package that loads directly (a compiled
|
|
673
1351
|
* descriptor, or Bun's native `.ts`) never reaches the fallback.
|
|
674
1352
|
*/
|
|
675
|
-
function
|
|
1353
|
+
function loadPluginDescriptor(
|
|
676
1354
|
request: string,
|
|
677
1355
|
context: ITtscPluginFactoryContext,
|
|
678
1356
|
effectiveEnv: NodeJS.ProcessEnv,
|
|
679
|
-
):
|
|
1357
|
+
): IsolatedPluginDescriptor {
|
|
680
1358
|
try {
|
|
681
|
-
return
|
|
1359
|
+
return loadCommonJsDescriptor(request, context, effectiveEnv);
|
|
682
1360
|
} catch (error) {
|
|
683
|
-
if (
|
|
1361
|
+
if (
|
|
1362
|
+
!TS_SOURCE_PATTERN.test(request) ||
|
|
1363
|
+
!(error instanceof CommonJsDescriptorLoadError) ||
|
|
1364
|
+
!error.retryWithTtsx
|
|
1365
|
+
) {
|
|
684
1366
|
throw error;
|
|
685
1367
|
}
|
|
686
|
-
const
|
|
687
|
-
if (
|
|
1368
|
+
const loaded = loadDescriptorViaTtsx(request, context, effectiveEnv);
|
|
1369
|
+
if (loaded === undefined) {
|
|
688
1370
|
throw error;
|
|
689
1371
|
}
|
|
690
|
-
return
|
|
1372
|
+
return loaded;
|
|
691
1373
|
}
|
|
692
1374
|
}
|
|
693
1375
|
|
|
1376
|
+
interface IsolatedPluginDescriptor {
|
|
1377
|
+
descriptor: unknown;
|
|
1378
|
+
hostInputHashes: Record<string, string | null>;
|
|
1379
|
+
hostInputRealpaths: Record<string, string | null>;
|
|
1380
|
+
inputs: string[];
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
class CommonJsDescriptorLoadError extends Error {
|
|
1384
|
+
public constructor(
|
|
1385
|
+
message: string,
|
|
1386
|
+
public readonly retryWithTtsx: boolean,
|
|
1387
|
+
) {
|
|
1388
|
+
super(message);
|
|
1389
|
+
this.name = "CommonJsDescriptorLoadError";
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Evaluate one CommonJS descriptor in a fresh runtime module-cache generation.
|
|
1395
|
+
*
|
|
1396
|
+
* Reloading an external descriptor dependency inside this process has no safe
|
|
1397
|
+
* cache operation: retaining it serves stale exports, while deleting it splits
|
|
1398
|
+
* any application singleton that required the same module first. Isolation
|
|
1399
|
+
* gives every descriptor load current bytes without mutating the host's cache.
|
|
1400
|
+
* The child invokes the factory before walking its graph, so lazy `require()`
|
|
1401
|
+
* calls are included, and a failed first load cannot strand poisoned children.
|
|
1402
|
+
*/
|
|
1403
|
+
function loadCommonJsDescriptor(
|
|
1404
|
+
request: string,
|
|
1405
|
+
context: ITtscPluginFactoryContext,
|
|
1406
|
+
effectiveEnv: NodeJS.ProcessEnv,
|
|
1407
|
+
): IsolatedPluginDescriptor {
|
|
1408
|
+
const runtime = pluginDescriptorRuntimeBinary(effectiveEnv);
|
|
1409
|
+
const runtimeCapabilities = javascriptRuntimeCapabilities(
|
|
1410
|
+
runtime,
|
|
1411
|
+
effectiveEnv,
|
|
1412
|
+
context.projectRoot,
|
|
1413
|
+
);
|
|
1414
|
+
const node =
|
|
1415
|
+
!runtimeCapabilities.bun &&
|
|
1416
|
+
runtimeCapabilities.registerHooks &&
|
|
1417
|
+
runtimeCapabilities.executable !== undefined
|
|
1418
|
+
? runtimeCapabilities.executable
|
|
1419
|
+
: resolveNodeBinary(effectiveEnv, context.projectRoot);
|
|
1420
|
+
const dir = createEvaluationTempDir();
|
|
1421
|
+
const out = path.join(dir, "descriptor.json");
|
|
1422
|
+
const inputsOut = path.join(dir, "descriptor-inputs.ndjson");
|
|
1423
|
+
const diagnostics = path.join(dir, "descriptor.stderr");
|
|
1424
|
+
const bunConfig = path.join(dir, "bunfig.toml");
|
|
1425
|
+
const runtimeHookPreload = path.join(
|
|
1426
|
+
__dirname,
|
|
1427
|
+
"..",
|
|
1428
|
+
"..",
|
|
1429
|
+
"launcher",
|
|
1430
|
+
"internal",
|
|
1431
|
+
"runtimeHookPreload.js",
|
|
1432
|
+
);
|
|
1433
|
+
try {
|
|
1434
|
+
if (runtimeCapabilities.bun) {
|
|
1435
|
+
// A descriptor receives exactly the environment supplied by its ttsc
|
|
1436
|
+
// invocation. Bun otherwise auto-loads project `.env*`, local/global
|
|
1437
|
+
// bunfig preloads and loaders, and may install a missing package from the
|
|
1438
|
+
// network. Those implicit authorities are neither part of Node's loader
|
|
1439
|
+
// contract nor reproducible host inputs, so isolate this evaluator from
|
|
1440
|
+
// them while retaining Bun's native TypeScript/module semantics.
|
|
1441
|
+
fs.writeFileSync(bunConfig, "", "utf8");
|
|
1442
|
+
}
|
|
1443
|
+
const diagnosticsFd = fs.openSync(diagnostics, "w");
|
|
1444
|
+
let result: ReturnType<typeof childProcess.spawnSync>;
|
|
1445
|
+
try {
|
|
1446
|
+
result = childProcess.spawnSync(
|
|
1447
|
+
runtime,
|
|
1448
|
+
[
|
|
1449
|
+
...(runtimeCapabilities.bun
|
|
1450
|
+
? [
|
|
1451
|
+
"--no-env-file",
|
|
1452
|
+
"--no-install",
|
|
1453
|
+
`--config=${bunConfig}`,
|
|
1454
|
+
`--tsconfig-override=${context.tsconfig}`,
|
|
1455
|
+
]
|
|
1456
|
+
: []),
|
|
1457
|
+
...(runtimeCapabilities.registerHooks
|
|
1458
|
+
? ["--require", runtimeHookPreload]
|
|
1459
|
+
: []),
|
|
1460
|
+
"-e",
|
|
1461
|
+
COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE,
|
|
1462
|
+
],
|
|
1463
|
+
{
|
|
1464
|
+
cwd: context.projectRoot,
|
|
1465
|
+
env: {
|
|
1466
|
+
...effectiveEnv,
|
|
1467
|
+
// The direct evaluator may be Bun, but ttsx and native config
|
|
1468
|
+
// loaders require a real Node runtime with synchronous hooks.
|
|
1469
|
+
...(node === undefined ? {} : { TTSC_NODE_BINARY: node }),
|
|
1470
|
+
TTSC_TTSX_BINARY:
|
|
1471
|
+
effectiveEnv.TTSC_TTSX_BINARY ?? process.env.TTSC_TTSX_BINARY,
|
|
1472
|
+
TTSC_PLUGIN_CONTEXT: JSON.stringify(context),
|
|
1473
|
+
TTSC_PLUGIN_DESCRIPTOR_LOAD: "1",
|
|
1474
|
+
TTSC_PLUGIN_DESCRIPTOR_OUT: out,
|
|
1475
|
+
TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE: "1",
|
|
1476
|
+
TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT: inputsOut,
|
|
1477
|
+
TTSC_PLUGIN_ENTRY: request,
|
|
1478
|
+
},
|
|
1479
|
+
// Hold direct-evaluator diagnostics until its retry decision is
|
|
1480
|
+
// known. A successful ttsx fallback must not inherit the expected
|
|
1481
|
+
// loader stack from the discarded first attempt.
|
|
1482
|
+
stdio: ["ignore", diagnosticsFd, diagnosticsFd],
|
|
1483
|
+
windowsHide: true,
|
|
1484
|
+
},
|
|
1485
|
+
);
|
|
1486
|
+
} finally {
|
|
1487
|
+
fs.closeSync(diagnosticsFd);
|
|
1488
|
+
}
|
|
1489
|
+
const failure = commonJsDescriptorProcessFailure(result, request);
|
|
1490
|
+
if (failure !== undefined) {
|
|
1491
|
+
const reason = pluginDescriptorFailureReason(out);
|
|
1492
|
+
const retryWithTtsx = commonJsDescriptorRetryWithTtsx(out);
|
|
1493
|
+
if (!retryWithTtsx) replayEvaluationDiagnostics(diagnostics);
|
|
1494
|
+
throw new CommonJsDescriptorLoadError(
|
|
1495
|
+
reason === "" ? failure.message : `${failure.message}\n${reason}`,
|
|
1496
|
+
retryWithTtsx,
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
replayEvaluationDiagnostics(diagnostics);
|
|
1500
|
+
if (!fs.existsSync(out)) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
`ttsc: plugin descriptor "${request}" evaluation in an isolated process produced no descriptor output.`,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
let parsed: unknown;
|
|
1506
|
+
try {
|
|
1507
|
+
parsed = JSON.parse(fs.readFileSync(out, "utf8"));
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
`ttsc: plugin descriptor "${request}" produced invalid isolated output: ${errorMessage(error)}`,
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.inputs)) {
|
|
1514
|
+
throw new Error(
|
|
1515
|
+
`ttsc: plugin descriptor "${request}" produced an invalid isolated result`,
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
const parsedHashes = isRecord(parsed.inputHashes)
|
|
1519
|
+
? Object.fromEntries(
|
|
1520
|
+
Object.entries(parsed.inputHashes).flatMap(([file, hash]) =>
|
|
1521
|
+
typeof hash === "string" || hash === null
|
|
1522
|
+
? [[path.resolve(file), hash]]
|
|
1523
|
+
: [],
|
|
1524
|
+
),
|
|
1525
|
+
)
|
|
1526
|
+
: {};
|
|
1527
|
+
const parsedRealpaths = isRecord(parsed.inputRealpaths)
|
|
1528
|
+
? Object.fromEntries(
|
|
1529
|
+
Object.entries(parsed.inputRealpaths).flatMap(([file, realpath]) =>
|
|
1530
|
+
(typeof realpath === "string" && path.isAbsolute(realpath)) ||
|
|
1531
|
+
realpath === null
|
|
1532
|
+
? [
|
|
1533
|
+
[
|
|
1534
|
+
path.resolve(file),
|
|
1535
|
+
realpath === null ? null : path.resolve(realpath),
|
|
1536
|
+
],
|
|
1537
|
+
]
|
|
1538
|
+
: [],
|
|
1539
|
+
),
|
|
1540
|
+
)
|
|
1541
|
+
: {};
|
|
1542
|
+
const stableParsedRealpaths = mergeObservedHostInputRealpaths(
|
|
1543
|
+
parsedRealpaths,
|
|
1544
|
+
realpathHostInputPaths(Object.keys(parsedRealpaths)),
|
|
1545
|
+
);
|
|
1546
|
+
for (const input of parsed.inputs) {
|
|
1547
|
+
const absolute = path.resolve(String(input));
|
|
1548
|
+
if (
|
|
1549
|
+
!Object.prototype.hasOwnProperty.call(stableParsedRealpaths, absolute)
|
|
1550
|
+
) {
|
|
1551
|
+
delete parsedHashes[absolute];
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const runtimeInputs = readTtsxDescriptorInputs(inputsOut, request);
|
|
1555
|
+
return {
|
|
1556
|
+
descriptor: parsed.descriptor,
|
|
1557
|
+
hostInputHashes: omitUnstableHostInputHashes(
|
|
1558
|
+
mergeObservedHostInputHashes(
|
|
1559
|
+
parsedHashes,
|
|
1560
|
+
runtimeInputs.hostInputHashes,
|
|
1561
|
+
),
|
|
1562
|
+
runtimeInputs.unstableInputs,
|
|
1563
|
+
),
|
|
1564
|
+
hostInputRealpaths: mergeObservedHostInputRealpaths(
|
|
1565
|
+
stableParsedRealpaths,
|
|
1566
|
+
runtimeInputs.hostInputRealpaths,
|
|
1567
|
+
),
|
|
1568
|
+
inputs: [
|
|
1569
|
+
...new Set([
|
|
1570
|
+
...parsed.inputs.map((input) => String(input)),
|
|
1571
|
+
...runtimeInputs.inputs,
|
|
1572
|
+
]),
|
|
1573
|
+
].sort(),
|
|
1574
|
+
};
|
|
1575
|
+
} finally {
|
|
1576
|
+
removeEvaluationTempDir(dir);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/** Validate plugin-declared universal host inputs. */
|
|
1581
|
+
function validatePluginHostInputs(
|
|
1582
|
+
plugin: ITtscPlugin,
|
|
1583
|
+
index: number,
|
|
1584
|
+
): string[] {
|
|
1585
|
+
const label = plugin.name ?? `#${index + 1}`;
|
|
1586
|
+
if (plugin.hostInputs === undefined) return [];
|
|
1587
|
+
if (!Array.isArray(plugin.hostInputs)) {
|
|
1588
|
+
throw new Error(
|
|
1589
|
+
`ttsc: plugin ${JSON.stringify(label)} has invalid "hostInputs"; expected an array of absolute paths`,
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
return plugin.hostInputs.map((file) => {
|
|
1593
|
+
if (typeof file !== "string" || !path.isAbsolute(file)) {
|
|
1594
|
+
throw new Error(
|
|
1595
|
+
`ttsc: plugin ${JSON.stringify(label)} has invalid "hostInputs" entry ${JSON.stringify(file)}; expected an absolute path`,
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
return path.resolve(file);
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
/** Validate descriptor-supplied evaluation fingerprints for host inputs. */
|
|
1603
|
+
function validatePluginHostInputHashes(
|
|
1604
|
+
plugin: ITtscPlugin,
|
|
1605
|
+
index: number,
|
|
1606
|
+
hostInputs: readonly string[],
|
|
1607
|
+
): Record<string, string | null> {
|
|
1608
|
+
const label = plugin.name ?? `#${index + 1}`;
|
|
1609
|
+
if (plugin.hostInputHashes === undefined) return {};
|
|
1610
|
+
if (
|
|
1611
|
+
!isRecord(plugin.hostInputHashes) ||
|
|
1612
|
+
Array.isArray(plugin.hostInputHashes)
|
|
1613
|
+
) {
|
|
1614
|
+
throw new Error(
|
|
1615
|
+
`ttsc: plugin ${JSON.stringify(label)} has invalid "hostInputHashes"; expected an object keyed by absolute hostInputs paths`,
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
const allowed = new Set(hostInputs.map((file) => path.resolve(file)));
|
|
1619
|
+
const output: Record<string, string | null> = {};
|
|
1620
|
+
for (const [file, hash] of Object.entries(plugin.hostInputHashes)) {
|
|
1621
|
+
if (!path.isAbsolute(file)) {
|
|
1622
|
+
throw new Error(
|
|
1623
|
+
`ttsc: plugin ${JSON.stringify(label)} has invalid "hostInputHashes" key ${JSON.stringify(file)}; expected an absolute path`,
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
const absolute = path.resolve(file);
|
|
1627
|
+
if (!allowed.has(absolute)) {
|
|
1628
|
+
throw new Error(
|
|
1629
|
+
`ttsc: plugin ${JSON.stringify(label)} fingerprints ${JSON.stringify(file)} without listing it in "hostInputs"`,
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
if (
|
|
1633
|
+
hash !== null &&
|
|
1634
|
+
(typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash))
|
|
1635
|
+
) {
|
|
1636
|
+
throw new Error(
|
|
1637
|
+
`ttsc: plugin ${JSON.stringify(label)} has invalid fingerprint for ${JSON.stringify(file)}; expected a lowercase SHA-256 digest or null`,
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
output[absolute] = hash;
|
|
1641
|
+
}
|
|
1642
|
+
return output;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/** Validate descriptor-supplied physical identities for host inputs. */
|
|
1646
|
+
function validatePluginHostInputRealpaths(
|
|
1647
|
+
plugin: ITtscPlugin,
|
|
1648
|
+
index: number,
|
|
1649
|
+
hostInputs: readonly string[],
|
|
1650
|
+
): Record<string, string | null> {
|
|
1651
|
+
const label = plugin.name ?? `#${index + 1}`;
|
|
1652
|
+
if (plugin.hostInputRealpaths === undefined) return {};
|
|
1653
|
+
if (
|
|
1654
|
+
!isRecord(plugin.hostInputRealpaths) ||
|
|
1655
|
+
Array.isArray(plugin.hostInputRealpaths)
|
|
1656
|
+
) {
|
|
1657
|
+
throw new Error(
|
|
1658
|
+
`ttsc: plugin ${JSON.stringify(label)} has invalid "hostInputRealpaths"; expected an object keyed by absolute hostInputs paths`,
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
const allowed = new Set(hostInputs.map((file) => path.resolve(file)));
|
|
1662
|
+
const output: Record<string, string | null> = {};
|
|
1663
|
+
for (const [file, realpath] of Object.entries(plugin.hostInputRealpaths)) {
|
|
1664
|
+
if (!path.isAbsolute(file)) {
|
|
1665
|
+
throw new Error(
|
|
1666
|
+
`ttsc: plugin ${JSON.stringify(label)} has invalid "hostInputRealpaths" key ${JSON.stringify(file)}; expected an absolute path`,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
const absolute = path.resolve(file);
|
|
1670
|
+
if (!allowed.has(absolute)) {
|
|
1671
|
+
throw new Error(
|
|
1672
|
+
`ttsc: plugin ${JSON.stringify(label)} identifies ${JSON.stringify(file)} without listing it in "hostInputs"`,
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
if (
|
|
1676
|
+
realpath !== null &&
|
|
1677
|
+
(typeof realpath !== "string" || !path.isAbsolute(realpath))
|
|
1678
|
+
) {
|
|
1679
|
+
throw new Error(
|
|
1680
|
+
`ttsc: plugin ${JSON.stringify(label)} has invalid physical identity for ${JSON.stringify(file)}; expected an absolute realpath or null`,
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
output[absolute] =
|
|
1684
|
+
realpath === null ? null : path.resolve(realpath as string);
|
|
1685
|
+
}
|
|
1686
|
+
return output;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/** Merge snapshots and omit every path observed in contradictory states. */
|
|
1690
|
+
function mergeObservedHostInputHashes(
|
|
1691
|
+
...sources: Readonly<Record<string, string | null>>[]
|
|
1692
|
+
): Record<string, string | null> {
|
|
1693
|
+
const conflicts = new Set<string>();
|
|
1694
|
+
const output: Record<string, string | null> = {};
|
|
1695
|
+
for (const source of sources) {
|
|
1696
|
+
for (const [file, hash] of Object.entries(source)) {
|
|
1697
|
+
const absolute = path.resolve(file);
|
|
1698
|
+
if (conflicts.has(absolute)) continue;
|
|
1699
|
+
if (
|
|
1700
|
+
Object.prototype.hasOwnProperty.call(output, absolute) &&
|
|
1701
|
+
output[absolute] !== hash
|
|
1702
|
+
) {
|
|
1703
|
+
delete output[absolute];
|
|
1704
|
+
conflicts.add(absolute);
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
output[absolute] = hash;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return output;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const mergeObservedHostInputRealpaths = mergeObservedHostInputHashes;
|
|
1714
|
+
|
|
1715
|
+
/** Prevent a later plugin claim from reviving an unstable loader input. */
|
|
1716
|
+
function mergePluginHostInputHashes(
|
|
1717
|
+
first: Readonly<Record<string, string | null>>,
|
|
1718
|
+
second: Readonly<Record<string, string | null>>,
|
|
1719
|
+
loaderInputs: readonly string[],
|
|
1720
|
+
pluginInputs: readonly string[],
|
|
1721
|
+
): Record<string, string | null> {
|
|
1722
|
+
const output = { ...first };
|
|
1723
|
+
const guarded = new Set(loaderInputs.map((file) => path.resolve(file)));
|
|
1724
|
+
for (const input of pluginInputs) {
|
|
1725
|
+
const absolute = path.resolve(input);
|
|
1726
|
+
if (!Object.prototype.hasOwnProperty.call(second, absolute)) {
|
|
1727
|
+
delete output[absolute];
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
for (const [file, hash] of Object.entries(second)) {
|
|
1731
|
+
const absolute = path.resolve(file);
|
|
1732
|
+
if (
|
|
1733
|
+
guarded.has(absolute) &&
|
|
1734
|
+
!Object.prototype.hasOwnProperty.call(first, absolute)
|
|
1735
|
+
) {
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
if (
|
|
1739
|
+
Object.prototype.hasOwnProperty.call(output, absolute) &&
|
|
1740
|
+
output[absolute] !== hash
|
|
1741
|
+
) {
|
|
1742
|
+
delete output[absolute];
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
output[absolute] = hash;
|
|
1746
|
+
}
|
|
1747
|
+
return output;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function omitUnstableHostInputHashes(
|
|
1751
|
+
hashes: Record<string, string | null>,
|
|
1752
|
+
unstableInputs: readonly string[],
|
|
1753
|
+
): Record<string, string | null> {
|
|
1754
|
+
for (const input of unstableInputs) delete hashes[path.resolve(input)];
|
|
1755
|
+
return hashes;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/** CommonJS evaluator emitted into a clean Node process for every load. */
|
|
1759
|
+
export const COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE = [
|
|
1760
|
+
`const crypto = require("node:crypto");`,
|
|
1761
|
+
`const fs = require("node:fs");`,
|
|
1762
|
+
`const Module = require("node:module");`,
|
|
1763
|
+
`const path = require("node:path");`,
|
|
1764
|
+
`const { fileURLToPath } = require("node:url");`,
|
|
1765
|
+
`const out = process.env.TTSC_PLUGIN_DESCRIPTOR_OUT;`,
|
|
1766
|
+
`let retryWithTtsx = false;`,
|
|
1767
|
+
`const moduleLoadFailures = new WeakMap();`,
|
|
1768
|
+
`const moduleResolutionFailures = new WeakMap();`,
|
|
1769
|
+
`function existingFile(file) { try { return fs.statSync(file).isFile(); } catch { return false; } }`,
|
|
1770
|
+
`function shouldRetryWithTtsx(error) {`,
|
|
1771
|
+
` const loadFailure = error && (typeof error === "object" || typeof error === "function") ? moduleLoadFailures.get(error) : undefined;`,
|
|
1772
|
+
` if (loadFailure?.retryWithTtsx === true) return true;`,
|
|
1773
|
+
` const failure = error && (typeof error === "object" || typeof error === "function") ? moduleResolutionFailures.get(error) : undefined;`,
|
|
1774
|
+
` return failure?.retryWithTtsx === true;`,
|
|
1775
|
+
`}`,
|
|
1776
|
+
`try {`,
|
|
1777
|
+
` const request = process.env.TTSC_PLUGIN_ENTRY;`,
|
|
1778
|
+
` const context = JSON.parse(process.env.TTSC_PLUGIN_CONTEXT);`,
|
|
1779
|
+
` const inputs = new Set();`,
|
|
1780
|
+
` const inputHashes = new Map();`,
|
|
1781
|
+
` const inputRealpaths = new Map();`,
|
|
1782
|
+
` const inputSignatures = new Map();`,
|
|
1783
|
+
` const unstableInputHashes = new Set();`,
|
|
1784
|
+
` function missingPathError(error) { return error && (error.code === "ENOENT" || error.code === "ENOTDIR"); }`,
|
|
1785
|
+
` function inputMetadataSignature(file) {`,
|
|
1786
|
+
` const requested = path.resolve(file);`,
|
|
1787
|
+
` let current = requested;`,
|
|
1788
|
+
` for (;;) {`,
|
|
1789
|
+
` try {`,
|
|
1790
|
+
` const link = fs.lstatSync(current, { bigint: true });`,
|
|
1791
|
+
` let target = link;`,
|
|
1792
|
+
` if (link.isSymbolicLink()) {`,
|
|
1793
|
+
` try { target = fs.statSync(current, { bigint: true }); }`,
|
|
1794
|
+
` catch { return undefined; }`,
|
|
1795
|
+
` }`,
|
|
1796
|
+
` return [path.relative(current, requested), link.dev, link.ino, link.mode, link.size, link.mtimeNs, link.ctimeNs, target.dev, target.ino, target.mode, target.size, target.mtimeNs, target.ctimeNs].join(":");`,
|
|
1797
|
+
` } catch (error) {`,
|
|
1798
|
+
` if (!missingPathError(error)) return undefined;`,
|
|
1799
|
+
` const parent = path.dirname(current);`,
|
|
1800
|
+
` if (parent === current) return undefined;`,
|
|
1801
|
+
` current = parent;`,
|
|
1802
|
+
` }`,
|
|
1803
|
+
` }`,
|
|
1804
|
+
` }`,
|
|
1805
|
+
` function recordOneInput(file) {`,
|
|
1806
|
+
` file = path.resolve(file);`,
|
|
1807
|
+
` inputs.add(file);`,
|
|
1808
|
+
` if (unstableInputHashes.has(file)) return;`,
|
|
1809
|
+
` const beforeSignature = inputMetadataSignature(file);`,
|
|
1810
|
+
` let observedRealpath;`,
|
|
1811
|
+
` try { observedRealpath = fs.realpathSync.native(file); } catch { observedRealpath = null; }`,
|
|
1812
|
+
` let observed;`,
|
|
1813
|
+
` try { observed = fs.statSync(file).isDirectory() ? crypto.createHash("sha256").update("ttsc:host-input:directory\\0").digest("hex") : crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); } catch { observed = null; }`,
|
|
1814
|
+
` const afterSignature = inputMetadataSignature(file);`,
|
|
1815
|
+
` if (beforeSignature === undefined || afterSignature === undefined || beforeSignature !== afterSignature || (inputSignatures.has(file) && inputSignatures.get(file) !== afterSignature) || (inputRealpaths.has(file) && inputRealpaths.get(file) !== observedRealpath) || (inputHashes.has(file) && inputHashes.get(file) !== observed)) { inputHashes.delete(file); inputRealpaths.delete(file); inputSignatures.delete(file); unstableInputHashes.add(file); return; }`,
|
|
1816
|
+
` inputSignatures.set(file, afterSignature);`,
|
|
1817
|
+
` inputRealpaths.set(file, observedRealpath);`,
|
|
1818
|
+
` inputHashes.set(file, observed);`,
|
|
1819
|
+
` }`,
|
|
1820
|
+
` function recordInput(file) {`,
|
|
1821
|
+
` file = path.resolve(file);`,
|
|
1822
|
+
` recordOneInput(file);`,
|
|
1823
|
+
` const parsed = path.parse(file);`,
|
|
1824
|
+
` let current = parsed.root;`,
|
|
1825
|
+
` const relative = path.relative(parsed.root, file);`,
|
|
1826
|
+
` for (const segment of relative.split(path.sep).slice(0, -1)) {`,
|
|
1827
|
+
` if (segment === "") continue;`,
|
|
1828
|
+
` current = path.join(current, segment);`,
|
|
1829
|
+
` try {`,
|
|
1830
|
+
` if (fs.lstatSync(current).isSymbolicLink()) recordOneInput(current);`,
|
|
1831
|
+
` } catch {`,
|
|
1832
|
+
` break;`,
|
|
1833
|
+
` }`,
|
|
1834
|
+
` }`,
|
|
1835
|
+
` }`,
|
|
1836
|
+
` function asFile(resolved) {`,
|
|
1837
|
+
` if (typeof resolved !== "string") return undefined;`,
|
|
1838
|
+
` if (!resolved.startsWith("file:")) return path.isAbsolute(resolved) ? path.resolve(resolved) : undefined;`,
|
|
1839
|
+
` try { return path.resolve(fileURLToPath(resolved)); } catch { return undefined; }`,
|
|
1840
|
+
` }`,
|
|
1841
|
+
` function recordFile(resolved) {`,
|
|
1842
|
+
` const file = asFile(resolved);`,
|
|
1843
|
+
` if (file === undefined) return;`,
|
|
1844
|
+
` recordInput(file);`,
|
|
1845
|
+
` for (let directory = path.dirname(file);;) {`,
|
|
1846
|
+
` const manifest = path.join(directory, "package.json");`,
|
|
1847
|
+
` recordInput(manifest);`,
|
|
1848
|
+
` if (existingFile(manifest)) break;`,
|
|
1849
|
+
` const parent = path.dirname(directory);`,
|
|
1850
|
+
` if (parent === directory) break;`,
|
|
1851
|
+
` directory = parent;`,
|
|
1852
|
+
` }`,
|
|
1853
|
+
` }`,
|
|
1854
|
+
` function recordPackageManifests(file) {`,
|
|
1855
|
+
` for (let directory = path.dirname(path.resolve(file));;) {`,
|
|
1856
|
+
` const manifest = path.join(directory, "package.json");`,
|
|
1857
|
+
` recordInput(manifest);`,
|
|
1858
|
+
` if (existingFile(manifest)) return;`,
|
|
1859
|
+
` const parent = path.dirname(directory);`,
|
|
1860
|
+
` if (parent === directory) return;`,
|
|
1861
|
+
` directory = parent;`,
|
|
1862
|
+
` }`,
|
|
1863
|
+
` }`,
|
|
1864
|
+
` recordFile(request);`,
|
|
1865
|
+
` const moduleProbeExtensions = typeof globalThis.Bun === "object" ? [".tsx", ".jsx", ".ts", ".mjs", ".js", ".cjs", ".json"] : [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json", ".node"];`,
|
|
1866
|
+
` const jsToTsProbeExtensions = new Map([[".js", [".ts", ".tsx"]], [".jsx", [".tsx"]], [".mjs", [".mts"]], [".cjs", [".cts"]]]);`,
|
|
1867
|
+
` function sourceSubstitutionCandidates(base) {`,
|
|
1868
|
+
` const extension = path.extname(base).toLowerCase();`,
|
|
1869
|
+
` const substitutions = jsToTsProbeExtensions.get(extension);`,
|
|
1870
|
+
` if (substitutions === undefined) return [];`,
|
|
1871
|
+
` const stem = base.slice(0, base.length - extension.length);`,
|
|
1872
|
+
` return substitutions.map((candidate) => stem + candidate);`,
|
|
1873
|
+
` }`,
|
|
1874
|
+
` function moduleCandidates(base) {`,
|
|
1875
|
+
` return [base, ...sourceSubstitutionCandidates(base), ...moduleProbeExtensions.map((extension) => base + extension), path.join(base, "package.json"), ...moduleProbeExtensions.map((extension) => path.join(base, "index" + extension))];`,
|
|
1876
|
+
` }`,
|
|
1877
|
+
` const recordedModuleBases = new Set();`,
|
|
1878
|
+
` function recordManifestTargets(value, directory, allowBare = false) {`,
|
|
1879
|
+
` if (typeof value === "string") {`,
|
|
1880
|
+
` if (value !== "" && (allowBare || value.startsWith("./") || value.startsWith("../"))) recordModuleCandidates(path.resolve(directory, value));`,
|
|
1881
|
+
` return;`,
|
|
1882
|
+
` }`,
|
|
1883
|
+
` if (Array.isArray(value)) { for (const item of value) recordManifestTargets(item, directory, allowBare); return; }`,
|
|
1884
|
+
` if (value && typeof value === "object") for (const item of Object.values(value)) recordManifestTargets(item, directory, allowBare);`,
|
|
1885
|
+
` }`,
|
|
1886
|
+
` function recordModuleCandidates(base) {`,
|
|
1887
|
+
` const resolvedBase = path.resolve(base);`,
|
|
1888
|
+
` if (recordedModuleBases.has(resolvedBase)) return;`,
|
|
1889
|
+
` recordedModuleBases.add(resolvedBase);`,
|
|
1890
|
+
` for (const candidate of moduleCandidates(resolvedBase)) recordInput(candidate);`,
|
|
1891
|
+
` try {`,
|
|
1892
|
+
` const manifest = JSON.parse(fs.readFileSync(path.join(resolvedBase, "package.json"), "utf8").replace(/^\uFEFF/, ""));`,
|
|
1893
|
+
` recordManifestTargets(manifest.exports, resolvedBase);`,
|
|
1894
|
+
` recordManifestTargets(manifest.module, resolvedBase, true);`,
|
|
1895
|
+
` recordManifestTargets(manifest.main, resolvedBase, true);`,
|
|
1896
|
+
` } catch {}`,
|
|
1897
|
+
` }`,
|
|
1898
|
+
` function candidateSelected(base, selected) {`,
|
|
1899
|
+
` if (selected === undefined) return false;`,
|
|
1900
|
+
` for (const candidate of moduleCandidates(base)) {`,
|
|
1901
|
+
` try {`,
|
|
1902
|
+
` const canonical = fs.realpathSync.native(candidate);`,
|
|
1903
|
+
` const relative = path.relative(canonical, selected);`,
|
|
1904
|
+
` if (relative === "" || (fs.statSync(canonical).isDirectory() && relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative))) return true;`,
|
|
1905
|
+
` } catch {}`,
|
|
1906
|
+
` }`,
|
|
1907
|
+
` return false;`,
|
|
1908
|
+
` }`,
|
|
1909
|
+
` function localBases(specifier, parentFile) {`,
|
|
1910
|
+
` if (specifier.startsWith("file:")) return [fileURLToPath(specifier)];`,
|
|
1911
|
+
` const directory = path.dirname(parentFile);`,
|
|
1912
|
+
` const raw = path.resolve(directory, specifier);`,
|
|
1913
|
+
` const suffixStart = specifier.search(/[?#]/);`,
|
|
1914
|
+
` if (suffixStart === -1) return [raw];`,
|
|
1915
|
+
` const pathname = specifier.slice(0, suffixStart);`,
|
|
1916
|
+
` return pathname === "" ? [raw] : [...new Set([raw, path.resolve(directory, pathname)])];`,
|
|
1917
|
+
` }`,
|
|
1918
|
+
` function recordResolutionCandidates(specifier, parent, resolved) {`,
|
|
1919
|
+
` const parentFile = asFile(parent);`,
|
|
1920
|
+
` if (typeof specifier !== "string" || parentFile === undefined) return;`,
|
|
1921
|
+
` let selected;`,
|
|
1922
|
+
` try { const file = asFile(resolved); selected = file === undefined ? undefined : fs.realpathSync.native(file); } catch {}`,
|
|
1923
|
+
` if (specifier.startsWith(".") || path.isAbsolute(specifier) || specifier.startsWith("file:")) {`,
|
|
1924
|
+
` try {`,
|
|
1925
|
+
` for (const base of localBases(specifier, parentFile)) {`,
|
|
1926
|
+
` recordPackageManifests(base);`,
|
|
1927
|
+
` let exact = false;`,
|
|
1928
|
+
` try { exact = selected === undefined ? fs.statSync(base).isFile() : fs.realpathSync.native(base) === selected; } catch {}`,
|
|
1929
|
+
` if (exact) recordInput(base);`,
|
|
1930
|
+
` if (!exact) recordModuleCandidates(base);`,
|
|
1931
|
+
` }`,
|
|
1932
|
+
` } catch {}`,
|
|
1933
|
+
` return;`,
|
|
1934
|
+
` }`,
|
|
1935
|
+
` if (Module.isBuiltin(specifier) || specifier.startsWith("#")) return;`,
|
|
1936
|
+
` const parts = specifier.split("/");`,
|
|
1937
|
+
` const packageParts = parts[0].startsWith("@") ? parts.slice(0, 2) : parts.slice(0, 1);`,
|
|
1938
|
+
` if (packageParts.some((part) => part === undefined || part === "")) return;`,
|
|
1939
|
+
` const packageName = packageParts.join("/");`,
|
|
1940
|
+
` const subpath = parts.slice(packageParts.length);`,
|
|
1941
|
+
` const searchPaths = Module.createRequire(parentFile).resolve.paths(specifier) ?? [];`,
|
|
1942
|
+
` for (const searchPath of searchPaths) {`,
|
|
1943
|
+
` const packageDirectory = path.join(searchPath, packageName);`,
|
|
1944
|
+
` recordModuleCandidates(packageDirectory);`,
|
|
1945
|
+
` if (subpath.length !== 0) recordModuleCandidates(path.join(packageDirectory, ...subpath));`,
|
|
1946
|
+
` if (candidateSelected(packageDirectory, selected)) break;`,
|
|
1947
|
+
` }`,
|
|
1948
|
+
` }`,
|
|
1949
|
+
` function scanBunModuleGraph(entry) {`,
|
|
1950
|
+
` if (typeof globalThis.Bun !== "object" || typeof globalThis.Bun.Transpiler !== "function") return;`,
|
|
1951
|
+
` const scanned = new Set();`,
|
|
1952
|
+
` function scan(file) {`,
|
|
1953
|
+
` file = asFile(file);`,
|
|
1954
|
+
` if (file === undefined) return;`,
|
|
1955
|
+
` let canonical;`,
|
|
1956
|
+
` try { canonical = fs.realpathSync.native(file); } catch { return; }`,
|
|
1957
|
+
` if (scanned.has(canonical)) return;`,
|
|
1958
|
+
` scanned.add(canonical);`,
|
|
1959
|
+
` const extension = path.extname(canonical).toLowerCase();`,
|
|
1960
|
+
` if (![".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"].includes(extension)) return;`,
|
|
1961
|
+
` const loader = extension === ".tsx" ? "tsx" : extension === ".jsx" ? "jsx" : [".ts", ".mts", ".cts"].includes(extension) ? "ts" : "js";`,
|
|
1962
|
+
` let imports;`,
|
|
1963
|
+
` try {`,
|
|
1964
|
+
` const source = fs.readFileSync(canonical, "utf8");`,
|
|
1965
|
+
` imports = new globalThis.Bun.Transpiler({ loader }).scan(source).imports;`,
|
|
1966
|
+
` } catch { return; }`,
|
|
1967
|
+
` for (const imported of imports) {`,
|
|
1968
|
+
` const specifier = imported && imported.path;`,
|
|
1969
|
+
` if (typeof specifier !== "string" || specifier.startsWith("node:") || Module.isBuiltin(specifier)) continue;`,
|
|
1970
|
+
` recordResolutionCandidates(specifier, canonical, undefined);`,
|
|
1971
|
+
` try {`,
|
|
1972
|
+
` const resolved = globalThis.Bun.resolveSync(specifier, path.dirname(canonical));`,
|
|
1973
|
+
` recordResolutionCandidates(specifier, canonical, resolved);`,
|
|
1974
|
+
` recordFile(resolved);`,
|
|
1975
|
+
` scan(resolved);`,
|
|
1976
|
+
` } catch {`,
|
|
1977
|
+
` // The real descriptor load below owns user-facing resolution errors.`,
|
|
1978
|
+
` }`,
|
|
1979
|
+
` }`,
|
|
1980
|
+
` }`,
|
|
1981
|
+
` scan(entry);`,
|
|
1982
|
+
` }`,
|
|
1983
|
+
` if (typeof Module.registerHooks === "function") {`,
|
|
1984
|
+
` Module.registerHooks({`,
|
|
1985
|
+
` load(url, loadContext, nextLoad) {`,
|
|
1986
|
+
` try { return nextLoad(url, loadContext); }`,
|
|
1987
|
+
` catch (error) {`,
|
|
1988
|
+
` if (error && (typeof error === "object" || typeof error === "function")) moduleLoadFailures.set(error, { retryWithTtsx: error.code === "ERR_UNKNOWN_FILE_EXTENSION" });`,
|
|
1989
|
+
` throw error;`,
|
|
1990
|
+
` }`,
|
|
1991
|
+
` },`,
|
|
1992
|
+
` resolve(specifier, resolveContext, nextResolve) {`,
|
|
1993
|
+
` recordResolutionCandidates(specifier, resolveContext.parentURL, undefined);`,
|
|
1994
|
+
` let resolved;`,
|
|
1995
|
+
` try {`,
|
|
1996
|
+
` resolved = nextResolve(specifier, resolveContext);`,
|
|
1997
|
+
` } catch (error) {`,
|
|
1998
|
+
` if (error && (typeof error === "object" || typeof error === "function")) {`,
|
|
1999
|
+
` const parent = asFile(resolveContext.parentURL);`,
|
|
2000
|
+
` const anchor = typeof parent === "string" ? parent : request;`,
|
|
2001
|
+
` const candidate = specifier.startsWith(".") && path.extname(specifier) === "" ? path.resolve(path.dirname(anchor), specifier) : undefined;`,
|
|
2002
|
+
` const retryWithTtsx = candidate !== undefined && [".ts", ".cts", ".mts", ".tsx"].some((extension) => existingFile(candidate + extension) || existingFile(path.join(candidate, "index" + extension)));`,
|
|
2003
|
+
` moduleResolutionFailures.set(error, { retryWithTtsx });`,
|
|
2004
|
+
` }`,
|
|
2005
|
+
` throw error;`,
|
|
2006
|
+
` }`,
|
|
2007
|
+
` const url = typeof resolved === "string" ? resolved : resolved && resolved.url;`,
|
|
2008
|
+
` recordResolutionCandidates(specifier, resolveContext.parentURL, url);`,
|
|
2009
|
+
` recordFile(url);`,
|
|
2010
|
+
` return resolved;`,
|
|
2011
|
+
` },`,
|
|
2012
|
+
` });`,
|
|
2013
|
+
` }`,
|
|
2014
|
+
` if (typeof globalThis.Bun === "object" && typeof globalThis.Bun.plugin === "function") {`,
|
|
2015
|
+
` let insideBunResolve = false;`,
|
|
2016
|
+
` globalThis.Bun.plugin({`,
|
|
2017
|
+
` name: "ttsc-plugin-descriptor-inputs",`,
|
|
2018
|
+
` setup(build) {`,
|
|
2019
|
+
` build.onResolve({ filter: /.*/ }, (args) => {`,
|
|
2020
|
+
` if (insideBunResolve || args.path.startsWith("node:")) return;`,
|
|
2021
|
+
` try {`,
|
|
2022
|
+
` insideBunResolve = true;`,
|
|
2023
|
+
` const importer = args.importer && args.importer.startsWith("file:") ? fileURLToPath(args.importer) : args.importer;`,
|
|
2024
|
+
` const from = importer ? path.dirname(importer) : process.cwd();`,
|
|
2025
|
+
` recordResolutionCandidates(args.path, importer, undefined);`,
|
|
2026
|
+
` const resolved = globalThis.Bun.resolveSync(args.path, from);`,
|
|
2027
|
+
` recordResolutionCandidates(args.path, importer, resolved);`,
|
|
2028
|
+
` recordFile(resolved);`,
|
|
2029
|
+
` } catch {`,
|
|
2030
|
+
` // The real resolver below owns user-facing resolution errors.`,
|
|
2031
|
+
` } finally {`,
|
|
2032
|
+
` insideBunResolve = false;`,
|
|
2033
|
+
` }`,
|
|
2034
|
+
` });`,
|
|
2035
|
+
` },`,
|
|
2036
|
+
` });`,
|
|
2037
|
+
` }`,
|
|
2038
|
+
` scanBunModuleGraph(request);`,
|
|
2039
|
+
` let mod;`,
|
|
2040
|
+
` try {`,
|
|
2041
|
+
` mod = require(request);`,
|
|
2042
|
+
` } catch (error) {`,
|
|
2043
|
+
` retryWithTtsx = shouldRetryWithTtsx(error);`,
|
|
2044
|
+
` throw error;`,
|
|
2045
|
+
` }`,
|
|
2046
|
+
` const candidate = mod.createTtscPlugin ?? mod.default ?? mod.plugin ?? mod;`,
|
|
2047
|
+
` const descriptor = typeof candidate === "function" ? candidate(context) : candidate;`,
|
|
2048
|
+
` if (descriptor && typeof descriptor === "object" && ("transformSource" in descriptor || "transformOutput" in descriptor)) {`,
|
|
2049
|
+
` throw new Error("ttsc: plugin descriptor declares unsupported JS transform functions; declare a native backend instead");`,
|
|
2050
|
+
` }`,
|
|
2051
|
+
// Serialize the descriptor while the module-resolution hooks are still
|
|
2052
|
+
// collecting. Getters are allowed by JavaScript's object model and can
|
|
2053
|
+
// lazily require a config; snapshotting inputs first would silently omit
|
|
2054
|
+
// that influence.
|
|
2055
|
+
` const serializedDescriptor = JSON.stringify(descriptor);`,
|
|
2056
|
+
` for (const input of [...inputs]) recordInput(input);`,
|
|
2057
|
+
` const payload = { inputHashes: Object.fromEntries(inputHashes), inputRealpaths: Object.fromEntries(inputRealpaths), inputs: [...inputs].sort() };`,
|
|
2058
|
+
` if (serializedDescriptor !== undefined) payload.descriptor = JSON.parse(serializedDescriptor);`,
|
|
2059
|
+
` fs.writeFileSync(out, JSON.stringify(payload));`,
|
|
2060
|
+
`} catch (error) {`,
|
|
2061
|
+
` const message = error instanceof Error ? error.message : String(error);`,
|
|
2062
|
+
` const stack = error instanceof Error && error.stack ? error.stack : String(error);`,
|
|
2063
|
+
` try {`,
|
|
2064
|
+
` fs.writeFileSync(out, JSON.stringify({ __ttscLoaderError: message, __ttscRetryWithTtsx: retryWithTtsx }));`,
|
|
2065
|
+
` } catch {}`,
|
|
2066
|
+
` try { process.stderr.write(stack + "\\n"); } catch {}`,
|
|
2067
|
+
` process.exit(1);`,
|
|
2068
|
+
`}`,
|
|
2069
|
+
``,
|
|
2070
|
+
].join("\n");
|
|
2071
|
+
|
|
2072
|
+
/** Read the isolated loader's explicit retry classification. */
|
|
2073
|
+
function commonJsDescriptorRetryWithTtsx(file: string): boolean {
|
|
2074
|
+
try {
|
|
2075
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2076
|
+
return isRecord(parsed) && parsed.__ttscRetryWithTtsx === true;
|
|
2077
|
+
} catch {
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function commonJsDescriptorProcessFailure(
|
|
2083
|
+
result: {
|
|
2084
|
+
error?: Error;
|
|
2085
|
+
signal: NodeJS.Signals | null;
|
|
2086
|
+
status: number | null;
|
|
2087
|
+
},
|
|
2088
|
+
request: string,
|
|
2089
|
+
): Error | undefined {
|
|
2090
|
+
if (result.error !== undefined) {
|
|
2091
|
+
return new Error(
|
|
2092
|
+
`ttsc: failed to launch an isolated process for plugin descriptor "${request}": ${result.error.message}`,
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
if (result.signal !== null) {
|
|
2096
|
+
return new Error(
|
|
2097
|
+
`ttsc: plugin descriptor "${request}" isolated evaluation was killed by signal ${result.signal}.`,
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
if (result.status !== 0) {
|
|
2101
|
+
return new Error(
|
|
2102
|
+
`ttsc: plugin descriptor "${request}" isolated evaluation failed with exit code ${String(result.status)}`,
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
return undefined;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
694
2108
|
const TS_SOURCE_PATTERN = /\.(?:[cm]?ts|tsx)$/i;
|
|
695
2109
|
|
|
696
2110
|
/**
|
|
@@ -714,6 +2128,11 @@ export const PLUGIN_DESCRIPTOR_SHIM_SOURCE = [
|
|
|
714
2128
|
// both — "Cannot find module ./missing" is as actionable as anything the
|
|
715
2129
|
// factory could have said.
|
|
716
2130
|
`try {`,
|
|
2131
|
+
// Runtime hooks are installed before this shim loads. Arm their internal
|
|
2132
|
+
// side channel only for the descriptor import itself, after this shim's own
|
|
2133
|
+
// imports have resolved, so ttsc implementation files never become project
|
|
2134
|
+
// cache inputs.
|
|
2135
|
+
` process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE = "1";`,
|
|
717
2136
|
` const mod = await import(pathToFileURL(process.env.TTSC_PLUGIN_ENTRY).href);`,
|
|
718
2137
|
` const context = JSON.parse(process.env.TTSC_PLUGIN_CONTEXT);`,
|
|
719
2138
|
` const candidate = mod.createTtscPlugin ?? mod.default ?? mod.plugin ?? mod;`,
|
|
@@ -748,36 +2167,34 @@ function loadDescriptorViaTtsx(
|
|
|
748
2167
|
request: string,
|
|
749
2168
|
context: ITtscPluginFactoryContext,
|
|
750
2169
|
effectiveEnv: NodeJS.ProcessEnv,
|
|
751
|
-
):
|
|
2170
|
+
): IsolatedPluginDescriptor | undefined {
|
|
752
2171
|
// Binary discovery prefers the instance environment, then the ambient
|
|
753
2172
|
// process.env (where `withPluginLoaderEnv` injects ttsc's own node/ttsx paths
|
|
754
2173
|
// just before this runs), then the running interpreter.
|
|
755
|
-
const node =
|
|
756
|
-
effectiveEnv.TTSC_NODE_BINARY ??
|
|
757
|
-
process.env.TTSC_NODE_BINARY ??
|
|
758
|
-
process.execPath;
|
|
2174
|
+
const node = resolveNodeBinary(effectiveEnv, context.projectRoot);
|
|
759
2175
|
const ttsx = effectiveEnv.TTSC_TTSX_BINARY ?? process.env.TTSC_TTSX_BINARY;
|
|
760
|
-
if (ttsx === undefined || ttsx.length === 0) {
|
|
2176
|
+
if (node === undefined || ttsx === undefined || ttsx.length === 0) {
|
|
761
2177
|
return undefined;
|
|
762
2178
|
}
|
|
763
|
-
const dir =
|
|
2179
|
+
const dir = createEvaluationTempDir();
|
|
764
2180
|
const out = path.join(dir, "descriptor.json");
|
|
2181
|
+
const inputsOut = path.join(dir, "descriptor-inputs.ndjson");
|
|
765
2182
|
const shim = path.join(dir, "load-descriptor.mts");
|
|
766
2183
|
// ttsx type-checks and builds the shim's own project, so it needs a tsconfig
|
|
767
2184
|
// to anchor on; a minimal one is enough (the shim is `@ts-nocheck`).
|
|
768
|
-
fs.writeFileSync(
|
|
769
|
-
path.join(dir, "tsconfig.json"),
|
|
770
|
-
JSON.stringify({
|
|
771
|
-
compilerOptions: {
|
|
772
|
-
module: "nodenext",
|
|
773
|
-
moduleResolution: "nodenext",
|
|
774
|
-
skipLibCheck: true,
|
|
775
|
-
target: "es2022",
|
|
776
|
-
},
|
|
777
|
-
}),
|
|
778
|
-
);
|
|
779
|
-
fs.writeFileSync(shim, PLUGIN_DESCRIPTOR_SHIM_SOURCE);
|
|
780
2185
|
try {
|
|
2186
|
+
fs.writeFileSync(
|
|
2187
|
+
path.join(dir, "tsconfig.json"),
|
|
2188
|
+
JSON.stringify({
|
|
2189
|
+
compilerOptions: {
|
|
2190
|
+
module: "nodenext",
|
|
2191
|
+
moduleResolution: "nodenext",
|
|
2192
|
+
skipLibCheck: true,
|
|
2193
|
+
target: "es2022",
|
|
2194
|
+
},
|
|
2195
|
+
}),
|
|
2196
|
+
);
|
|
2197
|
+
fs.writeFileSync(shim, PLUGIN_DESCRIPTOR_SHIM_SOURCE);
|
|
781
2198
|
const result = childProcess.spawnSync(node, [ttsx, "--no-plugins", shim], {
|
|
782
2199
|
cwd: context.projectRoot,
|
|
783
2200
|
encoding: "utf8",
|
|
@@ -794,11 +2211,13 @@ function loadDescriptorViaTtsx(
|
|
|
794
2211
|
dirname: context.dirname,
|
|
795
2212
|
filename: context.filename,
|
|
796
2213
|
plugin: context.plugin,
|
|
2214
|
+
pluginConfigDir: context.pluginConfigDir,
|
|
797
2215
|
projectRoot: context.projectRoot,
|
|
798
2216
|
tsconfig: context.tsconfig,
|
|
799
2217
|
}),
|
|
800
2218
|
TTSC_PLUGIN_DESCRIPTOR_LOAD: "1",
|
|
801
2219
|
TTSC_PLUGIN_DESCRIPTOR_OUT: out,
|
|
2220
|
+
TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT: inputsOut,
|
|
802
2221
|
TTSC_PLUGIN_ENTRY: request,
|
|
803
2222
|
},
|
|
804
2223
|
// Both child streams are human output, and they go straight to this
|
|
@@ -826,7 +2245,16 @@ ${reason}`);
|
|
|
826
2245
|
}
|
|
827
2246
|
const text = fs.readFileSync(out, "utf8");
|
|
828
2247
|
try {
|
|
829
|
-
|
|
2248
|
+
const inputSnapshot = readTtsxDescriptorInputs(inputsOut, request);
|
|
2249
|
+
return {
|
|
2250
|
+
descriptor: JSON.parse(text),
|
|
2251
|
+
hostInputHashes: omitUnstableHostInputHashes(
|
|
2252
|
+
inputSnapshot.hostInputHashes,
|
|
2253
|
+
inputSnapshot.unstableInputs,
|
|
2254
|
+
),
|
|
2255
|
+
hostInputRealpaths: inputSnapshot.hostInputRealpaths,
|
|
2256
|
+
inputs: inputSnapshot.inputs,
|
|
2257
|
+
};
|
|
830
2258
|
} catch (error) {
|
|
831
2259
|
throw new Error(
|
|
832
2260
|
`ttsc: plugin descriptor "${request}" produced invalid JSON: ${errorMessage(error)}`,
|
|
@@ -837,6 +2265,140 @@ ${reason}`);
|
|
|
837
2265
|
}
|
|
838
2266
|
}
|
|
839
2267
|
|
|
2268
|
+
interface TtsxDescriptorResolutionRecord {
|
|
2269
|
+
hash?: string | null;
|
|
2270
|
+
parent?: string;
|
|
2271
|
+
realpath?: string | null;
|
|
2272
|
+
resolved?: string;
|
|
2273
|
+
signature?: string;
|
|
2274
|
+
specifier?: string;
|
|
2275
|
+
unstable?: boolean;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* Expand the ttsx runtime's selected module edges into the same exact and
|
|
2280
|
+
* missing resolution inputs used by the direct isolated evaluator.
|
|
2281
|
+
*/
|
|
2282
|
+
function readTtsxDescriptorInputs(
|
|
2283
|
+
file: string,
|
|
2284
|
+
request: string,
|
|
2285
|
+
): {
|
|
2286
|
+
hostInputHashes: Record<string, string | null>;
|
|
2287
|
+
hostInputRealpaths: Record<string, string | null>;
|
|
2288
|
+
inputs: string[];
|
|
2289
|
+
unstableInputs: string[];
|
|
2290
|
+
} {
|
|
2291
|
+
const inputs = new Set<string>([path.resolve(request)]);
|
|
2292
|
+
const hashes = new Map<string, string | null>();
|
|
2293
|
+
const realpaths = new Map<string, string | null>();
|
|
2294
|
+
const signatures = new Map<string, string>();
|
|
2295
|
+
const unstableInputs = new Set<string>();
|
|
2296
|
+
let text: string;
|
|
2297
|
+
try {
|
|
2298
|
+
text = fs.readFileSync(file, "utf8");
|
|
2299
|
+
} catch {
|
|
2300
|
+
return {
|
|
2301
|
+
hostInputHashes: {},
|
|
2302
|
+
hostInputRealpaths: {},
|
|
2303
|
+
inputs: [...inputs],
|
|
2304
|
+
unstableInputs: [],
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2308
|
+
if (line.trim() === "") continue;
|
|
2309
|
+
let record: TtsxDescriptorResolutionRecord;
|
|
2310
|
+
try {
|
|
2311
|
+
record = JSON.parse(line) as TtsxDescriptorResolutionRecord;
|
|
2312
|
+
} catch {
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
if (typeof record.resolved === "string") {
|
|
2316
|
+
const resolved = path.resolve(record.resolved);
|
|
2317
|
+
inputs.add(resolved);
|
|
2318
|
+
if (record.unstable === true) {
|
|
2319
|
+
hashes.delete(resolved);
|
|
2320
|
+
realpaths.delete(resolved);
|
|
2321
|
+
signatures.delete(resolved);
|
|
2322
|
+
unstableInputs.add(resolved);
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (
|
|
2326
|
+
typeof record.signature !== "string" ||
|
|
2327
|
+
(signatures.has(resolved) &&
|
|
2328
|
+
signatures.get(resolved) !== record.signature)
|
|
2329
|
+
) {
|
|
2330
|
+
hashes.delete(resolved);
|
|
2331
|
+
realpaths.delete(resolved);
|
|
2332
|
+
signatures.delete(resolved);
|
|
2333
|
+
unstableInputs.add(resolved);
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
signatures.set(resolved, record.signature);
|
|
2337
|
+
if (
|
|
2338
|
+
(typeof record.realpath === "string" &&
|
|
2339
|
+
path.isAbsolute(record.realpath)) ||
|
|
2340
|
+
record.realpath === null
|
|
2341
|
+
) {
|
|
2342
|
+
const observed =
|
|
2343
|
+
record.realpath === null ? null : path.resolve(record.realpath);
|
|
2344
|
+
if (realpaths.has(resolved) && realpaths.get(resolved) !== observed) {
|
|
2345
|
+
hashes.delete(resolved);
|
|
2346
|
+
realpaths.delete(resolved);
|
|
2347
|
+
signatures.delete(resolved);
|
|
2348
|
+
unstableInputs.add(resolved);
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
realpaths.set(resolved, observed);
|
|
2352
|
+
}
|
|
2353
|
+
if (typeof record.hash === "string" || record.hash === null) {
|
|
2354
|
+
if (unstableInputs.has(resolved)) {
|
|
2355
|
+
// Keep a previously observed contradiction unstable.
|
|
2356
|
+
} else if (
|
|
2357
|
+
hashes.has(resolved) &&
|
|
2358
|
+
hashes.get(resolved) !== record.hash
|
|
2359
|
+
) {
|
|
2360
|
+
hashes.delete(resolved);
|
|
2361
|
+
realpaths.delete(resolved);
|
|
2362
|
+
signatures.delete(resolved);
|
|
2363
|
+
unstableInputs.add(resolved);
|
|
2364
|
+
} else {
|
|
2365
|
+
hashes.set(resolved, record.hash);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
if (
|
|
2370
|
+
typeof record.specifier === "string" &&
|
|
2371
|
+
typeof record.parent === "string"
|
|
2372
|
+
) {
|
|
2373
|
+
for (const candidate of collectModuleResolutionCandidates(
|
|
2374
|
+
record.specifier,
|
|
2375
|
+
record.parent,
|
|
2376
|
+
record.resolved,
|
|
2377
|
+
)) {
|
|
2378
|
+
inputs.add(path.resolve(candidate));
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
for (const [resolved, observed] of realpaths) {
|
|
2383
|
+
if (
|
|
2384
|
+
realpathHostInput(resolved) === observed &&
|
|
2385
|
+
signatures.get(resolved) === hostInputMetadataSignature(resolved)
|
|
2386
|
+
) {
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
hashes.delete(resolved);
|
|
2390
|
+
realpaths.delete(resolved);
|
|
2391
|
+
signatures.delete(resolved);
|
|
2392
|
+
unstableInputs.add(resolved);
|
|
2393
|
+
}
|
|
2394
|
+
return {
|
|
2395
|
+
hostInputHashes: Object.fromEntries(hashes),
|
|
2396
|
+
hostInputRealpaths: Object.fromEntries(realpaths),
|
|
2397
|
+
inputs: [...inputs].sort(),
|
|
2398
|
+
unstableInputs: [...unstableInputs],
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
|
|
840
2402
|
function errorMessage(error: unknown): string {
|
|
841
2403
|
return error instanceof Error ? error.message : String(error);
|
|
842
2404
|
}
|
|
@@ -844,7 +2406,10 @@ function errorMessage(error: unknown): string {
|
|
|
844
2406
|
function withPluginLoaderEnv<T>(run: () => T): T {
|
|
845
2407
|
const previousNode = process.env.TTSC_NODE_BINARY;
|
|
846
2408
|
const previousTtsx = process.env.TTSC_TTSX_BINARY;
|
|
847
|
-
|
|
2409
|
+
const node = resolveNodeBinary({}, process.cwd());
|
|
2410
|
+
if (process.env.TTSC_NODE_BINARY === undefined && node !== undefined) {
|
|
2411
|
+
process.env.TTSC_NODE_BINARY = node;
|
|
2412
|
+
}
|
|
848
2413
|
process.env.TTSC_TTSX_BINARY ??= path.join(
|
|
849
2414
|
__dirname,
|
|
850
2415
|
"..",
|
|
@@ -860,6 +2425,47 @@ function withPluginLoaderEnv<T>(run: () => T): T {
|
|
|
860
2425
|
}
|
|
861
2426
|
}
|
|
862
2427
|
|
|
2428
|
+
/**
|
|
2429
|
+
* Select the executable for isolated descriptor evaluation.
|
|
2430
|
+
*
|
|
2431
|
+
* Under Bun, `process.execPath` names Bun itself. Bun implements enough of the
|
|
2432
|
+
* Node module surface to evaluate descriptors natively, but not Node's
|
|
2433
|
+
* synchronous `module.registerHooks` preload. The caller therefore recognizes
|
|
2434
|
+
* this default and omits the Node-only preload while retaining the fresh
|
|
2435
|
+
* process boundary. An explicitly configured Node executable remains
|
|
2436
|
+
* authoritative and receives the preload as usual.
|
|
2437
|
+
*/
|
|
2438
|
+
function pluginDescriptorRuntimeBinary(env: NodeJS.ProcessEnv): string {
|
|
2439
|
+
if (
|
|
2440
|
+
env.TTSC_NODE_BINARY === undefined &&
|
|
2441
|
+
typeof (process.versions as Record<string, string | undefined>).bun ===
|
|
2442
|
+
"string"
|
|
2443
|
+
) {
|
|
2444
|
+
return process.execPath;
|
|
2445
|
+
}
|
|
2446
|
+
return (
|
|
2447
|
+
env.TTSC_NODE_BINARY ?? process.env.TTSC_NODE_BINARY ?? process.execPath
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
/** Replay a child's human output without imposing a fixed output ceiling. */
|
|
2452
|
+
function replayEvaluationDiagnostics(file: string): void {
|
|
2453
|
+
let fd: number | undefined;
|
|
2454
|
+
try {
|
|
2455
|
+
fd = fs.openSync(file, "r");
|
|
2456
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
2457
|
+
for (;;) {
|
|
2458
|
+
const length = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
2459
|
+
if (length === 0) break;
|
|
2460
|
+
fs.writeSync(2, buffer, 0, length);
|
|
2461
|
+
}
|
|
2462
|
+
} catch {
|
|
2463
|
+
// Diagnostic replay must never replace the descriptor result.
|
|
2464
|
+
} finally {
|
|
2465
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
|
|
863
2469
|
function restoreEnv(
|
|
864
2470
|
key: "TTSC_NODE_BINARY" | "TTSC_TTSX_BINARY",
|
|
865
2471
|
value: string | undefined,
|
|
@@ -1181,7 +2787,7 @@ function resolvePluginExportCondition(
|
|
|
1181
2787
|
return null;
|
|
1182
2788
|
}
|
|
1183
2789
|
const file = path.resolve(path.dirname(packageJson), resolved);
|
|
1184
|
-
return
|
|
2790
|
+
return existingFile(file) ? resolveRealPath(file) : null;
|
|
1185
2791
|
}
|
|
1186
2792
|
|
|
1187
2793
|
/**
|
|
@@ -1413,6 +3019,14 @@ function readTsgoVersion(projectRoot: string): string {
|
|
|
1413
3019
|
}
|
|
1414
3020
|
}
|
|
1415
3021
|
|
|
3022
|
+
/**
|
|
3023
|
+
* Create an evaluator directory whose cleanup path cannot follow a retargeted
|
|
3024
|
+
* parent alias.
|
|
3025
|
+
*/
|
|
3026
|
+
function createEvaluationTempDir(): string {
|
|
3027
|
+
return createCanonicalTempDirectory("ttsc-plugin-descriptor-");
|
|
3028
|
+
}
|
|
3029
|
+
|
|
1416
3030
|
/**
|
|
1417
3031
|
* Remove an evaluation temp directory without letting cleanup replace a result.
|
|
1418
3032
|
*
|