@mark1russell7/client-vitest 0.0.0-stage → 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +440 -2
- package/README_NEW.md +513 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +11 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +83 -0
- package/dist/module.js.map +1 -0
- package/dist/procedures/vitest/index.d.ts +5 -0
- package/dist/procedures/vitest/index.d.ts.map +1 -0
- package/dist/procedures/vitest/index.js +5 -0
- package/dist/procedures/vitest/index.js.map +1 -0
- package/dist/procedures/vitest/list.d.ts +13 -0
- package/dist/procedures/vitest/list.d.ts.map +1 -0
- package/dist/procedures/vitest/list.js +32 -0
- package/dist/procedures/vitest/list.js.map +1 -0
- package/dist/procedures/vitest/run.d.ts +17 -0
- package/dist/procedures/vitest/run.d.ts.map +1 -0
- package/dist/procedures/vitest/run.js +110 -0
- package/dist/procedures/vitest/run.js.map +1 -0
- package/dist/procedures/vitest/stop.d.ts +13 -0
- package/dist/procedures/vitest/stop.d.ts.map +1 -0
- package/dist/procedures/vitest/stop.js +25 -0
- package/dist/procedures/vitest/stop.js.map +1 -0
- package/dist/procedures/vitest/watch.d.ts +14 -0
- package/dist/procedures/vitest/watch.d.ts.map +1 -0
- package/dist/procedures/vitest/watch.js +37 -0
- package/dist/procedures/vitest/watch.js.map +1 -0
- package/dist/register.d.ts +8 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +14 -0
- package/dist/register.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +80 -0
- package/dist/types.js.map +1 -0
- package/dist/vitest-cli.d.ts +10 -0
- package/dist/vitest-cli.d.ts.map +1 -0
- package/dist/vitest-cli.js +28 -0
- package/dist/vitest-cli.js.map +1 -0
- package/package.json +74 -3
- package/src/index.ts +3 -0
- package/src/module.ts +104 -0
- package/src/procedures/vitest/index.ts +4 -0
- package/src/procedures/vitest/list.ts +33 -0
- package/src/procedures/vitest/run.ts +145 -0
- package/src/procedures/vitest/stop.ts +29 -0
- package/src/procedures/vitest/watch.ts +46 -0
- package/src/register.ts +17 -0
- package/src/types.ts +164 -0
- package/src/vitest-cli.ts +28 -0
package/src/module.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module (no side effects): the vitest procedures as data, for `createRegistry()`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createProcedure, zodAdapter, defineModule, type ProcedureModule } from "@mark1russell7/client";
|
|
6
|
+
import {
|
|
7
|
+
VitestCoverageInputSchema,
|
|
8
|
+
VitestListInputSchema,
|
|
9
|
+
VitestRunInputSchema,
|
|
10
|
+
VitestStopInputSchema,
|
|
11
|
+
VitestWatchInputSchema,
|
|
12
|
+
type VitestListOutput,
|
|
13
|
+
type VitestRunOutput,
|
|
14
|
+
type VitestStopOutput,
|
|
15
|
+
type VitestWatchOutput,
|
|
16
|
+
VitestRunOutputSchema,
|
|
17
|
+
VitestWatchOutputSchema,
|
|
18
|
+
VitestStopOutputSchema,
|
|
19
|
+
VitestListOutputSchema,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
import { vitestCoverage, vitestRun } from "./procedures/vitest/run.js";
|
|
22
|
+
import { vitestWatch } from "./procedures/vitest/watch.js";
|
|
23
|
+
import { vitestStop } from "./procedures/vitest/stop.js";
|
|
24
|
+
import { vitestList } from "./procedures/vitest/list.js";
|
|
25
|
+
|
|
26
|
+
const vitestRunProcedure = createProcedure()
|
|
27
|
+
.path(["vitest", "run"])
|
|
28
|
+
.input(zodAdapter(VitestRunInputSchema))
|
|
29
|
+
.output(zodAdapter<VitestRunOutput>(VitestRunOutputSchema))
|
|
30
|
+
.meta({
|
|
31
|
+
description: "Run vitest tests once (no shell; uses the project's own vitest)",
|
|
32
|
+
// Positional field names, and field -> short flag (the convention mark's CLI parser reads)
|
|
33
|
+
args: ["cwd"],
|
|
34
|
+
shorts: { coverage: "c" },
|
|
35
|
+
output: "json",
|
|
36
|
+
})
|
|
37
|
+
.handler(vitestRun)
|
|
38
|
+
.build();
|
|
39
|
+
|
|
40
|
+
const vitestCoverageProcedure = createProcedure()
|
|
41
|
+
.path(["vitest", "coverage"])
|
|
42
|
+
.input(zodAdapter(VitestCoverageInputSchema))
|
|
43
|
+
.output(zodAdapter<VitestRunOutput>(VitestRunOutputSchema))
|
|
44
|
+
.meta({
|
|
45
|
+
description: "Run vitest once with coverage, and check a minimum line coverage (no shell)",
|
|
46
|
+
args: ["cwd"],
|
|
47
|
+
shorts: { threshold: "t" },
|
|
48
|
+
output: "json",
|
|
49
|
+
})
|
|
50
|
+
.handler(vitestCoverage)
|
|
51
|
+
.build();
|
|
52
|
+
|
|
53
|
+
const vitestWatchProcedure = createProcedure()
|
|
54
|
+
.path(["vitest", "watch"])
|
|
55
|
+
.input(zodAdapter(VitestWatchInputSchema))
|
|
56
|
+
.output(zodAdapter<VitestWatchOutput>(VitestWatchOutputSchema))
|
|
57
|
+
.meta({
|
|
58
|
+
description: "Start vitest in watch mode (no shell; uses the project's own vitest)",
|
|
59
|
+
args: ["cwd"],
|
|
60
|
+
shorts: {},
|
|
61
|
+
output: "json",
|
|
62
|
+
})
|
|
63
|
+
.handler(vitestWatch)
|
|
64
|
+
.build();
|
|
65
|
+
|
|
66
|
+
const vitestStopProcedure = createProcedure()
|
|
67
|
+
.path(["vitest", "stop"])
|
|
68
|
+
.input(zodAdapter(VitestStopInputSchema))
|
|
69
|
+
.output(zodAdapter<VitestStopOutput>(VitestStopOutputSchema))
|
|
70
|
+
.meta({
|
|
71
|
+
description: "Stop a vitest watch process (all of them without an id)",
|
|
72
|
+
args: ["id"],
|
|
73
|
+
shorts: {},
|
|
74
|
+
output: "json",
|
|
75
|
+
})
|
|
76
|
+
.handler(vitestStop)
|
|
77
|
+
.build();
|
|
78
|
+
|
|
79
|
+
const vitestListProcedure = createProcedure()
|
|
80
|
+
.path(["vitest", "list"])
|
|
81
|
+
.input(zodAdapter(VitestListInputSchema))
|
|
82
|
+
.output(zodAdapter<VitestListOutput>(VitestListOutputSchema))
|
|
83
|
+
.meta({
|
|
84
|
+
description: "List the vitest watch processes of this host",
|
|
85
|
+
args: [],
|
|
86
|
+
shorts: {},
|
|
87
|
+
output: "json",
|
|
88
|
+
})
|
|
89
|
+
.handler(vitestList)
|
|
90
|
+
.build();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The module of this package, for `createRegistry()`: its procedures as data. Importing this
|
|
94
|
+
* file registers nothing. (`register.ts` registers the same procedures in the global registry.)
|
|
95
|
+
*/
|
|
96
|
+
export const vitestModule: ProcedureModule = defineModule("@mark1russell7/client-vitest", [
|
|
97
|
+
vitestRunProcedure,
|
|
98
|
+
vitestCoverageProcedure,
|
|
99
|
+
vitestWatchProcedure,
|
|
100
|
+
vitestStopProcedure,
|
|
101
|
+
vitestListProcedure,
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
export default vitestModule;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vitest.list procedure
|
|
3
|
+
*
|
|
4
|
+
* List the vitest watch processes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { processes } from "@mark1russell7/client-shell/command";
|
|
8
|
+
import type { VitestListInput, VitestListOutput, VitestProcessInfo } from "../../types.js";
|
|
9
|
+
import { VITEST_GROUP } from "./watch.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* List the vitest watch processes of this host: the running ones and the recent exits
|
|
13
|
+
*/
|
|
14
|
+
export async function vitestList(
|
|
15
|
+
input: VitestListInput,
|
|
16
|
+
_ctx: { metadata: Record<string, unknown> }
|
|
17
|
+
): Promise<VitestListOutput> {
|
|
18
|
+
return {
|
|
19
|
+
processes: processes.list({ group: VITEST_GROUP }).map((info) => {
|
|
20
|
+
const entry: VitestProcessInfo = {
|
|
21
|
+
id: info.id,
|
|
22
|
+
pid: info.pid,
|
|
23
|
+
status: info.status,
|
|
24
|
+
startedAt: info.startedAt,
|
|
25
|
+
};
|
|
26
|
+
if (info.cwd !== undefined) entry.cwd = info.cwd;
|
|
27
|
+
if (info.exitedAt !== undefined) entry.exitedAt = info.exitedAt;
|
|
28
|
+
if (info.exitCode !== undefined) entry.exitCode = info.exitCode;
|
|
29
|
+
if (input.output) entry.output = processes.output(info.id);
|
|
30
|
+
return entry;
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vitest.run and vitest.coverage: run the tests of a project once, with the project's own vitest.
|
|
3
|
+
*
|
|
4
|
+
* The result has the counts from vitest's JSON report, the exit code and the end of the output.
|
|
5
|
+
* With `coverage`, it also has the coverage percentages from a `json-summary` report. Both reports
|
|
6
|
+
* go to a temporary folder. (vitest.coverage replaces test.coverage of the retired client-test.)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { runCommand } from "@mark1russell7/client-shell/command";
|
|
13
|
+
import type { VitestCoverageInput, VitestRunInput, VitestRunOutput } from "../../types.js";
|
|
14
|
+
import { resolveVitestCli } from "../../vitest-cli.js";
|
|
15
|
+
|
|
16
|
+
/** The procedure keeps the last 64 KiB of each output stream. */
|
|
17
|
+
const OUTPUT_LIMIT = 64 * 1024;
|
|
18
|
+
|
|
19
|
+
function assertPattern(pattern: string): string {
|
|
20
|
+
if (pattern.startsWith("-")) {
|
|
21
|
+
throw new Error(`Invalid test pattern (starts with "-"): ${pattern}`);
|
|
22
|
+
}
|
|
23
|
+
return pattern;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface VitestJsonReport {
|
|
27
|
+
success?: boolean;
|
|
28
|
+
numPassedTests?: number;
|
|
29
|
+
numFailedTests?: number;
|
|
30
|
+
numPendingTests?: number;
|
|
31
|
+
startTime?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CoverageSummary {
|
|
35
|
+
total?: Record<"lines" | "branches" | "functions" | "statements", { pct?: number } | undefined>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readCoverage(file: string): VitestRunOutput["coverage"] {
|
|
39
|
+
if (!existsSync(file)) return undefined;
|
|
40
|
+
const total = (JSON.parse(readFileSync(file, "utf8")) as CoverageSummary).total;
|
|
41
|
+
if (!total) return undefined;
|
|
42
|
+
return {
|
|
43
|
+
lines: total.lines?.pct ?? 0,
|
|
44
|
+
branches: total.branches?.pct ?? 0,
|
|
45
|
+
functions: total.functions?.pct ?? 0,
|
|
46
|
+
statements: total.statements?.pct ?? 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The part of the procedure context that the vitest procedures read. */
|
|
51
|
+
interface VitestContext {
|
|
52
|
+
metadata: Record<string, unknown>;
|
|
53
|
+
signal?: AbortSignal | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function vitestRun(
|
|
57
|
+
input: VitestRunInput,
|
|
58
|
+
ctx: VitestContext
|
|
59
|
+
): Promise<VitestRunOutput> {
|
|
60
|
+
if (input.watch) {
|
|
61
|
+
throw new Error("vitest.run runs the tests once. Use vitest.watch for watch mode.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cwd = input.cwd ?? process.cwd();
|
|
65
|
+
const cli = resolveVitestCli(cwd);
|
|
66
|
+
|
|
67
|
+
// The reports go to files, so the reporter of the reader prints to stdout
|
|
68
|
+
const reportDir = mkdtempSync(join(tmpdir(), "vitest-run-"));
|
|
69
|
+
const reportFile = join(reportDir, "report.json");
|
|
70
|
+
const coverageDir = join(reportDir, "coverage");
|
|
71
|
+
|
|
72
|
+
const reporter = input.reporter && input.reporter !== "json" ? input.reporter : "default";
|
|
73
|
+
const args = [cli, "run", "--reporter", reporter, "--reporter", "json", "--outputFile.json", reportFile];
|
|
74
|
+
for (const pattern of input.include ?? []) {
|
|
75
|
+
args.push(assertPattern(pattern));
|
|
76
|
+
}
|
|
77
|
+
for (const pattern of input.exclude ?? []) {
|
|
78
|
+
args.push("--exclude", assertPattern(pattern));
|
|
79
|
+
}
|
|
80
|
+
if (input.coverage) {
|
|
81
|
+
args.push(
|
|
82
|
+
"--coverage.enabled",
|
|
83
|
+
"--coverage.reporter=json-summary",
|
|
84
|
+
"--coverage.reporter=text-summary",
|
|
85
|
+
`--coverage.reportsDirectory=${coverageDir}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (input.passWithNoTests) {
|
|
89
|
+
args.push("--passWithNoTests");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const started = Date.now();
|
|
93
|
+
// No shell: test patterns cannot inject commands. The signal kills vitest and its workers.
|
|
94
|
+
const run = await runCommand(process.execPath, {
|
|
95
|
+
args,
|
|
96
|
+
cwd,
|
|
97
|
+
signal: ctx.signal,
|
|
98
|
+
maxOutputBytes: OUTPUT_LIMIT,
|
|
99
|
+
keep: "tail",
|
|
100
|
+
});
|
|
101
|
+
if (run.error) {
|
|
102
|
+
rmSync(reportDir, { recursive: true, force: true });
|
|
103
|
+
throw new Error(`vitest did not start: ${run.error}`);
|
|
104
|
+
}
|
|
105
|
+
const code = run.signal ? null : run.exitCode;
|
|
106
|
+
|
|
107
|
+
const output = { exitCode: code ?? -1, stdout: run.stdout, stderr: run.stderr };
|
|
108
|
+
try {
|
|
109
|
+
const coverage = input.coverage ? readCoverage(join(coverageDir, "coverage-summary.json")) : undefined;
|
|
110
|
+
let report: VitestJsonReport | undefined;
|
|
111
|
+
try {
|
|
112
|
+
report = JSON.parse(readFileSync(reportFile, "utf8")) as VitestJsonReport;
|
|
113
|
+
} catch {
|
|
114
|
+
// No report: vitest failed before it ran the tests (for example, a config error)
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
success: report?.success ?? code === 0,
|
|
118
|
+
passed: report?.numPassedTests ?? 0,
|
|
119
|
+
failed: report?.numFailedTests ?? (code === 0 ? 0 : 1),
|
|
120
|
+
skipped: report?.numPendingTests ?? 0,
|
|
121
|
+
duration: report?.startTime ? Date.now() - report.startTime : Date.now() - started,
|
|
122
|
+
...output,
|
|
123
|
+
...(coverage ? { coverage } : {}),
|
|
124
|
+
};
|
|
125
|
+
} finally {
|
|
126
|
+
rmSync(reportDir, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function vitestCoverage(
|
|
131
|
+
input: VitestCoverageInput,
|
|
132
|
+
ctx: VitestContext
|
|
133
|
+
): Promise<VitestRunOutput> {
|
|
134
|
+
const { threshold, ...run } = input;
|
|
135
|
+
const result = await vitestRun({ ...run, coverage: true }, ctx);
|
|
136
|
+
if (threshold === undefined || !result.success) return result;
|
|
137
|
+
if (!result.coverage) {
|
|
138
|
+
return {
|
|
139
|
+
...result,
|
|
140
|
+
success: false,
|
|
141
|
+
stderr: `${result.stderr}\nNo coverage report: is a coverage provider (@vitest/coverage-v8) installed?`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { ...result, success: result.coverage.lines >= threshold };
|
|
145
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vitest.stop procedure
|
|
3
|
+
*
|
|
4
|
+
* Stop a vitest watch process
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { processes } from "@mark1russell7/client-shell/command";
|
|
8
|
+
import type { VitestStopInput, VitestStopOutput } from "../../types.js";
|
|
9
|
+
import { VITEST_GROUP } from "./watch.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stop a vitest watch process (the whole process tree), or every one when no id is given
|
|
13
|
+
*/
|
|
14
|
+
export async function vitestStop(
|
|
15
|
+
input: VitestStopInput,
|
|
16
|
+
_ctx: { metadata: Record<string, unknown> }
|
|
17
|
+
): Promise<VitestStopOutput> {
|
|
18
|
+
const running = processes
|
|
19
|
+
.list({ group: VITEST_GROUP })
|
|
20
|
+
.filter((info) => info.status === "running" && (input.id === undefined || info.id === input.id));
|
|
21
|
+
if (input.id !== undefined && running.length === 0) {
|
|
22
|
+
return { success: false, stopped: [] };
|
|
23
|
+
}
|
|
24
|
+
const stopped: string[] = [];
|
|
25
|
+
for (const info of running) {
|
|
26
|
+
if ((await processes.stop(info.id)).stopped) stopped.push(info.id);
|
|
27
|
+
}
|
|
28
|
+
return { success: true, stopped };
|
|
29
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vitest.watch procedure
|
|
3
|
+
*
|
|
4
|
+
* Start vitest in watch mode, as a process of the process registry of client-shell. vitest.list
|
|
5
|
+
* shows it and vitest.stop ends it, and it ends with the host (deep dive WRP-4). Before, the
|
|
6
|
+
* process started detached and the procedure forgot it: nothing could stop it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { processes } from "@mark1russell7/client-shell/command";
|
|
10
|
+
import type { VitestWatchInput, VitestWatchOutput } from "../../types.js";
|
|
11
|
+
import { resolveVitestCli } from "../../vitest-cli.js";
|
|
12
|
+
|
|
13
|
+
/** The group of the vitest processes in the process registry. */
|
|
14
|
+
export const VITEST_GROUP = "vitest";
|
|
15
|
+
|
|
16
|
+
export async function vitestWatch(
|
|
17
|
+
input: VitestWatchInput,
|
|
18
|
+
_ctx: { metadata: Record<string, unknown> }
|
|
19
|
+
): Promise<VitestWatchOutput> {
|
|
20
|
+
const cwd = input.cwd ?? process.cwd();
|
|
21
|
+
const args = [resolveVitestCli(cwd), "watch"];
|
|
22
|
+
|
|
23
|
+
for (const pattern of input.include ?? []) {
|
|
24
|
+
if (pattern.startsWith("-")) {
|
|
25
|
+
throw new Error(`Invalid test pattern (starts with "-"): ${pattern}`);
|
|
26
|
+
}
|
|
27
|
+
args.push(pattern);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// No shell: test patterns cannot inject commands
|
|
31
|
+
const started = processes.start(process.execPath, {
|
|
32
|
+
args,
|
|
33
|
+
cwd,
|
|
34
|
+
group: VITEST_GROUP,
|
|
35
|
+
label: `vitest watch ${cwd}`,
|
|
36
|
+
});
|
|
37
|
+
if (started.status === "error") {
|
|
38
|
+
throw new Error(`vitest did not start: ${started.error ?? "unknown error"}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id: started.id,
|
|
43
|
+
pid: started.pid,
|
|
44
|
+
status: "started",
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importing this file registers the procedures of @mark1russell7/client-vitest in the global
|
|
3
|
+
* registry (DECISIONS-2026-10, "Every package root registers its procedures"). The procedures as
|
|
4
|
+
* data are in `module.ts`, which registers nothing: a host with its own registry uses that one.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { registerProcedures } from "@mark1russell7/client";
|
|
8
|
+
import { vitestModule } from "./module.js";
|
|
9
|
+
|
|
10
|
+
export * from "./module.js";
|
|
11
|
+
|
|
12
|
+
export function registerVitestProcedures(): void {
|
|
13
|
+
registerProcedures(vitestModule.procedures);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Auto-register when this module is loaded
|
|
17
|
+
registerVitestProcedures();
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const VitestRunInputSchema: z.ZodObject<{
|
|
4
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
5
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
6
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
7
|
+
watch: z.ZodOptional<z.ZodBoolean>;
|
|
8
|
+
coverage: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
reporter: z.ZodOptional<z.ZodEnum<{ default: "default"; verbose: "verbose"; json: "json"; junit: "junit" }>>;
|
|
10
|
+
passWithNoTests: z.ZodOptional<z.ZodBoolean>;
|
|
11
|
+
}> = z.object({
|
|
12
|
+
cwd: z.string().optional().describe("The folder of the project (default: the current folder)"),
|
|
13
|
+
include: z.array(z.string()).optional().describe("Test files or patterns to run (default: all tests)"),
|
|
14
|
+
exclude: z.array(z.string()).optional().describe("Test files or patterns to leave out"),
|
|
15
|
+
watch: z.boolean().optional().describe("Run in watch mode (use vitest.watch for a long-running watcher)"),
|
|
16
|
+
coverage: z.boolean().optional().describe("Measure the coverage and give the percentages"),
|
|
17
|
+
reporter: z.enum(["default", "verbose", "json", "junit"]).optional().describe("The vitest reporter"),
|
|
18
|
+
passWithNoTests: z.boolean().optional().describe("Succeed when no test file matches"),
|
|
19
|
+
});
|
|
20
|
+
export type VitestRunInput = z.infer<typeof VitestRunInputSchema>;
|
|
21
|
+
|
|
22
|
+
export interface VitestRunOutput {
|
|
23
|
+
success: boolean;
|
|
24
|
+
passed: number;
|
|
25
|
+
failed: number;
|
|
26
|
+
skipped: number;
|
|
27
|
+
duration: number;
|
|
28
|
+
/** The exit code of vitest (-1 when a signal ended it) */
|
|
29
|
+
exitCode: number;
|
|
30
|
+
/** The last 64 KiB of the output */
|
|
31
|
+
stdout: string;
|
|
32
|
+
stderr: string;
|
|
33
|
+
coverage?: { lines: number; branches: number; functions: number; statements: number } | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Output schemas (roadmap 2.1). Loose objects: a field that a schema does not name stays in the
|
|
37
|
+
// result (a strict schema would remove it when the output is validated).
|
|
38
|
+
|
|
39
|
+
export const VitestRunOutputSchema: z.ZodType<VitestRunOutput> = z.looseObject({
|
|
40
|
+
success: z.boolean().describe("True when vitest ended with exit code 0 (and, for vitest.coverage, the threshold is met)"),
|
|
41
|
+
passed: z.number().describe("The number of tests that passed"),
|
|
42
|
+
failed: z.number().describe("The number of tests that failed"),
|
|
43
|
+
skipped: z.number().describe("The number of tests that were skipped"),
|
|
44
|
+
duration: z.number().describe("The time of the run, in milliseconds"),
|
|
45
|
+
exitCode: z.number().describe("The exit code of vitest (-1 when a signal ended it)"),
|
|
46
|
+
stdout: z.string().describe("The last 64 KiB of the output"),
|
|
47
|
+
stderr: z.string().describe("The last 64 KiB of the error output"),
|
|
48
|
+
coverage: z
|
|
49
|
+
.looseObject({
|
|
50
|
+
lines: z.number().describe("The line coverage, in percent"),
|
|
51
|
+
branches: z.number().describe("The branch coverage, in percent"),
|
|
52
|
+
functions: z.number().describe("The function coverage, in percent"),
|
|
53
|
+
statements: z.number().describe("The statement coverage, in percent"),
|
|
54
|
+
})
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("The coverage percentages, when the run measured coverage"),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const VitestCoverageInputSchema: z.ZodObject<{
|
|
60
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
61
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
62
|
+
exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
63
|
+
threshold: z.ZodOptional<z.ZodNumber>;
|
|
64
|
+
}> = z.object({
|
|
65
|
+
cwd: z.string().optional().describe("The folder of the project (default: the current folder)"),
|
|
66
|
+
include: z.array(z.string()).optional().describe("Test files or patterns to run (default: all tests)"),
|
|
67
|
+
exclude: z.array(z.string()).optional().describe("Test files or patterns to leave out"),
|
|
68
|
+
/** The minimum line coverage in percent. Below it, the result has `success: false`. */
|
|
69
|
+
threshold: z.number().optional().describe("The minimum line coverage in percent. Below it, the result has `success: false`."),
|
|
70
|
+
});
|
|
71
|
+
export type VitestCoverageInput = z.infer<typeof VitestCoverageInputSchema>;
|
|
72
|
+
|
|
73
|
+
export const VitestWatchInputSchema: z.ZodObject<{
|
|
74
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
75
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
76
|
+
}> = z.object({
|
|
77
|
+
cwd: z.string().optional().describe("The folder of the project (default: the current folder)"),
|
|
78
|
+
include: z.array(z.string()).optional().describe("Test files or patterns to watch (default: all tests)"),
|
|
79
|
+
});
|
|
80
|
+
export type VitestWatchInput = z.infer<typeof VitestWatchInputSchema>;
|
|
81
|
+
|
|
82
|
+
export interface VitestWatchOutput {
|
|
83
|
+
/** The id of the process in the process registry: vitest.stop takes it */
|
|
84
|
+
id: string;
|
|
85
|
+
pid: number;
|
|
86
|
+
status: "started" | "stopped";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const VitestWatchOutputSchema: z.ZodType<VitestWatchOutput> = z.looseObject({
|
|
90
|
+
id: z.string().describe("The id of the process in the process registry: vitest.stop takes it"),
|
|
91
|
+
pid: z.number().describe("The process id of vitest"),
|
|
92
|
+
status: z.enum(["started", "stopped"]).describe("started, or stopped when vitest ended at once"),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// vitest.stop Types - Stop a vitest watch process
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
export const VitestStopInputSchema: z.ZodObject<{
|
|
100
|
+
id: z.ZodOptional<z.ZodString>;
|
|
101
|
+
}> = z.object({
|
|
102
|
+
/** The id that vitest.watch gave. Without it, every vitest watch process stops. */
|
|
103
|
+
id: z.string().optional().describe("The id that vitest.watch gave. Without it, every vitest watch process stops."),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export type VitestStopInput = z.infer<typeof VitestStopInputSchema>;
|
|
107
|
+
|
|
108
|
+
export interface VitestStopOutput {
|
|
109
|
+
/** False when an id was given and no running watch process has it */
|
|
110
|
+
success: boolean;
|
|
111
|
+
/** The ids of the processes that stopped */
|
|
112
|
+
stopped: string[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// vitest.list Types - List the vitest watch processes
|
|
117
|
+
// =============================================================================
|
|
118
|
+
|
|
119
|
+
export const VitestListInputSchema: z.ZodObject<{
|
|
120
|
+
output: z.ZodOptional<z.ZodBoolean>;
|
|
121
|
+
}> = z.object({
|
|
122
|
+
/** Also give the end of the output of each process (the last 64 KiB) */
|
|
123
|
+
output: z.boolean().optional().describe("Also give the end of the output of each process (the last 64 KiB)"),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export type VitestListInput = z.infer<typeof VitestListInputSchema>;
|
|
127
|
+
|
|
128
|
+
export interface VitestProcessInfo {
|
|
129
|
+
id: string;
|
|
130
|
+
pid: number;
|
|
131
|
+
cwd?: string | undefined;
|
|
132
|
+
status: "running" | "exited" | "error";
|
|
133
|
+
startedAt: string;
|
|
134
|
+
exitedAt?: string | undefined;
|
|
135
|
+
exitCode?: number | undefined;
|
|
136
|
+
/** With `output: true`, the end of the output */
|
|
137
|
+
output?: string | undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface VitestListOutput {
|
|
141
|
+
processes: VitestProcessInfo[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const VitestStopOutputSchema: z.ZodType<VitestStopOutput> = z.looseObject({
|
|
145
|
+
success: z.boolean().describe("False when an id was given and no running watch process has it"),
|
|
146
|
+
stopped: z.array(z.string()).describe("The ids of the processes that stopped"),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
export const VitestListOutputSchema: z.ZodType<VitestListOutput> = z.looseObject({
|
|
150
|
+
processes: z
|
|
151
|
+
.array(
|
|
152
|
+
z.looseObject({
|
|
153
|
+
id: z.string().describe("The id that vitest.stop takes"),
|
|
154
|
+
pid: z.number().describe("The process id"),
|
|
155
|
+
cwd: z.string().optional().describe("The folder of the project"),
|
|
156
|
+
status: z.enum(["running", "exited", "error"]).describe("The state of the process"),
|
|
157
|
+
startedAt: z.string().describe("The start time (ISO 8601)"),
|
|
158
|
+
exitedAt: z.string().optional().describe("The end time (ISO 8601), when the process ended"),
|
|
159
|
+
exitCode: z.number().optional().describe("The exit code, when the process ended"),
|
|
160
|
+
output: z.string().optional().describe("With output: true, the last 64 KiB of the output"),
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
.describe("The vitest watch processes of this host"),
|
|
164
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locate the vitest CLI of a project.
|
|
3
|
+
*
|
|
4
|
+
* The procedures run vitest as `node <vitest.mjs> ...args` with no shell:
|
|
5
|
+
* - no shell means test patterns cannot inject commands;
|
|
6
|
+
* - running the project's own vitest (instead of `npx vitest`) never downloads a package,
|
|
7
|
+
* and avoids the .cmd shim that Node cannot spawn without a shell on Windows.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
export function resolveVitestCli(cwd: string): string {
|
|
15
|
+
const require = createRequire(join(resolve(cwd), "package.json"));
|
|
16
|
+
let packageJsonPath: string;
|
|
17
|
+
try {
|
|
18
|
+
packageJsonPath = require.resolve("vitest/package.json");
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error(`vitest is not installed for ${cwd}. Add it as a devDependency.`);
|
|
21
|
+
}
|
|
22
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { bin?: string | Record<string, string> };
|
|
23
|
+
const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["vitest"];
|
|
24
|
+
if (!bin) {
|
|
25
|
+
throw new Error(`The vitest package at ${dirname(packageJsonPath)} has no "vitest" bin.`);
|
|
26
|
+
}
|
|
27
|
+
return join(dirname(packageJsonPath), bin);
|
|
28
|
+
}
|