supercov 4.1.0 → 4.2.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.
@@ -49,6 +49,11 @@ and file paths in place of the examples. For example,
49
49
  Rust projects can use `cargo test`, Python projects `pytest`, and Ruby projects
50
50
  `bundle exec rspec`. Keep the baseline and verification commands identical.
51
51
 
52
+ A loss in the comparison is not always a regression. An end-to-end suite
53
+ reaches slightly different code from one run to the next, and adding a test
54
+ changes no source: when `diff` notes that both runs measured the same source,
55
+ rerun before treating a loss as real.
56
+
52
57
  The `line` query is useful before writing a test because it shows which tests
53
58
  already reach that line. Extending a nearby test is often better than adding a
54
59
  duplicate.
package/docs/cli.md CHANGED
@@ -356,6 +356,13 @@ npx supercov diff <older-run> <newer-run>
356
356
  expected behavior became covered without an unexplained regression elsewhere.
357
357
  Neither input run is changed.
358
358
 
359
+ Two runs of the same code can differ a little: what an end-to-end suite
360
+ reaches depends on timing, on the order of tests, and on state outside the
361
+ code. When both runs measured the same source, `diff` says so under the
362
+ totals, and a loss it lists is not a change in the code. Run again before
363
+ acting on one. In `--json`, `inputs.sourceChanged` and `inputs.testsChanged`
364
+ say what differed between the two runs.
365
+
359
366
  The same filters can focus a comparison:
360
367
 
