mrkeyoor.com_
Fri 07 Aug 20:53 UTC
npmTestingupdated 07 Aug 2026

nyc

nyc is the command line front end for Istanbul, the code coverage tool most of the JavaScript world standardised on. You put nyc in front of whatever command runs your tests, as in nyc mocha, and it hooks Node's require so that every source file your tests load gets rewritten on the fly with counters wired into each line, branch, and function. When the process exits, nyc reads the raw counts your tests left in .nyc_output and renders them as a terminal table, an HTML report, an lcov file for Codecov or Coveralls, or any combination. It also follows subprocesses, so a test that spawns a CLI still gets counted, and it reads source maps so coverage on TypeScript or Babel output maps back to the lines you actually wrote. The catch is the mechanism: instrumentation happens through the CommonJS require hook, which is why nyc predates native ES modules and has never fully caught up with them.

Verdict

nyc is a well-understood tool in maintenance mode whose core mechanism, the CommonJS require hook, is the wrong shape for how JavaScript is written now. Keep it on an existing CommonJS suite that depends on subprocess coverage or Istanbul ignore hints; for anything new, start with c8 or whatever coverage your runner already includes.

API stability5/5Flags and .nycrc keys have not moved since 15.1.0 in 2020, and both major bumps since then only raised the supported Node range. A config written six years ago still runs, which is the upside of a project that has stopped changing.
Docs3/5The README is genuinely thorough on the parts people get wrong: the three-step include, exclude, negated-exclude ordering, every config file flavour, watermarks, thresholds, and combining runs. Against that, the complete option list only exists in nyc --help, the linked istanbul.js.org tutorials are years out of date, ES module behaviour is not documented at all, and the CI badge at the top of the README points at the c8 repository's workflow.
Maintenance2/5The last release carrying a feature was 15.1.0 in June 2020. 17.0.0 and 18.0.0 were dependency and Node-floor bumps, there are 188 open issues and 19 open pull requests, and the repository was last pushed in May 2026. Security patching still happens; product work does not.
Ecosystem4/5Around 8.5M weekly downloads and the Istanbul report formats it emits are what Codecov, Coveralls, SonarQube, and most IDE coverage gutters read, so the format outlives the tool. There are maintained @istanbuljs/nyc-config presets for TypeScript and Babel, but new projects overwhelmingly get coverage from their runner instead.

Use it if

  • Your test suite is CommonJS and your runner has no coverage of its own: mocha, tape, AVA, or a plain node script are all cases nyc was built for
  • Your tests spawn child processes, such as a CLI integration suite, and you need coverage from those subprocesses merged into the same report; nyc does this through spawn-wrap and most alternatives do not
  • You need to combine several separate runs into one number, for example unit tests and integration tests invoked by different scripts, using nyc --silent, --no-clean, and a final nyc report
  • You want Istanbul's exact ignore hints, such as /* istanbul ignore next */ and the ignore-class-method config, which V8-based tools do not implement
  • You are on an existing project with a tuned .nycrc, shared @istanbuljs presets, and CI thresholds, and replacing all of it would cost more than the tool does
Skip it if

Setup reality

npm i -D nyc and prefixing your test script gets you a report, and then the defaults start arguing with you. By default nyc only counts files that were require()d during a test, so a file nobody imports is simply absent from the report rather than showing 0 percent, and fixing that means setting all: true, which then instruments things you never wanted. The default exclude list lives in @istanbuljs/schema rather than in your project, and the moment you write your own exclude array you replace those defaults entirely instead of adding to them, which is how test files start appearing in coverage numbers. Include and exclude globs are matched with minimatch against paths relative to the project root, the root being the nearest directory above cwd with a package.json, and any glob you pass on the command line has to be quoted or your shell expands it first. Configuration can live in seven places: .nycrc, .nycrc.json, .nycrc.yaml, .nycrc.yml, nyc.config.js, nyc.config.cjs, nyc.config.mjs, or an nyc stanza in package.json, and the full flag list is only in nyc --help rather than the README. TypeScript and Babel each need a preset, @istanbuljs/nyc-config-typescript or @istanbuljs/nyc-config-babel, pulled in through the extends key, plus a loader such as ts-node registered with --require, and with Babel the --require @babel/register has to go on nyc itself and not on the test runner or --all sees nothing. Three directories appear in your project and all three want gitignoring: .nyc_output for raw counts, coverage for reports, and node_modules/.cache/nyc for instrumented sources, and a stale cache after a toolchain change produces coverage numbers that make no sense until you delete it. Version 18 requires Node 20 or 22 and later.

Patterns

Add coverage to an existing test scriptbasic-usage

npm i -D nyc

// package.json
{
  "scripts": {
    "test": "mocha",
    "coverage": "nyc npm run test"
  }
}

Wrapping an npm script works as long as that script does not already invoke nyc, otherwise you get nested instrumentation and wrong numbers. Flags always go before the command being run: nyc --reporter=lcov mocha, never mocha with nyc flags after it.

Move settings out of the command lineconfig-file

// .nycrc
{
  "all": true,
  "include": ["src/**/*.js"],
  "exclude": ["**/*.spec.js", "**/fixtures/**"],
  "reporter": ["text-summary", "lcov"],
  "report-dir": "./coverage",
  "skip-full": true
}

Seven file names are accepted, including .nycrc.yaml and nyc.config.js, plus an nyc key in package.json. Writing your own exclude array replaces the defaults from @istanbuljs/schema rather than extending them, so re-add anything you still want excluded such as test directories.

