vitest-runner 1.0.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 ADDED
@@ -0,0 +1,342 @@
1
+ # vitest-runner
2
+
3
+ Sequential Vitest runner that spawns each test file in its own child process to avoid out-of-memory crashes in large test suites.
4
+
5
+ - Runs files one-at-a-time or in a configurable parallel worker pool
6
+ - Supports full coverage mode via blob-per-file + `--mergeReports` (no OOM)
7
+ - Auto-detects your vitest config; accepts an explicit path if needed
8
+ - All standard Vitest CLI flags are forwarded unchanged
9
+ - Usable as a **CLI binary** or as a **programmatic Node.js API**
10
+ - Pure ESM with a CJS shim for `require()` compatibility
11
+
12
+ [![npm version]][npm_version_url] [![npm downloads]][npm_downloads_url] <!-- [![GitHub release]][github_release_url] -->[![GitHub downloads]][github_downloads_url] [![Last commit]][last_commit_url] <!-- [![Release date]][release_date_url] -->[![npm last update]][npm_last_update_url] [![Coverage]][coverage_url]
13
+
14
+ [![Contributors]][contributors_url] [![Sponsor shinrai]][sponsor_url]
15
+
16
+ ---
17
+
18
+ ## Requirements
19
+
20
+ - Node.js ≥ 18
21
+ - `vitest` ≥ 1.0 (peer dependency, installed in your project)
22
+ - `chalk` (bundled dependency — no action needed)
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ ```sh
29
+ npm install --save-dev vitest-runner
30
+ ```
31
+
32
+ Or to use the CLI globally:
33
+
34
+ ```sh
35
+ npm install -g vitest-runner
36
+ ```
37
+
38
+ ---
39
+
40
+ ## CLI usage
41
+
42
+ ```sh
43
+ vitest-runner [OPTIONS] [PATTERNS...]
44
+ ```
45
+
46
+ ### Runner flags
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; implies `--coverage-quiet` when set without it. Defaults to `coverage/coverage-run.log` when `--coverage-quiet` is active |
57
+ | `--help`, `-h` | Print this help and exit |
58
+
59
+ ### Test patterns
60
+
61
+ Patterns are resolved against `cwd`. Any of the following forms work:
62
+
63
+ ```sh
64
+ # Absolute or relative file path
65
+ vitest-runner src/tests/config/background.test.vitest.mjs
66
+
67
+ # Partial path or filename — matched against all discovered test files
68
+ vitest-runner background.test.vitest.mjs
69
+ vitest-runner config/background.test.vitest.mjs
70
+
71
+ # Directory — all test files inside it are run
72
+ vitest-runner src/tests/metadata
73
+ ```
74
+
75
+ Multiple patterns can be combined:
76
+
77
+ ```sh
78
+ vitest-runner src/tests/config src/tests/metadata
79
+ ```
80
+
81
+ ### Vitest passthrough flags
82
+
83
+ All unrecognised flags are forwarded verbatim to every vitest child process:
84
+
85
+ ```sh
86
+ vitest-runner --reporter=verbose
87
+ vitest-runner -t "lazy materialization"
88
+ vitest-runner --coverage
89
+ vitest-runner --bail
90
+ ```
91
+
92
+ ### Environment variables
93
+
94
+ | Variable | Default | Description |
95
+ |----------|---------|-------------|
96
+ | `VITEST_HEAP_MB` | *(none)* | `--max-old-space-size` ceiling passed to every child process |
97
+ | `VITEST_WORKERS` | `4` | Maximum parallel worker slots in the non-solo phase (overridden by `--workers`) |
98
+
99
+ ### Examples
100
+
101
+ ```sh
102
+ # Run all test files discovered under the default testDir
103
+ vitest-runner
104
+
105
+ # Run all tests, filter by name
106
+ vitest-runner -t "should handle null input"
107
+
108
+ # Run a specific folder
109
+ vitest-runner src/tests/auth
110
+
111
+ # Run with coverage (blob + merge — OOM-safe)
112
+ vitest-runner --coverage
113
+
114
+ # Coverage with quiet output and live progress bar (ideal for CI)
115
+ vitest-runner --coverage --coverage-quiet
116
+
117
+ # Run only files listed in a JSON file
118
+ vitest-runner --test-list my-tests.json
119
+
120
+ # Use a custom file discovery pattern
121
+ vitest-runner --file-pattern '\.spec\.ts$'
122
+
123
+ # Run 2 workers, with certain files running solo first
124
+ vitest-runner --workers 2 --solo-pattern heavy/ --solo-pattern listener-cleanup/
125
+
126
+ # Custom heap and worker count
127
+ VITEST_HEAP_MB=8192 vitest-runner --workers 2 src/tests/heavy
128
+
129
+ # Suppress error details in the summary
130
+ vitest-runner --no-error-details
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Programmatic API
136
+
137
+ ```js
138
+ import { run } from 'vitest-runner';
139
+
140
+ // CommonJS
141
+ const { run } = await require('vitest-runner');
142
+ ```
143
+
144
+ ### `run(options)` → `Promise<number>`
145
+
146
+ Runs the test suite and resolves with an exit code (`0` = all passed, `1` = any failure). Does **not** call `process.exit` — that is the caller's responsibility.
147
+
148
+ ```js
149
+ import { run } from 'vitest-runner';
150
+
151
+ const code = await run({
152
+ cwd: process.cwd(),
153
+ testDir: 'src/tests',
154
+ });
155
+
156
+ process.exit(code);
157
+ ```
158
+
159
+ #### Options
160
+
161
+ | Option | Type | Default | Description |
162
+ |--------|------|---------|-------------|
163
+ | `cwd` | `string` | **required** | Absolute project root directory |
164
+ | `testDir` | `string` | `cwd` | Directory (absolute or relative to `cwd`) to scan for `*.test.vitest.{js,mjs}` files |
165
+ | `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` |
166
+ | `testPatterns` | `string[]` | `[]` | File / folder patterns to filter — empty means all files in `testDir` |
167
+ | `testListFile` | `string` | `undefined` | Path to a JSON array of test file paths; when set, scanning is skipped entirely |
168
+ | `testFilePattern` | `RegExp` | `DEFAULT_TEST_FILE_PATTERN` | Regex matched against file names during discovery (`*.test.vitest.{js,mjs,cjs}` by default) |
169
+ | `vitestArgs` | `string[]` | `[]` | Extra CLI args forwarded verbatim to every vitest invocation |
170
+ | `showErrorDetails` | `boolean` | `true` | Print inline error blocks under each failed file in the summary |
171
+ | `coverageQuiet` | `boolean` | `false` | Suppress per-file output; show only the progress bar and final summaries |
172
+ | `workers` | `number` | `4` | Maximum parallel worker slots (overrides `VITEST_WORKERS`) |
173
+ | `worstCoverageCount` | `number` | `10` | Rows in the worst-coverage table after a coverage run (`0` disables it) |
174
+ | `maxOldSpaceMb` | `number` | `undefined` | Global `--max-old-space-size` ceiling in MB (overrides `VITEST_HEAP_MB`) |
175
+ | `earlyRunPatterns` | `string[]` | `[]` | Path substrings — matching files run solo (one at a time) before the parallel worker pool starts |
176
+ | `perFileHeapOverrides` | `PerFileHeapOverride[]` | `[]` | Per-file minimum heap ceilings; the maximum of this and `maxOldSpaceMb` wins |
177
+ | `conditions` | `string[]` | `[]` | Additional `--conditions` Node flags forwarded to children |
178
+ | `nodeEnv` | `string` | `'development'` | Value written to `NODE_ENV` in child processes |
179
+
180
+ #### `PerFileHeapOverride`
181
+
182
+ ```ts
183
+ { pattern: string; heapMb: number }
184
+ ```
185
+
186
+ `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.
187
+
188
+ #### Examples
189
+
190
+ ```js
191
+ // Run all tests under src/tests/
192
+ await run({ cwd: process.cwd(), testDir: 'src/tests' });
193
+
194
+ // Run only the config and metadata suites
195
+ await run({
196
+ cwd: process.cwd(),
197
+ testDir: 'src/tests',
198
+ testPatterns: ['src/tests/config', 'src/tests/metadata'],
199
+ });
200
+
201
+ // Coverage run (OOM-safe blob + merge mode)
202
+ await run({
203
+ cwd: process.cwd(),
204
+ testDir: 'src/tests',
205
+ vitestArgs: ['--coverage'],
206
+ });
207
+
208
+ // Quiet coverage with live progress bar
209
+ await run({
210
+ cwd: process.cwd(),
211
+ testDir: 'src/tests',
212
+ coverageQuiet: true,
213
+ });
214
+
215
+ // Give heap-heavy files a larger ceiling while keeping the global limit lower
216
+ await run({
217
+ cwd: process.cwd(),
218
+ testDir: 'src/tests',
219
+ maxOldSpaceMb: 2048,
220
+ earlyRunPatterns: ['listener-cleanup/'],
221
+ perFileHeapOverrides: [
222
+ { pattern: 'listener-cleanup/', heapMb: 6144 },
223
+ ],
224
+ });
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Coverage mode
230
+
231
+ When `--coverage` (or `coverageQuiet: true`) is passed, the runner uses a blob-per-file strategy:
232
+
233
+ 1. Each file receives `--coverage --reporter=blob` with its own temp output directory.
234
+ 2. After all files complete, `vitest --mergeReports` combines the blobs into a single report.
235
+ 3. Temporary blob and coverage-tmp directories are cleaned up automatically.
236
+
237
+ This avoids the OOM crash that occurs when a single vitest process holds coverage data for thousands of files simultaneously.
238
+
239
+ ### Coverage quiet mode
240
+
241
+ `--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.
242
+
243
+ The log file path can be overridden with `--log-file <path>`. Passing `--log-file` alone (without `--coverage-quiet`) also enables quiet mode and log mirroring.
244
+
245
+ ---
246
+
247
+ ## Test list files
248
+
249
+ A test list file is a plain JSON array of test file paths (relative to `cwd`):
250
+
251
+ ```json
252
+ [
253
+ "src/tests/auth/login.test.vitest.mjs",
254
+ "src/tests/auth/register.test.vitest.mjs",
255
+ "src/tests/config/defaults.test.vitest.mjs"
256
+ ]
257
+ ```
258
+
259
+ Pass `--test-list <file>` (CLI) or `testListFile: 'path/to/list.json'` (API) to run exactly those files instead of scanning `testDir`.
260
+
261
+ ---
262
+
263
+ ## Test file naming
264
+
265
+ By default, the runner discovers files matching:
266
+
267
+ ```
268
+ *.test.vitest.js
269
+ *.test.vitest.mjs
270
+ *.test.vitest.cjs
271
+ ```
272
+
273
+ Files in `node_modules` or hidden directories (names starting with `.`) are always skipped.
274
+
275
+ The pattern can be overridden with `--file-pattern <regex>` (CLI) or the `testFilePattern` option (API):
276
+
277
+ ```sh
278
+ # Match .spec.ts files instead
279
+ vitest-runner --file-pattern '\.spec\.ts$'
280
+ ```
281
+
282
+ ```js
283
+ await run({ cwd, testDir: 'src', testFilePattern: /\.spec\.ts$/i });
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Source layout
289
+
290
+ ```
291
+ index.mjs ← ESM entry (re-exports src/runner.mjs)
292
+ index.cjs ← CJS shim (dynamic import of index.mjs)
293
+ bin/
294
+ vitest-runner.mjs ← CLI binary
295
+ src/
296
+ runner.mjs ← main run() API + re-exports
297
+ utils/
298
+ ansi.mjs ← stripAnsi, colourPct
299
+ duration.mjs ← formatDuration
300
+ env.mjs ← buildNodeOptions
301
+ resolve.mjs ← resolveBin, resolveVitestConfig
302
+ core/
303
+ discover.mjs ← discoverVitestFiles, sortWithPriority
304
+ parse.mjs ← parseVitestOutput, deduplicateErrors
305
+ spawn.mjs ← runSingleFile, runVitestDirect, runMergeReports
306
+ report.mjs ← printCoverageSummary, printMergeOutput
307
+ progress.mjs ← createCoverageProgressTracker
308
+ cli/
309
+ args.mjs ← parseArguments
310
+ help.mjs ← showHelp
311
+ ```
312
+
313
+ All sub-module utilities are re-exported from the root entry point, so deep imports are optional.
314
+
315
+ ---
316
+
317
+ ## License
318
+
319
+ MIT
320
+
321
+ <!-- Badge definitions -->
322
+ <!-- [github release]: https://img.shields.io/github/v/release/CLDMV/vitest-runner?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 -->
323
+ <!-- [github_release_url]: https://github.com/CLDMV/vitest-runner/releases -->
324
+ <!-- [release date]: https://img.shields.io/github/release-date/CLDMV/vitest-runner?style=for-the-badge&logo=github&logoColor=white&labelColor=181717 -->
325
+ <!-- [release_date_url]: https://github.com/CLDMV/vitest-runner/releases -->
326
+
327
+ [npm version]: https://img.shields.io/npm/v/vitest-runner.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
328
+ [npm_version_url]: https://www.npmjs.com/package/vitest-runner
329
+ [npm downloads]: https://img.shields.io/npm/dm/vitest-runner.svg?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
330
+ [npm_downloads_url]: https://www.npmjs.com/package/vitest-runner
331
+ [github downloads]: https://img.shields.io/github/downloads/CLDMV/vitest-runner/total?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
332
+ [github_downloads_url]: https://github.com/CLDMV/vitest-runner/releases
333
+ [last commit]: https://img.shields.io/github/last-commit/CLDMV/vitest-runner?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
334
+ [last_commit_url]: https://github.com/CLDMV/vitest-runner/commits
335
+ [npm last update]: https://img.shields.io/npm/last-update/vitest-runner?style=for-the-badge&logo=npm&logoColor=white&labelColor=CB3837
336
+ [npm_last_update_url]: https://www.npmjs.com/package/vitest-runner
337
+ [coverage]: https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FCLDMV%2Fvitest-runner%2Fbadges%2Fcoverage.json&style=for-the-badge&logo=vitest&logoColor=white
338
+ [coverage_url]: https://github.com/CLDMV/vitest-runner/blob/badges/coverage.json
339
+ [contributors]: https://img.shields.io/github/contributors/CLDMV/vitest-runner.svg?style=for-the-badge&logo=github&logoColor=white&labelColor=181717
340
+ [contributors_url]: https://github.com/CLDMV/vitest-runner/graphs/contributors
341
+ [sponsor shinrai]: https://img.shields.io/github/sponsors/shinrai?style=for-the-badge&logo=githubsponsors&logoColor=white&labelColor=EA4AAA&label=Sponsor
342
+ [sponsor_url]: https://github.com/sponsors/shinrai
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview CLI entry point for the vitest-runner binary.
4
+ * @module vitest-runner/bin/vitest-runner
5
+ *
6
+ * Mirrors its own output to a log file when --coverage-quiet is set, then
7
+ * delegates all logic to `src/runner.mjs` via the programmatic `run()` API.
8
+ */
9
+
10
+ import { createWriteStream, mkdirSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { parseArguments } from "../src/cli/args.mjs";
13
+ import { showHelp } from "../src/cli/help.mjs";
14
+ import { run } from "../src/runner.mjs";
15
+ import { stripAnsi } from "../src/utils/ansi.mjs";
16
+
17
+ const args = parseArguments(process.argv.slice(2));
18
+
19
+ if (args.help) {
20
+ showHelp();
21
+ process.exit(0);
22
+ }
23
+
24
+ // Mirror all output (excluding progress bar lines) to a log file
25
+ // when running in --coverage-quiet mode (or when --log-file is set).
26
+ if (args.coverageQuiet || args.logFile) {
27
+ const cwd = process.cwd();
28
+ const resolvedLogFile = args.logFile
29
+ ? path.isAbsolute(args.logFile)
30
+ ? args.logFile
31
+ : path.resolve(cwd, args.logFile)
32
+ : path.join(cwd, "coverage", "coverage-run.log");
33
+ mkdirSync(path.dirname(resolvedLogFile), { recursive: true });
34
+ const logStream = createWriteStream(resolvedLogFile, { flags: "a" });
35
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
36
+ const origStderrWrite = process.stderr.write.bind(process.stderr);
37
+
38
+ /**
39
+ * Determine if a chunk is a progress-bar write that should be excluded from the log.
40
+ * TTY mode uses `\r` to overwrite in place; non-TTY prints "progress N.N% ..." lines.
41
+ * @param {Buffer|string} chunk
42
+ * @returns {boolean}
43
+ */
44
+ function isProgressChunk(chunk) {
45
+ const str = chunk.toString();
46
+ return str.startsWith("\r") || /^progress \d+\.\d+%/.test(str);
47
+ }
48
+
49
+ process.stdout.write = (chunk, enc, cb) => {
50
+ if (!isProgressChunk(chunk)) logStream.write(stripAnsi(chunk.toString()));
51
+ return origStdoutWrite(chunk, enc, cb);
52
+ };
53
+
54
+ process.stderr.write = (chunk, enc, cb) => {
55
+ if (!isProgressChunk(chunk)) logStream.write(stripAnsi(chunk.toString()));
56
+ return origStderrWrite(chunk, enc, cb);
57
+ };
58
+
59
+ process.on("exit", () => logStream.end());
60
+ }
61
+
62
+ const cwd = process.cwd();
63
+
64
+ const vitestArgs = [...args.vitestPassthroughArgs];
65
+ if ((args.coverageQuiet || args.logFile) && !vitestArgs.some((a) => a === "--coverage" || a.startsWith("--coverage."))) {
66
+ vitestArgs.unshift("--coverage");
67
+ }
68
+
69
+ run({
70
+ cwd,
71
+ testPatterns: args.testPatterns,
72
+ testListFile: args.testListFile,
73
+ testFilePattern: args.testFilePattern,
74
+ vitestArgs,
75
+ showErrorDetails: args.showErrorDetails,
76
+ coverageQuiet: args.coverageQuiet,
77
+ ...(args.workers !== undefined && { workers: args.workers }),
78
+ ...(args.soloPatterns.length > 0 && { earlyRunPatterns: args.soloPatterns })
79
+ })
80
+ .then((code) => {
81
+ process.exit(code);
82
+ })
83
+ .catch((err) => {
84
+ console.error("Fatal error:", err);
85
+ process.exit(1);
86
+ });
package/index.cjs ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @fileoverview CJS shim — dynamically imports the ESM entry point so that
3
+ * CommonJS callers can `require('vitest-runner')`.
4
+ *
5
+ * Because the package is pure ESM (`"type": "module"`) we cannot use `module.exports =`
6
+ * directly; instead we export a promise and re-attach named exports once resolved.
7
+ *
8
+ * @example
9
+ * // CommonJS usage
10
+ * const { run } = await require('vitest-runner');
11
+ */
12
+
13
+ "use strict";
14
+
15
+ // Async shim: re-export everything from the ESM module.
16
+ // Callers must await the result or use .then().
17
+ module.exports = import("./index.mjs");
package/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview ESM entry-point — re-exports the public API from src/runner.mjs.
3
+ * @module vitest-runner
4
+ */
5
+ export * from "./src/runner.mjs";
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "vitest-runner",
3
+ "version": "1.0.0",
4
+ "description": "Sequential Vitest runner to avoid OOM issues with large test suites",
5
+ "type": "module",
6
+ "main": "index.cjs",
7
+ "module": "index.mjs",
8
+ "types": "./types/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./types/index.d.mts",
12
+ "import": "./index.mjs",
13
+ "require": "./index.cjs"
14
+ }
15
+ },
16
+ "bin": {
17
+ "vitest-runner": "./bin/vitest-runner.mjs"
18
+ },
19
+ "files": [
20
+ "index.mjs",
21
+ "index.cjs",
22
+ "types/",
23
+ "src/",
24
+ "bin/"
25
+ ],
26
+ "scripts": {
27
+ "lint": "eslint src/ bin/ index.mjs",
28
+ "test": "vitest run --config .configs/vitest.config.mjs",
29
+ "test:watch": "vitest --config .configs/vitest.config.mjs",
30
+ "test:coverage": "vitest run --coverage --config .configs/vitest.config.mjs",
31
+ "ci:coverage": "vitest run --coverage --reporter=dot --maxWorkers=1 --config .configs/vitest.config.mjs",
32
+ "types:build": "tsc -p .configs/tsconfig.json",
33
+ "types:check": "tsc -p .configs/tsconfig.json --noEmit"
34
+ },
35
+ "keywords": [
36
+ "vitest",
37
+ "test-runner",
38
+ "sequential",
39
+ "oom"
40
+ ],
41
+ "devDependencies": {
42
+ "@types/node": "^25.3.0",
43
+ "@vitest/coverage-v8": "^4.0.18",
44
+ "typescript": "^5.9.3",
45
+ "vitest": "^4.0.18"
46
+ },
47
+ "dependencies": {
48
+ "chalk": "^5.4.1"
49
+ },
50
+ "peerDependencies": {
51
+ "vitest": ">=1.0.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "license": "MIT"
60
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @fileoverview CLI argument parsing for the vitest-runner binary.
3
+ * @module vitest-runner/src/cli/args
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} ParsedArgs
8
+ * @property {string|undefined} testListFile - Path to a JSON file of test paths to run (`--test-list`).
9
+ * @property {boolean} showErrorDetails - `false` when `--no-error-details` was passed.
10
+ * @property {boolean} coverageQuiet - Whether `--coverage-quiet` was passed.
11
+ * @property {string|undefined} logFile - Path for the coverage run log (`--log-file`); defaults to `coverage/coverage-run.log`.
12
+ * @property {boolean} help - Whether `--help` / `-h` was passed.
13
+ * @property {number|undefined} workers - Worker count from `--workers <n>`, or undefined.
14
+ * @property {string[]} soloPatterns - Path substrings from `--solo-pattern <pattern>` (repeatable).
15
+ * @property {RegExp|undefined} testFilePattern - Compiled regex from `--file-pattern <regex>`, or undefined.
16
+ * @property {string[]} vitestPassthroughArgs - Flags forwarded verbatim to vitest.
17
+ * @property {string[]} testPatterns - Non-flag positional arguments (file / folder patterns).
18
+ */
19
+
20
+ /** Runner-owned flags that must not be forwarded to vitest. */
21
+ const RUNNER_FLAGS = new Set([
22
+ "--test-list",
23
+ "--no-error-details",
24
+ "--coverage-quiet",
25
+ "--log-file",
26
+ "--workers",
27
+ "--solo-pattern",
28
+ "--file-pattern",
29
+ "--help",
30
+ "-h"
31
+ ]);
32
+
33
+ /**
34
+ * Parse raw CLI arguments into structured runner options.
35
+ *
36
+ * Runner-specific flags are extracted; everything else (flags and their
37
+ * optional values) is forwarded to vitest as passthrough args.
38
+ * A flag that takes a value (where the next token does not start with `-`)
39
+ * consumes that token too.
40
+ *
41
+ * @param {string[]} args - Raw argument array (typically `process.argv.slice(2)`).
42
+ * @returns {ParsedArgs}
43
+ * @example
44
+ * parseArguments(['--test-list', 'tests.json', '--workers', '2', '--reporter=verbose']);
45
+ */
46
+ export function parseArguments(args) {
47
+ const vitestPassthroughArgs = [];
48
+ const testPatterns = [];
49
+ const soloPatterns = [];
50
+ let testListFile;
51
+ let showErrorDetails = true;
52
+ let coverageQuiet = false;
53
+ let logFile;
54
+ let workers;
55
+ let help = false;
56
+ let testFilePattern;
57
+ for (let i = 0; i < args.length; i++) {
58
+ const arg = args[i];
59
+
60
+ if (arg === "--test-list") {
61
+ testListFile = args[++i];
62
+ } else if (arg.startsWith("--test-list=")) {
63
+ testListFile = arg.slice("--test-list=".length);
64
+ } else if (arg === "--no-error-details") {
65
+ showErrorDetails = false;
66
+ } else if (arg === "--coverage-quiet") {
67
+ coverageQuiet = true;
68
+ } else if (arg === "--log-file") {
69
+ logFile = args[++i];
70
+ } else if (arg.startsWith("--log-file=")) {
71
+ logFile = arg.slice("--log-file=".length);
72
+ } else if (arg === "--workers") {
73
+ workers = parseInt(args[++i], 10);
74
+ } else if (arg.startsWith("--workers=")) {
75
+ workers = parseInt(arg.slice("--workers=".length), 10);
76
+ } else if (arg === "--solo-pattern") {
77
+ soloPatterns.push(args[++i]);
78
+ } else if (arg.startsWith("--solo-pattern=")) {
79
+ soloPatterns.push(arg.slice("--solo-pattern=".length));
80
+ } else if (arg === "--file-pattern") {
81
+ testFilePattern = new RegExp(args[++i], "i");
82
+ } else if (arg.startsWith("--file-pattern=")) {
83
+ testFilePattern = new RegExp(arg.slice("--file-pattern=".length), "i");
84
+ } else if (arg === "--help" || arg === "-h") {
85
+ help = true;
86
+ } else if ((arg.startsWith("--") || arg.startsWith("-")) && !RUNNER_FLAGS.has(arg)) {
87
+ vitestPassthroughArgs.push(arg);
88
+ // Consume the next token if it looks like a value (not another flag)
89
+ if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
90
+ vitestPassthroughArgs.push(args[++i]);
91
+ }
92
+ } else {
93
+ // Any remaining token (cannot start with '-'; those are caught above)
94
+ testPatterns.push(arg);
95
+ }
96
+ }
97
+
98
+ return {
99
+ testListFile,
100
+ showErrorDetails,
101
+ coverageQuiet,
102
+ logFile,
103
+ help,
104
+ workers,
105
+ soloPatterns,
106
+ testFilePattern,
107
+ vitestPassthroughArgs,
108
+ testPatterns
109
+ };
110
+ }