supercov 3.0.5 → 4.0.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
@@ -530,8 +530,8 @@ terminal or offline environment after the package has been downloaded.
530
530
  | --- | --- |
531
531
  | `SUPERCOV_SOURCE_ROOTS` | Comma-separated directories or files that hold your own code, in any language; everything else is left out |
532
532
  | `SUPERCOV_TEST_KIND` | Label the wrapped command as a test level such as `unit` or `e2e` |
533
- | `SUPERCOV_KEEP_WORKSPACE` | Set to `1` to leave the instrumented copy of the project in `.supercov/workspaces/` after the run, to inspect what the command ran. It is kept without this when the build Supercov runs before the tests fails |
534
- | `SUPERCOV_BUILD_COMMAND` | The build a JavaScript project's tests need, as words or a JSON array, such as `yarn workspace web build` in a monorepo whose root `build` builds every package. Without it Supercov runs the project's `build` script, through the package manager that started the tests, when the tests need built output |
533
+ | `SUPERCOV_SOURCE_TOOLS` | Comma-separated names of tools the test command runs that read source as text, such as a linter or a spell checker, besides Biome, oxlint, dprint, cspell and knip. Each is run on your source as you wrote it instead of on the instrumented copy |
534
+ | `SUPERCOV_KEEP_WORKSPACE` | Set to `1` to leave the instrumented copy of the project in `.supercov/workspaces/` after the run, to inspect what the command ran |
535
535
 
536
536
  Examples:
537
537
 
@@ -1,8 +1,9 @@
1
1
  # Speed and storage
2
2
 
3
- A Supercov run includes your test command, an instrumented build, and evidence
4
- publication. The first pass is usually the slowest; repeated passes can reuse
5
- the isolated build when the relevant inputs have not changed.
3
+ A Supercov run includes your test command and evidence publication, and for a
4
+ compiled language an instrumented build, which repeated passes can reuse when
5
+ the relevant inputs have not changed. A JavaScript project has no such build:
6
+ the command runs as given, and a build inside it is part of the command's time.
6
7
 
7
8
  ## See where the time went
8
9
 
@@ -17,7 +18,7 @@ The summary separates:
17
18
  | Initialization | Recovery, project discovery, and input checks |
18
19
  | Workspace preparation | Refreshing the isolated project copy |
19
20
  | Adapter setup | Preparing the runner integration |
20
- | Instrumented build | Building measured source, or almost nothing on a cache hit |
21
+ | Instrumented build | Building measured source for a compiled language, or almost nothing on a cache hit; 0 for JavaScript |
21
22
  | Test command | The wrapped command, including browser, VM, or remote latency |
22
23
  | Evidence publication | Validating and storing the completed run |
23
24
 
@@ -41,7 +42,9 @@ before Supercov starts and is not coverage-engine overhead.
41
42
 
42
43
  Supercov reuses an instrumented build only when source, dependencies,
43
44
  configuration, toolchain, build mode, and instrumenter identity match. A
44
- possible mismatch triggers a fresh build rather than risking stale coverage.
45
+ possible mismatch triggers a fresh build rather than risking stale coverage. A
46
+ build inside a JavaScript project's command is the command's own and runs every
47
+ time, as it does without Supercov.
45
48
 
46
49
  ## Use focused runs carefully
47
50
 
@@ -142,6 +142,21 @@ Its lines, methods and simple branches stay measured; everything that needs a
142
142
  probe is declared as a measurement limit for that file. Please report the file,
143
143
  since Supercov aims to instrument every Ruby source correctly.
144
144
 
145
+ ## The tests cannot find `dist/` or a production build
146
+
147
+ Supercov runs your command in a copy of the project, and the copy starts
148
+ without build output: `dist/`, `build/`, `.next/` and the like are not copied,
149
+ because they hold code that was never instrumented. Supercov does not run your
150
+ `build` script for you (earlier versions did). If the tests read build output, put
151
+ the build in the command:
152
+
153
+ ```bash
154
+ npx supercov -- sh -c "npm run build && npm test"
155
+ ```
156
+
157
+ A `pretest` script, or a Playwright `webServer` that builds, does the same. The
158
+ build then compiles the instrumented copy, and what the tests run is measured.
159
+
145
160
  ## A test that reads your source fails under Supercov
