CLI tool
bcoe/c8 avatar
bcoe/c8

c8's coverage number is optimistically wrong until you add a flag, and its reference link points at Node 10

output coverage reports using Node.js' built in coverage

2,121 stars99 forksJavaScriptISC

At a glance

What is it?
A coverage reporter that reads Node's built-in V8 output instead of instrumenting your code, designed to be a drop-in for the other one. Its two most consequential defaults are both off by default: uncovered files that were never loaded do not count, and the lists deciding which files count at all are maintained in a different repository.
Who is it for?
c8 is the right choice over an instrumenting coverage tool when you want a fast run with no build step, and the compatibility work is real: it reads the other tool's config filenames, its exclude and extension defaults, and its reporter format, so a project switching over needs almost no change. Read two defaults before you trust a number.
Can I use it commercially?
Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.

Editorial analysis

By default the number is wrong, and the flag that fixes it is off

This is the first thing to know about the tool, and it is in a section of the readme rather than in the opening example.

The V8 engine only reports coverage for files it actually loaded. So if your project has source files that are wired up in production but never touched by your tests, they are simply absent from the report. The readme gives the case precisely: if your main file loads two others, and your unit tests only load one of them, your total coverage can show a perfect score for the one file while both of the others are completely uncovered.

A file that is not measured does not drag the percentage down. It is not counted in it either.

The flag that fixes this loads every file in the directories you name as source, defaulting to the working directory, that passes your include and exclude checks, and factors the ones still uncovered into the report at zero percent. The readme is careful to say what that is for: making the numbers reflect reality rather than what the test suite happened to touch.

The sentence introducing the example also contains a typo, describing files that are flexed in production. Which is a fair summary of the section it is in.

So the default report is flattering, and everyone who adopts this tool reads the same section eventually. It is just the wrong section to find it in.

The reference link is Node 10 documentation and the engine range has three holes in it

The readme's opening sentence links to the built-in functionality the tool uses. The target is the distribution documentation for the latest Node 10 release, at an anchor about the coverage directory environment variable.

So the primary reference for how the underlying feature works points at documentation from 2018, in a repository whose current version is twelve major releases ahead and whose engine requirements are expressed as three disjoint ranges: twenty point nineteen or above within the 20 line, twenty-two point twelve or above within the 22 line, or twenty-three and later.

That is not a typo in the requirements, it is a deliberate floor. Each of the three entries is a version where something changed that this tool depends on. It also means the supported set has holes in it rather than being a simple floor, so a project pinned to a version just below one of those numbers is not in the supported range even though a later minor of the same line exists.

For a coverage tool that is a defensible amount of fussiness, since a reporter that misreads the runtime's output is worse than no reporter. The stale link is the cheap half of the problem and the part most likely to send someone to the wrong documentation.

It reads the other tool's config filenames, including in its own repository

The compatibility with the tool it is meant to replace is deliberate and goes deeper than output format.

Configuration can come from three places: command-line flags, a section inside your package file, or a JSON file on disk. The file can be named explicitly with a config flag. If you do not name it, c8 searches for four filenames, walking up the filesystem from the working directory. Two of those four belong to c8. The other two belong to the tool it replaces, in both the bare and the JSON-suffixed spelling.

Which means a project with an existing configuration file for that other tool needs to change nothing. The configuration is picked up, the long-form option names lose their double-dash prefix in file form, and the report comes out in the format the existing tooling already reads.

The proof is in the repository itself. There is a configuration file for the tool c8 replaces sitting in its own root. The project eats its own compatibility dog food, which is the kind of check that costs nothing and catches a lot.

Two of the default file lists are maintained in another repository

The options table has fourteen rows and eleven of them carry a local explanation. The other three are different.

The exclude option and the extension option, which decides which file types appear in a report at all, both default to a list whose link points into the schema repository belonging to the tool c8 replaces. The all option points at a section of the readme instead, and every other row is either a plain default or points at a local section.

So c8's definition of which files are not code, and of which files are code, is not in c8. It is wherever that other repository says it is, at a version this package resolves as a dependency.

That is a defensible decision. There is no reason for two projects to disagree about which files are build output, and keeping one copy removes a whole class of divergence. But it does mean the default behaviour of this tool can change when a dependency is updated, with no change in c8 and no note in its changelog. If you are trying to explain why your coverage percentage moved between two runs on identical code, that is one of the places to look, and it is not obvious from the readme.

The experimental path exists to skip the transformation the default path performs

Behind a flag marked experimental, the tool can use a different library to turn the runtime's output into reports.

The reason given is the interesting part. The alternative library also offers reporters that work directly on the engine's byte-offset-based output, and using them removes a complex transformation step, which the readme says may be less bug prone for some environments. Two of those reporter names are named, one for console output and one for the raw data.

Read that next to the dependency list. The default path goes through a separate package for converting V8 coverage into the Istanbul format, and it is a version-nine major of that converter in the runtime dependencies. So the project ships a code path built on a transformation, and an experimental flag whose stated purpose is to bypass that transformation.

That is not an accusation. Transformers like this are unavoidable in the design, and the alternative being experimental is the honest way to offer it. But the maintainer's own description of the default path as complex and possibly bug prone is the sentence to weigh when a coverage report looks strange.

