@cldmv/vitest-runner 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,20 +45,22 @@ vitest-runner [OPTIONS] [PATTERNS...]
45
45
 
46
46
  ### Runner flags
47
47
 
48
- | Flag | Description |
49
- |------|-------------|
50
- | `--test-list <file>` | Run only the files listed in a JSON array file instead of scanning |
51
- | `--file-pattern <regex>` | Override the file discovery regex (default: `\.test\.vitest\.(?:js\|mjs\|cjs)$`) |
52
- | `--workers <n>` | Number of parallel workers (default: `4` or `VITEST_WORKERS`) |
53
- | `--solo-pattern <pat>` | Run files matching this path substring solo (one at a time) before the worker pool; repeatable |
54
- | `--no-error-details` | Hide inline error blocks — show only counts in the summary |
55
- | `--coverage-quiet` | Implies `--coverage`; suppress per-file output and show only a live progress bar and final summaries |
56
- | `--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 |
57
- | `--suppress-file-output` | Suppress per-file runner output blocks in any mode |
58
- | `--suppress-passing-files` | Hide the `PASSED TEST FILES` section in the final summary |
59
- | `--no-top-summary` | Hide `TOP MEMORY USERS` and `TOP DURATION` summary sections |
60
- | `--json` | Print a JSON run report (no runner text output) |
61
- | `--help`, `-h` | Print this help and exit |
48
+ | Flag | Description |
49
+ | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | `--test-list <file>` | Run only the files listed in a JSON array file instead of scanning |
51
+ | `--file-pattern <regex>` | Override the file discovery regex (default: `\.test\.vitest\.(?:js\|mjs\|cjs)$`) |
52
+ | `--workers <n>` | Number of parallel workers (default: `4` or `VITEST_WORKERS`) |
53
+ | `--solo-pattern <pat>` | Run files matching this path substring solo (one at a time) before the worker pool; repeatable |
54
+ | `--no-error-details` | Hide inline error blocks — show only counts in the summary |
55
+ | `--coverage-quiet` | Implies `--coverage`; suppress per-file output and show only a live progress bar and final summaries |
56
+ | `--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 |
57
+ | `--suppress-file-output` | Suppress per-file runner output blocks in any mode |
58
+ | `--suppress-passing-files` | Hide the `PASSED TEST FILES` section in the final summary |
59
+ | `--no-top-summary` | Hide `TOP MEMORY USERS` and `TOP DURATION` summary sections |
60
+ | `--json` | Print a JSON run report (no runner text output) |
61
+ | `--blobs-dir <path>` | Directory for per-file coverage blobs (default: `.vitest-coverage-blobs`, relative to `cwd`) |
62
+ | `--no-merge-reports` | Produce the coverage blobs but skip the merge and summary, leaving them in `--blobs-dir` for an external merge step |
63
+ | `--help`, `-h` | Print this help and exit |
62
64
 
63
65
  ### Test patterns
64
66
 
@@ -95,10 +97,10 @@ vitest-runner --bail
95
97
 
96
98
  ### Environment variables
97
99
 
98
- | Variable | Default | Description |
99
- |----------|---------|-------------|
100
- | `VITEST_HEAP_MB` | *(none)* | `--max-old-space-size` ceiling passed to every child process |
101
- | `VITEST_WORKERS` | `4` | Maximum parallel worker slots in the non-solo phase (overridden by `--workers`) |
100
+ | Variable | Default | Description |
101
+ | ---------------- | -------- | ------------------------------------------------------------------------------- |
102
+ | `VITEST_HEAP_MB` | _(none)_ | `--max-old-space-size` ceiling passed to every child process |
103
+ | `VITEST_WORKERS` | `4` | Maximum parallel worker slots in the non-solo phase (overridden by `--workers`) |
102
104
 
103
105
  ### Examples
104
106
 
@@ -145,10 +147,10 @@ vitest-runner --json --no-top-summary
145
147
  ## Programmatic API
146
148
 
147
149
  ```js
148
- import { run } from 'vitest-runner';
150
+ import { run } from "vitest-runner";
149
151
 
150
152
  // CommonJS
151
- const { run } = await require('vitest-runner');
153
+ const { run } = await require("vitest-runner");
152
154
  ```
153
155
 
154
156
  ### `run(options)` → `Promise<number | object>`
@@ -156,10 +158,10 @@ const { run } = await require('vitest-runner');
156
158
  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.
