Monocart Coverage Reports

🌐 English | 简体中文
A JavaScript code coverage tool to generate native V8 reports or Istanbul reports.
Usage
MCR 3.x requires Node.js >= 22.15.0 for all APIs and CLI commands, including ordinary commands, merge and test.
Upgrading from 2.x: See the 3.x migration guide.
npm install monocart-coverage-reports
import { MCR } from 'monocart-coverage-reports';
const mcr = MCR({
name: 'My Coverage Report - 2024-02-28',
outputDir: './coverage-reports',
reports: ['v8', 'console-details'],
cleanCache: true
});
await mcr.add(coverageData);
await mcr.generate();
Using import and load options from config file
import { CoverageReport } from 'monocart-coverage-reports';
const mcr = new CoverageReport();
await mcr.loadConfig();
Both MCR({...}) and new CoverageReport() create the same instance. MCR() is a shorthand factory function; use new CoverageReport() when you prefer class instantiation or need to call loadConfig() separately.
For more information, see Multiprocessing Support
mcr node my-app.js -r v8,console-details
For more information, see Command Line
Best Practices
For new test setups, prefer native coverage collection without an instrumentation step:
| Target |
Recommended setup |
Responsibility |
| Node.js |
Node.js native test runner + mcr test |
node:test runs assertions; MCR automatically collects native V8 JavaScript coverage and generates reports. |
| Browser |
Playwright Test + MCR |
Playwright runs assertions; automatic fixtures collect Chromium JS/CSS coverage; MCR merges it and generates reports. |
| Browser with integrated test reporting |
Playwright Test + monocart-reporter |
The reporter already integrates MCR; configure its coverage option and submit coverage from fixtures for unified test and coverage reporting. |
For detailed setup instructions and examples, see the best-practices guide.
AI Agent Skill
Reliable AI-assisted development starts with automated testing. Test results validate behavior; coverage highlights testing gaps. With mcr test, AI coding agents can run Node.js tests, collect native V8 coverage, and generate reports without instrumentation. This feedback supports iterative development and CI/CD quality gates, whether an agent assists a developer or works autonomously.
The Monocart Coverage Reports skill guides agents through coverage collection, CLI configuration, sourcemap troubleshooting, report merging, and CI thresholds. It targets MCR 3.x and loads task-specific references on demand.
The skill ships in the npm package. Copy the entire skills/monocart-coverage-reports/ directory, including references/, into your agent’s skills directory, or have the agent read SKILL.md directly. Installation does not automatically register the skill. The instructions are in English; you can use any language with your agent.
Locate the installed skill from your project root:
node -p "require.resolve('monocart-coverage-reports/skills/monocart-coverage-reports/SKILL.md')"
Follow the skill’s reference links and the installed package’s version and runtime requirements. Older releases may not include the skill.
For development of MCR itself, see AGENTS.md.
Options
| Option |
Type |
Default |
Description |
logging |
"off" | "error" | "info" | "debug" |
"info" |
Logging level. Use "debug" to keep raw cache and dump sourcemaps for troubleshooting. See Debug for Coverage and Sourcemap. |
name |
string |
"Coverage Report" |
Report title shown in the UI and summary reports. |
reports |
string | (string | ReportDescription)[] |
"" (auto) |
Reports to generate. Defaults to "v8" for V8 data and "html" for Istanbul data. See Available Reports. |
outputDir |
string |
"./coverage-reports" |
Output directory for all reports and the .cache folder. |
inputDir |
string | string[] |
null |
Input directories of raw coverage data for merging. |
baseDir |
string |
process.cwd() |
Base dir used to normalize relative source paths. Set when the report is generated from a different working directory than the sources. |
dataDir |
string |
null |
Coverage data directory loaded automatically in generate() — alternative to addFromDir(). |
entryFilter |
string | object | function |
null |
(V8 only) Filter V8 entries by url. See Filtering Results. |
sourceFilter |
string | object | function |
null |
(V8 only) Filter sources unpacked from sourcemaps. |
outputFilter |
string | object | function |
null |
(V8 only) Filter final files by sourcePath after sourcemap conversion. See Filtering Results. |
filter |
string | object | function |
null |
Combined filter replacing both entryFilter and sourceFilter. |
sourcePath |
object | function |
null |
Rewrite source paths; see Resolve sourcePath. |
all |
string | string[] | object |
null |
Include untested files as empty coverage. See Adding Empty Coverage. |
outputFile |
string |
"index.html" |
(V8 only) Output [sub dir/]filename for the V8 report. |
inline |
boolean |
false |
(V8 only) Inline all assets into a single HTML file. |
assetsPath |
string |
"./assets" |
(V8 only) Assets directory when inline is false. |
lcov |
boolean |
false |
Also generate lcov.info (equivalent to adding the lcovonly report). |
v8Ignore |
boolean |
true |
(V8 only) Enable/disable comment-based ignoring (v8 ignore …). See Ignoring Uncovered Codes. |
reportPath |
string | () => string |
null |
Override the report entry path (defaults to outputDir/index.html). Useful when multiple reports share an outputDir. |
watermarks |
[number, number] | object |
[50, 80] |
Low/high thresholds (percent). Accepts per-metric object like { bytes:[50,80], lines:[50,80] }. |
clean |
boolean |
true |
Clean previous reports in outputDir before generating (the .cache dir is preserved unless cleanCache is also true). |
cleanCache |
boolean |
false |
Clean the cache dir on start. When both clean and cleanCache are false, cached coverage data from previous runs is preserved and can be reused across processes. |
gc |
number |
null |
Memory threshold in MB; force GC at critical stages when JavaScript heap usage (heapUsed) exceeds it. Useful for very large data sets, see JavaScript heap out of memory. |
sourceMap |
boolean |
false |
Save source/sourcemap files to cache for debugging (requires logging: "debug"). |
sourceMapResolver |
(url, defaultResolver) => Promise<string | object> |
null |
Custom sourcemap loader (e.g. from a build cache). Call defaultResolver(url) to fall back. |
onEntry |
(entry) => void | Promise<void> |
null |
(V8 only) Per-entry hook; see Hooks. |
onEnd |
(coverageResults) => void | Promise<void> |
null |
Hook invoked after report generation; see Hooks. |
Available Reports
V8 built-in reports (V8 data only):
v8
- Features:
- A Brand-New V8 Coverage Report User Interface
- Support for Native Byte Statistics
- Support processing big data with high performance
- Coverage for Any Runtime Code
- CSS Coverage Support
- Better Support for Sourcemap Conversion
- Demos: V8 and more


