c8
c8 is a Node.js command-line coverage tool that turns V8's built-in execution coverage into Istanbul-compatible reports. Put it before a test command, and it collects coverage from the Node process and children, remaps generated JavaScript through source maps, prints text summaries or writes formats such as HTML and lcov, and can fail CI when line, branch, function, or statement thresholds are missed. It measures executed JavaScript; it is not a test runner.
c8 is the strong default for runner-agnostic Node coverage when native V8 data and Istanbul reports are the right combination. Pin a compatible Node version, enable --all for honest totals, and do not layer it over a runner that already owns coverage without a specific reason.
Use it if
- You run tests or scripts on Node and want coverage without Babel or Istanbul instrumentation inserted into source files
- Your test runner has no coverage integration but can be launched as a child command, including node:test, Mocha, or a custom runner
- You need Istanbul reporters and lcov output for CI services while collecting with native V8 coverage
- You need coverage across spawned Node subprocesses and can keep them inside c8's inherited environment
- Your runtime is older than Node 20.19 or 22.12: c8 12.0.0's published engines require ^20.19.0, ^22.12.0, or Node 23 and newer, despite an outdated README section that still says Node 12
- You already use Vitest or Jest and their integrated coverage provider meets your needs: adding a second wrapper creates competing config, ignore syntax, and report ownership
- You need browser execution coverage: c8 launches Node and reads NODE_V8_COVERAGE data, so Playwright or browser-devtools coverage needs a different collection path
- You expect untouched TypeScript coverage to be automatically perfect: remapping depends on accurate source maps, retained generated files or inline maps, and include/exclude rules aligned with original paths
- You only care about loaded modules and will forget --all: V8 reports executed files by default, so an entirely unimported source file can disappear from the denominator and make coverage look better than it is
Setup reality
Install c8 as a development dependency with npm install --save-dev c8 and invoke it through a package script, npx c8, or your package manager. Version 12.0.0 is a CLI package with bundled types for its programmatic surface, but normal use is the c8 executable. Its npm engine range is ^20.19.0, ^22.12.0, or >=23, which is stricter than the README's stale Node >=12 sentence; CI and local machines must satisfy the package metadata. No native build or credentials are required. c8 creates raw V8 data in a temporary directory and reports in ./coverage by default; both should be ignored by version control. Configuration can live in package.json under c8, in .c8rc, .c8rc.json, .nycrc, or .nycrc.json, and discovery walks upward from the working directory, so a parent config can surprise a nested package. Flags belong before the child command. Quote include and exclude globs so the shell does not expand them first. By default only code loaded by V8 appears. Use all with src and explicit extensions to count untouched files as zero. TypeScript, JSX, and transpiled projects need correct inline or external source maps; exclude-after-remap changes whether filters see generated or original paths. Thresholds do nothing unless check-coverage, check-coverage subcommand, or 100 is enabled. c8 inherits NODE_V8_COVERAGE into Node children, but non-Node workers and detached processes are outside that guarantee. Multiple concurrent runs must use separate temp and report directories. HTML and lcov artifacts are generated files, and stale output can mislead if clean is disabled. The experimental Monocart path also requires a separate monocart-coverage-reports 2.x installation.
Patterns
Cover the built-in Node test runnercover-node-tests
npx c8 node --testc8 options go before node. Options after node belong to the Node test runner. c8 12 requires Node ^20.19, ^22.12, or >=23.
Add a repeatable coverage scriptadd-package-script
{
"scripts": {
"test": "node --test",
"coverage": "c8 --all --reporter=text --reporter=lcov npm test"
},
"devDependencies": {
"c8": "^12.0.0"
}
}Keep the normal test script usable without coverage. The lcov report is written under coverage by default.
Store c8 configuration in package.jsonconfigure-package-json
{
"c8": {
"all": true,
"src": ["src"],
"include": ["src/**/*.js"],
"exclude": ["src/**/*.generated.js"],
"reporter": ["text", "html", "lcov"]
}
}Configuration keys omit leading --. c8 also searches parent directories for rc files, so monorepos should make config ownership explicit.
Include completely untested source filescount-unloaded-files
npx c8 --all --src src --extension .js --include 'src/**/*.js' node --testWithout --all, files never loaded by V8 do not appear and cannot lower the total. Quote globs to stop shell expansion.
Fail the test command below coverage thresholdsenforce-thresholds
npx c8 \
--check-coverage \
--lines 90 \
--branches 85 \
--functions 90 \
--statements 90 \
node --testNumeric thresholds alone do not fail a run. --check-coverage enables enforcement and returns a nonzero exit when a threshold is missed.
Apply thresholds to every fileenforce-per-file
npx c8 --all --check-coverage --per-file --lines 80 --functions 80 node --testPer-file checks catch one neglected module hidden by a well-tested aggregate, but generated and intentionally thin files should be excluded deliberately.
Print a summary and write HTML and lcovgenerate-multiple-reports
npx c8 \
--reporter=text-summary \
--reporter=html \
--reporter=lcov \
--reports-dir coverage \
npm testReporters may be repeated. Add coverage and the raw temp directory to .gitignore rather than committing generated reports.
Report TypeScript through source mapsremap-typescript
// tsconfig.json
{
"compilerOptions": {
"outDir": "dist",
"sourceMap": true,
"inlineSources": true
}
}
// run compiled tests
npx c8 --all --src src --extension .ts --exclude-after-remap node --test dist/**/*.test.jsKeep .map files beside emitted JavaScript or emit inline maps. Verify report paths after remapping before trusting exclusions and thresholds.
Ignore a platform-only branchignore-unreachable-code
function openConfig() {
/* c8 ignore next 3 */
if (process.platform === 'win32') {
return openWindowsConfig()
}
return openPosixConfig()
}The numeric suffix counts following lines. Use ignore comments sparingly and explain why the branch cannot run in the relevant test matrix.
Regenerate reports from collected raw dataregenerate-report
npx c8 report --reporter=text --reporter=html --temp-directory .coverage-tmpRaw V8 files must still exist. If the original run used the default clean behavior or CI removed the temp directory, there is nothing to regenerate.
Separate parallel coverage jobsisolate-concurrent-runs
npx c8 \
--temp-directory .coverage-tmp/unit \
--reports-dir coverage/unit \
node --test test/unit
npx c8 \
--temp-directory .coverage-tmp/integration \
--reports-dir coverage/integration \
node --test test/integrationDo not point concurrent c8 processes at the same cleanable temp directory. Merge results only through a deliberate later reporting step.
Try the experimental native V8 reporter pathuse-monocart-reporter
npm install --save-dev monocart-coverage-reports@2
npx c8 \
--experimental-monocart \
--reporter=v8 \
--reporter=console-details \
node --testThis path is experimental and needs the separate Monocart package. Pin it and validate output before adopting it as a CI contract.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| nyc | npm | Use when an Istanbul instrumentation-first workflow or existing .nycrc setup is already established and V8-native collection is not required |
| @vitest/coverage-v8 | npm | Use with Vitest when coverage should share the runner's config, watch mode, workspace behavior, and UI |
| jest | npm | Use when Jest is already your runner and its built-in coverage command is preferable to wrapping the entire process separately |
| monocart-coverage-reports | npm | Use when you want native V8 byte-offset reports or mixed Node and browser coverage with more specialized reporting controls |