@cldmv/vitest-runner 1.1.0 → 1.4.2

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.
@@ -1,95 +1,1559 @@
1
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 or --log-file 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
2
 
19
- if (args.help) {
20
- showHelp();
21
- process.exit(0);
3
+ // src/bin/vitest-runner.mjs
4
+ import { createWriteStream, mkdirSync } from "fs";
5
+ import path6 from "path";
6
+
7
+ // src/cli/args.mjs
8
+ var RUNNER_FLAGS = /* @__PURE__ */ new Set([
9
+ "--test-list",
10
+ "--no-error-details",
11
+ "--coverage-quiet",
12
+ "--log-file",
13
+ "--suppress-file-output",
14
+ "--suppress-passing-files",
15
+ "--no-top-summary",
16
+ "--json",
17
+ "--blobs-dir",
18
+ "--no-merge-reports",
19
+ "--workers",
20
+ "--solo-pattern",
21
+ "--file-pattern",
22
+ "--keep-tmp",
23
+ "--scratch-dir",
24
+ "--help",
25
+ "-h"
26
+ ]);
27
+ function parseArguments(args2) {
28
+ const vitestPassthroughArgs = [];
29
+ const testPatterns = [];
30
+ const soloPatterns = [];
31
+ let testListFile;
32
+ let showErrorDetails = true;
33
+ let coverageQuiet = false;
34
+ let logFile;
35
+ let suppressFileOutput = false;
36
+ let suppressPassingFiles = false;
37
+ let topSummary = true;
38
+ let json = false;
39
+ let blobsDir;
40
+ let mergeReports = true;
41
+ let workers;
42
+ let help = false;
43
+ let testFilePattern;
44
+ let keepTmp = false;
45
+ let scratchDir;
46
+ for (let i = 0; i < args2.length; i++) {
47
+ const arg = args2[i];
48
+ if (arg === "--test-list") {
49
+ testListFile = args2[++i];
50
+ } else if (arg.startsWith("--test-list=")) {
51
+ testListFile = arg.slice("--test-list=".length);
52
+ } else if (arg === "--no-error-details") {
53
+ showErrorDetails = false;
54
+ } else if (arg === "--coverage-quiet") {
55
+ coverageQuiet = true;
56
+ } else if (arg === "--log-file") {
57
+ logFile = args2[++i];
58
+ } else if (arg.startsWith("--log-file=")) {
59
+ logFile = arg.slice("--log-file=".length);
60
+ } else if (arg === "--suppress-file-output") {
61
+ suppressFileOutput = true;
62
+ } else if (arg === "--suppress-passing-files") {
63
+ suppressPassingFiles = true;
64
+ } else if (arg === "--no-top-summary") {
65
+ topSummary = false;
66
+ } else if (arg === "--json") {
67
+ json = true;
68
+ } else if (arg === "--blobs-dir") {
69
+ blobsDir = args2[++i];
70
+ } else if (arg.startsWith("--blobs-dir=")) {
71
+ blobsDir = arg.slice("--blobs-dir=".length);
72
+ } else if (arg === "--no-merge-reports") {
73
+ mergeReports = false;
74
+ } else if (arg === "--workers") {
75
+ workers = parseInt(args2[++i], 10);
76
+ } else if (arg.startsWith("--workers=")) {
77
+ workers = parseInt(arg.slice("--workers=".length), 10);
78
+ } else if (arg === "--solo-pattern") {
79
+ soloPatterns.push(args2[++i]);
80
+ } else if (arg.startsWith("--solo-pattern=")) {
81
+ soloPatterns.push(arg.slice("--solo-pattern=".length));
82
+ } else if (arg === "--file-pattern") {
83
+ testFilePattern = new RegExp(args2[++i], "i");
84
+ } else if (arg.startsWith("--file-pattern=")) {
85
+ testFilePattern = new RegExp(arg.slice("--file-pattern=".length), "i");
86
+ } else if (arg === "--keep-tmp") {
87
+ keepTmp = true;
88
+ } else if (arg === "--scratch-dir") {
89
+ scratchDir = args2[++i];
90
+ } else if (arg.startsWith("--scratch-dir=")) {
91
+ scratchDir = arg.slice("--scratch-dir=".length);
92
+ } else if (arg === "--help" || arg === "-h") {
93
+ help = true;
94
+ } else if ((arg.startsWith("--") || arg.startsWith("-")) && !RUNNER_FLAGS.has(arg)) {
95
+ vitestPassthroughArgs.push(arg);
96
+ if (i + 1 < args2.length && !args2[i + 1].startsWith("-")) {
97
+ vitestPassthroughArgs.push(args2[++i]);
98
+ }
99
+ } else {
100
+ testPatterns.push(arg);
101
+ }
102
+ }
103
+ return {
104
+ testListFile,
105
+ showErrorDetails,
106
+ coverageQuiet,
107
+ logFile,
108
+ suppressFileOutput,
109
+ suppressPassingFiles,
110
+ topSummary,
111
+ json,
112
+ blobsDir,
113
+ mergeReports,
114
+ help,
115
+ workers,
116
+ soloPatterns,
117
+ testFilePattern,
118
+ vitestPassthroughArgs,
119
+ testPatterns,
120
+ keepTmp,
121
+ scratchDir
122
+ };
123
+ }
124
+
125
+ // src/cli/help.mjs
126
+ import chalk from "chalk";
127
+ function showHelp() {
128
+ console.log(`
129
+ ${chalk.bold("Vitest Sequential Runner")}
130
+ Runs each test file in its own Vitest process to avoid OOM issues.
131
+
132
+ ${chalk.bold("USAGE:")}
133
+ vitest-runner [OPTIONS] [PATTERNS]
134
+
135
+ ${chalk.bold("SPECIAL FLAGS:")}
136
+ --test-list <file> Run only the files listed in a JSON array file
137
+ --file-pattern <regex> Override the file discovery regex (default: \\.test\\.vitest\\.(?:js|mjs|cjs)$)
138
+ --workers <n> Number of parallel workers (default: 4 or VITEST_WORKERS)
139
+ --solo-pattern <pat> Run files matching this path substring solo first (repeatable)
140
+ --no-error-details Hide detailed error output (show only counts)
141
+ --coverage-quiet Implies --coverage; show progress bar + final summaries only
142
+ --log-file <path> Path for mirrored runner output (default: coverage/coverage-run.log with --coverage-quiet)
143
+ --suppress-file-output Suppress per-file runner output blocks
144
+ --suppress-passing-files Hide the PASSED TEST FILES section in the final summary
145
+ --no-top-summary Hide TOP MEMORY USERS and TOP DURATION sections
146
+ --json Emit JSON summary output instead of text logs
147
+ --blobs-dir <path> Directory for per-file coverage blobs (default: .vitest-coverage-blobs)
148
+ --no-merge-reports Leave coverage blobs in --blobs-dir without merging (for an external merge)
149
+ --keep-tmp Keep this run's scratch directory instead of removing it on completion
150
+ --scratch-dir <path> Per-run scratch root, relative to cwd (default: tmp/vitest-runner)
151
+ --help, -h Show this help message
152
+
153
+ ${chalk.bold("TEST PATTERNS:")}
154
+ [file] Run a specific test file (supports partial paths)
155
+ [folder] Run all tests in a folder
156
+
157
+ Examples:
158
+ src/tests/config/background.test.vitest.mjs
159
+ src/tests/metadata
160
+ background.test.vitest.mjs
161
+
162
+ ${chalk.bold("VITEST FLAGS:")}
163
+ All standard Vitest CLI flags are supported and passed through:
164
+ -t, --testNamePattern Filter tests by name pattern (regex)
165
+ --reporter Change reporter (verbose, dot, json, etc.)
166
+ --coverage Run full-suite coverage (blob-per-file + mergeReports)
167
+ --bail Stop on first failure
168
+
169
+ See the Vitest documentation for the full list.
170
+
171
+ ${chalk.bold("ENVIRONMENT VARIABLES:")}
172
+ VITEST_HEAP_MB Set max heap size per test (default: Node.js default)
173
+ VITEST_WORKERS Number of parallel workers (default: 4, overridden by --workers)
174
+ VITEST_RUNNER_TMP Set by the runner in each child \u2014 this run's per-file scratch
175
+ directory. Use makeRunTmpDir(label) from a test file instead
176
+ of reading it directly when you want a fresh subdirectory.
177
+ # Run all tests
178
+ vitest-runner
179
+
180
+ # Run a specific file (partial path ok)
181
+ vitest-runner src/tests/config/background.test.vitest.mjs
182
+
183
+ # Run only the files listed in a JSON file
184
+ vitest-runner --test-list my-tests.json
185
+
186
+ # Filter by test name
187
+ vitest-runner src/tests/metadata -t "lazy materialization"
188
+
189
+ # Run with 2 workers
190
+ vitest-runner --workers 2
191
+
192
+ # Run files matching a pattern solo first, then the rest in parallel
193
+ vitest-runner --solo-pattern listener-cleanup/ --solo-pattern heavy/
194
+
195
+ # Hide detailed errors
196
+ vitest-runner --no-error-details
197
+
198
+ # Coverage with quiet output + progress bar
199
+ vitest-runner --coverage --coverage-quiet
200
+
201
+ # Custom heap and workers
202
+ VITEST_HEAP_MB=8192 vitest-runner --workers 2 src/tests/heavy
203
+ `);
22
204
  }
23
205
 
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).
206
+ // src/runner.mjs
207
+ import fs5 from "fs/promises";
208
+ import path5 from "path";
209
+ import chalk4 from "chalk";
210
+
211
+ // src/utils/resolve.mjs
212
+ import { createRequire } from "module";
213
+ import path from "path";
214
+ import fs from "fs/promises";
215
+ import { pathToFileURL } from "url";
216
+ function resolveBin(cwd2, pkgName, binName = pkgName) {
217
+ const require2 = createRequire(pathToFileURL(path.join(cwd2, "package.json")));
218
+ const pkgJsonPath = require2.resolve(`${pkgName}/package.json`);
219
+ const pkg = require2(pkgJsonPath);
220
+ const rel = pkg.bin?.[binName] ?? pkg.bin;
221
+ if (!rel) throw new Error(`No bin "${binName}" found in ${pkgName}/package.json`);
222
+ return path.join(path.dirname(pkgJsonPath), rel);
223
+ }
224
+ var DEFAULT_CONFIG_NAMES = Object.freeze([
225
+ "vitest.config.ts",
226
+ "vitest.config.mts",
227
+ "vitest.config.cts",
228
+ "vitest.config.mjs",
229
+ "vitest.config.js",
230
+ "vitest.config.cjs",
231
+ "vite.config.ts",
232
+ "vite.config.mts",
233
+ "vite.config.mjs",
234
+ "vite.config.js"
235
+ ]);
236
+ async function resolveVitestConfig(cwd2, configPath) {
237
+ if (configPath) {
238
+ return path.isAbsolute(configPath) ? configPath : path.resolve(cwd2, configPath);
239
+ }
240
+ for (const name of DEFAULT_CONFIG_NAMES) {
241
+ const candidate = path.join(cwd2, name);
242
+ try {
243
+ await fs.access(candidate);
244
+ return candidate;
245
+ } catch {
246
+ }
247
+ }
248
+ return void 0;
249
+ }
250
+
251
+ // src/core/discover.mjs
252
+ import fs2 from "fs/promises";
253
+ import path2 from "path";
254
+ var DEFAULT_TEST_FILE_PATTERN = /\.test\.vitest\.(?:js|mjs|cjs)$/i;
255
+ async function discoverFilesInDir(dir, cwd2, pattern = DEFAULT_TEST_FILE_PATTERN) {
256
+ const queue = [dir];
257
+ const files = [];
258
+ while (queue.length) {
259
+ const current = queue.pop();
260
+ let entries;
261
+ try {
262
+ entries = await fs2.readdir(current, { withFileTypes: true });
263
+ } catch {
264
+ continue;
265
+ }
266
+ for (const entry of entries) {
267
+ if (entry.isDirectory()) {
268
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
269
+ queue.push(path2.join(current, entry.name));
270
+ continue;
271
+ }
272
+ if (entry.isFile() && pattern.test(entry.name)) {
273
+ files.push(path2.relative(cwd2, path2.join(current, entry.name)));
274
+ }
275
+ }
276
+ }
277
+ return files;
278
+ }
279
+ function sortWithPriority(files, earlyRunPatterns = []) {
280
+ const early = [];
281
+ const rest = [];
282
+ for (const file of files) {
283
+ const normalized = file.replace(/\\/g, "/");
284
+ const priorityIndex = earlyRunPatterns.findIndex((pat) => normalized.includes(pat));
285
+ if (priorityIndex !== -1) {
286
+ early.push({ file, priorityIndex });
287
+ } else {
288
+ rest.push(file);
289
+ }
290
+ }
291
+ early.sort((a, b) => a.priorityIndex - b.priorityIndex || a.file.localeCompare(b.file));
292
+ rest.sort((a, b) => a.localeCompare(b));
293
+ return [...early.map((e) => e.file), ...rest];
294
+ }
295
+ function computeFilterConflicts(files) {
296
+ const normalized = files.map((f) => f.replace(/\\/g, "/"));
297
+ const conflicts = /* @__PURE__ */ new Map();
298
+ for (let i = 0; i < files.length; i++) {
299
+ const matches = [];
300
+ for (let j = 0; j < files.length; j++) {
301
+ if (i === j) continue;
302
+ if (normalized[j].includes(normalized[i])) matches.push(files[j]);
303
+ }
304
+ conflicts.set(files[i], matches);
305
+ }
306
+ return conflicts;
307
+ }
308
+ async function discoverVitestFiles(opts) {
309
+ const { cwd: cwd2, testDir, testPatterns = [], testListFile, testFilePattern = DEFAULT_TEST_FILE_PATTERN, earlyRunPatterns = [] } = opts;
310
+ const resolvedTestDir = testDir ? path2.isAbsolute(testDir) ? testDir : path2.resolve(cwd2, testDir) : cwd2;
311
+ if (testListFile) {
312
+ const resolvedListPath = path2.isAbsolute(testListFile) ? testListFile : path2.resolve(cwd2, testListFile);
313
+ let testList;
314
+ try {
315
+ const content = await fs2.readFile(resolvedListPath, "utf8");
316
+ testList = JSON.parse(content);
317
+ } catch (err) {
318
+ throw new Error(`Failed to read test list file "${resolvedListPath}": ${err.message}`, { cause: err });
319
+ }
320
+ if (!Array.isArray(testList)) {
321
+ throw new Error(`Test list file "${resolvedListPath}" must contain a JSON array of test file paths`);
322
+ }
323
+ console.log(`\u{1F4CB} Loading test list from: ${path2.relative(cwd2, resolvedListPath)}`);
324
+ return sortWithPriority(testList, earlyRunPatterns);
325
+ }
326
+ if (testPatterns.length === 0) {
327
+ const files2 = await discoverFilesInDir(resolvedTestDir, cwd2, testFilePattern);
328
+ return sortWithPriority(files2, earlyRunPatterns);
329
+ }
330
+ const files = [];
331
+ for (const pattern of testPatterns) {
332
+ const absPath = path2.isAbsolute(pattern) ? pattern : path2.resolve(cwd2, pattern);
333
+ let stat = null;
334
+ try {
335
+ stat = await fs2.stat(absPath);
336
+ } catch {
337
+ }
338
+ if (stat?.isFile()) {
339
+ if (testFilePattern.test(absPath)) {
340
+ files.push(path2.relative(cwd2, absPath));
341
+ }
342
+ } else if (stat?.isDirectory()) {
343
+ files.push(...await discoverFilesInDir(absPath, cwd2, testFilePattern));
344
+ } else {
345
+ const allFiles = await discoverFilesInDir(resolvedTestDir, cwd2, testFilePattern);
346
+ const matched = allFiles.filter((f) => f.replace(/\\/g, "/").includes(pattern.replace(/\\/g, "/")));
347
+ if (matched.length > 0) {
348
+ files.push(...matched);
349
+ } else {
350
+ console.warn(`\u26A0\uFE0F No matches found for: ${pattern}`);
351
+ }
352
+ }
353
+ }
354
+ return sortWithPriority([...new Set(files)], earlyRunPatterns);
355
+ }
356
+
357
+ // src/core/spawn.mjs
358
+ import { spawn } from "child_process";
359
+
360
+ // src/utils/ansi.mjs
361
+ function stripAnsi(text) {
362
+ return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
363
+ }
364
+ function colourPct(chalk5, pct) {
365
+ const str = pct.toFixed(2).padStart(6);
366
+ if (pct >= 80) return chalk5.green(str);
367
+ if (pct >= 50) return chalk5.yellow(str);
368
+ return chalk5.red(str);
369
+ }
370
+
371
+ // src/core/parse.mjs
372
+ function parseVitestOutput(output) {
373
+ const cleanOutput = stripAnsi(output);
374
+ const result = {
375
+ testFilesPass: 0,
376
+ testFilesFail: 0,
377
+ testsPass: 0,
378
+ testsFail: 0,
379
+ testsSkip: 0,
380
+ duration: 0,
381
+ heapMb: null,
382
+ errors: []
383
+ };
384
+ const testFilesLineMatch = cleanOutput.match(/Test Files\s+(.+)/);
385
+ if (testFilesLineMatch) {
386
+ const line = testFilesLineMatch[1];
387
+ const passMatch = line.match(/(\d+)\s+passed/);
388
+ const failMatch = line.match(/(\d+)\s+failed/);
389
+ if (passMatch) result.testFilesPass = parseInt(passMatch[1], 10);
390
+ if (failMatch) result.testFilesFail = parseInt(failMatch[1], 10);
391
+ }
392
+ const testsLineMatch = cleanOutput.match(/^\s*Tests\s+(.+)$/m);
393
+ if (testsLineMatch) {
394
+ const line = testsLineMatch[1];
395
+ const passMatch = line.match(/(\d+)\s+passed/);
396
+ const failMatch = line.match(/(\d+)\s+failed/);
397
+ const skipMatch = line.match(/(\d+)\s+skipped/);
398
+ if (passMatch) result.testsPass = parseInt(passMatch[1], 10);
399
+ if (failMatch) result.testsFail = parseInt(failMatch[1], 10);
400
+ if (skipMatch) result.testsSkip = parseInt(skipMatch[1], 10);
401
+ }
402
+ const durationMatch = cleanOutput.match(/Duration\s+([\d.]+)s/);
403
+ if (durationMatch) {
404
+ result.duration = parseFloat(durationMatch[1]) * 1e3;
405
+ }
406
+ const heapMatch = cleanOutput.match(/(\d+)\s*MB\s+heap\s+used/i);
407
+ if (heapMatch) {
408
+ result.heapMb = parseInt(heapMatch[1], 10);
409
+ }
410
+ const failedSectionStart = output.indexOf("Failed Tests");
411
+ if (failedSectionStart !== -1) {
412
+ const failedSectionEnd = output.indexOf("\n Test Files", failedSectionStart);
413
+ const errorSection = failedSectionEnd !== -1 ? output.substring(failedSectionStart, failedSectionEnd) : output.substring(failedSectionStart);
414
+ const failPattern = /FAIL\s*(?:\x1B\[[0-9;]*[a-zA-Z]|\s)*tests\//g;
415
+ const matches = [...errorSection.matchAll(failPattern)];
416
+ for (let i = 0; i < matches.length; i++) {
417
+ const matchPos = matches[i].index;
418
+ const lineStart = errorSection.lastIndexOf("\n", matchPos);
419
+ const actualStart = lineStart === -1 ? 0 : lineStart;
420
+ const matchEnd = i < matches.length - 1 ? matches[i + 1].index : errorSection.length;
421
+ const nextLineStart = errorSection.lastIndexOf("\n", matchEnd);
422
+ const actualEnd = nextLineStart === -1 ? matchEnd : nextLineStart;
423
+ const errorBlock = errorSection.substring(actualStart, actualEnd).trim();
424
+ if (errorBlock) result.errors.push(errorBlock);
425
+ }
426
+ }
427
+ return result;
428
+ }
429
+ function deduplicateErrors(errors) {
430
+ const fullText = errors.join("\n");
431
+ const lines = fullText.split("\n");
432
+ const failLineMap = /* @__PURE__ */ new Map();
433
+ const lineIndices = /* @__PURE__ */ new Map();
434
+ lines.forEach((line, idx) => {
435
+ if (line.includes("FAIL") && line.includes("Config:")) {
436
+ lineIndices.set(line, idx);
437
+ const cleaned = stripAnsi(line);
438
+ const match = cleaned.match(/^(.+Config:\s+)'*([^'>]+)'*(.+)$/);
439
+ if (match) {
440
+ const [, before, config, after] = match;
441
+ const pattern = `${before.trim()}|||${after.trim()}`;
442
+ if (!failLineMap.has(pattern)) {
443
+ failLineMap.set(pattern, { lines: [], configs: [] });
444
+ }
445
+ failLineMap.get(pattern).lines.push(line);
446
+ failLineMap.get(pattern).configs.push(config.replace(/'/g, ""));
447
+ }
448
+ }
449
+ });
450
+ const skipIndices = /* @__PURE__ */ new Set();
451
+ for (const [, data] of failLineMap.entries()) {
452
+ if (data.configs.length > 1) {
453
+ for (let i = 1; i < data.lines.length; i++) {
454
+ const idx = lineIndices.get(data.lines[i]);
455
+ skipIndices.add(idx);
456
+ }
457
+ const firstLine = data.lines[0];
458
+ const configArray = `[${data.configs.map((c) => `'${c}'`).join(",")}]`;
459
+ const consolidated = stripAnsi(firstLine).replace(/(Config:\s+)'*[^'>]+'*/, `$1${configArray}`);
460
+ const firstIdx = lineIndices.get(firstLine);
461
+ lines[firstIdx] = consolidated;
462
+ }
463
+ }
464
+ return lines.filter((_, idx) => !skipIndices.has(idx)).join("\n");
465
+ }
466
+
467
+ // src/utils/env.mjs
468
+ function buildNodeOptions({ maxOldSpaceMb, conditions = [], base = process.env.NODE_OPTIONS ?? "" }) {
469
+ let opts = base;
470
+ for (const cond of conditions) {
471
+ const flag = `--conditions=${cond}`;
472
+ if (!opts.includes(flag)) {
473
+ opts = opts ? `${opts} ${flag}` : flag;
474
+ }
475
+ }
476
+ if (maxOldSpaceMb && !opts.includes("--max-old-space-size")) {
477
+ const flag = `--max-old-space-size=${maxOldSpaceMb}`;
478
+ opts = opts ? `${opts} ${flag}` : flag;
479
+ }
480
+ return opts.trim();
481
+ }
482
+
483
+ // src/core/spawn.mjs
484
+ function buildEnv({ maxOldSpaceMb, conditions = [], nodeEnv = "development", extraEnv = {} }) {
485
+ const env = { ...process.env };
486
+ if (!env.NODE_ENV) env.NODE_ENV = nodeEnv;
487
+ const nodeOptions = buildNodeOptions({ maxOldSpaceMb, conditions, base: env.NODE_OPTIONS ?? "" });
488
+ if (nodeOptions) env.NODE_OPTIONS = nodeOptions;
489
+ Object.assign(env, extraEnv);
490
+ return env;
491
+ }
492
+ function buildBaseArgs(vitestBin, vitestConfig) {
493
+ const configArgs = vitestConfig ? ["--config", vitestConfig] : [];
494
+ return [vitestBin, ...configArgs, "run"];
495
+ }
496
+ function runSingleFile(filePath, opts) {
497
+ const {
498
+ cwd: cwd2,
499
+ vitestBin,
500
+ vitestConfig,
501
+ maxOldSpaceMb,
502
+ conditions = [],
503
+ nodeEnv = "development",
504
+ vitestArgs: vitestArgs2 = [],
505
+ streamOutput = true,
506
+ // Other discovered files whose path would spuriously also match Vitest's
507
+ // own substring filter on `filePath` (see discover.mjs computeFilterConflicts).
508
+ // Excluded explicitly so this invocation runs exactly `filePath`.
509
+ excludePaths = [],
510
+ // Extra env vars for the child (e.g. VITEST_RUNNER_TMP — see core/scratch.mjs).
511
+ extraEnv = {}
512
+ } = opts;
513
+ return new Promise((resolve) => {
514
+ const startTime = Date.now();
515
+ const excludeArgs = excludePaths.flatMap((p) => ["--exclude", p]);
516
+ const args2 = [...buildBaseArgs(vitestBin, vitestConfig), ...vitestArgs2, ...excludeArgs, filePath];
517
+ const env = buildEnv({ maxOldSpaceMb, conditions, nodeEnv, extraEnv });
518
+ const child = spawn(process.execPath, args2, { cwd: cwd2, stdio: ["ignore", "pipe", "pipe"], env });
519
+ let stdout = "";
520
+ let stderr = "";
521
+ child.stdout?.on("data", (data) => {
522
+ stdout += data.toString();
523
+ if (streamOutput) process.stdout.write(data);
524
+ });
525
+ child.stderr?.on("data", (data) => {
526
+ stderr += data.toString();
527
+ if (streamOutput) process.stderr.write(data);
528
+ });
529
+ child.on("close", (code) => {
530
+ const spawnDuration = Date.now() - startTime;
531
+ const output = `${stdout}
532
+ ${stderr}`;
533
+ const parsed = parseVitestOutput(output);
534
+ resolve({
535
+ file: filePath,
536
+ code: code ?? 1,
537
+ // c8 ignore next -- false branch verified manually; V8 ternary probe mismatch inside object literal
538
+ duration: parsed.duration > 0 ? parsed.duration : spawnDuration,
539
+ testFilesPass: parsed.testFilesPass,
540
+ testFilesFail: parsed.testFilesFail,
541
+ testsPass: parsed.testsPass,
542
+ testsFail: parsed.testsFail,
543
+ testsSkip: parsed.testsSkip,
544
+ heapMb: parsed.heapMb,
545
+ errors: parsed.errors,
546
+ rawOutput: output
547
+ });
548
+ });
549
+ child.on("error", (err) => {
550
+ resolve({
551
+ file: filePath,
552
+ code: 1,
553
+ duration: Date.now() - startTime,
554
+ testFilesPass: 0,
555
+ testFilesFail: 1,
556
+ testsPass: 0,
557
+ testsFail: 0,
558
+ testsSkip: 0,
559
+ heapMb: null,
560
+ errors: [err.message],
561
+ rawOutput: err.toString()
562
+ });
563
+ });
564
+ });
565
+ }
566
+ function runMergeReports(blobsDir, opts) {
567
+ const {
568
+ cwd: cwd2,
569
+ vitestBin,
570
+ vitestConfig,
571
+ maxOldSpaceMb,
572
+ conditions = [],
573
+ nodeEnv = "development",
574
+ extraCoverageArgs = [],
575
+ quietOutput = false
576
+ } = opts;
577
+ return new Promise((resolve) => {
578
+ const configArgs = vitestConfig ? ["--config", vitestConfig] : [];
579
+ const mergeReporterArgs = quietOutput ? ["--color"] : [];
580
+ const args2 = [vitestBin, ...configArgs, "--mergeReports", blobsDir, "--run", "--coverage", ...mergeReporterArgs, ...extraCoverageArgs];
581
+ const env = buildEnv({ maxOldSpaceMb, conditions, nodeEnv });
582
+ const child = spawn(process.execPath, args2, {
583
+ cwd: cwd2,
584
+ stdio: quietOutput ? ["ignore", "pipe", "pipe"] : "inherit",
585
+ env
586
+ });
587
+ let stdout = "";
588
+ let stderr = "";
589
+ if (quietOutput) {
590
+ child.stdout?.on("data", (data) => stdout += data.toString());
591
+ child.stderr?.on("data", (data) => stderr += data.toString());
592
+ }
593
+ child.on("close", (code) => resolve({ exitCode: code ?? 1, output: `${stdout}
594
+ ${stderr}` }));
595
+ child.on("error", () => resolve({ exitCode: 1, output: "" }));
596
+ });
597
+ }
598
+
599
+ // src/core/progress.mjs
600
+ import chalk2 from "chalk";
601
+
602
+ // src/utils/duration.mjs
603
+ function formatDuration(ms) {
604
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
605
+ const hours = Math.floor(totalSeconds / 3600);
606
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
607
+ const seconds = totalSeconds % 60;
608
+ if (hours > 0) {
609
+ return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
610
+ }
611
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
612
+ }
613
+
614
+ // src/core/progress.mjs
615
+ function createCoverageProgressTracker(total) {
616
+ const isTTY = Boolean(process.stdout.isTTY);
617
+ const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
618
+ const barWidth = 26;
619
+ const startTime = Date.now();
620
+ let completed = 0;
621
+ let active = 0;
622
+ let failed = 0;
623
+ let frameIndex = 0;
624
+ let maxLineLength = 0;
625
+ let lastPlainLog = 0;
626
+ let spinnerTimer = null;
627
+ function buildLine() {
628
+ const percent = total === 0 ? 100 : completed / total * 100;
629
+ const elapsedMs = Date.now() - startTime;
630
+ const avgPerFile = completed > 0 ? elapsedMs / completed : 0;
631
+ const etaMs = avgPerFile * Math.max(total - completed, 0);
632
+ const filled = Math.round(percent / 100 * barWidth);
633
+ const spinner = spinnerFrames[frameIndex % spinnerFrames.length];
634
+ const bar = `[${"=".repeat(filled)}${"-".repeat(Math.max(0, barWidth - filled))}]`;
635
+ let _percent = percent.toFixed(1).padStart(5);
636
+ if (percent < 30) {
637
+ _percent = chalk2.red(_percent + "%");
638
+ } else if (percent < 70) {
639
+ _percent = chalk2.rgb(255, 136, 0)(_percent + "%");
640
+ } else if (percent > 99) {
641
+ _percent = chalk2.green(_percent + "%");
642
+ } else {
643
+ _percent = chalk2.yellow(_percent + "%");
644
+ }
645
+ if (isTTY) {
646
+ return `${chalk2.green(spinner)} ${chalk2.green(bar)} ${chalk2.bold(_percent)} ${completed}/${total} | active ${active} | failed ${failed} | ETA ${formatDuration(etaMs)} | elapsed ${formatDuration(elapsedMs)}`;
647
+ }
648
+ return `progress ${percent.toFixed(1)}% ${completed}/${total} | active ${active} | failed ${failed} | eta ${formatDuration(etaMs)} | elapsed ${formatDuration(elapsedMs)}`;
649
+ }
650
+ function render(forcePlainLog = false) {
651
+ const line = buildLine();
652
+ frameIndex++;
653
+ if (isTTY) {
654
+ const visibleLength = line.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").length;
655
+ maxLineLength = Math.max(maxLineLength, visibleLength);
656
+ process.stdout.write(`\r${line.padEnd(maxLineLength, " ")}`);
657
+ return;
658
+ }
659
+ const now = Date.now();
660
+ if (forcePlainLog || now - lastPlainLog >= 2e3) {
661
+ console.log(line);
662
+ lastPlainLog = now;
663
+ }
664
+ }
665
+ function startSpinnerLoop() {
666
+ if (!isTTY || spinnerTimer) return;
667
+ spinnerTimer = setInterval(() => {
668
+ if (completed >= total && active === 0) return;
669
+ render();
670
+ }, 120);
671
+ spinnerTimer.unref?.();
672
+ }
673
+ function stopSpinnerLoop() {
674
+ if (!spinnerTimer) return;
675
+ clearInterval(spinnerTimer);
676
+ spinnerTimer = null;
677
+ }
678
+ render(true);
679
+ startSpinnerLoop();
680
+ return {
681
+ /**
682
+ * Call when a file run starts (increments active count).
683
+ * @returns {void}
684
+ */
685
+ onStart() {
686
+ active++;
687
+ render();
688
+ },
689
+ /**
690
+ * Call when a file run completes.
691
+ * @param {boolean} failedRun - Whether the run exited with a non-zero code.
692
+ * @returns {void}
693
+ */
694
+ onComplete(failedRun) {
695
+ active = Math.max(0, active - 1);
696
+ completed++;
697
+ if (failedRun) failed++;
698
+ render(true);
699
+ },
700
+ /**
701
+ * Stop the spinner and print the final progress line.
702
+ * @returns {void}
703
+ */
704
+ finish() {
705
+ stopSpinnerLoop();
706
+ render(true);
707
+ if (isTTY) process.stdout.write("\n");
708
+ }
709
+ };
710
+ }
711
+ var noopProgressTracker = {
712
+ /** @returns {void} */
713
+ onStart() {
714
+ },
715
+ /** @param {boolean} ___failedRun @returns {void} */
716
+ onComplete(___failedRun) {
717
+ },
718
+ /** @returns {void} */
719
+ finish() {
720
+ }
721
+ };
722
+
723
+ // src/core/report.mjs
724
+ import fs3 from "fs/promises";
725
+ import path3 from "path";
726
+ import chalk3 from "chalk";
727
+ function printQuietCoverageFailureDetails(failedResults) {
728
+ if (failedResults.length === 0) return;
729
+ console.log(`
730
+ ${"=".repeat(80)}`);
731
+ console.log(chalk3.bold.red("\u2716 FAILED TEST FILES (VERBOSE OUTPUT)"));
732
+ console.log("=".repeat(80));
733
+ for (const r of failedResults) {
734
+ console.log(`
735
+ ${chalk3.red("\u2716")} ${chalk3.red(r.file)} ${chalk3.dim(`(exit ${r.code})`)}`);
736
+ if (r.rawOutput?.trim()) {
737
+ console.log(r.rawOutput.trimEnd());
738
+ } else {
739
+ console.log(chalk3.dim("(no child output captured)"));
740
+ }
741
+ }
742
+ console.log(`
743
+ ${"=".repeat(80)}`);
744
+ }
745
+ function printMergeOutput(exitCode, output) {
746
+ if (exitCode === 0) {
747
+ const marker = "% Coverage report from v8";
748
+ const rawLines = output.split("\n");
749
+ const markerLineIndex = rawLines.findIndex((line) => stripAnsi(line).includes(marker));
750
+ if (markerLineIndex >= 0) {
751
+ let endLineIndex = rawLines.length;
752
+ for (let i = markerLineIndex + 1; i < rawLines.length; i++) {
753
+ const line = stripAnsi(rawLines[i]).trimStart();
754
+ if (line.startsWith("stderr |") || line.startsWith("stdout |")) {
755
+ endLineIndex = i;
756
+ break;
757
+ }
758
+ }
759
+ const coverageBody = rawLines.slice(markerLineIndex + 1, endLineIndex).join("\n").trimEnd();
760
+ if (coverageBody) {
761
+ const fullBlock = rawLines.slice(markerLineIndex, endLineIndex).join("\n").trimEnd();
762
+ console.log(`
763
+ ${fullBlock}
764
+ `);
765
+ }
766
+ }
767
+ } else {
768
+ const trimmed = output.trimEnd();
769
+ if (trimmed) console.error(trimmed);
770
+ }
771
+ }
772
+ function computeSummaryFromFinal(finalData) {
773
+ const pct = (covered, total) => total === 0 ? 100 : parseFloat((covered / total * 100).toFixed(2));
774
+ const summary = {
775
+ total: {
776
+ statements: { total: 0, covered: 0, pct: 0 },
777
+ branches: { total: 0, covered: 0, pct: 0 },
778
+ functions: { total: 0, covered: 0, pct: 0 },
779
+ lines: { total: 0, covered: 0, pct: 0 }
780
+ }
781
+ };
782
+ for (const [filePath, data] of Object.entries(finalData)) {
783
+ const sKeys = Object.keys(data.s ?? {});
784
+ const stmtTotal = sKeys.length;
785
+ const stmtCovered = sKeys.filter((k) => data.s[k] > 0).length;
786
+ const fKeys = Object.keys(data.f ?? {});
787
+ const fnTotal = fKeys.length;
788
+ const fnCovered = fKeys.filter((k) => data.f[k] > 0).length;
789
+ let branchTotal = 0, branchCovered = 0;
790
+ for (const counts of Object.values(data.b ?? {})) {
791
+ branchTotal += counts.length;
792
+ branchCovered += counts.filter((c) => c > 0).length;
793
+ }
794
+ const coveredLines = /* @__PURE__ */ new Set();
795
+ const totalLines = /* @__PURE__ */ new Set();
796
+ for (const [sid, loc] of Object.entries(data.statementMap ?? {})) {
797
+ const line = loc?.start?.line;
798
+ if (line == null) continue;
799
+ totalLines.add(line);
800
+ if ((data.s ?? {})[sid] > 0) coveredLines.add(line);
801
+ }
802
+ const fileStats = {
803
+ statements: { total: stmtTotal, covered: stmtCovered, pct: pct(stmtCovered, stmtTotal) },
804
+ branches: { total: branchTotal, covered: branchCovered, pct: pct(branchCovered, branchTotal) },
805
+ functions: { total: fnTotal, covered: fnCovered, pct: pct(fnCovered, fnTotal) },
806
+ lines: { total: totalLines.size, covered: coveredLines.size, pct: pct(coveredLines.size, totalLines.size) }
807
+ };
808
+ summary[filePath] = fileStats;
809
+ for (const key of ["statements", "branches", "functions", "lines"]) {
810
+ summary.total[key].total += fileStats[key].total;
811
+ summary.total[key].covered += fileStats[key].covered;
812
+ }
813
+ }
814
+ for (const key of ["statements", "branches", "functions", "lines"]) {
815
+ const { total, covered } = summary.total[key];
816
+ summary.total[key].pct = pct(covered, total);
817
+ }
818
+ return summary;
819
+ }
820
+ async function printCoverageSummary(cwd2, extraCoverageArgs, worstCount = 10, options = {}) {
821
+ const { silent = false } = options;
822
+ let coverageDir = path3.resolve(cwd2, "coverage");
823
+ const repoDirArg = extraCoverageArgs.find((a) => a.startsWith("--coverage.reportsDirectory="));
824
+ if (repoDirArg) {
825
+ const raw = repoDirArg.split("=").slice(1).join("=");
826
+ coverageDir = path3.isAbsolute(raw) ? raw : path3.resolve(cwd2, raw);
827
+ }
828
+ let summary;
829
+ try {
830
+ const content = await fs3.readFile(path3.join(coverageDir, "coverage-summary.json"), "utf8");
831
+ summary = JSON.parse(content);
832
+ } catch {
833
+ try {
834
+ const content = await fs3.readFile(path3.join(coverageDir, "coverage-final.json"), "utf8");
835
+ summary = computeSummaryFromFinal(JSON.parse(content));
836
+ } catch {
837
+ if (!silent) console.log(chalk3.dim(" (no coverage JSON found \u2014 skipping summary)"));
838
+ return null;
839
+ }
840
+ }
841
+ const { total, ...fileSummaries } = summary;
842
+ if (worstCount > 0) {
843
+ const fileRows = Object.entries(fileSummaries).map(([absFile, data]) => ({
844
+ file: path3.relative(cwd2, absFile),
845
+ lines: data.lines?.pct ?? 0,
846
+ stmts: data.statements?.pct ?? 0,
847
+ fns: data.functions?.pct ?? 0,
848
+ branches: data.branches?.pct ?? 0
849
+ })).filter((r) => Math.min(r.lines, r.stmts, r.fns, r.branches) < 100).sort((a, b) => Math.min(a.lines, a.stmts, a.fns, a.branches) - Math.min(b.lines, b.stmts, b.fns, b.branches));
850
+ const rowsToShow = fileRows.slice(0, worstCount);
851
+ if (!silent) {
852
+ console.log("\n" + chalk3.bold("\u{1F4C9} WORST COVERAGE FILES (lowest metric)"));
853
+ console.log("-".repeat(80));
854
+ rowsToShow.forEach(({ file, lines, stmts, fns, branches }) => {
855
+ const worst = Math.min(lines, stmts, fns, branches);
856
+ const extras = chalk3.dim(
857
+ `lines ${lines.toFixed(0)}% | stmts ${stmts.toFixed(0)}% | fns ${fns.toFixed(0)}% | branches ${branches.toFixed(0)}%`
858
+ );
859
+ console.log(` ${colourPct(chalk3, worst)}% ${chalk3.dim(file)} ${extras}`);
860
+ });
861
+ if (fileRows.length > worstCount) {
862
+ console.log(chalk3.dim(` ... and ${fileRows.length - worstCount} more files`));
863
+ }
864
+ }
865
+ const tl2 = total.lines?.pct ?? 0;
866
+ const ts2 = total.statements?.pct ?? 0;
867
+ const tf2 = total.functions?.pct ?? 0;
868
+ const tb2 = total.branches?.pct ?? 0;
869
+ if (!silent) {
870
+ console.log(
871
+ `
872
+ ${chalk3.bold("Coverage")} ${colourPct(chalk3, tl2)}% lines ${chalk3.dim("|")} ${colourPct(chalk3, ts2)}% statements ${chalk3.dim("|")} ${colourPct(chalk3, tf2)}% functions ${chalk3.dim("|")} ${colourPct(chalk3, tb2)}% branches`
873
+ );
874
+ }
875
+ return {
876
+ coverageDir,
877
+ total,
878
+ worstFiles: rowsToShow,
879
+ worstFilesShown: rowsToShow.length,
880
+ worstFilesTotal: fileRows.length,
881
+ summary
882
+ };
883
+ }
884
+ const tl = total.lines?.pct ?? 0;
885
+ const ts = total.statements?.pct ?? 0;
886
+ const tf = total.functions?.pct ?? 0;
887
+ const tb = total.branches?.pct ?? 0;
888
+ if (!silent) {
889
+ console.log(
890
+ `
891
+ ${chalk3.bold("Coverage")} ${colourPct(chalk3, tl)}% lines ${chalk3.dim("|")} ${colourPct(chalk3, ts)}% statements ${chalk3.dim("|")} ${colourPct(chalk3, tf)}% functions ${chalk3.dim("|")} ${colourPct(chalk3, tb)}% branches`
892
+ );
893
+ }
894
+ return {
895
+ coverageDir,
896
+ total,
897
+ worstFiles: [],
898
+ worstFilesShown: 0,
899
+ worstFilesTotal: 0,
900
+ summary
901
+ };
902
+ }
903
+
904
+ // src/core/scratch.mjs
905
+ import fs4 from "fs/promises";
906
+ import fsSync from "fs";
907
+ import path4 from "path";
908
+ var DEFAULT_SCRATCH_DIR = "tmp/vitest-runner";
909
+ var RUN_ROOT_NAME_PATTERN = /^(\d+)-\d+$/;
910
+ function isPidAlive(pid) {
911
+ try {
912
+ process.kill(pid, 0);
913
+ return true;
914
+ } catch (err) {
915
+ return err.code !== "ESRCH";
916
+ }
917
+ }
918
+ async function sweepStaleScratchRoots(cwd2, scratchDir) {
919
+ const base = path4.resolve(cwd2, scratchDir);
920
+ let entries;
921
+ try {
922
+ entries = await fs4.readdir(base, { withFileTypes: true });
923
+ } catch {
924
+ return;
925
+ }
926
+ for (const entry of entries) {
927
+ if (!entry.isDirectory()) continue;
928
+ const match = entry.name.match(RUN_ROOT_NAME_PATTERN);
929
+ if (!match) continue;
930
+ const pid = parseInt(match[1], 10);
931
+ if (isPidAlive(pid)) continue;
932
+ await fs4.rm(path4.join(base, entry.name), { recursive: true, force: true }).catch(() => {
933
+ });
934
+ }
935
+ }
936
+ async function createRunScratchRoot(cwd2, scratchDir) {
937
+ const base = path4.resolve(cwd2, scratchDir);
938
+ const root = path4.join(base, `${process.pid}-${Date.now()}`);
939
+ await fs4.mkdir(root, { recursive: true });
940
+ return root;
941
+ }
942
+ async function createFileScratchDir(runRoot, index) {
943
+ const dir = path4.join(runRoot, `worker-${index}`);
944
+ await fs4.mkdir(dir, { recursive: true });
945
+ return dir;
946
+ }
947
+ async function removeScratchRoot(root) {
948
+ await fs4.rm(root, { recursive: true, force: true }).catch(() => {
949
+ });
950
+ }
951
+ function removeScratchRootSync(root) {
952
+ try {
953
+ fsSync.rmSync(root, { recursive: true, force: true });
954
+ } catch {
955
+ }
956
+ }
957
+
958
+ // src/runner.mjs
959
+ function getHeapForFile(filePath, globalMaxMb, overrides) {
960
+ const normalized = filePath.replace(/\\/g, "/");
961
+ let perFileMb;
962
+ for (const { pattern, heapMb } of overrides) {
963
+ if (normalized.includes(pattern)) {
964
+ perFileMb = heapMb;
965
+ break;
966
+ }
967
+ }
968
+ if (perFileMb === void 0 && globalMaxMb === void 0) return void 0;
969
+ if (perFileMb === void 0) return globalMaxMb;
970
+ if (globalMaxMb === void 0) return perFileMb;
971
+ return Math.max(perFileMb, globalMaxMb);
972
+ }
973
+ async function run(opts) {
974
+ const cwd2 = opts.cwd ?? process.cwd();
975
+ const scratchDirOpt = opts.scratchDir ?? DEFAULT_SCRATCH_DIR;
976
+ const keepTmp = opts.keepTmp ?? false;
977
+ await sweepStaleScratchRoots(cwd2, scratchDirOpt);
978
+ const scratchRoot = await createRunScratchRoot(cwd2, scratchDirOpt);
979
+ const onSignal = () => {
980
+ removeScratchRootSync(scratchRoot);
981
+ process.exitCode = 130;
982
+ process.exit();
983
+ };
984
+ if (!keepTmp) {
985
+ process.once("SIGINT", onSignal);
986
+ process.once("SIGTERM", onSignal);
987
+ }
988
+ try {
989
+ return await runImpl(opts, scratchRoot);
990
+ } finally {
991
+ process.removeListener("SIGINT", onSignal);
992
+ process.removeListener("SIGTERM", onSignal);
993
+ if (!keepTmp) await removeScratchRoot(scratchRoot);
994
+ }
995
+ }
996
+ async function runImpl(opts, scratchRoot) {
997
+ const {
998
+ cwd: cwd2 = process.cwd(),
999
+ testDir,
1000
+ testPatterns = [],
1001
+ testListFile,
1002
+ testFilePattern,
1003
+ vitestArgs: rawVitestArgs = [],
1004
+ showErrorDetails = true,
1005
+ coverageQuiet = false,
1006
+ suppressFileOutput = false,
1007
+ suppressPassingFiles = false,
1008
+ topSummary = true,
1009
+ json = false,
1010
+ workers = parseInt(process.env.VITEST_WORKERS ?? "4", 10),
1011
+ worstCoverageCount = 10,
1012
+ blobsDir: blobsDirOpt,
1013
+ mergeReports = true,
1014
+ earlyRunPatterns = [],
1015
+ perFileHeapOverrides = [],
1016
+ conditions = [],
1017
+ nodeEnv = "development",
1018
+ /** @internal Inject pre-built results to bypass discovery and spawn (for testing final-report render paths). */
1019
+ _testResultsOverride = null
1020
+ } = opts;
1021
+ let fileScratchIndex = 0;
1022
+ const maxOldSpaceMb = opts.maxOldSpaceMb ?? (process.env.VITEST_HEAP_MB ? parseInt(process.env.VITEST_HEAP_MB, 10) : void 0);
1023
+ const emitTextOutput = !json;
1024
+ const vitestBin = resolveBin(cwd2, "vitest", "vitest");
1025
+ const vitestConfig = await resolveVitestConfig(cwd2, opts.vitestConfig);
1026
+ const spawnBase = { cwd: cwd2, vitestBin, vitestConfig, conditions, nodeEnv };
1027
+ const vitestArgs2 = [...rawVitestArgs];
1028
+ if (coverageQuiet && !vitestArgs2.some((a) => a === "--coverage" || a.startsWith("--coverage."))) {
1029
+ vitestArgs2.unshift("--coverage");
1030
+ }
1031
+ const hasCoverage = vitestArgs2.some((a) => a === "--coverage" || a.startsWith("--coverage."));
1032
+ if (hasCoverage) {
1033
+ const blobsDir = path5.resolve(cwd2, blobsDirOpt ?? ".vitest-coverage-blobs");
1034
+ const coverageTmpBase = path5.resolve(cwd2, ".vitest-coverage-tmp");
1035
+ await Promise.all([fs5.rm(blobsDir, { recursive: true, force: true }), fs5.rm(coverageTmpBase, { recursive: true, force: true })]);
1036
+ await Promise.all([fs5.mkdir(blobsDir, { recursive: true }), fs5.mkdir(coverageTmpBase, { recursive: true })]);
1037
+ const allTestFiles = await discoverVitestFiles({ cwd: cwd2, testDir, testPatterns, testListFile, testFilePattern, earlyRunPatterns });
1038
+ const filterConflicts2 = computeFilterConflicts(allTestFiles);
1039
+ if (allTestFiles.length === 0) {
1040
+ const noTestsMessage = testPatterns.length > 0 ? `\u274C No Vitest test files found matching: ${testPatterns.join(", ")}` : "\u274C No Vitest test files found";
1041
+ if (emitTextOutput) console.log(noTestsMessage);
1042
+ if (json) {
1043
+ return {
1044
+ exitCode: 1,
1045
+ mode: "coverage",
1046
+ message: noTestsMessage,
1047
+ options: {
1048
+ cwd: cwd2,
1049
+ testDir,
1050
+ testPatterns,
1051
+ testListFile,
1052
+ workers,
1053
+ coverageQuiet,
1054
+ suppressFileOutput,
1055
+ suppressPassingFiles,
1056
+ topSummary,
1057
+ vitestArgs: vitestArgs2
1058
+ }
1059
+ };
1060
+ }
1061
+ return 1;
1062
+ }
1063
+ const extraCoverageArgs = vitestArgs2.filter((a) => a !== "--coverage" && a.startsWith("--coverage."));
1064
+ const nonCoveragePassthrough = vitestArgs2.filter((a) => a !== "--coverage" && !a.startsWith("--coverage."));
1065
+ const soloFiles2 = allTestFiles.filter((f) => earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
1066
+ const parallelFiles2 = allTestFiles.filter((f) => !earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
1067
+ const suppressPerFileCoverageOutput = coverageQuiet || suppressFileOutput || json;
1068
+ const showCoverageProgress = coverageQuiet && !json;
1069
+ if (!coverageQuiet && !suppressFileOutput && emitTextOutput) {
1070
+ console.log(`
1071
+ \u{1F9EA} Running ${allTestFiles.length} test files for coverage (blob + merge mode)`);
1072
+ console.log(`\u2699\uFE0F Workers: ${workers} (${soloFiles2.length} solo first, then parallel)`);
1073
+ if (maxOldSpaceMb) console.log(`\u{1F9E0} Heap limit: ${maxOldSpaceMb} MB`);
1074
+ console.log("");
1075
+ }
1076
+ const progress = showCoverageProgress ? createCoverageProgressTracker(allTestFiles.length) : noopProgressTracker;
1077
+ const coverageResults = [];
1078
+ let blobIndex = 0;
1079
+ const runCoverageFile = async (filePath) => {
1080
+ const blobPath = path5.join(blobsDir, `run-${blobIndex}.blob`);
1081
+ blobIndex++;
1082
+ if (!suppressPerFileCoverageOutput && emitTextOutput) {
1083
+ console.log(`
1084
+ ${"=".repeat(80)}`);
1085
+ console.log(`\u25B6\uFE0F ${filePath}`);
1086
+ console.log("=".repeat(80));
1087
+ }
1088
+ progress.onStart();
1089
+ const tmpCoverageDir = path5.join(coverageTmpBase, `run-${blobIndex}`);
1090
+ const blobArgs = [
1091
+ ...nonCoveragePassthrough,
1092
+ "--coverage",
1093
+ `--coverage.reportsDirectory=${tmpCoverageDir}`,
1094
+ "--reporter=default",
1095
+ "--reporter=blob",
1096
+ `--outputFile=${blobPath}`
1097
+ ];
1098
+ const scratchDir = await createFileScratchDir(scratchRoot, fileScratchIndex++);
1099
+ const result = await runSingleFile(filePath, {
1100
+ ...spawnBase,
1101
+ maxOldSpaceMb: getHeapForFile(filePath, maxOldSpaceMb, perFileHeapOverrides),
1102
+ vitestArgs: blobArgs,
1103
+ streamOutput: !suppressPerFileCoverageOutput,
1104
+ excludePaths: filterConflicts2.get(filePath),
1105
+ extraEnv: { VITEST_RUNNER_TMP: scratchDir }
1106
+ });
1107
+ coverageResults.push(result);
1108
+ progress.onComplete(result.code !== 0);
1109
+ if (!suppressPerFileCoverageOutput && emitTextOutput) {
1110
+ const durationSec = (result.duration / 1e3).toFixed(2);
1111
+ if (result.code === 0) {
1112
+ const heapInfo = result.heapMb ? ` | ${result.heapMb} MB heap` : "";
1113
+ console.log(`
1114
+ \u2705 PASSED (${durationSec}s${heapInfo})
1115
+ `);
1116
+ } else {
1117
+ console.log(`
1118
+ \u274C FAILED (exit code ${result.code}, ${durationSec}s)
1119
+ `);
1120
+ }
1121
+ }
1122
+ };
1123
+ for (const filePath of soloFiles2) {
1124
+ await runCoverageFile(filePath).catch((err) => {
1125
+ if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
1126
+ });
1127
+ }
1128
+ let coverageFileIndex = 0;
1129
+ const coverageActivePromises = /* @__PURE__ */ new Set();
1130
+ while (coverageFileIndex < parallelFiles2.length || coverageActivePromises.size > 0) {
1131
+ while (coverageFileIndex < parallelFiles2.length && coverageActivePromises.size < workers) {
1132
+ const filePath = parallelFiles2[coverageFileIndex++];
1133
+ const promise = runCoverageFile(filePath).catch((err) => {
1134
+ if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
1135
+ }).finally(() => coverageActivePromises.delete(promise));
1136
+ coverageActivePromises.add(promise);
1137
+ }
1138
+ if (coverageActivePromises.size > 0) await Promise.race(coverageActivePromises);
1139
+ }
1140
+ progress.finish();
1141
+ const blobFiles = (await fs5.readdir(blobsDir).catch(() => [])).filter((f) => f.endsWith(".blob"));
1142
+ if (blobFiles.length === 0) {
1143
+ const message = "\u274C No coverage blobs were generated \u2014 coverage report cannot be produced";
1144
+ if (emitTextOutput) console.error(message);
1145
+ if (json) {
1146
+ return {
1147
+ exitCode: 1,
1148
+ mode: "coverage",
1149
+ message,
1150
+ options: {
1151
+ cwd: cwd2,
1152
+ testDir,
1153
+ testPatterns,
1154
+ testListFile,
1155
+ workers,
1156
+ coverageQuiet,
1157
+ suppressFileOutput,
1158
+ suppressPassingFiles,
1159
+ topSummary,
1160
+ showErrorDetails,
1161
+ worstCoverageCount,
1162
+ vitestArgs: vitestArgs2
1163
+ },
1164
+ totals: {
1165
+ testFiles: allTestFiles.length,
1166
+ failedFiles: coverageResults.filter((r) => r.code !== 0).length,
1167
+ passedFiles: coverageResults.filter((r) => r.code === 0).length
1168
+ },
1169
+ results: {
1170
+ all: coverageResults,
1171
+ failed: coverageResults.filter((r) => r.code !== 0)
1172
+ },
1173
+ merge: {
1174
+ blobFiles: 0,
1175
+ exitCode: 1,
1176
+ output: ""
1177
+ },
1178
+ coverageSummary: null
1179
+ };
1180
+ }
1181
+ return 1;
1182
+ }
1183
+ let mergeExitCode = 0;
1184
+ let mergeOutput = "";
1185
+ let coverageSummary = null;
1186
+ if (mergeReports) {
1187
+ if (!coverageQuiet && !suppressFileOutput && emitTextOutput) {
1188
+ console.log(`
1189
+ ${"=".repeat(80)}`);
1190
+ console.log(`\u{1F4CA} Merging ${blobFiles.length} coverage blobs into final report...`);
1191
+ console.log("=".repeat(80));
1192
+ }
1193
+ ({ exitCode: mergeExitCode, output: mergeOutput } = await runMergeReports(blobsDir, {
1194
+ ...spawnBase,
1195
+ maxOldSpaceMb,
1196
+ extraCoverageArgs,
1197
+ quietOutput: coverageQuiet || json
1198
+ }));
1199
+ if (coverageQuiet && emitTextOutput) {
1200
+ printMergeOutput(mergeExitCode, mergeOutput);
1201
+ }
1202
+ coverageSummary = await printCoverageSummary(cwd2, extraCoverageArgs, worstCoverageCount, { silent: json });
1203
+ }
1204
+ await Promise.all([
1205
+ ...mergeReports ? [fs5.rm(blobsDir, { recursive: true, force: true }).catch(() => {
1206
+ })] : [],
1207
+ fs5.rm(coverageTmpBase, { recursive: true, force: true }).catch(() => {
1208
+ })
1209
+ ]);
1210
+ const coverageFailed = coverageResults.filter((r) => r.code !== 0);
1211
+ if (coverageQuiet && emitTextOutput) printQuietCoverageFailureDetails(coverageFailed);
1212
+ const exitCode2 = coverageFailed.length > 0 ? 1 : mergeExitCode;
1213
+ if (json) {
1214
+ const coverageWithHeap = coverageResults.filter((r) => r.heapMb !== null);
1215
+ return {
1216
+ exitCode: exitCode2,
1217
+ mode: "coverage",
1218
+ options: {
1219
+ cwd: cwd2,
1220
+ testDir,
1221
+ testPatterns,
1222
+ testListFile,
1223
+ workers,
1224
+ coverageQuiet,
1225
+ suppressFileOutput,
1226
+ suppressPassingFiles,
1227
+ topSummary,
1228
+ showErrorDetails,
1229
+ worstCoverageCount,
1230
+ vitestArgs: vitestArgs2
1231
+ },
1232
+ totals: {
1233
+ testFiles: allTestFiles.length,
1234
+ failedFiles: coverageFailed.length,
1235
+ passedFiles: coverageResults.length - coverageFailed.length
1236
+ },
1237
+ results: {
1238
+ all: coverageResults,
1239
+ failed: coverageFailed
1240
+ },
1241
+ merge: {
1242
+ blobFiles: blobFiles.length,
1243
+ exitCode: mergeExitCode,
1244
+ output: mergeOutput
1245
+ },
1246
+ coverageSummary,
1247
+ ...topSummary ? {
1248
+ topMemoryUsers: [...coverageWithHeap].sort((a, b) => b.heapMb - a.heapMb).slice(0, 10),
1249
+ topDuration: [...coverageResults].sort((a, b) => b.duration - a.duration).slice(0, 10)
1250
+ } : {}
1251
+ };
1252
+ }
1253
+ return exitCode2;
1254
+ }
1255
+ const testFiles = await discoverVitestFiles({ cwd: cwd2, testDir, testPatterns, testListFile, testFilePattern, earlyRunPatterns });
1256
+ const filterConflicts = computeFilterConflicts(testFiles);
1257
+ if (testFiles.length === 0) {
1258
+ const noTestsMessage = testPatterns.length > 0 ? `\u274C No Vitest test files found matching: ${testPatterns.join(", ")}` : "\u274C No Vitest test files found";
1259
+ if (emitTextOutput) console.log(noTestsMessage);
1260
+ if (json) {
1261
+ return {
1262
+ exitCode: 1,
1263
+ mode: "standard",
1264
+ message: noTestsMessage,
1265
+ options: {
1266
+ cwd: cwd2,
1267
+ testDir,
1268
+ testPatterns,
1269
+ testListFile,
1270
+ workers,
1271
+ coverageQuiet,
1272
+ suppressFileOutput,
1273
+ suppressPassingFiles,
1274
+ topSummary,
1275
+ vitestArgs: vitestArgs2
1276
+ }
1277
+ };
1278
+ }
1279
+ return 1;
1280
+ }
1281
+ const soloFiles = testFiles.filter((f) => earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
1282
+ const parallelFiles = testFiles.filter((f) => !earlyRunPatterns.some((p) => f.replace(/\\/g, "/").includes(p)));
1283
+ const scriptStartTime = Date.now();
1284
+ const scriptStartTimeFormatted = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
1285
+ if (emitTextOutput && !suppressFileOutput && testPatterns.length > 0) {
1286
+ console.log(`
1287
+ \u{1F9EA} Running ${testFiles.length} test files matching: ${testPatterns.join(", ")}`);
1288
+ } else if (emitTextOutput && !suppressFileOutput) {
1289
+ console.log(`
1290
+ \u{1F9EA} Running ${testFiles.length} test files (${soloFiles.length} solo first, then parallel)`);
1291
+ }
1292
+ if (emitTextOutput && !suppressFileOutput) {
1293
+ console.log(`\u2699\uFE0F Workers: ${workers}`);
1294
+ if (maxOldSpaceMb) console.log(`\u{1F9E0} Heap limit: ${maxOldSpaceMb} MB`);
1295
+ if (vitestArgs2.length > 0) console.log(`\u{1F527} Vitest args: ${vitestArgs2.join(" ")}`);
1296
+ console.log("");
1297
+ }
1298
+ const results = [..._testResultsOverride ?? []];
1299
+ const runTestFile = async (filePath) => {
1300
+ if (!suppressFileOutput && emitTextOutput) {
1301
+ console.log(`
1302
+ ${"=".repeat(80)}`);
1303
+ console.log(`\u25B6\uFE0F ${filePath}`);
1304
+ console.log("=".repeat(80));
1305
+ }
1306
+ const scratchDir = await createFileScratchDir(scratchRoot, fileScratchIndex++);
1307
+ const result = await runSingleFile(filePath, {
1308
+ ...spawnBase,
1309
+ maxOldSpaceMb: getHeapForFile(filePath, maxOldSpaceMb, perFileHeapOverrides),
1310
+ vitestArgs: vitestArgs2,
1311
+ streamOutput: !suppressFileOutput && !json,
1312
+ excludePaths: filterConflicts.get(filePath),
1313
+ extraEnv: { VITEST_RUNNER_TMP: scratchDir }
1314
+ });
1315
+ if (!suppressFileOutput && emitTextOutput) {
1316
+ const durationSec = (result.duration / 1e3).toFixed(2);
1317
+ if (result.code === 0) {
1318
+ const heapInfo = result.heapMb ? ` | ${result.heapMb} MB heap` : "";
1319
+ console.log(`
1320
+ \u2705 PASSED (${durationSec}s${heapInfo})
1321
+ `);
1322
+ } else {
1323
+ console.log(`
1324
+ \u274C FAILED (exit code ${result.code}, ${durationSec}s)
1325
+ `);
1326
+ }
1327
+ }
1328
+ return result;
1329
+ };
1330
+ if (!_testResultsOverride) {
1331
+ for (const filePath of soloFiles) {
1332
+ const result = await runTestFile(filePath).catch((err) => {
1333
+ if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
1334
+ return null;
1335
+ });
1336
+ if (result) results.push(result);
1337
+ }
1338
+ let index = 0;
1339
+ const activePromises = /* @__PURE__ */ new Set();
1340
+ while (index < parallelFiles.length || activePromises.size > 0) {
1341
+ while (index < parallelFiles.length && activePromises.size < workers) {
1342
+ const filePath = parallelFiles[index++];
1343
+ const promise = runTestFile(filePath).then((result) => results.push(result)).catch((err) => {
1344
+ if (emitTextOutput) console.error(`Error running ${filePath}:`, err);
1345
+ }).finally(() => activePromises.delete(promise));
1346
+ activePromises.add(promise);
1347
+ }
1348
+ if (activePromises.size > 0) await Promise.race(activePromises);
1349
+ }
1350
+ }
1351
+ const totalTestFilesPass = results.reduce((s, r) => s + r.testFilesPass, 0);
1352
+ const totalTestFilesFail = results.reduce((s, r) => s + r.testFilesFail, 0);
1353
+ const totalTestsPass = results.reduce((s, r) => s + r.testsPass, 0);
1354
+ const totalTestsFail = results.reduce((s, r) => s + r.testsFail, 0);
1355
+ const totalTestsSkip = results.reduce((s, r) => s + (r.testsSkip || 0), 0);
1356
+ const totalDuration = results.reduce((s, r) => s + r.duration, 0);
1357
+ const failedFiles = results.filter((r) => r.code !== 0);
1358
+ const passedFiles = results.filter((r) => r.code === 0);
1359
+ const exitCode = failedFiles.length > 0 ? 1 : 0;
1360
+ const scriptMemory = process.memoryUsage();
1361
+ const scriptHeapMb = Math.round(scriptMemory.heapUsed / 1024 / 1024);
1362
+ const scriptRssMb = Math.round(scriptMemory.rss / 1024 / 1024);
1363
+ const withHeap = results.filter((r) => r.heapMb !== null);
1364
+ const actualDurationSec = ((Date.now() - scriptStartTime) / 1e3).toFixed(2);
1365
+ const testsDurationSec = (totalDuration / 1e3).toFixed(2);
1366
+ if (json) {
1367
+ return {
1368
+ exitCode,
1369
+ mode: "standard",
1370
+ options: {
1371
+ cwd: cwd2,
1372
+ testDir,
1373
+ testPatterns,
1374
+ testListFile,
1375
+ workers,
1376
+ coverageQuiet,
1377
+ suppressFileOutput,
1378
+ suppressPassingFiles,
1379
+ topSummary,
1380
+ showErrorDetails,
1381
+ worstCoverageCount,
1382
+ vitestArgs: vitestArgs2
1383
+ },
1384
+ totals: {
1385
+ testFilesPass: totalTestFilesPass,
1386
+ testFilesFail: totalTestFilesFail,
1387
+ testsPass: totalTestsPass,
1388
+ testsFail: totalTestsFail,
1389
+ testsSkip: totalTestsSkip,
1390
+ totalTests: totalTestsPass + totalTestsFail + totalTestsSkip
1391
+ },
1392
+ timing: {
1393
+ startAt: scriptStartTimeFormatted,
1394
+ wallClockSeconds: Number(actualDurationSec),
1395
+ testsSeconds: Number(testsDurationSec)
1396
+ },
1397
+ heap: {
1398
+ scriptHeapMb,
1399
+ scriptRssMb,
1400
+ maxHeapMb: withHeap.length > 0 ? Math.max(...withHeap.map((r) => r.heapMb)) : null,
1401
+ avgHeapMb: withHeap.length > 0 ? Number((withHeap.reduce((sum, r) => sum + r.heapMb, 0) / withHeap.length).toFixed(0)) : null
1402
+ },
1403
+ results: {
1404
+ all: results,
1405
+ passed: passedFiles,
1406
+ failed: failedFiles
1407
+ },
1408
+ ...topSummary ? {
1409
+ topMemoryUsers: [...withHeap].sort((a, b) => b.heapMb - a.heapMb).slice(0, 10),
1410
+ topDuration: [...results].sort((a, b) => b.duration - a.duration).slice(0, 10)
1411
+ } : {}
1412
+ };
1413
+ }
1414
+ console.log("\n" + "=".repeat(80));
1415
+ if (topSummary && withHeap.length > 0) {
1416
+ console.log("\n" + chalk4.bold("\u{1F9E0} TOP MEMORY USERS"));
1417
+ console.log("-".repeat(80));
1418
+ [...withHeap].sort((a, b) => b.heapMb - a.heapMb).slice(0, 10).forEach((r) => {
1419
+ console.log(` ${String(r.heapMb).padStart(4)} MB ${chalk4.dim(r.file)}`);
1420
+ });
1421
+ }
1422
+ if (topSummary && results.length > 0) {
1423
+ console.log("\n" + chalk4.bold("\u23F1\uFE0F TOP DURATION"));
1424
+ console.log("-".repeat(80));
1425
+ [...results].sort((a, b) => b.duration - a.duration).slice(0, 10).forEach((r) => {
1426
+ const sec = (r.duration / 1e3).toFixed(2);
1427
+ console.log(` ${(sec + "s").padStart(8)} ${chalk4.dim(r.file)}`);
1428
+ });
1429
+ }
1430
+ if (passedFiles.length > 0 && !suppressPassingFiles) {
1431
+ console.log("\n" + "=".repeat(80));
1432
+ console.log(chalk4.bold.green("\u2713 PASSED TEST FILES"));
1433
+ console.log("=".repeat(80));
1434
+ passedFiles.forEach((r) => {
1435
+ const durationSec = (r.duration / 1e3).toFixed(2);
1436
+ const statsInfo = [...r.heapMb ? [`${r.heapMb} MB`] : [], `${durationSec}s`];
1437
+ const testInfo = r.testsPass > 0 ? ` - ${r.testsPass} tests` : "";
1438
+ console.log(chalk4.green(`\u2713 ${r.file}${testInfo}`) + chalk4.dim(` (${statsInfo.join(", ")})`));
1439
+ });
1440
+ }
1441
+ console.log("\n" + chalk4.bold("=".repeat(80)));
1442
+ if (failedFiles.length > 0) {
1443
+ console.log(`
1444
+ \u274C ${failedFiles.length} test file(s) failed`);
1445
+ console.log(chalk4.bold.red("\nFailed Test Files:"));
1446
+ failedFiles.forEach((r) => {
1447
+ const durationSec = (r.duration / 1e3).toFixed(2);
1448
+ const testCounts = [
1449
+ ...r.testsFail > 0 ? [chalk4.red(`${r.testsFail} failed`)] : [],
1450
+ ...r.testsPass > 0 ? [chalk4.green(`${r.testsPass} passed`)] : [],
1451
+ ...r.testsSkip > 0 ? [chalk4.yellow(`${r.testsSkip} skipped`)] : []
1452
+ ];
1453
+ const statsInfo = [...r.heapMb ? [`${r.heapMb} MB`] : [], `${durationSec}s`];
1454
+ const countStr = testCounts.length > 0 ? ` (${testCounts.join(", ")})` : "";
1455
+ console.log(` ${chalk4.red("\u2716")} ${r.file}${countStr}` + chalk4.dim(` [${statsInfo.join(", ")}]`));
1456
+ if (showErrorDetails && r.errors.length > 0) {
1457
+ const deduped = deduplicateErrors(r.errors).split("\n").map((line) => line.trim() ? ` ${line}` : "").join("\n");
1458
+ console.log(deduped);
1459
+ console.log("");
1460
+ }
1461
+ });
1462
+ console.log("");
1463
+ }
1464
+ console.log(chalk4.bold("=".repeat(80)));
1465
+ if (totalTestFilesFail > 0 && totalTestFilesPass > 0) {
1466
+ console.log(
1467
+ ` ${chalk4.bold("Test Files")} ${chalk4.red(`${totalTestFilesFail} failed`)} ${chalk4.dim("|")} ${chalk4.green(`${totalTestFilesPass} passed`)} ${chalk4.dim(`(${totalTestFilesPass + totalTestFilesFail})`)}`
1468
+ );
1469
+ } else if (totalTestFilesFail > 0) {
1470
+ console.log(` ${chalk4.bold("Test Files")} ${chalk4.red(`${totalTestFilesFail} failed`)} ${chalk4.dim(`(${totalTestFilesFail})`)}`);
1471
+ } else {
1472
+ console.log(` ${chalk4.bold("Test Files")} ${chalk4.green(`${totalTestFilesPass} passed`)} ${chalk4.dim(`(${totalTestFilesPass})`)}`);
1473
+ }
1474
+ const totalTests = totalTestsPass + totalTestsFail + totalTestsSkip;
1475
+ const testsParts = [
1476
+ ...totalTestsFail > 0 ? [chalk4.red(`${totalTestsFail} failed`)] : [],
1477
+ ...totalTestsPass > 0 ? [chalk4.green(`${totalTestsPass} passed`)] : [],
1478
+ ...totalTestsSkip > 0 ? [chalk4.yellow(`${totalTestsSkip} skipped`)] : []
1479
+ ];
1480
+ console.log(` ${chalk4.bold("Tests")} ${testsParts.join(` ${chalk4.dim("|")} `)} ${chalk4.dim(`(${totalTests})`)}`);
1481
+ console.log(` ${chalk4.bold("Start at")} ${scriptStartTimeFormatted}`);
1482
+ console.log(` ${chalk4.bold("Duration")} ${actualDurationSec}s ${chalk4.dim(`(tests ${testsDurationSec}s)`)}`);
1483
+ if (withHeap.length > 0) {
1484
+ const maxHeap = Math.max(...withHeap.map((r) => r.heapMb));
1485
+ const avgHeap = (withHeap.reduce((s, r) => s + r.heapMb, 0) / withHeap.length).toFixed(0);
1486
+ console.log(` ${chalk4.bold("Heap")} max ${maxHeap} MB | avg ${avgHeap} MB | script ${scriptHeapMb} MB (RSS ${scriptRssMb} MB)`);
1487
+ } else {
1488
+ console.log(` ${chalk4.bold("Heap")} script ${scriptHeapMb} MB (RSS ${scriptRssMb} MB)`);
1489
+ }
1490
+ if (failedFiles.length > 0) return 1;
1491
+ console.log(`
1492
+ \u2705 All ${passedFiles.length} test files passed
1493
+ `);
1494
+ return 0;
1495
+ }
1496
+
1497
+ // src/bin/vitest-runner.mjs
1498
+ var args = parseArguments(process.argv.slice(2));
1499
+ if (args.help) {
1500
+ showHelp();
1501
+ process.exit(0);
1502
+ }
26
1503
  if ((args.coverageQuiet || args.logFile) && !args.json) {
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];
1504
+ let isProgressChunk = function(chunk) {
1505
+ const str = chunk.toString();
1506
+ return str.startsWith("\r") || /^progress \d+\.\d+%/.test(str);
1507
+ };
1508
+ const cwd2 = process.cwd();
1509
+ const resolvedLogFile = args.logFile ? path6.isAbsolute(args.logFile) ? args.logFile : path6.resolve(cwd2, args.logFile) : path6.join(cwd2, "coverage", "coverage-run.log");
1510
+ mkdirSync(path6.dirname(resolvedLogFile), { recursive: true });
1511
+ const logStream = createWriteStream(resolvedLogFile, { flags: "a" });
1512
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
1513
+ const origStderrWrite = process.stderr.write.bind(process.stderr);
1514
+ process.stdout.write = (chunk, enc, cb) => {
1515
+ if (!isProgressChunk(chunk)) logStream.write(stripAnsi(chunk.toString()));
1516
+ return origStdoutWrite(chunk, enc, cb);
1517
+ };
1518
+ process.stderr.write = (chunk, enc, cb) => {
1519
+ if (!isProgressChunk(chunk)) logStream.write(stripAnsi(chunk.toString()));
1520
+ return origStderrWrite(chunk, enc, cb);
1521
+ };
1522
+ process.on("exit", () => logStream.end());
1523
+ }
1524
+ var cwd = process.cwd();
1525
+ var vitestArgs = [...args.vitestPassthroughArgs];
65
1526
  if (args.coverageQuiet && !vitestArgs.some((a) => a === "--coverage" || a.startsWith("--coverage."))) {
66
- vitestArgs.unshift("--coverage");
1527
+ vitestArgs.unshift("--coverage");
67
1528
  }
68
-
69
1529
  try {
70
- const runResult = await run({
71
- cwd,
72
- testPatterns: args.testPatterns,
73
- testListFile: args.testListFile,
74
- testFilePattern: args.testFilePattern,
75
- vitestArgs,
76
- showErrorDetails: args.showErrorDetails,
77
- coverageQuiet: args.coverageQuiet,
78
- suppressFileOutput: args.suppressFileOutput,
79
- suppressPassingFiles: args.suppressPassingFiles,
80
- topSummary: args.topSummary,
81
- json: args.json,
82
- ...(args.workers !== undefined && { workers: args.workers }),
83
- ...(args.soloPatterns.length > 0 && { earlyRunPatterns: args.soloPatterns })
84
- });
85
-
86
- if (args.json) {
87
- process.stdout.write(`${JSON.stringify(runResult, null, 2)}\n`);
88
- process.exit(typeof runResult === "object" && runResult !== null && "exitCode" in runResult ? runResult.exitCode : 1);
89
- }
90
-
91
- process.exit(typeof runResult === "number" ? runResult : 1);
1530
+ const runResult = await run({
1531
+ cwd,
1532
+ testPatterns: args.testPatterns,
1533
+ testListFile: args.testListFile,
1534
+ testFilePattern: args.testFilePattern,
1535
+ vitestArgs,
1536
+ showErrorDetails: args.showErrorDetails,
1537
+ coverageQuiet: args.coverageQuiet,
1538
+ suppressFileOutput: args.suppressFileOutput,
1539
+ suppressPassingFiles: args.suppressPassingFiles,
1540
+ topSummary: args.topSummary,
1541
+ json: args.json,
1542
+ mergeReports: args.mergeReports,
1543
+ keepTmp: args.keepTmp,
1544
+ ...args.blobsDir !== void 0 && { blobsDir: args.blobsDir },
1545
+ ...args.workers !== void 0 && { workers: args.workers },
1546
+ ...args.soloPatterns.length > 0 && { earlyRunPatterns: args.soloPatterns },
1547
+ ...args.scratchDir !== void 0 && { scratchDir: args.scratchDir }
1548
+ });
1549
+ if (args.json) {
1550
+ process.stdout.write(`${JSON.stringify(runResult, null, 2)}
1551
+ `);
1552
+ process.exit(typeof runResult === "object" && runResult !== null && "exitCode" in runResult ? runResult.exitCode : 1);
1553
+ }
1554
+ process.exit(typeof runResult === "number" ? runResult : 1);
92
1555
  } catch (err) {
93
- console.error("Fatal error:", err);
94
- process.exit(1);
1556
+ console.error("Fatal error:", err);
1557
+ process.exit(1);
95
1558
  }
1559
+ //# sourceMappingURL=vitest-runner.mjs.map