supercov 4.24.8 → 4.25.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.
@@ -0,0 +1,6 @@
1
+ // The thread Node runs module hooks in, where Playwright compiles ES modules
2
+ // and writes them into its cache: written whole there too (atomic.mjs says
3
+ // why). Supercov's own hooks run in the importing thread where Node can, so
4
+ // this goes to that thread on its own.
5
+ import { writePlaywrightCacheWhole } from "./atomic.mjs";
6
+ writePlaywrightCacheWhole();
@@ -14,6 +14,16 @@ import { installLaunchSupervisor, wrapImportedCapability } from "./launchSupervi
14
14
  import { __supercovBindCapabilityWrapper } from "./capability.mjs";
15
15
  import { adaptRunnerEntry, noteSkippedCoverageThresholds } from "./runnerEntry.mjs";
16
16
  import { writePlaywrightCacheWhole } from "./atomic.mjs";
17
+ // Supercov runs Node with --enable-source-maps so a stack through an
18
+ // instrumented copy names the project's file. The flag also reads the maps
19
+ // every package ships, which a plain run never does: a third of the CPU a
20
+ // measured Vitest suite added. Where Node can, Supercov's flag maps the
21
+ // project's files alone, unless the command asked for maps itself: a second
22
+ // flag in NODE_OPTIONS, or one on its own command line.
23
+ if ((process.env.NODE_OPTIONS ?? "").split(/\s+/).filter((option) => option === "--enable-source-maps").length === 1 &&
24
+ !process.execArgv.includes("--enable-source-maps") &&
25
+ typeof Module.setSourceMapsSupport === "function")
26
+ Module.setSourceMapsSupport(true, { nodeModules: false, generatedCode: true });
17
27
  installLaunchSupervisor();
18
28
  // Playwright's compile cache, written whole (atomic.mjs says why).
19
29
  writePlaywrightCacheWhole();
@@ -195,8 +205,16 @@ const nodeAssertWrapper = new URL("./nodeAssert.mjs", import.meta.url).href;
195
205
  const nodeAssertStrictWrapper = new URL("./nodeAssertStrict.mjs", import.meta.url).href;
196
206
  const isPlaywrightEntrypoint = /\/(?:node_modules\/\.bin\/playwright|node_modules\/(?:@playwright\/test|playwright)\/(?:cli\.js|.*\/program\.js))$/.test(entrypoint);
197
207
  const isJestEntrypoint = /\/node_modules\/(?:\.bin\/jest|(?:jest|jest-cli)\/bin\/jest\.js)$/.test(entrypoint);
198
- if (generatedPlaywrightConfig && isPlaywrightEntrypoint)
208
+ if (generatedPlaywrightConfig && isPlaywrightEntrypoint) {
199
209
  process.env.SUPERCOV_INSIDE_PLAYWRIGHT = "1";
210
+ // Every process the runner starts inherits SUPERCOV_INSIDE_PLAYWRIGHT, the
211
+ // app server and each mock server too, and loading the wrapper cost each of
212
+ // them 190 ms before its first line: a server Playwright waits on missed its
213
+ // first look, about 4 s a run. The runner's children are marked as not test
214
+ // workers, and the launch supervisor marks the ones it forks from its worker
215
+ // entry as workers, so only those load the wrapper.
216
+ process.env.SUPERCOV_PLAYWRIGHT_WORKER = "0";
217
+ }
200
218
  // Linters, formatters and type checks read the project to judge it, not to
201
219
  // run it. The workspace holds rewritten copies, and ESLint failed semver,
202
220
  // js-yaml and picomatch on code nobody wrote ("Strings must use singlequote",
@@ -966,13 +984,41 @@ if (missingFile) {
966
984
  }
967
985
  };
968
986
  }
969
- register(new URL("./resolve-loader.mjs", import.meta.url), {
970
- data: {
971
- typescriptRequireHook: typeof typescriptHandler === "function" && typescriptHandler.name !== "loadTS",
972
- supportFile,
973
- missingFile,
974
- },
975
- });
987
+ const loaderData = {
988
+ typescriptRequireHook: typeof typescriptHandler === "function" && typescriptHandler.name !== "loadTS",
989
+ supportFile,
990
+ missingFile,
991
+ };
992
+ // Where Node can (22.15, 23.5), the hooks run in this thread. Off it, every
993
+ // Node process started a thread of its own and compiled a module loader
994
+ // there, and each import and require waited on a message to it: on a Next.js
995
+ // app's Vitest suite on one worker, a third of the time a measured run added.
996
+ if (typeof Module.registerHooks === "function") {
997
+ const hooks = await import("./resolve-loader.mjs");
998
+ hooks.initialize(loaderData);
999
+ Module.registerHooks({ resolve: hooks.resolveSync, load: hooks.loadSync });
1000
+ // Playwright compiles ES modules in the hooks thread its own loader
1001
+ // starts, which these hooks no longer reach: the cache patch goes to that
1002
+ // thread by itself, ahead of the first module anyone registers off this
1003
+ // one, which is what starts it. Supercov names the cache for every run,
1004
+ // and registering it unasked started the thread in every Node process --
1005
+ // a Vitest worker -- and sent each of its imports through it.
1006
+ if (process.env.PWTEST_CACHE_DIR) {
1007
+ const registerOffThread = Module.register;
1008
+ let cacheThreadRegistered = false;
1009
+ Module.register = function (...args) {
1010
+ if (!cacheThreadRegistered) {
1011
+ cacheThreadRegistered = true;
1012
+ registerOffThread.call(this, new URL("./playwrightCacheThread.mjs", import.meta.url));
1013
+ }
1014
+ return registerOffThread.apply(this, args);
1015
+ };
1016
+ syncBuiltinESMExports();
1017
+ }
1018
+ }
1019
+ else {
1020
+ register(new URL("./resolve-loader.mjs", import.meta.url), { data: loaderData });
1021
+ }
976
1022
  if (process.env.SUPERCOV_DEBUG === "1") {
977
1023
  console.error("[supercov] preload", { entrypoint });
978
1024
  }
