supercov 0.0.11 → 0.0.13

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 (175) hide show
  1. package/README.md +28 -23
  2. package/bin/supercov.js +8 -19
  3. package/package.json +31 -43
  4. package/{dist → runtime/javascript}/atomic.js +0 -1
  5. package/{dist → runtime/javascript}/launchSupervisor.js +45 -2
  6. package/{dist → runtime/javascript}/nodeAssert.js +0 -1
  7. package/{dist → runtime/javascript}/nodeAssertAdapter.js +0 -1
  8. package/{dist → runtime/javascript}/nodeAssertStrict.js +0 -1
  9. package/{dist → runtime/javascript}/nodeTest.js +0 -1
  10. package/{dist → runtime/javascript}/playwright.js +0 -1
  11. package/{dist → runtime/javascript}/playwrightReporter.js +0 -1
  12. package/{dist → runtime/javascript}/provenance.js +0 -1
  13. package/{dist → runtime/javascript}/register.mjs +3 -2
  14. package/{dist → runtime/javascript}/resolve-loader.mjs +0 -42
  15. package/{dist → runtime/javascript}/runnerEvidence.js +0 -1
  16. package/{dist → runtime/javascript}/transport.js +0 -1
  17. package/{dist → runtime/javascript}/vitest.js +0 -1
  18. package/{dist → runtime/javascript}/vitestReporter.js +0 -1
  19. package/dist/agentJson.d.ts +0 -45
  20. package/dist/agentJson.d.ts.map +0 -1
  21. package/dist/agentJson.js +0 -102
  22. package/dist/agentJson.js.map +0 -1
  23. package/dist/analyze.d.ts +0 -12
  24. package/dist/analyze.d.ts.map +0 -1
  25. package/dist/analyze.js +0 -704
  26. package/dist/analyze.js.map +0 -1
  27. package/dist/atomic.d.ts +0 -6
  28. package/dist/atomic.d.ts.map +0 -1
  29. package/dist/atomic.js.map +0 -1
  30. package/dist/buildCache.d.ts +0 -14
  31. package/dist/buildCache.d.ts.map +0 -1
  32. package/dist/buildCache.js +0 -84
  33. package/dist/buildCache.js.map +0 -1
  34. package/dist/cli.d.ts +0 -2
  35. package/dist/cli.d.ts.map +0 -1
  36. package/dist/cli.js +0 -797
  37. package/dist/cli.js.map +0 -1
  38. package/dist/directInstrumenter.d.ts +0 -8
  39. package/dist/directInstrumenter.d.ts.map +0 -1
  40. package/dist/directInstrumenter.js +0 -112
  41. package/dist/directInstrumenter.js.map +0 -1
  42. package/dist/engineEvidence.d.ts +0 -9
  43. package/dist/engineEvidence.d.ts.map +0 -1
  44. package/dist/engineEvidence.js +0 -31
  45. package/dist/engineEvidence.js.map +0 -1
  46. package/dist/engineInstrumenter.d.ts +0 -16
  47. package/dist/engineInstrumenter.d.ts.map +0 -1
  48. package/dist/engineInstrumenter.js +0 -50
  49. package/dist/engineInstrumenter.js.map +0 -1
  50. package/dist/engineProcess.d.ts +0 -11
  51. package/dist/engineProcess.d.ts.map +0 -1
  52. package/dist/engineProcess.js +0 -59
  53. package/dist/engineProcess.js.map +0 -1
  54. package/dist/esmInterceptor.d.ts +0 -6
  55. package/dist/esmInterceptor.d.ts.map +0 -1
  56. package/dist/esmInterceptor.js +0 -63
  57. package/dist/esmInterceptor.js.map +0 -1
  58. package/dist/evidenceArchive.d.ts +0 -38
  59. package/dist/evidenceArchive.d.ts.map +0 -1
  60. package/dist/evidenceArchive.js +0 -156
  61. package/dist/evidenceArchive.js.map +0 -1
  62. package/dist/instrumenter.d.ts +0 -18
  63. package/dist/instrumenter.d.ts.map +0 -1
  64. package/dist/instrumenter.js +0 -1644
  65. package/dist/instrumenter.js.map +0 -1
  66. package/dist/integrity.d.ts +0 -10
  67. package/dist/integrity.d.ts.map +0 -1
  68. package/dist/integrity.js +0 -213
  69. package/dist/integrity.js.map +0 -1
  70. package/dist/launchSupervisor.d.ts +0 -38
  71. package/dist/launchSupervisor.d.ts.map +0 -1
  72. package/dist/launchSupervisor.js.map +0 -1
  73. package/dist/merge.d.ts +0 -2
  74. package/dist/merge.d.ts.map +0 -1
  75. package/dist/merge.js +0 -119
  76. package/dist/merge.js.map +0 -1
  77. package/dist/nodeAssert.d.ts +0 -31
  78. package/dist/nodeAssert.d.ts.map +0 -1
  79. package/dist/nodeAssert.js.map +0 -1
  80. package/dist/nodeAssertAdapter.d.ts +0 -6
  81. package/dist/nodeAssertAdapter.d.ts.map +0 -1
  82. package/dist/nodeAssertAdapter.js.map +0 -1
  83. package/dist/nodeAssertStrict.d.ts +0 -31
  84. package/dist/nodeAssertStrict.d.ts.map +0 -1
  85. package/dist/nodeAssertStrict.js.map +0 -1
  86. package/dist/nodeAssertionInstrumenter.d.ts +0 -8
  87. package/dist/nodeAssertionInstrumenter.d.ts.map +0 -1
  88. package/dist/nodeAssertionInstrumenter.js +0 -271
  89. package/dist/nodeAssertionInstrumenter.js.map +0 -1
  90. package/dist/nodeTest.d.ts +0 -15
  91. package/dist/nodeTest.d.ts.map +0 -1
  92. package/dist/nodeTest.js.map +0 -1
  93. package/dist/playwright.d.ts +0 -9
  94. package/dist/playwright.d.ts.map +0 -1
  95. package/dist/playwright.js.map +0 -1
  96. package/dist/playwrightReporter.d.ts +0 -6
  97. package/dist/playwrightReporter.d.ts.map +0 -1
  98. package/dist/playwrightReporter.js.map +0 -1
  99. package/dist/processDiagnostics.d.ts +0 -34
  100. package/dist/processDiagnostics.d.ts.map +0 -1
  101. package/dist/processDiagnostics.js +0 -155
  102. package/dist/processDiagnostics.js.map +0 -1
  103. package/dist/project.d.ts +0 -21
  104. package/dist/project.d.ts.map +0 -1
  105. package/dist/project.js +0 -331
  106. package/dist/project.js.map +0 -1
  107. package/dist/provenance.d.ts +0 -9
  108. package/dist/provenance.d.ts.map +0 -1
  109. package/dist/provenance.js.map +0 -1
  110. package/dist/query.d.ts +0 -81
  111. package/dist/query.d.ts.map +0 -1
  112. package/dist/query.js +0 -1656
  113. package/dist/query.js.map +0 -1
  114. package/dist/queryCache.d.ts +0 -15
  115. package/dist/queryCache.d.ts.map +0 -1
  116. package/dist/queryCache.js +0 -95
  117. package/dist/queryCache.js.map +0 -1
  118. package/dist/queueAdapters.d.ts +0 -15
  119. package/dist/queueAdapters.d.ts.map +0 -1
  120. package/dist/queueAdapters.js +0 -55
  121. package/dist/queueAdapters.js.map +0 -1
  122. package/dist/register.d.mts +0 -2
  123. package/dist/register.d.mts.map +0 -1
  124. package/dist/register.mjs.map +0 -1
  125. package/dist/reporter.d.ts +0 -13
  126. package/dist/reporter.d.ts.map +0 -1
  127. package/dist/reporter.js +0 -252
  128. package/dist/reporter.js.map +0 -1
  129. package/dist/resolve-loader.d.mts +0 -3
  130. package/dist/resolve-loader.d.mts.map +0 -1
  131. package/dist/resolve-loader.mjs.map +0 -1
  132. package/dist/runAnalysis.d.ts +0 -19
  133. package/dist/runAnalysis.d.ts.map +0 -1
  134. package/dist/runAnalysis.js +0 -171
  135. package/dist/runAnalysis.js.map +0 -1
  136. package/dist/runnerEvidence.d.ts +0 -19
  137. package/dist/runnerEvidence.d.ts.map +0 -1
  138. package/dist/runnerEvidence.js.map +0 -1
  139. package/dist/runtime.d.ts +0 -140
  140. package/dist/runtime.d.ts.map +0 -1
  141. package/dist/runtime.js.map +0 -1
  142. package/dist/runtimeIsolation.d.ts +0 -3
  143. package/dist/runtimeIsolation.d.ts.map +0 -1
  144. package/dist/runtimeIsolation.js +0 -9
  145. package/dist/runtimeIsolation.js.map +0 -1
  146. package/dist/sourceDiscovery.d.ts +0 -9
  147. package/dist/sourceDiscovery.d.ts.map +0 -1
  148. package/dist/sourceDiscovery.js +0 -242
  149. package/dist/sourceDiscovery.js.map +0 -1
  150. package/dist/transport.d.ts +0 -17
  151. package/dist/transport.d.ts.map +0 -1
  152. package/dist/transport.js.map +0 -1
  153. package/dist/types.d.ts +0 -378
  154. package/dist/types.d.ts.map +0 -1
  155. package/dist/types.js +0 -2
  156. package/dist/types.js.map +0 -1
  157. package/dist/vitePlugin.d.ts +0 -13
  158. package/dist/vitePlugin.d.ts.map +0 -1
  159. package/dist/vitePlugin.js +0 -139
  160. package/dist/vitePlugin.js.map +0 -1
  161. package/dist/vitest.d.ts +0 -2
  162. package/dist/vitest.d.ts.map +0 -1
  163. package/dist/vitest.js.map +0 -1
  164. package/dist/vitestReporter.d.ts +0 -43
  165. package/dist/vitestReporter.d.ts.map +0 -1
  166. package/dist/vitestReporter.js.map +0 -1
  167. package/dist/waivers.d.ts +0 -52
  168. package/dist/waivers.d.ts.map +0 -1
  169. package/dist/waivers.js +0 -121
  170. package/dist/waivers.js.map +0 -1
  171. package/dist/workspace.d.ts +0 -117
  172. package/dist/workspace.d.ts.map +0 -1
  173. package/dist/workspace.js +0 -680
  174. package/dist/workspace.js.map +0 -1
  175. /package/{dist → runtime/javascript}/runtime.js +0 -0
