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.
Files changed (170) hide show
  1. package/README.md +9 -0
  2. package/cmd/graphbench/main.go +26 -16
  3. package/cmd/graphdump/main.go +37 -12
  4. package/cmd/graphdump/run_declares_the_capability_its_output_carries_test.go +137 -0
  5. package/cmd/graphdump/run_reports_an_unknown_flag_without_exiting_the_process_test.go +56 -0
  6. package/cmd/graphdump/run_reports_an_unloadable_project_without_printing_a_dump_test.go +46 -0
  7. package/cmd/ttsc/api_transform.go +28 -3
  8. package/cmd/ttscgraph/auxiliary_identity_state_tracks_retargets_not_source_contents_test.go +85 -0
  9. package/cmd/ttscgraph/disk_realpath_other.go +16 -0
  10. package/cmd/ttscgraph/disk_realpath_windows.go +56 -0
  11. package/cmd/ttscgraph/dump.go +43 -11
  12. package/cmd/ttscgraph/run_dump_claims_artifacts_only_when_it_asked_test.go +135 -0
  13. package/cmd/ttscgraph/run_dump_declares_the_capability_its_output_carries_test.go +96 -0
  14. package/cmd/ttscgraph/serve.go +357 -60
  15. package/cmd/ttscgraph/serve_adopts_a_republished_artifact_set_test.go +348 -0
  16. package/cmd/ttscgraph/serve_projects_an_artifact_no_source_owns_test.go +90 -0
  17. package/cmd/ttscgraph/serve_shards.go +31 -9
  18. package/driver/config_discovery.go +121 -0
  19. package/driver/emit_plugin.go +77 -11
  20. package/driver/graph.go +64 -11
  21. package/driver/input_observation.go +273 -0
  22. package/driver/input_observation_test.go +149 -0
  23. package/driver/plugins.go +506 -15
  24. package/driver/program.go +54 -1
  25. package/driver/resolution_candidates.go +13 -5
  26. package/driver/rewrite.go +45 -2
  27. package/driver/transform_dependencies.go +283 -0
  28. package/driver/transform_dependencies_declares_every_file_without_a_contributor_test.go +28 -0
  29. package/driver/transform_dependencies_drops_a_self_reported_dependency_test.go +21 -0
  30. package/driver/transform_dependencies_keys_a_reported_path_like_the_envelope_test.go +46 -0
  31. package/driver/transform_dependencies_requires_every_contributor_to_declare_test.go +38 -0
  32. package/driver/transform_dependencies_stays_silent_for_an_undeclared_contributor_test.go +23 -0
  33. package/driver/transform_dependencies_withdraws_completeness_after_an_unusable_dependency_test.go +37 -0
  34. package/internal/graph/artifacts.go +230 -0
  35. package/internal/graph/artifacts_index_what_a_plugin_published_test.go +154 -0
  36. package/internal/graph/artifacts_survive_a_partial_rebuild_test.go +89 -0
  37. package/internal/graph/build.go +35 -0
  38. package/internal/graph/build_releases_the_scratch_it_no_longer_reads_test.go +118 -0
  39. package/internal/graph/doc_refs_are_the_use_the_checker_already_counts_test.go +93 -0
  40. package/internal/graph/doc_refs_resolve_documentation_links_test.go +335 -0
  41. package/internal/graph/doc_tags.go +416 -0
  42. package/internal/graph/doc_tags_are_claimed_before_they_are_read_test.go +131 -0
  43. package/internal/graph/doc_tags_capture_every_unrecognized_tag_test.go +165 -0
  44. package/internal/graph/doc_tags_read_only_what_the_parser_attached_test.go +97 -0
  45. package/internal/graph/doc_tags_stop_at_the_external_boundary_test.go +59 -0
  46. package/internal/graph/doc_tags_survive_line_endings_and_repeated_blocks_test.go +146 -0
  47. package/internal/graph/dump.go +60 -13
  48. package/internal/graph/dump_checkout_paths_are_stable_test.go +2 -2
  49. package/internal/graph/dump_paths.go +2 -2
  50. package/internal/graph/dump_paths_are_portable_and_injective_test.go +1 -1
  51. package/internal/graph/edge_kinds_names_every_declared_kind_test.go +49 -0
  52. package/internal/graph/edges.go +1 -0
  53. package/internal/graph/graph.go +79 -4
  54. package/internal/graph/provenance.go +29 -2
  55. package/lib/TtscCompiler.d.ts +12 -11
  56. package/lib/TtscCompiler.js +44 -29
  57. package/lib/TtscCompiler.js.map +1 -1
  58. package/lib/compiler/internal/captureProcessOutput.js +2 -2
  59. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  60. package/lib/compiler/internal/compileProjectInMemory.js +4 -3
  61. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  62. package/lib/compiler/internal/project/createNativeProjectContextArgs.d.ts +9 -0
  63. package/lib/compiler/internal/project/createNativeProjectContextArgs.js +16 -4
  64. package/lib/compiler/internal/project/createNativeProjectContextArgs.js.map +1 -1
  65. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -0
  66. package/lib/compiler/internal/resolveEmittedJavaScript.js +3 -0
  67. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  68. package/lib/compiler/internal/runBuild.d.ts +1 -1
  69. package/lib/compiler/internal/runBuild.js +80 -34
  70. package/lib/compiler/internal/runBuild.js.map +1 -1
  71. package/lib/compiler/internal/runSingleFileEmit.js +2 -2
  72. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  73. package/lib/compiler/internal/sharedHostHelpers.d.ts +28 -0
  74. package/lib/compiler/internal/sharedHostHelpers.js +39 -0
  75. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  76. package/lib/compiler/internal/startResidentTransform.js +12 -4
  77. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  78. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -0
  79. package/lib/compiler/internal/transformProjectInMemory.js +239 -35
  80. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  81. package/lib/flags/schema.js +12 -5
  82. package/lib/flags/schema.js.map +1 -1
  83. package/lib/index.d.ts +1 -0
  84. package/lib/index.js +1 -0
  85. package/lib/index.js.map +1 -1
  86. package/lib/internal/createCanonicalTempDirectory.d.ts +17 -0
  87. package/lib/internal/createCanonicalTempDirectory.js +42 -0
  88. package/lib/internal/createCanonicalTempDirectory.js.map +1 -0
  89. package/lib/internal/projectInputPathIdentity.d.ts +3 -0
  90. package/lib/internal/projectInputPathIdentity.js +11 -5
  91. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  92. package/lib/internal/resolveNodeBinary.d.ts +14 -0
  93. package/lib/internal/resolveNodeBinary.js +150 -0
  94. package/lib/internal/resolveNodeBinary.js.map +1 -0
  95. package/lib/internal/resolveSafeCacheCleanupTargets.d.ts +15 -0
  96. package/lib/internal/resolveSafeCacheCleanupTargets.js +61 -0
  97. package/lib/internal/resolveSafeCacheCleanupTargets.js.map +1 -0
  98. package/lib/launcher/internal/prepareExecution.d.ts +3 -0
  99. package/lib/launcher/internal/prepareExecution.js +42 -14
  100. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  101. package/lib/launcher/internal/runTtsc.js +25 -13
  102. package/lib/launcher/internal/runTtsc.js.map +1 -1
  103. package/lib/launcher/internal/runTtscserver.js +18 -3
  104. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  105. package/lib/launcher/internal/runTtsx.js +2 -0
  106. package/lib/launcher/internal/runTtsx.js.map +1 -1
  107. package/lib/launcher/internal/runtimeHooks.d.ts +47 -3
  108. package/lib/launcher/internal/runtimeHooks.js +746 -64
  109. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  110. package/lib/plugin/internal/buildSourcePlugin.d.ts +62 -8
  111. package/lib/plugin/internal/buildSourcePlugin.js +754 -72
  112. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  113. package/lib/plugin/internal/capabilityResolutionCache.d.ts +76 -0
  114. package/lib/plugin/internal/capabilityResolutionCache.js +224 -0
  115. package/lib/plugin/internal/capabilityResolutionCache.js.map +1 -0
  116. package/lib/plugin/internal/loadProjectPlugins.d.ts +17 -5
  117. package/lib/plugin/internal/loadProjectPlugins.js +1339 -77
  118. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  119. package/lib/plugin/resolveCapabilityPlugins.d.ts +55 -0
  120. package/lib/plugin/resolveCapabilityPlugins.js +118 -0
  121. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -0
  122. package/lib/register.d.ts +1 -0
  123. package/lib/register.js +46 -0
  124. package/lib/register.js.map +1 -0
  125. package/lib/structures/ITtscCompilerContext.d.ts +6 -3
  126. package/lib/structures/ITtscCompilerTransformation.d.ts +38 -0
  127. package/lib/structures/ITtscPlugin.d.ts +33 -0
  128. package/package.json +16 -9
  129. package/shim/ast/parent.go +16 -3
  130. package/shim/checker/enums_gen.go +11 -0
  131. package/shim/checker/go.mod +9 -0
  132. package/shim/checker/go.sum +22 -0
  133. package/shim/checker/shim.go +78 -0
  134. package/shim/compiler/emithook.go +10 -0
  135. package/shim/compiler/incremental.go +45 -8
  136. package/src/TtscCompiler.ts +55 -33
  137. package/src/compiler/internal/captureProcessOutput.ts +3 -2
  138. package/src/compiler/internal/compileProjectInMemory.ts +4 -3
  139. package/src/compiler/internal/project/createNativeProjectContextArgs.ts +19 -4
  140. package/src/compiler/internal/resolveEmittedJavaScript.ts +6 -0
  141. package/src/compiler/internal/runBuild.ts +102 -39
  142. package/src/compiler/internal/runSingleFileEmit.ts +2 -2
  143. package/src/compiler/internal/sharedHostHelpers.ts +44 -0
  144. package/src/compiler/internal/startResidentTransform.ts +12 -3
  145. package/src/compiler/internal/transformProjectInMemory.ts +410 -39
  146. package/src/flags/schema.ts +12 -5
  147. package/src/index.ts +1 -0
  148. package/src/internal/createCanonicalTempDirectory.ts +53 -0
  149. package/src/internal/projectInputPathIdentity.ts +17 -4
  150. package/src/internal/resolveNodeBinary.ts +173 -0
  151. package/src/internal/resolveSafeCacheCleanupTargets.ts +85 -0
  152. package/src/launcher/internal/prepareExecution.ts +50 -14
  153. package/src/launcher/internal/runTtsc.ts +24 -13
  154. package/src/launcher/internal/runTtscserver.ts +18 -3
  155. package/src/launcher/internal/runTtsx.ts +2 -0
  156. package/src/launcher/internal/runtimeHooks.ts +934 -68
  157. package/src/plugin/internal/buildSourcePlugin.ts +984 -79
  158. package/src/plugin/internal/capabilityResolutionCache.ts +305 -0
  159. package/src/plugin/internal/loadProjectPlugins.ts +1718 -104
  160. package/src/plugin/resolveCapabilityPlugins.ts +171 -0
  161. package/src/register.ts +47 -0
  162. package/src/structures/ITtscCompilerContext.ts +6 -3
  163. package/src/structures/ITtscCompilerTransformation.ts +46 -0
  164. package/src/structures/ITtscPlugin.ts +36 -0
  165. package/tools/shim_audit/baseline.json +0 -2
  166. package/utility/host.go +18 -3
  167. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +0 -5
  168. package/lib/internal/assertSafeExplicitCacheDirectory.js +0 -26
  169. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +0 -1
  170. package/src/internal/assertSafeExplicitCacheDirectory.ts +0 -29