157
159
 
158
160
  ```js
159
- import { run } from 'vitest-runner';
161
+ import { run } from "vitest-runner";
160
162
 
161
163
  const code = await run({
162
- testDir: 'src/tests',
164
+ testDir: "src/tests"
163
165
  });
164
166
 
165
167
  process.exit(code);
@@ -167,33 +169,38 @@ process.exit(code);
167
169
 
168
170
  #### Options
169
171
 
170
- | Option | Type | Default | Description |
171
- |--------|------|---------|-------------|
172
- | `cwd` | `string` | `process.cwd()` | Absolute project root directory |
173
- | `testDir` | `string` | `cwd` | Directory (absolute or relative to `cwd`) to scan for `*.test.vitest.{js,mjs}` files |
174
- | `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` |
175
- | `testPatterns` | `string[]` | `[]` | File / folder patterns to filter — empty means all files in `testDir` |
176
- | `testListFile` | `string` | `undefined` | Path to a JSON array of test file paths; when set, scanning is skipped entirely |
177
- | `testFilePattern` | `RegExp` | `DEFAULT_TEST_FILE_PATTERN` | Regex matched against file names during discovery (`*.test.vitest.{js,mjs,cjs}` by default) |
178
- | `vitestArgs` | `string[]` | `[]` | Extra CLI args forwarded verbatim to every vitest invocation |
179
- | `showErrorDetails` | `boolean` | `true` | Print inline error blocks under each failed file in the summary |
180
- | `coverageQuiet` | `boolean` | `false` | Suppress per-file output; show only the progress bar and final summaries |
181
- | `suppressFileOutput` | `boolean` | `false` | Suppress per-file runner output blocks in all modes |
182
- | `suppressPassingFiles` | `boolean` | `false` | Hide passed-file rows in the final summary |
183
- | `topSummary` | `boolean` | `true` | Show or hide top memory/duration summary sections (and JSON arrays) |
184
- | `json` | `boolean` | `false` | Return a JSON report object instead of printing text output |
185
- | `workers` | `number` | `4` | Maximum parallel worker slots (overrides `VITEST_WORKERS`) |
186
- | `worstCoverageCount` | `number` | `10` | Rows in the worst-coverage table after a coverage run (`0` disables it) |
187
- | `maxOldSpaceMb` | `number` | `undefined` | Global `--max-old-space-size` ceiling in MB (overrides `VITEST_HEAP_MB`) |
188
- | `earlyRunPatterns` | `string[]` | `[]` | Path substrings — matching files run solo (one at a time) before the parallel worker pool starts |
189
- | `perFileHeapOverrides` | `PerFileHeapOverride[]` | `[]` | Per-file minimum heap ceilings; the maximum of this and `maxOldSpaceMb` wins |
190
- | `conditions` | `string[]` | `[]` | Additional `--conditions` Node flags forwarded to children |
191
- | `nodeEnv` | `string` | `'development'` | Value written to `NODE_ENV` in child processes |
172
+ | Option | Type | Default | Description |
173
+ | ---------------------- | ----------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
174
+ | `cwd` | `string` | `process.cwd()` | Absolute project root directory |
175
+ | `testDir` | `string` | `cwd` | Directory (absolute or relative to `cwd`) to scan for `*.test.vitest.{js,mjs}` files |
176
+ | `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` |
177
+ | `testPatterns` | `string[]` | `[]` | File / folder patterns to filter — empty means all files in `testDir` |
178
+ | `testListFile` | `string` | `undefined` | Path to a JSON array of test file paths; when set, scanning is skipped entirely |
179
+ | `testFilePattern` | `RegExp` | `DEFAULT_TEST_FILE_PATTERN` | Regex matched against file names during discovery (`*.test.vitest.{js,mjs,cjs}` by default) |
180
+ | `vitestArgs` | `string[]` | `[]` | Extra CLI args forwarded verbatim to every vitest invocation |
181
+ | `showErrorDetails` | `boolean` | `true` | Print inline error blocks under each failed file in the summary |
182
+ | `coverageQuiet` | `boolean` | `false` | Suppress per-file output; show only the progress bar and final summaries |
183
+ | `suppressFileOutput` | `boolean` | `false` | Suppress per-file runner output blocks in all modes |
184
+ | `suppressPassingFiles` | `boolean` | `false` | Hide passed-file rows in the final summary |
185
+ | `topSummary` | `boolean` | `true` | Show or hide top memory/duration summary sections (and JSON arrays) |
186
+ | `json` | `boolean` | `false` | Return a JSON report object instead of printing text output |
187
+ | `workers` | `number` | `4` | Maximum parallel worker slots (overrides `VITEST_WORKERS`) |
188
+ | `worstCoverageCount` | `number` | `10` | Rows in the worst-coverage table after a coverage run (`0` disables it) |
189
+ | `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 |
190
+ | `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 |
191
+ | `maxOldSpaceMb` | `number` | `undefined` | Global `--max-old-space-size` ceiling in MB (overrides `VITEST_HEAP_MB`) |
192
+ | `earlyRunPatterns` | `string[]` | `[]` | Path substrings — matching files run solo (one at a time) before the parallel worker pool starts |
193
+ | `perFileHeapOverrides` | `PerFileHeapOverride[]` | `[]` | Per-file minimum heap ceilings; the maximum of this and `maxOldSpaceMb` wins |
194
+ | `conditions` | `string[]` | `[]` | Additional `--conditions` Node flags forwarded to children |
195
+ | `nodeEnv` | `string` | `'development'` | Value written to `NODE_ENV` in child processes |
192
196
 
193
197
  #### `PerFileHeapOverride`
194
198
 
195
199
  ```ts