146
161
 
147
162
  A test that opens your source files and asserts on their text sees the probes,
@@ -220,10 +235,8 @@ runs remain intact.
220
235
 
221
236
  ## The first run is slow
222
237
 
223
- The first run may include the npm download, browser or toolchain startup,
224
- workspace creation, and an instrumented build. Repeated runs can reuse the
225
- isolated build when source, dependencies, configuration, toolchain, and build
226
- mode still match.
238
+ The first run may include the npm download, browser or toolchain startup, and
239
+ workspace creation.
227
240
 
228
241
  Inspect the recorded phases with:
229
242
 
@@ -36,6 +36,19 @@ and the instrumented copy is not what you wrote:
36
36
  the like) read each file as you wrote it, and don't see `.supercov`.
37
37
  - The type check `next build` runs reads each file as you wrote it too, while
38
38
  the build itself compiles the instrumented copy.
39
+ - Biome, oxlint, dprint, cspell and knip run on your source as you wrote it:
40
+ the copy's `node_modules/.bin` starts each of them in a view of the copy
41
+ that holds every file in its original text. A file the tool writes there
42
+ (a report, a cache) is the command's output like any other. To have another
43
+ tool that reads source as text run the same way, name it:
44
+ `SUPERCOV_SOURCE_TOOLS=typos,stylelint`.
45
+ - A change one of these tools makes to a source file (`prettier --write`,
46
+ `eslint --fix`, `biome check --write`, `dprint fmt`) is made in your
47
+ project when the command ends, and a tool later in the same command reads
48
+ the changed text. The tests ran the copy instrumented before the change, so
49
+ the run measured the file as it was, says so, and reads as stale; the next
50
+ run measures the changed file. A file you edited yourself while the command
51
+ ran keeps your edit, and the run names it.
39
52
  - Coverage tools the command runs itself (tap, c8, nyc, Jest's and Vitest's
40
53
  `--coverage`) still collect and report coverage. They measure the
41
54
  instrumented copy, and report close to what they report without Supercov,
@@ -58,9 +71,19 @@ outputs.
58
71
  The run prints where the synced files went, what stayed behind and what the
59
72
  command deleted, by top-level directory (`test-results/ 6`, `.next/ 2547`).
60
73
 
61
- When the build Supercov runs before the tests fails, the instrumented copy is
62
- left in `.supercov/workspaces/` until the next run, and the run prints its
63
- path. `SUPERCOV_KEEP_WORKSPACE=1` leaves it after any run.
74
+ The copy starts without build output: `dist/`, `build/`, `.next/` and the
75
+ like are not copied, because they hold code that was never instrumented.
76
+ Supercov runs the command you give it and no build of its own, so a suite that
77
+ needs build output has the build in its command:
78
+
79
+ ```bash
80
+ npx supercov -- sh -c "npm run build && npm test"
81
+ ```
82
+
83
+ A `pretest` script, or a Playwright `webServer` that builds, does the same.
84
+ When a run fails in a project that has a `build` script the command does not
85
+ reach, it says this. `SUPERCOV_KEEP_WORKSPACE=1` leaves the instrumented copy
86
+ in `.supercov/workspaces/` after a run, to inspect.
64
87
 
65
88
  ## Files Supercov creates
66
89
 
@@ -80,9 +103,10 @@ fallback instead of adopting or deleting it.
80
103
 
81
104
  ## Repeated runs and the build cache
82
105
 
83
- When source, dependencies, configuration, toolchain, and build mode still
84
- match, Supercov can reuse the isolated instrumented build. Test-only changes do
85
- not force an unrelated application rebuild.
106
+ For a compiled language, when source, dependencies, configuration, toolchain,
107
+ and build mode still match, Supercov can reuse the isolated instrumented build,
108
+ so test-only changes do not force an unrelated rebuild. A JavaScript project's
109
+ build is part of its command and runs every time.
86
110
 
87
111
  Workspace refreshes are prepared separately and become active only when
88
112
  complete. An interrupted refresh does not replace the last complete cache with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "3.0.5",
