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
package/src/index.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface CanonicalTempDirectoryOperations {
|
|
6
|
+
lstat(location: string): { isDirectory(): boolean };
|
|
7
|
+
mkdtemp(prefix: string): string;
|
|
8
|
+
realpath(location: string): string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const FILESYSTEM_OPERATIONS: CanonicalTempDirectoryOperations = {
|
|
12
|
+
lstat: fs.lstatSync,
|
|
13
|
+
mkdtemp: fs.mkdtempSync,
|
|
14
|
+
realpath: fs.realpathSync.native,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a unique temporary directory under a frozen physical parent.
|
|
19
|
+
*
|
|
20
|
+
* The parent is resolved before creation so retargeting the caller's
|
|
21
|
+
* TEMP/TMPDIR alias cannot redirect later writes or recursive cleanup. The
|
|
22
|
+
* postflight accepts only a real directory that resolves to a direct child of
|
|
23
|
+
* that same parent; a rejected child is deliberately left untouched because no
|
|
24
|
+
* safe ownership identity was established.
|
|
25
|
+
*/
|
|
26
|
+
export function createCanonicalTempDirectory(
|
|
27
|
+
prefix: string,
|
|
28
|
+
parent: string = os.tmpdir(),
|
|
29
|
+
operations: CanonicalTempDirectoryOperations = FILESYSTEM_OPERATIONS,
|
|
30
|
+
): string {
|
|
31
|
+
if (prefix.length === 0 || path.basename(prefix) !== prefix) {
|
|
32
|
+
throw new TypeError("ttsc: temporary directory prefix must be a basename");
|
|
33
|
+
}
|
|
34
|
+
const physicalParent = operations.realpath(path.resolve(parent));
|
|
35
|
+
if (!operations.lstat(physicalParent).isDirectory()) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`ttsc: temporary directory parent is not a directory: ${physicalParent}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const directory = operations.mkdtemp(path.join(physicalParent, prefix));
|
|
41
|
+
if (!operations.lstat(directory).isDirectory()) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`ttsc: temporary directory postflight is not a directory: ${directory}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const physicalDirectory = operations.realpath(directory);
|
|
47
|
+
if (path.dirname(physicalDirectory) !== physicalParent) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`ttsc: temporary directory escaped its physical parent: ${physicalDirectory}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return physicalDirectory;
|
|
53
|
+
}
|
|
@@ -11,7 +11,9 @@ export type FilesystemPathIdentity = {
|
|
|
11
11
|
|
|
12
12
|
export type FilesystemPathIdentityOperations = {
|
|
13
13
|
caseSensitive(directory: string): boolean;
|
|
14
|
+
lstat?(location: string): fs.Stats | fs.BigIntStats;
|
|
14
15
|
platform: NodeJS.Platform;
|
|
16
|
+
readdir?(directory: string): string[];
|
|
15
17
|
realpath(location: string): string;
|
|
16
18
|
throwOnRealpathError: boolean;
|
|
17
19
|
};
|
|
@@ -53,10 +55,16 @@ export function createFilesystemPathIdentityContext(
|
|
|
53
55
|
const pathApi = platform === "win32" ? path.win32 : path;
|
|
54
56
|
const throwOnRealpathError = operations.throwOnRealpathError ?? true;
|
|
55
57
|
const realpath = operations.realpath ?? physicalRealpath;
|
|
58
|
+
const lstat = operations.lstat ?? fs.lstatSync;
|
|
59
|
+
const readdir = operations.readdir ?? fs.readdirSync;
|
|
56
60
|
const caseSensitive =
|
|
57
61
|
operations.caseSensitive ??
|
|
58
62
|
((directory: string) =>
|
|
59
|
-
filesystemDirectoryIsCaseSensitive(directory, platform
|
|
63
|
+
filesystemDirectoryIsCaseSensitive(directory, platform, {
|
|
64
|
+
lstat,
|
|
65
|
+
readdir,
|
|
66
|
+
realpath,
|
|
67
|
+
}));
|
|
60
68
|
|
|
61
69
|
const resolve = (location: string): FilesystemPathIdentity => {
|
|
62
70
|
const normalized = resolveFilesystemPath(location, platform);
|
|
@@ -210,10 +218,15 @@ function physicalRealpath(location: string): string {
|
|
|
210
218
|
function filesystemDirectoryIsCaseSensitive(
|
|
211
219
|
directory: string,
|
|
212
220
|
platform: NodeJS.Platform,
|
|
221
|
+
operations: {
|
|
222
|
+
lstat(location: string): fs.Stats | fs.BigIntStats;
|
|
223
|
+
readdir(directory: string): string[];
|
|
224
|
+
realpath(location: string): string;
|
|
225
|
+
},
|
|
213
226
|
): boolean {
|
|
214
227
|
let entries: string[];
|
|
215
228
|
try {
|
|
216
|
-
entries =
|
|
229
|
+
entries = operations.readdir(directory);
|
|
217
230
|
} catch {
|
|
218
231
|
return unprovenCaseSensitivity(platform);
|
|
219
232
|
}
|
|
@@ -229,7 +242,7 @@ function filesystemDirectoryIsCaseSensitive(
|
|
|
229
242
|
const alternate = alternateCase(name);
|
|
230
243
|
if (alternate === name) continue;
|
|
231
244
|
try {
|
|
232
|
-
|
|
245
|
+
operations.lstat(path.join(directory, alternate));
|
|
233
246
|
return false;
|
|
234
247
|
} catch (error) {
|
|
235
248
|
if (isMissingFilesystemEntry(error)) {
|
|
@@ -254,7 +267,7 @@ function filesystemDirectoryIsCaseSensitive(
|
|
|
254
267
|
const alternate = alternateCase(name);
|
|
255
268
|
if (alternate !== name) {
|
|
256
269
|
try {
|
|
257
|
-
|
|
270
|
+
operations.realpath(path.join(parent, alternate));
|
|
258
271
|
return false;
|
|
259
272
|
} catch (error) {
|
|
260
273
|
if (isMissingFilesystemEntry(error) === false) throw error;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export type JavaScriptRuntimeCapabilities = {
|
|
6
|
+
bun: boolean;
|
|
7
|
+
executable?: string;
|
|
8
|
+
registerHooks: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type RuntimeCapabilityCacheEntry = {
|
|
12
|
+
capabilities: JavaScriptRuntimeCapabilities;
|
|
13
|
+
identity: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const runtimeCapabilityCache = new Map<string, RuntimeCapabilityCacheEntry>();
|
|
17
|
+
|
|
18
|
+
/** Probe an interpreter instead of inferring its identity from the host. */
|
|
19
|
+
export function javascriptRuntimeCapabilities(
|
|
20
|
+
runtime: string,
|
|
21
|
+
env: NodeJS.ProcessEnv,
|
|
22
|
+
cwd: string,
|
|
23
|
+
): JavaScriptRuntimeCapabilities {
|
|
24
|
+
const effectiveEnv = { ...process.env, ...env };
|
|
25
|
+
const cacheKey = runtimeCapabilityCacheKey(runtime, effectiveEnv);
|
|
26
|
+
const beforeIdentity = runtimeExecutableIdentity(runtime);
|
|
27
|
+
if (cacheKey !== undefined && beforeIdentity !== undefined) {
|
|
28
|
+
const cached = runtimeCapabilityCache.get(cacheKey);
|
|
29
|
+
if (cached?.identity === beforeIdentity) return { ...cached.capabilities };
|
|
30
|
+
runtimeCapabilityCache.delete(cacheKey);
|
|
31
|
+
}
|
|
32
|
+
const result = childProcess.spawnSync(
|
|
33
|
+
runtime,
|
|
34
|
+
[
|
|
35
|
+
"-e",
|
|
36
|
+
`const Module = require("node:module"); process.stdout.write(JSON.stringify({ bun: typeof globalThis.Bun === "object", executable: process.execPath, registerHooks: typeof Module.registerHooks === "function" }));`,
|
|
37
|
+
],
|
|
38
|
+
{
|
|
39
|
+
cwd,
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
env: effectiveEnv,
|
|
42
|
+
timeout: 30_000,
|
|
43
|
+
windowsHide: true,
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
let capabilities: JavaScriptRuntimeCapabilities = {
|
|
47
|
+
bun: false,
|
|
48
|
+
registerHooks: false,
|
|
49
|
+
};
|
|
50
|
+
if (result.status === 0) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(
|
|
53
|
+
result.stdout,
|
|
54
|
+
) as Partial<JavaScriptRuntimeCapabilities>;
|
|
55
|
+
capabilities = {
|
|
56
|
+
bun: parsed.bun === true,
|
|
57
|
+
...(typeof parsed.executable === "string" &&
|
|
58
|
+
path.isAbsolute(parsed.executable)
|
|
59
|
+
? { executable: path.resolve(parsed.executable) }
|
|
60
|
+
: {}),
|
|
61
|
+
registerHooks: parsed.registerHooks === true,
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
// An incompatible executable is not a Node runtime candidate.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Cache successful absolute candidates only when the child reports that same
|
|
68
|
+
// executable and both its lexical link and physical target retain the same
|
|
69
|
+
// filesystem identity. Relative/bare commands and wrappers can resolve
|
|
70
|
+
// differently by cwd/PATH/environment, negative probes can become valid later,
|
|
71
|
+
// and NODE_OPTIONS can load mutable user code, so none of those states are
|
|
72
|
+
// memoized. This removes a process spawn from the common path without
|
|
73
|
+
// authorizing a replaced or redirected runtime in a long-lived host.
|
|
74
|
+
const afterIdentity = runtimeExecutableIdentity(runtime);
|
|
75
|
+
if (
|
|
76
|
+
cacheKey !== undefined &&
|
|
77
|
+
capabilities.executable !== undefined &&
|
|
78
|
+
beforeIdentity !== undefined &&
|
|
79
|
+
beforeIdentity === afterIdentity &&
|
|
80
|
+
sameRuntimeExecutable(runtime, capabilities.executable)
|
|
81
|
+
) {
|
|
82
|
+
runtimeCapabilityCache.set(cacheKey, {
|
|
83
|
+
capabilities: { ...capabilities },
|
|
84
|
+
identity: afterIdentity,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return capabilities;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** True when a probe ran the candidate itself rather than a mutable wrapper. */
|
|
91
|
+
function sameRuntimeExecutable(candidate: string, executable: string): boolean {
|
|
92
|
+
try {
|
|
93
|
+
const candidateStats = fs.statSync(candidate, { bigint: true });
|
|
94
|
+
const executableStats = fs.statSync(executable, { bigint: true });
|
|
95
|
+
return (
|
|
96
|
+
(candidateStats.ino !== 0n &&
|
|
97
|
+
candidateStats.dev === executableStats.dev &&
|
|
98
|
+
candidateStats.ino === executableStats.ino) ||
|
|
99
|
+
fs.realpathSync.native(candidate) === fs.realpathSync.native(executable)
|
|
100
|
+
);
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Stable cache key only for an absolute runtime with no preload authority. */
|
|
107
|
+
function runtimeCapabilityCacheKey(
|
|
108
|
+
runtime: string,
|
|
109
|
+
env: NodeJS.ProcessEnv,
|
|
110
|
+
): string | undefined {
|
|
111
|
+
if (!path.isAbsolute(runtime) || env.NODE_OPTIONS?.trim()) return undefined;
|
|
112
|
+
return path.resolve(runtime);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Identity of an executable spelling and the physical file it selects. */
|
|
116
|
+
function runtimeExecutableIdentity(runtime: string): string | undefined {
|
|
117
|
+
if (!path.isAbsolute(runtime)) return undefined;
|
|
118
|
+
try {
|
|
119
|
+
const lexical = fs.lstatSync(runtime, { bigint: true });
|
|
120
|
+
const physicalPath = fs.realpathSync.native(runtime);
|
|
121
|
+
const physical = fs.statSync(physicalPath, { bigint: true });
|
|
122
|
+
return [
|
|
123
|
+
physicalPath,
|
|
124
|
+
lexical.dev,
|
|
125
|
+
lexical.ino,
|
|
126
|
+
lexical.mode,
|
|
127
|
+
lexical.size,
|
|
128
|
+
lexical.mtimeNs,
|
|
129
|
+
lexical.ctimeNs,
|
|
130
|
+
physical.dev,
|
|
131
|
+
physical.ino,
|
|
132
|
+
physical.mode,
|
|
133
|
+
physical.size,
|
|
134
|
+
physical.mtimeNs,
|
|
135
|
+
physical.ctimeNs,
|
|
136
|
+
].join("\0");
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Locate a real Node runtime for ttsx and native JavaScript config loaders.
|
|
144
|
+
*
|
|
145
|
+
* Bun can directly evaluate a descriptor, but it does not implement the
|
|
146
|
+
* synchronous `module.registerHooks` contract used by those loaders.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveNodeBinary(
|
|
149
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
150
|
+
cwd: string = process.cwd(),
|
|
151
|
+
): string | undefined {
|
|
152
|
+
const candidates = [
|
|
153
|
+
env.TTSC_NODE_BINARY,
|
|
154
|
+
process.env.TTSC_NODE_BINARY,
|
|
155
|
+
process.execPath,
|
|
156
|
+
"node",
|
|
157
|
+
];
|
|
158
|
+
const seen = new Set<string>();
|
|
159
|
+
for (const candidate of candidates) {
|
|
160
|
+
if (candidate === undefined || candidate.trim() === "") continue;
|
|
161
|
+
if (seen.has(candidate)) continue;
|
|
162
|
+
seen.add(candidate);
|
|
163
|
+
const capabilities = javascriptRuntimeCapabilities(candidate, env, cwd);
|
|
164
|
+
if (
|
|
165
|
+
!capabilities.bun &&
|
|
166
|
+
capabilities.registerHooks &&
|
|
167
|
+
capabilities.executable !== undefined
|
|
168
|
+
) {
|
|
169
|
+
return capabilities.executable;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type FilesystemPathIdentityOperations,
|
|
6
|
+
createFilesystemPathIdentityContext,
|
|
7
|
+
isFilesystemPathIdentityWithin,
|
|
8
|
+
resolveFilesystemPath,
|
|
9
|
+
} from "./projectInputPathIdentity";
|
|
10
|
+
|
|
11
|
+
export interface SafeCacheCleanupTarget {
|
|
12
|
+
exists: boolean;
|
|
13
|
+
path: string;
|
|
14
|
+
requestedPath: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve one cache-clean transaction to physical deletion targets.
|
|
19
|
+
*
|
|
20
|
+
* Every target is proved before the caller removes the first one. Existing
|
|
21
|
+
* aliases are pinned to their physical spelling, while a missing suffix keeps
|
|
22
|
+
* the identity of its nearest existing parent. Identity errors other than a
|
|
23
|
+
* genuinely missing path fail closed.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveSafeCacheCleanupTargets(
|
|
26
|
+
projectRoot: string,
|
|
27
|
+
cacheDirectories: readonly string[],
|
|
28
|
+
operations: Partial<FilesystemPathIdentityOperations> = {},
|
|
29
|
+
): SafeCacheCleanupTarget[] {
|
|
30
|
+
const identities = createFilesystemPathIdentityContext(operations);
|
|
31
|
+
const lstat = operations.lstat ?? fs.lstatSync;
|
|
32
|
+
const project = identities.resolve(projectRoot);
|
|
33
|
+
return cacheDirectories.map((cacheDirectory) => {
|
|
34
|
+
const requestedCache = resolveFilesystemPath(cacheDirectory);
|
|
35
|
+
// Fix every mutable alias ancestor before inspecting the terminal entry.
|
|
36
|
+
// All later identity checks and deletion paths use this physical-parent
|
|
37
|
+
// candidate, so a concurrent retarget cannot move ownership mid-proof.
|
|
38
|
+
const pinnedCache = path.join(
|
|
39
|
+
identities.resolve(path.dirname(requestedCache)).path,
|
|
40
|
+
path.basename(requestedCache),
|
|
41
|
+
);
|
|
42
|
+
const cache = identities.resolve(pinnedCache);
|
|
43
|
+
if (
|
|
44
|
+
requestedCache === path.parse(requestedCache).root ||
|
|
45
|
+
cache.path === path.parse(cache.path).root
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`ttsc: refusing to clean cache directory ${JSON.stringify(requestedCache)} because filesystem roots are never valid cache directories`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (isFilesystemPathIdentityWithin(cache.key, project.key)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`ttsc: refusing to clean cache directory ${JSON.stringify(requestedCache)} because it equals or contains project root ${JSON.stringify(project.path)}; choose a dedicated cache directory`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const status = lstatIfPresent(pinnedCache, lstat);
|
|
57
|
+
// Recursive rm removes a terminal symlink or junction itself rather than
|
|
58
|
+
// following it. Preserve that behavior while pinning any mutable alias in
|
|
59
|
+
// its ancestors to the physical parent selected by this transaction.
|
|
60
|
+
const deletionPath = status?.isSymbolicLink() ? pinnedCache : cache.path;
|
|
61
|
+
return {
|
|
62
|
+
exists: status !== undefined,
|
|
63
|
+
path: deletionPath,
|
|
64
|
+
requestedPath: requestedCache,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function lstatIfPresent(
|
|
70
|
+
location: string,
|
|
71
|
+
lstat: NonNullable<FilesystemPathIdentityOperations["lstat"]>,
|
|
72
|
+
): fs.Stats | fs.BigIntStats | undefined {
|
|
73
|
+
try {
|
|
74
|
+
return lstat(location);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (
|
|
77
|
+
error instanceof Error &&
|
|
78
|
+
"code" in error &&
|
|
79
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
80
|
+
) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -10,8 +10,6 @@ import { createFilesystemPathIdentityContext } from "../../internal/projectInput
|
|
|
10
10
|
import type { TtscCommonOptions } from "../../structures/internal/TtscCommonOptions";
|
|
11
11
|
import { type OwningModuleOptions, projectModuleOptions } from "./runtimeHooks";
|
|
12
12
|
|
|
13
|
-
/** Subdirectory name that isolates concurrent ttsx processes by PID. */
|
|
14
|
-
const PROCESS_CACHE_KEY = String(process.pid);
|
|
15
13
|
/**
|
|
16
14
|
* Maximum number of ancestor directories above the project root that the
|
|
17
15
|
* virtual filesystem overlay mirrors. Three levels covers the common monorepo
|
|
@@ -32,12 +30,15 @@ export function prepareExecution(
|
|
|
32
30
|
options: TtscCommonOptions & {
|
|
33
31
|
cacheDir?: string;
|
|
34
32
|
project?: string;
|
|
33
|
+
/** Internal cache key for more than one checked entry in this process. */
|
|
34
|
+
runtimeCacheKey?: string;
|
|
35
35
|
} = {},
|
|
36
36
|
): {
|
|
37
37
|
cleanupDir: string;
|
|
38
38
|
emitDir: string;
|
|
39
39
|
emittedFiles?: readonly string[];
|
|
40
40
|
entryFile: string;
|
|
41
|
+
entrySource: string;
|
|
41
42
|
moduleOptions: OwningModuleOptions;
|
|
42
43
|
projectRoot: string;
|
|
43
44
|
rootDir: string;
|
|
@@ -74,6 +75,7 @@ export function prepareExecution(
|
|
|
74
75
|
emitDir: context.emitDir,
|
|
75
76
|
emittedFiles: context.emittedFiles ?? undefined,
|
|
76
77
|
entryFile: emittedEntry,
|
|
78
|
+
entrySource: entry,
|
|
77
79
|
moduleOptions: context.moduleOptions,
|
|
78
80
|
projectRoot: context.root,
|
|
79
81
|
rootDir: context.runtimeRootDir,
|
|
@@ -147,6 +149,19 @@ function resolveEntrySpelling(cwd: string, entryFile: string): string {
|
|
|
147
149
|
return identities.resolve(path.resolve(cwd, entryFile)).path;
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Directory-safe identity for one prepared runtime. Direct `ttsx` retains its
|
|
154
|
+
* historical PID directory; the public preload supplies a distinct key for
|
|
155
|
+
* every late TypeScript root so one preparation cannot erase another's emit.
|
|
156
|
+
*/
|
|
157
|
+
function resolveRuntimeCacheKey(runtimeCacheKey: string | undefined): string {
|
|
158
|
+
const key = runtimeCacheKey ?? String(process.pid);
|
|
159
|
+
if (!/^[A-Za-z0-9._-]+$/.test(key) || key === "." || key === "..") {
|
|
160
|
+
throw new Error(`ttsx: invalid runtime cache key ${JSON.stringify(key)}`);
|
|
161
|
+
}
|
|
162
|
+
return key;
|
|
163
|
+
}
|
|
164
|
+
|
|
150
165
|
/**
|
|
151
166
|
* @param discoveryFile - The entry as the user named it. Project discovery
|
|
152
167
|
* walks up from here, so it must not be retargeted through a symlink.
|
|
@@ -168,21 +183,30 @@ function createProjectContext(
|
|
|
168
183
|
const tsconfig = project.path;
|
|
169
184
|
const root = project.root;
|
|
170
185
|
const explicitCacheDir = resolveCacheDir(cwd, options.cacheDir);
|
|
171
|
-
const
|
|
186
|
+
const cacheDirSpelling =
|
|
172
187
|
explicitCacheDir ??
|
|
173
188
|
path.join(root, "node_modules", ".cache", "ttsc", "ttsx");
|
|
174
|
-
const
|
|
175
|
-
const virtualRoot = path.join(processDir, "fs");
|
|
189
|
+
const runtimeCacheKey = resolveRuntimeCacheKey(options.runtimeCacheKey);
|
|
176
190
|
// Resolved once: it now costs a realpath (and, for a missing directory on
|
|
177
191
|
// Windows, a case-sensitivity probe) rather than a string join.
|
|
178
192
|
const runtimeRootDir = resolveRuntimeSourceRoot(project);
|
|
193
|
+
fs.mkdirSync(cacheDirSpelling, { recursive: true });
|
|
194
|
+
// Pin the cache parent before deriving a generation path. Descriptors run
|
|
195
|
+
// after this point and may retarget a caller-controlled symlink or junction;
|
|
196
|
+
// every later runtime write and recursive cleanup must remain below the
|
|
197
|
+
// physical parent selected here.
|
|
198
|
+
const cacheDir =
|
|
199
|
+
createFilesystemPathIdentityContext().resolve(cacheDirSpelling).path;
|
|
200
|
+
const processDir = path.join(cacheDir, "project", runtimeCacheKey);
|
|
201
|
+
const virtualRoot = path.join(processDir, "fs");
|
|
179
202
|
return {
|
|
180
203
|
project,
|
|
181
204
|
tsconfig,
|
|
182
205
|
root,
|
|
183
206
|
cacheDir,
|
|
207
|
+
runtimeCacheKey,
|
|
184
208
|
processDir,
|
|
185
|
-
pluginCacheDir: explicitCacheDir,
|
|
209
|
+
pluginCacheDir: explicitCacheDir === undefined ? undefined : cacheDir,
|
|
186
210
|
virtualRoot,
|
|
187
211
|
emitDir: project.compilerOptions.outDir
|
|
188
212
|
? virtualPath(virtualRoot, project.compilerOptions.outDir)
|
|
@@ -361,7 +385,7 @@ function buildEntryProject(
|
|
|
361
385
|
const rootDir = commonAncestorDirectory(path.dirname(entry), context.root);
|
|
362
386
|
const tsconfig = path.join(
|
|
363
387
|
context.root,
|
|
364
|
-
`.ttsx-entry.${
|
|
388
|
+
`.ttsx-entry.${context.runtimeCacheKey}.tsconfig.json`,
|
|
365
389
|
);
|
|
366
390
|
fs.writeFileSync(
|
|
367
391
|
tsconfig,
|
|
@@ -430,8 +454,8 @@ function buildEntryProject(
|
|
|
430
454
|
fs.rmSync(tsconfig, { force: true });
|
|
431
455
|
} catch {
|
|
432
456
|
// Best effort: a leftover synthesized tsconfig must not mask a build
|
|
433
|
-
// failure, and it is
|
|
434
|
-
// project config.
|
|
457
|
+
// failure, and it is runtime-scoped so it can never be mistaken for a
|
|
458
|
+
// real project config.
|
|
435
459
|
}
|
|
436
460
|
}
|
|
437
461
|
}
|
|
@@ -575,6 +599,9 @@ function isDirectorySymlinkTarget(realEntry: string): boolean {
|
|
|
575
599
|
*/
|
|
576
600
|
function collectLinkDirectories(projectRoot: string): string[] {
|
|
577
601
|
const out: string[] = [];
|
|
602
|
+
const identities = createFilesystemPathIdentityContext({
|
|
603
|
+
throwOnRealpathError: false,
|
|
604
|
+
});
|
|
578
605
|
let current = projectRoot;
|
|
579
606
|
for (let depth = 0; depth <= MAX_VIRTUAL_PARENT_DEPTH; depth += 1) {
|
|
580
607
|
out.push(current);
|
|
@@ -586,7 +613,7 @@ function collectLinkDirectories(projectRoot: string): string[] {
|
|
|
586
613
|
break;
|
|
587
614
|
}
|
|
588
615
|
const parent = path.dirname(current);
|
|
589
|
-
if (parent === current || isUnsafeVirtualParent(parent)) {
|
|
616
|
+
if (parent === current || isUnsafeVirtualParent(parent, identities)) {
|
|
590
617
|
break;
|
|
591
618
|
}
|
|
592
619
|
current = parent;
|
|
@@ -594,10 +621,19 @@ function collectLinkDirectories(projectRoot: string): string[] {
|
|
|
594
621
|
return out.reverse();
|
|
595
622
|
}
|
|
596
623
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
624
|
+
/**
|
|
625
|
+
* Whether mirroring `directory` would reach a filesystem or temporary root.
|
|
626
|
+
* Identity comparison is required on Windows, where `os.tmpdir()` can carry an
|
|
627
|
+
* 8.3 component while a project created below it is returned in long spelling.
|
|
628
|
+
*/
|
|
629
|
+
function isUnsafeVirtualParent(
|
|
630
|
+
directory: string,
|
|
631
|
+
identities: ReturnType<typeof createFilesystemPathIdentityContext>,
|
|
632
|
+
): boolean {
|
|
633
|
+
const resolved = identities.resolve(directory);
|
|
634
|
+
const root = identities.resolve(path.parse(resolved.path).root);
|
|
635
|
+
const temporaryRoot = identities.resolve(os.tmpdir());
|
|
636
|
+
return resolved.key === root.key || resolved.key === temporaryRoot.key;
|
|
601
637
|
}
|
|
602
638
|
|
|
603
639
|
/**
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
parseFlags,
|
|
18
18
|
} from "../../flags/parser";
|
|
19
19
|
import { resolveFlagSpec } from "../../flags/schema";
|
|
20
|
-
import {
|
|
20
|
+
import { resolveSafeCacheCleanupTargets } from "../../internal/resolveSafeCacheCleanupTargets";
|
|
21
21
|
import {
|
|
22
22
|
isPathWithin,
|
|
23
23
|
legacyGlobalCacheTargets,
|
|
@@ -207,9 +207,6 @@ function runClean(argv: readonly string[]): number {
|
|
|
207
207
|
const explicitCacheDir = options.cacheDir
|
|
208
208
|
? path.resolve(cwd, options.cacheDir)
|
|
209
209
|
: undefined;
|
|
210
|
-
if (explicitCacheDir !== undefined) {
|
|
211
|
-
assertSafeExplicitCacheDirectory(projectRoot, explicitCacheDir);
|
|
212
|
-
}
|
|
213
210
|
const targets = explicitCacheDir
|
|
214
211
|
? // Explicit `ttsc clean --cache-dir X`: the user names X as the cache to
|
|
215
212
|
// remove for this command, so remove it wholesale plus the legacy
|
|
@@ -223,11 +220,19 @@ function runClean(argv: readonly string[]): number {
|
|
|
223
220
|
// possibly-shared root is never deleted) plus the pre-0.17 machine-global
|
|
224
221
|
// cache so upgraders reclaim that disk.
|
|
225
222
|
[...resolveCleanTargets(projectRoot), ...legacyGlobalCacheTargets()];
|
|
223
|
+
// Check the complete deletion set before removing the first directory. An
|
|
224
|
+
// environment-selected TTSC_GO_CACHE_DIR and a legacy-global cache can be as
|
|
225
|
+
// destructive as an explicit --cache-dir when either equals or contains the
|
|
226
|
+
// project through its lexical spelling or a filesystem alias.
|
|
227
|
+
const safeTargets = resolveSafeCacheCleanupTargets(projectRoot, targets);
|
|
226
228
|
const removed: string[] = [];
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
const visited = new Set<string>();
|
|
230
|
+
for (const target of safeTargets) {
|
|
231
|
+
if (visited.has(target.path)) continue;
|
|
232
|
+
visited.add(target.path);
|
|
233
|
+
if (!target.exists || !fs.existsSync(target.path)) continue;
|
|
234
|
+
fs.rmSync(target.path, { recursive: true, force: true });
|
|
235
|
+
removed.push(target.requestedPath);
|
|
231
236
|
}
|
|
232
237
|
if (removed.length === 0) {
|
|
233
238
|
process.stdout.write(
|
|
@@ -269,24 +274,28 @@ function runCachePaths(argv: readonly string[]): number {
|
|
|
269
274
|
const cacheDir = resolveCacheDir(cwd, options.cacheDir);
|
|
270
275
|
const projectRoot = resolveCleanProjectRoot(cwd, options.tsconfig);
|
|
271
276
|
const paths = resolveSourceBuildCachePaths(projectRoot, cacheDir);
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
277
|
+
// Legacy combined roots retained for JSON compatibility. `requiredRoots`
|
|
278
|
+
// identifies the compiled binaries that skip cold builds;
|
|
279
|
+
// `acceleratorRoots` separately identifies the optional Go object cache.
|
|
275
280
|
const cacheableRoots = [
|
|
276
281
|
paths.root,
|
|
277
282
|
...(isPathWithin(paths.goBuildRoot, paths.root) ? [] : [paths.goBuildRoot]),
|
|
278
283
|
];
|
|
284
|
+
const requiredRoots = [paths.pluginRoot];
|
|
285
|
+
const acceleratorRoots = [paths.goBuildRoot];
|
|
279
286
|
if (options.json) {
|
|
280
287
|
process.stdout.write(
|
|
281
288
|
`${JSON.stringify(
|
|
282
289
|
{
|
|
283
290
|
cacheRoot: paths.root,
|
|
291
|
+
acceleratorRoots,
|
|
284
292
|
cacheableRoots,
|
|
285
293
|
cwd,
|
|
286
294
|
goBuildCacheRoot: paths.goBuildRoot,
|
|
287
295
|
goBuildCacheSource: paths.goBuildRootSource,
|
|
288
296
|
pluginCacheRoot: paths.pluginRoot,
|
|
289
297
|
projectRoot,
|
|
298
|
+
requiredRoots,
|
|
290
299
|
},
|
|
291
300
|
null,
|
|
292
301
|
2,
|
|
@@ -301,8 +310,10 @@ function runCachePaths(argv: readonly string[]): number {
|
|
|
301
310
|
` plugin binaries ${paths.pluginRoot}`,
|
|
302
311
|
` go build cache ${paths.goBuildRoot} (${paths.goBuildRootSource})`,
|
|
303
312
|
"",
|
|
304
|
-
"Persist
|
|
305
|
-
...
|
|
313
|
+
"Persist this required cache to skip cold plugin rebuilds:",
|
|
314
|
+
...requiredRoots.map((root) => ` ${root}`),
|
|
315
|
+
"Optionally persist this Go object accelerator for future cache misses:",
|
|
316
|
+
...acceleratorRoots.map((root) => ` ${root}`),
|
|
306
317
|
"",
|
|
307
318
|
].join("\n"),
|
|
308
319
|
);
|