@cldmv/vitest-runner

中文文档
1.6.1发布于 2026/11/02Apache-2.0TypeScriptESM + CJS构建来源已验证近 30 天下载 1万

Sequential Vitest runner to avoid OOM issues with large test suites

原始使用文档
文档目录29 节
  1. ✨ What's New
  2. Latest: v1.6.1 (November 2026)
  3. Recent Releases
  4. 🚀 Key Features
  5. 📦 Installation
  6. Requirements
  7. Install
  8. 🚀 Quick Start
  9. 💻 CLI usage
  10. Runner flags
  11. Test patterns
  12. Vitest passthrough flags
  13. Environment variables
  14. Examples
  15. 🔧 Programmatic API
  16. run(options) → Promise<number | object>
  17. Scratch directories ( VITEST_RUNNER_TMP / makeRunTmpDir )
  18. Vitest's module cache
  19. 📊 Coverage mode
  20. Coverage quiet mode
  21. When no files are measured
  22. Producing blobs for an external merge
  23. 📋 Test list files
  24. 🔍 Test file naming
  25. 📁 Source layout
  26. 📚 Documentation
  27. 🤝 Contributing
  28. 🔗 Links
  29. 📄 License

@cldmv/vitest-runner

@cldmv/vitest-runner is a sequential Vitest runner that spawns each test file in its own child process, so a large test suite never has to fit in one Vitest process's memory. It runs files one at a time or in a parallel worker pool, gives heavy files their own heap ceiling or a solo slot, and prints one combined summary of results, memory use and duration at the end.

Coverage stays out-of-memory-safe too: each file writes a coverage blob, and a single vitest --mergeReports step combines them into one report, with a worst-coverage table after it. The runner auto-detects your Vitest config, forwards every standard Vitest flag unchanged, and works as a vitest-runner CLI or as a programmatic run() API from ESM and CommonJS.

Big suites, small processes: every test file gets its own Vitest, and the results come back as one run.

npm version npm downloads GitHub downloads Last commit npm last update Coverage

Contributors Sponsor shinrai


✨ What's New

