ttsc 0.16.12 → 0.17.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/lib/TtscCompiler.d.ts +7 -7
- package/lib/TtscCompiler.js +21 -25
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/launcher/internal/resolveCacheDir.d.ts +4 -3
- package/lib/launcher/internal/resolveCacheDir.js +4 -3
- package/lib/launcher/internal/resolveCacheDir.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +140 -12
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/plugin/internal/buildSourcePlugin.d.ts +38 -19
- package/lib/plugin/internal/buildSourcePlugin.js +284 -76
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/package.json +8 -8
- package/src/TtscCompiler.ts +28 -27
- package/src/launcher/internal/resolveCacheDir.ts +4 -3
- package/src/launcher/internal/runTtsc.ts +184 -13
- package/src/plugin/internal/buildSourcePlugin.ts +360 -89
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type SpawnSyncOptionsWithStringEncoding,
|
|
3
|
+
type SpawnSyncReturns,
|
|
4
|
+
spawnSync,
|
|
5
|
+
} from "node:child_process";
|
|
2
6
|
import crypto from "node:crypto";
|
|
3
7
|
import fs from "node:fs";
|
|
4
8
|
import { createRequire } from "node:module";
|
|
@@ -100,15 +104,36 @@ const CONTRIB_DIRNAME = "contrib";
|
|
|
100
104
|
// runtimeHooks.ts.
|
|
101
105
|
const PLUGIN_BUILD_LOCK_STEAL_MS = 600_000;
|
|
102
106
|
const PLUGIN_BUILD_LOCK_POLL_MS = 50;
|
|
103
|
-
|
|
107
|
+
// The default cache lives INSIDE the workspace, at
|
|
108
|
+
// `<workspaceRoot>/node_modules/.cache/ttsc`, so `rm -rf node_modules` (or
|
|
109
|
+
// deleting the repo) reclaims every compiled plugin binary and Go object file.
|
|
110
|
+
// This is the `find-cache-dir` convention (Babel, webpack, ESLint, Nuxt, …): a
|
|
111
|
+
// disposable build cache under `node_modules/.cache/<tool>`. ttsc keeps NO
|
|
112
|
+
// global (`~/.cache`) cache — a machine-wide cache silently grew to hundreds of
|
|
113
|
+
// GB across tsgo/plugin version bumps, so it was removed outright. See
|
|
114
|
+
// resolveSourceBuildCacheRoot for the (override → workspace-local) priority.
|
|
115
|
+
const NODE_MODULES_DIRNAME = "node_modules";
|
|
116
|
+
const LOCAL_CACHE_PARENT_DIRNAME = ".cache";
|
|
117
|
+
const TTSC_CACHE_DIRNAME = "ttsc";
|
|
104
118
|
const PLUGIN_CACHE_DIRNAME = "plugins";
|
|
119
|
+
const GO_BUILD_CACHE_DIRNAME = "go-build";
|
|
120
|
+
// Directories whose presence marks a monorepo/workspace root, so every package
|
|
121
|
+
// in the workspace shares ONE cache and a plugin builds once, not once per
|
|
122
|
+
// package. `package.json` with a `workspaces` field (yarn/npm/bun) is checked
|
|
123
|
+
// separately in isWorkspaceRootDir.
|
|
124
|
+
const WORKSPACE_ROOT_MARKER_FILES: readonly string[] = ["pnpm-workspace.yaml"];
|
|
105
125
|
const CACHE_LAST_USED_FILE = ".last-used";
|
|
106
126
|
const CACHE_GC_MARKER_FILE = ".gc-last-run";
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
// The plugin binary cache is content-keyed, so a project that bumps tsgo/typia
|
|
128
|
+
// many times leaves one stale entry per superseded key. An opportunistic GC
|
|
129
|
+
// (once/day) evicts entries unused for 30 days and, past a 2 GB ceiling, the
|
|
130
|
+
// least-recently-used down to 80%. It is scoped to the resolved cache root only
|
|
131
|
+
// — ttsc never scans a shared or global location.
|
|
132
|
+
const PLUGIN_CACHE_GC_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
133
|
+
const PLUGIN_CACHE_ENTRY_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
134
|
+
const PLUGIN_CACHE_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
135
|
+
const PLUGIN_CACHE_TARGET_BYTES = Math.floor(PLUGIN_CACHE_MAX_BYTES * 0.8);
|
|
136
|
+
const PLUGIN_CACHE_PROTECTED_AGE_MS = 60 * 60 * 1000;
|
|
112
137
|
|
|
113
138
|
/** One contributor's resolved Go source plus its target sub-package name. */
|
|
114
139
|
export interface ITtscBuildContributor {
|
|
@@ -118,6 +143,18 @@ export interface ITtscBuildContributor {
|
|
|
118
143
|
source: string;
|
|
119
144
|
}
|
|
120
145
|
|
|
146
|
+
/** Source-plugin cache locations resolved for one ttsc invocation. */
|
|
147
|
+
export interface ITtscSourceBuildCachePaths {
|
|
148
|
+
/** Root directory containing all ttsc-owned source build caches. */
|
|
149
|
+
root: string;
|
|
150
|
+
/** Directory containing content-addressed compiled plugin binaries. */
|
|
151
|
+
pluginRoot: string;
|
|
152
|
+
/** Directory passed to Go as `GOCACHE` for source-plugin builds. */
|
|
153
|
+
goBuildRoot: string;
|
|
154
|
+
/** How `goBuildRoot` was selected. */
|
|
155
|
+
goBuildRootSource: "ttsc-cache" | "TTSC_GO_CACHE_DIR" | "GOCACHE";
|
|
156
|
+
}
|
|
157
|
+
|
|
121
158
|
/** Build one Go source plugin into a cached executable. */
|
|
122
159
|
export function buildSourcePlugin(opts: {
|
|
123
160
|
source: string;
|
|
@@ -145,8 +182,9 @@ export function buildSourcePlugin(opts: {
|
|
|
145
182
|
ttscVersion: opts.ttscVersion,
|
|
146
183
|
tsgoVersion: opts.tsgoVersion,
|
|
147
184
|
});
|
|
148
|
-
const
|
|
149
|
-
|
|
185
|
+
const paths = resolveSourceBuildCachePaths(opts.baseDir, opts.cacheDir);
|
|
186
|
+
maybePrunePluginCache(paths, opts.cacheDir);
|
|
187
|
+
const cacheDir = path.join(paths.pluginRoot, key);
|
|
150
188
|
const binaryName = process.platform === "win32" ? "plugin.exe" : "plugin";
|
|
151
189
|
const binaryPath = path.join(cacheDir, binaryName);
|
|
152
190
|
if (fs.existsSync(binaryPath)) {
|
|
@@ -164,6 +202,7 @@ export function buildSourcePlugin(opts: {
|
|
|
164
202
|
goBinary,
|
|
165
203
|
key,
|
|
166
204
|
label: opts.label ?? "source plugin",
|
|
205
|
+
goBuildCacheRoot: paths.goBuildRoot,
|
|
167
206
|
overlayDirs,
|
|
168
207
|
pluginName: opts.pluginName,
|
|
169
208
|
quiet: opts.quiet === true,
|
|
@@ -180,6 +219,7 @@ function compileSourcePlugin(opts: {
|
|
|
180
219
|
dir: string;
|
|
181
220
|
entry: string;
|
|
182
221
|
goBinary: string;
|
|
222
|
+
goBuildCacheRoot: string;
|
|
183
223
|
key: string;
|
|
184
224
|
label: string;
|
|
185
225
|
overlayDirs: readonly string[];
|
|
@@ -223,6 +263,7 @@ function compileSourcePlugin(opts: {
|
|
|
223
263
|
scratchBinaryName,
|
|
224
264
|
opts.pluginName,
|
|
225
265
|
opts.goBinary,
|
|
266
|
+
opts.goBuildCacheRoot,
|
|
226
267
|
);
|
|
227
268
|
const builtBinary = path.join(scratchDir, scratchBinaryName);
|
|
228
269
|
publishBuiltBinary(builtBinary, opts.binaryPath);
|
|
@@ -678,7 +719,7 @@ function readGoModInfo(
|
|
|
678
719
|
return emptyGoModInfo();
|
|
679
720
|
}
|
|
680
721
|
|
|
681
|
-
const result =
|
|
722
|
+
const result = spawnGoTool(goBinary, ["mod", "edit", "-json"], {
|
|
682
723
|
cwd: dir,
|
|
683
724
|
encoding: "utf8",
|
|
684
725
|
env: goBuildEnv(goBinary),
|
|
@@ -765,12 +806,13 @@ function runGoBuild(
|
|
|
765
806
|
binaryName: string,
|
|
766
807
|
pluginName: string,
|
|
767
808
|
goBinary: string,
|
|
809
|
+
goBuildCacheRoot: string,
|
|
768
810
|
): void {
|
|
769
811
|
ensureExecutableGoToolchain(goBinary);
|
|
770
|
-
const result =
|
|
812
|
+
const result = spawnGoTool(goBinary, ["build", "-o", binaryName, entry], {
|
|
771
813
|
cwd,
|
|
772
814
|
encoding: "utf8",
|
|
773
|
-
env: goBuildEnv(goBinary),
|
|
815
|
+
env: goBuildEnv(goBinary, goBuildCacheRoot),
|
|
774
816
|
maxBuffer: 1024 * 1024 * 64,
|
|
775
817
|
windowsHide: true,
|
|
776
818
|
});
|
|
@@ -797,9 +839,39 @@ function goToolchainNotFoundMessage(pluginName: string): string {
|
|
|
797
839
|
);
|
|
798
840
|
}
|
|
799
841
|
|
|
800
|
-
function
|
|
842
|
+
function spawnGoTool(
|
|
843
|
+
goBinary: string,
|
|
844
|
+
args: readonly string[],
|
|
845
|
+
options: SpawnSyncOptionsWithStringEncoding,
|
|
846
|
+
): SpawnSyncReturns<string> {
|
|
847
|
+
const shell = shouldSpawnGoToolThroughShell(goBinary);
|
|
848
|
+
// Under shell:true (a Windows .cmd/.bat wrapper) Node does not quote the
|
|
849
|
+
// command, so a wrapper at a path containing a space would be split by cmd.
|
|
850
|
+
// Quote it here; the go arguments ttsc passes never contain spaces, and
|
|
851
|
+
// quoting a space-free path is harmless.
|
|
852
|
+
return spawnSync(shell ? `"${goBinary}"` : goBinary, [...args], {
|
|
853
|
+
...options,
|
|
854
|
+
shell,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function shouldSpawnGoToolThroughShell(goBinary: string): boolean {
|
|
859
|
+
return process.platform === "win32" && /\.(?:bat|cmd)$/i.test(goBinary);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function goBuildEnv(
|
|
863
|
+
goBinary: string,
|
|
864
|
+
goBuildCacheRoot?: string,
|
|
865
|
+
): NodeJS.ProcessEnv {
|
|
801
866
|
const env = { ...process.env };
|
|
802
867
|
env.GOWORK = "auto";
|
|
868
|
+
// Only the actual `go build` needs ttsc's GOCACHE; read-only metadata spawns
|
|
869
|
+
// (`go mod edit`, `go env`, `go version`) call goBuildEnv with no cache root
|
|
870
|
+
// and inherit the ambient GOCACHE, which they never write to anyway. GOCACHE
|
|
871
|
+
// is not part of the plugin cache key, so this cannot affect it.
|
|
872
|
+
if (goBuildCacheRoot) {
|
|
873
|
+
env.GOCACHE = goBuildCacheRoot;
|
|
874
|
+
}
|
|
803
875
|
const goRoot = inferGoRoot(goBinary);
|
|
804
876
|
if (goRoot && !env.GOROOT) {
|
|
805
877
|
env.GOROOT = goRoot;
|
|
@@ -893,87 +965,267 @@ function walkForGoMod(dir: string, out: string[]): void {
|
|
|
893
965
|
/**
|
|
894
966
|
* Resolve the directory where compiled plugin binaries are cached.
|
|
895
967
|
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
* 2. `TTSC_CACHE_DIR` environment variable.
|
|
900
|
-
* 3. Platform-specific global user cache (XDG / AppData / Library/Caches).
|
|
901
|
-
*
|
|
902
|
-
* When falling back to the global cache, a GC pass is triggered
|
|
903
|
-
* opportunistically to evict old or oversized entries before the cache is
|
|
904
|
-
* used.
|
|
968
|
+
* Delegates to {@link resolveSourceBuildCachePaths}; kept as a thin accessor for
|
|
969
|
+
* callers (and tests) that only need the plugin-binary root. Triggers the
|
|
970
|
+
* opportunistic project-cache GC as a side effect for the default location.
|
|
905
971
|
*/
|
|
906
972
|
export function resolvePluginCacheRoot(
|
|
907
973
|
projectRoot: string,
|
|
908
974
|
cacheDir?: string,
|
|
975
|
+
): string {
|
|
976
|
+
const paths = resolveSourceBuildCachePaths(projectRoot, cacheDir);
|
|
977
|
+
maybePrunePluginCache(paths, cacheDir);
|
|
978
|
+
return paths.pluginRoot;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Resolve all source-plugin build cache directories for one invocation.
|
|
983
|
+
*
|
|
984
|
+
* `pluginRoot` stores compiled plugin binaries; `goBuildRoot` is the Go object
|
|
985
|
+
* cache passed as `GOCACHE` while ttsc builds those binaries. Both live under a
|
|
986
|
+
* single `root`, so persisting one directory covers the whole source-build
|
|
987
|
+
* cache without depending on ttsc internals.
|
|
988
|
+
*/
|
|
989
|
+
export function resolveSourceBuildCachePaths(
|
|
990
|
+
projectRoot: string,
|
|
991
|
+
cacheDir?: string,
|
|
992
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
993
|
+
): ITtscSourceBuildCachePaths {
|
|
994
|
+
const root = resolveSourceBuildCacheRoot(projectRoot, cacheDir, env);
|
|
995
|
+
const goBuild = resolveGoBuildCacheRoot(root, projectRoot, env);
|
|
996
|
+
return {
|
|
997
|
+
root,
|
|
998
|
+
pluginRoot: path.join(root, PLUGIN_CACHE_DIRNAME),
|
|
999
|
+
goBuildRoot: goBuild.root,
|
|
1000
|
+
goBuildRootSource: goBuild.source,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Resolve the cache root for one invocation.
|
|
1006
|
+
*
|
|
1007
|
+
* Priority:
|
|
1008
|
+
*
|
|
1009
|
+
* 1. Explicit `cacheDir` option (resolved relative to `projectRoot`);
|
|
1010
|
+
* 2. `TTSC_CACHE_DIR` environment variable (resolved absolute);
|
|
1011
|
+
* 3. `<workspaceRoot>/node_modules/.cache/ttsc` — project-local by default.
|
|
1012
|
+
*
|
|
1013
|
+
* There is deliberately NO global (`~/.cache`) fallback: the cache is scoped to
|
|
1014
|
+
* the workspace so it can never accumulate machine-wide, and `rm -rf
|
|
1015
|
+
* node_modules` reclaims it.
|
|
1016
|
+
*/
|
|
1017
|
+
function resolveSourceBuildCacheRoot(
|
|
1018
|
+
projectRoot: string,
|
|
1019
|
+
cacheDir: string | undefined,
|
|
1020
|
+
env: NodeJS.ProcessEnv,
|
|
909
1021
|
): string {
|
|
910
1022
|
if (cacheDir) {
|
|
911
|
-
return path.resolve(projectRoot, cacheDir
|
|
1023
|
+
return path.resolve(projectRoot, cacheDir);
|
|
912
1024
|
}
|
|
913
|
-
if (
|
|
914
|
-
|
|
1025
|
+
if (env.TTSC_CACHE_DIR) {
|
|
1026
|
+
// Anchor a relative TTSC_CACHE_DIR to the project root (not the process
|
|
1027
|
+
// cwd) so a programmatic host whose cwd differs from the project still
|
|
1028
|
+
// resolves — and later cleans — the same cache. Absolute values pass
|
|
1029
|
+
// through path.resolve unchanged.
|
|
1030
|
+
return path.resolve(projectRoot, env.TTSC_CACHE_DIR);
|
|
915
1031
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1032
|
+
return path.join(
|
|
1033
|
+
resolveWorkspaceRoot(projectRoot),
|
|
1034
|
+
NODE_MODULES_DIRNAME,
|
|
1035
|
+
LOCAL_CACHE_PARENT_DIRNAME,
|
|
1036
|
+
TTSC_CACHE_DIRNAME,
|
|
1037
|
+
);
|
|
919
1038
|
}
|
|
920
1039
|
|
|
921
1040
|
/**
|
|
922
|
-
*
|
|
1041
|
+
* Resolve the monorepo/workspace root for `projectRoot` so every package shares
|
|
1042
|
+
* one cache and a plugin builds once per workspace, not once per package.
|
|
1043
|
+
*
|
|
1044
|
+
* Walks up from `projectRoot` and returns, in order of preference: the NEAREST
|
|
1045
|
+
* ancestor that is a workspace root (holds `pnpm-workspace.yaml`, or a
|
|
1046
|
+
* `package.json` with a `workspaces` field); else the nearest ancestor that
|
|
1047
|
+
* already contains a `node_modules` directory; else `projectRoot` itself.
|
|
923
1048
|
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
1049
|
+
* Nearest (not highest) so an unrelated ancestor that happens to declare
|
|
1050
|
+
* `workspaces` — for example a `package.json` in the user's home directory —
|
|
1051
|
+
* cannot pull the cache above the project's real monorepo root.
|
|
927
1052
|
*/
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1053
|
+
function resolveWorkspaceRoot(projectRoot: string): string {
|
|
1054
|
+
let dir = path.resolve(projectRoot);
|
|
1055
|
+
let nearestNodeModulesOwner: string | null = null;
|
|
1056
|
+
for (;;) {
|
|
1057
|
+
if (isWorkspaceRootDir(dir)) {
|
|
1058
|
+
return dir;
|
|
1059
|
+
}
|
|
1060
|
+
if (
|
|
1061
|
+
nearestNodeModulesOwner === null &&
|
|
1062
|
+
fs.existsSync(path.join(dir, NODE_MODULES_DIRNAME))
|
|
1063
|
+
) {
|
|
1064
|
+
nearestNodeModulesOwner = dir;
|
|
1065
|
+
}
|
|
1066
|
+
const parent = path.dirname(dir);
|
|
1067
|
+
if (parent === dir) {
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
dir = parent;
|
|
1071
|
+
}
|
|
1072
|
+
return nearestNodeModulesOwner ?? path.resolve(projectRoot);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function isWorkspaceRootDir(dir: string): boolean {
|
|
1076
|
+
for (const marker of WORKSPACE_ROOT_MARKER_FILES) {
|
|
1077
|
+
if (fs.existsSync(path.join(dir, marker))) {
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return packageJsonDeclaresWorkspaces(path.join(dir, "package.json"));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function packageJsonDeclaresWorkspaces(packageJsonPath: string): boolean {
|
|
1085
|
+
let text: string;
|
|
1086
|
+
try {
|
|
1087
|
+
text = fs.readFileSync(packageJsonPath, "utf8");
|
|
1088
|
+
} catch {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
const workspaces = (JSON.parse(text) as { workspaces?: unknown })
|
|
1093
|
+
.workspaces;
|
|
1094
|
+
// A real workspace root declares a NON-EMPTY package list (an array, or an
|
|
1095
|
+
// object with a `packages` array). Ignore `false`, `[]`, `null`, or `{}` so
|
|
1096
|
+
// a disabled or empty declaration on an unrelated ancestor cannot hijack
|
|
1097
|
+
// the cache location.
|
|
1098
|
+
if (Array.isArray(workspaces)) {
|
|
1099
|
+
return workspaces.length > 0;
|
|
1100
|
+
}
|
|
1101
|
+
if (workspaces !== null && typeof workspaces === "object") {
|
|
1102
|
+
const packages = (workspaces as { packages?: unknown }).packages;
|
|
1103
|
+
return Array.isArray(packages) && packages.length > 0;
|
|
1104
|
+
}
|
|
1105
|
+
return false;
|
|
1106
|
+
} catch {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function resolveGoBuildCacheRoot(
|
|
1112
|
+
root: string,
|
|
1113
|
+
projectRoot: string,
|
|
1114
|
+
env: NodeJS.ProcessEnv,
|
|
1115
|
+
): {
|
|
1116
|
+
root: string;
|
|
1117
|
+
source: ITtscSourceBuildCachePaths["goBuildRootSource"];
|
|
1118
|
+
} {
|
|
1119
|
+
if (env.TTSC_GO_CACHE_DIR) {
|
|
1120
|
+
// Anchor a relative TTSC_GO_CACHE_DIR to the project root, matching
|
|
1121
|
+
// TTSC_CACHE_DIR, so the build and a later `clean` from a different cwd
|
|
1122
|
+
// agree on the directory. Absolute values pass through unchanged.
|
|
1123
|
+
return {
|
|
1124
|
+
root: path.resolve(projectRoot, env.TTSC_GO_CACHE_DIR),
|
|
1125
|
+
source: "TTSC_GO_CACHE_DIR",
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
if (env.GOCACHE && env.GOCACHE.length > 0) {
|
|
1129
|
+
return {
|
|
1130
|
+
root: env.GOCACHE,
|
|
1131
|
+
source: "GOCACHE",
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
root: path.join(root, GO_BUILD_CACHE_DIRNAME),
|
|
1136
|
+
source: "ttsc-cache",
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function maybePrunePluginCache(
|
|
1141
|
+
paths: ITtscSourceBuildCachePaths,
|
|
1142
|
+
cacheDir?: string,
|
|
1143
|
+
): void {
|
|
1144
|
+
// GC only the default (workspace-local) location. When the user pins an
|
|
1145
|
+
// explicit `cacheDir`/`TTSC_CACHE_DIR`, they own its lifetime, so ttsc must
|
|
1146
|
+
// not delete entries out from under them.
|
|
1147
|
+
if (!cacheDir && !process.env.TTSC_CACHE_DIR) {
|
|
1148
|
+
prunePluginCacheRoot(paths.pluginRoot);
|
|
1149
|
+
}
|
|
934
1150
|
}
|
|
935
1151
|
|
|
936
1152
|
/**
|
|
937
|
-
* Return
|
|
1153
|
+
* Return every directory `ttsc clean` should remove for `projectRoot`.
|
|
938
1154
|
*
|
|
939
|
-
* Covers
|
|
940
|
-
*
|
|
941
|
-
* project-local
|
|
1155
|
+
* Covers the resolved cache root (which holds `plugins/` and, when ttsc-owned,
|
|
1156
|
+
* `go-build/`), a ttsc-owned Go build cache that lives OUTSIDE that root
|
|
1157
|
+
* (`TTSC_GO_CACHE_DIR`), and the two legacy project-local caches. A
|
|
1158
|
+
* user-provided `GOCACHE` is never removed. Pure over `env`, so the CLI passes
|
|
1159
|
+
* `process.env` and a programmatic caller can pass an injected environment.
|
|
942
1160
|
*/
|
|
943
|
-
export function
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1161
|
+
export function resolveCleanTargets(
|
|
1162
|
+
projectRoot: string,
|
|
1163
|
+
cacheDir?: string,
|
|
1164
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
1165
|
+
): string[] {
|
|
1166
|
+
const paths = resolveSourceBuildCachePaths(projectRoot, cacheDir, env);
|
|
1167
|
+
// Remove ttsc-OWNED directories only, never the parent cache root.
|
|
1168
|
+
const targets = [paths.pluginRoot];
|
|
1169
|
+
// ttsc's nested `<root>/go-build` is only safe to delete when we are certain
|
|
1170
|
+
// the root belongs to ttsc: the default `node_modules/.cache/ttsc`, or a root
|
|
1171
|
+
// the user explicitly named `ttsc`. Under a shared root (e.g.
|
|
1172
|
+
// `TTSC_CACHE_DIR=~/.cache`) a bare `<root>/go-build` could be the user's
|
|
1173
|
+
// machine-wide GOCACHE, so it must never be removed by name.
|
|
1174
|
+
const isTtscOwnedRoot =
|
|
1175
|
+
(!cacheDir && !env.TTSC_CACHE_DIR) ||
|
|
1176
|
+
path.basename(paths.root) === TTSC_CACHE_DIRNAME;
|
|
1177
|
+
if (isTtscOwnedRoot) {
|
|
1178
|
+
targets.push(path.join(paths.root, GO_BUILD_CACHE_DIRNAME));
|
|
1179
|
+
}
|
|
1180
|
+
// An explicit TTSC_GO_CACHE_DIR is a ttsc-dedicated external cache; a
|
|
1181
|
+
// user-provided GOCACHE (source "GOCACHE") is never removed.
|
|
1182
|
+
if (paths.goBuildRootSource === "TTSC_GO_CACHE_DIR") {
|
|
1183
|
+
targets.push(paths.goBuildRoot);
|
|
1184
|
+
}
|
|
1185
|
+
targets.push(path.join(projectRoot, NODE_MODULES_DIRNAME, ".ttsc"));
|
|
1186
|
+
targets.push(path.join(projectRoot, ".ttsc"));
|
|
1187
|
+
return targets;
|
|
949
1188
|
}
|
|
950
1189
|
|
|
951
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* Machine-global cache directories created by pre-0.17 ttsc releases (XDG /
|
|
1192
|
+
* AppData / Library / `~/.cache`). ttsc no longer writes to any of these, but
|
|
1193
|
+
* an upgraded machine can still hold a multi-GB orphaned cache here, so `ttsc
|
|
1194
|
+
* clean` offers them for removal to reclaim that disk. Each entry is the whole
|
|
1195
|
+
* `<userCacheRoot>/ttsc` directory (both its `plugins` and `go-build`), which
|
|
1196
|
+
* was entirely ttsc-owned in those releases and is safe to remove.
|
|
1197
|
+
*/
|
|
1198
|
+
export function legacyGlobalCacheTargets(): string[] {
|
|
1199
|
+
const roots = new Set<string>();
|
|
1200
|
+
const home = os.homedir();
|
|
952
1201
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
953
1202
|
if (xdg && path.isAbsolute(xdg)) {
|
|
954
|
-
|
|
1203
|
+
roots.add(path.join(xdg, TTSC_CACHE_DIRNAME));
|
|
955
1204
|
}
|
|
956
1205
|
if (process.platform === "win32") {
|
|
957
1206
|
const local = process.env.LOCALAPPDATA;
|
|
958
1207
|
if (local && path.isAbsolute(local)) {
|
|
959
|
-
|
|
1208
|
+
roots.add(path.join(local, TTSC_CACHE_DIRNAME));
|
|
960
1209
|
}
|
|
961
|
-
const home = os.homedir();
|
|
962
|
-
if (home) {
|
|
963
|
-
return path.join(home, "AppData", "Local");
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
if (process.platform === "darwin") {
|
|
967
|
-
const home = os.homedir();
|
|
968
1210
|
if (home) {
|
|
969
|
-
|
|
1211
|
+
roots.add(path.join(home, "AppData", "Local", TTSC_CACHE_DIRNAME));
|
|
970
1212
|
}
|
|
1213
|
+
} else if (process.platform === "darwin" && home) {
|
|
1214
|
+
roots.add(path.join(home, "Library", "Caches", TTSC_CACHE_DIRNAME));
|
|
971
1215
|
}
|
|
972
|
-
const home = os.homedir();
|
|
973
1216
|
if (home) {
|
|
974
|
-
|
|
1217
|
+
roots.add(path.join(home, ".cache", TTSC_CACHE_DIRNAME));
|
|
975
1218
|
}
|
|
976
|
-
return
|
|
1219
|
+
return [...roots];
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/** Report whether `child` equals `parent` or is nested beneath it. */
|
|
1223
|
+
export function isPathWithin(child: string, parent: string): boolean {
|
|
1224
|
+
const rel = path.relative(parent, child);
|
|
1225
|
+
return (
|
|
1226
|
+
rel === "" ||
|
|
1227
|
+
(rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel))
|
|
1228
|
+
);
|
|
977
1229
|
}
|
|
978
1230
|
|
|
979
1231
|
function resolveGoCompiler(): string {
|
|
@@ -1186,7 +1438,7 @@ function computeGoCompilerIdentity(goBinary: string, resolved: string): string {
|
|
|
1186
1438
|
if (!fs.existsSync(resolved)) {
|
|
1187
1439
|
return "missing";
|
|
1188
1440
|
}
|
|
1189
|
-
const version =
|
|
1441
|
+
const version = spawnGoTool(goBinary, ["version"], {
|
|
1190
1442
|
encoding: "utf8",
|
|
1191
1443
|
windowsHide: true,
|
|
1192
1444
|
});
|
|
@@ -1212,15 +1464,30 @@ function resolveExecutableIdentityPath(binary: string): string {
|
|
|
1212
1464
|
return resolveRealPath(candidate);
|
|
1213
1465
|
}
|
|
1214
1466
|
if (process.platform === "win32") {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1467
|
+
// Probe every PATHEXT extension, not just `.exe`, so a compiler backed by
|
|
1468
|
+
// a `.cmd`/`.bat` wrapper resolves to its real file and is hashed into the
|
|
1469
|
+
// cache key. Otherwise the wrapper reads as "missing" and a change to it
|
|
1470
|
+
// would not invalidate the cached plugin binary.
|
|
1471
|
+
for (const ext of windowsExecutableExtensions()) {
|
|
1472
|
+
const executable = `${candidate}${ext}`;
|
|
1473
|
+
if (fs.existsSync(executable)) {
|
|
1474
|
+
return resolveRealPath(executable);
|
|
1475
|
+
}
|
|
1218
1476
|
}
|
|
1219
1477
|
}
|
|
1220
1478
|
}
|
|
1221
1479
|
return binary;
|
|
1222
1480
|
}
|
|
1223
1481
|
|
|
1482
|
+
function windowsExecutableExtensions(): readonly string[] {
|
|
1483
|
+
const pathext = process.env.PATHEXT;
|
|
1484
|
+
const raw = pathext && pathext.length > 0 ? pathext : ".COM;.EXE;.BAT;.CMD";
|
|
1485
|
+
return raw
|
|
1486
|
+
.split(";")
|
|
1487
|
+
.map((ext) => ext.trim().toLowerCase())
|
|
1488
|
+
.filter((ext) => ext.length > 0);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1224
1491
|
function readPathEnvironment(): string {
|
|
1225
1492
|
return process.env.PATH ?? process.env.Path ?? "";
|
|
1226
1493
|
}
|
|
@@ -1259,13 +1526,17 @@ function resolveGoBuildEnvironment(
|
|
|
1259
1526
|
): Map<string, string> {
|
|
1260
1527
|
const values = new Map<string, string>();
|
|
1261
1528
|
if (goBinary !== undefined) {
|
|
1262
|
-
const result =
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1529
|
+
const result = spawnGoTool(
|
|
1530
|
+
goBinary,
|
|
1531
|
+
["env", "-json", ...GO_BUILD_ENV_KEYS],
|
|
1532
|
+
{
|
|
1533
|
+
cwd,
|
|
1534
|
+
encoding: "utf8",
|
|
1535
|
+
env: goBuildEnv(goBinary),
|
|
1536
|
+
maxBuffer: 1024 * 1024,
|
|
1537
|
+
windowsHide: true,
|
|
1538
|
+
},
|
|
1539
|
+
);
|
|
1269
1540
|
if (result.error === undefined && result.status === 0) {
|
|
1270
1541
|
try {
|
|
1271
1542
|
const parsed = JSON.parse(result.stdout) as Record<string, unknown>;
|
|
@@ -1430,41 +1701,41 @@ function touchCacheEntry(cacheDir: string): void {
|
|
|
1430
1701
|
}
|
|
1431
1702
|
}
|
|
1432
1703
|
|
|
1433
|
-
function
|
|
1704
|
+
function prunePluginCacheRoot(root: string): void {
|
|
1434
1705
|
try {
|
|
1435
1706
|
fs.mkdirSync(root, { recursive: true });
|
|
1436
1707
|
const marker = path.join(root, CACHE_GC_MARKER_FILE);
|
|
1437
1708
|
const now = Date.now();
|
|
1438
1709
|
const lastRun = readTimestamp(marker);
|
|
1439
|
-
if (lastRun !== null && now - lastRun <
|
|
1710
|
+
if (lastRun !== null && now - lastRun < PLUGIN_CACHE_GC_INTERVAL_MS) {
|
|
1440
1711
|
return;
|
|
1441
1712
|
}
|
|
1442
1713
|
fs.writeFileSync(marker, `${now}\n`);
|
|
1443
|
-
|
|
1714
|
+
prunePluginCacheEntries(root, now);
|
|
1444
1715
|
} catch {
|
|
1445
|
-
//
|
|
1716
|
+
// Plugin-cache GC is opportunistic; builds still proceed when it fails.
|
|
1446
1717
|
}
|
|
1447
1718
|
}
|
|
1448
1719
|
|
|
1449
|
-
function
|
|
1450
|
-
const entries =
|
|
1720
|
+
function prunePluginCacheEntries(root: string, now: number): void {
|
|
1721
|
+
const entries = collectPluginCacheEntries(root, now);
|
|
1451
1722
|
for (const entry of entries) {
|
|
1452
|
-
if (now - entry.lastUsedAt <=
|
|
1723
|
+
if (now - entry.lastUsedAt <= PLUGIN_CACHE_ENTRY_MAX_AGE_MS) {
|
|
1453
1724
|
continue;
|
|
1454
1725
|
}
|
|
1455
1726
|
removeCacheEntry(entry);
|
|
1456
1727
|
}
|
|
1457
1728
|
|
|
1458
|
-
const remaining =
|
|
1729
|
+
const remaining = collectPluginCacheEntries(root, now);
|
|
1459
1730
|
let total = remaining.reduce((sum, entry) => sum + entry.size, 0);
|
|
1460
|
-
if (total <=
|
|
1731
|
+
if (total <= PLUGIN_CACHE_MAX_BYTES) {
|
|
1461
1732
|
return;
|
|
1462
1733
|
}
|
|
1463
1734
|
for (const entry of remaining.sort((a, b) => a.lastUsedAt - b.lastUsedAt)) {
|
|
1464
|
-
if (total <=
|
|
1735
|
+
if (total <= PLUGIN_CACHE_TARGET_BYTES) {
|
|
1465
1736
|
return;
|
|
1466
1737
|
}
|
|
1467
|
-
if (now - entry.lastUsedAt <=
|
|
1738
|
+
if (now - entry.lastUsedAt <= PLUGIN_CACHE_PROTECTED_AGE_MS) {
|
|
1468
1739
|
continue;
|
|
1469
1740
|
}
|
|
1470
1741
|
removeCacheEntry(entry);
|
|
@@ -1472,17 +1743,17 @@ function pruneGlobalPluginCache(root: string, now: number): void {
|
|
|
1472
1743
|
}
|
|
1473
1744
|
}
|
|
1474
1745
|
|
|
1475
|
-
interface
|
|
1746
|
+
interface PluginCacheEntry {
|
|
1476
1747
|
dir: string;
|
|
1477
1748
|
lastUsedAt: number;
|
|
1478
1749
|
size: number;
|
|
1479
1750
|
}
|
|
1480
1751
|
|
|
1481
|
-
function
|
|
1752
|
+
function collectPluginCacheEntries(
|
|
1482
1753
|
root: string,
|
|
1483
1754
|
now: number,
|
|
1484
|
-
):
|
|
1485
|
-
const entries:
|
|
1755
|
+
): PluginCacheEntry[] {
|
|
1756
|
+
const entries: PluginCacheEntry[] = [];
|
|
1486
1757
|
let dirents: fs.Dirent[];
|
|
1487
1758
|
try {
|
|
1488
1759
|
dirents = fs.readdirSync(root, { withFileTypes: true });
|
|
@@ -1557,7 +1828,7 @@ function directorySize(dir: string): number {
|
|
|
1557
1828
|
return total;
|
|
1558
1829
|
}
|
|
1559
1830
|
|
|
1560
|
-
function removeCacheEntry(entry:
|
|
1831
|
+
function removeCacheEntry(entry: PluginCacheEntry): void {
|
|
1561
1832
|
try {
|
|
1562
1833
|
fs.rmSync(entry.dir, { recursive: true, force: true });
|
|
1563
1834
|
} catch {
|