@@ -1038,9 +1084,21 @@ if (generatedPlaywrightConfig &&
1038
1084
  }
1039
1085
  process.argv.push("--config", generatedPlaywrightConfig);
1040
1086
  }
1087
+ // A process the runner marked as not a test worker skips the wrapper, unless
1088
+ // it was started from Playwright's own files: a worker Playwright starts some
1089
+ // other way than its worker entry still has the wrapper, only later. The
1090
+ // runner itself has it too: it lists each test with the fixtures its spec's
1091
+ // `test` carries, and puts tests with the same worker fixtures in one worker.
1092
+ // A CommonJS spec it listed with Playwright's own `test` ran in the worker
1093
+ // with the wrapper's, beside a spec through a facade that has Playwright's
1094
+ // own in both, and the worker refused the pair: "Playwright detected
1095
+ // inconsistent test.use() options". An ES module spec already had the wrapper
1096
+ // in the runner, through the module hooks.
1097
+ const isPlaywrightProcess = /\/node_modules\/(?:@playwright\/test|playwright)\//.test(entrypoint);
1098
+ const mayBePlaywrightWorker = process.env.SUPERCOV_PLAYWRIGHT_WORKER !== "0" || isPlaywrightProcess;
1041
1099
  if (process.env.SUPERCOV_CJS_INTERCEPT === "1" &&
1042
1100
  process.env.SUPERCOV_INSIDE_PLAYWRIGHT === "1" &&
1043
- !isPlaywrightEntrypoint &&
1101
+ (isPlaywrightEntrypoint || mayBePlaywrightWorker) &&
1044
1102
  process.env.SUPERCOV_INSIDE_VITEST !== "1" &&
1045
1103
  process.env.VITEST !== "true") {
1046
1104
  const target = playwrightTarget ?? "@playwright/test";
@@ -1048,6 +1106,16 @@ if (process.env.SUPERCOV_CJS_INTERCEPT === "1" &&
1048
1106
  const originalPlaywrightConfig = process.env.SUPERCOV_ORIGINAL_PLAYWRIGHT_CONFIG
1049
1107
  ?.replaceAll("\\", "/");
1050
1108
  const wrapper = await import(__rewriteRelativeImportExtension(new URL("./playwright.mjs", import.meta.url)));
1109
+ // A spec transpiled from `import test from "@playwright/test"` reads the
1110
+ // module's `default` when it says it is an ES module, and the module
1111
+ // itself otherwise: the wrapper's namespace says nothing and is no
1112
+ // function, and a Next.js app's spec failed with `(0 , _test.default) is not
1113
+ // a function`. Its exports, marked as an ES module's, with `test` itself
1114
+ // as the default. Not a function that calls `test` (Playwright names a
1115
+ // test by where it was declared, read from the stack, and an extra frame
1116
+ // moved it), nor the exports assigned onto the wrapper's `test` (its
1117
+ // fixtures then differed between the runner and a worker).
1118
+ const commonjsWrapper = { ...wrapper, __esModule: true, default: wrapper.test };
1051
1119
  const originalLoad = Module._load;
1052
1120
  Module._load = function supercovLoad(request, parent, isMain) {
1053
1121
  const parentFile = parent?.filename?.replaceAll("\\", "/");
@@ -1065,7 +1133,7 @@ if (process.env.SUPERCOV_CJS_INTERCEPT === "1" &&
1065
1133
  ? parentFile.startsWith(`${normalizedRoot}/`)
1066
1134
  : parentFile.includes("/tests/"));
1067
1135
  if (request === target && belongsToProject)
1068
- return wrapper;
1136
+ return commonjsWrapper;
1069
1137
  return originalLoad.call(this, request, parent, isMain);
1070
1138
  };
1071
1139
  }
@@ -1118,7 +1186,13 @@ if (process.env.SUPERCOV_CJS_INTERCEPT === "1") {
1118
1186
  }
1119
1187
  }