361
368
  ```sh supercov
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Coverage, security and code quality for coding agents",
5
5
  "keywords": [
6
6
  "coverage",
@@ -119,14 +119,14 @@
119
119
  "test:windows-tls": "node scripts/windows-tls-test.mjs"
120
120
  },
121
121
  "optionalDependencies": {
122
- "@supercov/cli-darwin-arm64": "4.1.0",
123
- "@supercov/cli-darwin-x64": "4.1.0",
124
- "@supercov/cli-linux-arm64-gnu": "4.1.0",
125
- "@supercov/cli-linux-arm64-musl": "4.1.0",
126
- "@supercov/cli-linux-x64-gnu": "4.1.0",
127
- "@supercov/cli-linux-x64-musl": "4.1.0",
128
- "@supercov/cli-win32-arm64": "4.1.0",
129
- "@supercov/cli-win32-x64": "4.1.0"
122
+ "@supercov/cli-darwin-arm64": "4.2.0",
123
+ "@supercov/cli-darwin-x64": "4.2.0",
124
+ "@supercov/cli-linux-arm64-gnu": "4.2.0",
125
+ "@supercov/cli-linux-arm64-musl": "4.2.0",
126
+ "@supercov/cli-linux-x64-gnu": "4.2.0",
127
+ "@supercov/cli-linux-x64-musl": "4.2.0",
128
+ "@supercov/cli-win32-arm64": "4.2.0",
129
+ "@supercov/cli-win32-x64": "4.2.0"
130
130
  },
131
131
  "peerDependencies": {
132
132
  "@playwright/test": ">=1.55.0",
@@ -9,4 +9,9 @@ const runtime = globalThis.__SUPERCOV_DIRECT_RUNTIME__ ?? process.__SUPERCOV_DIR
9
9
  require("node:vm").runInThisContext("globalThis.__SUPERCOV_DIRECT_RUNTIME__");
10
10
  if (!runtime) throw new Error("[supercov] Jest runtime was not initialized by the preload");
11
11
  globalThis.__SUPERCOV_DIRECT_RUNTIME__ = runtime;
12
+ // A process that turns out to run Jest (a wrapper that calls it in place)
13
+ // prints a failure's paths as the project's too. See register.mjs.
14
+ const failureOutput = process.__SUPERCOV_FAILURE_OUTPUT__ ??
15
+ require("node:vm").runInThisContext("process.__SUPERCOV_FAILURE_OUTPUT__");
16
+ if (typeof failureOutput === "function") failureOutput("jest");
12
17
  module.exports = runtime;
@@ -8,7 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import Module, { register, syncBuiltinESMExports } from "node:module";
10
10
  import fs, { closeSync, openSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
11
- import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
11
+ import path, { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { installLaunchSupervisor, wrapImportedCapability } from "./launchSupervisor.mjs";
14
14
  import { __supercovBindCapabilityWrapper } from "./capability.mjs";
@@ -224,28 +224,324 @@ else if (/\/node_modules\/next\/dist\/compiled\/jest-worker\/processChild\.js$/.
224
224
  return Reflect.apply(compile, this, [content, filename, ...rest]);
225
225
  };
226
226
  }
227
+ // A runner prints a failure with the files it names read from disk, and with
228
+ // their paths relative to its root. Both are the copy's: the code under a
229
+ // frame was Supercov's, and a file whose map names the project's own (so that
230
+ // a Node stack trace reads as the project's) printed as
231
+ // `../../../../src/crypto.js:10:11`. A runner's own printing reads a
232
+ // rewritten file as the text it was rewritten from, and names it as it lies
233
+ // in the copy. What runs is read as it is.
234
+ //
235
+ // Jest keeps the function it reads with from before any test loads, so its
236
+ // processes are set up here, when they start. Its setup file and Vitest's
237
+ // reporter ask again, which is what a process that only turns out to run a
238
+ // suite gets.
239
+ const failureOutput = { readers: [], installed: false };
240
+ process.__SUPERCOV_FAILURE_OUTPUT__ ??= function supercovFailureOutput(runner) {
241
+ const reader = {
242
+ // Its message formatter reads the file under a frame and makes each
243
+ // frame's path relative to the root directory. So does the reporter
244
+ // that writes the tracefile coverage services read, where a file was
245
+ // `SF:../../../../src/crypto.js`.
246
+ jest: { prints: /[\\/](?:jest-message-util|istanbul-reports)[\\/]/, names: false, composes: true },
247
+ // Its own code reads the file under a frame, and Vitest 4 also looks in
248
+ // it for a source map, mapping positions a second time. Vite, which
249
+ // loads what runs, gets a map that names the file itself: frames then
250
+ // name the copy's file, and coverage reports it by its path in the
251
+ // project's layout.
252
+ vitest: { prints: /[\\/]vitest[\\/]dist[\\/]/, names: true },
253
+ }[runner];
254
+ // Asked with no runner, the reads are in place and change nothing until
255
+ // one is named: a process that may yet run a suite.
256
+ if (reader && !failureOutput.readers.includes(reader.prints.source)) {
257
+ failureOutput.readers.push(reader.prints.source);
258
+ failureOutput.prints = new RegExp(failureOutput.readers.join("|"));
259
+ failureOutput.names || (failureOutput.names = reader.names);
260
+ failureOutput.composes || (failureOutput.composes = reader.composes === true);
261
+ }
262
+ if (failureOutput.installed)
263
+ return;
264
+ failureOutput.installed = true;
265
+ let rewritten;
266
+ const copies = (() => {
267
+ const copy = fileURLToPath(new URL("../../", import.meta.url));
268
+ try {
269
+ return [...new Set([copy, `${realpathSync(copy)}${sep}`])];
270
+ }
271
+ catch {
272
+ return [copy];
273
+ }
274
+ })();
275
+ // The copy lies in the project at .supercov/workspaces/workspace/<name>.
276
+ const projects = copies
277
+ .filter((copy) => /[\\/]\.supercov[\\/]workspaces[\\/]workspace[\\/][^\\/]+[\\/]$/.test(copy))
278
+ .map((copy) => resolve(copy, "../../../..") + sep);
279
+ // The path inside the copy of a rewritten file that lies under one of `roots`.
280
+ const rewrittenUnder = (roots, file) => {
281
+ if (typeof file !== "string")
282
+ return undefined;
283
+ if (rewritten === undefined) {
284
+ try {
285
+ rewritten = new Set(JSON.parse(readFileSync(new URL("./authored-sources.json", import.meta.url), "utf8")));
286
+ }
287
+ catch {
288
+ rewritten = new Set();
289
+ }
290
+ }
291
+ for (const root of roots) {
292
+ if (file.startsWith(root)) {
293
+ const local = file.slice(root.length).split(sep).join("/");
294
+ return rewritten.has(local) ? local : undefined;
295
+ }
296
+ }
297
+ return undefined;
298
+ };
299
+ const kept = (name, file) => fileURLToPath(new URL(`./${name}/${file}`, import.meta.url));
300
+ const calledByPrinter = (above) => {
301
+ const holder = {};
302
+ const limit = Error.stackTraceLimit;
303
+ try {
304
+ Error.stackTraceLimit = 1;
305
+ Error.captureStackTrace(holder, above);
306
+ }
307
+ finally {
308
+ Error.stackTraceLimit = limit;
309
+ }
310
+ return failureOutput.prints.test(String(holder.stack).split("\n")[1] ?? "");
311
+ };
312
+ const mapComment = "\n//# sourceMappingURL=data:application/json;base64,";
313
+ const namedForItself = (file, contents) => {
314
+ const text = typeof contents === "string" ? contents : contents.toString("utf8");
315
+ const found = text.lastIndexOf(mapComment);
316
+ if (found < 0)
317
+ return contents;
318
+ const encoded = text.slice(found + mapComment.length).trimEnd();
319
+ if (/\s/.test(encoded))
320
+ return contents;
321
+ try {
322
+ const map = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
323
+ map.sources = [basename(file)];
324
+ const renamed = `${text.slice(0, found + mapComment.length)}${Buffer.from(JSON.stringify(map)).toString("base64")}\n`;
325
+ return typeof contents === "string" ? renamed : Buffer.from(renamed, "utf8");
326
+ }
327
+ catch {
328
+ return contents;
329
+ }
330
+ };
331
+ // A transformer that compiles what it is given and reads no map in it
332
+ // (ts-jest) leaves Jest a map from its output to the rewritten file, so a
333
+ // frame read `src/crypto.ts:51:10` for line 10 of what the author wrote.
334
+ // Such a map is taken through the rewritten file's own before Jest reads
335
+ // it. One that already carries the author's text went through it in the
336
+ // transformer, as Babel's and SWC's do.
337
+ const digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
338
+ const decoded = (mappings) => {
339
+ const lines = [];
340
+ const state = [0, 0, 0, 0, 0];
341
+ for (const line of mappings.split(";")) {
342
+ const segments = [];
343
+ state[0] = 0;
344
+ for (const text of line.split(",")) {
345
+ if (text === "")
346
+ continue;
347
+ const fields = [];
348
+ let value = 0, shift = 0;
349
+ for (const digit of text) {
350
+ const bits = digits.indexOf(digit);
351
+ value += (bits & 31) << shift;
352
+ if (bits & 32) {
353
+ shift += 5;
354
+ continue;
355
+ }
356
+ fields.push(value & 1 ? -(value >> 1) : value >> 1);
357
+ value = shift = 0;
358
+ }
359
+ segments.push(fields.map((field, index) => (state[index] += field)));
360
+ }
361
+ lines.push(segments);
362
+ }
363
+ return lines;
364
+ };
365
+ const encoded = (lines) => {
366
+ const state = [0, 0, 0, 0, 0];
367
+ const number = (value) => {
368
+ let rest = value < 0 ? (-value << 1) | 1 : value << 1;
369
+ let text = "";
370
+ do {
371
+ const bits = rest & 31;
372
+ rest >>>= 5;
373
+ text += digits[rest > 0 ? bits | 32 : bits];
374
+ } while (rest > 0);
375
+ return text;
376
+ };
377
+ return lines.map((segments) => {
378
+ state[0] = 0;
379
+ return segments.map((fields) => fields.map((field, index) => {
380
+ const text = number(field - state[index]);
381
+ state[index] = field;
382
+ return text;
383
+ }).join("")).join(",");
384
+ }).join(";");
385
+ };
386
+ const ownMap = (file) => {
387
+ const text = readSync(file, "utf8");
388
+ const found = text.lastIndexOf(mapComment);
389
+ return found < 0 ? undefined : JSON.parse(Buffer.from(text.slice(found + mapComment.length).trimEnd(), "base64").toString("utf8"));
390
+ };
391
+ const composed = (text) => {
392
+ let map;
393
+ try {
394
+ map = JSON.parse(text);
395
+ if (map?.version !== 3 || typeof map.mappings !== "string" || !Array.isArray(map.sources))
396
+ return text;
397
+ const through = map.sources.map((source, index) => {
398
+ if (typeof source !== "string" || rewrittenUnder(copies, source) === undefined)
399
+ return undefined;
400
+ const own = ownMap(source);
401
+ if (!own || own.sourcesContent?.[0] === undefined || map.sourcesContent?.[index] === own.sourcesContent[0])
402
+ return undefined;
403
+ return { own, lines: decoded(own.mappings) };
404
+ });
405
+ if (!through.some(Boolean))
406
+ return text;
407
+ const lines = decoded(map.mappings).map((segments) => segments.map((fields) => {
408
+ const inner = fields.length >= 4 ? through[fields[1]] : undefined;
409
+ if (!inner)
410
+ return fields;
411
+ // The segment of the rewritten file's line that starts at or
412
+ // before the position; one that maps nothing is Supercov's.
413
+ const found = (inner.lines[fields[2]] ?? []).filter((segment) => segment[0] <= fields[3]).pop();
414
+ if (!found || found.length < 4)
415
+ return [fields[0]];
416
+ return [fields[0], fields[1], found[2], found[3], ...fields.slice(4)];
417
+ }));
418
+ through.forEach((inner, index) => {
419
+ if (!inner)
420
+ return;
421
+ const source = inner.own.sources?.[0];
422
+ if (typeof source === "string" && isAbsolute(source))
423
+ map.sources[index] = source;
424
+ map.sourcesContent ?? (map.sourcesContent = []);
425
+ map.sourcesContent[index] = inner.own.sourcesContent[0];
426
+ });
427
+ map.mappings = encoded(lines);
428
+ return JSON.stringify(map);
429
+ }
430
+ catch {
431
+ return text;
432
+ }
433
+ };
434
+ const { readFileSync: readSync, existsSync: exists } = fs;
435
+ const { readFile: readPromise } = fs.promises;
436
+ const { relative: relativePath } = path;
437
+ function supercovReadFileSync(file, ...rest) {
438
+ if (!failureOutput.prints)
439
+ return Reflect.apply(readSync, this, [file, ...rest]);
440
+ const local = rewrittenUnder(copies, file);
441
+ if (local === undefined) {
442
+ const contents = Reflect.apply(readSync, this, [file, ...rest]);
443
+ return failureOutput.composes && typeof contents === "string" && typeof file === "string" && file.endsWith(".map")
444
+ ? composed(contents)
445
+ : contents;
446
+ }
447
+ if (calledByPrinter(supercovReadFileSync)) {
448
+ const changed = kept(".changed", local);
449
+ return Reflect.apply(readSync, this, [exists(changed) ? changed : kept(".authored", local), ...rest]);
450
+ }
451
+ const contents = Reflect.apply(readSync, this, [file, ...rest]);
452
+ return failureOutput.names ? namedForItself(file, contents) : contents;
453
+ }
454
+ fs.readFileSync = supercovReadFileSync;
455
+ // graceful-fs, which Jest reads through, copies `fs` when it loads, and a
456
+ // tool that runs Jest in its own process has usually loaded it by then.
457
+ for (const [file, cached] of Object.entries(Module._cache)) {
458
+ if (/[\\/]graceful-fs[\\/]graceful-fs\.js$/.test(file) && typeof cached?.exports?.readFileSync === "function") {
459
+ try {
460
+ cached.exports.readFileSync = supercovReadFileSync;
461
+ }
462
+ catch { }
463
+ }
464
+ }
465
+ fs.promises.readFile = async function supercovReadFile(file, ...rest) {
466
+ const contents = await Reflect.apply(readPromise, this, [file, ...rest]);
467
+ return failureOutput.names && rewrittenUnder(copies, file) !== undefined && (typeof contents === "string" || Buffer.isBuffer(contents))
468
+ ? namedForItself(file, contents)
469
+ : contents;
470
+ };
471
+ function supercovRelative(from, to) {
472
+ if (!failureOutput.prints)
473
+ return Reflect.apply(relativePath, this, [from, to]);
474
+ const local = rewrittenUnder(copies, to) === undefined ? rewrittenUnder(projects, to) : undefined;
475
+ return local !== undefined && calledByPrinter(supercovRelative)
476
+ ? Reflect.apply(relativePath, this, [from, copies[0] + local.split("/").join(sep)])
477
+ : Reflect.apply(relativePath, this, [from, to]);
478
+ }
479
+ path.relative = supercovRelative;
480
+ syncBuiltinESMExports();
481
+ };
482
+ // Jest itself prints from here on. Any other process is Jest's from the
483
+ // moment it loads Jest: a worker of its, or a tool that runs Jest in its own
484
+ // process (`require("jest").run()`, as react-scripts and Vue's CLI do), where
485
+ // a failure in a single test file still printed the line Supercov had
486
+ // wrapped. A worker of another tool's (a bundler minifies in the same kind
487
+ // of process) never loads it.
488
+ if (isJestEntrypoint)
489
+ process.__SUPERCOV_FAILURE_OUTPUT__("jest");
490
+ else {
491
+ const jest = /(?:^|[\\/])(?:jest|jest-cli|@jest[\\/]core|jest-runner)(?:[\\/]|$)/;
492
+ const load = Module._load;
493
+ let loaded = false;
494
+ Module._load = function supercovJestLoad(request, ...rest) {
495
+ if (!loaded && typeof request === "string" && request.includes("jest") && jest.test(request)) {
496
+ loaded = true;
497
+ process.__SUPERCOV_FAILURE_OUTPUT__("jest");
498
+ }
499
+ return Reflect.apply(load, this, [request, ...rest]);
500
+ };
501
+ }
227
502
  // Next.js looks for lockfiles from its directory upwards, takes the outermost
228
503
  // as its workspace root, and warns when it finds more than one. The copy
229
504
  // holds the project's lockfile and lies inside the project, so every build
230
505
  // under Supercov warned twice that the root "may not be correct", naming the
231
506
  // copy's lockfile. The root Next picks is the project, which is right: it
232
- // holds the copy and the dependencies. The copy's lockfile is left out of
233
- // what the warning counts, and one the project really has twice still warns.
234
- if (/\/node_modules\/(?:\.bin\/next$|next\/dist\/)/.test(entrypoint)) {
507
+ // holds the copy and the dependencies. The copy's lockfile is taken out of
508
+ // the warning, which is not printed when no other is left; one the project
509
+ // really has twice still warns.
510
+ //
511
+ // The warning is recognised by its text where Next prints it, in whatever
512
+ // process loads Next: started by its own command, by a server of the
513
+ // project's, or from a script. Next 15.4 to 16.4 print it through `warnOnce`.
514
+ {
235
515
  const copy = fileURLToPath(new URL("../../", import.meta.url));
236
516
  const copies = [...new Set([copy, (() => {
237
517
  try {
238
- return realpathSync(copy);
518
+ return `${realpathSync(copy)}${sep}`;
239
519
  }
240
520
  catch {
241
521
  return copy;
242
522
  }
243
523
  })()])];
244
- const declared = "function warnDuplicatedLockFiles(lockFiles) {";
524
+ const quieted = `
525
+ ;try { typeof warnOnce === "function" && (warnOnce = ((print) => function (...message) {
526
+ const text = message[0];
527
+ if (typeof text !== "string" || !text.includes("multiple lockfiles"))
528
+ return print.apply(this, message);
529
+ const copies = ${JSON.stringify(copies)};
530
+ const lines = text.split("\\n");
531
+ const listed = (line) => /^\\s+\\* \\S/.test(line);
532
+ const kept = lines.filter((line) => !listed(line) || !copies.some((copy) => line.trim().slice(2).startsWith(copy)));
533
+ if (!kept.some(listed))
534
+ return undefined;
535
+ return print.apply(this, [kept.join("\\n"), ...message.slice(1)]);
536
+ })(warnOnce)); } catch {}
537
+ `.replace(/\n\s*/g, " ");
245
538
  const compile = Module.prototype._compile;
246
539
  Module.prototype._compile = function _compile(content, filename, ...rest) {
247
- if (typeof content === "string" && /[\\/]next[\\/]dist[\\/]lib[\\/]find-root\.js$/.test(filename)) {
248
- content = content.replace(declared, `${declared} lockFiles = lockFiles.filter((file, index) => index === lockFiles.length - 1 || !${JSON.stringify(copies)}.some((copy) => file.startsWith(copy)));`);
540
+ if (typeof content === "string" && typeof filename === "string" && filename.endsWith("log.js") &&
541
+ /[\\/]next[\\/]dist[\\/]build[\\/]output[\\/]log\.js$/.test(filename)) {
542
+ // After the last statement, before the source map's line.
543
+ const map = content.lastIndexOf("\n//# sourceMappingURL=");
544
+ content = map < 0 ? `${content}${quieted}` : `${content.slice(0, map)}${quieted}${content.slice(map)}`;
249
545
  }
250
546
  return Reflect.apply(compile, this, [content, filename, ...rest]);
251
547
  };
@@ -905,8 +905,12 @@ function cleanInstrumentationStack(error, lexical = false) {
905
905
  const frame = /^(\s*at (?:async )?)(?:(.*?) \()?(.*?:\d+:\d+)\)?$/;
906
906
  const added = lexical && first > 1 ? frame.exec(lines[first - 1]) : null;
907
907
  const written = added && last + 1 < lines.length ? frame.exec(lines[last + 1]) : null;
908
- if (added && written && added[2] === void 0 && !/^node:/.test(written[3])) {
909
- const merged = written[2] === void 0 ? lines[first - 1] : `${written[1]}${written[2]} (${added[3]})`;
908
+ const file = (location) => location.replace(/:\d+:\d+$/, "");
909
+ // Both are frames of the author's file, or the error is another's. A name
910
+ // on the added one comes from a source map (Jest names a frame by what is
911
+ // called at its position), and it is the name the author's line would have.
912
+ if (added && written && file(added[3]) === file(written[3])) {
913
+ const merged = added[2] !== void 0 || written[2] === void 0 ? lines[first - 1] : `${written[1]}${written[2]} (${added[3]})`;
910
914
  visible = [...lines.slice(0, first - 1), merged, ...lines.slice(last + 2)];
911
915
  } else {
912
916
  visible = lines.filter((line, index) => !hidden[index]);
@@ -922,8 +926,16 @@ function withNodeAssertionPhase(operation, source, callback) {
922
926
  const lexical = typeof source === "string";
923
927
  const context = currentRequestContext();
924
928
  const scope = context.scope;
925
- if (!scope)
926
- return callback();
929
+ // No test is known to be running: a runner Supercov has no adapter in, such
930
+ // as Jest started from another tool's process. The assertion still fails
931
+ // with the frames its author would see.
932
+ if (!scope) {
933
+ try {
934
+ return callback();
935
+ } catch (error) {
936
+ throw cleanInstrumentationStack(error, lexical);
937
+ }
938
+ }
927
939
  // A lexically instrumented occurrence is an authored assertion site in its
928
940
  // own right, even when it runs inside another assertion's callback: the
929
941
  // validator passed to assert.throws is the ordinary case, and its inner
@@ -1,112 +1,7 @@
1
- import fs from "node:fs";
2
- import { syncBuiltinESMExports } from "node:module";
3
- import { basename, isAbsolute, relative, resolve, sep } from "node:path";
1
+ import { relative, resolve, sep } from "node:path";
4
2
  import { fileURLToPath } from "node:url";
5
3
  import { inferTestProvenance } from "./provenance.mjs";
6
4
  import { appendEvidenceRecord } from "./atomic.mjs";
7
- // The files of the copy Supercov rewrote, by path inside it, and where each
8
- // is kept as its author wrote it or as a tool of the command changed it.
9
- const copyRoots = (() => {
10
- const copy = fileURLToPath(new URL("../../", import.meta.url));
11
- try {
12
- return [...new Set([copy, fs.realpathSync(copy)])];
13
- }
14
- catch {
15
- return [copy];
16
- }
17
- })();
18
- let rewritten;
19
- function rewrittenFile(path) {
20
- if (rewritten === undefined) {
21
- try {
22
- rewritten = new Set(JSON.parse(fs.readFileSync(new URL("./authored-sources.json", import.meta.url), "utf8")));
23
- }
24
- catch {
25
- rewritten = new Set();
26
- }
27
- }
28
- if (typeof path !== "string" || !isAbsolute(path))
29
- return undefined;
30
- for (const root of copyRoots) {
31
- const local = relative(root, path);
32
- if (local !== ".." && !local.startsWith(`..${sep}`) && !isAbsolute(local)) {
33
- const file = local.split(sep).join("/");
34
- return rewritten.has(file) ? file : undefined;
35
- }
36
- }
37
- return undefined;
38
- }
39
- const SOURCE_MAP = "\n//# sourceMappingURL=data:application/json;base64,";
40
- /**
41
- * A rewritten file's source map names the project's own file, by absolute
42
- * path, so a Node stack trace reads as the project's. Vitest prints a frame
43
- * relative to its root, which is the copy: `../../../../lib/crypto.ts:3:11`
44
- * where it prints `lib/crypto.ts:3:11` without Supercov, and a file outside
45
- * its module graph gets no code under the frame. Vite is handed the same
46
- * code with a map that names the file itself.
47
- */
48
- export function supercovSourceMaps() {
49
- const { readFile } = fs.promises;
50
- return {
51
- name: "supercov:source-maps",
52
- enforce: "pre",
53
- async load(id) {
54
- if (rewrittenFile(id) === undefined)
55
- return null;
56
- try {
57
- const code = await readFile(id, "utf8");
58
- const found = code.lastIndexOf(SOURCE_MAP);
59
- if (found < 0)
60
- return null;
61
- const encoded = code.slice(found + SOURCE_MAP.length).trim();
62
- if (/\s/.test(encoded))
63
- return null;
64
- const map = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
65
- map.sources = [basename(id)];
66
- return { code: code.slice(0, found + 1), map };
67
- }
68
- catch {
69
- return null;
70
- }
71
- },
72
- };
73
- }
74
- // Vitest reads a file a failure names: to print the code around the nearest
75
- // frame, and (Vitest 4) to look for a source map in it. In the copy that is
76
- // the rewritten file. An assertion wrapped into one long line printed no code
77
- // at all, any other line printed Supercov's, and positions that were already
78
- // the author's were mapped a second time through the file's own map: a test
79
- // file's frame at `:12:35` read `:12:0`. What Vitest itself reads is the text
80
- // the file was rewritten from. What runs is not read here: Vite loads it.
81
- let readsAuthored = false;
82
- function readAuthoredFromVitest() {
83
- if (readsAuthored)
84
- return;
85
- readsAuthored = true;
86
- const { readFileSync, existsSync } = fs;
87
- const kept = (name, file) => fileURLToPath(new URL(`./${name}/${file}`, import.meta.url));
88
- const calledByVitest = () => {
89
- const holder = {};
90
- const limit = Error.stackTraceLimit;
91
- try {
92
- Error.stackTraceLimit = 1;
93
- Error.captureStackTrace(holder, supercovReadFileSync);
94
- }
95
- finally {
96
- Error.stackTraceLimit = limit;
97
- }
98
- return /[\\/]vitest[\\/]dist[\\/]/.test(String(holder.stack).split("\n")[1] ?? "");
99
- };
100
- function supercovReadFileSync(path, ...rest) {
101
- const file = rewrittenFile(path);
102
- if (file === undefined || !calledByVitest())
103
- return Reflect.apply(readFileSync, this, [path, ...rest]);
104
- const changed = kept(".changed", file);
105
- return Reflect.apply(readFileSync, this, [existsSync(changed) ? changed : kept(".authored", file), ...rest]);
106
- }
107
- fs.readFileSync = supercovReadFileSync;
108
- syncBuiltinESMExports();
109
- }
110
5
  function sourcePath(moduleId) {
111
6
  const absolute = moduleId.startsWith("file:")
112
7
  ? fileURLToPath(moduleId)
@@ -134,7 +29,8 @@ export default class SupercovVitestReporter {
134
29
  }
135
30
  onInit(vitest) {
136
31
  this.configureProjects?.(vitest.projects);
137
- readAuthoredFromVitest();
32
+ // Failures print the project's paths and code: see register.mjs.
33
+ process.__SUPERCOV_FAILURE_OUTPUT__?.("vitest");
138
34
  }
139
35
  onBrowserInit(project) {
140
36
  this.configureProjects?.([project]);