ttsc 0.11.0 → 0.12.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.
Files changed (79) hide show
  1. package/README.md +25 -6
  2. package/cmd/ttsc/api_compile.go +101 -106
  3. package/cmd/ttsc/api_compile_branches_test.go +81 -0
  4. package/cmd/ttsc/api_transform.go +49 -54
  5. package/cmd/ttsc/api_transform_branches_test.go +41 -0
  6. package/cmd/ttsc/build.go +108 -109
  7. package/cmd/ttsc/build_branches_test.go +130 -0
  8. package/cmd/ttsc/helpers_test.go +47 -0
  9. package/cmd/ttsc/main.go +104 -103
  10. package/cmd/ttsc/main_alias_branches_test.go +29 -0
  11. package/cmd/ttscserver/main.go +104 -96
  12. package/cmd/ttscserver/run_lsp_prefers_tsgo_flag_test.go +42 -0
  13. package/cmd/ttscserver/run_lsp_reports_server_error_test.go +23 -25
  14. package/cmd/ttscserver/run_lsp_uses_tsgo_environment_test.go +44 -0
  15. package/cmd/utility-host/main.go +42 -0
  16. package/driver/lsp.go +60 -0
  17. package/driver/plugins.go +122 -0
  18. package/driver/program.go +346 -356
  19. package/driver/rewrite.go +336 -339
  20. package/{driver → internal/lspserver}/lsp_envelope.go +70 -64
  21. package/{driver → internal/lspserver}/lsp_frame.go +62 -62
  22. package/{driver → internal/lspserver}/lsp_plugin.go +58 -58
  23. package/internal/lspserver/lsp_proxy.go +391 -0
  24. package/internal/lspserver/lsp_server.go +231 -0
  25. package/lib/TtscCompiler.d.ts +3 -3
  26. package/lib/TtscCompiler.js +20 -6
  27. package/lib/TtscCompiler.js.map +1 -1
  28. package/lib/compiler/internal/resolveTsgo.d.ts +1 -0
  29. package/lib/compiler/internal/resolveTsgo.js +15 -4
  30. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  31. package/lib/compiler/internal/runBuild.js +12 -4
  32. package/lib/compiler/internal/runBuild.js.map +1 -1
  33. package/lib/compiler/internal/sharedHostHelpers.d.ts +12 -33
  34. package/lib/compiler/internal/sharedHostHelpers.js +27 -70
  35. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  36. package/lib/compiler/internal/transformProjectInMemory.js +12 -5
  37. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  38. package/lib/index.d.ts +0 -1
  39. package/lib/index.js +0 -1
  40. package/lib/index.js.map +1 -1
  41. package/lib/launcher/internal/runTtsc.js +9 -4
  42. package/lib/launcher/internal/runTtsc.js.map +1 -1
  43. package/lib/launcher/internal/runTtscserver.d.ts +1 -0
  44. package/lib/launcher/internal/runTtscserver.js +38 -1
  45. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  46. package/lib/plugin/internal/buildSourcePlugin.d.ts +3 -0
  47. package/lib/plugin/internal/buildSourcePlugin.js +444 -41
  48. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  49. package/lib/plugin/internal/loadProjectPlugins.js +153 -34
  50. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  51. package/lib/structures/ITtscCompilerContext.d.ts +2 -1
  52. package/lib/structures/ITtscPlugin.d.ts +8 -13
  53. package/lib/structures/ITtscPluginFactoryContext.d.ts +4 -5
  54. package/lib/structures/ITtscProjectPluginConfig.d.ts +2 -2
  55. package/lib/structures/internal/ITtscLoadedNativePlugin.d.ts +4 -2
  56. package/package.json +8 -8
  57. package/shim/lsp/shim.go +11 -17
  58. package/src/TtscCompiler.ts +20 -7
  59. package/src/compiler/internal/resolveTsgo.ts +18 -5
  60. package/src/compiler/internal/runBuild.ts +17 -4
  61. package/src/compiler/internal/sharedHostHelpers.ts +34 -67
  62. package/src/compiler/internal/transformProjectInMemory.ts +18 -5
  63. package/src/index.ts +0 -1
  64. package/src/launcher/internal/runTtsc.ts +9 -4
  65. package/src/launcher/internal/runTtscserver.ts +43 -1
  66. package/src/plugin/internal/buildSourcePlugin.ts +475 -41
  67. package/src/plugin/internal/loadProjectPlugins.ts +199 -39
  68. package/src/structures/ITtscCompilerContext.ts +2 -1
  69. package/src/structures/ITtscPlugin.ts +9 -13
  70. package/src/structures/ITtscPluginFactoryContext.ts +4 -5
  71. package/src/structures/ITtscProjectPluginConfig.ts +2 -2
  72. package/src/structures/internal/ITtscLoadedNativePlugin.ts +4 -2
  73. package/utility/host.go +267 -1296
  74. package/driver/lsp_proxy.go +0 -394
  75. package/driver/lsp_server.go +0 -197
  76. package/lib/defineTtscPlugin.d.ts +0 -18
  77. package/lib/defineTtscPlugin.js +0 -7
  78. package/lib/defineTtscPlugin.js.map +0 -1
  79. package/src/defineTtscPlugin.ts +0 -26