The alternative is not a free choice: it is an optional peer dependency at a major version, which you install yourself in your project if you want it.

Four ways to say the same threshold, and a subcommand to re-render

The threshold interface is repetitive by design, which is arguably the right call for a flag people copy into scripts.

You can run the suite under a coverage flag and pass a threshold. You can run a check-coverage subcommand afterwards against a report that already exists. You can pass a single flag for a hundred percent across all four dimensions, on either the wrapper or the subcommand, and the readme spells out the long form it is equivalent to so you can see exactly what it sets: lines, functions, branches and statements, each at one hundred. And you can add a per-file flag to apply a threshold to each file rather than to the total.

The subcommand is the part worth internalising. Running the check separately from the run means you can re-render or re-check without executing the test suite again, which matters on a slow suite where the coverage data is already on disk in the directory the tool writes it to.

That directory is worth knowing too. It is where the engine writes its raw coverage data, it defaults to the environment variable the runtime itself uses, and the tool deletes what is in it before the script runs. So if your own project writes runtime coverage to the same place, this tool will clear it first.

It holds its own tests to a hundred percent and lints itself after every run

The package file is a small and tidy thing. The test script runs the suite through the working copy of the tool rather than the published one, with an environment override that stops a TypeScript configuration elsewhere in the tree from interfering, and a ten second timeout. The coverage script is the same command with a check flag added, so the project's own coverage gate runs as part of its test script.

There is a separate snapshot script that sets an environment variable to update every snapshot at once, which is the correct shape for a tool whose reporters produce text output that people assert on. A fix script runs the linter in write mode, and a post-test hook runs it in check mode, so a style error fails the test run rather than waiting for a separate step.

Two more details. The linter is configured to ignore the test fixtures directory, which is where the reporter output is generated, so generated files do not fail the check. And the repository field in the package file is an SSH URL rather than an HTTPS one, which means installing from a git reference pulls over SSH unless you edit it.

The project also ships a hand-written type declaration file at its root alongside a TypeScript compiler in the dev dependencies. For a package whose runtime output is text, that is a reasonable amount of ceremony.

The way to exclude code is a comment, and the second example ends mid-string

There is one escape hatch for the cases a percentage cannot express, and the readme names the case that motivates it: you run your tests on one operating system and have logic that only executes on another.

The mechanism is a special comment, and the first form ignores the next line:

js
const myVariable = 99
/* c8 ignore next */
if (process.platform === 'win32') console.info('hello world')

The second form takes a count, so the comment ignores the next several lines, which is what you want for a block rather than a statement.

The readme is honest about the cost, in the sense that it says outright that sometimes you will want to ignore uncovered portions of your codebase. Every ignore comment is a place where the number stops meaning what it says, and the readme does not pretend otherwise.

The example for the counted form is where the document breaks off. The block opens a conditional, calls an info log, and the string literal is cut partway through the word hello. So the one example you would copy to see the counted form working is the one you cannot copy.

Editorial conclusion

c8 is the right choice over an instrumenting coverage tool when you want a fast run with no build step, and the compatibility work is real: it reads the other tool's config filenames, its exclude and extension defaults, and its reporter format, so a project switching over needs almost no change. Read two defaults before you trust a number. Files that were never loaded are not counted, which makes the default report flattering, and the file lists that decide what counts live in another repository, so a dependency update can move your percentage without a change to your code. Add the full-source flag on the first run you care about, and treat the hundred-percent target the project sets for its own tests as its standard rather than a suggestion for yours.

Frequently asked questions

What does c8 do differently from other coverage tools?

It reports coverage using Node's built-in V8 coverage output rather than instrumenting your source, and it produces reports compatible with the Istanbul reporters. Because of that, coverage is only gathered for files the engine actually loaded unless you pass the full-source flag.

Why is my c8 coverage higher than I expect?

Because the engine only reports files it loaded, so a file that production code reaches but no test touches is absent from the report rather than counted as uncovered. The readme's example has a main file loading two others while the tests load one, and the total still shows a perfect score for the loaded file.

Which Node versions does c8 support?

The engine field is written as three disjoint ranges: twenty point nineteen or above in the 20 line, twenty-two point twelve or above in the 22 line, or twenty-three and later. The readme's link explaining the underlying built-in feature points to the documentation for the latest Node 10 release.

Does c8 read an existing nyc configuration file?

Yes. If no configuration path is given, it searches for four filenames walking up from the working directory, two belonging to c8 and two belonging to the tool it replaces, in both bare and JSON-suffixed form. The repository's own root contains a configuration file for the other tool.

How do I exclude code from c8 coverage?

With a special comment in the source. The single form ignores the next line and the counted form ignores the next several. The readme suggests this for code that only runs on a different operating system than the one running the tests.

What does the experimental monocart option in c8 change?

It switches to a different library for producing reports, and its own reporters can work directly on the engine's byte-offset output, which the readme says removes a complex transformation step and may be less bug prone. It requires an additional optional peer dependency that you install yourself.

Official sources

  1. bcoe/c8 on GitHub
  2. Issues
  3. License: ISC
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/bcoe-c8.svg)](https://hysenlabs.com/projects/bcoe-c8)