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 +342 -0
- package/bin/vitest-runner.mjs +86 -0
- package/index.cjs +17 -0
- package/index.mjs +5 -0
- package/package.json +60 -0
- package/src/cli/args.mjs +110 -0
- package/src/cli/help.mjs +80 -0
- package/src/core/discover.mjs +167 -0
- package/src/core/parse.mjs +167 -0
- package/src/core/progress.mjs +164 -0
- package/src/core/report.mjs +211 -0
- package/src/core/spawn.mjs +219 -0
- package/src/runner.mjs +504 -0
- package/src/utils/ansi.mjs +32 -0
- package/src/utils/duration.mjs +25 -0
- package/src/utils/env.mjs +38 -0
- package/src/utils/resolve.mjs +86 -0
- package/types/index.d.mts +1 -0
- package/types/src/cli/args.d.mts +56 -0
- package/types/src/cli/help.d.mts +7 -0
- package/types/src/core/discover.d.mts +75 -0
- package/types/src/core/parse.d.mts +73 -0
- package/types/src/core/progress.d.mts +30 -0
- package/types/src/core/report.d.mts +47 -0
- package/types/src/core/spawn.d.mts +114 -0
- package/types/src/runner.d.mts +97 -0
- package/types/src/utils/ansi.d.mts +22 -0
- package/types/src/utils/duration.d.mts +13 -0
- package/types/src/utils/env.d.mts +25 -0
- package/types/src/utils/resolve.d.mts +32 -0
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
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
|
+
}
|
package/src/cli/args.mjs
ADDED
|
@@ -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
|
+
}
|