Istanbul built-in reports (both V8 and Istanbul data):
clover
cobertura
html
html-spa
json
json-summary
lcov
lcovonly
none
teamcity
text
text-lcov
text-summary
Other built-in reports (both V8 and Istanbul data):
-
codecov Save coverage data to a json file with Codecov format (defaults to codecov.json), see example.
-
codacy Save coverage data to a json file with Codacy API format (defaults to codacy.json).
-
console-summary shows coverage summary in the console.

console-details Show coverage details in the console. Like text, but for V8. For Github actions, we can enforce color with env: FORCE_COLOR: true.

markdown-summary Save coverage summary to a markdown file (defaults to coverage-summary.md). For Github actions, we can show the markdown content to a job summary
cat path-to/coverage-summary.md >> $GITHUB_STEP_SUMMARY

-
markdown-details Save coverage details to a markdown file (defaults to coverage-details.md).
-
raw only keep all original data, which can be used for other reports input with inputDir. see Merge Coverage Reports
-
Custom Reporter
const coverageOptions = {
reports: [
[path.resolve('./test/custom-istanbul-reporter.js'), {
type: 'istanbul',
file: 'custom-istanbul-coverage.text'
}],
[path.resolve('./test/custom-v8-reporter.js'), {
type: 'v8',
outputFile: 'custom-v8-coverage.json'
}],
[path.resolve('./test/custom-v8-reporter.mjs'), {
type: 'both'
}]
]
};
example: ./test/custom-istanbul-reporter.js, see istanbul built-in reporters’ implementation for reference.
example: ./test/custom-v8-reporter.js
The type option selects the reporter interface and when it runs:
'istanbul' — uses the Istanbul reporter interface: new Reporter(reportOptions) followed by execute(context). The context provides Istanbul report trees and source lookup. V8 input is converted to Istanbul coverage first.
'v8' (default) — uses new Reporter(reportOptions, globalOptions) followed by await generate(reportData) for V8 input. reportData is the V8 coverage result, with type, summary and files; each file contains V8 ranges and summaries.
'both' — uses the same generate(reportData) interface for either V8 or Istanbul input. Check reportData.type to handle the corresponding result structure. This does not supply both raw formats together; there are no v8Data or istanbulData properties.
Reporters are loaded with native ESM import(). Use export default Reporter for ESM; Node’s built-in interoperability also accepts CommonJS module.exports = Reporter (use .cjs inside an ESM project). Local paths must include the file extension; extensionless paths and directory entries are not resolved. No separate CommonJS loader is provided. Reporter loading and initialization errors reject report generation instead of silently skipping the reporter.
Multiple Reports:
import { MCR } from 'monocart-coverage-reports';
const coverageOptions = {
outputDir: './coverage-reports',
reports: [
['console-summary'],
['v8'],
['html', {
subdir: 'istanbul'
}],
['json', {
file: 'my-json-file.json'
}],
'lcovonly',
['custom-reporter-1'],
['custom-reporter-2', {
type: 'istanbul',
key: 'value'
}],
['/absolute/path/to/custom-reporter.js']
]
};
const mcr = MCR(coverageOptions);
Compare Reports
If the V8 data format is used for Istanbul reports, it will be automatically converted from V8 to Istanbul.
|
Istanbul |
V8 |
V8 to Istanbul |
| Coverage data |
Istanbul (Object) |
V8 (Array) |
V8 (Array) |
| Output |
Istanbul reports |
V8 reports |
Istanbul reports |
| - Bytes |
❌ |
✅ |
❌ |
| - Statements |
✅ |
✅ |
✅ |
| - Branches |
✅ |
✅ |
✅ |
| - Functions |
✅ |
✅ |
✅ |
| - Lines |
✅ |
✅ |
✅ |
| - Execution counts |
✅ |
✅ |
✅ |
| CSS coverage |
❌ |
✅ |
✅ |
| Minified code |
❌ |
✅ |
❌ |
Collecting Istanbul Coverage Data
Collecting V8 Coverage Data
Collecting V8 Coverage Data with Playwright
await Promise.all([
page.coverage.startJSCoverage({
resetOnNavigation: false
}),
page.coverage.startCSSCoverage({
resetOnNavigation: false
})
]);
await page.goto('your page url');
const [jsCoverage, cssCoverage] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage()
]);
const coverageData = [... jsCoverage, ... cssCoverage];
For Playwright Test automatic fixtures, see fixtures.js in the complete playwright-coverage example. The best-practices guide explains the integration.
For more examples, see ./test/test-v8.js, css
Collecting Raw V8 Coverage Data with Puppeteer
Depending on your package manager, Puppeteer may not download a browser during installation. If no browser is available, install one manually:
npx puppeteer browsers install chrome
await Promise.all([
page.coverage.startJSCoverage({
resetOnNavigation: false,
includeRawScriptCoverage: true
}),
page.coverage.startCSSCoverage({
resetOnNavigation: false
})
]);
await page.goto('your page url');
const [jsCoverage, cssCoverage] = await Promise.all([
page.coverage.stopJSCoverage(),
page.coverage.stopCSSCoverage()
]);
const coverageData = [... jsCoverage.map((it) => {
return {
source: it.text,
... it.rawScriptCoverage
};
}), ... cssCoverage];
Example: ./test/test-puppeteer.js
Collecting V8 Coverage Data from Node.js
Possible solutions:
-
NODE_V8_COVERAGE=dir
- Sets Node.js env
NODE_V8_COVERAGE=dir before the program running, the coverage data will be saved to the dir after the program exits gracefully.
- Read the JSON file(s) from the
dir and generate coverage report.
- Example:
cross-env NODE_V8_COVERAGE=.temp/v8-coverage-env node ./test/test-node-env.js && node ./test/generate-report.js
-
V8 API + NODE_V8_COVERAGE
- Writing the coverage started by NODE_V8_COVERAGE to disk on demand with
v8.takeCoverage(), it does not require waiting until the program exits gracefully.
- Example:
cross-env NODE_V8_COVERAGE=.temp/v8-coverage-api node ./test/test-node-api.js
-
Inspector API
- Connecting to the V8 inspector and enable V8 coverage.
- Taking coverage data and adding it to the report.
- Example:
node ./test/test-node-ins.js
- vm Example (scriptOffset):
node ./test/test-node-vm.js
-
CDP API
- Enabling Node Debugging.
- Collecting coverage data with CDP API.
- Example:
node --inspect=9229 ./test/test-node-cdp.js
-
Node Debugging + CDP + NODE_V8_COVERAGE + V8 API
- When the program starts a server, it will not exit on its own, thus requiring a manual invocation of the
v8.takeCoverage() interface to manually collect coverage data. Remote invocation of the v8.takeCoverage() interface can be accomplished through the Runtime.evaluate of the CDP.
- Example for koa web server:
node ./test/test-node-koa.js
-
Child Process + NODE_V8_COVERAGE
Collecting V8 Coverage Data with CDPClient API
timeout controls HTTP discovery and WebSocket connection deadlines (default: 10000ms). requestTimeout controls each CDP command on built-in WebSocket sessions (default: 30000ms). Set either to 0 to disable its deadline. Custom Playwright/Puppeteer sessions manage their own command timeouts. For custom sockets, use new MCR.WSSession(ws, { timeout: 30000 }).
interface CoverageClient {
startJSCoverage: () => Promise<void>;
stopJSCoverage: () => Promise<V8CoverageEntry[]>;
startCSSCoverage: () => Promise<void>;
stopCSSCoverage: () => Promise<V8CoverageEntry[]>;
/** start both js and css coverage */
startCoverage: () => Promise<void>;
/** stop and return both js and css coverage */
stopCoverage: () => Promise<V8CoverageEntry[]>;
/** write the coverage started by NODE_V8_COVERAGE to disk on demand, returns v8 coverage dir */
writeCoverage: () => Promise<string>;
/** get istanbul coverage data */
getIstanbulCoverage: (coverageKey?: string) => Promise<any>;
}
- Work with node debugger port
--inspect=9229 or browser debugging port --remote-debugging-port=9229
import { MCR } from 'monocart-coverage-reports';
const client = await MCR.CDPClient({
port: 9229
});
await client.startJSCoverage();
const coverageData = await client.stopJSCoverage();
import { chromium } from 'playwright';
import { MCR } from 'monocart-coverage-reports';
const browser = await chromium.launch();
const page = await browser.newPage();
const session = await page.context().newCDPSession(page);
const client = await MCR.CDPClient({
session
});
await client.startCoverage();
await page.goto('your page url');
const coverageData = await client.stopCoverage();
import puppeteer from 'puppeteer';
import { MCR } from 'monocart-coverage-reports';
const browser = await puppeteer.launch({});
const page = await browser.newPage();
const session = await page.target().createCDPSession();
const client = await MCR.CDPClient({
session
});
await client.startCoverage();
await page.goto('your page url');
const coverageData = await client.stopCoverage();
import { Builder, Browser } from 'selenium-webdriver';
import { MCR } from 'monocart-coverage-reports';
const driver = await new Builder().forBrowser(Browser.CHROME).build();
const pageCdpConnection = await driver.createCDPConnection('page');
const session = new MCR.WSSession(pageCdpConnection._wsConnection);
const client = await MCR.CDPClient({
session
});
V8 Coverage Data API
// Coverage data for a source range.
export interface CoverageRange {
// JavaScript script source offset for the range start.
startOffset: integer;
// JavaScript script source offset for the range end.
endOffset: integer;
// Collected execution count of the source range.
count: integer;
}
// Coverage data for a JavaScript function.
/**
* @functionName can be an empty string.
* @ranges is always non-empty. The first range is called the "root range".
* @isBlockCoverage indicates if the function has block coverage information.
If this is false, it usually means that the function was never called.
It seems to be equivalent to ranges.length === 1 && ranges[0].count === 0.
*/
export interface FunctionCoverage {
// JavaScript function name.
functionName: string;
// Source ranges inside the function with coverage data.
ranges: CoverageRange[];
// Whether coverage data for this function has block granularity.
isBlockCoverage: boolean;
}
// Coverage data for a JavaScript script.
export interface ScriptCoverage {
// JavaScript script id.
scriptId: Runtime.ScriptId;
// JavaScript script name or url.
url: string;
// Functions contained in the script that has coverage data.
functions: FunctionCoverage[];
}
export type V8CoverageData = ScriptCoverage[];
| JavaScript Runtime |
V8 Coverage |
|
| Chrome (65%) |
✅ |
Chromium-based |
| Safari (18%) |
❌ |
|
| Edge (5%) |
✅ |
Chromium-based |
| Firefox (2%) |
❌ |
|
| Node.js |
✅ |
|
| Deno |
❌ |
issue |
| Bun |
❌ |
issue |
MCR extends each V8 coverage entry with additional fields:
| Field |
Type |
Description |
url |
string |
Script URL or file path |
type |
"js" | "css" |
Entry type |
source |
string |
(JS) Source code text |
sourceMap |
object |
(JS) Parsed sourcemap object |
scriptOffset |
number |
(JS) Byte offset for the script wrapper (e.g. in vm modules). This offset is automatically added to all range start/end values |
distFile |
string |
(JS) The dist bundle file this source was unpacked from |
empty |
boolean |
Marked as true when this entry was added via the all option (no real coverage data), showing 0% coverage |
fake |
boolean |
Marked as true when the source code is not original (e.g. compiled from .ts/.jsx). The AST parser skips coverage extraction for fake sources |
These fields can be read/mutated in onEntry hooks.
Filtering Results
Using entryFilter and sourceFilter to filter the results for V8 report
When V8 coverage data collected, it actually contains the data of all entry files, for example:
- dist/main.js
- dist/vendor.js
- dist/something-else.js
We can use entryFilter to filter the entry files. For example, we should remove vendor.js and something-else.js if they are not in our coverage scope.
When inline or linked sourcemap exists to the entry file, the source files will be extracted from the sourcemap for the entry file, and the entry file will be removed if logging is not debug.
- src/index.js
- src/components/app.js
- node_modules/dependency/dist/dependency.js
We can use sourceFilter to filter the source files. For example, we should remove dependency.js if it is not in our coverage scope.
- src/index.js
- src/components/app.js
For example:
const coverageOptions = {
entryFilter: (entry) => entry.url.indexOf('main.js') !== -1,
sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
};
Or using minimatch pattern:
const coverageOptions = {
entryFilter: '**/main.js',
sourceFilter: '**/src/**'
};
Support multiple patterns:
const coverageOptions = {
entryFilter: {
'**/node_modules/**': false,
'**/vendor.js': false,
'**/src/**': true
},
sourceFilter: {
'**/node_modules/**': false,
'**/**': true
}
};
As CLI args (JSON-like string. Added in: v2.8):
mcr --sourceFilter "{'**/node_modules/**':false,'**/**':true}"
Note, those patterns will be transformed to a function, and the order of the patterns will impact the results:
const coverageOptions = {
entryFilter: (entry) => {
if (minimatch(entry.url, '**/node_modules/**')) {
return false;
}
if (minimatch(entry.url, '**/vendor.js')) {
return false;
}
if (minimatch(entry.url, '**/src/**')) {
return true;
}
return false;
}
};
Using filter instead of entryFilter and sourceFilter
If you don’t want to define both entryFilter and sourceFilter, you can use filter instead. (Added in: v2.8)
const coverageOptions = {
filter: {
'**/node_modules/**': false,
'**/vendor.js': false,
'**/src/**': true,
'**/**': true
}
};
Using outputFilter to filter final files
Version requirement: outputFilter is introduced in v3.0.0. It is not supported in v2.13.x or earlier.
Choose one filtering approach. outputFilter and the existing entryFilter + sourceFilter approach serve the same purpose: deciding which files appear in the report. They are alternatives, not additional filters you need to configure together. The combined filter option is shorthand for entryFilter + sourceFilter; it does not configure outputFilter.
| Approach |
When and what it filters |
Advantages |
Trade-offs |
entryFilter + sourceFilter (or filter) |
Filters inputs during discovery: entryFilter matches entry URLs, and sourceFilter matches source paths as sourcemaps are unpacked. |
Excludes unwanted files earlier, avoiding unnecessary downstream processing. Prefer this approach for better performance, especially with large projects or bundles. |
Requires understanding entry files versus sourcemap sources and configuring both stages. Excluding a bundle via entryFilter also removes any desired sources inside it. |
outputFilter |
Filters final files by sourcePath after sourcemap conversion, merging and adding untested files (all), immediately before report generation. |
Simpler configuration: use one filter against the final file list, without distinguishing entries from unpacked sources. |
Unwanted files are still discovered and processed before being discarded. This does not save their earlier processing cost and can increase generation time and memory usage, especially when many files will ultimately be excluded. |
Recommendation: use input filtering (entryFilter + sourceFilter, or filter) when performance matters. Choose outputFilter when simpler configuration and filtering the final results are more important, and the extra processing cost is acceptable. If switching to outputFilter, remove the old input filters for the same scope: output filtering cannot restore files already excluded during input filtering.
For V8 input, outputFilter filters both V8 reports and converted Istanbul reports. It defaults to null (disabled).
Like entryFilter and sourceFilter, it supports strings and ordered pattern objects (the first matching pattern wins; unmatched files are excluded). Patterns match the final sourcePath, including any sourcePath rewrites. A function receives the final file entry, not a path string.
const coverageOptions = {
outputFilter: {
'**/node_modules/**': false,
'**/src/**': true
}
};
CLI examples (quote patterns to prevent shell expansion):
mcr merge --inputDir ./raw --outputFilter "**/src/**"
mcr merge --inputDir ./raw --outputFilter "{'**/node_modules/**':false,'**/src/**':true}"
outputFilter does not change raw coverage exports and does not apply to Istanbul input.
Resolve sourcePath for the Source Files
If the source file comes from the sourcemap, then its path is a virtual path. Using the sourcePath option to resolve a custom path.
For example, we have tested multiple dist files, which contain some common files. We hope to merge the coverage of the same files, so we need to unify the sourcePath in order to be able to merge the coverage data.
const coverageOptions = {
sourcePath: (filePath) => {
const list = ['my-dist-file1/', 'my-dist-file2/'];
for (const str of list) {
if (filePath.startsWith(str)) {
return filePath.slice(str.length);
}
}
return filePath;
}
};
Built-in report path normalization is best-effort; it cannot fully reconcile every nonstandard, virtual, or foreign-platform path. Use sourcePath to correct these special cases rather than relying on automatic inference. The callback receives the normalized filePath; for V8 entries and sourcemap sources, info.url provides the original URL before report path normalization (including the sourcemap’s sourceRoot, when present). This rewrites report paths, not the filesystem paths used to load missing source content. When sourcesContent is missing, local sources are loaded relative to the external map’s URL (or the generated file for inline maps), with sourceRoot applied as a directory.
It also supports simple key/value replacement:
const coverageOptions = {
sourcePath: {
'my-dist-file1/': '',
'my-dist-file2/': ''
}
};
Normalize the full path of the file:
import path from 'node:path';
const coverageOptions = {
sourcePath: (filePath, info) => {
if (!filePath.includes('/') && info.distFile) {
return `${path.dirname(info.distFile)}/${filePath}`;
}
return filePath;
}
};
Adding Empty Coverage for Untested Files
Untested files are excluded by default. Use all to include them with 0% coverage, even when no coverage data was collected. For the same original source, real V8 coverage takes precedence over empty entries.
const coverageOptions = {
all: './src'
};
The untested files will apply to the sourceFilter. And it also supports additional filter (return the file type for js or css coverage):
const coverageOptions = {
all: {
dir: ['./src'],
filter: {
'**/ignored-*.js': false,
'**/*.html': false,
'**/*.scss': 'css',
'**/*': true
}
}
};
Untested files can be transformed into JavaScript for AST analysis. This example handles TypeScript without JSX and assumes @swc/core is installed. Use an appropriate compiler/parser for JSX/TSX or Vue; this is not a universal transformer. Preserve the full filename to keep same-basename files in different directories distinct, and parse SWC’s JSON map before assigning it.
import { fileURLToPath } from 'node:url';
import { transform } from '@swc/core';
const coverageOptions = {
all: {
dir: ['./src'],
filter: '**/*.ts',
transformer: async (entry) => {
const { code, map } = await transform(entry.source, {
filename: fileURLToPath(entry.url),
sourceMaps: true,
isModule: true,
jsc: {
parser: {
syntax: 'typescript'
}
}
});
entry.source = code;
entry.sourceMap = JSON.parse(map);
}
}
};
Hooks
onEnd — after the report is generated
Typical use case: enforce coverage thresholds. Throw on failure so API callers can handle the error and the CLI exits nonzero, without forcing process.exit() before cleanup finishes.
const getCoverageError = (metric, summary, minimum) => {
if (!summary || !(summary.total > 0)) {
return `${metric}: missing or empty coverage`;
}
const { pct } = summary;
if (typeof pct !== 'number' || !Number.isFinite(pct) || pct < minimum) {
return `${metric}: ${pct}%, required ${minimum}%`;
}
return '';
};
const coverageOptions = {
name: 'My Coverage Report',
outputDir: './coverage-reports',
onEnd: (coverageResults) => {
if (!coverageResults) {
throw new Error('No coverage results');
}
const thresholds = {
bytes: 80,
lines: 60
};
const errors = [];
for (const [metric, minimum] of Object.entries(thresholds)) {
const error = getCoverageError(metric, coverageResults.summary?.[metric], minimum);
if (error) {
errors.push(error);
}
}
if (errors.length) {
throw new Error(errors.join('\n'));
}
}
};
This policy rejects missing/zero-total metrics; adapt it explicitly if a metric is legitimately empty. With no usable input, generate() may return undefined without calling onEnd. API callers must check that result; ordinary CLI workflows should use a fresh output directory and verify a newly generated nonempty report. See the strict CI recipes.
onEntry — before each V8 entry is processed (V8 only)
Use this to mutate the entry (for example, transform non-standard sources into valid ECMAScript before the AST parser runs). See Unparsable source.
const coverageOptions = {
onEntry: async (entry) => {
}
};
onTestEnd — mcr test only
onTestEnd(testResults) runs once after the test runner exits and its JSON results are collected, before publishing the test results cache, onReady, and coverage generation. It is awaited even when assertions fail. Other commands, API calls, and runs skipped because no targets were supplied do not call it. The existing onEnd(coverageResults) is unchanged.
mcr test always collects structured test results using an internal JSON reporter, preserving terminal output and user-specified reporter/destination pairs. onTestEnd is optional; omitting it does not disable result collection. To retain results for later use, enable raw reports or debug logging; normal cache cleanup still applies.
export default {
test: './test/**/*.test.js',
onTestEnd: (testResults) => {
console.log(testResults.summary);
for (const test of testResults.tests) {
console.log(test.name, test.status, test.error);
}
}
};
id: a collection ID generated with the same Util.uid() mechanism as coverage cache data.
framework: 'node:test'.
complete: whether the final runner summary was received, not whether tests passed.
summary: { tests, suites, passed, failed, cancelled, skipped, todo, duration, success }; null if incomplete. Counts come from the runner’s final summary; tests excludes suites. Durations are in milliseconds.
tests: completed test and suite records, including nested tests: { name, type, nesting, testNumber, file?, line?, column?, duration, status, skip?, todo?, error? }. Status is passed, failed, cancelled, skipped, or todo. Errors always include string name and message (defaults: 'Error' and ''). Other own enumerable data properties are copied only when their values are strings, booleans, finite numbers, or null; BigInts become decimal strings. Getters/setters, objects/arrays, functions, symbols, undefined, NaN, and infinities are omitted without recursive traversal. Apart from the two fixed fields, inherited/non-enumerable properties are not collected, so normal stack is omitted and fields inside cause are not flattened. Explicitly enumerable string stack is copied like any other string. Reserved keys __proto__, constructor, and prototype are excluded; the fixed fields cannot be overwritten by extra properties.
Test failures, incomplete/missing/invalid reports, and hook errors keep the command nonzero. An incomplete report is delivered with complete: false; a missing or invalid JSON report cannot be delivered and is logged as a collection error. A nonzero process.exitCode set by onTestEnd cannot be cleared by later coverage hooks. Hook/collection errors do not prevent MCR from attempting coverage generation. If the hook throws, test results are not published or exported, rather than falling back to possibly unredacted data. Test results are independent of coverage, so this hook can run even if coverage generation later fails.
Test results use the same ID and filename mechanism as coverage cache data: outputDir/.cache/test-results-<id>.json, with the same id stored in the JSON. The reporter streams records into .json.tmp with its collection ID already included. A small .meta.tmp completion record verifies the final byte count. Without a hook, MCR reads only this completion record and renames the JSON, without loading or rewriting the full report. With a hook, MCR reads the full result object and publishes its mutations after the hook completes. Temporary output and metadata are removed before coverage generation. Finalized JSON follows the coverage cache lifecycle: normally cleaned after generation, retained in debug mode, or exported by raw (including ZIP). It is not embedded in HTML/coverage JSON reports, and importing raw data does not yet merge test results. Save results yourself if needed; files written into outputDir by this hook may be removed by the subsequent coverage cleanup when clean: true.
Sensitive data: Primitive does not mean non-sensitive: a string token, password, or response body is also copied. Messages, test names, and paths may contain credentials or personal data; large strings can also increase report size. MCR does not redact them automatically. Keep the output directory private and review raw/debug artifacts before uploading them. Mutate results in onTestEnd to sanitize the persisted JSON; this does not redact Node’s terminal output or data already written to temporary files. For example, adapt the following rule to your application’s data:
export default {
onTestEnd: (results) => {
for (const test of results.tests) {
if (test.error) {
for (const [key, value] of Object.entries(test.error)) {
if (typeof value === 'string') {
test.error[key] = value.replace(/(Bearer\s+)\S+/gi, '$1[REDACTED]');
}
}
}
}
}
};
onStart / onReady — CLI-only
Only available when using mcr <command>:
onStart(coverageReport) — runs before the child process is spawned.
onReady(coverageReport, nodeV8CoverageDir, subprocess) — runs after the child exits, before MCR reads coverage from nodeV8CoverageDir. Useful when the child hasn’t finished flushing coverage data and needs to be waited on.
export default {
onReady: async (coverageReport, nodeV8CoverageDir, subprocess) => {
}
};
Ignoring Uncovered Codes
To ignore codes, use the special comment which starts with v8 ignore :
function uncovered() {
}
- Ignoring the next line or next N lines
const os = platform === 'win32' ? 'Windows' : 'Other';
const os = platform === 'win32' ? 'Windows' : 'Other';
if (platform === 'linux') {
console.log('hello linux');
}
function uncovered() {
}
function uncovered() {
}
- To disable comment-based ignoring entirely, set
v8Ignore: false.
Multiprocessing Support
The data will be added to [outputDir]/.cache, After the generation of the report, this data will be removed unless debugging has been enabled or a raw report has been used, see Debug for Coverage and Sourcemap
- Main process, before the start of testing
import { MCR } from 'monocart-coverage-reports';
import coverageOptions from 'path-to/same-options.js';
const mcr = MCR(coverageOptions);
mcr.cleanCache();
- Sub process 1, testing stage 1
import { MCR } from 'monocart-coverage-reports';
import coverageOptions from 'path-to/same-options.js';
const mcr = MCR(coverageOptions);
await mcr.add(coverageData1);
- Sub process 2, testing stage 2
import { MCR } from 'monocart-coverage-reports';
import coverageOptions from 'path-to/same-options.js';
const mcr = MCR(coverageOptions);
await mcr.add(coverageData2);
- Main process, after the completion of testing
import { MCR } from 'monocart-coverage-reports';
import coverageOptions from 'path-to/same-options.js';
const mcr = MCR(coverageOptions);
await mcr.generate();
Command Line
The CLI will run the program as a child process with NODE_V8_COVERAGE=dir until it exits gracefully, and generate the coverage report with the coverage data from the dir.
npm i monocart-coverage-reports -g
mcr node ./test/specs/node.test.js -r v8,console-details --lcov
npm i monocart-coverage-reports
npx mcr node ./test/specs/node.test.js -r v8,console-details --lcov
Commands are executed with Execa’s default options, without enabling shell mode. Pass the executable and its arguments separately, for example mcr node "./my app.js"; do not quote an entire command as a single argument. Argument boundaries are preserved: MCR does not expand environment variables or globs, or interpret pipes, redirections, && or other shell syntax. Move command sequences into a script instead. Use mcr [options] -- executable ...args to keep child options separate from MCR options. Execa handles platform-specific command execution, including escaped batch-file invocation through cmd.exe when needed on Windows.
The options below are only available when using the CLI (mcr <command>):
| Option |
Type |
Default |
Description |
-c, --config |
string |
— |
Custom config file path |
--import |
string |
null |
Preload ESM module via NODE_OPTIONS, e.g. tsx for TypeScript. |
--require |
string |
null |
Deprecated alias for --import; uses ESM loading, not CommonJS resolution |
--env |
string |
null |
Load environment variables from a dotenv file (defaults to .env). |
test (config only) |
string | string[] | null |
null |
Default test file paths or glob patterns accepted by node --test, passed unchanged when mcr test has no positional targets; ignored by other commands and the API. Requires Node.js >= 22.15.0 |
onTestEnd (config only) |
(testResults) => void | Promise<void> |
null |
Only mcr test: runs after test result collection, before onReady and coverage generation; see Hooks |
onStart |
(coverageReport) => Promise<void> |
null |
Runs before the child process is spawned; see Hooks |
onReady |
(coverageReport, nodeV8CoverageDir, subprocess) => Promise<void> |
null |
Runs after the child exits, before MCR reads coverage; see Hooks |
For other options (e.g. --name, --reports, --outputDir, --lcov, etc.), see the Options table — they work the same way in CLI.
- Use
-- to separate sub CLI args
mcr -c mcr.config.js -- sub-cli -c sub-cli.config.js
--import <module> / --require <module>: forward a preload module to the child process via NODE_OPTIONS. Use this for TypeScript/JSX runtimes (tsx, ts-node, etc.) and also when loading a mcr.config.ts. Available on all supported Node.js versions. Local paths are relative to the current working directory and must include the file extension; paths containing spaces are supported. Package names use native ESM resolution from MCR’s module location. Both flags use import() and forward --import to Node; --require is a deprecated alias, not a CommonJS loader. CJS modules work only through Node’s built-in import interoperability.
mcr --import tsx node ./test.ts
--env [path]: load environment variables from a dotenv file before the child starts (defaults to .env). Uses process.loadEnvFile, available on all supported Node.js versions.
mcr --env .env.test node ./test.js
-
test: run Node.js tests and generate coverage reports. See Run Tests with mcr test.
-
merge: merge raw coverage data from inputDir(s) and generate merged reports directly, without running a child process.
mcr merge -r v8,console-details --inputDir ./coverage-reports/unit/raw,./coverage-reports/e2e/raw --outputDir ./coverage-reports/merged
See Merge Coverage Reports.
Config File
In MCR 3.x, loadConfig() throws if an explicitly requested config does not exist or a discovered config fails to load (including JSON parsing, module syntax and startup errors). All CLI commands exit with a non-zero status for these errors. No strict option is needed. If no custom config is requested and no default config is found, execution continues with existing options.
Loading config file by priority:
- Custom config file:
- CLI:
mcr --config <my-config-file-path>
- API:
await mcr.loadConfig("my-config-file-path")
mcr.config.js
mcr.config.cjs
mcr.config.mjs
mcr.config.json - json format
mcr.config.ts (requires preloading the TypeScript execution module, for example with tsx):
mcr --import tsx -c mcr.config.ts node ./test.js
If no custom path is given, MCR walks up from the current working directory to find the default config file (via find-up). Files outside the search path will be ignored.
Run Tests with mcr test
New in MCR 3.x: Run tests with the built-in Node.js test runner and automatically generate coverage reports with one command. Requires Node.js >= 22.15.0.
Install MCR and run your test files directly — no config file is required:
npm install --save-dev monocart-coverage-reports
npx mcr test "./test/**/*.test.js" -r v8,console-summary -o ./coverage-reports
This runs the matching tests, prints a coverage summary in the terminal, and writes the V8 HTML report to ./coverage-reports/index.html. Quote glob patterns so Node.js, rather than your shell, handles matching.
For repeated runs, save the test targets and coverage options in mcr.config.js (ESM; use "type": "module" in package.json, or name the config mcr.config.mjs):
export default {
test: ['./test/unit/**/*.test.js', './test/integration/**/*.test.js'],
reports: ['v8', 'console-summary'],
outputDir: './coverage-reports'
};
# Run the targets from the config
npx mcr test
# Override the configured targets and forward Node.js test runner options after --
npx mcr test "./test/unit/**/*.test.js" -- --test-reporter=spec
# Use an explicit config file
npx mcr test "../tests/**/*.test.js" -c ./mcr.config.js
# Choose reports from the command line
npx mcr test "../tests/**/*.test.js" -r v8,console-summary
# Run multiple test files
npx mcr test ./test/first.test.js ./test/second.test.js
# Filter test names and select a reporter
npx mcr test "../tests/**/*.test.js" -c ./mcr.config.js -- --test-name-pattern="my test" --test-reporter=spec
# Place the test command after an initial separator
npx mcr -c ./mcr.config.js -- test "../tests/**/*.test.js" -- --test-reporter=spec
Positional targets override (rather than append to) the config test option. If no positional targets are given, the config option is used. A config file is not required when targets are supplied on the command line. Existing MCR options such as -c, -r, -o and --lcov can appear before or after targets. Put Node runner options after --; those arguments are forwarded unchanged, not parsed as MCR options or target overrides. Quote globs to prevent shell expansion. In the alternative mcr [options] -- test ... form, put all MCR options before the first --; a second -- separates Node runner options.
Targets are passed unchanged to node --test. MCR does not check their types or filesystem existence, expand directories/globs, resolve or normalize paths, escape special characters, or deduplicate targets. node --test accepts positional test file paths or glob patterns, not a JavaScript configuration object. In MCR config, a string represents one such argument (it is not split on spaces), and a string array represents multiple arguments forwarded in their original order. null is only MCR’s skip default and is never passed to Node. Node handles discovery, matching, interpretation and errors. Relative paths use the current working directory, not the config file directory. A bare directory is not promised to trigger recursive discovery; use a glob such as "../tests/**/*.test.js" for recursive matching, following the installed Node version’s native behavior. TypeScript execution depends on your Node version or a loader such as --import tsx.
test defaults to null; when no positional targets are supplied, null, an omitted value, an empty string or [] skips tests without generating a report. MCR only checks whether targets were supplied; invalid or unmatched targets are left to Node. If no coverage data is produced and no report can be generated, the command fails at report generation. Missing explicit config files and failed config loading also fail the command rather than silently skipping tests. Test failures still produce coverage reports and retain a non-zero exit status; report generation failures also fail the command. Existing coverage options and CLI hooks are supported. Use coverage filters to exclude test files if needed. The test option has no effect on other CLI commands or API calls, including merge and generate().
See Command Line for other CLI options.
Merge Coverage Reports
The following usage scenarios may require merging coverage reports:
- When the code is executed in different environments, like Node.js
server side and browser client side (Next.js for instance). Each environment may generate its own coverage report. Merging them can give a more comprehensive view of the test coverage.
- When the code is subjected to different kinds of testing. For example,
unit tests with Jest might cover certain parts of the code, while end-to-end tests with Playwright might cover other parts. Merging these different coverage reports can provide a holistic view of what code has been tested.
- When tests are run on different machines or containers, each might produce its own coverage report. Merging these can give a complete picture of the test coverage across all machines or shards.
Automatic Merging
- The
MCR will automatically merge all the added coverage data when executing generate(). And it supports adding coverage data asynchronously across processes, see Multiprocessing Support
- For
Next.js, it can actually add coverage data including both server side and client side before executing generate(), see example nextjs-with-playwright
- Using
Codecov, a popular online code coverage service, which supports automatic merging of reports. Please use report codecov, it will generate report file codecov.json. If multiple codecov.json files are generated, upload all these files, they will be automatically merged. see Codecov and merging reports
Manual Merging
If the reports cannot be merged automatically, then here is how to manually merge the reports.
First, using the raw report to export the original coverage data to the specified directory.
Before uploading raw/debug artifacts (3.2.0+): mcr test also exports test-results-<id>.json in raw reports (including ZIP) and retains it in debug caches, even without a hook. Test names, paths and error strings may contain secrets or personal data. Review artifacts and use onTestEnd to redact persisted test results before publication; this does not redact terminal output or temporary files already written. Raw imports merge coverage only, not test results.
- For example, we have
raw coverage data from unit test, which is output to ./coverage-reports/unit/raw. Unit test examples:
const coverageOptions = {
name: 'My Unit Test Coverage Report',
outputDir: './coverage-reports/unit',
reports: [
['raw', {
outputDir: 'raw'
}],
['v8'],
['console-details']
]
};
-
We also have raw coverage data from e2e test, which is output to ./coverage-reports/e2e/raw. E2E test examples:
-
Then create a script merge-coverage.js to generate a merged report with option inputDir.
import fs from 'fs';
import { CoverageReport } from 'monocart-coverage-reports';
const inputDir = [
'./coverage-reports/unit/raw',
'./coverage-reports/e2e/raw'
];
const coverageOptions = {
name: 'My Merged Coverage Report',
inputDir,
outputDir: './coverage-reports/merged',
entryFilter: {
'**/node_modules/**': false,
'**/*': true
},
sourceFilter: {
'**/node_modules/**': false,
'**/src/**': true
},
sourcePath: (filePath, info) => {
return filePath;
},
reports: [
['v8'],
['console-details']
],
onEnd: () => {
}
};
await new CoverageReport(coverageOptions).generate();
- Running script
node path/to/merge-coverage.js after all the tests are completed. All the command scripts are probably like following:
{
"scripts": {
"test:unit": "jest",
"test:e2e": "playwright test",
"merge-coverage": "node path/to/merge-coverage.js",
"test": "npm run test:unit && npm run test:e2e && npm run merge-coverage"
}
}
see example: merge-code-coverage
Common issues
Unexpected coverage
In most cases, it happens when the coverage of the generated code is converted to the coverage of the original code through a sourcemap. In other words, it’s an issue with the sourcemap. Most of the time, we can solve this by setting minify to false in the configuration of build tools. Let’s take a look at an example:
const a = tf ? 'true' : 'false';
^ ^ ^
m1 p m2
In the generated code, there is a position p, and we need to find out its corresponding position in the original code. Unfortunately, there is no matched mapping for the position p. Instead, it has two adjacent ups
README 内容过长,已截取前 64 KB。完整内容请前往 GitHub 仓库查看。
评论