package/dist/workspace.js DELETED
@@ -1,680 +0,0 @@
1
- import { copyFileSync, constants, existsSync, lstatSync, mkdirSync, openSync, closeSync, fsyncSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, symlinkSync, statSync, writeFileSync, } from "node:fs";
2
- import { spawn } from "node:child_process";
3
- import { randomUUID } from "node:crypto";
4
- import { tmpdir } from "node:os";
5
- import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
6
- import { atomicRenameSync, atomicWriteFileSync } from "./atomic.js";
7
- export const RUN_STORE_CONTRACT_VERSION = 1;
8
- const TERMINAL = new Set([
9
- "complete",
10
- "failed",
11
- "interrupted",
12
- "abandoned",
13
- ]);
14
- const ROOT_EXCLUSIONS = new Set([
15
- ".cache",
16
- ".git",
17
- ".supercov",
18
- ".mcdc-pool",
19
- "node_modules",
20
- "build",
21
- "dist",
22
- ".next",
23
- ".nuxt",
24
- ".output",
25
- "coverage",
26
- "playwright-report",
27
- "test-results",
28
- ]);
29
- const NESTED_SUPERCOV_EXCLUSIONS = new Set([".supercov", ".mcdc-pool"]);
30
- const TRASH_DIRECTORY = ".supercov/.trash";
31
- /**
32
- * Unlinking a large tree (workspace copies, per-attempt evidence) can take
33
- * minutes of uninterruptible I/O — observed at 26+ minutes on a real project.
34
- * Removal must never block the command: rename the tree into the store's
35
- * trash (atomic on the same filesystem, so the path disappears instantly) and
36
- * let a detached deleter unlink it after this process has already returned.
37
- * A crash between rename and unlink just leaves the entry in the trash, and
38
- * every later invocation sweeps whatever it finds there.
39
- */
40
- export function removeStoredTreeDeferred(root, target) {
41
- const resolvedRoot = resolve(root);
42
- const resolvedTarget = resolve(target);
43
- if (!existsSync(resolvedTarget))
44
- return undefined;
45
- const trash = resolve(root, TRASH_DIRECTORY);
46
- const store = resolve(resolvedRoot, ".supercov");
47
- const container = workspaceContainerPath(resolvedRoot);
48
- const inStore = resolvedTarget !== store &&
49
- inside(store, resolvedTarget) &&
50
- !inside(trash, resolvedTarget);
51
- const inWorkspaceContainer = isWorkspaceContainer(container) && inside(container, resolvedTarget);
52
- if (!inStore && !inWorkspaceContainer)
53
- throw new Error(`Refusing to defer removal outside Supercov-owned storage: ${resolvedTarget}`);
54
- mkdirSync(trash, { recursive: true });
55
- const destination = resolve(trash, `${process.pid}-${randomUUID()}`);
56
- try {
57
- renameSync(resolvedTarget, destination);
58
- }
59
- catch (error) {
60
- const code = error.code ?? "unknown";
61
- throw new Error(`Could not move Supercov data into deferred trash (${code}): ${resolvedTarget}`, { cause: error });
62
- }
63
- return destination;
64
- }
65
- /** The detached deleter's whole program; also executed verbatim by tests. */
66
- export const TRASH_DELETER_SCRIPT = [
67
- `const { closeSync, openSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } = require("node:fs");`,
68
- `const { resolve } = require("node:path");`,
69
- `const trash = process.argv[1];`,
70
- `const lock = resolve(trash, ".deleter.lock");`,
71
- `const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };`,
72
- `try {`,
73
- ` const existing = Number(readFileSync(lock, "utf8"));`,
74
- ` if (Number.isSafeInteger(existing) && existing > 0 && alive(existing)) process.exit(0);`,
75
- ` try { unlinkSync(lock); } catch {}`,
76
- `} catch {}`,
77
- `let descriptor;`,
78
- `try { descriptor = openSync(lock, "wx"); writeFileSync(descriptor, String(process.pid)); closeSync(descriptor); } catch { process.exit(0); }`,
79
- `let entries = [];`,
80
- `try { entries = readdirSync(trash); } catch {}`,
81
- `for (const entry of entries) {`,
82
- ` if (entry === ".deleter.lock") continue;`,
83
- ` try { rmSync(resolve(trash, entry), { recursive: true, force: true, maxRetries: 3 }); } catch {}`,
84
- `}`,
85
- `try { unlinkSync(lock); } catch {}`,
86
- ].join("\n");
87
- /** Unlink everything in the trash without making anyone wait for it. */
88
- export function spawnTrashDeleter(root) {
89
- const trash = resolve(root, TRASH_DIRECTORY);
90
- try {
91
- if (readdirSync(trash).length === 0)
92
- return;
93
- }
94
- catch {
95
- return;
96
- }
97
- try {
98
- spawn(process.execPath, ["-e", TRASH_DELETER_SCRIPT, trash], {
99
- // A detached process must never inherit a project-owned directory as
100
- // its cwd. Windows keeps that directory open and rejects the final
101
- // recursive removal with EPERM until the deleter exits.
102
- cwd: tmpdir(),
103
- detached: true,
104
- stdio: "ignore",
105
- }).unref();
106
- }
107
- catch {
108
- // Spawning is best-effort; the next invocation sweeps the same trash.
109
- }
110
- }
111
- /** Marks `supercov/` as ours, so a project may own a directory by that name. */
112
- const WORKSPACE_CONTAINER_MARKER = ".supercov-workspace-store";
113
- /**
114
- * The instrumented workspace deliberately lives outside the dotted store.
115
- * Widely used libraries treat *any* dot-prefixed path segment as a hidden
116
- * dotfile — `send` (and therefore `express.static`, `serve-static` and
117
- * `res.sendFile`) answers 404 for them by default — so an application under
118
- * test cannot serve its own files from a dotted workspace. Copied test files
119
- * are kept out of the user's ordinary runner discovery by
120
- * `pruneCachedWorkspaceSources` at run end, not by hiding the path.
121
- */
122
- export function workspaceContainerPath(root) {
123
- return resolve(root, "supercov");
124
- }
125
- /** True only for a `supercov/` directory this tool created. */
126
- function isWorkspaceContainer(path) {
127
- return (basename(path) === "supercov" &&
128
- existsSync(resolve(path, WORKSPACE_CONTAINER_MARKER)));
129
- }
130
- function ensureWorkspaceContainer(root) {
131
- const container = workspaceContainerPath(root);
132
- mkdirSync(container, { recursive: true });
133
- // Self-ignoring, so the directory never reaches the user's diff, and marked
134
- // so source copying can distinguish it from a project's own `supercov/`.
135
- for (const [file, contents] of [
136
- [".gitignore", "*\n"],
137
- [WORKSPACE_CONTAINER_MARKER, "Supercov instrumented workspace. Safe to delete.\n"],
138
- ]) {
139
- const path = resolve(container, file);
140
- if (!existsSync(path))
141
- atomicWriteFileSync(path, contents);
142
- }
143
- return container;
144
- }
145
- function processExists(pid) {
146
- if (!Number.isSafeInteger(pid) || pid <= 0)
147
- return false;
148
- try {
149
- process.kill(pid, 0);
150
- return true;
151
- }
152
- catch (error) {
153
- return error.code === "EPERM";
154
- }
155
- }
156
- function readJson(path) {
157
- try {
158
- return JSON.parse(readFileSync(path, "utf8"));
159
- }
160
- catch {
161
- return undefined;
162
- }
163
- }
164
- function statePath(root, runId) {
165
- return resolve(root, ".supercov/work", runId, "state.json");
166
- }
167
- export function isolatedWorkspacePath(root, runId) {
168
- return resolve(root, ".supercov/work", runId, basename(root));
169
- }
170
- /**
171
- * Stable isolated namespace used as a live mount by VM/container runners.
172
- * Its path must remain stable so providers whose snapshot key includes mount
173
- * paths can reuse an instrumented snapshot across equivalent coverage runs.
174
- * The project lock makes refreshes single-writer.
175
- */
176
- export function cachedWorkspacePath(root) {
177
- return resolve(workspaceContainerPath(root), "workspace", basename(root));
178
- }
179
- /**
180
- * Remove the copied source and test files from the stable cached workspace
181
- * once a run ends. Between runs those copies are pure liability: the next
182
- * refresh re-copies them anyway, but an ordinary test runner invoked at the
183
- * project root (whose default discovery does not exclude dot-directories —
184
- * Vitest 4 excludes only node_modules and .git) would find the copied test
185
- * files and silently double-count the user's suite. Keep only what the next
186
- * run genuinely reuses: dependency symlinks, the generated adapter directory,
187
- * and the instrumented build artifacts the build cache declares.
188
- */
189
- export function pruneCachedWorkspaceSources(root) {
190
- const workspace = cachedWorkspacePath(root);
191
- if (!existsSync(workspace))
192
- return [];
193
- const metadata = readJson(resolve(workspace, ".supercov/build-cache.json"));
194
- const keepRoots = new Set(["node_modules", ".supercov"]);
195
- for (const artifact of metadata?.artifactPaths ?? []) {
196
- const top = artifact.split(/[\\/]/)[0];
197
- if (top)
198
- keepRoots.add(top);
199
- }
200
- const removed = [];
201
- for (const entry of readdirSync(workspace, { withFileTypes: true })) {
202
- if (keepRoots.has(entry.name))
203
- continue;
204
- removeStoredTreeDeferred(root, resolve(workspace, entry.name));
205
- removed.push(entry.name);
206
- }
207
- if (removed.length > 0)
208
- spawnTrashDeleter(root);
209
- return removed.sort();
210
- }
211
- function cacheTransactionPrefix(root, kind) {
212
- return `.${basename(root)}.${kind}-`;
213
- }
214
- function cacheTransactionPath(root, kind) {
215
- const workspace = cachedWorkspacePath(root);
216
- return resolve(dirname(workspace), `${cacheTransactionPrefix(root, kind)}${process.pid}-${randomUUID()}`);
217
- }
218
- /**
219
- * Recover the stable cache at transaction boundaries that SIGKILL or a host
220
- * crash can interrupt. A staging tree is never live. A previous tree is
221
- * restored only when the stable name is absent; otherwise it is obsolete.
222
- */
223
- export function recoverCachedWorkspace(root) {
224
- const workspace = cachedWorkspacePath(root);
225
- const parent = dirname(workspace);
226
- if (!existsSync(parent))
227
- return { restoredPrevious: false, removedStaging: 0, removedPrevious: 0 };
228
- const entries = readdirSync(parent, { withFileTypes: true });
229
- const stagingPrefix = cacheTransactionPrefix(root, "staging");
230
- const previousPrefix = cacheTransactionPrefix(root, "previous");
231
- const staging = entries
232
- .filter((entry) => entry.name.startsWith(stagingPrefix))
233
- .map((entry) => resolve(parent, entry.name));
234
- const previous = entries
235
- .filter((entry) => entry.name.startsWith(previousPrefix) && entry.isDirectory())
236
- .map((entry) => resolve(parent, entry.name))
237
- .sort((left, right) => lstatSync(right).mtimeMs - lstatSync(left).mtimeMs);
238
- const invalidPrevious = entries
239
- .filter((entry) => entry.name.startsWith(previousPrefix) && !entry.isDirectory())
240
- .map((entry) => resolve(parent, entry.name));
241
- let restoredPrevious = false;
242
- if (!existsSync(workspace) && previous[0]) {
243
- atomicRenameSync(previous[0], workspace);
244
- previous.shift();
245
- restoredPrevious = true;
246
- }
247
- for (const path of staging)
248
- removeStoredTreeDeferred(root, path);
249
- for (const path of previous)
250
- removeStoredTreeDeferred(root, path);
251
- for (const path of invalidPrevious)
252
- removeStoredTreeDeferred(root, path);
253
- if (staging.length + previous.length + invalidPrevious.length > 0)
254
- spawnTrashDeleter(root);
255
- return {
256
- restoredPrevious,
257
- removedStaging: staging.length,
258
- removedPrevious: previous.length + invalidPrevious.length,
259
- };
260
- }
261
- export function writeRunState(root, runId, state) {
262
- const complete = { ...state, updatedAt: new Date().toISOString() };
263
- atomicWriteFileSync(statePath(root, runId), `${JSON.stringify(complete, null, 2)}\n`);
264
- return complete;
265
- }
266
- export function updateRunState(root, runId, update) {
267
- const current = readJson(statePath(root, runId));
268
- if (!current)
269
- throw new Error(`Run state is missing for ${runId}`);
270
- const next = {
271
- ...current,
272
- ...update,
273
- updatedAt: new Date().toISOString(),
274
- };
275
- atomicWriteFileSync(statePath(root, runId), `${JSON.stringify(next, null, 2)}\n`);
276
- return next;
277
- }
278
- /**
279
- * A published run is self-contained only once its raw-evidence archive exists.
280
- * At that point loose evidence and per-run state are transactional leftovers,
281
- * not user history. Derive every deletion target from root + run ID.
282
- */
283
- export function finalizePublishedRunStorage(root, runId) {
284
- const runDirectory = resolve(root, ".supercov/runs", runId);
285
- const publishedRun = readJson(resolve(runDirectory, "run.json"));
286
- if (publishedRun?.id !== runId ||
287
- !existsSync(resolve(runDirectory, "evidence.raw.gz"))) {
288
- return false;
289
- }
290
- removeStoredTreeDeferred(root, resolve(root, ".supercov/evidence", runId));
291
- removeStoredTreeDeferred(root, resolve(root, ".supercov/work", runId));
292
- spawnTrashDeleter(root);
293
- return true;
294
- }
295
- /** Recover dead runs and finish any publication whose atomic rename landed. */
296
- export function recoverAbandonedRuns(root) {
297
- // A prior detached deleter may have been killed with the host. Sweep its
298
- // durable trash before inspecting current run state.
299
- spawnTrashDeleter(root);
300
- const workRoot = resolve(root, ".supercov/work");
301
- if (!existsSync(workRoot))
302
- return [];
303
- const recovered = [];
304
- for (const entry of readdirSync(workRoot, { withFileTypes: true })) {
305
- if (!entry.isDirectory())
306
- continue;
307
- const path = statePath(root, entry.name);
308
- const state = readJson(path);
309
- if (!state)
310
- continue;
311
- if (TERMINAL.has(state.status)) {
312
- finalizePublishedRunStorage(root, entry.name);
313
- continue;
314
- }
315
- if (processExists(state.pid))
316
- continue;
317
- // Never trust a persisted path as a deletion target. A partially written
318
- // or manually edited state file cannot widen cleanup beyond this run's
319
- // deterministic workspace namespace.
320
- removeStoredTreeDeferred(root, isolatedWorkspacePath(root, entry.name));
321
- removeStoredTreeDeferred(root, resolve(root, ".supercov/work", entry.name, "run-publication"));
322
- if (finalizePublishedRunStorage(root, entry.name)) {
323
- // The run directory is published by one atomic rename before the run
324
- // state flips terminal. Its run.json is the durable terminal record, so
325
- // recovery completes cleanup without recreating disposable state.
326
- }
327
- else {
328
- // Evidence moved out of the disposable workspace before run
329
- // generation is not a visible run. Remove that orphan on recovery so a
330
- // hard kill cannot accumulate partial run data indefinitely.
331
- removeStoredTreeDeferred(root, resolve(root, ".supercov/evidence", entry.name));
332
- updateRunState(root, entry.name, {
333
- status: "abandoned",
334
- error: `Recovered after process ${state.pid} exited without cleanup`,
335
- });
336
- }
337
- recovered.push(entry.name);
338
- }
339
- if (recovered.length > 0)
340
- spawnTrashDeleter(root);
341
- return recovered.sort();
342
- }
343
- export function acquireProjectLock(root, runId) {
344
- const lockPath = resolve(root, ".supercov/locks/active.json");
345
- mkdirSync(dirname(lockPath), { recursive: true });
346
- for (let attempt = 0; attempt < 2; attempt += 1) {
347
- let descriptor;
348
- try {
349
- descriptor = openSync(lockPath, "wx", 0o600);
350
- const payload = `${JSON.stringify({ runId, pid: process.pid, startedAt: new Date().toISOString() }, null, 2)}\n`;
351
- writeFileSync(descriptor, payload);
352
- fsyncSync(descriptor);
353
- closeSync(descriptor);
354
- descriptor = undefined;
355
- let released = false;
356
- return {
357
- path: lockPath,
358
- release() {
359
- if (released)
360
- return;
361
- released = true;
362
- const owner = readJson(lockPath);
363
- if (owner?.runId === runId && owner.pid === process.pid)
364
- rmSync(lockPath, { force: true });
365
- },
366
- };
367
- }
368
- catch (error) {
369
- if (descriptor !== undefined)
370
- closeSync(descriptor);
371
- const failure = error;
372
- if (failure.code !== "EEXIST")
373
- throw error;
374
- const owner = readJson(lockPath);
375
- if (owner?.pid && processExists(owner.pid)) {
376
- throw new Error(`Coverage run ${owner.runId ?? "unknown"} is already active in this project (pid ${owner.pid})`);
377
- }
378
- if (!owner) {
379
- const ageMs = Date.now() - statSync(lockPath).mtimeMs;
380
- if (ageMs < 30_000)
381
- throw new Error("A coverage run is currently acquiring the project lock");
382
- }
383
- rmSync(lockPath, { force: true });
384
- }
385
- }
386
- throw new Error("Could not acquire the Supercov project lock");
387
- }
388
- function inside(root, path) {
389
- const local = relative(root, path);
390
- return (local === "" ||
391
- (!local.startsWith(`..${sep}`) && local !== ".." && !isAbsolute(local)));
392
- }
393
- function copyTree(source, destination, root = false, sourceRoot = source, destinationRoot = destination, hooks = {}, finalDestinationRoot = destinationRoot) {
394
- mkdirSync(destination, { recursive: true });
395
- for (const entry of readdirSync(source, { withFileTypes: true })) {
396
- if ((root && ROOT_EXCLUSIONS.has(entry.name)) ||
397
- NESTED_SUPERCOV_EXCLUSIONS.has(entry.name))
398
- continue;
399
- const from = resolve(source, entry.name);
400
- // Never copy our own workspace into itself. Matched by marker rather than
401
- // by name so a project may legitimately own a `supercov/` directory.
402
- if (entry.isDirectory() && isWorkspaceContainer(from))
403
- continue;
404
- const to = resolve(destination, entry.name);
405
- const stat = lstatSync(from);
406
- if (stat.isDirectory())
407
- copyTree(from, to, false, sourceRoot, destinationRoot, hooks, finalDestinationRoot);
408
- else if (stat.isSymbolicLink()) {
409
- const link = readlinkSync(from);
410
- const lexicalTarget = isAbsolute(link)
411
- ? resolve(link)
412
- : resolve(dirname(from), link);
413
- let finalTarget;
414
- try {
415
- finalTarget = realpathSync(from);
416
- }
417
- catch (error) {
418
- if (error.code !== "ENOENT")
419
- throw error;
420
- }
421
- if (!inside(sourceRoot, lexicalTarget) ||
422
- (finalTarget && !inside(realpathSync(sourceRoot), finalTarget))) {
423
- throw new Error(`Refusing to preserve symlink outside the isolated project: ${relative(sourceRoot, from)} -> ${link}`);
424
- }
425
- const targetIsDirectory = statSync(from).isDirectory();
426
- const relocatedAbsoluteTarget = resolve(destinationRoot, relative(sourceRoot, lexicalTarget));
427
- // Windows junctions are absolute. Point them at the stable name that the
428
- // staging tree will have after publication, not the staging name that is
429
- // about to disappear. POSIX links remain relative and survive rename.
430
- const isolatedLink = process.platform === "win32" && targetIsDirectory
431
- ? resolve(finalDestinationRoot, relative(sourceRoot, lexicalTarget))
432
- : isAbsolute(link)
433
- ? relative(dirname(to), relocatedAbsoluteTarget)
434
- : link;
435
- symlinkSync(isolatedLink, to, process.platform === "win32"
436
- ? targetIsDirectory
437
- ? "junction"
438
- : "file"
439
- : undefined);
440
- }
441
- else if (stat.isFile()) {
442
- if (hooks.copyFile)
443
- hooks.copyFile(from, to);
444
- else
445
- copyFileSync(from, to, constants.COPYFILE_FICLONE);
446
- }
447
- else
448
- throw new Error(`Unsupported filesystem entry in isolated project: ${relative(sourceRoot, from)}`);
449
- }
450
- }
451
- /** Create a copy-on-write project snapshot whose build outputs are disposable. */
452
- export function prepareIsolatedWorkspace(root, runId) {
453
- const workspace = isolatedWorkspacePath(root, runId);
454
- if (removeStoredTreeDeferred(root, workspace))
455
- spawnTrashDeleter(root);
456
- copyTree(root, workspace, true);
457
- const nodeModules = resolve(root, "node_modules");
458
- if (existsSync(nodeModules)) {
459
- // Keep the node_modules mount point itself as a real directory. VM-based
460
- // runners commonly layer a Linux dependency cache onto the mounted
461
- // workspace's node_modules path;
462
- // making the mount point an external symlink causes virtio-fs's safe
463
- // `opaque` policy to reject LOOKUP with EACCES before the nested mount can
464
- // be attached. Per-entry links still give host-side builds zero-copy
465
- // access to the user's installed dependencies and are hidden by the VM's
466
- // nested mount.
467
- const isolatedNodeModules = resolve(workspace, "node_modules");
468
- mkdirSync(isolatedNodeModules, { recursive: true });
469
- for (const entry of readdirSync(nodeModules, { withFileTypes: true })) {
470
- const target = resolve(nodeModules, entry.name);
471
- symlinkSync(target, resolve(isolatedNodeModules, entry.name), process.platform === "win32"
472
- ? entry.isDirectory()
473
- ? "junction"
474
- : "file"
475
- : undefined);
476
- }
477
- }
478
- return workspace;
479
- }
480
- /** Refresh the stable, disposable instrumented-build namespace. */
481
- export function prepareCachedWorkspace(root, hooks = {}) {
482
- const workspace = cachedWorkspacePath(root);
483
- ensureWorkspaceContainer(root);
484
- recoverCachedWorkspace(root);
485
- const staging = cacheTransactionPath(root, "staging");
486
- const previous = cacheTransactionPath(root, "previous");
487
- let movedPrevious = false;
488
- try {
489
- copyTree(root, staging, true, root, staging, hooks, workspace);
490
- const nodeModules = resolve(root, "node_modules");
491
- if (existsSync(nodeModules)) {
492
- const isolatedNodeModules = resolve(staging, "node_modules");
493
- mkdirSync(isolatedNodeModules, { recursive: true });
494
- for (const entry of readdirSync(nodeModules, { withFileTypes: true })) {
495
- symlinkSync(resolve(nodeModules, entry.name), resolve(isolatedNodeModules, entry.name), process.platform === "win32"
496
- ? entry.isDirectory()
497
- ? "junction"
498
- : "file"
499
- : undefined);
500
- }
501
- }
502
- for (const requested of hooks.reusePaths ?? []) {
503
- const from = resolve(workspace, requested);
504
- const to = resolve(staging, requested);
505
- if (!inside(workspace, from) ||
506
- from === workspace ||
507
- !inside(staging, to) ||
508
- to === staging ||
509
- !existsSync(from)) {
510
- throw new Error(`Refusing to reuse unexpected build path: ${requested}`);
511
- }
512
- const stat = lstatSync(from);
513
- if (stat.isDirectory())
514
- copyTree(from, to, false, workspace, staging, hooks, workspace);
515
- else if (stat.isFile()) {
516
- mkdirSync(dirname(to), { recursive: true });
517
- if (hooks.copyFile)
518
- hooks.copyFile(from, to);
519
- else
520
- copyFileSync(from, to, constants.COPYFILE_FICLONE);
521
- }
522
- else {
523
- throw new Error(`Unsupported reusable build entry: ${requested}`);
524
- }
525
- }
526
- hooks.beforePublish?.(staging);
527
- // Keep the last complete cache available for the whole refresh. These two
528
- // same-filesystem renames are the only publication boundary. Recovery
529
- // restores `previous` if the process is killed in the narrow gap.
530
- const publishRename = hooks.rename ?? atomicRenameSync;
531
- if (existsSync(workspace)) {
532
- publishRename(workspace, previous);
533
- movedPrevious = true;
534
- hooks.afterPreviousMoved?.(previous);
535
- }
536
- publishRename(staging, workspace);
537
- hooks.afterPublished?.(workspace);
538
- }
539
- catch (error) {
540
- if (movedPrevious && !existsSync(workspace) && existsSync(previous)) {
541
- try {
542
- atomicRenameSync(previous, workspace);
543
- }
544
- catch {
545
- // Leave the previous tree for deterministic recovery next invocation.
546
- }
547
- }
548
- throw error;
549
- }
550
- finally {
551
- if (removeStoredTreeDeferred(root, staging))
552
- spawnTrashDeleter(root);
553
- }
554
- // Failure to remove an obsolete previous generation must not invalidate the
555
- // newly published cache. The next invocation and `supercov clean` both
556
- // remove it deterministically.
557
- try {
558
- if (removeStoredTreeDeferred(root, previous))
559
- spawnTrashDeleter(root);
560
- }
561
- catch {
562
- // Deliberately retained for recoverCachedWorkspace().
563
- }
564
- return workspace;
565
- }
566
- export function removeIsolatedWorkspace(root, runId) {
567
- const workspace = isolatedWorkspacePath(root, runId);
568
- const relativePath = relative(resolve(root, ".supercov/work"), workspace);
569
- if (relativePath.startsWith("..") || relativePath.split(sep).length < 2) {
570
- throw new Error(`Refusing to remove unexpected workspace path: ${workspace}`);
571
- }
572
- if (removeStoredTreeDeferred(root, workspace))
573
- spawnTrashDeleter(root);
574
- }
575
- /** Deterministic retention: IDs sort newest-first because run IDs are UTC. */
576
- function cleanCoverageStorageLocked(root, options, removeBuildCache) {
577
- recoverAbandonedRuns(root);
578
- const runsRoot = resolve(root, ".supercov/runs");
579
- const workRoot = resolve(root, ".supercov/work");
580
- const evidenceRoot = resolve(root, ".supercov/evidence");
581
- const bases = [runsRoot, workRoot, evidenceRoot];
582
- const ids = new Set();
583
- for (const base of bases) {
584
- if (!existsSync(base))
585
- continue;
586
- for (const entry of readdirSync(base, { withFileTypes: true }))
587
- if (entry.isDirectory())
588
- ids.add(entry.name);
589
- }
590
- const ordered = [...ids].sort((left, right) => right.localeCompare(left));
591
- const active = new Set(ordered.filter((id) => {
592
- const state = readJson(resolve(root, ".supercov/work", id, "state.json"));
593
- return Boolean(state && !TERMINAL.has(state.status));
594
- }));
595
- const published = existsSync(runsRoot)
596
- ? readdirSync(runsRoot, { withFileTypes: true })
597
- .filter((entry) => entry.isDirectory())
598
- .map((entry) => entry.name)
599
- .sort((left, right) => right.localeCompare(left))
600
- : [];
601
- const retained = new Set(published.filter((id) => !active.has(id)).slice(0, Math.max(0, options.keep)));
602
- const removedRuns = [];
603
- const removedWorkspaces = [];
604
- const removedEvidence = [];
605
- for (const id of ordered) {
606
- const work = resolve(root, ".supercov/work", id);
607
- const state = readJson(resolve(work, "state.json"));
608
- if (active.has(id))
609
- continue;
610
- const hasPublishedRun = existsSync(resolve(runsRoot, id));
611
- const removeHistory = hasPublishedRun && !retained.has(id);
612
- const removeTransientWork = existsSync(work) && (!state || TERMINAL.has(state.status));
613
- if (removeTransientWork) {
614
- removedWorkspaces.push(id);
615
- if (!options.dryRun)
616
- removeStoredTreeDeferred(root, work);
617
- }
618
- const hasLooseEvidence = existsSync(resolve(evidenceRoot, id));
619
- if (hasLooseEvidence && (!hasPublishedRun || removeHistory)) {
620
- removedEvidence.push(id);
621
- if (!options.dryRun)
622
- removeStoredTreeDeferred(root, resolve(evidenceRoot, id));
623
- }
624
- if (removeHistory) {
625
- removedRuns.push(id);
626
- if (!options.dryRun)
627
- removeStoredTreeDeferred(root, resolve(runsRoot, id));
628
- }
629
- }
630
- const buildCache = workspaceContainerPath(root);
631
- // Releases before the workspace was moved out of the dotted store used
632
- // both of these names. `clean` is the explicit destructive command, so it
633
- // must migrate them too; otherwise a project can retain millions of files
634
- // in a layout the current engine will never reuse. `prune` deliberately
635
- // preserves every cache generation.
636
- const legacyBuildCaches = [
637
- resolve(root, ".supercov/.cache"),
638
- resolve(root, ".supercov/cache"),
639
- ];
640
- const removableBuildCaches = removeBuildCache && active.size === 0
641
- ? [
642
- ...(existsSync(buildCache) && isWorkspaceContainer(buildCache)
643
- ? [buildCache]
644
- : []),
645
- ...legacyBuildCaches.filter((path) => existsSync(path)),
646
- ]
647
- : [];
648
- const removedBuildCache = removableBuildCaches.length > 0;
649
- if (!options.dryRun)
650
- for (const path of removableBuildCaches)
651
- removeStoredTreeDeferred(root, path);
652
- if (!options.dryRun &&
653
- (removedRuns.length > 0 ||
654
- removedWorkspaces.length > 0 ||
655
- removedEvidence.length > 0 ||
656
- removedBuildCache))
657
- spawnTrashDeleter(root);
658
- return { removedRuns, removedWorkspaces, removedEvidence, removedBuildCache };
659
- }
660
- /** Cleanup is itself a project-wide transaction, so it cannot race a run. */
661
- export function cleanCoverageStorage(root, options) {
662
- const lock = acquireProjectLock(root, `clean-${process.pid}-${randomUUID()}`);
663
- try {
664
- return cleanCoverageStorageLocked(root, options, true);
665
- }
666
- finally {
667
- lock.release();
668
- }
669
- }
670
- /** Explicit history retention that deliberately preserves the shared cache. */
671
- export function pruneCoverageStorage(root, options) {
672
- const lock = acquireProjectLock(root, `prune-${process.pid}-${randomUUID()}`);
673
- try {
674
- return cleanCoverageStorageLocked(root, options, false);
675
- }
676
- finally {
677
- lock.release();
678
- }
679
- }
680
- //# sourceMappingURL=workspace.js.map