ttsc 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/cmd/ttsc/api_transform.go +17 -3
- package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
- package/cmd/ttscgraph/disk_realpath_other.go +16 -0
- package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
- package/cmd/ttscgraph/serve.go +104 -35
- package/driver/diagnostics_preamble.go +284 -0
- package/driver/emit_plugin.go +71 -19
- package/driver/graph.go +64 -11
- package/driver/input_observation.go +273 -0
- package/driver/input_observation_test.go +149 -0
- package/driver/plugins.go +384 -4
- package/driver/program.go +81 -15
- package/driver/resolution_candidates.go +13 -5
- package/driver/rewrite.go +47 -4
- package/internal/graph/diagnostics.go +4 -3
- package/internal/graph/provenance.go +10 -1
- 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 +11 -3
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +29 -1
- package/lib/compiler/internal/runBuild.js +117 -32
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +9 -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 +33 -5
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
- package/lib/internal/createCanonicalTempDirectory.js +42 -0
- package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
- package/lib/internal/projectInputPathIdentity.d.ts +3 -0
- package/lib/internal/projectInputPathIdentity.js +11 -5
- package/lib/internal/projectInputPathIdentity.js.map +1 -1
- package/lib/internal/resolveNodeBinary.d.ts +14 -0
- package/lib/internal/resolveNodeBinary.js +150 -0
- package/lib/internal/resolveNodeBinary.js.map +1 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
- package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
- package/lib/launcher/internal/assertNoSolutionBuild.d.ts +31 -0
- package/lib/launcher/internal/assertNoSolutionBuild.js +38 -0
- package/lib/launcher/internal/assertNoSolutionBuild.js.map +1 -0
- package/lib/launcher/internal/prepareExecution.d.ts +3 -0
- package/lib/launcher/internal/prepareExecution.js +73 -26
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +27 -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 +4 -0
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +37 -1
- package/lib/launcher/internal/runtimeHooks.js +660 -62
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
- package/lib/plugin/internal/buildSourcePlugin.js +754 -72
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
- package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/register.d.ts +1 -0
- package/lib/register.js +46 -0
- package/lib/register.js.map +1 -0
- package/lib/structures/ITtscCompilerContext.d.ts +6 -3
- package/lib/structures/ITtscCompilerTransformation.d.ts +31 -0
- package/lib/structures/ITtscPlugin.d.ts +33 -0
- package/package.json +16 -9
- package/shim/checker/go.mod +9 -0
- package/shim/checker/go.sum +22 -0
- package/shim/checker/shim.go +75 -0
- package/shim/compiler/emit_sourcemap.go +56 -15
- 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 +11 -3
- package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
- package/src/compiler/internal/runBuild.ts +172 -36
- package/src/compiler/internal/runSingleFileEmit.ts +9 -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 +35 -5
- package/src/internal/createCanonicalTempDirectory.ts +53 -0
- package/src/internal/projectInputPathIdentity.ts +17 -4
- package/src/internal/resolveNodeBinary.ts +173 -0
- package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
- package/src/launcher/internal/assertNoSolutionBuild.ts +40 -0
- package/src/launcher/internal/prepareExecution.ts +78 -26
- package/src/launcher/internal/runTtsc.ts +26 -13
- package/src/launcher/internal/runTtscserver.ts +18 -3
- package/src/launcher/internal/runTtsx.ts +4 -0
- package/src/launcher/internal/runtimeHooks.ts +830 -66
- package/src/plugin/internal/buildSourcePlugin.ts +984 -79
- package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
- package/src/register.ts +47 -0
- package/src/structures/ITtscCompilerContext.ts +6 -3
- package/src/structures/ITtscCompilerTransformation.ts +39 -0
- package/src/structures/ITtscPlugin.ts +36 -0
- package/tools/shim_audit/baseline.json +0 -1
- package/utility/host.go +9 -3
- package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
- package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
- package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
- package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
package/src/flags/schema.ts
CHANGED
|
@@ -219,6 +219,29 @@ export const FLAG_SCHEMA: readonly FlagSpec[] = [
|
|
|
219
219
|
description: "Print the launcher version and exit.",
|
|
220
220
|
},
|
|
221
221
|
|
|
222
|
+
// -------------------------------------------------------------------------
|
|
223
|
+
// Solution build mode — declared so the launcher can refuse it in its own
|
|
224
|
+
// voice instead of forwarding it.
|
|
225
|
+
//
|
|
226
|
+
// `createTsgoBuildArgs` opens the forwarded argv with `-p <tsconfig>` because
|
|
227
|
+
// ttsc resolves the project itself (extends chains, plugin config discovery,
|
|
228
|
+
// cache keys, resident session identity) and pins the result. A forwarded
|
|
229
|
+
// `--build` therefore always lands after `-p`, and tsgo answers with TS6369
|
|
230
|
+
// "Option '--build' must be the first command line argument" — a diagnostic
|
|
231
|
+
// that contradicts the command line the user actually typed. ttsc's plugin,
|
|
232
|
+
// cache, and emit architecture is built around one resolved project, so
|
|
233
|
+
// solution mode is unsupported rather than merely misordered.
|
|
234
|
+
// -------------------------------------------------------------------------
|
|
235
|
+
{
|
|
236
|
+
name: "--build",
|
|
237
|
+
aliases: ["-b"],
|
|
238
|
+
kind: "boolean",
|
|
239
|
+
subcommands: ["ttsc", "ttsx", "build", "check", "fix", "format"],
|
|
240
|
+
consumedBy: ["launcher"],
|
|
241
|
+
description:
|
|
242
|
+
"Refused by the launcher: ttsc pins one resolved project, so tsgo's solution-build mode is unsupported.",
|
|
243
|
+
},
|
|
244
|
+
|
|
222
245
|
// -------------------------------------------------------------------------
|
|
223
246
|
// Project location: shared by every subcommand.
|
|
224
247
|
// -------------------------------------------------------------------------
|
|
@@ -612,10 +635,17 @@ export const FLAG_SCHEMA: readonly FlagSpec[] = [
|
|
|
612
635
|
},
|
|
613
636
|
|
|
614
637
|
// -------------------------------------------------------------------------
|
|
615
|
-
// `--tsgo-args` — JSON-encoded passthrough envelope
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
638
|
+
// `--tsgo-args` — JSON-encoded passthrough envelope a native sidecar decodes
|
|
639
|
+
// back into the tsgo argv. Listed here so the schema describes every flag
|
|
640
|
+
// the Go layers accept, not just the user-facing ones.
|
|
641
|
+
//
|
|
642
|
+
// The launcher no longer emits it. A CLI flag is fatal to any host whose
|
|
643
|
+
// `flag.FlagSet` does not declare it, and this one was added to a plugin
|
|
644
|
+
// protocol third-party hosts had already frozen, so every forwarded compiler
|
|
645
|
+
// flag exited 2 on a typia/nestia-shaped sidecar (issue #1188). The payload
|
|
646
|
+
// now rides the `TTSC_TSGO_ARGS` environment variable, which an unaware host
|
|
647
|
+
// simply ignores. ttsc's own hosts still accept the flag so an older
|
|
648
|
+
// launcher, or an embedder that composes sidecar argv itself, keeps working.
|
|
619
649
|
// -------------------------------------------------------------------------
|
|
620
650
|
{
|
|
621
651
|
name: "--tsgo-args",
|
|
@@ -631,7 +661,7 @@ export const FLAG_SCHEMA: readonly FlagSpec[] = [
|
|
|
631
661
|
],
|
|
632
662
|
consumedBy: ["host", "lint"],
|
|
633
663
|
description:
|
|
634
|
-
"JSON-encoded tsgo passthrough argv (internal:
|
|
664
|
+
"JSON-encoded tsgo passthrough argv (internal: accepted by ttsc's own hosts; the launcher forwards it in TTSC_TSGO_ARGS).",
|
|
635
665
|
},
|
|
636
666
|
{
|
|
637
667
|
name: "--plugins-json",
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ParseResult } from "../../flags/parser";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Refuse tsgo's solution-build mode (`--build` / `-b`) in the launcher's own
|
|
5
|
+
* voice, for whichever launcher parsed the arguments.
|
|
6
|
+
*
|
|
7
|
+
* Every ttsc build lane hands tsgo an argument list that opens with the project
|
|
8
|
+
* ttsc resolved (`-p <tsconfig>` in `createTsgoBuildArgs`), because ttsc — not
|
|
9
|
+
* tsgo — owns the `extends` chain, plugin config discovery, cache keys, and the
|
|
10
|
+
* resident session's identity, and pins that one resolved project instead of
|
|
11
|
+
* forwarding raw argv. A forwarded `--build` therefore always arrives after
|
|
12
|
+
* `-p`, and tsgo replies "Option '--build' must be the first command line
|
|
13
|
+
* argument" even though the user wrote it first. No spelling of the command
|
|
14
|
+
* line can satisfy that diagnostic, so the flag is rejected here rather than
|
|
15
|
+
* forwarded into an error that blames the user's argument order.
|
|
16
|
+
*
|
|
17
|
+
* Both launchers refuse it, because both reach tsgo through the same pinned
|
|
18
|
+
* project. `ttsx` additionally builds one entry inside that project, so a mode
|
|
19
|
+
* whose whole purpose is compiling many projects has nothing to mean there
|
|
20
|
+
* either.
|
|
21
|
+
*
|
|
22
|
+
* Presence — not the parsed boolean — is what is refused: `--build false` still
|
|
23
|
+
* asks for a mode ttsc does not implement, and silently consuming it would drop
|
|
24
|
+
* the flag with no diagnostic at all.
|
|
25
|
+
*
|
|
26
|
+
* What this deliberately does not reach is a `--build` that belongs to the
|
|
27
|
+
* program `ttsx` is running. `runTtsx` parses with
|
|
28
|
+
* `forwardAfterFirstPositional` and `honorDoubleDashSeparator`, so a token
|
|
29
|
+
* after the entry lands in `result.tail` and never in `result.values`; reading
|
|
30
|
+
* `values` is what keeps `ttsx script.ts --build` the user program's own flag.
|
|
31
|
+
*/
|
|
32
|
+
export function assertNoSolutionBuild(
|
|
33
|
+
result: ParseResult,
|
|
34
|
+
errorPrefix: string,
|
|
35
|
+
): void {
|
|
36
|
+
if (!result.values.has("--build")) return;
|
|
37
|
+
throw new Error(
|
|
38
|
+
`${errorPrefix} --build (solution mode) is not supported; ttsc resolves and pins one project per run, so compile each referenced project with its own ttsc -p <tsconfig> run`,
|
|
39
|
+
);
|
|
40
|
+
}
|