npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cldmv/vitest-runner

v1.5.6

Published

Sequential Vitest runner to avoid OOM issues with large test suites

Readme

@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.5.6 (October 2026)

  • Dev-tooling dependency update, nothing new in the package — all open dependency updates in one change: tsup 9 (with esbuild 0.28.2), Vitest 5.0.5 and the patch updates, plus an override that moves the dev-only katex to a fixed release. Both open Dependabot alerts are cleared, and the built files are byte-identical to v1.5.5 (#104).
  • View full v1.5.6 Changelog

Recent Releases

  • 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)
  • v1.5.3 (October 2026) — A bundler-friendly CommonJS entry that fails clearly on Node.js without require(esm), and engines.node raised to >=22.12.0 (Changelog)
  • v1.5.2 (October 2026) — chalk moves to 6.0.1, a skipped PR run can no longer satisfy ✅ Required PR Check, and the repository adopts the shared CLDMV fix-headers config (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.12.0 or later (engines.node is >=22.12.0, matching the chalk 6 dependency, the vitest 5 peer and the CI matrix). Node.js 20 is not supported from v1.5.3; stay on v1.5.1 there.
  • The package is an ES module and loads with import. require("@cldmv/vitest-runner") loads the ES module build synchronously, which needs Node.js ^20.19.0 or >=22.12.0; on older Node.js, use import() instead.
  • 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

| 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 | | --exclude <glob> | Directory / file glob, relative to cwd, that discovery never enters (e.g. tmp/**); 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 | | --keep-tmp | Keep 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, -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/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

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

# 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 ^20.19.0 or >=22.12.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) 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

| 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,cjs} 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) | | exclude | string[] | [] | Directory / file globs, relative to cwd, that discovery never enters (e.g. ['tmp/**']). Applies to both the default scan and partial-path pattern resolution | | 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 | | scratchDir | string | '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 | | keepTmp | boolean | false | Keep the run's scratch root instead of removing it on completion (normal exit, failure, or SIGINT/SIGTERM) | | fsModuleCachePath | string \| false | <cwd>/node_modules/.vitest-cache | Vitest 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


🔗 Links


📄 License

npm license

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