@@ -14,12 +14,16 @@ exports.formatGoWorkPath = formatGoWorkPath;
14
14
  exports.autoQuoteGoModToken = autoQuoteGoModToken;
15
15
  exports.spawnGoTool = spawnGoTool;
16
16
  exports.windowsGoCommandArgs = windowsGoCommandArgs;
17
+ exports.ensureExecutableGoToolchain = ensureExecutableGoToolchain;
17
18
  exports.resolvePluginCacheRoot = resolvePluginCacheRoot;
18
19
  exports.resolveSourceBuildCachePaths = resolveSourceBuildCachePaths;
19
20
  exports.resolveCleanTargets = resolveCleanTargets;
20
21
  exports.legacyGlobalCacheTargets = legacyGlobalCacheTargets;
21
22
  exports.isPathWithin = isPathWithin;
22
23
  exports.computeCacheKey = computeCacheKey;
24
+ exports.prunePluginCacheRoot = prunePluginCacheRoot;
25
+ exports.pruneGoBuildCacheRoot = pruneGoBuildCacheRoot;
26
+ exports.withGoBuildCacheLease = withGoBuildCacheLease;
23
27
  const node_child_process_1 = require("node:child_process");
24
28
  const node_crypto_1 = __importDefault(require("node:crypto"));
25
29
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -27,8 +31,10 @@ const node_module_1 = require("node:module");
27
31
  const node_os_1 = __importDefault(require("node:os"));
28
32
  const node_path_1 = __importDefault(require("node:path"));
29
33
  const node_url_1 = require("node:url");
34
+ const node_worker_threads_1 = require("node:worker_threads");
30
35
  const captureProcessOutput_1 = require("../../compiler/internal/captureProcessOutput");
31
36
  const paths_1 = require("../../compiler/internal/paths");
37
+ const createCanonicalTempDirectory_1 = require("../../internal/createCanonicalTempDirectory");
32
38
  const GO_MOD_SEARCH_MAX_DEPTH = 3;
33
39
  const TTSC_GO_MODULE_PATH = "github.com/samchon/ttsc/packages/ttsc";
34
40
  const TSGO_GO_MODULE_PATH = "github.com/microsoft/typescript-go";
@@ -161,6 +167,29 @@ const PLUGIN_CACHE_ENTRY_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
161
167
  const PLUGIN_CACHE_MAX_BYTES = 2 * 1024 * 1024 * 1024;
162
168
  const PLUGIN_CACHE_TARGET_BYTES = Math.floor(PLUGIN_CACHE_MAX_BYTES * 0.8);
163
169
  const PLUGIN_CACHE_PROTECTED_AGE_MS = 60 * 60 * 1000;
170
+ // Go's own object-cache trim is age-based and has no size ceiling. The default
171
+ // ttsc-owned cache therefore keeps up to 8 GiB and trims oldest objects toward
172
+ // 6 GiB. The newest target-sized set used within an hour remains protected so
173
+ // crossing the ceiling cannot immediately force another cold build.
174
+ const GO_BUILD_CACHE_GC_INTERVAL_MS = 24 * 60 * 60 * 1000;
175
+ const GO_BUILD_CACHE_MAX_BYTES = 8 * 1024 * 1024 * 1024;
176
+ const GO_BUILD_CACHE_TARGET_BYTES = 6 * 1024 * 1024 * 1024;
177
+ const GO_BUILD_CACHE_PROTECTED_AGE_MS = 60 * 60 * 1000;
178
+ const GO_BUILD_CACHE_GC_MARKER_FILE = ".ttsc-gc";
179
+ const GO_BUILD_CACHE_LEASE_DIR = ".ttsc-build-leases";
180
+ const GO_BUILD_CACHE_MAINTENANCE_DIR = ".ttsc-maintenance";
181
+ const GO_BUILD_CACHE_COORDINATION_STALE_MS = 60 * 60 * 1000;
182
+ const GO_BUILD_CACHE_MAINTENANCE_STALE_MS = 60 * 1000;
183
+ const GO_BUILD_CACHE_COORDINATION_CLOCK_SKEW_MS = 5 * 60 * 1000;
184
+ const GO_BUILD_CACHE_COORDINATION_HEARTBEAT_MS = 5_000;
185
+ const GO_BUILD_CACHE_COORDINATION_POLL_MS = 25;
186
+ // Go amortizes cache-hit mtime writes over one hour. An object used inside
187
+ // ttsc's one-hour protection window may therefore still carry an mtime almost
188
+ // one additional hour old.
189
+ const GO_BUILD_CACHE_ACCESS_MTIME_GRANULARITY_MS = 60 * 60 * 1000;
190
+ const DEFAULT_SOURCE_BUILD_FILESYSTEM = Object.freeze({
191
+ readFile: (location) => node_fs_1.default.readFileSync(location),
192
+ });
164
193
  /**
165
194
  * Build one Go source plugin into a cached executable.
166
195
  *
@@ -176,21 +205,30 @@ function buildSourcePlugin(opts) {
176
205
  const { dir, entry, source } = resolveSourceBuildTarget(opts);
177
206
  const overlayDirs = [...(opts.overlayDirs ?? findTtscOverlayDirs())].sort();
178
207
  const contributors = opts.contributors ?? [];
179
- const goBinary = resolveGoToolForBuild(resolveGoCompiler(env), env, dir);
180
- ensureExecutableGoToolchain(goBinary);
208
+ const compiler = resolveGoCompiler(env);
209
+ const goBinary = resolveGoToolForBuild(compiler.binary, env, dir);
210
+ ensureExecutableGoToolchain(goBinary, compiler.bundled);
181
211
  const key = computeCacheKey({
182
212
  contributors,
183
213
  dir,
184
214
  entry,
185
215
  env,
216
+ filesystem: opts.filesystem,
186
217
  goBinary,
187
218
  overlayDirs,
188
219
  ttscVersion: opts.ttscVersion,
189
220
  tsgoVersion: opts.tsgoVersion,
190
221
  });
191
222
  const paths = resolveSourceBuildCachePaths(opts.baseDir, opts.cacheDir, env);
192
- maybePrunePluginCache(paths, opts.cacheDir, env);
193
- const cacheDir = node_path_1.default.join(paths.pluginRoot, key);
223
+ const managePluginCache = !opts.cacheDir && !env.TTSC_CACHE_DIR;
224
+ const manageGoBuildCache = shouldManageSourceBuildCaches(paths, opts.cacheDir, env);
225
+ const pluginRoot = managePluginCache
226
+ ? canonicalPluginCacheRoot(paths.pluginRoot)
227
+ : paths.pluginRoot;
228
+ maybePruneSourceBuildCaches({ ...paths, pluginRoot }, opts.cacheDir, env);
229
+ const cacheDir = managePluginCache
230
+ ? canonicalPluginCacheEntry(pluginRoot, key)
231
+ : node_path_1.default.join(pluginRoot, key);
194
232
  const binaryName = process.platform === "win32" ? "plugin.exe" : "plugin";
195
233
  const binaryPath = node_path_1.default.join(cacheDir, binaryName);
196
234
  if (node_fs_1.default.existsSync(binaryPath)) {
@@ -200,7 +238,7 @@ function buildSourcePlugin(opts) {
200
238
  node_fs_1.default.mkdirSync(cacheDir, { recursive: true });
201
239
  const label = opts.label ?? "source plugin";
202
240
  const quiet = opts.quiet === true;
203
- return buildUnderPluginLock(cacheDir, binaryPath, { label, pluginName: opts.pluginName, quiet }, () => compileSourcePlugin({
241
+ const built = buildUnderPluginLock(cacheDir, binaryPath, { label, pluginName: opts.pluginName, quiet }, () => compileSourcePlugin({
204
242
  binaryPath,
205
243
  cacheDir,
206
244
  contributors,
@@ -208,14 +246,26 @@ function buildSourcePlugin(opts) {
208
246
  entry,
209
247
  env,
210
248
  goBinary,
249
+ normalizeGoToolPermissions: compiler.bundled,
211
250
  key,
212
251
  label,
213
252
  goBuildCacheRoot: paths.goBuildRoot,
253
+ manageGoBuildCache,
214
254
  overlayDirs,
215
255
  pluginName: opts.pluginName,
216
256
  quiet,
217
257
  source,
218
258
  }));
259
+ if (managePluginCache) {
260
+ // The pre-build daily pass cannot account for the binary this cold build
261
+ // just published. Enforce the size policy after publication, once this
262
+ // process has released its per-key build lock.
263
+ prunePluginCacheRoot(pluginRoot, {
264
+ force: true,
265
+ protectedEntries: [cacheDir],
266
+ });
267
+ }
268
+ return built;
219
269
  }
220
270
  /** Run the actual `go build` and publish the binary; assumes the lock is held. */
