supercov 4.0.1 → 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/docs/cli.md CHANGED
@@ -147,6 +147,7 @@ npx supercov runs <run-id> [query] [options]
147
147
  | `decision <id \| path:line>` | Understand missing boolean outcomes and MC/DC witnesses |
148
148
  | `line <path:line>` | See one line's state, obligations, and covering tests |
149
149
  | `test <id \| name>` | See the coverage attributed to one test |
150
+ | `tests without-evidence` | List the tests that made assertions and recorded no coverage, which the summary counts in a warning |
150
151
  | `kinds` | Group coverage by test level, such as unit or E2E |
151
152
  | `runners` | Group coverage by test runner |
152
153
  | `scope` | See what is source and what is not, by directory and reason; `--files` lists every file |
@@ -175,12 +176,16 @@ npx supercov runs latest files --group dir --depth 2 --metric branches
175
176
  ```
176
177
 
177
178
  ```
178
- Directory Files Lines e2e unit All
179
- app 4 15 86.67% 0.00% 100.00%
180
- lib 1 10 90.00% 40.00% 90.00%
181
- components 1 2 100.00% 0.00% 100.00%
179
+ Directory Files Lines e2e unit no test All
180
+ app 5 23 86.96% 0.00% 8.70% 95.65%
181
+ lib 1 10 90.00% 40.00% 0.00% 90.00%
182
+ . 2 8 75.00% 0.00% 25.00% 100.00%
183
+ components 1 2 100.00% 0.00% 0.00% 100.00%
182
184
  ```
183
185
 
186
+ `no test` is what ran only while no test was running, such as a server
187
+ starting before the first test. The column is there when a directory has any.
188
+
184
189
  In `--json`, every file of `files` and `gaps` carries `totals` and `covered`
185
190
  for each metric beside what is missing, so a percentage can be worked out for
186
191
  any file or set of files.
@@ -106,6 +106,14 @@ By test kind
106
106
  no test lines 17.14% branches 0.00% MC/DC 0.00%
107
107
  ```
108
108
 
109
+ `files --group dir` has the same figure as a `no test` column wherever a
110
+ directory has any, so a directory's `All` never stands above its kinds
111
+ unexplained.
112
+
113
+ A test that made assertions and has no covered line to its name is counted in
114
+ a warning on the summary. `npx supercov runs latest tests without-evidence`
115
+ lists every such test, with its file, kind and outcome.
116
+
109
117
  See [Supported suites](supported-suites.md) for the attribution available from
110
118
  each runner.
111
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "4.0.1",
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.0.1",
123
- "@supercov/cli-darwin-x64": "4.0.1",
124
- "@supercov/cli-linux-arm64-gnu": "4.0.1",
125
- "@supercov/cli-linux-arm64-musl": "4.0.1",
126
- "@supercov/cli-linux-x64-gnu": "4.0.1",
127
- "@supercov/cli-linux-x64-musl": "4.0.1",
128
- "@supercov/cli-win32-arm64": "4.0.1",
129
- "@supercov/cli-win32-x64": "4.0.1"
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";
@@ -85,6 +85,34 @@ if (process.env.SUPERCOV_DURABLE_EVIDENCE_EACH_TEST === "1" ||
85
85
  // and may evaluate before any ESM instrumented file imports it.
86
86
  globalThis.__supercovRuntime ??= globalThis.__SUPERCOV_DIRECT_RUNTIME__;
87
87
  }
88
+ // Next.js runs a middleware and a route with `runtime = "edge"` in a VM
89
+ // context of its own, the Edge Runtime's, and that context's global is not
90
+ // this one. An instrumented file found no runtime there: every request to an
91
+ // application with a middleware.ts failed on "Cannot read properties of
92
+ // undefined (reading 'mcdcBegin')", and `next build` failed collecting page
93
+ // data for an edge route. Such a context reads this process's runtime, so
94
+ // what runs there is measured like the rest of the server.
95
+ const virtualMachine = Module._load("node:vm", undefined, false);
96
+ const createContext = virtualMachine.createContext;
97
+ virtualMachine.createContext = function createContextWithRuntime(...parameters) {
98
+ const context = Reflect.apply(createContext, this, parameters);
99
+ if (parameters[1]?.name === "Edge Runtime") {
100
+ try {
101
+ Object.defineProperty(context, "__SUPERCOV_DIRECT_RUNTIME__", {
102
+ configurable: true,
103
+ enumerable: false,
104
+ get: () => globalThis.__SUPERCOV_DIRECT_RUNTIME__ ?? process.__SUPERCOV_DIRECT_RUNTIME__,
105
+ set(value) {
106
+ Object.defineProperty(context, "__SUPERCOV_DIRECT_RUNTIME__", {
107
+ configurable: true, enumerable: false, writable: true, value,
108
+ });
109
+ },
110
+ });
111
+ }
112
+ catch { }
113
+ }
114
+ return context;
115
+ };
88
116
  // Workers are independent Node processes and an explicit `execArgv: []`
89
117
  // otherwise strips the preload that supplies the isolated runtime. Preserve
90
118
  // every user option while adding exactly one Supercov import.
@@ -196,6 +224,284 @@ else if (/\/node_modules\/next\/dist\/compiled\/jest-worker\/processChild\.js$/.
196
224
  return Reflect.apply(compile, this, [content, filename, ...rest]);
197
225
  };
