monocart-coverage-reports 2.13.2 → 3.0.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.
Files changed (68) hide show
  1. package/README.md +267 -114
  2. package/README.zh-Hans.md +268 -115
  3. package/lib/assets.js +6 -6
  4. package/lib/cli.js +191 -95
  5. package/lib/client/cdp-client.js +74 -43
  6. package/lib/client/coverage-client.js +24 -8
  7. package/lib/client/ws-session.js +115 -37
  8. package/lib/converter/ast-visitor.js +5 -7
  9. package/lib/converter/ast.js +6 -6
  10. package/lib/converter/collect-source-maps.js +9 -9
  11. package/lib/converter/converter.js +41 -115
  12. package/lib/converter/fake-source-coverage.js +64 -0
  13. package/lib/converter/find-original-range.js +3 -3
  14. package/lib/converter/flatten-source-maps.js +2 -4
  15. package/lib/converter/ignore.js +2 -4
  16. package/lib/converter/info-branch.js +5 -3
  17. package/lib/converter/info-function.js +5 -3
  18. package/lib/converter/info-statement.js +5 -3
  19. package/lib/converter/range-index.js +78 -0
  20. package/lib/converter/source-range-merge.js +103 -0
  21. package/lib/converter/untested.js +27 -55
  22. package/lib/default/options.js +13 -1
  23. package/lib/generate.js +44 -26
  24. package/lib/index.d.ts +52 -4
  25. package/lib/index.js +35 -41
  26. package/lib/istanbul/istanbul-summary.js +2 -2
  27. package/lib/istanbul/istanbul.js +35 -13
  28. package/lib/packages/chunks/chunk-RVOSNMVU.js +2 -0
  29. package/lib/packages/monocart-coverage-assets.js +4 -3
  30. package/lib/packages/monocart-coverage-converter.js +27 -0
  31. package/lib/packages/monocart-coverage-vendor.js +2 -41
  32. package/lib/packages/monocart-coverage-vendor.js.LICENSE +3 -221
  33. package/lib/platform/concurrency.js +47 -43
  34. package/lib/platform/share.js +1 -1
  35. package/lib/register/hooks.js +19 -13
  36. package/lib/register/register.js +5 -3
  37. package/lib/reports/codacy.js +3 -5
  38. package/lib/reports/codecov.js +3 -5
  39. package/lib/reports/console-details.js +5 -5
  40. package/lib/reports/console-summary.js +4 -4
  41. package/lib/reports/custom.js +8 -24
  42. package/lib/reports/markdown-details.js +4 -6
  43. package/lib/reports/markdown-summary.js +4 -6
  44. package/lib/reports/raw.js +10 -12
  45. package/lib/reports/v8-json.js +3 -5
  46. package/lib/reports/v8.js +3 -5
  47. package/lib/utils/dedupe.js +1 -1
  48. package/lib/utils/gc.js +30 -14
  49. package/lib/utils/markdown.js +2 -2
  50. package/lib/utils/merge/merge.js +2 -4
  51. package/lib/utils/merge/range-tree.js +1 -1
  52. package/lib/utils/request.js +85 -73
  53. package/lib/utils/snapshot.js +4 -4
  54. package/lib/utils/source-path.js +5 -5
  55. package/lib/utils/util.js +61 -41
  56. package/lib/v8/v8-summary.js +2 -4
  57. package/lib/v8/v8.js +11 -11
  58. package/migrate-to-3.md +175 -0
  59. package/package.json +153 -149
  60. package/skills/monocart-coverage-reports/SKILL.md +106 -0
  61. package/skills/monocart-coverage-reports/references/cli.md +108 -0
  62. package/skills/monocart-coverage-reports/references/collection.md +152 -0
  63. package/skills/monocart-coverage-reports/references/filtering-and-sourcemaps.md +170 -0
  64. package/skills/monocart-coverage-reports/references/reports-and-ci.md +219 -0
  65. package/skills/monocart-coverage-reports/references/repository-map.md +55 -0
  66. package/lib/index.mjs +0 -9
  67. package/lib/register/hooks.mjs +0 -3
  68. package/lib/register/register.mjs +0 -3
