[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(coverage): istanbul to support `instrumenter` option (#10119)

Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>

authored by

Bart Waardenburg
Ari Perkkiö
and committed by
GitHub
(Apr 20, 2026, 1:44 PM +0200) 0e0ff41c 86807add

+201 -21
+40
docs/config/coverage.md
··· 390 390 391 391 Concurrency limit used when processing the coverage results. 392 392 393 + ## coverage.instrumenter <Version type="experimental">4.1.5</Version> {#coverage-instrumenter} 394 + 395 + - **Type:** `(options: InstrumenterOptions) => CoverageInstrumenter` 396 + - **Available for providers:** `'istanbul'` 397 + 398 + Factory for a custom instrumenter to use in place of the default `istanbul-lib-instrument`. Vitest calls the factory once during initialization and reuses the returned instrumenter for every file. The rest of the Istanbul pipeline (collection, merging, reporting) is unchanged. 399 + 400 + The factory receives an `InstrumenterOptions` object with Vitest's runtime coverage settings, and must return an object implementing the `CoverageInstrumenter` interface. Both types are exported from `vitest/node`. 401 + 402 + <!-- eslint-skip --> 403 + ```ts 404 + interface InstrumenterOptions { 405 + coverageVariable: string 406 + coverageGlobalScope: string 407 + coverageGlobalScopeFunc: boolean 408 + ignoreClassMethods: string[] 409 + } 410 + 411 + interface CoverageInstrumenter { 412 + instrumentSync: (code: string, filename: string, inputSourceMap?: any) => string 413 + lastSourceMap: () => any 414 + lastFileCoverage: () => any 415 + } 416 + ``` 417 + 418 + <!-- eslint-skip --> 419 + ```ts 420 + import { defineConfig } from 'vitest/config' 421 + import { createInstrumenter } from '@vitest/some-custom-instrumenter' 422 + 423 + export default defineConfig({ 424 + test: { 425 + coverage: { 426 + provider: 'istanbul', 427 + instrumenter: options => createInstrumenter(options), 428 + } 429 + } 430 + }) 431 + ``` 432 + 393 433 ## coverage.customProviderModule 394 434 395 435 - **Type:** `string`
+30 -20
packages/coverage-istanbul/src/provider.ts
··· 31 31 initialize(ctx: Vitest): void { 32 32 this._initialize(ctx) 33 33 34 - this.instrumenter = createInstrumenter({ 35 - produceSourceMap: true, 36 - autoWrap: false, 37 - esModules: true, 38 - compact: false, 39 - coverageVariable: COVERAGE_STORE_KEY, 40 - coverageGlobalScope: 'globalThis', 41 - coverageGlobalScopeFunc: false, 42 - ignoreClassMethods: this.options.ignoreClassMethods, 43 - parserPlugins: [ 44 - ...istanbulDefaults.instrumenter.parserPlugins, 45 - ['importAttributes', { deprecatedAssertSyntax: true }], 46 - ], 47 - generatorOpts: { 48 - // @ts-expect-error missing type 49 - importAttributesKeyword: 'with', 50 - }, 34 + if (this.options.instrumenter) { 35 + this.instrumenter = this.options.instrumenter({ 36 + coverageVariable: COVERAGE_STORE_KEY, 37 + coverageGlobalScope: 'globalThis', 38 + coverageGlobalScopeFunc: false, 39 + ignoreClassMethods: this.options.ignoreClassMethods, 40 + }) as Instrumenter 41 + } 42 + else { 43 + this.instrumenter = createInstrumenter({ 44 + produceSourceMap: true, 45 + autoWrap: false, 46 + esModules: true, 47 + compact: false, 48 + coverageVariable: COVERAGE_STORE_KEY, 49 + coverageGlobalScope: 'globalThis', 50 + coverageGlobalScopeFunc: false, 51 + ignoreClassMethods: this.options.ignoreClassMethods, 52 + parserPlugins: [ 53 + ...istanbulDefaults.instrumenter.parserPlugins, 54 + ['importAttributes', { deprecatedAssertSyntax: true }], 55 + ], 56 + generatorOpts: { 57 + // @ts-expect-error missing type 58 + importAttributesKeyword: 'with', 59 + }, 51 60 52 - // Custom option from the patched istanbul-lib-instrument: https://github.com/istanbuljs/istanbuljs/pull/835 53 - ignoreLines: true, 54 - }) 61 + // Custom option from the patched istanbul-lib-instrument: https://github.com/istanbuljs/istanbuljs/pull/835 62 + ignoreLines: true, 63 + }) 64 + } 55 65 } 56 66 57 67 requiresTransform(id: string): boolean {
+37 -1
test/coverage-test/test/configuration-options.test-d.ts
··· 1 1 import type { defineConfig } from 'vitest/config' 2 - import type { CoverageProviderModule, ResolvedCoverageOptions, Vitest } from 'vitest/node' 2 + import type { CoverageInstrumenter, CoverageProviderModule, InstrumenterOptions, ResolvedCoverageOptions, Vitest } from 'vitest/node' 3 3 import { assertType, test } from 'vitest' 4 4 5 5 type NarrowToTestConfig<T> = T extends { test?: any } ? NonNullable<T['test']> : never ··· 206 206 ['html', { verbose: true, subdir: 'string' }], 207 207 ['custom-reporter-4', { some: 'option', width: 123 }], 208 208 ], 209 + }) 210 + }) 211 + 212 + test('custom instrumenter', () => { 213 + // Custom instrumenter factory function 214 + assertType<Coverage>({ 215 + provider: 'istanbul', 216 + instrumenter: _options => ({ 217 + instrumentSync: (code, _filename, _sourceMap?) => code, 218 + lastSourceMap: () => ({}), 219 + lastFileCoverage: () => ({}), 220 + }), 221 + }) 222 + 223 + // Without instrumenter (default behavior) 224 + assertType<Coverage>({ 225 + provider: 'istanbul', 226 + }) 227 + 228 + // Verify CoverageInstrumenter type can be used as return type 229 + const factory: (opts: InstrumenterOptions) => CoverageInstrumenter = _opts => ({ 230 + instrumentSync: code => code, 231 + lastSourceMap: () => null, 232 + lastFileCoverage: () => ({}), 233 + }) 234 + 235 + assertType<InstrumenterOptions>({ 236 + coverageVariable: '__VITEST_COVERAGE__', 237 + coverageGlobalScope: 'globalThis', 238 + coverageGlobalScopeFunc: false, 239 + ignoreClassMethods: ['test-method'], 240 + }) 241 + 242 + assertType<Coverage>({ 243 + provider: 'istanbul', 244 + instrumenter: factory, 209 245 }) 210 246 })
+34
test/coverage-test/test/custom-instrumenter.istanbul.test.ts
··· 1 + import { expect, vi } from 'vitest' 2 + import { normalizeURL, runVitest, test } from '../utils' 3 + 4 + test('custom instrumenter receives correct options', async () => { 5 + const instrumenter = vi.fn().mockReturnValue({ 6 + instrumentSync: (code: string) => code, 7 + lastSourceMap: () => ({}), 8 + lastFileCoverage: () => ({ 9 + path: 'test.ts', 10 + statementMap: {}, 11 + fnMap: {}, 12 + branchMap: {}, 13 + s: {}, 14 + f: {}, 15 + b: {}, 16 + }), 17 + }) 18 + 19 + await runVitest({ 20 + include: [normalizeURL(import.meta.url)], 21 + coverage: { 22 + reporter: 'json', 23 + ignoreClassMethods: ['test-method'], 24 + instrumenter, 25 + }, 26 + }, { throwOnError: false }) 27 + 28 + expect(instrumenter).toHaveBeenCalledWith({ 29 + coverageVariable: '__VITEST_COVERAGE__', 30 + coverageGlobalScope: 'globalThis', 31 + coverageGlobalScopeFunc: false, 32 + ignoreClassMethods: ['test-method'], 33 + }) 34 + })
+2
packages/vitest/src/public/node.ts
··· 152 152 } from '../node/types/config' 153 153 export type { 154 154 BaseCoverageOptions, 155 + CoverageInstrumenter, 155 156 CoverageIstanbulOptions, 156 157 CoverageOptions, 157 158 CoverageProvider, ··· 159 160 CoverageReporter, 160 161 CoverageV8Options, 161 162 CustomProviderOptions, 163 + InstrumenterOptions, 162 164 ReportContext, 163 165 ResolvedCoverageOptions, 164 166 } from '../node/types/coverage'
+58
packages/vitest/src/node/types/coverage.ts
··· 272 272 ignoreClassMethods?: string[] 273 273 274 274 /** 275 + * Custom instrumenter factory to use instead of the default `istanbul-lib-instrument`. 276 + * 277 + * The factory receives the same runtime coverage options Vitest passes to its 278 + * built-in Istanbul instrumenter and must return an object implementing the 279 + * `CoverageInstrumenter` interface. 280 + * 281 + * This allows using faster instrumenters (e.g., oxc-coverage-instrument, SWC) while 282 + * keeping the Istanbul coverage pipeline for collection, merging, and reporting. 283 + * 284 + * @example 285 + * ```ts 286 + * import { defineConfig } from 'vitest/config' 287 + * import { createOxcInstrumenter } from 'oxc-coverage-instrument/vitest' 288 + * 289 + * export default defineConfig({ 290 + * test: { 291 + * coverage: { 292 + * provider: 'istanbul', 293 + * instrumenter: options => createOxcInstrumenter(options), 294 + * } 295 + * } 296 + * }) 297 + * 298 + * @experimental 299 + */ 300 + instrumenter?: (options: InstrumenterOptions) => CoverageInstrumenter 301 + 302 + /** 275 303 * Directory of HTML coverage output to be served in UI mode and HTML reporter. 276 304 * This is automatically configured for builtin reporter with html output (`html`, `html-spa`, and `lcov` reporters). 277 305 * Use this option to override with custom coverage reporting location. ··· 316 344 317 345 /** Thresholds for lines */ 318 346 lines?: number 347 + } 348 + 349 + /** 350 + * Options passed to the custom instrumenter factory. 351 + */ 352 + export interface InstrumenterOptions { 353 + /** Global variable name that Vitest uses to store coverage data at runtime. */ 354 + coverageVariable: string 355 + /** Global scope where the coverage variable is attached at runtime. */ 356 + coverageGlobalScope: string 357 + /** Whether the coverage global scope should be resolved through an evaluated function. */ 358 + coverageGlobalScopeFunc: boolean 359 + /** Class method names to exclude from function coverage. */ 360 + ignoreClassMethods: string[] 361 + } 362 + 363 + /** 364 + * Interface for custom coverage instrumenters. 365 + * 366 + * Matches the subset of istanbul-lib-instrument's `Instrumenter` that Vitest 367 + * actually uses. Implement this to plug in a faster instrumenter while keeping 368 + * the Istanbul coverage pipeline for collection, merging, and reporting. 369 + */ 370 + export interface CoverageInstrumenter { 371 + /** Instrument source code synchronously. Returns the instrumented code string. */ 372 + instrumentSync: (code: string, filename: string, inputSourceMap?: any) => string 373 + /** Get the source map of the last instrumented file. */ 374 + lastSourceMap: () => any 375 + /** Get the Istanbul-compatible file coverage object of the last instrumented file. */ 376 + lastFileCoverage: () => any 319 377 } 320 378 321 379 /** @deprecated Use `CoverageOptions` instead */