supercov 4.1.0 → 4.1.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
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.1.1",
123
+ "@supercov/cli-darwin-x64": "4.1.1",
124
+ "@supercov/cli-linux-arm64-gnu": "4.1.1",
125
+ "@supercov/cli-linux-arm64-musl": "4.1.1",
126
+ "@supercov/cli-linux-x64-gnu": "4.1.1",
127
+ "@supercov/cli-linux-x64-musl": "4.1.1",
128
+ "@supercov/cli-win32-arm64": "4.1.1",
129
+ "@supercov/cli-win32-x64": "4.1.1"
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,6 +224,258 @@ 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
+ fs.promises.readFile = async function supercovReadFile(file, ...rest) {
456
+ const contents = await Reflect.apply(readPromise, this, [file, ...rest]);
457
+ return failureOutput.names && rewrittenUnder(copies, file) !== undefined && (typeof contents === "string" || Buffer.isBuffer(contents))
458
+ ? namedForItself(file, contents)
459
+ : contents;
460
+ };
461
+ function supercovRelative(from, to) {
462
+ if (!failureOutput.prints)
463
+ return Reflect.apply(relativePath, this, [from, to]);
464
+ const local = rewrittenUnder(copies, to) === undefined ? rewrittenUnder(projects, to) : undefined;
465
+ return local !== undefined && calledByPrinter(supercovRelative)
466
+ ? Reflect.apply(relativePath, this, [from, copies[0] + local.split("/").join(sep)])
467
+ : Reflect.apply(relativePath, this, [from, to]);
468
+ }
469
+ path.relative = supercovRelative;
470
+ syncBuiltinESMExports();
471
+ };
472
+ // Jest itself prints from here on. A worker may be Jest's or another tool's
473
+ // (a bundler minifies in the same kind of process): Jest's setup file names
474
+ // the runner when a test file loads there.
475
+ if (isJestEntrypoint)
476
+ process.__SUPERCOV_FAILURE_OUTPUT__("jest");
477
+ else if (process.env.JEST_WORKER_ID)
478
+ process.__SUPERCOV_FAILURE_OUTPUT__();
227
479
  // Next.js looks for lockfiles from its directory upwards, takes the outermost
228
480
  // as its workspace root, and warns when it finds more than one. The copy
229
481
  // holds the project's lockfile and lies inside the project, so every build
@@ -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]);
@@ -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]);