@@ -0,0 +1,108 @@
1
+ # CLI and configuration
2
+
3
+ These recipes support MCR 3.x only (Node.js >= 22.15.0). Verify the installed major version and runtime before using them.
4
+
5
+ ## Executable and argument boundaries
6
+
7
+ ```sh
8
+ npx mcr node ./app.js -r v8,console-summary
9
+ npx mcr -c mcr.config.mjs -- npm run test:unit
10
+ npx mcr -c mcr.config.mjs -- node "./path with spaces/app.js"
11
+ ```
12
+
13
+ The separator keeps child flags distinct from MCR flags. In 3.x the child executes through Execa without explicitly enabling shell mode. Do **not** pass a whole command as one string (`mcr "node app.js"`), expect MCR to expand environment variables/globs, or embed `&&`, pipes, and redirections inside a child command string. Move composite steps into an existing npm script or a dedicated script. The outer terminal shell still has its own expansion/redirection rules.
14
+
15
+ Useful MCR options include `-c/--config`, `-r/--reports`, `-o/--outputDir`, `-i/--inputDir`, `-b/--baseDir`, `-a/--all`, `--entryFilter`, `--sourceFilter`, `--outputFilter` (3.x), `--filter`, `--inline`, `--lcov`, `--import`, `--require`, and `--env [path]`. Use a JS config for functions/complex options rather than inventing flags. `--env` without a path loads `.env` on the current runtime; avoid logging secrets.
16
+
17
+ ## Configuration
18
+
19
+ ```js
20
+ // mcr.config.mjs
21
+ export default {
22
+ name: 'Application Coverage',
23
+ outputDir: './coverage-reports',
24
+ reports: ['v8', 'console-summary', 'lcovonly'],
25
+ entryFilter: { '**/node_modules/**': false, '**/src/**': true },
26
+ sourceFilter: { '**/node_modules/**': false, '**/src/**': true }
27
+ };
28
+ ```
29
+
30
+ Choose the filtering approach using the README's Approach trade-offs: input filtering for early exclusion and better performance, or 3.x `outputFilter` for simpler final-path selection at the cost of processing unwanted inputs first. This config chooses input filtering for directly executed `src/` modules. For bundles, change `entryFilter` to allow the actual bundle URLs, then use `sourceFilter` for their unpacked sources. `sourceFilter` alone does not exclude unmapped collected entries. Do not stack both strategies by default; see [filtering-and-sourcemaps.md](filtering-and-sourcemaps.md) for both alternatives.
31
+
32
+ A CommonJS config can remain `mcr.config.cjs` with `module.exports = { ... }`, even though MCR's own API is ESM. `.js` follows the host's `package.json` module type. Do not add `"type": "module"` to an entire host project merely for one config file.
33
+
34
+ Default discovery walks upward from cwd and checks `mcr.config.js`, `.cjs`, `.mjs`, `.json`, then `.ts`. Prefer explicit `-c` when a monorepo could find an unintended parent config. TypeScript configs need an appropriate loader, for example when the host already has `tsx`:
35
+
36
+ ```sh
37
+ npx mcr --import tsx -c mcr.config.ts -- node ./app.js
38
+ ```
39
+
40
+ API loading is explicit:
41
+
42
+ ```js
43
+ import { CoverageReport } from 'monocart-coverage-reports';
44
+ const report = new CoverageReport();
45
+ await report.loadConfig('./mcr.config.mjs');
46
+ await report.addFromDir('./coverage-data');
47
+ const results = await report.generate();
48
+ if (!results) throw new Error('No coverage generated');
49
+ ```
50
+
51
+ Constructor/CLI options override config. In 3.x, an explicit missing config or a discovered but broken config throws/exits nonzero; no `strict` switch is needed. No default config found is allowed. Await and propagate errors rather than generating a misleading default report.
52
+
53
+ ## Built-in Node test runner (3.x)
54
+
55
+ For simple code-validation tasks, write a test with `node:test` and `node:assert/strict`, install `monocart-coverage-reports`, then run `npx mcr test <test-file> -r v8,console-summary`. It runs the tests and automatically generates an interactive V8 report plus a terminal summary without requiring another test framework or a custom coverage collector. See the [complete quick start](../SKILL.md#quick-start-test-your-code-and-get-coverage-3x) for a module and its test. The two reports are an explicitly selected recommendation, not MCR's implicit report default.
56
+
57
+ ```sh
58
+ npx mcr test "./test/**/*.test.js" -r v8,console-summary
59
+ npx mcr test "./test/unit/**/*.test.js" -c mcr.config.mjs -- --test-reporter=spec
60
+ ```
61
+
62
+ Or put targets in config:
63
+
64
+ ```js
65
+ export default {
66
+ test: ['./test/unit/**/*.test.js', './test/integration/**/*.test.js'],
67
+ reports: ['v8', 'console-summary'],
68
+ outputDir: './coverage-reports'
69
+ };
70
+ ```
71
+
72
+ - `mcr test` invokes Node's `--test`; it is not a replacement entry for Jest/Mocha/Playwright tests.
73
+ - CLI positional targets **replace** config `test`. Arrays supply separate arguments; a string is one target argument, not a shell command.
74
+ - Quote globs for Node to interpret; options after `--` are forwarded to the Node test runner.
75
+ - With neither CLI targets nor config `test`, it **skips tests and generates no report**. Do not treat such a run as validated coverage.
76
+ - `test` is CLI-only and ignored by ordinary commands/API. Existing runners remain supported through `mcr npm run ...`.
77
+ - Test failure still attempts report generation and retains a nonzero exit status. A report-generation failure must fail CI too. Validate both paths.
78
+
79
+ ## Hooks and shutdown
80
+
81
+ | Hook | Scope / timing |
82
+ | --- | --- |
83
+ | `onStart(report)` | CLI child execution, before spawn |
84
+ | `onReady(report, nodeV8CoverageDir, subprocess)` | CLI, after child exits, before reading inputs |
85
+ | `onEntry(entry)` | V8 input processing; can mutate entry |
86
+ | `onEnd(results)` | After report generation; suitable for thresholds |
87
+
88
+ Hooks may be async. Use `onReady` for a concrete flush/readiness condition when necessary, not an arbitrary permanent delay. API integrations manage their own process lifecycle; they do not receive CLI-only hooks automatically. Preserve test/child failures even if cleanup or reporting also fails.
89
+
90
+ ## API use in CommonJS hosts
91
+
92
+ MCR 3.x uses ESM, but a CommonJS host can use dynamic import without converting its entire application:
93
+
94
+ ```js
95
+ // coverage.cjs
96
+ async function main() {
97
+ const { MCR } = await import('monocart-coverage-reports');
98
+ const report = MCR({ outputDir: './coverage-reports', cleanCache: true });
99
+ await report.addFromDir('./coverage-data');
100
+ if (!await report.generate()) throw new Error('No coverage generated');
101
+ }
102
+ main().catch((error) => {
103
+ console.error(error);
104
+ process.exitCode = 1;
105
+ });
106
+ ```
107
+
108
+ Use ESM-aware TypeScript module settings and verify emitted JavaScript. Secondary `/util` and `/converter` entries provide **named exports**, not defaults. These recipes do not cover other major versions; approved upgrades should follow the package's `migrate-to-3.md`.
@@ -0,0 +1,152 @@
1
+ # Coverage collection recipes
2
+
3
+ Examples use ESM (`.mjs`, or `.js` under `"type": "module"`) and the MCR 3.x runtime contract. Reuse installed browser/test tools; do not install every collector. MCR itself is a Node-side library, not a browser import.
4
+
5
+ ## Choose the input
6
+
7
+ | Runtime / existing data | Collection path | MCR input |
8
+ | --- | --- | --- |
9
+ | Node program or test command | `mcr <executable> <args>` | CLI collects and generates |
10
+ | Node raw V8 directory | `NODE_V8_COVERAGE` before process start | `addFromDir(dir)` or `dataDir` |
11
+ | Playwright Chromium | `page.coverage` | JS + CSS entry array |
12
+ | Puppeteer | Raw script coverage + source text | Converted JS + CSS entry array |
13
+ | Chromium/Node inspector | `CDPClient` | `stopCoverage()` / `stopJSCoverage()` result |
14
+ | Already Istanbul-instrumented program | `window.__coverage__` / `globalThis.__coverage__` | Istanbul map object |
15
+
16
+ Do not assume Chromium V8 coverage works in Firefox/WebKit or that it automatically covers every browser context, page, worker, or remote backend. Explicitly collect the runtimes/targets the task requires.
17
+
18
+ ## Playwright Chromium: complete standalone script
19
+
20
+ Assumes the application's test server is already running at `APP_URL` (or the local default). Prefer the host's existing server lifecycle and readiness checks.
21
+
22
+ ```js
23
+ // collect-browser.mjs
24
+ import { chromium } from 'playwright';
25
+ import { MCR } from 'monocart-coverage-reports';
26
+
27
+ const report = MCR({
28
+ outputDir: './coverage-reports/browser',
29
+ reports: ['v8', 'console-summary'],
30
+ cleanCache: true
31
+ });
32
+ const browser = await chromium.launch();
33
+ try {
34
+ const page = await browser.newPage();
35
+ await Promise.all([
36
+ page.coverage.startJSCoverage({ resetOnNavigation: false }),
37
+ page.coverage.startCSSCoverage({ resetOnNavigation: false })
38
+ ]);
39
+ await page.goto(process.env.APP_URL || 'http://localhost:3000');
40
+ // Perform real assertions/interactions here before stopping coverage.
41
+ const [js, css] = await Promise.all([
42
+ page.coverage.stopJSCoverage(),
43
+ page.coverage.stopCSSCoverage()
44
+ ]);
45
+ await report.add([...js, ...css]);
46
+ const results = await report.generate();
47
+ if (!results) throw new Error('No browser coverage generated');
48
+ } finally {
49
+ await browser.close();
50
+ }
51
+ ```
52
+
53
+ For test suites, start/stop in the host runner's fixtures/hooks, persist each test's data, and generate **once after all workers finish**. Do not create a `cleanCache: true` report per test against a shared directory. Collect before teardown/navigation destroys the execution target. On failure, retain coverage where possible without replacing the original test failure with a successful exit.
54
+
55
+ Anonymous scripts require deliberate URL/source handling; Playwright CSS coverage does not support anonymous styles without source URLs. Use CDP when that limitation matters.
56
+
57
+ ## Puppeteer: convert the JS data shape
58
+
59
+ Within an existing Puppeteer `page` lifecycle:
60
+
61
+ ```js
62
+ await Promise.all([
63
+ page.coverage.startJSCoverage({
64
+ resetOnNavigation: false,
65
+ includeRawScriptCoverage: true
66
+ }),
67
+ page.coverage.startCSSCoverage({ resetOnNavigation: false })
68
+ ]);
69
+ await page.goto(appUrl); // appUrl is the host application's test URL
70
+ // Exercise and assert the application here.
71
+ const [js, css] = await Promise.all([
72
+ page.coverage.stopJSCoverage(),
73
+ page.coverage.stopCSSCoverage()
74
+ ]);
75
+ const entries = [
76
+ ...js.map((entry) => ({
77
+ ...entry.rawScriptCoverage,
78
+ source: entry.text
79
+ })),
80
+ ...css
81
+ ];
82
+ await report.add(entries); // report is an MCR instance; generate after collection
83
+ ```
84
+
85
+ Do not pass Puppeteer's simplified JS coverage directly as raw V8 function coverage. Preserve `rawScriptCoverage.functions` and the exact matching source text. Close the browser in `finally` after coverage collection.
86
+
87
+ ## CDPClient
88
+
89
+ For Playwright use `await page.context().newCDPSession(page)`; for Puppeteer use `await page.target().createCDPSession()`. Pass the resulting session to MCR:
90
+
91
+ ```js
92
+ import { CDPClient } from 'monocart-coverage-reports';
93
+
94
+ const client = await CDPClient({ session });
95
+ if (!client) throw new Error('Cannot connect coverage client');
96
+ try {
97
+ await client.startCoverage(); // both JS and CSS on a browser target
98
+ // Navigate/exercise the target here.
99
+ const entries = await client.stopCoverage();
100
+ await report.add(entries);
101
+ } finally {
102
+ await client.close();
103
+ }
104
+ ```
105
+
106
+ For a Node debugger target, connect with `{ port: 9229 }` (start the program with `--inspect=9229`) and use `startJSCoverage()` / `stopJSCoverage()`, not CSS methods. Ensure the target remains alive during collection; restrict debugger exposure to trusted/local interfaces.
107
+
108
+ In 3.x, `timeout` controls discovery/connection (default 10000 ms), and `requestTimeout` controls built-in WebSocket commands (default 30000 ms). `0` disables the respective deadline. External Playwright/Puppeteer sessions manage their own command deadlines. Handle disconnects/rejected commands rather than waiting indefinitely.
109
+
110
+ ## Node raw directory
111
+
112
+ The easiest path is the CLI because it also registers source capture. For manual collection, set the environment **before** starting the program. Use a fresh directory per independent run. POSIX shell example:
113
+
114
+ ```sh
115
+ NODE_V8_COVERAGE=./coverage-data node --import monocart-coverage-reports/register ./app.js
116
+ node ./generate-coverage.mjs
117
+ ```
118
+
119
+ In Windows PowerShell, set `$env:NODE_V8_COVERAGE='./coverage-data'` before `node`; remove it afterward with `Remove-Item Env:NODE_V8_COVERAGE`. Or use the host's existing cross-platform environment tooling. Do not add a dependency solely for this example.
120
+
121
+ ```js
122
+ // generate-coverage.mjs
123
+ import { MCR } from 'monocart-coverage-reports';
124
+ const report = MCR({
125
+ outputDir: './coverage-reports/node',
126
+ reports: ['v8', 'lcovonly', 'console-summary'],
127
+ cleanCache: true
128
+ });
129
+ await report.addFromDir('./coverage-data');
130
+ const results = await report.generate();
131
+ if (!results) throw new Error('No Node coverage generated');
132
+ ```
133
+
134
+ Wait for the target/children to finish flushing. Long-lived servers can use `node:v8`'s `takeCoverage()` with `NODE_V8_COVERAGE`, or MCR's `client.writeCoverage()` through a debugger connection. Flushing still requires coordinated shutdown; do not kill the process before writing inputs.
135
+
136
+ A Node coverage JSON file contains a `result` array and may have sourcemap metadata. Prefer `addFromDir()` over manually discarding that metadata. Inspector collection requires fetching/attaching the actual script source. Do not create fake source strings for real application scripts.
137
+
138
+ ## Istanbul input
139
+
140
+ Instrument the tested build with the host's existing Istanbul plugin/tool first. Then collect before the page closes:
141
+
142
+ ```js
143
+ const data = await page.evaluate(() => globalThis.__coverage__);
144
+ if (!data || !Object.keys(data).length) {
145
+ throw new Error('Missing Istanbul coverage: verify instrumentation');
146
+ }
147
+ await report.add(data);
148
+ ```
149
+
150
+ For Node, read `globalThis.__coverage__` in the instrumented process. Use `reports: ['html', 'lcovonly', 'console-summary']` for Istanbul input; native V8 byte/CSS reports cannot be reconstructed from an Istanbul map. MCR does not apply its V8 input filters to Istanbul maps; configure instrumentation/runner scope appropriately.
151
+
152
+ Existing Jest/Vitest coverage should use their compatible MCR integration or raw export path. Do not assume a reporter for one framework can be installed as another framework's reporter, or that an arbitrary JSON report is an MCR raw export.
@@ -0,0 +1,170 @@
1
+ # Filtering, sourcemaps, and untested files
2
+
3
+ ## Choose a filtering strategy
4
+
5
+ For V8 input, choose according to the README's **Approach** comparison, rather than always selecting `outputFilter`. The following comparison is self-contained for package-only/offline use. Both approaches decide which files appear in the report; choose one for the intended scope, not both automatically.
6
+
7
+ | Approach | When and what it filters | Advantages | Trade-offs |
8
+ | --- | --- | --- | --- |
9
+ | `entryFilter` + `sourceFilter` (or `filter`) | Entry URLs during discovery, then source paths as sourcemaps are unpacked | Early exclusion avoids unnecessary downstream work; prefer when performance matters, especially for large projects/bundles | Requires entry/source knowledge; excluding a bundle also removes desired sources inside it |
10
+ | `outputFilter` (3.x only) | Final `sourcePath` after conversion, merging, and adding untested files (`all`) | One filter against the final file list; simpler configuration | Unwanted inputs are still processed first, increasing generation time and memory usage |
11
+
12
+ Use input filtering when performance matters. Choose output filtering when simpler final-file selection is more important and the earlier processing cost is acceptable. If switching strategies, remove old input filters for the same scope: output filtering cannot restore previously excluded inputs.
13
+
14
+ | Option | Receives / matches | Stage |
15
+ | --- | --- | --- |
16
+ | `entryFilter` | Entry object function (`entry.url`), or URL patterns | Before converting bundles |
17
+ | `sourceFilter` | Source path string, or path patterns | Sources unpacked from sourcemaps; also untested files |
18
+ | `filter` | Entry object or source path string | Shorthand for both input filters; not `outputFilter` |
19
+ | `outputFilter` | Final file object function (`entry.sourcePath`), or final path patterns | After sourcemap conversion, merging, and `all` |
20
+
21
+ Example input filtering for **directly executed source modules**, including entries without sourcemaps:
22
+
23
+ ```js
24
+ // input-filter.config.mjs
25
+ export default {
26
+ entryFilter: {
27
+ '**/node_modules/**': false,
28
+ '**/src/**': true
29
+ },
30
+ sourceFilter: {
31
+ '**/node_modules/**': false,
32
+ '**/src/**': true
33
+ }
34
+ };
35
+ ```
36
+
37
+ For a **bundled build**, allow the actual application bundle URLs instead (adapt `dist/` and vendor rules to the tested build):
38
+
39
+ ```js
40
+ // bundle-filter.config.mjs
41
+ export default {
42
+ entryFilter: {
43
+ '**/node_modules/**': false,
44
+ '**/vendor.js': false,
45
+ '**/dist/**': true
46
+ },
47
+ sourceFilter: {
48
+ '**/node_modules/**': false,
49
+ '**/src/**': true
50
+ }
51
+ };
52
+ ```
53
+
54
+ `sourceFilter` does not filter already collected entries without sourcemaps. For directly executed sources, restrict their URLs with `entryFilter`, or use 3.x `outputFilter` to limit the final file list. A permissive `entryFilter` plus `sourceFilter: '**/src/**'` can still include unmapped test/helper scripts.
55
+
56
+ An entry may be `dist/main.js` but contain the desired `src/*` sources. Filtering entries to `**/src/**` would discard that bundle before its sources can be recovered.
57
+
58
+ Alternative final-file selection:
59
+
60
+ ```js
61
+ // output-filter.config.mjs
62
+ export default {
63
+ outputFilter: {
64
+ '**/node_modules/**': false,
65
+ '**/src/**': true
66
+ }
67
+ };
68
+ // Or: outputFilter: (entry) => entry.sourcePath.startsWith('src/')
69
+ ```
70
+
71
+ - Ordered pattern objects use **first match wins**; unmatched files are excluded. Put exclusions before a broad inclusion. Quote CLI patterns to avoid shell expansion.
72
+ - Glob wildcards do not necessarily include dot-prefixed path segments (for example `.temp` or framework build directories). Inspect actual URLs/paths and use an explicit pattern for the hidden segment or a function filter when necessary; do not assume `**/*` includes every generated entry.
73
+ - `sourceFilter` functions receive a string; `outputFilter` functions receive an object. Do not interchange signatures.
74
+ - `outputFilter` matches paths **after `sourcePath` rewriting**, cannot recover entries removed by earlier filters, and does not avoid earlier conversion cost.
75
+ - `outputFilter` affects V8 reports and Istanbul reports converted from V8, but not raw exports or Istanbul input maps.
76
+ - When migrating a scope to `outputFilter`, remove old input filters for that same scope unless their intersection is intentional.
77
+
78
+ ## Build the tested code with usable maps
79
+
80
+ Generate sourcemaps for the exact build that tests execute, preferably with original source content available. Common coverage-build settings:
81
+
82
+ | Build tool | Settings |
83
+ | --- | --- |
84
+ | Vite | `build: { sourcemap: true, minify: false }` |
85
+ | webpack | `devtool: 'source-map'`, `mode: 'development'` |
86
+ | Rollup | `output.sourcemap: true`, `treeshake: false` |
87
+ | esbuild | `sourcemap: true`, `treeShaking: false`, `minify: false` |
88
+
89
+ Change only the intended test/coverage build, not production settings without approval. Minification/transformation can make branch mapping approximate; MCR cannot promise perfect mapping of arbitrary transformed code. Ensure external maps are reachable or attach matching `entry.sourceMap` data; do not substitute maps from a different build.
90
+
91
+ ## Normalize paths for merging
92
+
93
+ Use a shared `baseDir` when collection/generation working directories differ. Use `sourcePath` to remove known build-specific virtual prefixes, preserving unique business paths:
94
+
95
+ ```js
96
+ const options = {
97
+ sourcePath: (filePath) => {
98
+ const prefixes = ['unit-dist/', 'e2e-dist/'];
99
+ const prefix = prefixes.find((value) => filePath.startsWith(value));
100
+ return prefix ? filePath.slice(prefix.length) : filePath;
101
+ }
102
+ };
103
+ ```
104
+
105
+ Only rewrite prefixes actually observed in reports. Do not collapse everything to basename: unrelated `index.ts` files would collide. Normalize the same logical source consistently across server/client/shards, and verify that supposedly identical paths refer to the same source version.
106
+
107
+ ## Include untested files
108
+
109
+ By default, files never loaded are absent, not automatically 0%. Choose the real application directories:
110
+
111
+ ```js
112
+ const options = {
113
+ all: {
114
+ dir: ['./src'],
115
+ filter: {
116
+ '**/*.test.js': false,
117
+ '**/*.spec.js': false,
118
+ '**/*.js': true,
119
+ '**/*.css': 'css'
120
+ }
121
+ },
122
+ sourceFilter: '**/src/**'
123
+ };
124
+ ```
125
+
126
+ `all.filter` runs before `sourceFilter`. With 3.x final filtering, `outputFilter` also applies after untested files are added. Current checkout behavior can generate an all-untested V8 report with no collected entries; treat this separately from unexpected missing collection data.
127
+
128
+ For TS/JSX/Vue sources that the JS AST parser cannot directly parse, use an appropriate transformer already present in the host. Example for **TypeScript without JSX**, if `@swc/core` is installed:
129
+
130
+ ```js
131
+ // untested-typescript.config.mjs
132
+ import { fileURLToPath } from 'node:url';
133
+ import { transform } from '@swc/core';
134
+
135
+ export default {
136
+ all: {
137
+ dir: './src',
138
+ filter: '**/*.ts',
139
+ transformer: async (entry) => {
140
+ const { code, map } = await transform(entry.source, {
141
+ filename: fileURLToPath(entry.url),
142
+ sourceMaps: true,
143
+ isModule: true,
144
+ jsc: { parser: { syntax: 'typescript' } }
145
+ });
146
+ entry.source = code;
147
+ entry.sourceMap = JSON.parse(map);
148
+ }
149
+ }
150
+ };
151
+ ```
152
+
153
+ SWC returns its map as a JSON string; parse it before assigning `entry.sourceMap`. Preserve the full filename so different directories' `index.ts` files retain distinct source paths. Use a consistent `baseDir` when generating or merging.
154
+
155
+ Use the matching JSX/TSX or Vue compiler for those languages; this is not a universal `.vue` transformer. `all.transformer` handles untested sources; `onEntry` is the separate per-collected-V8-entry hook. Preserve sources/maps/offsets consistently when transforming collected data.
156
+
157
+ ## Intentional ignores
158
+
159
+ MCR supports `/* v8 ignore next */`, `/* v8 ignore next 3 */`, and `/* v8 ignore start */` / `/* v8 ignore stop */`. `v8Ignore` defaults to true. Verify that the comments survive the tested build. Do not add ignore comments or exclude difficult files merely to raise coverage; explain and agree on legitimate exceptions.
160
+
161
+ ## Debugging checklist
162
+
163
+ 1. Verify collection started before module import/navigation, and that entries include expected URLs and real source text.
164
+ 2. Temporarily inspect entry URLs and mapped source paths before tightening filters. A wrong stage/pattern is a common cause of empty reports.
165
+ 3. Rebuild without minification; verify maps and source content match the executing bundle.
166
+ 4. Use `{ logging: 'debug', sourceMap: true }` to retain cache/source/map files for investigation. Debug mode also retains generated dist entries in V8 results, so its file list can differ from normal mode.
167
+ 5. Compare generated versus original ranges and inspect a representative covered/uncovered branch. Never fix results by editing coverage counts.
168
+ 6. For large inputs, filter early and reduce unnecessary maps/entries before raising memory limits. The `gc` option is a heap threshold in MB; `NODE_OPTIONS=--max-old-space-size=8192` is a separate Node heap setting. Measure rather than promising either will solve OOM.
169
+
170
+ Debug/raw artifacts and inline HTML can contain source code, URLs, and internal paths. Apply the host's artifact-retention and publication rules; do not upload private sources to public services without permission.
@@ -0,0 +1,219 @@
1
+ # Reports, merging, and CI
2
+
3
+ ## Select outputs
4
+
5
+ | Need | Report | Input support / artifact |
6
+ | --- | --- | --- |
7
+ | Native interactive V8/CSS report | `v8` | V8 only; `index.html` + assets |
8
+ | Native machine-readable V8 results | `v8-json` | V8 only; `coverage-report.json` |
9
+ | Istanbul UI | `html` | Istanbul or converted V8 |
10
+ | Interoperable coverage | `lcovonly` | Istanbul or converted V8; `lcov.info` |
11
+ | XML coverage | `cobertura`, `clover` | Istanbul or converted V8 |
12
+ | Istanbul maps / summary | `json`, `json-summary` | Not native V8 JSON and not MCR raw |
13
+ | Console | `console-summary`, `console-details` | V8 or Istanbul |
14
+ | CI summary | `markdown-summary`, `markdown-details` | `coverage-summary.md`, `coverage-details.md` |
15
+ | Upload data | `codecov`, `codacy` | `codecov.json`, `codacy.json` |
16
+ | Later merging | `raw` | Original inputs and sources, not rendered output |
17
+
18
+ Multiple report syntax is `reports: ['v8', ['html', { subdir: 'istanbul' }], 'lcovonly']`. Keep multiple HTML formats in separate paths. Native V8 report options use `outputFile`; Istanbul JSON/XML options commonly use `file` — check the declaration for the selected reporter rather than assuming one universal option.
19
+
20
+ For a portable single native HTML file, use a report descriptor **inside** the `reports` array:
21
+
22
+ ```js
23
+ // inline.config.mjs
24
+ export default {
25
+ outputDir: './coverage-reports',
26
+ reports: [['v8', { inline: true }]]
27
+ };
28
+ ```
29
+
30
+ Do not use `reports: ['v8', { inline: true }]`: the separate object does not configure the V8 reporter. Otherwise upload the HTML **and its assets**. Do not claim Istanbul-only input can recover V8 bytes or CSS metrics. Custom reporters receive format-specific data; inspect the reporter interface and installed implementation rather than treating `CoverageResults` as a universal custom reporter payload.
31
+
32
+ ## Enforce thresholds explicitly
33
+
34
+ `watermarks: [50, 80]` changes low/medium/high colors, not exit status. A strict `onEnd` implementation for a 3.x config:
35
+
36
+ ```js
37
+ // mcr.config.mjs
38
+ export default {
39
+ outputDir: './coverage-reports',
40
+ reports: ['v8', 'v8-json', 'lcovonly', 'markdown-summary'],
41
+ onEnd(results) {
42
+ if (!results) throw new Error('No coverage results');
43
+ const thresholds = { lines: 80, branches: 70 };
44
+ const errors = [];
45
+ for (const [metric, minimum] of Object.entries(thresholds)) {
46
+ const summary = results.summary?.[metric];
47
+ const pct = summary?.pct;
48
+ if (!summary || summary.total === 0 ||
49
+ typeof pct !== 'number' || !Number.isFinite(pct) || pct < minimum) {
50
+ errors.push(`${metric}: ${pct ?? 'missing'}%, required ${minimum}% (nonempty)`);
51
+ }
52
+ }
53
+ if (errors.length) throw new Error(errors.join('\n'));
54
+ }
55
+ };
56
+ ```
57
+
58
+ This deliberately fails absent/zero-total metrics; adapt that policy explicitly if a project legitimately has no branches. Do not coerce `''` into 0 or silently skip missing metrics. For API workflows, also check `generate()`'s return because no usable input can return early without invoking `onEnd`. For ordinary CLI workflows where missing collection must fail, verify a **fresh, nonempty** result as shown below; built-in `mcr test` already treats a missing generated report as an error on 3.x.
59
+
60
+ Use `process.exitCode = 1` or throw rather than calling `process.exit()` before asynchronous writes/cleanup finish. Preserve the test process's original failing status. Test the threshold-failure path, not just passing coverage.
61
+
62
+ ## Reject stale reports and missing collection in CI
63
+
64
+ An ordinary CLI run can return success without calling `onEnd` when no usable input was collected. In that case `generate()` returns before its normal output cleanup: **`clean: true` and `cleanCache: true` do not guarantee old reports are removed**. Checking only whether an existing report file exists can therefore pass using a previous run's result.
65
+
66
+ Prefer a new, isolated output directory per CI run. If reusing a fixed directory, remove it **before** collecting, then require a newly generated machine-readable report with nonempty intended metrics. For the threshold config above, save these two scripts in the host root:
67
+
68
+ ```js
69
+ // clean-coverage.mjs
70
+ import fs from 'node:fs';
71
+
72
+ // This must be a dedicated report directory, never a source or shared worker directory.
73
+ fs.rmSync('./coverage-reports', { recursive: true, force: true });
74
+ ```
75
+
76
+ ```js
77
+ // verify-coverage.mjs
78
+ import fs from 'node:fs';
79
+
80
+ const results = JSON.parse(fs.readFileSync('./coverage-reports/coverage-report.json', 'utf8'));
81
+ const lines = results.summary?.lines;
82
+ if (!Array.isArray(results.files) || results.files.length === 0 ||
83
+ !lines || !(lines.total > 0) || typeof lines.pct !== 'number' || !Number.isFinite(lines.pct)) {
84
+ throw new Error('Missing or empty coverage from this run');
85
+ }
86
+ ```
87
+
88
+ Run all three steps with failure propagation. For example, in a POSIX shell or npm script (replace `./app.js` with the real target):
89
+
90
+ ```sh
91
+ node ./clean-coverage.mjs && npx mcr -c mcr.config.mjs -- node ./app.js && node ./verify-coverage.mjs
92
+ ```
93
+
94
+ Here `&&` belongs to the **outer shell**, not a command string passed to MCR. In other CI environments, use equivalent sequential steps that stop on failure. The cleanup, config, and verifier must use the same dedicated output path. Never clear shared output while workers are collecting; clean once in the coordinator before starting them. The verifier rejects missing/invalid/empty data; `onEnd` separately enforces the configured thresholds, and the original test/CLI failure must still fail CI.
95
+
96
+ ## Multi-process collection on one filesystem
97
+
98
+ Use shared options with a dedicated `outputDir` and **no `cleanCache: true` on workers**:
99
+
100
+ ```js
101
+ // coverage-options.mjs
102
+ export default {
103
+ outputDir: './coverage-reports/shared',
104
+ reports: ['v8', 'console-summary'],
105
+ cleanCache: false
106
+ };
107
+ ```
108
+
109
+ 1. Coordinator before workers start: `MCR(options).cleanCache()` once.
110
+ 2. Each worker: `await MCR(options).add(itsCoverageData)`; do not generate a final report.
111
+ 3. Coordinator **after every worker completes its writes**: `await MCR(options).generate()`; require a nonempty result.
112
+
113
+ `add()` persists inputs to the output's `.cache`; generation merges them. `clean` governs old output artifacts, not the worker input collection lifecycle. Default non-debug generation removes cache afterward. Do not let an early worker generate or clear shared cache while other workers are still adding data.
114
+
115
+ ## Merge separate jobs/shards with raw exports
116
+
117
+ Each independent job has its own output and exports raw data:
118
+
119
+ ```js
120
+ const options = {
121
+ outputDir: './coverage-reports/shard-1', // unique per job
122
+ reports: [
123
+ ['raw', { outputDir: 'raw' }],
124
+ 'v8',
125
+ 'console-summary'
126
+ ],
127
+ cleanCache: true
128
+ };
129
+ ```
130
+
131
+ The relative raw directory becomes `coverage-reports/shard-1/raw`. Upload/download the **entire** raw directory, including source files. After every required job finishes, merge in a separate output directory:
132
+
133
+ ```js
134
+ // merge-coverage.mjs
135
+ import fs from 'node:fs';
136
+ import path from 'node:path';
137
+ import { MCR } from 'monocart-coverage-reports';
138
+
139
+ const inputDir = [
140
+ './downloaded/shard-1/raw',
141
+ './downloaded/shard-2/raw'
142
+ ];
143
+ // Check all expected shards BEFORE generation can clean old output.
144
+ for (const dir of inputDir) {
145
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
146
+ throw new Error(`Missing coverage shard: ${dir}`);
147
+ }
148
+ const coverageFiles = fs.readdirSync(dir).filter((name) =>
149
+ name.startsWith('coverage-') && name.endsWith('.json'));
150
+ let hasData = false;
151
+ for (const name of coverageFiles) {
152
+ const coverage = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8'));
153
+ if (coverage.type === 'v8' && Array.isArray(coverage.data)) {
154
+ hasData ||= coverage.data.length > 0;
155
+ for (const entry of coverage.data) {
156
+ const sourceFile = path.join(dir, `source-${entry.id}.json`);
157
+ const source = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
158
+ if (source.id !== entry.id || typeof source.source !== 'string') {
159
+ throw new Error(`Invalid raw source: ${sourceFile}`);
160
+ }
161
+ }
162
+ } else if (coverage.type === 'istanbul' && coverage.data &&
163
+ typeof coverage.data === 'object' && !Array.isArray(coverage.data)) {
164
+ hasData ||= Object.keys(coverage.data).length > 0;
165
+ } else {
166
+ throw new Error(`Invalid raw coverage: ${dir}/${name}`);
167
+ }
168
+ }
169
+ if (!hasData) throw new Error(`Empty coverage shard: ${dir}`);
170
+ }
171
+
172
+ const report = MCR({
173
+ inputDir,
174
+ outputDir: './coverage-reports/merged',
175
+ reports: ['v8', 'lcovonly', 'console-summary'],
176
+ cleanCache: true
177
+ });
178
+ const results = await report.generate();
179
+ if (!results || !results.files.length) throw new Error('No merged coverage generated');
180
+ ```
181
+
182
+ CLI merge syntax (does **not** perform the preflight above; prefer the validated script in CI):
183
+
184
+ ```sh
185
+ npx mcr merge --inputDir "./downloaded/shard-1/raw,./downloaded/shard-2/raw" --outputDir ./coverage-reports/merged -r v8,lcovonly,console-summary
186
+ ```
187
+
188
+ - Derive the expected shard list from the CI matrix, not only the directories that happened to download. The script rejects missing/empty shards, malformed coverage, and missing/invalid V8 source files; a successful merge alone does not prove all shards contributed.
189
+ - For independently built artifacts, record and compare source revision/build identifiers and shard IDs before merging; directory checks alone cannot prove the builds match.
190
+ - Keep output separate from inputs; default cleaning must not remove downloaded artifacts. Apply the same path normalization/source revision across jobs.
191
+ - HTML, `v8-json`, LCOV, and `json-summary` are not substitutes for MCR raw input. Do not average percentages.
192
+ - Do not assume arbitrary V8 and Istanbul datasets can be mixed in one run. Prefer consistent raw formats or verify the installed release's mixed-format handling with a targeted fixture.
193
+ - Keep raw artifacts until merge validation passes, then apply the host's retention/security policy. Raw exports are not affected by final `outputFilter`.
194
+
195
+ ## CI checklist
196
+
197
+ - Pin a supported Node runtime and install from the existing lockfile.
198
+ - Build the exact coverage target with maps, run tests, then generate/merge only when inputs are flushed.
199
+ - Upload coverage artifacts on failed tests too, using the CI platform's failure-safe artifact step. Do not mask exit codes with unconditional success or `continue-on-error`.
200
+ - Check source scope and nonempty metrics before gating. Supply `all` when coverage must account for files never imported.
201
+ - Publish `coverage-summary.md` to the job summary using the host CI's supported file append mechanism; upload LCOV/Codecov with explicit file paths.
202
+ - Never include tokens in generated HTML/config examples. Do not publicly publish source-containing reports without approval.
203
+
204
+ ## Regression snapshots
205
+
206
+ The public `getSnapshot(results)` and `diffSnapshot(old, next, options)` helpers compare coverage results, not raw data merges:
207
+
208
+ ```js
209
+ import { getSnapshot, diffSnapshot } from 'monocart-coverage-reports';
210
+ const next = getSnapshot(results);
211
+ const diff = diffSnapshot(previousSnapshot, next, {
212
+ skipEqual: true,
213
+ showSummary: true,
214
+ metrics: ['lines', 'branches']
215
+ });
216
+ if (diff.change) console.log(diff.message);
217
+ ```
218
+
219
+ Here `results` is a validated generation result and `previousSnapshot` is a loaded baseline. Inspect differences; do not overwrite baselines simply to make tests pass. Identical percentages alone do not guarantee identical mapped source ranges.