196
- { pattern: string; heapMb: number }
200
+ {
201
+ pattern: string;
202
+ heapMb: number;
203
+ }
197
204
  ```
198
205
 
199
206
  `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.
@@ -202,42 +209,40 @@ process.exit(code);
202
209
 
203
210
  ```js
204
211
  // Run all tests under src/tests/ (cwd defaults to process.cwd())
205
- await run({ testDir: 'src/tests' });
212
+ await run({ testDir: "src/tests" });
206
213
 
207
214
  // Run only the config and metadata suites
208
215
  await run({
209
- testDir: 'src/tests',
210
- testPatterns: ['src/tests/config', 'src/tests/metadata'],
216
+ testDir: "src/tests",
217
+ testPatterns: ["src/tests/config", "src/tests/metadata"]
211
218
  });
212
219
 
213
220
  // Coverage run (OOM-safe blob + merge mode)
214
221
  await run({
215
- testDir: 'src/tests',
216
- vitestArgs: ['--coverage'],
222
+ testDir: "src/tests",
223
+ vitestArgs: ["--coverage"]
217
224
  });
218
225
 
219
226
  // Quiet coverage with live progress bar
220
227
  await run({
221
- testDir: 'src/tests',
222
- coverageQuiet: true,
228
+ testDir: "src/tests",
229
+ coverageQuiet: true
223
230
  });
224
231
 
225
232
  // Machine-readable output (no text logs)
226
233
  const report = await run({
227
- testDir: 'src/tests',
228
- json: true,
234
+ testDir: "src/tests",
235
+ json: true
229
236
  });
230
237
 
231
238
  console.log(report.exitCode);
232
239
 
233
240
  // Give heap-heavy files a larger ceiling while keeping the global limit lower
234
241
  await run({
235
- testDir: 'src/tests',
236
- maxOldSpaceMb: 2048,
237
- earlyRunPatterns: ['listener-cleanup/'],
238
- perFileHeapOverrides: [
239
- { pattern: 'listener-cleanup/', heapMb: 6144 },
240
- ],
242
+ testDir: "src/tests",
243
+ maxOldSpaceMb: 2048,
244
+ earlyRunPatterns: ["listener-cleanup/"],
245
+ perFileHeapOverrides: [{ pattern: "listener-cleanup/", heapMb: 6144 }]
241
246
  });
242
247
  ```
243
248
 
@@ -259,6 +264,28 @@ This avoids the OOM crash that occurs when a single vitest process holds coverag
259
264
 
260
265
  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).
261
266
 
267
+ ### Producing blobs for an external merge
268
+
269
+ 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.
270
+
271
+ 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:
272
+
273
+ ```js
274
+ // Node-mode blobs, no internal merge
275
+ await run({
276
+ testDir: "src/tests",
277
+ vitestArgs: ["--coverage"],
278
+ blobsDir: ".coverage-blobs/node",
279
+ mergeReports: false
280
+ });
281
+
282
+ // (separately produce browser-mode blobs into .coverage-blobs/browser)
283
+ // then merge both blob sets in a single external step:
284
+ // vitest --mergeReports .coverage-blobs --coverage
285
+ ```
286
+
287
+ The exit code still reflects test pass/fail; there is just no coverage-merge result to fold in.
288
+
262
289
  ---
