istanbuljs/nyc
github.com/istanbuljs/nyc ↗ · 173 files · 240 KB
nyc Having problems? want to contribute? join our community slack.
Overview
nyc is Istanbul’s JavaScript coverage CLI for developers who want to run existing test commands, collect coverage across subprocesses, and enforce coverage thresholds. It instruments source through Node loading hooks or an offline instrument command, persists per-process coverage, then merges and remaps results into configurable reports. At commit 908620475199fa7b9ea0ea8b21d6d8ad6921e3ae, the package is version 18.0.0 and supports Node 20 or Node 22 and newer.
System map
The npm package contains a reusable NYC class, a CLI coordinator, child bootstrap modules, and four command handlers. Instrumentation, coverage merging, source-map transformation, reporting, and process indexing use separate Istanbul libraries. Runtime boundaries follow parent and child processes, with configuration propagated through environment variables and coverage exchanged through files. Packaging and CI describe an npm CLI distribution with generated self-coverage used during development.
- package.json publishes NYC through index.js and the nyc executable through bin/nyc.js; its files allowlist contains index.js, bin JavaScript, and lib JavaScript.
- bin/nyc.js main() coordinates normal command execution, while report, check-coverage, instrument, and merge are registered as yargs command modules.
- NYC owns configuration, exclusions, extension transforms, cache state, SourceMaps, and ProcessInfo; it dynamically loads the selected instrumenter module.
- Child collection supports node-preload plus process-on-spawn environment propagation, or spawn-wrap with bin/wrap.js calling runMain().
- Coverage persistence uses UUID-named JSON files beneath the configured temporary directory and a processinfo subdirectory; ProcessDB writes an index and renders process trees.
- Instrumentation delegates to istanbul-lib-instrument, while coverage maps, source-map stores, report contexts, and report factories come from distinct Istanbul packages.
- The npm lifecycle runs Standard lint, cleanup, and self-instrumentation before TAP tests, then generates text and lcov self-coverage reports afterward.
- The engine range is Node 20 or >=22; CI specifies Ubuntu Node 20 and 24 plus Windows lts/*, and release-please runs on main only for istanbuljs/nyc.
Feature catalog
The primary interface wraps an existing executable or test runner to collect coverage. Users can control file selection, account for unloaded files, preload transpilers, and choose reports or threshold enforcement. Separate commands instrument source trees, regenerate reports, check stored coverage, and merge raw results. Source maps and documented external uploader workflows connect coverage to transpiled projects and hosted reporting services.
- Normal CLI execution accepts a child command and preserves its arguments; --silent suppresses reporting, and --no-clean retains stored results across runs.
- File selection supports include/exclude patterns, additional extensions, and optional node_modules inclusion; --all adds initial coverage for eligible unloaded files, including an explicit ESM reading branch.
- --require loads additional modules for transpilation or other setup; configurable require and VM hooks support ordinary modules, RequireJS/AMD, and vm.runInContext workloads.
- nyc instrument accepts a file or directory and writes transformed source to an output tree or stdout; options include complete-copy, explicit in-place operation, deletion, and exit-on-error.
- nyc report invokes each configured reporter with watermarks, skipEmpty, skipFull, projectRoot, and terminal width; show-process-tree additionally renders stored process ancestry.
- Coverage checks support lines, functions, branches, and statements globally or per file; a percentage below its threshold sets process.exitCode to 1 and prints a diagnostic.
- nyc merge validates an input directory and writes combined Istanbul coverage to an output file defaulting to coverage.json; its builder defaults exclude-after-remap to false.
- Source-map handling accepts inline maps or referenced .map files and optionally emits instrumented-code maps; Codecov and Coveralls integration docs invoke external uploaders on nyc report output.
Data flow
Arguments and project configuration become an NYC configuration object before child execution. Environment variables carry that configuration and ancestry into child processes, while loading hooks transform eligible source into counter-bearing code. Counters become coverage JSON and process metadata at exit. Report and merge operations read these artifacts, recover source maps, combine counters, and produce reports, merged JSON, or failure status.
- guessCWD() starts from its argument, NYC_CWD, or process.cwd(), searches upward for package.json, then processConfig() separates child arguments and passes nyc arguments to loadNycConfig().
- setupOptions() converts schema property names to dashed CLI names, carries defaults and aliases, treats arrays as string options, and hides options outside their declared command scope.
- The parent serializes argv into NYC_CONFIG and records process.cwd() in NYC_CWD; register-env snapshots selected variables and copies them into spawned environments.
- lib/wrap.js parses NYC_CONFIG, marks child execution, records pid/ppid/parent UUID, consumes an external process ID, and merges NYC_CONFIG_OVERRIDE before constructing NYC.
- Hooked code flows through _handleJs(), shouldInstrument(), extension lookup, caching-transform, and instrumentSync(); transform hashing includes filename metadata and a configuration/version salt.
- coverageFinder() obtains __coverage__; writeCoverageFile() removes excluded entries, attaches available content hashes when caching, serializes coverage, then saves matching process metadata.
- SourceMaps registers maps in memory or writes basename/hash .map cache files; coverage loading uses contentHash to reload maps and records failed map loads as false to avoid repeated reads.
- getCoverageMapFromAllCoverageFiles() reads directory entries with CPU-count concurrency, merges loaded reports, remaps coverage, and optionally filters afterward; report() renders that map and merge writes it as JSON.
Behavior traces
The source supports ordered walkthroughs of command startup, child instrumentation, persistence and reporting, and offline instrumentation. These traces describe execution order and conditional branches rather than observed runs. Threshold checks and reporting differ in order between normal execution and the standalone report command. The --all and merge branches reuse core collection machinery while changing their input and output boundaries.
- Startup: cliWrapper(main) → configUtil()/processConfig() → guessCWD() → setupOptions() → argument separation → loadNycConfig() → final yargs parse; recognized subcommands return from main, while missing commands set exitCode 1 and show help.
- Wrapped execution: main() selects Istanbul or noop → new NYC(argv) → reset() or createTempDirectory() → establishes preload modules or spawn-wrap → optionally addAllFiles() → foregroundChild(childArgs).
- Child instrumentation: lib/wrap.js constructs NYC → wrap() publishes NYC_PROCESS_ID → installs require/VM hooks → registers the exit handler → loads additional spawn-wrap modules; hooked source then passes _handleJs() → _maybeInstrumentSource() → _transform() → instrumentSync().
- Persistence: the alwaysLast signal-exit callback calls writeCoverageFile() → coverageFinder() → exclusion cleanup → optional contentHash attachment → synchronous UUID JSON write → ProcessInfo.saveSync().
- Parent completion: foregroundChild callback combines exit status → removes watchdog artifacts → writeProcessIndex() → maybePurgeSourceMapCache() → optional checkCoverage() → report() unless silent; caught completion errors force a nonzero result.
- Offline instrumentation: command handler validates input/output and deletion conditions → selects instrumenter → adjusts in-place settings → preloads modules → instrumentAllFiles(); directory mode globs eligible files, optionally copies the remaining tree, then transforms and writes files with original modes.
- --all: addAllFiles() loads additional modules → enables fakeRequire → globs eligible sources → directly instruments ESM or reads other files through Module._extensions → stores lastFileCoverage with all:true → disables fakeRequire → writes coverage.
- Stored-result commands: report handler calls report() before optional checkCoverage(); merge handler validates the input directory, creates the output parent, loads/merges/remaps coverage through getCoverageMapFromAllCoverageFiles(), then writes JSON.
Testing signals
Tests and CI were inspected, not executed; test existence establishes intended checks, not passing results. TAP tests combine direct NYC calls, spawned CLI fixtures, and output snapshots. Development tests generally target generated self-coverage copies, with a separate coverage variable and exit writer. Coverage inspection is bounded, and explicit Windows branches and ignored error paths limit what the configured checks establish.
- package.json specifies TAP ^18.7.2 and Standard lint; .taprc disables TAP coverage, excludes helper/fixture paths, sets one job and a 360-second timeout, and preloads self-coverage-helper.js. These settings and CI were inspected only.
- build-self-coverage.js instruments runtime files into self-coverage using ___NYC_SELF_COVERAGE___; test helper paths resolve the generated CLI, and self-coverage-helper writes that separate global coverage at exit.
- CLI integration tests define checks for threshold success/failure, failing child status, configuration precedence, argument forwarding, process trees, and both spawn-wrap settings; testSuccess asserts zero status, empty stderr, and a stdout snapshot.
- test/instrument.js checks mode preservation, complete-copy, include/exclude behavior, explicit in-place operation, outside-root rejection, and deletion guards.
- Collection tests assert zero counters for unloaded --all sources and nonzero counters after require(); cache tests run collision and identical-file fixtures and assert child status 0.
- Source-map tests cover external/inline maps, cache-file creation, exclude-after-remap behavior, and --all in a module package; TypeScript tests run committed mapping.js with mapping enabled and disabled, rather than directly testing a TypeScript loader.
- Process tests inspect matching UUID coverage/process files and external-ID indexing; SIGTERM/SIGINT tests return without assertions on Windows, and nyc.config.js disables threshold enforcement there despite configured 100% thresholds.
- CI declares npm test on Ubuntu Node 20/24 and Windows lts/*, but suppressEPIPE and several parent-completion error branches are explicitly excluded from instrumentation; no execution results were collected.
Risk map
The sensitive boundaries inspected are local module execution, environment propagation, process hooks, and filesystem operations. Findings below are source observations, not confirmed vulnerabilities. Coverage can become incomplete through permissive instrumentation failures or suppressed artifact-loading errors. Cleanup, tree copying, and global hook state deserve focused review because their effects extend beyond a single transformed file.
- Configuration can select a dynamically required instrumenter, and --require modules execute in the parent or child setup paths; these are executable trust boundaries rather than data-only configuration.
- reset() recursively removes the resolved temporary directory when NYC_CWD is absent; configured temporary/report paths are resolved without a containment check, while instrument --delete has a separate working-directory guard.
- complete-copy globs dotfiles and copies files beyond the instrumentation selection, excluding .git and the output tree; docs explicitly state symlinks are dereferenced, so copy scope is broader than coverage scope.
- _transformFactory() returns original source after instrumentation errors unless exitOnError is set; parser-plugin tests explicitly expect a successful uninstrumented result when the required plugin is unset.
- coverageFiles() lists every directory entry, and coverageFileLoad() converts any read, parse, or cached-map reload error into {}; cached source-map read failures are also suppressed and remembered.
- NYC_CONFIG and NYC_CONFIG_OVERRIDE are parsed without local validation in the child bootstrap, and overrides are serialized back into NYC_CONFIG; environment configuration controls the constructed NYC instance.
- wrap() documents a spawn-wrap limitation in forcing NYC_PROCESS_ID propagation; hook installation, global coverage, and require-extension state are process-wide, and tests explicitly reset require caches/loaders between cases.
- Legacy guidance remains alongside current implementation: coverageData() still carries a 'Remove from nyc v16' TODO, profiling docs discuss index.covered.js, and .travis.yml lists Node 8/10 despite the current Node 20/>=22 engine range.
Extension points
Extensions enter through command modules, schema-derived options, configuration presets, preloaded modules, instrumenter factories, and reporter factories. NYC already centralizes the selection and transform pipeline, so additions should reuse those boundaries. Some extension contracts depend on external packages rather than a repository-local registry. Command dispatch and cache invalidation require coordinated changes across multiple files.
- A command module exports command, describe, builder, and handler; adding a command requires coordinating yargs registration, process-args' recognized command list, and bin/nyc.js' early-return list.
- CLI options are derived from schema.nyc.properties through setupOptions(); coverageVariable and global-scope options are explicitly blocked because they are not connected to the instrumenter.
- Reusable configurations use extends and can be exported as JSON or JavaScript npm modules; processConfig() delegates configuration loading to @istanbuljs/load-nyc-config.
- The instrumenter factory receives ignoreClassMethods, source-map production, compactness, comments, ESM parsing, and parser plugins; built-in adapters expose instrumentSync() and lastFileCoverage().
- Additional extensions receive entries in the transforms table and hook registration; addAllFiles() can obtain transpiled source through Module._extensions, falling back to the .js loader when an extension is unknown.
- --require is the module-setup extension point, with resolution attempted relative to NYC.cwd before fallback; non-spawn-wrap startup places these modules before lib/wrap.js in the preload list.
- Reporter names are stored as an array and passed to istanbul-reports.create(); README documents built-in reporters and npm package reporters, with per-reporter presentation options passed at execution.
- Instrumentation cache behavior has explicit extension-sensitive hooks: _createTransform() chooses eager or lazy factories, records hashes by filename, and Hash.salt() includes package versions and selected configuration values.
Contributor handoff
Begin with the CLI/core boundary and the generated self-coverage test setup before making changes. Keep configuration, argument routing, source selection, subprocess ancestry, and report remapping consistent. Tests and CI were inspected, not executed, and this brief is a bounded source investigation rather than exhaustive verification. Future checks should use existing npm/TAP machinery only when execution is authorized, with particular care around cleanup and source-mutating fixtures.
- Read package.json, bin/nyc.js, index.js, and lib/config-util.js first: they define publication, orchestration, state ownership, and configuration dispatch. This handoff is based on inspected source and CI, with no tests executed.
- Follow Standard JavaScript style and two-space, LF, UTF-8 EditorConfig settings; the lint script excludes snapshots, fixtures, and generated test build paths.
- Before running a targeted TAP file after authorized edits, refresh self-coverage through the existing instrument script: tests resolve generated runtime copies, and build-self-coverage copies package.json for generated hash-version lookup.
- Select tests by boundary: argument routing uses test/process-args.js, offline filesystem behavior uses test/instrument.js, and subprocess CLI behavior uses test/nyc-integration.js; npm test invokes the broader configured lifecycle.
- Treat cleanup and tests as mutating operations: npm-run-clean.js recursively removes artifact/cache paths, instrument tests temporarily chmod fixtures, and add-all-files tests create and remove source fixtures.
- Keep command names synchronized across parsing and dispatch, and preserve the distinction between nyc arguments and child arguments, including Node execArgv passed after --.
- For cache/source-map edits, inspect Hash.salt(), filename hash tracking, contentHash persistence, and cached map reloading together; report filtering occurs after remapping only when excludeAfterRemap is enabled.
- Account for platform limits when reporting verification: inspected CI covers Ubuntu Node 20/24 and Windows lts/*, signal tests bypass Windows, and configured 100% coverage checks are disabled there; none of these definitions proves a passing run.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 119 | 99% |
| JSON | 24 | docs/config |
| Markdown | 7 | docs/config |
| YAML | 6 | docs/config |
| TypeScript | 1 | 1% |
Start reading here
Probable source entrypoint: index.js — inferred from the directory tree and manifest, not the upstream docs.
Source roots
lib/ (14 files)
Source layout — lib/
| Path | Files |
|---|---|
lib/commands/ | 5 |
lib/instrumenters/ | 2 |
lib/config-util.js | 1 |
lib/fs-promises.js | 1 |
lib/hash.js | 1 |
lib/process-args.js | 1 |
lib/register-env.js | 1 |
lib/source-maps.js | 1 |
lib/wrap.js | 1 |
Repository structure
| Top-level path | Files |
|---|---|
test/ | 129 |
lib/ | 14 |
tap-snapshots/ | 5 |
.github/ | 4 |
docs/ | 4 |
bin/ | 2 |
Docs found: README.mddocs/instrument.mddocs/profiling.mddocs/setup-codecov.mddocs/setup-coveralls.md
Package
package.json — nyc v18.0.0, 36 declared dependencies.
@istanbuljs/load-nyc-config@istanbuljs/schemaany-pathcaching-transformconvert-source-mapdecamelizefind-cache-dirfind-upforeground-childget-package-typeglobis-windows
CI & config entrypoints
README.md— Repository readme.github/workflows/release-please.yml— GitHub Actions workflowpackage.json— npm scripts: clean, fix, instrument, lint, posttest, pretest, release, reporttest/fixtures/tsc/package.json— npm scripts: postinstall
Structural health signals
Average score 9.7/10 across 96 analyzed source files; 1 hotspot surfaced.
index.js— 541 lines, score 5.7 (Large file, Long function/block)test/nyc-integration.js— 678 lines, score 7.4 (Large file, High churn)lib/source-maps.js— 84 lines, score 7.9 (Deep nesting, Moderate churn)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
9086204751992026-05-17 — Add ESM config support to README.md (#1541)bc52ab10673e2022-04-27 — chore: remove git.iob81e2d1749fa2026-02-23 — docs: fix several spelling mistakes3ce6d979a1c62026-02-22 — chore(main): release nyc 18.0.0 (#1613)b9f6781c69f92026-02-22 — build: publication is now manual again due to changes in tokens