3
+ "version": "4.0.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": "3.0.5",
123
- "@supercov/cli-darwin-x64": "3.0.5",
124
- "@supercov/cli-linux-arm64-gnu": "3.0.5",
125
- "@supercov/cli-linux-arm64-musl": "3.0.5",
126
- "@supercov/cli-linux-x64-gnu": "3.0.5",
127
- "@supercov/cli-linux-x64-musl": "3.0.5",
128
- "@supercov/cli-win32-arm64": "3.0.5",
129
- "@supercov/cli-win32-x64": "3.0.5"
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"
130
130
  },
131
131
  "peerDependencies": {
132
132
  "@playwright/test": ">=1.55.0",
@@ -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 { isAbsolute, relative, resolve, sep } from "node:path";
11
+ import { 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";
@@ -205,6 +205,8 @@ function installAuthoredSourceView() {
205
205
  authored = new Set();
206
206
  }
207
207
  const authoredRoot = fileURLToPath(new URL("./.authored/", import.meta.url));
208
+ // What a tool of the command wrote to a rewritten file: `--fix`, `--write`.
209
+ const changedRoot = fileURLToPath(new URL("./.changed/", import.meta.url));
208
210
  const workspace = fileURLToPath(new URL("../../", import.meta.url));
209
211
  const roots = [...new Set([workspace, (() => {
210
212
  try {
@@ -231,9 +233,28 @@ function installAuthoredSourceView() {
231
233
  }
232
234
  return undefined;
233
235
  };
236
+ const { existsSync: exists, mkdirSync: makeDirectory } = fs;
237
+ // A rewritten file reads as the command has it now: what one of its
238
+ // tools made of it, or what its author wrote.
234
239
  const authoredPath = (path) => {
235
240
  const local = inside(path);
236
- return local !== undefined && authored.has(local) ? resolve(authoredRoot, local) : path;
241
+ if (local === undefined || !authored.has(local))
242
+ return path;
243
+ const changed = resolve(changedRoot, local);
244
+ return exists(changed) ? changed : resolve(authoredRoot, local);
245
+ };
246
+ // And it is written beside the instrumented copy, never over it. Prettier
247
+ // with `--write` and ESLint with `--fix` replaced the copy with the
248
+ // source they had read: the file then ran unmeasured, and one whose tests
249
+ // passed read 0% covered. Supercov gives the project the change when the
250
+ // command ends.
251
+ const changedPath = (path) => {
252
+ const local = inside(path);
253
+ if (local === undefined || !authored.has(local))
254
+ return path;
255
+ const changed = resolve(changedRoot, local);
256
+ makeDirectory(dirname(changed), { recursive: true });
257
+ return changed;
237
258
  };
238
259
  // A recursive listing names nested entries by relative path, or as
239
260
  // entries whose parent is inside .supercov.
@@ -250,7 +271,17 @@ function installAuthoredSourceView() {
250
271
  ? entries
251
272
  : entries.filter((entry) => !hidden(entry));
252
273
  const { readFileSync: readSync, readFile: readCallback, readdirSync: listSync, readdir: listCallback } = fs;
253
- const { readFile: readPromise, readdir: listPromise } = fs.promises;
274
+ const { readFile: readPromise, readdir: listPromise, writeFile: writePromise } = fs.promises;
275
+ const { writeFileSync: writeSync, writeFile: writeCallback } = fs;
276
+ fs.writeFileSync = function writeFileSync(path, ...rest) {
277
+ return Reflect.apply(writeSync, this, [changedPath(path), ...rest]);
278
+ };
279
+ fs.writeFile = function writeFile(path, ...rest) {
280
+ return Reflect.apply(writeCallback, this, [changedPath(path), ...rest]);
281
+ };
282
+ fs.promises.writeFile = function writeFile(path, ...rest) {
283
+ return Reflect.apply(writePromise, this, [changedPath(path), ...rest]);
284
+ };
254
285
  fs.readFileSync = function readFileSync(path, ...rest) {
255
286
  return Reflect.apply(readSync, this, [authoredPath(path), ...rest]);
256
287
  };
@@ -390,7 +421,16 @@ function skipCoverageThresholds(tool) {
390
421
  console.error(`[supercov] ${tool}'s coverage thresholds stay as configured`, error);
391
422
  }
392
423
  }
393
- register(new URL("./resolve-loader.mjs", import.meta.url));
424
+ // A handler for `.ts` in the CommonJS loader that is not Node's own is a
425
+ // transpiler's require hook: ts-node/register, @swc/register and the like.
426
+ // It was there before this preload, and it is how the project's TypeScript
427
+ // loads. Any `--import` sends the entry point through the module loader
428
+ // instead, where such a file is read as an ES module and the hook never
429
+ // sees it; the loader is told so it can hand those files back.
430
+ const typescriptHandler = Module._extensions[".ts"];
431
+ register(new URL("./resolve-loader.mjs", import.meta.url), {
432
+ data: { typescriptRequireHook: typeof typescriptHandler === "function" && typescriptHandler.name !== "loadTS" },
433
+ });
394
434
  if (process.env.SUPERCOV_DEBUG === "1") {
395
435
  console.error("[supercov] preload", { entrypoint });
396
436
  }
@@ -1,4 +1,5 @@
1
- import { resolve as resolvePath } from "node:path";
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, resolve as resolvePath } from "node:path";
2
3
  import { fileURLToPath, pathToFileURL } from "node:url";
3
4
  const GENERATED_TARGET = "__SUPERCOV_PLAYWRIGHT_MODULE__";
4
5
  const TARGET = process.env.SUPERCOV_PLAYWRIGHT_MODULE ??
@@ -25,6 +26,41 @@ function belongsToProject(parentURL) {
25
26
  const generatedURL = `${projectURL}.supercov/`;
26
27
  return (parentURL.startsWith(projectURL) && !parentURL.startsWith(generatedURL));
27
28
  }
29
+ let typescriptRequireHook = false;
30
+ export function initialize(data) {
31
+ typescriptRequireHook = data?.typescriptRequireHook === true;
32
+ }
33
+ const packageTypes = new Map();
34
+ function packageType(directory) {
35
+ if (packageTypes.has(directory))
36
+ return packageTypes.get(directory);
37
+ let type;
38
+ try {
39
+ type = JSON.parse(readFileSync(resolvePath(directory, "package.json"), "utf8")).type ?? "commonjs";
40
+ }
41
+ catch {
42
+ const parent = dirname(directory);
43
+ type = parent === directory ? "commonjs" : packageType(parent);
44
+ }
45
+ packageTypes.set(directory, type);
46
+ return type;
47
+ }
48
+ // `node -r ts-node/register --test tests/a.test.ts` in a package without
49
+ // `"type"` passed alone and failed here with ERR_MODULE_NOT_FOUND on an
50
+ // extensionless import: the preload made Node load the entry point as an ES
51
+ // module, past the require hook that compiles it. A TypeScript file of a
52
+ // CommonJS package is given back to the CommonJS loader, where that hook is.
53
+ function throughRequireHook(resolved) {
54
+ if (!typescriptRequireHook || !resolved?.url?.startsWith("file:") || resolved.url.includes("/node_modules/"))
55
+ return resolved;
56
+ if (!/\.(?:ts|tsx|cts)$/.test(resolved.url) || /\.d\.c?ts$/.test(resolved.url))
57
+ return resolved;
58
+ if (resolved.format === "module" || resolved.format === "module-typescript")
59
+ return resolved;
60
+ if (!resolved.url.endsWith(".cts") && packageType(dirname(fileURLToPath(resolved.url))) === "module")
61
+ return resolved;
62
+ return { ...resolved, format: "commonjs" };
63
+ }
28
64
  export async function resolve(specifier, context, nextResolve) {
29
65
  // Some transpilers preserve the source-relative runtime import while moving
30
66
  // only the transformed application file to an output directory. The
@@ -73,7 +109,7 @@ export async function resolve(specifier, context, nextResolve) {
73
109
  }
74
110
  return nextResolve(REPLACEMENT, context);
75
111
  }
76
- return nextResolve(specifier, context);
112
+ return throughRequireHook(await nextResolve(specifier, context));
77
113
  }
78
114
 
79
115
  // Node's own test coverage filters files by `--test-coverage-include` and
@@ -1402,6 +1402,10 @@ function registerProbeV2(definition) {
1402
1402
  // A selection's outcomes -- short falsy, short truthy, right falsy, right
1403
1403
  // truthy -- are four consecutive points per site from the first index.
1404
1404
  selectionPoints: definition.selectionPoints,
1405
+ // What selectEndV2 and selectNextV2 read, for each tree of more than two
1406
+ // leaves.
1407
+ selectionSteps: (definition.selectionTrees || []).map((tree) => tree[0]),
1408
+ selectionChoices: (definition.selectionTrees || []).map((tree) => tree[1]),
1405
1409
  none: optionalCallEmptySpread
1406
1410
  };
1407
1411
  state.probeV2Files.add(file);
@@ -1626,6 +1630,44 @@ function selectPathV2(file, value) {
1626
1630
  }
1627
1631
  return value;
1628
1632
  }
1633
+ // A selection tree whose leaves stay the program's own expressions: each is
1634
+ // the last operand of a comma that names it in the site's frame, so a type
1635
+ // checker reading the instrumented copy narrows through `a && a.b` as it does
1636
+ // in the source, and the tree's value is recorded here, once, for the leaf
1637
+ // that produced it. What a leaf before the last decided on the way is known
1638
+ // from where evaluation went next, and is recorded there as plain hits.
1639
+ // Two leaves: the left one is last only when it decided the result.
1640
+ function selectEnd2V2(file, first, value, right) {
1641
+ coverageHitV2(file, first + (right ? 2 : 0) + (value ? 1 : 0));
1642
+ return value;
1643
+ }
1644
+ // More: the leaf's steps, the ones selectPathV2 takes as arguments, come from
1645
+ // the file's table, whose entry for a tree is `[steps of each leaf, for each
1646
+ // leaf the points each leaf before it has decided by then]`.
1647
+ function selectEndV2(file, tree, value, leaf) {
1648
+ const steps = file.selectionSteps[tree][leaf];
1649
+ for (let index = 0; index < steps.length; index += 1) {
1650
+ const step = steps[index];
1651
+ const code = step & 3;
1652
+ const first = (step - code) / 4;
1653
+ if (code === 3)
1654
+ coverageHitV2(file, value ? first + 3 : first + 2);
1655
+ else if (code === 0 ? value : code === 1 ? !value : value !== null && value !== void 0)
1656
+ coverageHitV2(file, value ? first + 1 : first);
1657
+ else
1658
+ break;
1659
+ }
1660
+ return value;
1661
+ }
1662
+ // Where one of several leaves can come before a leaf, the frame says which.
1663
+ // Written out in the program as a choice, it would be a branch of the user's
1664
+ // line to a coverage tool the tests run.
1665
+ function selectNextV2(file, tree, from, to) {
1666
+ const points = file.selectionChoices[tree][to][from];
1667
+ if (points)
1668
+ for (let index = 0; index < points.length; index += 1)
1669
+ coverageHitV2(file, points[index]);
1670
+ }
1629
1671
  // `x ||= y`, `x &&= y`, `x ??= y` keep their operator and their single
1630
1672
  // evaluation of the target: the right side goes through selectRightV2 (or the
1631
1673
  // named form, for an anonymous function the assignment would have named), and
@@ -1851,6 +1893,9 @@ const directRuntimeApi = {
1851
1893
  selectNamedRightV2,
1852
1894
  selectAssignEndV2,
1853
1895
  selectPathV2,
1896
+ selectEnd2V2,
1897
+ selectEndV2,
1898
+ selectNextV2,
1854
1899
  takeNodeAssertionPhases,
1855
1900
  tryBegin,
1856
1901
  tryCatch,
@@ -1913,6 +1958,9 @@ export {
1913
1958
  selectNamedRightV2,
1914
1959
  selectAssignEndV2,
1915
1960
  selectPathV2,
1961
+ selectEnd2V2,
1962
+ selectEndV2,
1963
+ selectNextV2,
1916
1964
  takeNodeAssertionPhases,
1917
1965
  tryBegin,
1918
1966
  tryCatch,