198
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__();
479
+ // Next.js looks for lockfiles from its directory upwards, takes the outermost
480
+ // as its workspace root, and warns when it finds more than one. The copy
481
+ // holds the project's lockfile and lies inside the project, so every build
482
+ // under Supercov warned twice that the root "may not be correct", naming the
483
+ // copy's lockfile. The root Next picks is the project, which is right: it
484
+ // holds the copy and the dependencies. The copy's lockfile is left out of
485
+ // what the warning counts, and one the project really has twice still warns.
486
+ if (/\/node_modules\/(?:\.bin\/next$|next\/dist\/)/.test(entrypoint)) {
487
+ const copy = fileURLToPath(new URL("../../", import.meta.url));
488
+ const copies = [...new Set([copy, (() => {
489
+ try {
490
+ return realpathSync(copy);
491
+ }
492
+ catch {
493
+ return copy;
494
+ }
495
+ })()])];
496
+ const declared = "function warnDuplicatedLockFiles(lockFiles) {";
497
+ const compile = Module.prototype._compile;
498
+ Module.prototype._compile = function _compile(content, filename, ...rest) {
499
+ if (typeof content === "string" && /[\\/]next[\\/]dist[\\/]lib[\\/]find-root\.js$/.test(filename)) {
500
+ content = content.replace(declared, `${declared} lockFiles = lockFiles.filter((file, index) => index === lockFiles.length - 1 || !${JSON.stringify(copies)}.some((copy) => file.startsWith(copy)));`);
501
+ }
502
+ return Reflect.apply(compile, this, [content, filename, ...rest]);
503
+ };
504
+ }
199
505
  function installAuthoredSourceView() {
200
506
  let authored;
201
507
  try {
@@ -886,21 +886,44 @@ function finishAssertionPhase(phase, error) {
886
886
  phase.error = error instanceof Error ? error.message : String(error);
887
887
  }
888
888
 
889
- function cleanInstrumentationStack(error) {
889
+ const INSTRUMENTATION_FRAME = /[\\/]\.supercov[\\/](?:node_modules[\\/])?(?:playwright|nodeTest|vitest|runtime|launchSupervisor|nodeAssert|nodeAssertStrict|nodeAssertAdapter|register|resolve-loader)\.(?:js|mjs)(?::|\))/u;
890
+ // A lexical wrapper runs the assertion in a function of its own, so its
891
+ // stack had two frames where the author wrote one: the function Supercov
892
+ // added, at the assertion, and below the runtime's frames the function the
893
+ // author wrote, at the wrapper. Vitest printed both, `:12:35` and `:12:1`.
894
+ // They are one frame again: the author's function, where the assertion is.
895
+ function cleanInstrumentationStack(error, lexical = false) {
890
896
  if (!error || typeof error !== "object" || typeof error.stack !== "string")
891
897
  return error;
892
898
  const lines = error.stack.split("\n");
893
- const visible = lines.filter((line, index) => index === 0 || !/[\\/]\.supercov[\\/](?:node_modules[\\/])?(?:playwright|nodeTest|vitest|runtime|launchSupervisor|nodeAssert|nodeAssertStrict|nodeAssertAdapter|register|resolve-loader)\.(?:js|mjs)(?::|\))/u.test(line));
894
- if (visible.length !== lines.length) {
895
- try {
896
- error.stack = visible.join("\n");
897
- } catch (e) {
898
- }
899
+ const hidden = lines.map((line, index) => index > 0 && INSTRUMENTATION_FRAME.test(line));
900
+ const first = hidden.indexOf(true);
901
+ if (first < 0)
902
+ return error;
903
+ const last = hidden.lastIndexOf(true);
904
+ let visible;
905
+ const frame = /^(\s*at (?:async )?)(?:(.*?) \()?(.*?:\d+:\d+)\)?$/;
906
+ const added = lexical && first > 1 ? frame.exec(lines[first - 1]) : null;
907
+ const written = added && last + 1 < lines.length ? frame.exec(lines[last + 1]) : null;
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]})`;
914
+ visible = [...lines.slice(0, first - 1), merged, ...lines.slice(last + 2)];
915
+ } else {
916
+ visible = lines.filter((line, index) => !hidden[index]);
917
+ }
918
+ try {
919
+ error.stack = visible.join("\n");
920
+ } catch (e) {
899
921
  }
900
922
  return error;
901
923
  }
902
924
  function withNodeAssertionPhase(operation, source, callback) {
903
925
  var _a8;
926
+ const lexical = typeof source === "string";
904
927
  const context = currentRequestContext();
905
928
  const scope = context.scope;
906
929
  if (!scope)
@@ -944,13 +967,13 @@ function withNodeAssertionPhase(operation, source, callback) {
944
967
  return value;
945
968
  }, (error) => {
946
969
  finishAssertionPhase(phase, error);
947
- throw cleanInstrumentationStack(error);
970
+ throw cleanInstrumentationStack(error, lexical);
948
971
  });
949
972
  finishAssertionPhase(phase);
950
973
  return result;
951
974
  } catch (error) {
952
975
  finishAssertionPhase(phase, error);
953
- throw cleanInstrumentationStack(error);
976
+ throw cleanInstrumentationStack(error, lexical);
954
977
  }
955
978
  }
956
979
  // Callee binding preserves receiver, getter/evaluation order, spreads and the
@@ -29,6 +29,8 @@ export default class SupercovVitestReporter {
29
29
  }
30
30
  onInit(vitest) {
31
31
  this.configureProjects?.(vitest.projects);
32
+ // Failures print the project's paths and code: see register.mjs.
33
+ process.__SUPERCOV_FAILURE_OUTPUT__?.("vitest");
32
34
  }
33
35
  onBrowserInit(project) {
34
36
  this.configureProjects?.([project]);