@cldmv/vitest-runner
v1.2.0
Published
Sequential Vitest runner to avoid OOM issues with large test suites
Maintainers
Readme
vitest-runner
Sequential Vitest runner that spawns each test file in its own child process to avoid out-of-memory crashes in large test suites.
- Runs files one-at-a-time or in a configurable parallel worker pool
- Supports full coverage mode via blob-per-file +
--mergeReports(no OOM) - Auto-detects your vitest config; accepts an explicit path if needed
- All standard Vitest CLI flags are forwarded unchanged
- Usable as a CLI binary or as a programmatic Node.js API
- Pure ESM with a CJS shim for
require()compatibility
Requirements
- Node.js ≥ 18
vitest≥ 1.0 (peer dependency, installed in your project)chalk(bundled dependency — no action needed)
Installation
npm install --save-dev vitest-runnerOr to use the CLI globally:
npm install -g vitest-runnerCLI usage
vitest-runner [OPTIONS] [PATTERNS...]Runner flags
| Flag | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| --test-list <file> | Run only the files listed in a JSON array file instead of scanning |
| --file-pattern <regex> | Override the file discovery regex (default: \.test\.vitest\.(?:js\|mjs\|cjs)$) |
| --workers <n> | Number of parallel workers (default: 4 or VITEST_WORKERS) |
| --solo-pattern <pat> | Run files matching this path substring solo (one at a time) before the worker pool; repeatable |
| --no-error-details | Hide inline error blocks — show only counts in the summary |
| --coverage-quiet | Implies --coverage; suppress per-file output and show only a live progress bar and final summaries |
| --log-file <path> | Write a clean (ANSI-stripped) copy of all output to this file. Defaults to coverage/coverage-run.log when --coverage-quiet is active |
| --suppress-file-output | Suppress per-file runner output blocks in any mode |
| --suppress-passing-files | Hide the PASSED TEST FILES section in the final summary |
| --no-top-summary | Hide TOP MEMORY USERS and TOP DURATION summary sections |
| --json | Print a JSON run report (no runner text output) |
| --blobs-dir <path> | Directory for per-file coverage blobs (default: .vitest-coverage-blobs, relative to cwd) |
| --no-merge-reports | Produce the coverage blobs but skip the merge and summary, leaving them in --blobs-dir for an external merge step |
| --help, -h | Print this help and exit |
Test patterns
Patterns are resolved against cwd. Any of the following forms work:
# Absolute or relative file path
vitest-runner src/tests/config/background.test.vitest.mjs
# Partial path or filename — matched against all discovered test files
vitest-runner background.test.vitest.mjs
vitest-runner config/background.test.vitest.mjs
# Directory — all test files inside it are run
vitest-runner src/tests/metadataMultiple patterns can be combined:
vitest-runner src/tests/config src/tests/metadataVitest passthrough flags
All unrecognised flags are forwarded verbatim to every vitest child process:
vitest-runner --reporter=verbose
vitest-runner -t "lazy materialization"
vitest-runner --coverage
vitest-runner --bailEnvironment variables
| Variable | Default | Description |
| ---------------- | -------- | ------------------------------------------------------------------------------- |
| VITEST_HEAP_MB | (none) | --max-old-space-size ceiling passed to every child process |
| VITEST_WORKERS | 4 | Maximum parallel worker slots in the non-solo phase (overridden by --workers) |
Examples
# Run all test files discovered under the default testDir
vitest-runner
# Run all tests, filter by name
vitest-runner -t "should handle null input"
# Run a specific folder
vitest-runner src/tests/auth
# Run with coverage (blob + merge — OOM-safe)
vitest-runner --coverage
# Coverage with quiet output and live progress bar (ideal for CI)
vitest-runner --coverage --coverage-quiet
# Run only files listed in a JSON file
vitest-runner --test-list my-tests.json
# Use a custom file discovery pattern
vitest-runner --file-pattern '\.spec\.ts$'
# Run 2 workers, with certain files running solo first
vitest-runner --workers 2 --solo-pattern heavy/ --solo-pattern listener-cleanup/
# Custom heap and worker count
VITEST_HEAP_MB=8192 vitest-runner --workers 2 src/tests/heavy
# Suppress error details in the summary
vitest-runner --no-error-details
# JSON output for automation
vitest-runner --json
# JSON output without top summary arrays
vitest-runner --json --no-top-summaryProgrammatic API
import { run } from "vitest-runner";
// CommonJS
const { run } = await require("vitest-runner");run(options) → Promise<number | object>
Runs the test suite and resolves with an exit code (0 = all passed, 1 = any failure) by default. When json: true is passed, it returns a structured JSON report object (including exitCode) instead of printing runner text output.
import { run } from "vitest-runner";
const code = await run({
testDir: "src/tests"
});
process.exit(code);Options
| Option | Type | Default | Description |
| ---------------------- | ----------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| cwd | string | process.cwd() | Absolute project root directory |
| testDir | string | cwd | Directory (absolute or relative to cwd) to scan for *.test.vitest.{js,mjs} files |
| vitestConfig | string | auto-detect | Explicit vitest config path; when omitted the runner walks standard config names (vitest.config.ts, vite.config.ts, etc.) relative to cwd |
| testPatterns | string[] | [] | File / folder patterns to filter — empty means all files in testDir |
| testListFile | string | undefined | Path to a JSON array of test file paths; when set, scanning is skipped entirely |
| testFilePattern | RegExp | DEFAULT_TEST_FILE_PATTERN | Regex matched against file names during discovery (*.test.vitest.{js,mjs,cjs} by default) |
| vitestArgs | string[] | [] | Extra CLI args forwarded verbatim to every vitest invocation |
| showErrorDetails | boolean | true | Print inline error blocks under each failed file in the summary |
| coverageQuiet | boolean | false | Suppress per-file output; show only the progress bar and final summaries |
| suppressFileOutput | boolean | false | Suppress per-file runner output blocks in all modes |
| suppressPassingFiles | boolean | false | Hide passed-file rows in the final summary |
| topSummary | boolean | true | Show or hide top memory/duration summary sections (and JSON arrays) |
| json | boolean | false | Return a JSON report object instead of printing text output |
| workers | number | 4 | Maximum parallel worker slots (overrides VITEST_WORKERS) |
| worstCoverageCount | number | 10 | Rows in the worst-coverage table after a coverage run (0 disables it) |
| blobsDir | string | <cwd>/.vitest-coverage-blobs | Directory for per-file coverage blobs. Relative paths resolve against cwd. Always cleared at the start of a coverage run |
| mergeReports | boolean | true | When true, blobs are merged via vitest --mergeReports, the coverage summary is printed, and blobsDir is deleted. When false, the run stops after producing blobs — no merge, no summary — and blobsDir is left populated for an external merge |
| maxOldSpaceMb | number | undefined | Global --max-old-space-size ceiling in MB (overrides VITEST_HEAP_MB) |
| earlyRunPatterns | string[] | [] | Path substrings — matching files run solo (one at a time) before the parallel worker pool starts |
| perFileHeapOverrides | PerFileHeapOverride[] | [] | Per-file minimum heap ceilings; the maximum of this and maxOldSpaceMb wins |
| conditions | string[] | [] | Additional --conditions Node flags forwarded to children |
| nodeEnv | string | 'development' | Value written to NODE_ENV in child processes |
PerFileHeapOverride
{
pattern: string;
heapMb: number;
}pattern is a substring matched against the normalised (forward-slash) file path. The first match wins and is compared against the global maxOldSpaceMb; the larger value is used.
Examples
// Run all tests under src/tests/ (cwd defaults to process.cwd())
await run({ testDir: "src/tests" });
// Run only the config and metadata suites
await run({
testDir: "src/tests",
testPatterns: ["src/tests/config", "src/tests/metadata"]
});
// Coverage run (OOM-safe blob + merge mode)
await run({
testDir: "src/tests",
vitestArgs: ["--coverage"]
});
// Quiet coverage with live progress bar
await run({
testDir: "src/tests",
coverageQuiet: true
});
// Machine-readable output (no text logs)
const report = await run({
testDir: "src/tests",
json: true
});
console.log(report.exitCode);
// Give heap-heavy files a larger ceiling while keeping the global limit lower
await run({
testDir: "src/tests",
maxOldSpaceMb: 2048,
earlyRunPatterns: ["listener-cleanup/"],
perFileHeapOverrides: [{ pattern: "listener-cleanup/", heapMb: 6144 }]
});Coverage mode
When --coverage (or coverageQuiet: true) is passed, the runner uses a blob-per-file strategy:
- Each file receives
--coverage --reporter=blobwith its own temp output directory. - After all files complete,
vitest --mergeReportscombines the blobs into a single report. - Temporary blob and coverage-tmp directories are cleaned up automatically.
This avoids the OOM crash that occurs when a single vitest process holds coverage data for thousands of files simultaneously.
Coverage quiet mode
--coverage-quiet / coverageQuiet: true suppresses all per-file output and renders a live progress bar instead. On completion it prints the coverage table and any failures verbosely. When running in this mode, output is also mirrored to coverage/coverage-run.log (CLI only) with ANSI colour codes stripped so the file is human-readable in any editor.
The log file path can be overridden with --log-file <path>. Passing --log-file by itself only enables log mirroring (it does not enable coverage mode).
Producing blobs for an external merge
Set mergeReports: false (CLI: --no-merge-reports) to stop the run after the per-file blobs are written. The internal vitest --mergeReports call and the coverage summary are skipped, and blobsDir is left intact instead of being deleted. The blobs directory is still cleared at the start of each run, so it only ever contains the current run's output.
This is useful when a second coverage blob set (for example, a browser-mode run with a different coverage transform) needs to be merged together with the node-mode blobs. Point both runs at known directories with blobsDir, then merge them in one external vitest --mergeReports step:
// Node-mode blobs, no internal merge
await run({
testDir: "src/tests",
vitestArgs: ["--coverage"],
blobsDir: ".coverage-blobs/node",
mergeReports: false
});
// (separately produce browser-mode blobs into .coverage-blobs/browser)
// then merge both blob sets in a single external step:
// vitest --mergeReports .coverage-blobs --coverageThe exit code still reflects test pass/fail; there is just no coverage-merge result to fold in.
Test list files
A test list file is a plain JSON array of test file paths (relative to cwd):
["src/tests/auth/login.test.vitest.mjs", "src/tests/auth/register.test.vitest.mjs", "src/tests/config/defaults.test.vitest.mjs"]Pass --test-list <file> (CLI) or testListFile: 'path/to/list.json' (API) to run exactly those files instead of scanning testDir.
Test file naming
By default, the runner discovers files matching:
*.test.vitest.js
*.test.vitest.mjs
*.test.vitest.cjsFiles in node_modules or hidden directories (names starting with .) are always skipped.
The pattern can be overridden with --file-pattern <regex> (CLI) or the testFilePattern option (API):
# Match .spec.ts files instead
vitest-runner --file-pattern '\.spec\.ts$'await run({ cwd, testDir: "src", testFilePattern: /\.spec\.ts$/i });Source layout
index.mjs ← ESM entry (re-exports src/runner.mjs)
index.cjs ← CJS shim (dynamic import of index.mjs)
bin/
vitest-runner.mjs ← CLI binary
src/
runner.mjs ← main run() API + re-exports
utils/
ansi.mjs ← stripAnsi, colourPct
duration.mjs ← formatDuration
env.mjs ← buildNodeOptions
resolve.mjs ← resolveBin, resolveVitestConfig
core/
discover.mjs ← discoverVitestFiles, sortWithPriority
parse.mjs ← parseVitestOutput, deduplicateErrors
spawn.mjs ← runSingleFile, runVitestDirect, runMergeReports
report.mjs ← printCoverageSummary, printMergeOutput
progress.mjs ← createCoverageProgressTracker
cli/
args.mjs ← parseArguments
help.mjs ← showHelpAll sub-module utilities are re-exported from the root entry point, so deep imports are optional.
License
MIT
