oxc_coverage_instrument
Istanbul-compatible JavaScript/TypeScript coverage instrumentation, built on the Oxc parser. 8-44x faster than existing tools.
Why
swc-coverage-instrument fills this role for SWC. There is no equivalent for the Oxc ecosystem. Any tool built on oxc_parser that needs coverage instrumentation currently has to pull in SWC or Babel.
This crate fills that gap. AST-level instrumentation via oxc_traverse + oxc_codegen produces correct Istanbul-compatible output, verified against the canonical istanbul-lib-instrument on 27 shared fixtures.
Install
Rust
[]
= "0.4"
Node.js
CLI
Usage
Rust
use ;
let source = "function add(a, b) { return a + b; }";
let result = instrument.unwrap;
// Istanbul-compatible coverage map
assert_eq!;
// Instrumented source with counters injected
println!;
Node.js
import from 'oxc-coverage-instrument';
const result = ;
result.; // instrumented source
const coverageMap = JSON.; // Istanbul format
result.; // source map JSON (if enabled)
CLI
# Print instrumented code to stdout
# Write to file
# Print coverage map only
# With source map
Vitest integration
Requires vitest and @vitest/coverage-istanbul >= 4.1.5 (coverage.instrumenter option).
import { defineConfig } from 'vitest/config'
import { createOxcInstrumenter } from 'oxc-coverage-instrument/vitest'
export default defineConfig({
test: {
coverage: {
provider: 'istanbul',
instrumenter: (options) => createOxcInstrumenter(options),
}
}
})
The factory forwards coverageVariable and ignoreClassMethods to the native instrumenter. Everything else in the Istanbul provider (collection, merging, reporting) keeps working unchanged.
vite-plugin-istanbul integration
Requires vite-plugin-istanbul >= 9.0.0 (instrumenter option).
import { defineConfig } from 'vite'
import istanbul from 'vite-plugin-istanbul'
import { createOxcInstrumenter } from 'oxc-coverage-instrument/vitest'
export default defineConfig({
plugins: [
istanbul({
instrumenter: createOxcInstrumenter(),
include: ['src/**/*.{js,ts,jsx,tsx}'],
exclude: ['node_modules', 'test/'],
}),
],
})
Custom Vite/Rollup plugin
If you're not using vite-plugin-istanbul, you can call instrument directly from a transform hook:
import from 'oxc-coverage-instrument';
export
Reading existing coverage data
use parse_coverage_map;
// Parse a coverage-final.json file
let json = read_to_string.unwrap;
let map = parse_coverage_map.unwrap;
for in &map
What it tracks
| Dimension | What gets a counter |
|---|---|
| Statements | Every executable statement |
| Functions | Declarations, expressions, arrows, class methods |
| Branches | if/else, ternary, switch, &&/||/??, ??=/||=/&&=, default-arg |
| Pragmas | istanbul/v8/c8 ignore next/if/else/file/start/stop |
Istanbul conformance
Verified against istanbul-lib-instrument on 27 shared fixtures covering all branch types, function forms, Unicode columns, pragma boundaries, and edge cases. 189 automated conformance checks validate:
- Function counts match exactly
- Branch counts, types, and location counts match exactly
- Statement counts match exactly
- JSON structure matches Istanbul's field set
- Instrumented output re-parses as valid JS
CI also runs a blocking byte-for-byte Istanbul diff over the shared fixture corpus after filtering documented intentional divergences. This catches span-level and counter-shape drift that count-only tests can miss.
Real-world verification: 1,061 TS/TSX/JS files from a production React monorepo produce byte-for-byte identical statement, function, and branch counts to istanbul-lib-instrument (when both instrumenters receive the same Babel-transpiled input).
Independently validated against the Vitest test suite: from v0.3.5 onward, coverage-final.json for the Vitest math.ts fixture is byte-for-byte identical to @vitest/coverage-istanbul's output — including statementMap, fnMap spans, branchMap, and all counter arrays.
Column conventions: all start.column / end.column values in statementMap, fnMap, branchMap, and unhandledPragmas are reported as UTF-16 code units (JavaScript string indices), matching Babel and istanbul-lib-instrument. Sources containing non-ASCII characters — π, accented identifiers, emoji — produce the same column numbers as the reference tool. Verified by the 26-non-ascii-identifiers.js conformance fixture (tests/conformance/fixtures/).
Differences from istanbul-lib-instrument
Intentional divergences from istanbul-lib-instrument:
1. ES2021 logical-assignment operators are instrumented as branches
x ??= y, x ||= y, and x &&= y each contain a genuine short-circuit conditional: the right-hand side is evaluated (and the assignment happens) only when the left operand matches the operator's polarity. oxc-coverage-instrument emits one binary-expr branch entry per logical-assignment with two locations (left = always reached, right = conditional). istanbul-lib-instrument has no AssignmentExpression visitor entry and emits zero branches for these operators.
Pinned by tests/conformance_test.rs::logical_assignment_is_intentional_branch_superset.
2. Inferred function names over (anonymous_N)
For anonymous function expressions assigned to a variable or declared as a class method, oxc-coverage-instrument uses the name the JavaScript runtime actually assigns to Function.prototype.name:
| Source | oxc fnMap[].name |
istanbul fnMap[].name |
|---|---|---|
const f = function() {} |
f |
(anonymous_0) |
const g = () => 1 |
g |
(anonymous_0) |
class C { bar() {} } |
bar |
(anonymous_0) |
(function() {})() (IIFE) |
(anonymous_0) |
(anonymous_0) |
Coverage reports and stack traces benefit from real names. Pinned by tests/conformance_test.rs::fn_name_inference_is_intentional_superset.
3. Full method-key spans in fnMap[*].decl
For class and object methods, oxc-coverage-instrument records the whole method key as the declaration span. istanbul-lib-instrument truncates method declarations to the key's first character.
| Source | oxc decl |
istanbul decl |
|---|---|---|
class C { bar() {} } |
bar |
b |
The byte-diff check still pins the method declaration start, line, body loc, and all non-method function declaration spans.
Migration from @vitest/coverage-istanbul: a codebase that uses ??=/||=/&&= heavily will see a higher branch-coverage denominator (and so a slightly lower branch %) after switching providers. To rebaseline CI thresholds after the swap:
This is additional coverage signal, not a regression. Every extra branch represents a real runtime decision path.
Performance
Benchmarked on real-world JavaScript libraries, all running in the same Node.js process for a fair comparison. Reproduce with ./scripts/benchmark-comparison.sh.
| File | Size | oxc (napi) | babel-plugin-istanbul | swc-plugin (wasm) | istanbul-lib |
|---|---|---|---|---|---|
| react.development.js | 107 KB | 1.8 ms | 19.2 ms | 26.5 ms | 72.7 ms |
| lodash.js | 531 KB | 7.4 ms | 57.0 ms | 100.1 ms | 226.3 ms |
| vue.global.js | 462 KB | 12.4 ms | 125.1 ms | 225.5 ms | 548.3 ms |
| d3.js | 573 KB | 22.7 ms | 192.9 ms | 311.1 ms | 773.8 ms |
| three.js | 1.2 MB | 30.7 ms | 293.6 ms | 449.0 ms | 1094.0 ms |
8-11x faster than babel-plugin-istanbul, 13-18x faster than swc-plugin-coverage-instrument (Rust/WASM), 30-44x faster than istanbul-lib-instrument.
Note: swc-plugin-coverage-instrument is written in Rust but runs as a WASM module inside SWC's sandbox, adding serialisation overhead at every AST boundary. The comparison measures end-to-end instrumentation time as users experience it.
Architecture
source code (JS/TS)
|
v
oxc_parser -- parse to AST
|
v
SemanticBuilder -- build scope tree
|
v
CoverageTransform -- traverse AST, inject ++cov().s[N] counters
|
v
oxc_codegen -- emit instrumented code + source map
|
v
instrumented code + coverage map
Coverage stack
This crate is the instrumentation stage of a larger Rust-native coverage pipeline maintained alongside srcmap, a companion source map SDK from the same author. Together they cover the hot path of the Istanbul pipeline; the cold path (report formats, merging) stays on existing packages.
source code (JS/TS)
|
v
+-----------------------------+
| oxc-coverage-instrument | this crate
| (parse, transform, codegen) | AST-level Istanbul counters
+-----------------------------+
|
| instrumented code + composed source map
v
+-----------------------------+
| runtime collection | browser / Node / V8
| (writes __coverage__) |
+-----------------------------+
|
| raw FileCoverage objects
v
+-----------------------------+
| source map remap | istanbul-lib-source-maps today
| (-> original source paths) | srcmap-symbolicate as Rust path
+-----------------------------+
|
| remapped FileCoverage
v
+-----------------------------+
| report | istanbul-reports
| (lcov, html, json-summary) |
+-----------------------------+
When an inputSourceMap is supplied, the instrumenter composes the codegen's output map with the input map so that downstream remappers (Vitest, nyc, monocart) resolve coverage positions all the way back to the original source. Composition is delegated to srcmap-remapping, which mirrors @ampproject/remapping semantics (the same primitive istanbul-lib-source-maps and every major bundler rely on).
Related projects
| Project | AST | Notes |
|---|---|---|
istanbul-lib-instrument |
Babel | The canonical Istanbul instrumenter |
babel-plugin-istanbul |
Babel | Babel plugin wrapper around istanbul-lib-instrument |
swc-plugin-coverage-instrument |
SWC | SWC WASM plugin |
| this crate | Oxc | Native Rust, 8-44x faster |
Compatibility
- Rust: 1.92+ (2024 edition)
- Oxc: 0.126.x
- Istanbul:
coverage-final.jsonv3+ format - Node.js: 18+ (via napi-rs)
License
MIT