Stop untested files from vanishing from the reportcover-untested-files

nyc --all --include 'src/**/*.js' npm test

Without --all, nyc only reports on files that were actually require()d, so a module with no tests at all is missing from the report instead of showing 0 percent, and your coverage number looks better than it is. Turning it on usually means tightening include at the same time.

Fail CI when coverage dropsenforce-thresholds

// .nycrc
{
  "check-coverage": true,
  "branches": 80,
  "lines": 90,
  "functions": 85,
  "statements": 90,
  "per-file": false
}

check-coverage must be true or the thresholds are decoration. per-file switches from an aggregate check to failing on any single file below the bar, which on a large codebase turns one red build into dozens. You can also run nyc check-coverage as a separate step against an existing .nyc_output.

Get source-mapped coverage on TypeScripttypescript-setup

npm i -D @istanbuljs/nyc-config-typescript source-map-support ts-node

// .nycrc
{
  "extends": "@istanbuljs/nyc-config-typescript",
  "all": true,
  "check-coverage": true
}

# then
nyc mocha -r ts-node/register 'test/**/*.spec.ts'

The preset sets the extensions, source map handling, and exclusions you would otherwise get wrong by hand. It only helps if the TypeScript is compiled through a require hook such as ts-node; if you run compiled ESM output directly, nyc's hook never fires and you get an empty report.

Emit different reports for humans and for CIchoose-reporters

nyc --reporter=text-summary --reporter=lcov --reporter=json mocha

# reports land in ./coverage
#   coverage/lcov.info      -> Codecov, Coveralls, SonarQube
#   coverage/index.html     -> browsable HTML
#   coverage/coverage-final.json

--reporter is repeatable and the default is just text. lcov writes both lcov.info and the HTML report. Any npm package whose name matches a reporter can be used, so a custom reporter is a devDependency plus its name in the array.

One coverage number from several test commandscombine-multiple-runs

{
  "scripts": {
    "cover": "npm run cover:unit && npm run cover:integration && npm run cover:report",
    "cover:unit": "nyc --silent npm run test:unit",
    "cover:integration": "nyc --silent --no-clean npm run test:integration",
    "cover:report": "nyc report --reporter=lcov --reporter=text"
  }
}

--silent collects without reporting, and --no-clean stops the second run from wiping .nyc_output. Forget --no-clean and your final report silently covers only the last run. nyc merge is a different thing: it flattens .nyc_output into one raw JSON file for an external tool, it does not produce a report.

Exclude code you deliberately do not testignore-code

/* istanbul ignore file */        // whole file, must be at the top

/* istanbul ignore next */
function debugOnly () { /* ... */ }

/* istanbul ignore else */
if (typeof window === 'undefined') { /* ... */ }

// .nycrc
{ "ignore-class-method": ["render"] }

These are Istanbul's own comments and V8-based tools such as c8 do not honour them, so they are one of the real reasons to stay on nyc. ignore next skips the entire next construct, which on a large class is more than people expect.

Count code that runs in a spawned processsubprocess-coverage

nyc --require ts-node/register mocha test/cli.spec.js

# child processes are wrapped automatically;
# to see what is happening when they are not:
nyc --show-process-tree mocha

This is nyc's strongest remaining advantage: spawn-wrap injects the instrumentation into child processes so a CLI integration test still reports coverage. It also causes the most platform-specific breakage, particularly on Windows and with processes that replace argv or re-exec themselves.

What to do when the report comes back emptyesm-limitation

# nyc with native ESM: instrumentation never runs
nyc mocha test/*.mjs        # -> 0% or missing files

# c8 reads V8 coverage instead, same reporters
npm i -D c8
c8 --reporter=lcov --reporter=text mocha test/*.mjs

An empty or wildly low report on an ESM project is not a config mistake, it is the require hook not being involved at all. c8 accepts most of the same flags and writes the same Istanbul reports, so the migration is usually one word in a package.json script.

Know which directories nyc createscache-and-artifacts

# .gitignore
.nyc_output/
coverage/

# clear a stale instrumentation cache
nyc --cache false npm test
rm -rf node_modules/.cache/nyc

Instrumented sources are cached in node_modules/.cache/nyc, and after changing a Babel or TypeScript config that cache can keep serving code instrumented under the old settings, which produces numbers that do not match the source. Deleting it is the first thing to try when coverage looks impossible.

Report on code you instrumented at build timepre-instrumented-code

nyc instrument src/ instrumented/

# then, when reporting:
nyc --exclude-after-remap false report --reporter=lcov

With babel-plugin-istanbul or nyc instrument doing the work up front, nyc's own hook is not involved. Leaving exclude-after-remap at its default drops every file whose source map points back into an excluded folder, which is why pre-instrumented setups often report on nothing at all.

Alternatives

PackageRegistryPick it when
c8npmYou want the same Istanbul reports without instrumentation: it reads V8's native coverage, so ES modules, TypeScript, and workers all work with far fewer dependencies
@vitest/coverage-v8npmYour tests already run on Vitest and you want coverage configured in the same file as everything else
babel-plugin-istanbulnpmYou need Istanbul-quality instrumentation applied at build time, typically for browser or bundled code where a Node require hook never runs
jestnpmYou are choosing a runner rather than a coverage tool, since jest --coverage uses the same Istanbul reporters with nothing extra to install