Profile
Back to NewsBack
GitHub Trending 27 min
Reader Mode
cenfun/monocart-coverage-reports: A code coverage tool to generate native V8 reports or Istanbul reports.

cenfun/monocart-coverage-reports: A code coverage tool to generate native V8 reports or Istanbul reports.

12 hours ago

Monocart Coverage Reports

npm</a> !license !build install size</a> npm graph</a> !dependencies downloads</a>

🌐 English | 简体中文

A JavaScript code coverage tool to generate native V8 reports or Istanbul reports.
- Collecting V8 Coverage Data with Playwright - Collecting Raw V8 Coverage Data with Puppeteer - Collecting V8 Coverage Data from Node.js - Collecting V8 Coverage Data with CDPClient API - V8 Coverage Data API - Automatic Merging - Manual Merging - Unexpected coverage - Unparsable source - JavaScript heap out of memory - Playwright - c8 - CodeceptJS - VSCode - Jest - Vitest - Node Test Runner - Puppeteer - Cypress - WebdriverIO - Storybook Test Runner - TestCafe - Selenium Webdriver - Mocha - TypeScript - AVA - Codecov - Codacy - Coveralls - Sonar Cloud

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.

  • Install
npm install monocart-coverage-reports
  • API
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

  • CLI
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

For AI-assisted development in a source checkout, start with AGENTS.md. It describes the source layout, development commands, validation, and safety boundaries. .github/copilot-instructions.md provides a self-contained summary of the core rules for GitHub Copilot. These checkout-only guides are not included in the npm package and are separate from the reusable integration skill below.

The reusable Monocart Coverage Reports skill helps AI coding agents collect coverage, configure the CLI, debug sourcemaps and filters, merge raw reports, and enforce CI thresholds. It uses a main SKILL.md with task-specific references loaded on demand, targeting MCR 3.x only and checking its runtime requirements.

The npm package includes skills/monocart-coverage-reports/. Copy this entire directory, including references/, into your agent's supported skills location, or ask the agent to read SKILL.md directly. Installing MCR does not automatically register the skill or modify agent settings. The instructions are in English; requests and responses can use any language.

From the host project root, locate the installed skill and package manifest (older releases may not include the skill):

node -p "require.resolve('monocart-coverage-reports/skills/monocart-coverage-reports/SKILL.md')"
node -p "require.resolve('monocart-coverage-reports/package.json')"

Read the resolved skill, then follow its relative reference links. Use the installed package's runtime, API, and version requirements rather than assuming all examples apply to older releases. These commands resolve files; they do not import Markdown as JavaScript.

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 | null | Custom sourcemap loader (e.g. from a build cache). Call defaultResolver(url) to fall back. | | onEntry | (entry) => void \| Promise | null | (V8 only) Per-entry hook; see Hooks. | | onEnd | (coverageResults) => void \| Promise | 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
  • v8-json
- Save CoverageResults to a json file (defaults to coverage-report.json). - Shows native V8 code coverage with VSCode extension: Monocart Coverage for VSCode
Istanbul built-in reports (both V8 and Istanbul data):
  • clover
  • cobertura
  • html
- Istanbul html - V8 to Istanbul
  • html-spa
  • json
  • json-summary
  • lcov
  • lcovonly
- V8 lcov.info - Istanbul lcov.info
  • 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).
- Preview in runs
  • 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'
            }]
        ]
    };
- Istanbul custom reporter > example: ./test/custom-istanbul-reporter.js, see istanbul built-in reporters' implementation for reference. - V8 custom reporter > 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: [
        // built-in reports
        ['console-summary'],
        ['v8'],
        ['html', {
            subdir: 'istanbul'
        }],
        ['json', {
            file: 'my-json-file.json'
        }],
        'lcovonly',

// custom reports // Specify reporter name with the NPM package ['custom-reporter-1'], ['custom-reporter-2', { type: 'istanbul', key: 'value' }], // Specify reporter name with local path ['/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

  • Before coverage collection: Instrumenting source code with Istanbul
- webpack with babel loader: babel-plugin-istanbul, see example: webpack.config-istanbul.js - CLI: nyc instrument or API: istanbul-lib-instrument - vite: vite-plugin-istanbul - rollup: rollup-plugin-istanbul - swc: swc-plugin-coverage-instrument
  • Browser