263
290
 
264
291
  ## Test list files
@@ -266,11 +293,7 @@ The log file path can be overridden with `--log-file <path>`. Passing `--log-fil
266
293
  A test list file is a plain JSON array of test file paths (relative to `cwd`):
267
294
 
268
295
  ```json
269
- [
270
- "src/tests/auth/login.test.vitest.mjs",
271
- "src/tests/auth/register.test.vitest.mjs",
272
- "src/tests/config/defaults.test.vitest.mjs"
273
- ]
296
+ ["src/tests/auth/login.test.vitest.mjs", "src/tests/auth/register.test.vitest.mjs", "src/tests/config/defaults.test.vitest.mjs"]
274
297
  ```
275
298
 
276
299
  Pass `--test-list <file>` (CLI) or `testListFile: 'path/to/list.json'` (API) to run exactly those files instead of scanning `testDir`.
@@ -281,7 +304,7 @@ Pass `--test-list <file>` (CLI) or `testListFile: 'path/to/list.json'` (API) to
281
304
 
282
305
  By default, the runner discovers files matching:
283
306
 
284
- ```
307
+ ```text
285
308
  *.test.vitest.js
286
309
  *.test.vitest.mjs
287
310
  *.test.vitest.cjs
@@ -297,14 +320,14 @@ vitest-runner --file-pattern '\.spec\.ts$'
297
320
  ```
298
321
 
299
322
  ```js
300
- await run({ cwd, testDir: 'src', testFilePattern: /\.spec\.ts$/i });
323
+ await run({ cwd, testDir: "src", testFilePattern: /\.spec\.ts$/i });
301
324
  ```
302
325
 
303
326
  ---
304
327
 
305
328
  ## Source layout
306
329
 
307
- ```
330
+ ```text
308
331
  index.mjs ← ESM entry (re-exports src/runner.mjs)
309
332
  index.cjs ← CJS shim (dynamic import of index.mjs)
310
333
  bin/
@@ -79,6 +79,8 @@ try {
79
79
  suppressPassingFiles: args.suppressPassingFiles,
80
80
  topSummary: args.topSummary,
81
81
  json: args.json,
82
+ mergeReports: args.mergeReports,
83
+ ...(args.blobsDir !== undefined && { blobsDir: args.blobsDir }),
82
84
  ...(args.workers !== undefined && { workers: args.workers }),
83
85
  ...(args.soloPatterns.length > 0 && { earlyRunPatterns: args.soloPatterns })
84
86
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cldmv/vitest-runner",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Sequential Vitest runner to avoid OOM issues with large test suites",
5
5
  "type": "module",
6
6
  "main": "index.cjs",
@@ -24,7 +24,9 @@
24
24
  "bin/"
25
25
  ],
26
26
  "scripts": {
27
- "lint": "eslint src/ bin/ index.mjs",
27
+ "lint": "eslint --config .configs/eslint.config.mjs .",
28
+ "format": "prettier --write . --config .configs/.prettierrc",
29
+ "format:check": "prettier --check . --config .configs/.prettierrc",
28
30
  "test": "vitest run --config .configs/vitest.config.mjs",
29
31
  "test:watch": "vitest --config .configs/vitest.config.mjs",
30
32
  "test:coverage": "vitest run --coverage --config .configs/vitest.config.mjs",
@@ -39,10 +41,16 @@
39
41
  "oom"
40
42
  ],
41
43
  "devDependencies": {
44
+ "@eslint/js": "^10.0.1",
45
+ "@eslint/json": "^2.0.0",
46
+ "@eslint/markdown": "^8.0.2",
42
47
  "@types/node": "^25.3.0",
43
- "@vitest/coverage-v8": "^4.0.18",
48
+ "@vitest/coverage-v8": "^4.1.9",
49
+ "eslint": "^10.5.0",
50
+ "globals": "^17.6.0",
51
+ "prettier": "^3.8.4",
44
52
  "typescript": "^5.9.3",
45
- "vitest": "^4.0.18"
53
+ "vitest": "^4.1.9"
46
54
  },