1120
1188
  const originalLoad = Module._load;
1189
+ const redirected = new Set(["test", "node:test", "assert", "node:assert", "assert/strict", "node:assert/strict"]);
1121
1190
  Module._load = function supercovNodeTestLoad(request, parent, isMain) {
1191
+ // Every require comes here, cached or not, thousands in a test
1192
+ // worker: only these six names are redirected, and nothing else is
1193
+ // looked at for any other.
1194
+ if (!redirected.has(request))
1195
+ return originalLoad.call(this, request, parent, isMain);
1122
1196
  const parentFile = parent?.filename?.replaceAll("\\", "/");
1123
1197
  const belongsToProject = Boolean(projectRoot &&
1124
1198
  parentFile?.startsWith(`${projectRoot}/`) &&
@@ -115,7 +115,9 @@ function throughRequireHook(resolved) {
115
115
  return resolved;
116
116
  return { ...resolved, format: "commonjs" };
117
117
  }
118
- export async function resolve(specifier, context, nextResolve) {
118
+ // A specifier Supercov answers itself (`url`), or sends on as another
119
+ // (`next`, answered as it is); undefined for every other.
120
+ function redirected(specifier, context) {
119
121
  // Some transpilers preserve the source-relative runtime import while moving
120
122
  // only the transformed application file to an output directory. The
121
123
  // source-local copy keeps strict rootDir compilers happy; this fallback
@@ -161,8 +163,16 @@ export async function resolve(specifier, context, nextResolve) {
161
163
  shortCircuit: true,
162
164
  };
163
165
  }
164
- return nextResolve(REPLACEMENT, context);
166
+ return { next: REPLACEMENT };
165
167
  }
168
+ return undefined;
169
+ }
170
+ export async function resolve(specifier, context, nextResolve) {
171
+ const redirect = redirected(specifier, context);
172
+ if (redirect?.url)
173
+ return redirect;
174
+ if (redirect?.next)
175
+ return nextResolve(redirect.next, context);
166
176
  try {
167
177
  return throughRequireHook(await nextResolve(specifier, context));
168
178
  }
@@ -172,6 +182,28 @@ export async function resolve(specifier, context, nextResolve) {
172
182
  throw error;
173
183
  }
174
184
  }
185
+ // The same hooks run in the loading thread (`module.registerHooks`), where
186
+ // the next hook answers at once. There they see `require` calls too, which
187
+ // the hooks off the thread never did, and the CommonJS loader routes itself:
188
+ // a TypeScript file it was given back as CommonJS was compiled as JavaScript,
189
+ // past the handler ts-node/register installed for it.
190
+ export function resolveSync(specifier, context, nextResolve) {
191
+ if (context.conditions?.includes("require"))
192
+ return nextResolve(specifier, context);
193
+ const redirect = redirected(specifier, context);
194
+ if (redirect?.url)
195
+ return redirect;
196
+ if (redirect?.next)
197
+ return nextResolve(redirect.next, context);
198
+ try {
199
+ return throughRequireHook(nextResolve(specifier, context));
200
+ }
201
+ catch (error) {
202
+ if (error?.code === "ERR_MODULE_NOT_FOUND")
203
+ noteMissing(specifier, context.parentURL);
204
+ throw error;
205
+ }
206
+ }
175
207
 
176
208
  // Node's own test coverage filters files by `--test-coverage-include` and
177
209
  // `--test-coverage-exclude` globs relative to the working directory, and the
@@ -204,7 +236,17 @@ function mapToCopy(source, filename) {
204
236
  }
205
237
  }
206
238
  export async function load(url, context, nextLoad) {
207
- const result = await nextLoad(url, context);
239
+ return loaded(url, await nextLoad(url, context));
240
+ }
241
+ export function loadSync(url, context, nextLoad) {
242
+ // Every module this process loads, required or imported: a server's
243
+ // runtime rescans the module cache for new ones only when this moved.
244
+ globalThis.__SUPERCOV_MODULE_LOADS__ = (globalThis.__SUPERCOV_MODULE_LOADS__ ?? 0) + 1;
245
+ if (context.conditions?.includes("require"))
246
+ return nextLoad(url, context);
247
+ return loaded(url, nextLoad(url, context));
248
+ }
249
+ function loaded(url, result) {
208
250
  noteLoaded(url);
209
251
  if (!NODE_COVERAGE_FILTERS || result.source == null || !url.startsWith("file:"))
210
252
  return result;
@@ -98,12 +98,27 @@ export function callerLocation(boundary = callerLocation) {
98
98
  return {};
99
99
  }
100
100
  }