- Collecting coverage data from window.__coverage__, example: test-istanbul.js
  • Node.js
- Collecting coverage data from global.__coverage__
  • CDP
- getIstanbulCoverage() see CDPClient API

Collecting V8 Coverage Data

  • Before coverage collection: Enabling sourcemap for source code
- webpack: devtool: source-map and mode: development, example webpack.config-v8.js - rollup: sourcemap: true and treeshake: false - esbuild: sourcemap: true, treeShaking: false and minify: false - vite: sourcemap: true and minify: false
  • Browser (Chromium-based Only)
- Collecting V8 Coverage Data with Playwright - Collecting Raw V8 Coverage Data with Puppeteer
  • Node.js
- Collecting V8 Coverage Data from Node.js
  • CDP
- Collecting V8 Coverage Data with CDPClient API

Collecting V8 Coverage Data with Playwright

await Promise.all([
    page.coverage.startJSCoverage({
        // reportAnonymousScripts: true,
        resetOnNavigation: false
    }),
    page.coverage.startCSSCoverage({
        // Note, anonymous styles (without sourceURLs) are not supported, alternatively, you can use CDPClient
        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({
        // reportAnonymousScripts: true,
        resetOnNavigation: false,
        // provide raw v8 coverage data
        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() ]);

// to raw V8 script coverage 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: - 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 - 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 - Enabling Node Debugging. - Collecting coverage data with CDP API. - Example: > node --inspect=9229 ./test/test-node-cdp.js - 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 - see Command Line

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 }).

  • CDPClient available APIs
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();
// run your test here
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
});
// both js and css coverage
await client.startCoverage();
// run your test page here
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
});
// both js and css coverage
await client.startCoverage();
// run your test page here
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.
  • dist/main.js
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;
        }
        // else unmatched
        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 = {
    // combined patterns
    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
    }
};
// Alternatively:
// outputFilter: '/src/'
// outputFilter: (entry) => entry.sourcePath.startsWith('src/')

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) => {
        // Remove the virtual prefix
        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.

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';

// MCR coverage options 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'

// or multiple dirs // all: ['./src', './lib'] };

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: {
            // exclude files
            '*/ignored-.js': false,
            '*/.html': false,
            // empty css coverage
            '*/.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) => {
        // entry.source, entry.sourceMap, entry.fake, ...
    }
};

onStart / onReady — CLI-only

Only available when using mcr :
  • 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.
// mcr.config.js
export default {
    onReady: async (coverageReport, nodeV8CoverageDir, subprocess) => {
        // wait for something, or pre-process the raw files
    }
};

Ignoring Uncovered Codes

To ignore codes, use the special comment which starts with v8 ignore :

  • Ignoring all until stop
/ v8 ignore start /
function uncovered() {
}
/ v8 ignore stop /
  • Ignoring the next line or next N lines
/ v8 ignore next /
const os = platform === 'win32' ? 'Windows' : 'Other';

const os = platform === 'win32' ? 'Windows' / v8 ignore next / : 'Other';

// v8 ignore next 3 if (platform === 'linux') { console.log('hello linux'); }

/ c8 ignore start /
function uncovered() {
}
/ c8 ignore stop /

/ node:coverage disable / function uncovered() { } / node:coverage enable /

  • 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);
// clean previous cache before the start of testing
// unless the running environment is new and no cache
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
// generate coverage reports 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.
  • Installing globally
npm i monocart-coverage-reports -g
mcr node ./test/specs/node.test.js -r v8,console-details --lcov
  • Locally in your project
npm i monocart-coverage-reports
npx mcr node ./test/specs/node.test.js -r v8,console-details --lcov
  • CLI Options
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 ):

| 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 | | onStart | (coverageReport) => Promise | null | Runs before the child process is spawned; see Hooks | | onReady | (coverageReport, nodeV8CoverageDir, subprocess) => Promise | 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 / --require : 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
  • 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.
  • Examples
- Mocha - TypeScript - AVA

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 - 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 i

... (README truncated for length)

Chat with me