221
271
  function compileSourcePlugin(opts) {
@@ -229,7 +279,7 @@ function compileSourcePlugin(opts) {
229
279
  `(this runs once per cache key and can take several minutes on a cold Go cache) ` +
230
280
  `See https://ttsc.dev/docs/ttsc/compile#plugin-cache to persist it across builds.\n`);
231
281
  }
232
- const scratchDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), `ttsc-plugin-${opts.key}-`));
282
+ const scratchDir = (0, createCanonicalTempDirectory_1.createCanonicalTempDirectory)(`ttsc-plugin-${opts.key}-`);
233
283
  try {
234
284
  materializeScratchDir(opts.dir, scratchDir);
235
285
  const goModReader = createGoModReader(opts.goBinary, opts.pluginName, opts.env);
@@ -244,7 +294,22 @@ function compileSourcePlugin(opts) {
244
294
  }
245
295
  writeGoWork(scratchDir, opts.overlayDirs, opts.goBinary, opts.pluginName, opts.env);
246
296
  const scratchBinaryName = process.platform === "win32" ? ".ttsc-plugin.exe" : ".ttsc-plugin";
247
- runGoBuild(scratchDir, opts.entry, scratchBinaryName, opts.pluginName, opts.goBinary, opts.goBuildCacheRoot, opts.env);
297
+ let attemptedGoBuildCacheRoot;
298
+ try {
299
+ withGoBuildCacheLease(opts.goBuildCacheRoot, opts.manageGoBuildCache, (goBuildCacheRoot) => {
300
+ attemptedGoBuildCacheRoot = goBuildCacheRoot;
301
+ runGoBuild(scratchDir, opts.entry, scratchBinaryName, opts.pluginName, opts.goBinary, goBuildCacheRoot, opts.env, opts.normalizeGoToolPermissions);
302
+ });
303
+ }
304
+ finally {
305
+ if (opts.manageGoBuildCache && attemptedGoBuildCacheRoot !== undefined) {
306
+ // The daily pre-build pass cannot see objects the build is about to
307
+ // add, including objects left behind by a failed compile. Enforce the
308
+ // size policy after every actual cold-build attempt so churn cannot
309
+ // grow the cache unchecked behind a fresh daily marker.
310
+ pruneGoBuildCacheRoot(attemptedGoBuildCacheRoot, { force: true });
311
+ }
312
+ }
248
313
  const builtBinary = node_path_1.default.join(scratchDir, scratchBinaryName);
249
314
  publishBuiltBinary(builtBinary, opts.binaryPath);
250
315
  touchCacheEntry(opts.cacheDir);
@@ -431,6 +496,9 @@ function ensurePluginBuildLockProtocol(protocolDir) {
431
496
  }
432
497
  function isPluginBuildLockProtocolV2(lockDir) {
433
498
  try {
499
+ const stats = node_fs_1.default.lstatSync(lockDir);
500
+ if (!stats.isDirectory() || stats.isSymbolicLink())
501
+ return false;
434
502
  return (node_fs_1.default.readFileSync(node_path_1.default.join(lockDir, PLUGIN_BUILD_LOCK_PROTOCOL_FILE), "utf8") === PLUGIN_BUILD_LOCK_PROTOCOL);
435
503
  }
436
504
  catch {
@@ -794,7 +862,10 @@ function pluginBuildLockAgeMs(lockDir, now) {
794
862
  }
795
863
  function pluginBuildLockPathExists(lockDir) {
796
864
  try {
797
- node_fs_1.default.statSync(lockDir);
865
+ const stats = node_fs_1.default.lstatSync(lockDir);
866
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
867
+ throw new Error(`ttsc: unsafe plugin build lock path: ${lockDir}`);
868
+ }
798
869
  return true;
799
870
  }
800
871
  catch (error) {
@@ -1293,8 +1364,8 @@ function isTtscManagedModulePath(modulePath) {
1293
1364
  modulePath === TSGO_GO_MODULE_PATH ||
1294
1365
  modulePath.startsWith("github.com/microsoft/typescript-go/shim/"));
1295
1366
  }
1296
- function runGoBuild(cwd, entry, binaryName, pluginName, goBinary, goBuildCacheRoot, env) {
1297
- ensureExecutableGoToolchain(goBinary);
1367
+ function runGoBuild(cwd, entry, binaryName, pluginName, goBinary, goBuildCacheRoot, env, normalizeGoToolPermissions) {
1368
+ ensureExecutableGoToolchain(goBinary, normalizeGoToolPermissions);
1298
1369
  const result = spawnGoTool(goBinary, ["build", "-o", binaryName, entry], {
1299
1370
  cwd,
1300
1371
  encoding: "utf8",
@@ -1428,30 +1499,42 @@ function inferGoRoot(goBinary) {
1428
1499
  const goRoot = node_path_1.default.dirname(binDir);
1429
1500
  return node_fs_1.default.existsSync(node_path_1.default.join(goRoot, "src", "runtime")) ? goRoot : null;
1430
1501
  }
1431
- function ensureExecutableGoToolchain(goBinary) {
1502
+ function ensureExecutableGoToolchain(goBinary, normalizeBundledPermissions) {
1432
1503
  if (process.platform === "win32")
1433
1504
  return;
1434
1505
  if (!node_path_1.default.isAbsolute(goBinary) || !node_fs_1.default.existsSync(goBinary))
1435
1506
  return;
1436
1507
  try {
1437
- node_fs_1.default.chmodSync(goBinary, 0o755);
1508
+ ensureExecutableFile(goBinary, normalizeBundledPermissions);
1438
1509
  const goRoot = inferGoRoot(goBinary);
1439
1510
  if (!goRoot)
1440
1511
  return;
1441
1512
  const gofmt = node_path_1.default.join(node_path_1.default.dirname(goBinary), "gofmt");
1442
- if (node_fs_1.default.existsSync(gofmt))
1443
- node_fs_1.default.chmodSync(gofmt, 0o755);
1513
+ if (node_fs_1.default.existsSync(gofmt)) {
1514
+ ensureExecutableFile(gofmt, normalizeBundledPermissions);
1515
+ }
1444
1516
  const toolDir = node_path_1.default.join(goRoot, "pkg", "tool");
1445
1517
  if (!node_fs_1.default.existsSync(toolDir))
1446
1518
  return;
1447
1519
  for (const file of walkToolFiles(toolDir)) {
1448
- node_fs_1.default.chmodSync(file, 0o755);
1520
+ ensureExecutableFile(file, normalizeBundledPermissions);
1449
1521
  }
1450
1522
  }
1451
1523
  catch {
1452
1524
  // Let the subsequent go build spawn fail with the real OS error.
1453
1525
  }
1454
1526
  }
1527
+ /** Repair tool execution without widening an explicitly selected toolchain. */
1528
+ function ensureExecutableFile(file, normalizeBundledPermissions) {
1529
+ const mode = node_fs_1.default.statSync(file).mode & 0o7777;
1530
+ if (normalizeBundledPermissions) {
1531
+ if (mode !== 0o755)
1532
+ node_fs_1.default.chmodSync(file, 0o755);
1533
+ }
1534
+ else if ((mode & 0o111) === 0) {
1535
+ node_fs_1.default.chmodSync(file, mode | 0o100);
1536
+ }
1537
+ }
1455
1538
  function walkToolFiles(dir) {
1456
1539
  const out = [];
1457
1540
  let entries;
@@ -1519,16 +1602,16 @@ function walkForGoMod(dir, out) {
1519
1602
  */
1520
1603
  function resolvePluginCacheRoot(projectRoot, cacheDir, env = process.env) {
1521
1604
  const paths = resolveSourceBuildCachePaths(projectRoot, cacheDir, env);
1522
- maybePrunePluginCache(paths, cacheDir, env);
1605
+ maybePruneSourceBuildCaches(paths, cacheDir, env);
1523
1606
  return paths.pluginRoot;
1524
1607
  }
1525
1608
  /**
1526
1609
  * Resolve all source-plugin build cache directories for one invocation.
1527
1610
  *
1528
1611
  * `pluginRoot` stores compiled plugin binaries; `goBuildRoot` is the Go object
1529
- * cache passed as `GOCACHE` while ttsc builds those binaries. Both live under a
1530
- * single `root`, so persisting one directory covers the whole source-build
1531
- * cache without depending on ttsc internals.
1612
+ * cache passed as `GOCACHE` while ttsc builds those binaries. The default Go
1613
+ * cache lives under `root`; an explicit `TTSC_GO_CACHE_DIR` or `GOCACHE` keeps
1614
+ * its independently resolved location and ownership policy.
1532
1615
  */
1533
1616
  function resolveSourceBuildCachePaths(projectRoot, cacheDir, env = process.env) {
1534
1617
  const root = resolveSourceBuildCacheRoot(projectRoot, cacheDir, env);
@@ -1655,7 +1738,7 @@ function resolveGoBuildCacheRoot(root, projectRoot, env) {
1655
1738
  source: "ttsc-cache",
1656
1739
  };
1657
1740
  }
1658
- function maybePrunePluginCache(paths, cacheDir, env = process.env) {
1741
+ function maybePruneSourceBuildCaches(paths, cacheDir, env = process.env) {
1659
1742
  // GC only the default (workspace-local) location. When the user pins an
1660
1743
  // explicit `cacheDir`/`TTSC_CACHE_DIR`, they own its lifetime, so ttsc must
1661
1744
  // not delete entries out from under them. `env` is the effective instance
@@ -1663,16 +1746,23 @@ function maybePrunePluginCache(paths, cacheDir, env = process.env) {
1663
1746
  // `context.env` is honored without leaning on the shared `process.env`.
1664
1747
  if (!cacheDir && !env.TTSC_CACHE_DIR) {
1665
1748
  prunePluginCacheRoot(paths.pluginRoot);
1749
+ if (paths.goBuildRootSource === "ttsc-cache") {
1750
+ pruneGoBuildCacheRoot(paths.goBuildRoot);
1751
+ }
1666
1752
  }
1667
1753
  }
1754
+ /** Report whether this invocation owns and automatically maintains both caches. */
1755
+ function shouldManageSourceBuildCaches(paths, cacheDir, env) {
1756
+ return (!cacheDir && !env.TTSC_CACHE_DIR && paths.goBuildRootSource === "ttsc-cache");
1757
+ }
1668
1758
  /**
1669
1759
  * Return every directory `ttsc clean` should remove for `projectRoot`.
1670
1760
  *
1671
- * Covers the resolved cache root (which holds `plugins/` and, when ttsc-owned,
1672
- * `go-build/`), a ttsc-owned Go build cache that lives OUTSIDE that root
1673
- * (`TTSC_GO_CACHE_DIR`), and the two legacy project-local caches. A
1674
- * user-provided `GOCACHE` is never removed. Pure over `env`, so the CLI passes
1675
- * `process.env` and a programmatic caller can pass an injected environment.
1761
+ * Covers the resolved plugin-binary root, a safely named nested `go-build/`, a
1762
+ * ttsc-owned Go build cache that lives OUTSIDE that root (`TTSC_GO_CACHE_DIR`),
1763
+ * and the two legacy project-local caches. A user-provided `GOCACHE` is never
1764
+ * removed. Pure over `env`, so the CLI passes `process.env` and a programmatic
1765
+ * caller can pass an injected environment.
1676
1766
  */
1677
1767
  function resolveCleanTargets(projectRoot, cacheDir, env = process.env) {
1678
1768
  const paths = resolveSourceBuildCachePaths(projectRoot, cacheDir, env);
@@ -1737,24 +1827,29 @@ function isPathWithin(child, parent) {
1737
1827
  }
1738
1828
  function resolveGoCompiler(env = process.env) {
1739
1829
  const explicit = env.TTSC_GO_BINARY;
1740
- if (explicit && explicit.length > 0)
1741
- return explicit;
1830
+ if (explicit && explicit.length > 0) {
1831
+ return { binary: explicit, bundled: false };
1832
+ }
1742
1833
  try {
1743
- return (0, node_module_1.createRequire)(__filename).resolve(`@ttsc/${process.platform}-${process.arch}/bin/go/bin/${process.platform === "win32" ? "go.exe" : "go"}`);
1834
+ return {
1835
+ binary: (0, node_module_1.createRequire)(__filename).resolve(`@ttsc/${process.platform}-${process.arch}/bin/go/bin/${process.platform === "win32" ? "go.exe" : "go"}`),
1836
+ bundled: true,
1837
+ };
1744
1838
  }
1745
1839
  catch {
1746
1840
  /* fall through */
1747
1841
  }
1748
1842
  const platformPackage = node_path_1.default.resolve(__dirname, "..", "..", "..", "..", `ttsc-${process.platform}-${process.arch}`, "bin", "go", "bin", process.platform === "win32" ? "go.exe" : "go");
1749
- if (node_fs_1.default.existsSync(platformPackage))
1750
- return platformPackage;
1843
+ if (node_fs_1.default.existsSync(platformPackage)) {
1844
+ return { binary: platformPackage, bundled: true };
1845
+ }
1751
1846
  const local = node_path_1.default.resolve(__dirname, "..", "..", "..", "..", "native", "go", "bin", process.platform === "win32" ? "go.exe" : "go");
1752
1847
  if (node_fs_1.default.existsSync(local))
1753
- return local;
1848
+ return { binary: local, bundled: true };
1754
1849
  const homeSdk = node_path_1.default.join(env.HOME ?? "", "go-sdk", "go", "bin", process.platform === "win32" ? "go.exe" : "go");
1755
1850
  if (node_fs_1.default.existsSync(homeSdk))
1756
- return homeSdk;
1757
- return "go";
1851
+ return { binary: homeSdk, bundled: false };
1852
+ return { binary: "go", bundled: false };
1758
1853
  }
1759
1854
  /**
1760
1855
  * Compute a deterministic SHA-256 cache key for a plugin build.
@@ -1769,6 +1864,9 @@ function resolveGoCompiler(env = process.env) {
1769
1864
  */
1770
1865
  function computeCacheKey(inputs) {
1771
1866
  const env = inputs.env ?? process.env;
1867
+ const filesystem = {
1868
+ readFile: inputs.filesystem?.readFile ?? DEFAULT_SOURCE_BUILD_FILESYSTEM.readFile,
1869
+ };
1772
1870
  const goBinary = inputs.goBinary === undefined
1773
1871
  ? undefined
1774
1872
  : resolveGoToolForBuild(inputs.goBinary, env, inputs.dir);
@@ -1780,7 +1878,7 @@ function computeCacheKey(inputs) {
1780
1878
  if (goBinary !== undefined) {
1781
1879
  hash.update(`go=${resolveGoCompilerIdentity(goBinary, env, inputs.dir)}\n`);
1782
1880
  }
1783
- hashGoBuildEnvironment(hash, goBinary, inputs.dir, env);
1881
+ hashGoBuildEnvironment(hash, goBinary, inputs.dir, env, filesystem);
1784
1882
  hashExternalGoBuildEnvironment(hash, env);
1785
1883
  hashSourceDirectory(hash, "plugin", inputs.dir);
1786
1884
  for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
@@ -1860,8 +1958,9 @@ function isHashableFile(name) {
1860
1958
  // binary hashes for a value that does not change between plugins. The result
1861
1959
  // is a pure function of the go binary's resolved real path plus its on-disk
1862
1960
  // content; the memo key therefore mixes the resolved real path with a cheap
1863
- // content signature (byte size + nanosecond mtime). That signature changes if
1864
- // a long-lived host rewrites the binary in place between calls, so the memo
1961
+ // content signature (filesystem identity, mode, byte size, and nanosecond
1962
+ // change/modify times). That signature changes if a long-lived host rewrites
1963
+ // or atomically replaces the binary between calls, so the memo
1865
1964
  // re-derives the identity exactly as the un-memoized code would and the
1866
1965
  // cache-key bytes stay byte-for-byte identical to today. The selected compiler
1867
1966
  // path is shared by every build subprocess on Windows, while `go version` uses
@@ -1893,7 +1992,7 @@ function resolveGoCompilerIdentity(goBinary, env = process.env, cwd = process.cw
1893
1992
  // stat-ed, so the rare unstattable case never serves a stale identity.
1894
1993
  function goCompilerIdentityMemoKey(goBinary, resolved, env, cwd) {
1895
1994
  try {
1896
- const stat = node_fs_1.default.statSync(resolved);
1995
+ const stat = node_fs_1.default.statSync(resolved, { bigint: true });
1897
1996
  const context = node_crypto_1.default.createHash("sha256");
1898
1997
  context.update(cwd);
1899
1998
  for (const [key, value] of Object.entries(env).sort(([left], [right]) => left === right ? 0 : left < right ? -1 : 1)) {
@@ -1901,7 +2000,17 @@ function goCompilerIdentityMemoKey(goBinary, resolved, env, cwd) {
1901
2000
  continue;
1902
2001
  context.update(`\0${key.length}:${key}${value.length}:${value}`);
1903
2002
  }
1904
- return `${goBinary}\0${resolved}\0${stat.size}\0${stat.mtimeMs}\0${context.digest("hex")}`;
2003
+ return [
2004
+ goBinary,
2005
+ resolved,
2006
+ stat.dev,
2007
+ stat.ino,
2008
+ stat.mode,
2009
+ stat.size,
2010
+ stat.mtimeNs,
2011
+ stat.ctimeNs,
2012
+ context.digest("hex"),
2013
+ ].join("\0");
1905
2014
  }
1906
2015
  catch {
1907
2016
  return null;
@@ -2094,8 +2203,8 @@ function hashFile(file) {
2094
2203
  hash.update(node_fs_1.default.readFileSync(file));
2095
2204
  return hash.digest("hex");
2096
2205
  }
2097
- function hashGoBuildEnvironment(hash, goBinary, cwd, env) {
2098
- const values = resolveGoBuildEnvironment(goBinary, cwd, env);
2206
+ function hashGoBuildEnvironment(hash, goBinary, cwd, env, filesystem) {
2207
+ const values = resolveGoBuildEnvironment(goBinary, cwd, env, filesystem);
2099
2208
  for (const key of GO_BUILD_ENV_KEYS) {
2100
2209
  const value = values.get(key);
2101
2210
  if (value !== undefined && value !== "") {
@@ -2103,7 +2212,7 @@ function hashGoBuildEnvironment(hash, goBinary, cwd, env) {
2103
2212
  }
2104
2213
  }
2105
2214
  }
2106
- function resolveGoBuildEnvironment(goBinary, cwd, env) {
2215
+ function resolveGoBuildEnvironment(goBinary, cwd, env, filesystem) {
2107
2216
  const values = new Map();
2108
2217
  if (goBinary !== undefined) {
2109
2218
  const result = spawnGoTool(goBinary, ["env", "-json", ...GO_BUILD_ENV_KEYS], {
@@ -2118,7 +2227,7 @@ function resolveGoBuildEnvironment(goBinary, cwd, env) {
2118
2227
  for (const key of GO_BUILD_ENV_KEYS) {
2119
2228
  const raw = parsed[key];
2120
2229
  if (typeof raw === "string" && raw !== "") {
2121
- values.set(key, normalizeGoBuildEnvValue(key, raw, env));
2230
+ values.set(key, normalizeGoBuildEnvValue(key, raw, env, filesystem));
2122
2231
  }
2123
2232
  }
2124
2233
  }
@@ -2133,14 +2242,14 @@ function resolveGoBuildEnvironment(goBinary, cwd, env) {
2133
2242
  continue;
2134
2243
  const value = env[key];
2135
2244
  if (value !== undefined && value !== "") {
2136
- values.set(key, normalizeGoBuildEnvValue(key, value, env));
2245
+ values.set(key, normalizeGoBuildEnvValue(key, value, env, filesystem));
2137
2246
  }
2138
2247
  }
2139
2248
  return values;
2140
2249
  }
2141
- function normalizeGoBuildEnvValue(key, value, env) {
2250
+ function normalizeGoBuildEnvValue(key, value, env, filesystem) {
2142
2251
  if (key === "GOROOT") {
2143
- return resolveGoRootCacheIdentity(value);
2252
+ return resolveGoRootCacheIdentity(value, filesystem);
2144
2253
  }
2145
2254
  if (GO_BUILD_COMMAND_ENV_KEYS.has(key)) {
2146
2255
  return `${value}\0${resolveCommandCacheIdentity(value, env)}`;
@@ -2183,32 +2292,81 @@ function hashExternalGoBuildEnvironment(hash, env) {
2183
2292
  }
2184
2293
  }
2185
2294
  }
2186
- function resolveGoRootCacheIdentity(goRoot) {
2295
+ // GOROOT is immutable during normal use but contributes roughly 140 MiB of
2296
+ // source/tool content to every plugin key. Retain only the final pathless
2297
+ // content identity, guarded by a fresh metadata manifest on every call. A
2298
+ // changed or incomplete manifest falls through to the historical full read.
2299
+ const goRootIdentityCache = new Map();
2300
+ function resolveGoRootCacheIdentity(goRoot, filesystem) {
2187
2301
  const resolved = resolveRealPath(goRoot);
2188
2302
  if (!node_fs_1.default.existsSync(resolved)) {
2189
2303
  return `missing:${goRoot}`;
2190
2304
  }
2305
+ const snapshot = collectGoRootIdentitySnapshot(resolved);
2306
+ if (snapshot.complete) {
2307
+ const cached = goRootIdentityCache.get(resolved);
2308
+ if (cached?.signature === snapshot.signature) {
2309
+ return cached.identity;
2310
+ }
2311
+ }
2191
2312
  const hash = node_crypto_1.default.createHash("sha256");
2192
- for (const file of collectGoRootIdentityFiles(resolved)) {
2313
+ for (const file of snapshot.files) {
2193
2314
  const relative = node_path_1.default.relative(resolved, file).replace(/\\/g, "/");
2194
2315
  hash.update(`f=${relative}\n`);
2195
- hash.update(node_fs_1.default.readFileSync(file));
2316
+ hash.update(filesystem.readFile(file));
2196
2317
  hash.update("\n");
2197
2318
  }
2198
- return `sha256:${hash.digest("hex")}`;
2319
+ const identity = `sha256:${hash.digest("hex")}`;
2320
+ if (snapshot.complete) {
2321
+ goRootIdentityCache.set(resolved, {
2322
+ identity,
2323
+ signature: snapshot.signature,
2324
+ });
2325
+ }
2326
+ return identity;
2199
2327
  }
2200
- function collectGoRootIdentityFiles(root) {
2328
+ function collectGoRootIdentitySnapshot(root) {
2201
2329
  const out = [];
2202
- walkGoRootIdentity(root, root, out);
2330
+ const state = { complete: true };
2331
+ walkGoRootIdentity(root, root, out, state);
2203
2332
  out.sort();
2204
- return out;
2333
+ const signature = node_crypto_1.default.createHash("sha256");
2334
+ for (const file of out) {
2335
+ const relative = node_path_1.default.relative(root, file).replace(/\\/g, "/");
2336
+ try {
2337
+ const stats = node_fs_1.default.statSync(file, { bigint: true });
2338
+ if (!stats.isFile()) {
2339
+ state.complete = false;
2340
+ continue;
2341
+ }
2342
+ signature.update([
2343
+ relative,
2344
+ stats.dev,
2345
+ stats.ino,
2346
+ stats.mode,
2347
+ stats.size,
2348
+ stats.mtimeNs,
2349
+ stats.ctimeNs,
2350
+ ].join("\0"));
2351
+ signature.update("\n");
2352
+ }
2353
+ catch {
2354
+ state.complete = false;
2355
+ }
2356
+ }
2357
+ return {
2358
+ complete: state.complete,
2359
+ files: out,
2360
+ signature: signature.digest("hex"),
2361
+ };
2205
2362
  }
2206
- function walkGoRootIdentity(root, dir, out) {
2363
+ function walkGoRootIdentity(root, dir, out, state) {
2207
2364
  let entries;
2208
2365
  try {
2209
2366
  entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
2210
2367
  }
2211
2368
  catch {
2369
+ state.complete = false;
2212
2370
  return;
2213
2371
  }
2214
2372
  for (const entry of entries) {
@@ -2216,7 +2374,7 @@ function walkGoRootIdentity(root, dir, out) {
2216
2374
  const rel = node_path_1.default.relative(root, file).replace(/\\/g, "/");
2217
2375
  if (entry.isDirectory()) {
2218
2376
  if (shouldHashGoRootPath(rel, true)) {
2219
- walkGoRootIdentity(root, file, out);
2377
+ walkGoRootIdentity(root, file, out, state);
2220
2378
  }
2221
2379
  }
2222
2380
  else if (entry.isFile() && shouldHashGoRootPath(rel, false)) {
@@ -2265,50 +2423,554 @@ function shouldHashGoRootPath(rel, isDir) {
2265
2423
  function touchCacheEntry(cacheDir) {
2266
2424
  try {
2267
2425
  node_fs_1.default.mkdirSync(cacheDir, { recursive: true });
2268
- node_fs_1.default.writeFileSync(node_path_1.default.join(cacheDir, CACHE_LAST_USED_FILE), `${Date.now()}\n`);
2426
+ replaceCacheMetadataFile(node_path_1.default.join(cacheDir, CACHE_LAST_USED_FILE), `${Date.now()}\n`);
2269
2427
  }
2270
2428
  catch {
2271
2429
  // Cache hits must not fail because metadata touch failed.
2272
2430
  }
2273
2431
  }
2274
- function prunePluginCacheRoot(root) {
2432
+ function prunePluginCacheRoot(root, options = {}) {
2275
2433
  try {
2276
- node_fs_1.default.mkdirSync(root, { recursive: true });
2277
- const marker = node_path_1.default.join(root, CACHE_GC_MARKER_FILE);
2278
- const now = Date.now();
2434
+ const cacheRoot = canonicalPluginCacheRoot(root);
2435
+ const marker = node_path_1.default.join(cacheRoot, CACHE_GC_MARKER_FILE);
2436
+ const now = options.now ?? Date.now();
2279
2437
  const lastRun = readTimestamp(marker);
2280
- if (lastRun !== null && now - lastRun < PLUGIN_CACHE_GC_INTERVAL_MS) {
2438
+ if (options.force !== true &&
2439
+ lastRun !== null &&
2440
+ lastRun <= now &&
2441
+ now - lastRun < PLUGIN_CACHE_GC_INTERVAL_MS) {
2281
2442
  return;
2282
2443
  }
2283
- node_fs_1.default.writeFileSync(marker, `${now}\n`);
2284
- prunePluginCacheEntries(root, now);
2444
+ const remainingBytes = prunePluginCacheEntries(cacheRoot, {
2445
+ maxBytes: options.maxBytes ?? PLUGIN_CACHE_MAX_BYTES,
2446
+ now,
2447
+ protectedEntries: canonicalPluginCacheProtectedEntries(cacheRoot, options.protectedEntries ?? []),
2448
+ protectedAgeMs: options.protectedAgeMs ?? PLUGIN_CACHE_PROTECTED_AGE_MS,
2449
+ targetBytes: options.targetBytes ?? PLUGIN_CACHE_TARGET_BYTES,
2450
+ });
2451
+ const maxBytes = options.maxBytes ?? PLUGIN_CACHE_MAX_BYTES;
2452
+ const protectedAgeMs = options.protectedAgeMs ?? PLUGIN_CACHE_PROTECTED_AGE_MS;
2453
+ const markerTimestamp = remainingBytes > maxBytes
2454
+ ? now - PLUGIN_CACHE_GC_INTERVAL_MS + protectedAgeMs
2455
+ : now;
2456
+ replaceCacheMetadataFile(marker, `${markerTimestamp}\n`);
2285
2457
  }
2286
2458
  catch {
2287
2459
  // Plugin-cache GC is opportunistic; builds still proceed when it fails.
2288
2460
  }
2289
2461
  }
2290
- function prunePluginCacheEntries(root, now) {
2291
- const entries = collectPluginCacheEntries(root, now);
2462
+ /** Resolve explicit GC exclusions without allowing an alias outside root. */
2463
+ function canonicalPluginCacheProtectedEntries(root, entries) {
2464
+ const protectedEntries = new Set();
2465
+ for (const entry of entries) {
2466
+ const candidate = node_path_1.default.resolve(entry);
2467
+ const stats = node_fs_1.default.lstatSync(candidate);
2468
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
2469
+ throw new Error(`ttsc: unsafe protected plugin cache entry: ${entry}`);
2470
+ }
2471
+ const physical = node_fs_1.default.realpathSync.native(candidate);
2472
+ if (node_path_1.default.dirname(physical) !== root) {
2473
+ throw new Error(`ttsc: protected plugin cache entry escaped root: ${entry}`);
2474
+ }
2475
+ protectedEntries.add(physical);
2476
+ }
2477
+ return protectedEntries;
2478
+ }
2479
+ /** Pin the default plugin cache to one ordinary physical directory. */
2480
+ function canonicalPluginCacheRoot(root) {
2481
+ node_fs_1.default.mkdirSync(root, { recursive: true });
2482
+ const physicalParent = node_fs_1.default.realpathSync.native(node_path_1.default.dirname(root));
2483
+ const stats = node_fs_1.default.lstatSync(root);
2484
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
2485
+ throw new Error(`ttsc: unsafe plugin cache root: ${root}`);
2486
+ }
2487
+ // If an ancestor alias is retargeted after this point, all later cache work
2488
+ // stays on the original physical directory rather than following it.
2489
+ const physicalRoot = node_fs_1.default.realpathSync.native(root);
2490
+ if (node_path_1.default.dirname(physicalRoot) !== physicalParent) {
2491
+ throw new Error(`ttsc: plugin cache root escaped its parent: ${root}`);
2492
+ }
2493
+ return physicalRoot;
2494
+ }
2495
+ /** Create one content-addressed cache entry without following a leaf alias. */
2496
+ function canonicalPluginCacheEntry(root, key) {
2497
+ const directory = node_path_1.default.join(root, key);
2498
+ node_fs_1.default.mkdirSync(directory, { recursive: true });
2499
+ const stats = node_fs_1.default.lstatSync(directory);
2500
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
2501
+ throw new Error(`ttsc: unsafe plugin cache entry: ${directory}`);
2502
+ }
2503
+ const physicalDirectory = node_fs_1.default.realpathSync.native(directory);
2504
+ if (node_path_1.default.dirname(physicalDirectory) !== root) {
2505
+ throw new Error(`ttsc: plugin cache entry escaped its root: ${directory}`);
2506
+ }
2507
+ return physicalDirectory;
2508
+ }
2509
+ function prunePluginCacheEntries(root, options) {
2510
+ const entries = collectPluginCacheEntries(root, options.now);
2292
2511
  for (const entry of entries) {
2293
- if (now - entry.lastUsedAt <= PLUGIN_CACHE_ENTRY_MAX_AGE_MS) {
2512
+ if (options.now - entry.lastUsedAt <= PLUGIN_CACHE_ENTRY_MAX_AGE_MS ||
2513
+ options.protectedEntries.has(entry.dir) ||
2514
+ pluginCacheEntryHasActiveBuild(entry, options.now)) {
2294
2515
  continue;
2295
2516
  }
2296
2517
  removeCacheEntry(entry);
2297
2518
  }
2298
- const remaining = collectPluginCacheEntries(root, now);
2519
+ const remaining = collectPluginCacheEntries(root, options.now);
2299
2520
  let total = remaining.reduce((sum, entry) => sum + entry.size, 0);
2300
- if (total <= PLUGIN_CACHE_MAX_BYTES) {
2301
- return;
2521
+ if (total <= options.maxBytes) {
2522
+ return total;
2523
+ }
2524
+ const protectedEntries = new Set(options.protectedEntries);
2525
+ let protectedBytes = 0;
2526
+ for (const entry of [...remaining].sort((a, b) => b.lastUsedAt - a.lastUsedAt)) {
2527
+ if (pluginCacheEntryHasActiveBuild(entry, options.now)) {
2528
+ protectedEntries.add(entry.dir);
2529
+ continue;
2530
+ }
2531
+ if (options.now - entry.lastUsedAt > options.protectedAgeMs)
2532
+ continue;
2533
+ if (protectedBytes >= options.targetBytes)
2534
+ continue;
2535
+ if (protectedBytes + entry.size > options.targetBytes)
2536
+ continue;
2537
+ protectedEntries.add(entry.dir);
2538
+ protectedBytes += entry.size;
2302
2539
  }
2303
2540
  for (const entry of remaining.sort((a, b) => a.lastUsedAt - b.lastUsedAt)) {
2304
- if (total <= PLUGIN_CACHE_TARGET_BYTES) {
2541
+ if (total <= options.targetBytes) {
2542
+ return total;
2543
+ }
2544
+ if (protectedEntries.has(entry.dir))
2545
+ continue;
2546
+ if (removeCacheEntry(entry))
2547
+ total -= entry.size;
2548
+ }
2549
+ return total;
2550
+ }
2551
+ /** Conservatively protect an entry while any build generation owns its key. */
2552
+ function pluginCacheEntryHasActiveBuild(entry, now) {
2553
+ const lockDir = `${entry.dir}.lock`;
2554
+ try {
2555
+ return inspectPluginBuildLock(lockDir, now).state === "active";
2556
+ }
2557
+ catch {
2558
+ // Malformed or unreadable coordination state cannot disprove ownership.
2559
+ return true;
2560
+ }
2561
+ }
2562
+ /**
2563
+ * Opportunistically bound one ttsc-owned Go object cache.
2564
+ *
2565
+ * A maintenance intent blocks new build leases. If any existing lease remains,
2566
+ * maintenance yields without touching the cache; the next invocation retries.
2567
+ * Only Go's two-hex object directories are scanned, so coordination metadata
2568
+ * and Go's own trim marker remain outside the size policy.
2569
+ */
2570
+ function pruneGoBuildCacheRoot(root, options = {}) {
2571
+ let intent;
2572
+ try {
2573
+ const cacheRoot = canonicalGoBuildCacheRoot(root);
2574
+ const marker = node_path_1.default.join(cacheRoot, GO_BUILD_CACHE_GC_MARKER_FILE);
2575
+ const now = options.now ?? Date.now();
2576
+ const lastRun = readTimestamp(marker);
2577
+ if (options.force !== true &&
2578
+ lastRun !== null &&
2579
+ lastRun <= now &&
2580
+ now - lastRun < GO_BUILD_CACHE_GC_INTERVAL_MS) {
2305
2581
  return;
2306
2582
  }
2307
- if (now - entry.lastUsedAt <= PLUGIN_CACHE_PROTECTED_AGE_MS) {
2583
+ intent = createGoBuildCacheCoordinationRecord(cacheRoot, GO_BUILD_CACHE_MAINTENANCE_DIR);
2584
+ if (!intent.startHeartbeat()) {
2585
+ // Maintenance is opportunistic. Without an independent heartbeat a
2586
+ // synchronous scan could look abandoned while it is still deleting, so
2587
+ // yield instead of weakening the build/maintenance exclusion.
2588
+ return;
2589
+ }
2590
+ if (collectLiveGoBuildCacheCoordinationRecords(cacheRoot, GO_BUILD_CACHE_LEASE_DIR, now).length !== 0) {
2591
+ return;
2592
+ }
2593
+ const remainingBytes = pruneGoBuildCacheEntries(cacheRoot, {
2594
+ maxBytes: options.maxBytes ?? GO_BUILD_CACHE_MAX_BYTES,
2595
+ now,
2596
+ protectedAgeMs: options.protectedAgeMs ?? GO_BUILD_CACHE_PROTECTED_AGE_MS,
2597
+ targetBytes: options.targetBytes ?? GO_BUILD_CACHE_TARGET_BYTES,
2598
+ });
2599
+ // If recent protection or a transient delete failure left the cache above
2600
+ // the ceiling, retry after the protection window instead of suppressing
2601
+ // every maintenance attempt for a full day.
2602
+ const maxBytes = options.maxBytes ?? GO_BUILD_CACHE_MAX_BYTES;
2603
+ const protectedAgeMs = options.protectedAgeMs ?? GO_BUILD_CACHE_PROTECTED_AGE_MS;
2604
+ const markerTimestamp = remainingBytes > maxBytes
2605
+ ? now - GO_BUILD_CACHE_GC_INTERVAL_MS + protectedAgeMs
2606
+ : now;
2607
+ replaceCacheMetadataFile(marker, `${markerTimestamp}\n`);
2608
+ }
2609
+ catch {
2610
+ // Go-cache GC is opportunistic; builds still proceed when it fails.
2611
+ }
2612
+ finally {
2613
+ intent?.finish();
2614
+ }
2615
+ }
2616
+ /**
2617
+ * Run one Go build under a cross-process cache lease when ttsc owns the cache.
2618
+ *
2619
+ * The lease is published before checking maintenance. A maintenance process
2620
+ * that arrived first keeps its intent visible, so this builder withdraws and
2621
+ * retries; one that arrives second sees the lease and yields. That ordering
2622
+ * closes the scan/start race without serializing independent Go builds.
2623
+ */
2624
+ function withGoBuildCacheLease(root, managed, callback) {
2625
+ if (!managed) {
2626
+ return callback(root);
2627
+ }
2628
+ const cacheRoot = canonicalGoBuildCacheRoot(root);
2629
+ const started = Date.now();
2630
+ for (;;) {
2631
+ const lease = createGoBuildCacheCoordinationRecord(cacheRoot, GO_BUILD_CACHE_LEASE_DIR);
2632
+ const maintenance = collectLiveGoBuildCacheCoordinationRecords(cacheRoot, GO_BUILD_CACHE_MAINTENANCE_DIR, Date.now());
2633
+ if (maintenance.length === 0) {
2634
+ try {
2635
+ if (!lease.startHeartbeat()) {
2636
+ throw new Error(`ttsc: unable to start Go build cache lease heartbeat at ${cacheRoot}`);
2637
+ }
2638
+ return callback(cacheRoot);
2639
+ }
2640
+ finally {
2641
+ lease.finish();
2642
+ }
2643
+ }
2644
+ lease.finish();
2645
+ if (Date.now() - started > PLUGIN_BUILD_LOCK_STEAL_MS) {
2646
+ throw new Error(`ttsc: timed out waiting for Go build cache maintenance at ${cacheRoot}`);
2647
+ }
2648
+ sleepSync(GO_BUILD_CACHE_COORDINATION_POLL_MS);
2649
+ }
2650
+ }
2651
+ /**
2652
+ * Create and pin the owned Go cache to an ordinary physical directory.
2653
+ *
2654
+ * The leaf may be user-controlled inside `node_modules/.cache`; accepting a
2655
+ * symlink or junction there would let LRU deletion escape into an arbitrary
2656
+ * two-hex directory. Returning the canonical spelling also keeps the build,
2657
+ * leases, and maintenance on the same directory if an ancestor alias moves.
2658
+ */
2659
+ function canonicalGoBuildCacheRoot(root) {
2660
+ node_fs_1.default.mkdirSync(root, { recursive: true });
2661
+ const physicalParent = node_fs_1.default.realpathSync.native(node_path_1.default.dirname(root));
2662
+ const stats = node_fs_1.default.lstatSync(root);
2663
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
2664
+ throw new Error(`ttsc: unsafe Go build cache root: ${root}`);
2665
+ }
2666
+ const physicalRoot = node_fs_1.default.realpathSync.native(root);
2667
+ if (node_path_1.default.dirname(physicalRoot) !== physicalParent) {
2668
+ throw new Error(`ttsc: Go build cache root escaped its parent: ${root}`);
2669
+ }
2670
+ return physicalRoot;
2671
+ }
2672
+ /** Prune oldest Go cache objects to the requested deterministic size target. */
2673
+ function pruneGoBuildCacheEntries(root, options) {
2674
+ const entries = collectGoBuildCacheObjects(root);
2675
+ let total = entries.reduce((sum, entry) => sum + entry.size, 0);
2676
+ if (total <= options.maxBytes) {
2677
+ return total;
2678
+ }
2679
+ // Protect the newest recent objects only up to one target-sized cohort
2680
+ // reserve. Protecting every object younger than an hour would let repeated
2681
+ // 2.9 GiB cold builds grow without bound during that hour.
2682
+ const protectedFiles = new Set();
2683
+ let protectedBytes = 0;
2684
+ for (const entry of [...entries].sort((a, b) => b.lastUsedAt - a.lastUsedAt)) {
2685
+ if (options.now - entry.lastUsedAt >
2686
+ options.protectedAgeMs + GO_BUILD_CACHE_ACCESS_MTIME_GRANULARITY_MS) {
2308
2687
  continue;
2309
2688
  }
2310
- removeCacheEntry(entry);
2311
- total -= entry.size;
2689
+ if (protectedBytes >= options.targetBytes) {
2690
+ break;
2691
+ }
2692
+ if (protectedBytes + entry.size > options.targetBytes) {
2693
+ break;
2694
+ }
2695
+ protectedFiles.add(entry.file);
2696
+ protectedBytes += entry.size;
2697
+ }
2698
+ for (const entry of entries.sort((a, b) => a.lastUsedAt - b.lastUsedAt)) {
2699
+ if (total <= options.targetBytes) {
2700
+ return total;
2701
+ }
2702
+ if (protectedFiles.has(entry.file)) {
2703
+ continue;
2704
+ }
2705
+ try {
2706
+ node_fs_1.default.rmSync(entry.file, { force: true });
2707
+ total -= entry.size;
2708
+ }
2709
+ catch {
2710
+ // A concurrent antivirus/indexer can transiently hold a file on Windows.
2711
+ }
2712
+ }
2713
+ return total;
2714
+ }
2715
+ function collectGoBuildCacheObjects(root) {
2716
+ const output = [];
2717
+ let buckets;
2718
+ try {
2719
+ buckets = node_fs_1.default.readdirSync(root, { withFileTypes: true });
2720
+ }
2721
+ catch {
2722
+ return output;
2723
+ }
2724
+ for (const bucket of buckets) {
2725
+ if (!bucket.isDirectory() || !/^[0-9a-f]{2}$/.test(bucket.name)) {
2726
+ continue;
2727
+ }
2728
+ const directory = node_path_1.default.join(root, bucket.name);
2729
+ let files;
2730
+ try {
2731
+ files = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
2732
+ }
2733
+ catch {
2734
+ continue;
2735
+ }
2736
+ for (const file of files) {
2737
+ if (!file.isFile()) {
2738
+ continue;
2739
+ }
2740
+ const absolute = node_path_1.default.join(directory, file.name);
2741
+ try {
2742
+ const stats = node_fs_1.default.statSync(absolute);
2743
+ output.push({
2744
+ file: absolute,
2745
+ lastUsedAt: stats.mtimeMs,
2746
+ size: stats.size,
2747
+ });
2748
+ }
2749
+ catch { }
2750
+ }
2751
+ }
2752
+ return output;
2753
+ }
2754
+ function createGoBuildCacheCoordinationRecord(root, directoryName) {
2755
+ const directory = goBuildCacheCoordinationDirectory(root, directoryName, true);
2756
+ const record = node_path_1.default.join(directory, `${process.pid}-${node_crypto_1.default.randomBytes(16).toString("hex")}.json`);
2757
+ const metadata = {
2758
+ directoryName,
2759
+ hostname: node_os_1.default.hostname(),
2760
+ pid: process.pid,
2761
+ startedAt: Date.now(),
2762
+ };
2763
+ writeGoBuildCacheCoordinationRecord(record, metadata, "active");
2764
+ let heartbeat;
2765
+ return {
2766
+ file: record,
2767
+ finish: () => {
2768
+ heartbeat?.stop();
2769
+ heartbeat = undefined;
2770
+ // A failed unlink must not leave a completed task looking active until
2771
+ // its stale timeout. Persist completion first; collectors discard it.
2772
+ try {
2773
+ writeGoBuildCacheCoordinationRecord(record, metadata, "complete");
2774
+ }
2775
+ catch { }
2776
+ try {
2777
+ node_fs_1.default.rmSync(record, { force: true });
2778
+ }
2779
+ catch { }
2780
+ },
2781
+ startHeartbeat: () => {
2782
+ heartbeat ??= startGoBuildCacheHeartbeat(record);
2783
+ return heartbeat !== undefined;
2784
+ },
2785
+ };
2786
+ }
2787
+ function writeGoBuildCacheCoordinationRecord(file, metadata, status) {
2788
+ replaceCacheMetadataFile(file, `${JSON.stringify({ ...metadata, status, version: 1 })}\n`);
2789
+ }
2790
+ /** Refresh one synchronous cache task's record from a background worker. */
2791
+ function startGoBuildCacheHeartbeat(file) {
2792
+ const control = new SharedArrayBuffer(4);
2793
+ const state = new Int32Array(control);
2794
+ try {
2795
+ const worker = new node_worker_threads_1.Worker([
2796
+ 'const fs = process.getBuiltinModule("node:fs");',
2797
+ 'const { workerData } = process.getBuiltinModule("node:worker_threads");',
2798
+ "const state = new Int32Array(workerData.control);",
2799
+ "for (;;) {",
2800
+ " const result = Atomics.wait(state, 0, 0, workerData.interval);",
2801
+ ' if (result !== "timed-out" || Atomics.load(state, 0) !== 0) break;',
2802
+ " try {",
2803
+ " const now = new Date();",
2804
+ " fs.utimesSync(workerData.file, now, now);",
2805
+ " } catch {}",
2806
+ "}",
2807
+ ].join("\n"), {
2808
+ eval: true,
2809
+ workerData: {
2810
+ control,
2811
+ file,
2812
+ interval: GO_BUILD_CACHE_COORDINATION_HEARTBEAT_MS,
2813
+ },
2814
+ });
2815
+ worker.unref();
2816
+ return {
2817
+ stop: () => {
2818
+ Atomics.store(state, 0, 1);
2819
+ Atomics.notify(state, 0);
2820
+ void worker.terminate();
2821
+ },
2822
+ };
2823
+ }
2824
+ catch { }
2825
+ // Node's permission model can deny Worker construction while still allowing
2826
+ // the child process required for `go build`. An IPC-bound helper provides
2827
+ // the same independent heartbeat and exits automatically if its parent dies.
2828
+ try {
2829
+ const child = (0, node_child_process_1.spawn)(process.execPath, [
2830
+ "-e",
2831
+ [
2832
+ 'const fs = require("node:fs");',
2833
+ "const file = process.argv[1];",
2834
+ "const interval = Number(process.argv[2]);",
2835
+ "const timer = setInterval(() => {",
2836
+ " try {",
2837
+ " const now = new Date();",
2838
+ " fs.utimesSync(file, now, now);",
2839
+ " } catch {}",
2840
+ "}, interval);",
2841
+ 'process.on("disconnect", () => {',
2842
+ " clearInterval(timer);",
2843
+ " process.exit(0);",
2844
+ "});",
2845
+ ].join("\n"),
2846
+ file,
2847
+ String(GO_BUILD_CACHE_COORDINATION_HEARTBEAT_MS),
2848
+ ], {
2849
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
2850
+ windowsHide: true,
2851
+ });
2852
+ child.unref();
2853
+ child.channel?.unref();
2854
+ return {
2855
+ stop: () => {
2856
+ if (child.connected)
2857
+ child.disconnect();
2858
+ child.kill();
2859
+ },
2860
+ };
2861
+ }
2862
+ catch {
2863
+ return undefined;
2864
+ }
2865
+ }
2866
+ function collectLiveGoBuildCacheCoordinationRecords(root, directoryName, now) {
2867
+ const directory = goBuildCacheCoordinationDirectory(root, directoryName, false);
2868
+ if (directory === undefined)
2869
+ return [];
2870
+ let records;
2871
+ try {
2872
+ records = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
2873
+ }
2874
+ catch (error) {
2875
+ if (error.code === "ENOENT")
2876
+ return [];
2877
+ throw error;
2878
+ }
2879
+ const live = [];
2880
+ for (const record of records) {
2881
+ if (!record.isFile()) {
2882
+ continue;
2883
+ }
2884
+ const file = node_path_1.default.join(directory, record.name);
2885
+ if (goBuildCacheCoordinationRecordIsLive(file, directoryName, now)) {
2886
+ live.push(file);
2887
+ continue;
2888
+ }
2889
+ try {
2890
+ node_fs_1.default.rmSync(file, { force: true });
2891
+ }
2892
+ catch { }
2893
+ }
2894
+ return live;
2895
+ }
2896
+ /**
2897
+ * Resolve one private coordination directory without following a project-
2898
+ * supplied symlink or junction outside the owned Go cache.
2899
+ */
2900
+ function goBuildCacheCoordinationDirectory(root, directoryName, create) {
2901
+ if (create)
2902
+ node_fs_1.default.mkdirSync(root, { recursive: true });
2903
+ const directory = node_path_1.default.join(root, directoryName);
2904
+ if (create) {
2905
+ try {
2906
+ node_fs_1.default.mkdirSync(directory);
2907
+ }
2908
+ catch (error) {
2909
+ if (error.code !== "EEXIST")
2910
+ throw error;
2911
+ }
2912
+ }
2913
+ let stats;
2914
+ try {
2915
+ stats = node_fs_1.default.lstatSync(directory);
2916
+ }
2917
+ catch (error) {
2918
+ if (!create && error.code === "ENOENT") {
2919
+ return undefined;
2920
+ }
2921
+ throw error;
2922
+ }
2923
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
2924
+ throw new Error(`ttsc: unsafe Go build cache coordination directory: ${directory}`);
2925
+ }
2926
+ const physicalRoot = node_fs_1.default.realpathSync.native(root);
2927
+ const physicalDirectory = node_fs_1.default.realpathSync.native(directory);
2928
+ if (node_path_1.default.dirname(physicalDirectory) !== physicalRoot) {
2929
+ throw new Error(`ttsc: Go build cache coordination directory escaped its root: ${directory}`);
2930
+ }
2931
+ return physicalDirectory;
2932
+ }
2933
+ function goBuildCacheCoordinationRecordIsLive(file, directoryName, now) {
2934
+ let contents;
2935
+ try {
2936
+ contents = node_fs_1.default.readFileSync(file, "utf8");
2937
+ const parsed = JSON.parse(contents);
2938
+ if (parsed.status === "complete")
2939
+ return false;
2940
+ // Parse valid records for forward compatibility, but do not equate a PID's
2941
+ // lifetime with one task. A failed unlink can leave a record owned by a
2942
+ // still-running Vite process, while a dead Node parent can leave its
2943
+ // spawnSync Go child alive. The heartbeat/grace below models the task.
2944
+ void parsed;
2945
+ }
2946
+ catch { }
2947
+ try {
2948
+ const age = now - node_fs_1.default.statSync(file).mtimeMs;
2949
+ if (age < -GO_BUILD_CACHE_COORDINATION_CLOCK_SKEW_MS) {
2950
+ // A restored cache can carry a far-future timestamp, but the same state
2951
+ // also occurs when the system clock moves backward during a real build.
2952
+ // Rebase the record and grant one ordinary grace period; an active
2953
+ // heartbeat keeps refreshing it, while an orphan then expires normally.
2954
+ // Treat a failed rebase as live too: the safe failure mode is to defer
2955
+ // opportunistic maintenance, never to delete under a possibly live Go.
2956
+ // Replace the directory entry rather than changing its inode in place.
2957
+ // A restored or user-modified cache can contain a hard-linked record;
2958
+ // utimesSync(file) would then mutate metadata outside the owned cache.
2959
+ if (contents !== undefined) {
2960
+ try {
2961
+ replaceCacheMetadataFile(file, contents);
2962
+ }
2963
+ catch { }
2964
+ }
2965
+ return true;
2966
+ }
2967
+ const staleMs = directoryName === GO_BUILD_CACHE_MAINTENANCE_DIR
2968
+ ? GO_BUILD_CACHE_MAINTENANCE_STALE_MS
2969
+ : GO_BUILD_CACHE_COORDINATION_STALE_MS;
2970
+ return age <= staleMs;
2971
+ }
2972
+ catch (error) {
2973
+ return error.code !== "ENOENT";
2312
2974
  }
2313
2975
  }
2314
2976
  function collectPluginCacheEntries(root, now) {
@@ -2370,6 +3032,24 @@ function readTimestamp(file) {
2370
3032
  return null;
2371
3033
  }
2372
3034
  }
3035
+ /** Replace cache metadata without following a pre-existing link or hard link. */
3036
+ function replaceCacheMetadataFile(file, contents) {
3037
+ const temporary = node_path_1.default.join(node_path_1.default.dirname(file), `.${node_path_1.default.basename(file)}.${process.pid}-${node_crypto_1.default
3038
+ .randomBytes(16)
3039
+ .toString("hex")}.tmp`);
3040
+ try {
3041
+ node_fs_1.default.writeFileSync(temporary, contents, { encoding: "utf8", flag: "wx" });
3042
+ // rename replaces the directory entry itself. Unlike writeFile(file), it
3043
+ // cannot follow a symlink or mutate another hard link to the old inode.
3044
+ node_fs_1.default.renameSync(temporary, file);
3045
+ }
3046
+ finally {
3047
+ try {
3048
+ node_fs_1.default.rmSync(temporary, { force: true });
3049
+ }
3050
+ catch { }
3051
+ }
3052
+ }
2373
3053
  function directorySize(dir) {
2374
3054
  let total = 0;
2375
3055
  let entries;
@@ -2396,9 +3076,11 @@ function directorySize(dir) {
2396
3076
  function removeCacheEntry(entry) {
2397
3077
  try {
2398
3078
  node_fs_1.default.rmSync(entry.dir, { recursive: true, force: true });
3079
+ return !node_fs_1.default.existsSync(entry.dir);
2399
3080
  }
2400
3081
  catch {
2401
3082
  // Windows may reject removal while a plugin binary is still running.
3083
+ return false;
2402
3084
  }
2403
3085
  }
2404
3086
  //# sourceMappingURL=buildSourcePlugin.js.map