101
+ /**
102
+ * A compact attempt file names its scope on a writer's first line only; each
103
+ * line without one is the attempt of the line before it.
104
+ */
105
+ function inheritingScope() {
106
+ let current;
107
+ return (line) => {
108
+ const record = JSON.parse(line);
109
+ if (record.scope)
110
+ current = record.scope;
111
+ else if (current)
112
+ record.scope = current;
113
+ return record;
114
+ };
115
+ }
101
116
  export function readScopedServerEvidence(scope, evidencePath = serverEvidencePath(scope)) {
102
117
  try {
103
118
  return readFileSync(evidencePath, "utf8")
104
119
  .split("\n")
105
120
  .filter(Boolean)
106
- .map((line) => JSON.parse(line))
121
+ .map(inheritingScope())
107
122
  .filter((record) => record.scope?.attemptId === scope.attemptId);
108
123
  }
109
124
  catch {
@@ -37,7 +37,8 @@ var COVERAGE_CARRIER_ENV = "SUPERCOV_CONTEXT";
37
37
  var DEFAULT_SERVER_EVIDENCE_ROOT = "/tmp/supercov-server-evidence";
38
38
  function configuredServerEvidenceRoot() {
39
39
  var _a8;
40
- return typeof process !== "undefined" && ((_a8 = process.env) == null ? void 0 : _a8["SUPERCOV_SERVER_EVIDENCE_ROOT"]) ? process.env["SUPERCOV_SERVER_EVIDENCE_ROOT"] : DEFAULT_SERVER_EVIDENCE_ROOT;
40
+ const configured = typeof process !== "undefined" ? (_a8 = process.env) == null ? void 0 : _a8["SUPERCOV_SERVER_EVIDENCE_ROOT"] : void 0;
41
+ return configured ? configured : DEFAULT_SERVER_EVIDENCE_ROOT;
41
42
  }
42
43
  function nonEmpty(value) {
43
44
  return typeof value === "string" && value.length > 0;
@@ -117,6 +118,18 @@ function serverEvidenceDirectory(scope, root = configuredServerEvidenceRoot()) {
117
118
  function serverEvidencePath(scope, root = configuredServerEvidenceRoot()) {
118
119
  return `${serverEvidenceDirectory(scope, root)}/${pathComponent(scope.attemptId)}.jsonl`;
119
120
  }
121
+ // A scope's evidence directory and file, made once per scope and root: a
122
+ // server works them out for every record it writes, thousands per test.
123
+ var scopedEvidenceLocations = /* @__PURE__ */ new WeakMap();
124
+ function scopedEvidenceLocation(scope) {
125
+ const root = configuredServerEvidenceRoot();
126
+ const known = scopedEvidenceLocations.get(scope);
127
+ if (known !== void 0 && known.root === root)
128
+ return known;
129
+ const location = { root, directory: serverEvidenceDirectory(scope, root), path: serverEvidencePath(scope, root) };
130
+ scopedEvidenceLocations.set(scope, location);
131
+ return location;
132
+ }
120
133
  function backgroundEvidenceDirectory(runId, root = configuredServerEvidenceRoot()) {
121
134
  return `${serverRunEvidenceDirectory(runId, root)}/background`;
122
135
  }
@@ -241,6 +254,7 @@ function activateProbeV2Key(key, force = false) {
241
254
  if (state.probeV2NextEpoch >= 4294967295) {
242
255
  state.probeV2NextEpoch = 1;
243
256
  state.probeV2ContextEpochs.clear();
257
+ state.probeV2EpochGeneration = (state.probeV2EpochGeneration || 0) + 1;
244
258
  for (const file of state.probeV2Files) {
245
259
  file.hitEpochs.fill(0);
246
260
  file.decisionObservationEpochs.fill(0);
@@ -260,8 +274,38 @@ function activateProbeV2Key(key, force = false) {
260
274
  state.probeV2Clock.epoch = epoch;
261
275
  return epoch;
262
276
  }
277
+ // The async hook activates the current request's context before every
278
+ // callback the server runs, and building its key and looking it up was most of
279
+ // a Next server's time in the hook. A request's context object is never
280
+ // changed once made, and its key is made of it alone, so each keeps the epoch
281
+ // it was given until the epochs are cleared.
282
+ // Every copy of the runtime in a process shares one state, and the hook is the
283
+ // first copy's, so the cache and its generation live on that state too: a
284
+ // reset from another copy has to reach them.
285
+ var emptyProbeV2Context = Object.freeze({});
263
286
  function activateProbeV2Context(context) {
264
- return activateProbeV2Key(probeV2ContextKey(context));
287
+ // Work no request owns is keyed by the test running when it runs, which
288
+ // changes under the same context: only a request's context keeps its epoch.
289
+ if (!context.scope)
290
+ return activateProbeV2Key(probeV2ContextKey(context));
291
+ if (state.probeV2ContextEpochCache === void 0) {
292
+ state.probeV2ContextEpochCache = /* @__PURE__ */ new WeakMap();
293
+ state.probeV2EpochGeneration = 0;
294
+ }
295
+ const cached = state.probeV2ContextEpochCache.get(context);
296
+ if (cached !== void 0 && cached.generation === state.probeV2EpochGeneration) {
297
+ state.probeV2Clock.epoch = cached.epoch;
298
+ return cached.epoch;
299
+ }
300
+ const epoch = activateProbeV2Key(probeV2ContextKey(context));
301
+ state.probeV2ContextEpochCache.set(context, { generation: state.probeV2EpochGeneration, epoch });
302
+ return epoch;
303
+ }
304
+ // The context the async hook leaves to a callback's first probe: the
305
+ // request's store, or else the one the environment carries.
306
+ function resolveProbeV2Epoch() {
307
+ var _a8, _b;
308
+ return activateProbeV2Context((_b = (_a8 = serverPhaseStorage == null ? void 0 : serverPhaseStorage.getStore()) != null ? _a8 : environmentRequestContext()) != null ? _b : emptyProbeV2Context);
265
309
  }
266
310
  function withProbeV2Context(context, callback) {
267
311
  const previous = state.probeV2Clock.epoch;
@@ -283,10 +327,14 @@ function installProbeV2AsyncHook() {
283
327
  return;
284
328
  const epochs = [];
285
329
  createHook({
330
+ // NaN, which no epoch equals: the callback's first probe works out
331
+ // its context. Most callbacks a server or a test runner runs reach no
332
+ // instrumented code, and working it out here, for every one, read the
333
+ // environment twice and built a key each time: on a Next app's suite,
334
+ // more of the server's time than any of Supercov's other work.
286
335
  before() {
287
- var _a9, _b;
288
336
  epochs.push(state.probeV2Clock.epoch);
289
- activateProbeV2Context((_b = (_a9 = serverPhaseStorage == null ? void 0 : serverPhaseStorage.getStore()) != null ? _a9 : environmentRequestContext()) != null ? _b : {});
337
+ state.probeV2Clock.epoch = NaN;
290
338
  },
291
339
  after() {
292
340
  var _a9;
@@ -329,6 +377,7 @@ function resetCoverage(testId2) {
329
377
  state.logicals.clear();
330
378
  state.eventKeys.clear();
331
379
  state.probeV2ContextEpochs.clear();
380
+ state.probeV2EpochGeneration = (state.probeV2EpochGeneration || 0) + 1;
332
381
  activateProbeV2Key(`reset\0${state.probeV2NextEpoch}`, true);
333
382
  if (testId2)
334
383
  runtimeGlobal.__SUPERCOV_MCDC_TEST_ID__ = testId2;
@@ -341,6 +390,78 @@ function resetCoverage(testId2) {
341
390
  }
342
391
  runtimeGlobal.__SUPERCOV_MCDC_SNAPSHOT__ = decisionSnapshot;
343
392
  runtimeGlobal.__SUPERCOV_COVERAGE_SNAPSHOT__ = coverageSnapshot;
393
+ // A snapshot as the text the Playwright collector reads back. Its events
394
+ // become rows over one table of the strings they repeat -- point ids, phase
395
+ // ids, kinds -- each row in the key order of its event (`shapes`), and its
396
+ // hits indexes into the same table; snapshots.mjs's expandSnapshot gives
397
+ // back what JSON.parse of the whole would have. A large frontend's page
398
+ // holds tens of thousands of events, each naming its point, phase and
399
+ // environment in full, read back at the end of every test and from every
400
+ // page that leaves: relaying them cost the browser's own process more than
401
+ // its renderers spent running the app.
402
+ function compactSnapshotText(snapshot) {
403
+ if (!snapshot || typeof snapshot !== "object" || !Array.isArray(snapshot.events) || !Array.isArray(snapshot.hits))
404
+ return JSON.stringify(snapshot);
405
+ const strings = [];
406
+ const stringIndexes = /* @__PURE__ */ new Map();
407
+ const intern = (value) => {
408
+ let index = stringIndexes.get(value);
409
+ if (index === void 0) {
410
+ index = strings.length;
411
+ strings.push(value);
412
+ stringIndexes.set(value, index);
413
+ }
414
+ return index;
415
+ };
416
+ const shapes = [];
417
+ const shapeIndexes = /* @__PURE__ */ new Map();
418
+ const rows = [];
419
+ for (let position = 0; position < snapshot.events.length; position += 1) {
420
+ const event = snapshot.events[position];
421
+ if (!event || typeof event !== "object" || Array.isArray(event))
422
+ return JSON.stringify(snapshot);
423
+ const fields = [];
424
+ const values = [];
425
+ const keys = Object.keys(event);
426
+ for (let index = 0; index < keys.length; index += 1) {
427
+ const value = event[keys[index]];
428
+ // What JSON.stringify leaves out of an object.
429
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol")
430
+ continue;
431
+ if (typeof value === "string") {
432
+ fields.push(`s${keys[index]}`);
433
+ values.push(intern(value));
434
+ } else {
435
+ fields.push(`v${keys[index]}`);
436
+ values.push(value);
437
+ }
438
+ }
439
+ const name = fields.join("\0");
440
+ let shape = shapeIndexes.get(name);
441
+ if (shape === void 0) {
442
+ shape = shapes.length;
443
+ shapes.push(fields);
444
+ shapeIndexes.set(name, shape);
445
+ }
446
+ rows.push(shape);
447
+ for (let index = 0; index < values.length; index += 1)
448
+ rows.push(values[index]);
449
+ }
450
+ const hits = [];
451
+ for (let index = 0; index < snapshot.hits.length; index += 1) {
452
+ if (typeof snapshot.hits[index] !== "string")
453
+ return JSON.stringify(snapshot);
454
+ hits.push(intern(snapshot.hits[index]));
455
+ }
456
+ const rest = {};
457
+ const keys = Object.keys(snapshot);
458
+ for (let index = 0; index < keys.length; index += 1) {
459
+ if (keys[index] !== "events" && keys[index] !== "hits")
460
+ rest[keys[index]] = snapshot[keys[index]];
461
+ }
462
+ return JSON.stringify({ supercovCompactSnapshot: 1, order: keys, rest, strings, shapes, rows, hits });
463
+ }
464
+ runtimeGlobal.__SUPERCOV_COMPACT_SNAPSHOT__ = compactSnapshotText;
344
465
  runtimeGlobal.__SUPERCOV_RESET__ = resetCoverage;
345
466
  // A dedicated worker blocked in `Atomics.wait` runs no evaluation, so the
346
467
  // test runner could not read its evidence and reported the test as a lower
@@ -417,11 +538,32 @@ function pushWorkerEvidence() {
417
538
  runtimeGlobal.__SUPERCOV_WORKER_EVIDENCE__ = () => Array.from(received, ([token, pushed]) => ({ token, url: pushed.url, current: pushed.current, snapshot: pushed.snapshot }));
418
539
  })();
419
540
  var persistBrowserListenersInstalled = false;
541
+ // How much evidence the document holds. Every part of it only grows until a
542
+ // reset, so the same size twice is the same evidence.
543
+ function evidenceSize() {
544
+ let size = state.hits.size + state.events.length;
545
+ for (const decision of state.decisions.values())
546
+ size += decision.vectors.size;
547
+ for (const vectors of state.logicals.values())
548
+ size += vectors.size;
549
+ return size;
550
+ }
551
+ // The evidence's size when this document gave it to the Playwright collector
552
+ // on the way out. Its next document need not inherit it then: carried through
553
+ // localStorage as well, every page of a test parsed all the evidence of the
554
+ // pages before it on load and wrote it all again on leaving, a test of thirty
555
+ // pages paying for the first one thirty times, and the collector merged the
556
+ // same evidence once per page. What runs after the hand-over -- the rest of a
557
+ // handler that set `location.href`, a render it started -- is still carried.
558
+ var handedOverSize = -1;
420
559
  function persistBrowserNow() {
421
560
  if (!isBrowser)
422
561
  return;
423
562
  try {
424
- localStorage.setItem(storageKey, JSON.stringify(coverageSnapshot()));
563
+ if (handedOverSize === evidenceSize())
564
+ localStorage.removeItem(storageKey);
565
+ else
566
+ localStorage.setItem(storageKey, JSON.stringify(coverageSnapshot()));
425
567
  } catch (e) {
426
568
  }
427
569
  }
@@ -433,8 +575,10 @@ function persistBrowserNow() {
433
575
  // it does.
434
576
  function handOverBrowser() {
435
577
  try {
436
- if (typeof runtimeGlobal.__supercovLeaving === "function")
437
- runtimeGlobal.__supercovLeaving(JSON.stringify(coverageSnapshot()));
578
+ if (typeof runtimeGlobal.__supercovLeaving === "function") {
579
+ runtimeGlobal.__supercovLeaving(compactSnapshotText(coverageSnapshot()));
580
+ handedOverSize = evidenceSize();
581
+ }
438
582
  } catch (e) {
439
583
  }
440
584
  }
@@ -468,6 +612,19 @@ function persistBrowser() {
468
612
  function attemptKey(scope) {
469
613
  return `${scope.runId}\0${scope.workerId}\0${scope.attemptId}`;
470
614
  }
615
+ // An attempt's file says its scope once per writing process: each line
616
+ // without one is of the scope of the line before it in the same file.
617
+ var scopedPathsWritten = /* @__PURE__ */ new Set();
618
+ function serverLine(record, path) {
619
+ if (!path)
620
+ return JSON.stringify(record);
621
+ const { scope, ...line } = record;
622
+ if (scope && !scopedPathsWritten.has(path)) {
623
+ scopedPathsWritten.add(path);
624
+ line.scope = scope;
625
+ }
626
+ return JSON.stringify(line);
627
+ }
471
628
  function serverRecordKey(record) {
472
629
  var _a8;
473
630
  const suffix = record.type === "decision" ? `${record.id}:${vectorKey(record.vector)}` : record.id;
@@ -530,7 +687,7 @@ function flushBufferedServerEvidence(scope) {
530
687
  return void 0;
531
688
  try {
532
689
  makeEvidenceDirectory(fs, buffered.directory);
533
- fs.appendFileSync(buffered.path, [...buffered.records.values()].map((record) => JSON.stringify(record)).join("\n") + "\n");
690
+ fs.appendFileSync(buffered.path, [...buffered.records.values()].map((record) => serverLine(record, buffered.path)).join("\n") + "\n");
534
691
  return buffered.path;
535
692
  } catch (e) {
536
693
  return void 0;
@@ -758,7 +915,8 @@ function flushOnTermination(flush) {
758
915
  }
759
916
  } catch (e) {
760
917
  }
761
- for (const signal of ["SIGTERM", "SIGINT", "SIGHUP", "SIGBREAK"]) {
918
+ const signals = ["SIGTERM", "SIGINT", "SIGHUP", "SIGBREAK"];
919
+ for (const signal of signals) {
762
920
  try {
763
921
  process.on(signal, () => {
764
922
  drain();
@@ -770,6 +928,24 @@ function flushOnTermination(flush) {
770
928
  } catch (e) {
771
929
  }
772
930
  }
931
+ // A listener keeps nothing alive, and a signal a process sends itself is
932
+ // handled on a later turn of its event loop: `process.kill(process.pid,
933
+ // "SIGTERM")` as a script's last line exited 0, once nothing else held the
934
+ // loop open, where alone it ended by the signal. A signal to itself that
935
+ // only this listener would take is drained and left to act at once.
936
+ try {
937
+ const kill = process.kill;
938
+ const numbered = { 1: "SIGHUP", 2: "SIGINT", 15: "SIGTERM", 21: "SIGBREAK" };
939
+ process.kill = function supercovKill(pid, signal) {
940
+ const name = signal === void 0 ? "SIGTERM" : typeof signal === "number" ? numbered[signal] : signal;
941
+ if (Number(pid) === process.pid && signals.indexOf(name) >= 0 && process.listenerCount(name) <= 1) {
942
+ drain();
943
+ process.removeAllListeners(name);
944
+ }
945
+ return kill.apply(this, arguments);
946
+ };
947
+ } catch (e) {
948
+ }
773
949
  }
774
950
  // Rust compiled to wasm and measured by the Rust frontend keeps what it ran
775
951
  // in the module's memory, behind `__supercov_region_<crate>`. When the run
@@ -1024,8 +1200,9 @@ function appendServer(record) {
1024
1200
  if (!runId)
1025
1201
  return;
1026
1202
  try {
1027
- const directory = scope ? serverEvidenceDirectory(scope) : void 0;
1028
- const path = scope ? serverEvidencePath(scope) : void 0;
1203
+ const location = scope ? scopedEvidenceLocation(scope) : void 0;
1204
+ const directory = location == null ? void 0 : location.directory;
1205
+ const path = location == null ? void 0 : location.path;
1029
1206
  // Work done while handling a request that named no test -- a webhook a
1030
1207
  // mock server sends -- says so, with the request's number.
1031
1208
  const stored = serverPhaseStorage == null ? void 0 : serverPhaseStorage.getStore();
@@ -1050,7 +1227,7 @@ function appendServer(record) {
1050
1227
  appendDurableBackgroundRecord(fs, runId, serialized);
1051
1228
  return;
1052
1229
  }
1053
- enqueueServerAppend(directory, path, JSON.stringify(serialized) + "\n", runId);
1230
+ enqueueServerAppend(directory, path, serverLine(serialized, path) + "\n", runId);
1054
1231
  if (deduplicationKey)
1055
1232
  state.persistedServerRecords.add(deduplicationKey);
1056
1233
  } catch (cause) {
@@ -1639,13 +1816,24 @@ function noteSupport(scope) {
1639
1816
  const moduleBuiltin = typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("node:module") : void 0;
1640
1817
  const cache = moduleBuiltin && moduleBuiltin._cache;
1641
1818
  const root = process.env.SUPERCOV_PROJECT_ROOT;
1642
- if (cache && root) {
1819
+ // The in-thread module hooks count every load (resolve-loader.mjs): a
1820
+ // cache nothing was loaded into since the last request holds nothing
1821
+ // new. Listing a Next server's thousands of keys on every request was
1822
+ // the most of its time any of Supercov's code took. Without the count
1823
+ // (off-thread hooks), every request looks.
1824
+ const loads = runtimeGlobal.__SUPERCOV_MODULE_LOADS__;
1825
+ const unchanged = typeof loads === "number" && loads === supportNoted.loads;
1826
+ supportNoted.loads = loads;
1827
+ if (cache && root && !unchanged) {
1643
1828
  for (const name of Object.keys(cache)) {
1829
+ // Each is looked at once, whether it is noted or not.
1830
+ if (supportNoted.modules.has(name))
1831
+ continue;
1832
+ supportNoted.modules.add(name);
1644
1833
  // The copy lives under `.supercov/`: the project root decides.
1645
1834
  const inside = name.indexOf(root) === 0 ? name.slice(root.length).replace(/^[\\/]/, "") : "";
1646
- if (supportNoted.modules.has(name) || inside === "" || /^\.supercov[\\/]/.test(inside) || /(?:^|[\\/])node_modules[\\/]/.test(inside))
1835
+ if (inside === "" || /^\.supercov[\\/]/.test(inside) || /(?:^|[\\/])node_modules[\\/]/.test(inside))
1647
1836
  continue;
1648
- supportNoted.modules.add(name);
1649
1837
  lines.push({ loaded: name });
1650
1838
  }
1651
1839
  }
@@ -1789,6 +1977,8 @@ function recordCoverageHit(id) {
1789
1977
  activateProbeV2Context(currentRequestContext());
1790
1978
  epoch = clock.epoch;
1791
1979
  clock.epoch = previousEpoch;
1980
+ } else if (epoch !== epoch) {
1981
+ epoch = resolveProbeV2Epoch();
1792
1982
  }
1793
1983
  if (!state.hitEpochs)
1794
1984
  state.hitEpochs = /* @__PURE__ */ new Map();
@@ -1814,24 +2004,31 @@ function recordCoverageHit(id) {
1814
2004
  }, phaseId ? { phaseId } : {}));
1815
2005
  }
1816
2006
  }
2007
+ // A file's ids, as the instrumenter writes them: one string of ids separated
2008
+ // by spaces, or a list where an id holds one.
2009
+ function listedIds(ids) {
2010
+ return typeof ids === "string" ? ids === "" ? [] : ids.split(" ") : ids;
2011
+ }
1817
2012
  function registerProbeV2(definition) {
1818
2013
  var _a8, _b;
2014
+ const decisions = listedIds(definition.decisions);
2015
+ const pointIds = listedIds(definition.pointIds);
1819
2016
  const file = {
1820
2017
  // Each decision's id and its number of conditions: the module carries
1821
2018
  // no source text, which the manifest holds.
1822
- decisions: definition.decisions,
2019
+ decisions,
1823
2020
  decisionWidths: definition.decisionWidths,
1824
- pointIds: definition.pointIds,
2021
+ pointIds,
1825
2022
  clock: state.probeV2Clock,
1826
- hitEpochs: new Uint32Array(definition.pointIds.length),
2023
+ hitEpochs: new Uint32Array(pointIds.length),
1827
2024
  decisionEpochs: definition.decisionWidths.map((width) => width <= 6 ? new Uint32Array(2 * 3 ** width) : /* @__PURE__ */ new Map()),
1828
- decisionVectorCounts: definition.decisions.map((_, index) => {
2025
+ decisionVectorCounts: decisions.map((_, index) => {
1829
2026
  var _a9, _b2;
1830
2027
  return (_b2 = (_a9 = definition.decisionVectorCounts) == null ? void 0 : _a9[index]) != null ? _b2 : 0;
1831
2028
  }),
1832
- decisionObservationEpochs: new Uint32Array(definition.decisions.length),
1833
- decisionObservationCounts: new Uint16Array(definition.decisions.length),
1834
- decisionCompleteEpochs: new Uint32Array(definition.decisions.length),
2029
+ decisionObservationEpochs: new Uint32Array(decisions.length),
2030
+ decisionObservationCounts: new Uint16Array(decisions.length),
2031
+ decisionCompleteEpochs: new Uint32Array(decisions.length),
1835
2032
  pendingDefaults: new Uint32Array(definition.defaultCount || 0),
1836
2033
  // A selection's outcomes -- short falsy, short truthy, right falsy, right
1837
2034
  // truthy -- are four consecutive points per site from the first index.
@@ -1876,6 +2073,8 @@ function recordCoverageHitV2(file, index) {
1876
2073
  const previousEpoch = file.clock.epoch;
1877
2074
  if (fallbackEpoch)
1878
2075
  activateProbeV2Context(currentRequestContext());
2076
+ else if (previousEpoch !== previousEpoch)
2077
+ resolveProbeV2Epoch();
1879
2078
  try {
1880
2079
  const epoch = file.clock.epoch;
1881
2080
  if (file.hitEpochs[index] === epoch)
@@ -1928,6 +2127,8 @@ function recordMcdcEndV2(file, decisionIndex, encoded, value) {
1928
2127
  const previousEpoch = file.clock.epoch;
1929
2128
  if (fallbackEpoch)
1930
2129
  activateProbeV2Context(currentRequestContext());
2130
+ else if (previousEpoch !== previousEpoch)
2131
+ resolveProbeV2Epoch();
1931
2132
  try {
1932
2133
  const epoch = file.clock.epoch;
1933
2134
  const seen = file.decisionEpochs[decisionIndex];