@@ -12,14 +12,15 @@ const TSGO_GO_MODULE_PATH = "github.com/microsoft/typescript-go";
12
12
  const PRUNE_DIRS = new Set(["node_modules", ".git", ".ttsc"]);
13
13
  const GENERATED_WORKSPACE_FILES = new Set(["go.work", "go.work.sum"]);
14
14
 
15
- // Go env vars that change the produced binary. Hashed into the plugin
16
- // build cache key so cross-compile target / build tags / cgo toggle
17
- // don't collide with the native-platform key.
15
+ // Go build environment values that can change the produced binary or decide
16
+ // whether `go build` succeeds. Hashed into the plugin cache key so target,
17
+ // build-tag, cgo, FIPS, and external-link variants never collide.
18
18
  const GO_BUILD_ENV_KEYS: readonly string[] = [
19
19
  "GOOS",
20
20
  "GOARCH",
21
21
  "GOAMD64",
22
22
  "GOARM",
23
+ "GOARM64",
23
24
  "GO386",
24
25
  "GOMIPS",
25
26
  "GOMIPS64",
@@ -28,11 +29,73 @@ const GO_BUILD_ENV_KEYS: readonly string[] = [
28
29
  "GOWASM",
29
30
  "GOFLAGS",
30
31
  "GOEXPERIMENT",
32
+ "GOFIPS140",
33
+ "GO_EXTLINK_ENABLED",
34
+ "GCCGO",
35
+ "GCCGOTOOLDIR",
31
36
  "CGO_ENABLED",
37
+ "AR",
38
+ "CC",
39
+ "CXX",
40
+ "FC",
41
+ "PKG_CONFIG",
42
+ "CGO_CFLAGS",
43
+ "CGO_CFLAGS_ALLOW",
44
+ "CGO_CFLAGS_DISALLOW",
45
+ "CGO_CPPFLAGS",
46
+ "CGO_CPPFLAGS_ALLOW",
47
+ "CGO_CPPFLAGS_DISALLOW",
48
+ "CGO_CXXFLAGS",
49
+ "CGO_CXXFLAGS_ALLOW",
50
+ "CGO_CXXFLAGS_DISALLOW",
51
+ "CGO_FFLAGS",
52
+ "CGO_FFLAGS_ALLOW",
53
+ "CGO_FFLAGS_DISALLOW",
54
+ "CGO_LDFLAGS",
55
+ "CGO_LDFLAGS_ALLOW",
56
+ "CGO_LDFLAGS_DISALLOW",
32
57
  "GOTOOLCHAIN",
58
+ "GOROOT",
59
+ ];
60
+ const GO_BUILD_COMMAND_ENV_KEYS = new Set([
61
+ "AR",
62
+ "CC",
63
+ "CXX",
64
+ "FC",
65
+ "GCCGO",
66
+ "PKG_CONFIG",
67
+ ]);
68
+ const EXTERNAL_GO_BUILD_ENV_KEYS: readonly string[] = [
69
+ "CPATH",
70
+ "C_INCLUDE_PATH",
71
+ "CPLUS_INCLUDE_PATH",
72
+ "DYLD_LIBRARY_PATH",
73
+ "INCLUDE",
74
+ "LD_LIBRARY_PATH",
75
+ "LIB",
76
+ "LIBRARY_PATH",
77
+ "LIBPATH",
78
+ "MACOSX_DEPLOYMENT_TARGET",
79
+ "OBJC_INCLUDE_PATH",
80
+ "PKG_CONFIG_ALLOW_SYSTEM_CFLAGS",
81
+ "PKG_CONFIG_ALLOW_SYSTEM_LIBS",
82
+ "PKG_CONFIG_LIBDIR",
83
+ "PKG_CONFIG_PATH",
84
+ "PKG_CONFIG_SYSROOT_DIR",
85
+ "PKG_CONFIG_TOP_BUILD_DIR",
86
+ "SDKROOT",
33
87
  ];
34
88
  const CONTRIBUTIONS_FILE_NAME = "ttsc_contributions.go";
35
89
  const CONTRIB_DIRNAME = "contrib";
90
+ const GLOBAL_CACHE_DIRNAME = "ttsc";
91
+ const PLUGIN_CACHE_DIRNAME = "plugins";
92
+ const CACHE_LAST_USED_FILE = ".last-used";
93
+ const CACHE_GC_MARKER_FILE = ".gc-last-run";
94
+ const GLOBAL_CACHE_GC_INTERVAL_MS = 24 * 60 * 60 * 1000;
95
+ const GLOBAL_CACHE_ENTRY_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
96
+ const GLOBAL_CACHE_MAX_BYTES = 2 * 1024 * 1024 * 1024;
97
+ const GLOBAL_CACHE_TARGET_BYTES = Math.floor(GLOBAL_CACHE_MAX_BYTES * 0.8);
98
+ const GLOBAL_CACHE_PROTECTED_AGE_MS = 60 * 60 * 1000;
36
99
 
37
100
  /** One contributor's resolved Go source plus its target sub-package name. */
38
101
  export interface ITtscBuildContributor {
@@ -56,7 +119,7 @@ export function buildSourcePlugin(opts: {
56
119
  tsgoVersion: string;
57
120
  }): string {
58
121
  const { dir, entry, source } = resolveSourceBuildTarget(opts);
59
- const overlayDirs = opts.overlayDirs ?? findTtscOverlayDirs();
122
+ const overlayDirs = [...(opts.overlayDirs ?? findTtscOverlayDirs())].sort();
60
123
  const contributors = opts.contributors ?? [];
61
124
  const goBinary = resolveGoCompiler();
62
125
  ensureExecutableGoToolchain(goBinary);
@@ -74,6 +137,7 @@ export function buildSourcePlugin(opts: {
74
137
  const binaryName = process.platform === "win32" ? "plugin.exe" : "plugin";
75
138
  const binaryPath = path.join(cacheDir, binaryName);
76
139
  if (fs.existsSync(binaryPath)) {
140
+ touchCacheEntry(cacheDir);
77
141
  return binaryPath;
78
142
  }
79
143
  fs.mkdirSync(cacheDir, { recursive: true });
@@ -111,6 +175,7 @@ export function buildSourcePlugin(opts: {
111
175
  runGoBuild(scratchDir, entry, scratchBinaryName, opts.pluginName, goBinary);
112
176
  const builtBinary = path.join(scratchDir, scratchBinaryName);
113
177
  publishBuiltBinary(builtBinary, binaryPath);
178
+ touchCacheEntry(cacheDir);
114
179
  return binaryPath;
115
180
  } finally {
116
181
  fs.rmSync(scratchDir, { recursive: true, force: true });
@@ -367,11 +432,22 @@ function writeGoWork(
367
432
  ): void {
368
433
  const goModReader = createGoModReader(goBinary, pluginName);
369
434
  validateSourceReplacements(scratchDir, useDirs, goModReader, pluginName);
435
+ const sourceInfo = goModReader.read(scratchDir);
436
+ const effectiveUseDirs =
437
+ sourceInfo.modulePath === TTSC_GO_MODULE_PATH
438
+ ? useDirs.filter((dir) => {
439
+ const modulePath = goModReader.read(dir).modulePath;
440
+ return modulePath !== null && !isTtscManagedModulePath(modulePath);
441
+ })
442
+ : useDirs;
370
443
  const useLines = ["\t."];
371
- for (const dir of useDirs) {
444
+ for (const dir of effectiveUseDirs) {
372
445
  useLines.push(`\t${dir.replace(/\\/g, "/")}`);
373
446
  }
374
- const replaceLines = sourceBuildWorkspaceReplacements(useDirs, goModReader);
447
+ const replaceLines = sourceBuildWorkspaceReplacements(
448
+ effectiveUseDirs,
449
+ goModReader,
450
+ );
375
451
  const replaceBlock =
376
452
  replaceLines.length === 0 ? "" : `\n\n${replaceLines.join("\n")}\n`;
377
453
  const goWork = `go 1.26\n\nuse (\n${useLines.join("\n")}\n)${replaceBlock}`;
@@ -601,6 +677,7 @@ function goToolchainNotFoundMessage(pluginName: string): string {
601
677
 
602
678
  function goBuildEnv(goBinary: string): NodeJS.ProcessEnv {
603
679
  const env = { ...process.env };
680
+ env.GOWORK = "auto";
604
681
  const goRoot = inferGoRoot(goBinary);
605
682
  if (goRoot && !env.GOROOT) {
606
683
  env.GOROOT = goRoot;
@@ -691,30 +768,63 @@ function walkForGoMod(dir: string, out: string[]): void {
691
768
  }
692
769
  }
693
770
 
694
- function resolvePluginCacheRoot(
771
+ export function resolvePluginCacheRoot(
695
772
  projectRoot: string,
696
773
  cacheDir?: string,
697
774
  ): string {
698
775
  if (cacheDir) {
699
- return path.resolve(projectRoot, cacheDir, "plugins");
776
+ return path.resolve(projectRoot, cacheDir, PLUGIN_CACHE_DIRNAME);
700
777
  }
701
778
  if (process.env.TTSC_CACHE_DIR) {
702
- return path.resolve(process.env.TTSC_CACHE_DIR, "plugins");
779
+ return path.resolve(process.env.TTSC_CACHE_DIR, PLUGIN_CACHE_DIRNAME);
703
780
  }
704
- const root = path.resolve(projectRoot);
705
- const nodeModules = path.join(root, "node_modules");
706
- if (isDirectory(nodeModules)) {
707
- return path.join(nodeModules, ".ttsc", "plugins");
708
- }
709
- return path.join(root, ".ttsc", "plugins");
781
+ const root = resolveGlobalPluginCacheRoot();
782
+ maybePruneGlobalPluginCache(root);
783
+ return root;
710
784
  }
711
785
 
712
- function isDirectory(candidate: string): boolean {
713
- try {
714
- return fs.statSync(candidate).isDirectory();
715
- } catch {
716
- return false;
786
+ export function resolveGlobalPluginCacheRoot(): string {
787
+ return path.join(
788
+ resolveUserCacheRoot(),
789
+ GLOBAL_CACHE_DIRNAME,
790
+ PLUGIN_CACHE_DIRNAME,
791
+ );
792
+ }
793
+
794
+ export function defaultPluginCacheCleanTargets(projectRoot: string): string[] {
795
+ return [
796
+ resolveGlobalPluginCacheRoot(),
797
+ path.join(projectRoot, "node_modules", ".ttsc"),
798
+ path.join(projectRoot, ".ttsc"),
799
+ ];
800
+ }
801
+
802
+ function resolveUserCacheRoot(): string {
803
+ const xdg = process.env.XDG_CACHE_HOME;
804
+ if (xdg && path.isAbsolute(xdg)) {
805
+ return xdg;
806
+ }
807
+ if (process.platform === "win32") {
808
+ const local = process.env.LOCALAPPDATA;
809
+ if (local && path.isAbsolute(local)) {
810
+ return local;
811
+ }
812
+ const home = os.homedir();
813
+ if (home) {
814
+ return path.join(home, "AppData", "Local");
815
+ }
816
+ }
817
+ if (process.platform === "darwin") {
818
+ const home = os.homedir();
819
+ if (home) {
820
+ return path.join(home, "Library", "Caches");
821
+ }
717
822
  }
823
+ const home = os.homedir();
824
+ if (home) {
825
+ return path.join(home, ".cache");
826
+ }
827
+ return path.join(os.tmpdir(), "ttsc-cache");
718
828
  }
719
829
 
720
830
  function resolveGoCompiler(): string {
@@ -785,17 +895,8 @@ export function computeCacheKey(inputs: {
785
895
  if (inputs.goBinary !== undefined) {
786
896
  hash.update(`go=${resolveGoCompilerIdentity(inputs.goBinary)}\n`);
787
897
  }
788
- // Hash the Go env vars that change `go build`'s output (cross-compile
789
- // target, build tags, cgo toggle, etc.). Without this, a user setting
790
- // `GOOS=linux GOARCH=arm64` on a darwin host would write a Linux-arm64
791
- // binary into the same cache slot as the native build, and the next
792
- // native invocation would spawn an unrunnable artifact.
793
- for (const key of GO_BUILD_ENV_KEYS) {
794
- const value = process.env[key];
795
- if (value !== undefined && value !== "") {
796
- hash.update(`${key}=${value}\n`);
797
- }
798
- }
898
+ hashGoBuildEnvironment(hash, inputs.goBinary, inputs.dir);
899
+ hashExternalGoBuildEnvironment(hash);
799
900
  hashSourceDirectory(hash, "plugin", inputs.dir);
800
901
  for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
801
902
  hashSourceDirectory(hash, `overlay:${index}`, dir);
@@ -879,22 +980,19 @@ function isHashableFile(name: string): boolean {
879
980
 
880
981
  function resolveGoCompilerIdentity(goBinary: string): string {
881
982
  const resolved = resolveExecutableIdentityPath(goBinary);
983
+ if (!fs.existsSync(resolved)) {
984
+ return "missing";
985
+ }
882
986
  const version = spawnSync(goBinary, ["version"], {
883
987
  encoding: "utf8",
884
988
  windowsHide: true,
885
989
  });
886
990
  const versionText =
887
991
  version.error !== undefined
888
- ? version.error.message
992
+ ? ((version.error as NodeJS.ErrnoException).code ?? version.error.message)
889
993
  : `${version.status ?? 0}:${version.stdout}${version.stderr}`;
890
- let statText = "";
891
- try {
892
- const stat = fs.statSync(resolved);
893
- statText = `${stat.size}:${stat.mtimeMs}`;
894
- } catch {
895
- statText = "missing";
896
- }
897
- return `${goBinary}:${resolved}:${statText}:${versionText}`;
994
+ const binaryHash = hashFile(resolved);
995
+ return `sha256:${binaryHash}:${versionText}`;
898
996
  }
899
997
 
900
998
  function resolveExecutableIdentityPath(binary: string): string {
@@ -904,7 +1002,7 @@ function resolveExecutableIdentityPath(binary: string): string {
904
1002
  if (binary.includes(path.sep)) {
905
1003
  return resolveRealPath(path.resolve(binary));
906
1004
  }
907
- for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
1005
+ for (const dir of readPathEnvironment().split(path.delimiter)) {
908
1006
  if (dir.length === 0) continue;
909
1007
  const candidate = path.join(dir, binary);
910
1008
  if (fs.existsSync(candidate)) {
@@ -920,6 +1018,10 @@ function resolveExecutableIdentityPath(binary: string): string {
920
1018
  return binary;
921
1019
  }
922
1020
 
1021
+ function readPathEnvironment(): string {
1022
+ return process.env.PATH ?? process.env.Path ?? "";
1023
+ }
1024
+
923
1025
  function resolveRealPath(location: string): string {
924
1026
  try {
925
1027
  return fs.realpathSync(location);
@@ -927,3 +1029,335 @@ function resolveRealPath(location: string): string {
927
1029
  return location;
928
1030
  }
929
1031
  }
1032
+
1033
+ function hashFile(file: string): string {
1034
+ const hash = crypto.createHash("sha256");
1035
+ hash.update(fs.readFileSync(file));
1036
+ return hash.digest("hex");
1037
+ }
1038
+
1039
+ function hashGoBuildEnvironment(
1040
+ hash: crypto.Hash,
1041
+ goBinary: string | undefined,
1042
+ cwd: string,
1043
+ ): void {
1044
+ const values = resolveGoBuildEnvironment(goBinary, cwd);
1045
+ for (const key of GO_BUILD_ENV_KEYS) {
1046
+ const value = values.get(key);
1047
+ if (value !== undefined && value !== "") {
1048
+ hash.update(`${key}=${value}\n`);
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ function resolveGoBuildEnvironment(
1054
+ goBinary: string | undefined,
1055
+ cwd: string,
1056
+ ): Map<string, string> {
1057
+ const values = new Map<string, string>();
1058
+ if (goBinary !== undefined) {
1059
+ const result = spawnSync(goBinary, ["env", "-json", ...GO_BUILD_ENV_KEYS], {
1060
+ cwd,
1061
+ encoding: "utf8",
1062
+ env: goBuildEnv(goBinary),
1063
+ maxBuffer: 1024 * 1024,
1064
+ windowsHide: true,
1065
+ });
1066
+ if (result.error === undefined && result.status === 0) {
1067
+ try {
1068
+ const parsed = JSON.parse(result.stdout) as Record<string, unknown>;
1069
+ for (const key of GO_BUILD_ENV_KEYS) {
1070
+ const raw = parsed[key];
1071
+ if (typeof raw === "string" && raw !== "") {
1072
+ values.set(key, normalizeGoBuildEnvValue(key, raw));
1073
+ }
1074
+ }
1075
+ } catch {
1076
+ // Fall back to process.env below; a cache key is still better than
1077
+ // failing before `go build` can produce the actionable error.
1078
+ }
1079
+ }
1080
+ }
1081
+ for (const key of GO_BUILD_ENV_KEYS) {
1082
+ if (values.has(key)) continue;
1083
+ const value = process.env[key];
1084
+ if (value !== undefined && value !== "") {
1085
+ values.set(key, normalizeGoBuildEnvValue(key, value));
1086
+ }
1087
+ }
1088
+ return values;
1089
+ }
1090
+
1091
+ function normalizeGoBuildEnvValue(key: string, value: string): string {
1092
+ if (key === "GOROOT") {
1093
+ return resolveGoRootCacheIdentity(value);
1094
+ }
1095
+ if (GO_BUILD_COMMAND_ENV_KEYS.has(key)) {
1096
+ return `${value}\0${resolveCommandCacheIdentity(value)}`;
1097
+ }
1098
+ return value;
1099
+ }
1100
+
1101
+ function resolveCommandCacheIdentity(command: string): string {
1102
+ const executable = firstCommandToken(command);
1103
+ if (executable === null) {
1104
+ return "command:empty";
1105
+ }
1106
+ const resolved = resolveExecutableIdentityPath(executable);
1107
+ if (!fs.existsSync(resolved)) {
1108
+ return `command:missing:${executable}`;
1109
+ }
1110
+ try {
1111
+ return `command:sha256:${hashFile(resolved)}`;
1112
+ } catch {
1113
+ return `command:unreadable:${resolved}`;
1114
+ }
1115
+ }
1116
+
1117
+ function firstCommandToken(command: string): string | null {
1118
+ const trimmed = command.trim();
1119
+ if (trimmed === "") {
1120
+ return null;
1121
+ }
1122
+ const quote = trimmed[0];
1123
+ if (quote === "'" || quote === '"') {
1124
+ const end = trimmed.indexOf(quote, 1);
1125
+ return end === -1 ? trimmed.slice(1) : trimmed.slice(1, end);
1126
+ }
1127
+ return trimmed.split(/\s+/)[0] ?? null;
1128
+ }
1129
+
1130
+ function hashExternalGoBuildEnvironment(hash: crypto.Hash): void {
1131
+ for (const key of EXTERNAL_GO_BUILD_ENV_KEYS) {
1132
+ const value = process.env[key];
1133
+ if (value !== undefined && value !== "") {
1134
+ hash.update(`${key}=${value}\n`);
1135
+ }
1136
+ }
1137
+ }
1138
+
1139
+ function resolveGoRootCacheIdentity(goRoot: string): string {
1140
+ const resolved = resolveRealPath(goRoot);
1141
+ if (!fs.existsSync(resolved)) {
1142
+ return `missing:${goRoot}`;
1143
+ }
1144
+ const hash = crypto.createHash("sha256");
1145
+ for (const file of collectGoRootIdentityFiles(resolved)) {
1146
+ const relative = path.relative(resolved, file).replace(/\\/g, "/");
1147
+ hash.update(`f=${relative}\n`);
1148
+ hash.update(fs.readFileSync(file));
1149
+ hash.update("\n");
1150
+ }
1151
+ return `sha256:${hash.digest("hex")}`;
1152
+ }
1153
+
1154
+ function collectGoRootIdentityFiles(root: string): string[] {
1155
+ const out: string[] = [];
1156
+ walkGoRootIdentity(root, root, out);
1157
+ out.sort();
1158
+ return out;
1159
+ }
1160
+
1161
+ function walkGoRootIdentity(root: string, dir: string, out: string[]): void {
1162
+ let entries: fs.Dirent[];
1163
+ try {
1164
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1165
+ } catch {
1166
+ return;
1167
+ }
1168
+ for (const entry of entries) {
1169
+ const file = path.join(dir, entry.name);
1170
+ const rel = path.relative(root, file).replace(/\\/g, "/");
1171
+ if (entry.isDirectory()) {
1172
+ if (shouldHashGoRootPath(rel, true)) {
1173
+ walkGoRootIdentity(root, file, out);
1174
+ }
1175
+ } else if (entry.isFile() && shouldHashGoRootPath(rel, false)) {
1176
+ out.push(file);
1177
+ }
1178
+ }
1179
+ }
1180
+
1181
+ function shouldHashGoRootPath(rel: string, isDir: boolean): boolean {
1182
+ if (rel === "") return true;
1183
+ const parts = rel.split("/");
1184
+ if (parts.includes(".git") || parts.includes("testdata")) return false;
1185
+ if (!isDir && rel.endsWith("_test.go")) return false;
1186
+
1187
+ const first = parts[0]!;
1188
+ if (parts.length === 1) {
1189
+ if (isDir) return ["bin", "pkg", "src", "lib"].includes(first);
1190
+ return ["VERSION", "go.env"].includes(first);
1191
+ }
1192
+ if (first === "bin") {
1193
+ if (isDir) return true;
1194
+ const base = path.basename(rel);
1195
+ return (
1196
+ base === "go" ||
1197
+ base === "go.exe" ||
1198
+ base === "gofmt" ||
1199
+ base === "gofmt.exe"
1200
+ );
1201
+ }
1202
+ if (first === "pkg") {
1203
+ const second = parts[1]!;
1204
+ return second === "tool" || second === "include";
1205
+ }
1206
+ if (first === "src") {
1207
+ if (!isDir && parts.length === 2) {
1208
+ return ["go.mod", "go.sum"].includes(parts[1]!);
1209
+ }
1210
+ return parts[1] !== "cmd";
1211
+ }
1212
+ if (first === "lib") {
1213
+ return parts[1] === "time";
1214
+ }
1215
+ return false;
1216
+ }
1217
+
1218
+ function touchCacheEntry(cacheDir: string): void {
1219
+ try {
1220
+ fs.mkdirSync(cacheDir, { recursive: true });
1221
+ fs.writeFileSync(
1222
+ path.join(cacheDir, CACHE_LAST_USED_FILE),
1223
+ `${Date.now()}\n`,
1224
+ );
1225
+ } catch {
1226
+ // Cache hits must not fail because metadata touch failed.
1227
+ }
1228
+ }
1229
+
1230
+ function maybePruneGlobalPluginCache(root: string): void {
1231
+ try {
1232
+ fs.mkdirSync(root, { recursive: true });
1233
+ const marker = path.join(root, CACHE_GC_MARKER_FILE);
1234
+ const now = Date.now();
1235
+ const lastRun = readTimestamp(marker);
1236
+ if (lastRun !== null && now - lastRun < GLOBAL_CACHE_GC_INTERVAL_MS) {
1237
+ return;
1238
+ }
1239
+ fs.writeFileSync(marker, `${now}\n`);
1240
+ pruneGlobalPluginCache(root, now);
1241
+ } catch {
1242
+ // Global cache GC is opportunistic; builds still proceed when it fails.
1243
+ }
1244
+ }
1245
+
1246
+ function pruneGlobalPluginCache(root: string, now: number): void {
1247
+ const entries = collectGlobalCacheEntries(root, now);
1248
+ for (const entry of entries) {
1249
+ if (now - entry.lastUsedAt <= GLOBAL_CACHE_ENTRY_MAX_AGE_MS) {
1250
+ continue;
1251
+ }
1252
+ removeCacheEntry(entry);
1253
+ }
1254
+
1255
+ const remaining = collectGlobalCacheEntries(root, now);
1256
+ let total = remaining.reduce((sum, entry) => sum + entry.size, 0);
1257
+ if (total <= GLOBAL_CACHE_MAX_BYTES) {
1258
+ return;
1259
+ }
1260
+ for (const entry of remaining.sort((a, b) => a.lastUsedAt - b.lastUsedAt)) {
1261
+ if (total <= GLOBAL_CACHE_TARGET_BYTES) {
1262
+ return;
1263
+ }
1264
+ if (now - entry.lastUsedAt <= GLOBAL_CACHE_PROTECTED_AGE_MS) {
1265
+ continue;
1266
+ }
1267
+ removeCacheEntry(entry);
1268
+ total -= entry.size;
1269
+ }
1270
+ }
1271
+
1272
+ interface GlobalCacheEntry {
1273
+ dir: string;
1274
+ lastUsedAt: number;
1275
+ size: number;
1276
+ }
1277
+
1278
+ function collectGlobalCacheEntries(
1279
+ root: string,
1280
+ now: number,
1281
+ ): GlobalCacheEntry[] {
1282
+ const entries: GlobalCacheEntry[] = [];
1283
+ let dirents: fs.Dirent[];
1284
+ try {
1285
+ dirents = fs.readdirSync(root, { withFileTypes: true });
1286
+ } catch {
1287
+ return entries;
1288
+ }
1289
+ for (const dirent of dirents) {
1290
+ if (!dirent.isDirectory()) {
1291
+ continue;
1292
+ }
1293
+ const dir = path.join(root, dirent.name);
1294
+ const lastUsedAt = readCacheEntryLastUsedAt(dir, now);
1295
+ entries.push({
1296
+ dir,
1297
+ lastUsedAt,
1298
+ size: directorySize(dir),
1299
+ });
1300
+ }
1301
+ return entries;
1302
+ }
1303
+
1304
+ function readCacheEntryLastUsedAt(dir: string, now: number): number {
1305
+ const touched = readTimestamp(path.join(dir, CACHE_LAST_USED_FILE));
1306
+ if (touched !== null) {
1307
+ return touched;
1308
+ }
1309
+ for (const name of ["plugin", "plugin.exe"]) {
1310
+ try {
1311
+ return fs.statSync(path.join(dir, name)).mtimeMs;
1312
+ } catch {}
1313
+ }
1314
+ try {
1315
+ return fs.statSync(dir).mtimeMs;
1316
+ } catch {
1317
+ return now;
1318
+ }
1319
+ }
1320
+
1321
+ function readTimestamp(file: string): number | null {
1322
+ try {
1323
+ const text = fs.readFileSync(file, "utf8").trim();
1324
+ const value = Number(text);
1325
+ if (Number.isFinite(value)) {
1326
+ return value;
1327
+ }
1328
+ } catch {}
1329
+ try {
1330
+ return fs.statSync(file).mtimeMs;
1331
+ } catch {
1332
+ return null;
1333
+ }
1334
+ }
1335
+
1336
+ function directorySize(dir: string): number {
1337
+ let total = 0;
1338
+ let entries: fs.Dirent[];
1339
+ try {
1340
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1341
+ } catch {
1342
+ return total;
1343
+ }
1344
+ for (const entry of entries) {
1345
+ const file = path.join(dir, entry.name);
1346
+ try {
1347
+ if (entry.isDirectory()) {
1348
+ total += directorySize(file);
1349
+ } else if (entry.isFile()) {
1350
+ total += fs.statSync(file).size;
1351
+ }
1352
+ } catch {}
1353
+ }
1354
+ return total;
1355
+ }
1356
+
1357
+ function removeCacheEntry(entry: GlobalCacheEntry): void {
1358
+ try {
1359
+ fs.rmSync(entry.dir, { recursive: true, force: true });
1360
+ } catch {
1361
+ // Windows may reject removal while a plugin binary is still running.
1362
+ }
1363
+ }