Latest: v1.6.1 (November 2026)

  • engines.node set to the runtime floor — v1.6.0 declared >=22.22.0, the build and test toolchain floor. engines.node is now >=22.0.0, the floor of the chalk 6 dependency, and the 22.22.0 development floor moves to devEngines, so earlier Node.js 22 releases install the runner without an EBADENGINE warning. require() needs Node.js >=22.13.0; behavior and options are unchanged (#115).
  • View full v1.6.1 Changelog

Recent Releases

  • v1.6.0 (November 2026) — --conditions=vitest-runner-dev sends require() to src/index.cjs too, and engines.node raised to >=22.22.0; runner behavior unchanged (Changelog)
  • v1.5.6 (October 2026) — Dev-tooling dependency update: tsup 9, Vitest 5.0.5 and a katex override clear both Dependabot alerts; the built files are byte-identical to v1.5.5 (Changelog)
  • v1.5.5 (October 2026) — No more ENOTEMPTY startup errors from Vitest 5's module cache: a cache directory per package, one lockfile check per run, and a new fsModuleCachePath option (Changelog)
  • v1.5.4 (October 2026) — Dev-tooling dependency update (@cldmv/fix-headers 2.2.0, @cldmv/configs 1.2.4); the published package is identical to v1.5.3 (Changelog)

📚 For complete release notes, see the docs/changelog/ folder.


🚀 Key Features

  • Runs every test file in its own child process, one at a time or in a configurable parallel worker pool
  • Solo slots for heavy files (--solo-pattern) and per-file heap ceilings (perFileHeapOverrides)
  • Full coverage mode via blob-per-file + --mergeReports (no OOM), with a worst-coverage table
  • 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, with an optional JSON run report
  • A per-run scratch directory for every test file (VITEST_RUNNER_TMP / makeRunTmpDir), cleaned up on exit
  • ES module build with a thin CommonJS entry for require()

📦 Installation

Requirements

  • Node.js 22.0.0 or later (engines.node is >=22.0.0, set by the chalk 6 dependency). The package is an ES module and loads with import. The Vitest version you install has its own Node.js floor. Node.js 20 is not supported from v1.5.3; stay on v1.5.1 there.
  • require("@cldmv/vitest-runner") loads the ES module build synchronously, which needs Node.js >=22.13.0; on older Node.js, use import() instead.
  • Developing this package needs Node.js 22.22.0 or later (devEngines).
  • vitest ≥ 1.0 (peer dependency, installed in your project)
  • chalk (bundled dependency — no action needed)

Install

npm install --save-dev @cldmv/vitest-runner

Or to use the CLI globally:

npm install -g @cldmv/vitest-runner

🚀 Quick Start

Run every discovered *.test.vitest.{js,mjs,cjs} file, each in its own process:

npx vitest-runner

Run with OOM-safe coverage and a live progress bar:

npx vitest-runner --coverage-quiet

From code:

import { run } from "@cldmv/vitest-runner";

const code = await run({ testDir: "src/tests" });
process.exit(code);

The full flag list is under CLI usage and every option under Programmatic API.


💻 CLI usage

vitest-runner [OPTIONS] [PATTERNS...]

Runner flags

FlagDescription
--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
--exclude <glob>Directory / file glob, relative to cwd, that discovery never enters (e.g. tmp/**); repeatable
--no-error-detailsHide inline error blocks — show only counts in the summary
--coverage-quietImplies --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-outputSuppress per-file runner output blocks in any mode
--suppress-passing-filesHide the PASSED TEST FILES section in the final summary
--no-top-summaryHide TOP MEMORY USERS and TOP DURATION summary sections
--jsonPrint 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-reportsProduce the coverage blobs but skip the merge and summary, leaving them in --blobs-dir for an external merge step
--keep-tmpKeep this run's scratch directory instead of removing it on completion
--scratch-dir <path>Per-run scratch root, relative to cwd (default: tmp/vitest-runner)
--help, -hPrint 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/metadata

Multiple patterns can be combined:

vitest-runner src/tests/config src/tests/metadata

Vitest 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 --bail

Environment variables

VariableDefaultDescription
VITEST_HEAP_MB(none)--max-old-space-size ceiling passed to every child process
VITEST_WORKERS4Maximum 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/

# Skip scratch worktrees / build output when discovery walks the repo root
vitest-runner --exclude 'tmp/**' --exclude 'dist/**'

# 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-summary

🔧 Programmatic API

import { run } from "@cldmv/vitest-runner";

// CommonJS
const { run } = require("@cldmv/vitest-runner");

require() loads the ES module build synchronously through Node's require(esm), which needs Node.js >=22.13.0; on older Node.js it throws a clear ERR_REQUIRE_ESM error, and import() is the way to load the package there.

When developing against a local checkout or workspace copy instead of the published package, run Node with --conditions=vitest-runner-dev so that import of the package resolves to its src/ entry (src/index.mjs) and require to src/index.cjs, instead of the built dist/.

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 "@cldmv/vitest-runner";

const code = await run({
    testDir: "src/tests"
});

process.exit(code);

Options

OptionTypeDefaultDescription
cwdstringprocess.cwd()Absolute project root directory
testDirstringcwdDirectory (absolute or relative to cwd) to scan for *.test.vitest.{js,mjs,cjs} files
vitestConfigstringauto-detectExplicit vitest config path; when omitted the runner walks standard config names (vitest.config.ts, vite.config.ts, etc.) relative to cwd
testPatternsstring[][]File / folder patterns to filter — empty means all files in testDir
testListFilestringundefinedPath to a JSON array of test file paths; when set, scanning is skipped entirely
testFilePatternRegExpDEFAULT_TEST_FILE_PATTERNRegex matched against file names during discovery (*.test.vitest.{js,mjs,cjs} by default)
excludestring[][]Directory / file globs, relative to cwd, that discovery never enters (e.g. ['tmp/**']). Applies to both the default scan and partial-path pattern resolution
vitestArgsstring[][]Extra CLI args forwarded verbatim to every vitest invocation
showErrorDetailsbooleantruePrint inline error blocks under each failed file in the summary
coverageQuietbooleanfalseSuppress per-file output; show only the progress bar and final summaries
suppressFileOutputbooleanfalseSuppress per-file runner output blocks in all modes
suppressPassingFilesbooleanfalseHide passed-file rows in the final summary
topSummarybooleantrueShow or hide top memory/duration summary sections (and JSON arrays)
jsonbooleanfalseReturn a JSON report object instead of printing text output
workersnumber4Maximum parallel worker slots (overrides VITEST_WORKERS)
worstCoverageCountnumber10Rows in the worst-coverage table after a coverage run (0 disables it)
blobsDirstring<cwd>/.vitest-coverage-blobsDirectory for per-file coverage blobs. Relative paths resolve against cwd. Always cleared at the start of a coverage run
mergeReportsbooleantrueWhen 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
maxOldSpaceMbnumberundefinedGlobal --max-old-space-size ceiling in MB (overrides VITEST_HEAP_MB)
earlyRunPatternsstring[][]Path substrings — matching files run solo (one at a time) before the parallel worker pool starts
perFileHeapOverridesPerFileHeapOverride[][]Per-file minimum heap ceilings; the maximum of this and maxOldSpaceMb wins
conditionsstring[][]Additional --conditions Node flags forwarded to children
nodeEnvstring'development'Value written to NODE_ENV in child processes
scratchDirstring'tmp/vitest-runner'Per-run scratch root, relative to cwd (or absolute). A subdirectory is created per file invocation and exposed to it via VITEST_RUNNER_TMP
keepTmpbooleanfalseKeep the run's scratch root instead of removing it on completion (normal exit, failure, or SIGINT/SIGTERM)
fsModuleCachePathstring | false<cwd>/node_modules/.vitest-cacheVitest 5+ fsModuleCache directory for every child, relative to cwd (or absolute). Overrides a path in the vitest config; a --fsModuleCachePath passthrough arg overrides this. When unset, the default applies only if the resolved config (root or any project) sets no path. false adds nothing. See Vitest's module cache

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 }]
});

Scratch directories (VITEST_RUNNER_TMP / makeRunTmpDir)

Every run gets its own scratch root (<scratchDir>/<pid>-<timestamp>/), created before any file runs and removed once the run completes — on success, on failure, and on SIGINT/SIGTERM — unless keepTmp is set. Each file invocation gets its own subdirectory under that root, exposed to the child as process.env.VITEST_RUNNER_TMP. Stale roots left by a crashed prior run (dead PID) are swept at the start of the next run.

From a test file, use makeRunTmpDir(label) to get a fresh, uniquely-named subdirectory instead of managing your own mkdtemp base:

import { makeRunTmpDir } from "@cldmv/vitest-runner";

const dir = makeRunTmpDir("my-fixture"); // a fresh directory under VITEST_RUNNER_TMP

CLI flags: --scratch-dir <path> and --keep-tmp (see Runner flags above).

Vitest's module cache

Vitest 5 caches transformed modules on disk (fsModuleCache, on by default from 5.0.5). Its default location is node_modules/.vitest-cache at the workspace root, shared by every package in the workspace, and each vitest process deletes the whole directory on startup when the lockfile differs from the one the cache was written for. Several processes starting at once after a lockfile change all try to delete it, and some fail with ENOTEMPTY. Packages in one workspace that have their own lockfile also keep clearing each other's cache.

Before any file runs, the runner starts one short helper process that loads the project's Vitest config (the root and every project) the way the workers will, and:

  • picks a cache per package: when the cache is on and nothing sets a path, every child gets --fsModuleCachePath=<cwd>/node_modules/.vitest-cache, so one package's run never clears another's cache;
  • checks the lockfile once: it starts Vitest once with the workers' args, which clears a stale cache, and exits. The workers then start on a cache that matches the lockfile. This step is skipped when solo files run first (the first solo file does the same job), when files never run in parallel, or when the cache is off.

The cache path is chosen in this order:

  1. a --fsModuleCachePath passthrough arg;
  2. the fsModuleCachePath option (false adds nothing and leaves Vitest's own setting in place);
  3. fsModuleCachePath set anywhere in the resolved config, including a project's own config;
  4. <cwd>/node_modules/.vitest-cache.

None of this applies to Vitest before 5, and if the helper fails or takes longer than a minute, the run goes ahead without it. To turn the cache off, pass --fsModuleCache=false or set fsModuleCache: false in the vitest config.

📊 Coverage mode

When --coverage (or coverageQuiet: true) is passed, the runner uses a blob-per-file strategy:

  1. Each file receives --coverage --reporter=blob with its own temp output directory.
  2. After all files complete, vitest --mergeReports combines the blobs into a single report.
  3. 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).

When no files are measured

If the coverage include matches no files (for example, a scaffold repo with a test but no source yet), istanbul reports every percentage as Unknown rather than a number. The runner prints those metrics as Unknown — Coverage Unknown% lines | Unknown% statements | … — adds a note that no files were measured, and exits 0 if the tests passed. The same applies to any individual non-numeric percentage in the worst-coverage table.

Coverage thresholds are skipped in this case, because there is nothing to measure: vitest's own threshold check does not fail on an Unknown percentage, and the runner does not add a check of its own. Thresholds apply again as soon as at least one file is measured. The JSON report's coverageSummary.noFilesMeasured is true for such a run.

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 --coverage

The 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.cjs

Files 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

dist/                  ← built library entry (npm run build / tsup) — generated, not committed
  index.mjs            ← bundled ESM entry
  index.cjs            ← thin CJS entry, copied verbatim from src/index.cjs; require()s index.mjs
bin/                   ← built CLI binary (npm run build / tsup) — generated, not committed
  vitest-runner.mjs    ← bundled CLI, from src/bin/vitest-runner.mjs (shebang preserved)
src/
  index.mjs            ← main run() API + re-exports
  index.cjs            ← CommonJS entry source (copied to dist/index.cjs by the build)
  bin/
    vitest-runner.mjs  ← CLI entry SOURCE — run this directly for source-level dev/testing
  utils/
    ansi.mjs           ← stripAnsi, colourPct
    duration.mjs       ← formatDuration
    env.mjs            ← buildNodeOptions, buildEnv
    resolve.mjs        ← resolveBin, resolveVitestConfig
  core/
    discover.mjs       ← discoverVitestFiles, sortWithPriority, computeFilterConflicts
    parse.mjs          ← parseVitestOutput, deduplicateErrors
    spawn.mjs          ← runSingleFile, runVitestDirect, runMergeReports
    report.mjs         ← printCoverageSummary, printMergeOutput
    progress.mjs       ← createCoverageProgressTracker
    scratch.mjs        ← makeRunTmpDir + the scratch-directory lifecycle
    fs-cache.mjs       ← prepareFsModuleCache: Vitest module cache path + one-time lockfile check
    fs-cache-helper.mjs ← helper process started by fs-cache.mjs (built to dist/ and bin/)
  cli/
    args.mjs           ← parseArguments
    help.mjs           ← showHelp

src/ is not published — only dist/, bin/, and types/ ship (see Programmatic API for the vitest-runner-dev export condition, used when developing against a workspace/local checkout instead of the published package). All sub-module utilities are re-exported from the root entry point, so deep imports are optional.


📚 Documentation

CodeFactor OpenSSF Scorecard npms.io score npm unpacked size Repo size


🤝 Contributing

Bug reports and pull requests are welcome on GitHub. Pull requests target the next branch; releases ship from next to master.

Contributors Sponsor shinrai



📄 License

npm license

Apache-2.0 © Shinrai / CLDMV. See LICENSE for the full text.

版本与发布记录

@cldmv/vitest-runner 共发布 18 个版本,首次发布于 2026/03/01;过去 12 个月发布了 18 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.6.1latest2026/11/027 天前
1.6.02026/11/028 天前
1.5.62026/10/2714 天前
1.5.52026/10/2714 天前
1.5.42026/10/051 个月前
1.5.32026/10/041 个月前

关于 @cldmv/vitest-runner 的常见问题

@cldmv/vitest-runner 是什么?

Sequential Vitest runner to avoid OOM issues with large test suites

如何安装 @cldmv/vitest-runner?

在项目目录运行 npm install @cldmv/vitest-runner,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:vitest(>=1.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 vitest-runner,可以用 npx vitest-runner 直接运行。

@cldmv/vitest-runner 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cldmv/vitest-runner 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@cldmv/vitest-runner 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cldmv/vitest-runner 可以商用吗?

@cldmv/vitest-runner 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@cldmv/vitest-runner 的下载量有多少?

根据 npm 官方下载统计,@cldmv/vitest-runner 在 2026/10/10 – 2026/11/08 这 30 天内被下载 10,023 次,日均约 334 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cldmv/vitest-runner 还在维护吗?

最新版本 1.6.1 发布于 2026/11/02(7 天前);至今共发布 18 个版本,其中过去 12 个月发布了 18 个;首次发布于 2026/03/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。