47
55
  "dependencies": {
48
56
  "chalk": "^5.4.1"
@@ -51,7 +59,7 @@
51
59
  "vitest": ">=1.0.0"
52
60
  },
53
61
  "engines": {
54
- "node": ">=18.0.0"
62
+ "node": ">=20.19.0"
55
63
  },
56
64
  "publishConfig": {
57
65
  "access": "public"
package/src/cli/args.mjs CHANGED
@@ -13,6 +13,8 @@
13
13
  * @property {boolean} suppressPassingFiles - Suppress the passed-files section in the final summary (`--suppress-passing-files`).
14
14
  * @property {boolean} topSummary - Show top-summary sections for memory and duration (`true` by default, disabled by `--no-top-summary`).
15
15
  * @property {boolean} json - Emit a JSON run report instead of text output (`--json`).
16
+ * @property {string|undefined} blobsDir - Directory for per-file coverage blobs (`--blobs-dir`); defaults to `<cwd>/.vitest-coverage-blobs`.
17
+ * @property {boolean} mergeReports - `false` when `--no-merge-reports` was passed; leaves blobs in `blobsDir` without merging.
16
18
  * @property {boolean} help - Whether `--help` / `-h` was passed.
17
19
  * @property {number|undefined} workers - Worker count from `--workers <n>`, or undefined.
18
20
  * @property {string[]} soloPatterns - Path substrings from `--solo-pattern <pattern>` (repeatable).
@@ -31,6 +33,8 @@ const RUNNER_FLAGS = new Set([
31
33
  "--suppress-passing-files",
32
34
  "--no-top-summary",
33
35
  "--json",
36
+ "--blobs-dir",
37
+ "--no-merge-reports",
34
38
  "--workers",
35
39
  "--solo-pattern",
36
40
  "--file-pattern",
@@ -63,6 +67,8 @@ export function parseArguments(args) {
63
67
  let suppressPassingFiles = false;
64
68
  let topSummary = true;
65
69
  let json = false;
70
+ let blobsDir;
71
+ let mergeReports = true;
66
72
  let workers;
67
73
  let help = false;
68
74
  let testFilePattern;
@@ -89,6 +95,12 @@ export function parseArguments(args) {
89
95
  topSummary = false;
90
96
  } else if (arg === "--json") {
91
97
  json = true;
98
+ } else if (arg === "--blobs-dir") {
99
+ blobsDir = args[++i];
100
+ } else if (arg.startsWith("--blobs-dir=")) {
101
+ blobsDir = arg.slice("--blobs-dir=".length);
102
+ } else if (arg === "--no-merge-reports") {
103
+ mergeReports = false;
92
104
  } else if (arg === "--workers") {
93
105
  workers = parseInt(args[++i], 10);
94
106
  } else if (arg.startsWith("--workers=")) {
@@ -124,6 +136,8 @@ export function parseArguments(args) {
124
136
  suppressPassingFiles,
125
137
  topSummary,
126
138
  json,
139
+ blobsDir,
140
+ mergeReports,
127
141
  help,
128
142
  workers,
129
143
  soloPatterns,
package/src/cli/help.mjs CHANGED
@@ -21,7 +21,7 @@ ${chalk.bold("USAGE:")}
21
21
 
22
22
  ${chalk.bold("SPECIAL FLAGS:")}
23
23
  --test-list <file> Run only the files listed in a JSON array file
24
- --file-pattern <regex> Override the file discovery regex (default: \.test\.vitest\.(?:js|mjs|cjs)$)
24
+ --file-pattern <regex> Override the file discovery regex (default: \\.test\\.vitest\\.(?:js|mjs|cjs)$)
25
25
  --workers <n> Number of parallel workers (default: 4 or VITEST_WORKERS)
26
26
  --solo-pattern <pat> Run files matching this path substring solo first (repeatable)
27
27
  --no-error-details Hide detailed error output (show only counts)
@@ -31,6 +31,8 @@ ${chalk.bold("SPECIAL FLAGS:")}
31
31
  --suppress-passing-files Hide the PASSED TEST FILES section in the final summary
32
32
  --no-top-summary Hide TOP MEMORY USERS and TOP DURATION sections
33
33
  --json Emit JSON summary output instead of text logs
34
+ --blobs-dir <path> Directory for per-file coverage blobs (default: .vitest-coverage-blobs)
35
+ --no-merge-reports Leave coverage blobs in --blobs-dir without merging (for an external merge)
34
36
  --help, -h Show this help message
35
37
 
36
38
  ${chalk.bold("TEST PATTERNS:")}
@@ -116,7 +116,7 @@ export async function discoverVitestFiles(opts) {
116
116
  const content = await fs.readFile(resolvedListPath, "utf8");
117
117
  testList = JSON.parse(content);
118
118
  } catch (err) {
119
- throw new Error(`Failed to read test list file "${resolvedListPath}": ${err.message}`);
119
+ throw new Error(`Failed to read test list file "${resolvedListPath}": ${err.message}`, { cause: err });
120
120
  }
121
121
 
122
122
  if (!Array.isArray(testList)) {
@@ -157,8 +157,8 @@ export function createCoverageProgressTracker(total) {
157
157
  export const noopProgressTracker = {
158
158
  /** @returns {void} */
159
159
  onStart() {},
160
- /** @param {boolean} _failedRun @returns {void} */
161
- onComplete(_failedRun) {},
160
+ /** @param {boolean} ___failedRun @returns {void} */
161
+ onComplete(___failedRun) {},
162
162
  /** @returns {void} */
163
163
  finish() {}
164
164
  };
package/src/runner.mjs CHANGED
@@ -35,8 +35,6 @@ import { runSingleFile, runMergeReports } from "./core/spawn.mjs";
35
35
  import { deduplicateErrors } from "./core/parse.mjs";
36
36
  import { createCoverageProgressTracker, noopProgressTracker } from "./core/progress.mjs";
37
37
  import { printQuietCoverageFailureDetails, printMergeOutput, printCoverageSummary } from "./core/report.mjs";
38
- import { formatDuration } from "./utils/duration.mjs";
39
- import { colourPct } from "./utils/ansi.mjs";
40
38
 
41
39
  /**
42
40
  * @typedef {Object} PerFileHeapOverride
@@ -61,6 +59,8 @@ import { colourPct } from "./utils/ansi.mjs";
61
59
  * @property {boolean} [json=false] - Return a JSON run report instead of printing text output.
62
60
  * @property {number} [workers=4] - Maximum number of parallel worker slots.
63
61
  * @property {number} [worstCoverageCount=10] - Rows in the worst-coverage table (0 = disable).
62
+ * @property {string} [blobsDir] - Directory for per-file coverage blobs (default `<cwd>/.vitest-coverage-blobs`). Relative paths resolve against `cwd`. Always cleared at the start of a coverage run.
63
+ * @property {boolean} [mergeReports=true] - When `true`, blobs are merged via `vitest --mergeReports`, the coverage summary is printed, and `blobsDir` is deleted at the end. When `false`, the run stops after producing blobs: no merge, no summary, and `blobsDir` is left populated for an external merge step.
64
64
  * @property {number} [maxOldSpaceMb] - Global `--max-old-space-size` ceiling; per-file overrides may raise it.
65
65
  * @property {string[]} [earlyRunPatterns=[]] - Path substrings — matching files run solo before the worker pool.
66
66
  * @property {PerFileHeapOverride[]} [perFileHeapOverrides=[]] - Per-file minimum heap overrides.
@@ -118,6 +118,8 @@ export async function run(opts) {
118
118
  json = false,
119
119
  workers = parseInt(process.env.VITEST_WORKERS ?? "4", 10),
120
120
  worstCoverageCount = 10,
121
+ blobsDir: blobsDirOpt,
122
+ mergeReports = true,
121
123
  earlyRunPatterns = [],
122
124
  perFileHeapOverrides = [],
123
125
  conditions = [],
@@ -147,7 +149,7 @@ export async function run(opts) {
147
149
 
148
150
  // ─── COVERAGE MODE ───────────────────────────────────────────────────────────
149
151
  if (hasCoverage) {
150
- const blobsDir = path.resolve(cwd, ".vitest-coverage-blobs");
152
+ const blobsDir = path.resolve(cwd, blobsDirOpt ?? ".vitest-coverage-blobs");
151
153
  // Temp coverage dirs live OUTSIDE blobsDir — vitest --mergeReports errors on
152
154
  // any non-blob entry found inside the blobs directory.
153
155
  const coverageTmpBase = path.resolve(cwd, ".vitest-coverage-tmp");
@@ -321,28 +323,38 @@ export async function run(opts) {
321
323
  return 1;
322
324
  }
323
325
 
324
- if (!coverageQuiet && !suppressFileOutput && emitTextOutput) {
325
- console.log(`\n${"=".repeat(80)}`);
326
- console.log(`📊 Merging ${blobFiles.length} coverage blobs into final report...`);
327
- console.log("=".repeat(80));
328
- }
326
+ // When mergeReports is false, stop after producing the blobs: skip the merge
327
+ // step and the coverage summary, and leave blobsDir intact so an external step
328
+ // can merge these blobs together with a separately-produced blob set.
329
+ let mergeExitCode = 0;
330
+ let mergeOutput = "";
331
+ let coverageSummary = null;
329
332
 
330
- const { exitCode: mergeExitCode, output: mergeOutput } = await runMergeReports(blobsDir, {
331
- ...spawnBase,
332
- maxOldSpaceMb,
333
- extraCoverageArgs,
334
- quietOutput: coverageQuiet || json
335
- });
333
+ if (mergeReports) {
334
+ if (!coverageQuiet && !suppressFileOutput && emitTextOutput) {
335
+ console.log(`\n${"=".repeat(80)}`);
336
+ console.log(`📊 Merging ${blobFiles.length} coverage blobs into final report...`);
337
+ console.log("=".repeat(80));
338
+ }
336
339
 
337
- if (coverageQuiet && emitTextOutput) {
338
- printMergeOutput(mergeExitCode, mergeOutput);
339
- }
340
+ ({ exitCode: mergeExitCode, output: mergeOutput } = await runMergeReports(blobsDir, {
341
+ ...spawnBase,
342
+ maxOldSpaceMb,
343
+ extraCoverageArgs,
344
+ quietOutput: coverageQuiet || json
345
+ }));
340
346
 
341
- const coverageSummary = await printCoverageSummary(cwd, extraCoverageArgs, worstCoverageCount, { silent: json });
347
+ if (coverageQuiet && emitTextOutput) {
348
+ printMergeOutput(mergeExitCode, mergeOutput);
349
+ }
350
+
351
+ coverageSummary = await printCoverageSummary(cwd, extraCoverageArgs, worstCoverageCount, { silent: json });
352
+ }
342
353
 
343
- // Clean up blobs and temp dirs
354
+ // Clean up temp dirs always; only delete blobsDir when we consumed it via merge.
355
+ // With mergeReports:false the blobs must survive for the caller's external merge.
344
356
  await Promise.all([
345
- fs.rm(blobsDir, { recursive: true, force: true }).catch(() => {}),
357
+ ...(mergeReports ? [fs.rm(blobsDir, { recursive: true, force: true }).catch(() => {})] : []),
346
358
  fs.rm(coverageTmpBase, { recursive: true, force: true }).catch(() => {})
347
359
  ]);
348
360
 
@@ -45,6 +45,14 @@ export type ParsedArgs = {
45
45
  * - Emit a JSON run report instead of text output (`--json`).
46
46
  */
47
47
  json: boolean;
48
+ /**
49
+ * - Directory for per-file coverage blobs (`--blobs-dir`); defaults to `<cwd>/.vitest-coverage-blobs`.
50
+ */
51
+ blobsDir: string | undefined;
52
+ /**
53
+ * - `false` when `--no-merge-reports` was passed; leaves blobs in `blobsDir` without merging.
54
+ */
55
+ mergeReports: boolean;
48
56
  /**
49
57
  * - Whether `--help` / `-h` was passed.
50
58
  */
@@ -79,6 +79,14 @@ export type RunOptions = {
79
79
  * - Rows in the worst-coverage table (0 = disable).
80
80
  */
81
81
  worstCoverageCount?: number;
82
+ /**
83
+ * - Directory for per-file coverage blobs (default `<cwd>/.vitest-coverage-blobs`). Relative paths resolve against `cwd`. Always cleared at the start of a coverage run.
84
+ */
85
+ blobsDir?: string;
86
+ /**
87
+ * - When `true`, blobs are merged via `vitest --mergeReports`, the coverage summary is printed, and `blobsDir` is deleted at the end. When `false`, the run stops after producing blobs: no merge, no summary, and `blobsDir` is left populated for an external merge step.
88
+ */
89
+ mergeReports?: boolean;
82
90
  /**
83
91
  * - Global `--max-old-space-size` ceiling; per-file overrides may raise it.
84
92
  */