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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Result reporting and coverage summary utilities.
|
|
3
|
+
* @module vitest-runner/src/core/report
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { stripAnsi, colourPct } from "../utils/ansi.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Print verbose output for files that failed during a quiet coverage run.
|
|
13
|
+
*
|
|
14
|
+
* @param {Array<{file: string, code: number, rawOutput: string}>} failedResults
|
|
15
|
+
* @returns {void}
|
|
16
|
+
* @example
|
|
17
|
+
* printQuietCoverageFailureDetails(failedResults);
|
|
18
|
+
*/
|
|
19
|
+
export function printQuietCoverageFailureDetails(failedResults) {
|
|
20
|
+
if (failedResults.length === 0) return;
|
|
21
|
+
|
|
22
|
+
console.log(`\n${"=".repeat(80)}`);
|
|
23
|
+
console.log(chalk.bold.red("✖ FAILED TEST FILES (VERBOSE OUTPUT)"));
|
|
24
|
+
console.log("=".repeat(80));
|
|
25
|
+
|
|
26
|
+
for (const r of failedResults) {
|
|
27
|
+
console.log(`\n${chalk.red("✖")} ${chalk.red(r.file)} ${chalk.dim(`(exit ${r.code})`)}`);
|
|
28
|
+
if (r.rawOutput?.trim()) {
|
|
29
|
+
console.log(r.rawOutput.trimEnd());
|
|
30
|
+
} else {
|
|
31
|
+
console.log(chalk.dim("(no child output captured)"));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`\n${"=".repeat(80)}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Print the captured output from a quiet `--mergeReports` step.
|
|
40
|
+
*
|
|
41
|
+
* On success prints only the coverage block (from "% Coverage report from v8").
|
|
42
|
+
* On failure prints the full raw output to stderr.
|
|
43
|
+
*
|
|
44
|
+
* @param {number} exitCode - The merge process exit code.
|
|
45
|
+
* @param {string} output - Raw stdout + stderr from the merge process.
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
export function printMergeOutput(exitCode, output) {
|
|
49
|
+
if (exitCode === 0) {
|
|
50
|
+
const marker = "% Coverage report from v8";
|
|
51
|
+
const rawLines = output.split("\n");
|
|
52
|
+
const markerLineIndex = rawLines.findIndex((line) => stripAnsi(line).includes(marker));
|
|
53
|
+
|
|
54
|
+
if (markerLineIndex >= 0) {
|
|
55
|
+
let endLineIndex = rawLines.length;
|
|
56
|
+
for (let i = markerLineIndex + 1; i < rawLines.length; i++) {
|
|
57
|
+
const line = stripAnsi(rawLines[i]).trimStart();
|
|
58
|
+
if (line.startsWith("stderr |") || line.startsWith("stdout |")) {
|
|
59
|
+
endLineIndex = i;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const coverageBody = rawLines
|
|
65
|
+
.slice(markerLineIndex + 1, endLineIndex)
|
|
66
|
+
.join("\n")
|
|
67
|
+
.trimEnd();
|
|
68
|
+
if (coverageBody) {
|
|
69
|
+
const fullBlock = rawLines.slice(markerLineIndex, endLineIndex).join("\n").trimEnd();
|
|
70
|
+
console.log(`\n${fullBlock}\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const trimmed = output.trimEnd();
|
|
75
|
+
if (trimmed) console.error(trimmed);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compute a coverage-summary-style object from a raw V8/Istanbul `coverage-final.json`.
|
|
81
|
+
*
|
|
82
|
+
* @param {Record<string, object>} finalData - Parsed `coverage-final.json` contents.
|
|
83
|
+
* @returns {{ total: object, [filePath: string]: object }} Istanbul coverage-summary format.
|
|
84
|
+
*/
|
|
85
|
+
export function computeSummaryFromFinal(finalData) {
|
|
86
|
+
const pct = (covered, total) => (total === 0 ? 100 : parseFloat(((covered / total) * 100).toFixed(2)));
|
|
87
|
+
const summary = {
|
|
88
|
+
total: {
|
|
89
|
+
statements: { total: 0, covered: 0, pct: 0 },
|
|
90
|
+
branches: { total: 0, covered: 0, pct: 0 },
|
|
91
|
+
functions: { total: 0, covered: 0, pct: 0 },
|
|
92
|
+
lines: { total: 0, covered: 0, pct: 0 }
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const [filePath, data] of Object.entries(finalData)) {
|
|
97
|
+
const sKeys = Object.keys(data.s ?? {});
|
|
98
|
+
const stmtTotal = sKeys.length;
|
|
99
|
+
const stmtCovered = sKeys.filter((k) => data.s[k] > 0).length;
|
|
100
|
+
|
|
101
|
+
const fKeys = Object.keys(data.f ?? {});
|
|
102
|
+
const fnTotal = fKeys.length;
|
|
103
|
+
const fnCovered = fKeys.filter((k) => data.f[k] > 0).length;
|
|
104
|
+
|
|
105
|
+
let branchTotal = 0,
|
|
106
|
+
branchCovered = 0;
|
|
107
|
+
for (const counts of Object.values(data.b ?? {})) {
|
|
108
|
+
branchTotal += counts.length;
|
|
109
|
+
branchCovered += counts.filter((c) => c > 0).length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const coveredLines = new Set();
|
|
113
|
+
const totalLines = new Set();
|
|
114
|
+
for (const [sid, loc] of Object.entries(data.statementMap ?? {})) {
|
|
115
|
+
const line = loc?.start?.line;
|
|
116
|
+
if (line == null) continue;
|
|
117
|
+
totalLines.add(line);
|
|
118
|
+
if ((data.s ?? {})[sid] > 0) coveredLines.add(line);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fileStats = {
|
|
122
|
+
statements: { total: stmtTotal, covered: stmtCovered, pct: pct(stmtCovered, stmtTotal) },
|
|
123
|
+
branches: { total: branchTotal, covered: branchCovered, pct: pct(branchCovered, branchTotal) },
|
|
124
|
+
functions: { total: fnTotal, covered: fnCovered, pct: pct(fnCovered, fnTotal) },
|
|
125
|
+
lines: { total: totalLines.size, covered: coveredLines.size, pct: pct(coveredLines.size, totalLines.size) }
|
|
126
|
+
};
|
|
127
|
+
summary[filePath] = fileStats;
|
|
128
|
+
|
|
129
|
+
for (const key of ["statements", "branches", "functions", "lines"]) {
|
|
130
|
+
summary.total[key].total += fileStats[key].total;
|
|
131
|
+
summary.total[key].covered += fileStats[key].covered;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const key of ["statements", "branches", "functions", "lines"]) {
|
|
136
|
+
const { total, covered } = summary.total[key];
|
|
137
|
+
summary.total[key].pct = pct(covered, total);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return summary;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Read the coverage JSON produced after a `mergeReports` run and print a
|
|
145
|
+
* worst-offenders table plus overall-coverage totals.
|
|
146
|
+
*
|
|
147
|
+
* Tries `coverage-summary.json` first; falls back to computing from
|
|
148
|
+
* `coverage-final.json` if that is not present.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} cwd - Project root (used to make absolute file paths relative).
|
|
151
|
+
* @param {string[]} extraCoverageArgs - Passthrough `--coverage.*` args (checked for `reportsDirectory`).
|
|
152
|
+
* @param {number} [worstCount=10] - Number of worst-coverage files to show (0 = skip table).
|
|
153
|
+
* @returns {Promise<void>}
|
|
154
|
+
*/
|
|
155
|
+
export async function printCoverageSummary(cwd, extraCoverageArgs, worstCount = 10) {
|
|
156
|
+
let coverageDir = path.resolve(cwd, "coverage");
|
|
157
|
+
const repoDirArg = extraCoverageArgs.find((a) => a.startsWith("--coverage.reportsDirectory="));
|
|
158
|
+
if (repoDirArg) {
|
|
159
|
+
const raw = repoDirArg.split("=").slice(1).join("=");
|
|
160
|
+
coverageDir = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let summary;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const content = await fs.readFile(path.join(coverageDir, "coverage-summary.json"), "utf8");
|
|
167
|
+
summary = JSON.parse(content);
|
|
168
|
+
} catch {
|
|
169
|
+
try {
|
|
170
|
+
const content = await fs.readFile(path.join(coverageDir, "coverage-final.json"), "utf8");
|
|
171
|
+
summary = computeSummaryFromFinal(JSON.parse(content));
|
|
172
|
+
} catch {
|
|
173
|
+
console.log(chalk.dim(" (no coverage JSON found — skipping summary)"));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { total, ...fileSummaries } = summary;
|
|
179
|
+
|
|
180
|
+
if (worstCount > 0) {
|
|
181
|
+
const fileRows = Object.entries(fileSummaries)
|
|
182
|
+
.map(([absFile, data]) => ({
|
|
183
|
+
file: path.relative(cwd, absFile),
|
|
184
|
+
lines: data.lines?.pct ?? 0,
|
|
185
|
+
stmts: data.statements?.pct ?? 0,
|
|
186
|
+
fns: data.functions?.pct ?? 0,
|
|
187
|
+
branches: data.branches?.pct ?? 0
|
|
188
|
+
}))
|
|
189
|
+
.sort((a, b) => a.lines - b.lines);
|
|
190
|
+
|
|
191
|
+
console.log("\n" + chalk.bold("📉 WORST COVERAGE FILES (lines)"));
|
|
192
|
+
console.log("-".repeat(80));
|
|
193
|
+
|
|
194
|
+
fileRows.slice(0, worstCount).forEach(({ file, lines, stmts, fns, branches }) => {
|
|
195
|
+
const extras = chalk.dim(`stmts ${stmts.toFixed(0)}% | fns ${fns.toFixed(0)}% | branches ${branches.toFixed(0)}%`);
|
|
196
|
+
console.log(` ${colourPct(chalk, lines)}% ${chalk.dim(file)} ${extras}`);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (fileRows.length > worstCount) {
|
|
200
|
+
console.log(chalk.dim(` ... and ${fileRows.length - worstCount} more files`));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const tl = total.lines?.pct ?? 0;
|
|
205
|
+
const ts = total.statements?.pct ?? 0;
|
|
206
|
+
const tf = total.functions?.pct ?? 0;
|
|
207
|
+
const tb = total.branches?.pct ?? 0;
|
|
208
|
+
console.log(
|
|
209
|
+
`\n ${chalk.bold("Coverage")} ${colourPct(chalk, tl)}% lines ${chalk.dim("|")} ${colourPct(chalk, ts)}% statements ${chalk.dim("|")} ${colourPct(chalk, tf)}% functions ${chalk.dim("|")} ${colourPct(chalk, tb)}% branches`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Child-process spawning helpers for running vitest.
|
|
3
|
+
* @module vitest-runner/src/core/spawn
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { parseVitestOutput } from "./parse.mjs";
|
|
8
|
+
import { buildNodeOptions } from "../utils/env.mjs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} SpawnBaseOptions
|
|
12
|
+
* @property {string} cwd - Working directory for the child process.
|
|
13
|
+
* @property {string} vitestBin - Absolute path to the vitest binary.
|
|
14
|
+
* @property {string|undefined} vitestConfig - Vitest config path (omit to let vitest auto-detect).
|
|
15
|
+
* @property {number|undefined} maxOldSpaceMb - Optional `--max-old-space-size` ceiling.
|
|
16
|
+
* @property {string[]} [conditions=[]] - Additional `--conditions` flags.
|
|
17
|
+
* @property {string} [nodeEnv='development'] - Value for `NODE_ENV`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the environment object for a vitest child process.
|
|
22
|
+
* @param {Pick<SpawnBaseOptions, 'maxOldSpaceMb'|'conditions'|'nodeEnv'>} opts
|
|
23
|
+
* @returns {NodeJS.ProcessEnv}
|
|
24
|
+
*/
|
|
25
|
+
function buildEnv({ maxOldSpaceMb, conditions = [], nodeEnv = "development" }) {
|
|
26
|
+
const env = { ...process.env };
|
|
27
|
+
if (!env.NODE_ENV) env.NODE_ENV = nodeEnv;
|
|
28
|
+
|
|
29
|
+
const nodeOptions = buildNodeOptions({ maxOldSpaceMb, conditions, base: env.NODE_OPTIONS ?? "" });
|
|
30
|
+
if (nodeOptions) env.NODE_OPTIONS = nodeOptions;
|
|
31
|
+
|
|
32
|
+
return env;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build the base argument list `[vitestBin, ...configArgs, 'run']`.
|
|
37
|
+
* @param {string} vitestBin
|
|
38
|
+
* @param {string|undefined} vitestConfig
|
|
39
|
+
* @returns {string[]}
|
|
40
|
+
*/
|
|
41
|
+
function buildBaseArgs(vitestBin, vitestConfig) {
|
|
42
|
+
const configArgs = vitestConfig ? ["--config", vitestConfig] : [];
|
|
43
|
+
return [vitestBin, ...configArgs, "run"];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} SingleFileResult
|
|
48
|
+
* @property {string} file - Test file path.
|
|
49
|
+
* @property {number} code - Process exit code.
|
|
50
|
+
* @property {number} duration - Run duration in milliseconds.
|
|
51
|
+
* @property {number} testFilesPass
|
|
52
|
+
* @property {number} testFilesFail
|
|
53
|
+
* @property {number} testsPass
|
|
54
|
+
* @property {number} testsFail
|
|
55
|
+
* @property {number} testsSkip
|
|
56
|
+
* @property {number|null} heapMb
|
|
57
|
+
* @property {string[]} errors
|
|
58
|
+
* @property {string} rawOutput
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Run a single Vitest test file in a child process and return parsed results.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} filePath - Test file path (relative to `cwd` or absolute).
|
|
65
|
+
* @param {SpawnBaseOptions & { vitestArgs?: string[], streamOutput?: boolean }} opts
|
|
66
|
+
* @returns {Promise<SingleFileResult>}
|
|
67
|
+
* @example
|
|
68
|
+
* const result = await runSingleFile('src/tests/foo.test.vitest.mjs', {
|
|
69
|
+
* cwd: '/project',
|
|
70
|
+
* vitestBin: '/project/node_modules/.bin/vitest',
|
|
71
|
+
* vitestConfig: '/project/vitest.config.ts',
|
|
72
|
+
* });
|
|
73
|
+
*/
|
|
74
|
+
export function runSingleFile(filePath, opts) {
|
|
75
|
+
const {
|
|
76
|
+
cwd,
|
|
77
|
+
vitestBin,
|
|
78
|
+
vitestConfig,
|
|
79
|
+
maxOldSpaceMb,
|
|
80
|
+
conditions = [],
|
|
81
|
+
nodeEnv = "development",
|
|
82
|
+
vitestArgs = [],
|
|
83
|
+
streamOutput = true
|
|
84
|
+
} = opts;
|
|
85
|
+
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const startTime = Date.now();
|
|
88
|
+
const args = [...buildBaseArgs(vitestBin, vitestConfig), ...vitestArgs, filePath];
|
|
89
|
+
const env = buildEnv({ maxOldSpaceMb, conditions, nodeEnv });
|
|
90
|
+
|
|
91
|
+
const child = spawn(process.execPath, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env });
|
|
92
|
+
|
|
93
|
+
let stdout = "";
|
|
94
|
+
let stderr = "";
|
|
95
|
+
|
|
96
|
+
child.stdout?.on("data", (data) => {
|
|
97
|
+
stdout += data.toString();
|
|
98
|
+
if (streamOutput) process.stdout.write(data);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
child.stderr?.on("data", (data) => {
|
|
102
|
+
stderr += data.toString();
|
|
103
|
+
if (streamOutput) process.stderr.write(data);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
child.on("close", (code) => {
|
|
107
|
+
const spawnDuration = Date.now() - startTime;
|
|
108
|
+
const output = `${stdout}\n${stderr}`;
|
|
109
|
+
const parsed = parseVitestOutput(output);
|
|
110
|
+
|
|
111
|
+
resolve({
|
|
112
|
+
file: filePath,
|
|
113
|
+
code: code ?? 1,
|
|
114
|
+
// c8 ignore next -- false branch verified manually; V8 ternary probe mismatch inside object literal
|
|
115
|
+
duration: parsed.duration > 0 ? parsed.duration : spawnDuration,
|
|
116
|
+
testFilesPass: parsed.testFilesPass,
|
|
117
|
+
testFilesFail: parsed.testFilesFail,
|
|
118
|
+
testsPass: parsed.testsPass,
|
|
119
|
+
testsFail: parsed.testsFail,
|
|
120
|
+
testsSkip: parsed.testsSkip,
|
|
121
|
+
heapMb: parsed.heapMb,
|
|
122
|
+
errors: parsed.errors,
|
|
123
|
+
rawOutput: output
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
child.on("error", (err) => {
|
|
128
|
+
resolve({
|
|
129
|
+
file: filePath,
|
|
130
|
+
code: 1,
|
|
131
|
+
duration: Date.now() - startTime,
|
|
132
|
+
testFilesPass: 0,
|
|
133
|
+
testFilesFail: 1,
|
|
134
|
+
testsPass: 0,
|
|
135
|
+
testsFail: 0,
|
|
136
|
+
testsSkip: 0,
|
|
137
|
+
heapMb: null,
|
|
138
|
+
errors: [err.message],
|
|
139
|
+
rawOutput: err.toString()
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run Vitest directly (all files in one process) with inherited stdio.
|
|
147
|
+
*
|
|
148
|
+
* @param {SpawnBaseOptions & { vitestArgs?: string[] }} opts
|
|
149
|
+
* @returns {Promise<number>} Process exit code.
|
|
150
|
+
* @example
|
|
151
|
+
* const code = await runVitestDirect({
|
|
152
|
+
* cwd: '/project',
|
|
153
|
+
* vitestBin: '/project/node_modules/.bin/vitest',
|
|
154
|
+
* vitestArgs: ['--reporter=verbose'],
|
|
155
|
+
* });
|
|
156
|
+
*/
|
|
157
|
+
export function runVitestDirect(opts) {
|
|
158
|
+
const { cwd, vitestBin, vitestConfig, maxOldSpaceMb, conditions = [], nodeEnv = "development", vitestArgs = [] } = opts;
|
|
159
|
+
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
const args = [...buildBaseArgs(vitestBin, vitestConfig), ...vitestArgs];
|
|
162
|
+
const env = buildEnv({ maxOldSpaceMb, conditions, nodeEnv });
|
|
163
|
+
|
|
164
|
+
const child = spawn(process.execPath, args, { cwd, stdio: "inherit", env });
|
|
165
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
166
|
+
child.on("error", () => resolve(1));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Merge blob reports from individual coverage runs into a single coverage report
|
|
172
|
+
* using `vitest --mergeReports`.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} blobsDir - Directory containing the `.blob` files to merge.
|
|
175
|
+
* @param {SpawnBaseOptions & { extraCoverageArgs?: string[], quietOutput?: boolean }} opts
|
|
176
|
+
* @returns {Promise<{ exitCode: number, output: string }>}
|
|
177
|
+
* @example
|
|
178
|
+
* const { exitCode } = await runMergeReports('/project/.vitest-blobs', {
|
|
179
|
+
* cwd: '/project',
|
|
180
|
+
* vitestBin: '/project/node_modules/.bin/vitest',
|
|
181
|
+
* });
|
|
182
|
+
*/
|
|
183
|
+
export function runMergeReports(blobsDir, opts) {
|
|
184
|
+
const {
|
|
185
|
+
cwd,
|
|
186
|
+
vitestBin,
|
|
187
|
+
vitestConfig,
|
|
188
|
+
maxOldSpaceMb,
|
|
189
|
+
conditions = [],
|
|
190
|
+
nodeEnv = "development",
|
|
191
|
+
extraCoverageArgs = [],
|
|
192
|
+
quietOutput = false
|
|
193
|
+
} = opts;
|
|
194
|
+
|
|
195
|
+
return new Promise((resolve) => {
|
|
196
|
+
const configArgs = vitestConfig ? ["--config", vitestConfig] : [];
|
|
197
|
+
const mergeReporterArgs = quietOutput ? ["--color"] : [];
|
|
198
|
+
|
|
199
|
+
const args = [vitestBin, ...configArgs, "--mergeReports", blobsDir, "--run", "--coverage", ...mergeReporterArgs, ...extraCoverageArgs];
|
|
200
|
+
|
|
201
|
+
const env = buildEnv({ maxOldSpaceMb, conditions, nodeEnv });
|
|
202
|
+
const child = spawn(process.execPath, args, {
|
|
203
|
+
cwd,
|
|
204
|
+
stdio: quietOutput ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
205
|
+
env
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
let stdout = "";
|
|
209
|
+
let stderr = "";
|
|
210
|
+
|
|
211
|
+
if (quietOutput) {
|
|
212
|
+
child.stdout?.on("data", (data) => (stdout += data.toString()));
|
|
213
|
+
child.stderr?.on("data", (data) => (stderr += data.toString()));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, output: `${stdout}\n${stderr}` }));
|
|
217
|
+
child.on("error", () => resolve({ exitCode: 1, output: "" }));
|
|
218
|
+
});
|
|
219
|
+
}
|