@vitest-agent/plugin 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/LICENSE +21 -0
- package/README.md +50 -0
- package/index.d.ts +1216 -0
- package/index.js +31 -0
- package/layers/ConfigValidationLive.js +148 -0
- package/layers/ConfigValidationTest.js +16 -0
- package/layers/CoverageAnalyzerLive.js +138 -0
- package/layers/CoverageAnalyzerTest.js +15 -0
- package/layers/ReporterLive.js +21 -0
- package/package.json +65 -0
- package/plugin.js +329 -0
- package/reporter.js +1349 -0
- package/services/ConfigValidation.js +11 -0
- package/services/CoverageAnalyzer.js +11 -0
- package/tsdoc-metadata.json +11 -0
- package/utils/build-module-info.js +62 -0
- package/utils/build-reporter-kit.js +49 -0
- package/utils/capture-env.js +23 -0
- package/utils/capture-settings.js +54 -0
- package/utils/classify-helpers.js +72 -0
- package/utils/discover-projects.js +68 -0
- package/utils/discover-strategy.js +158 -0
- package/utils/find-test-files.js +94 -0
- package/utils/inject-tags.js +94 -0
- package/utils/process-failure.js +89 -0
- package/utils/resolve-thresholds.js +65 -0
- package/utils/route-rendered-output.js +45 -0
- package/utils/stringify-failure-value.js +36 -0
- package/utils/strip-console-reporters.js +45 -0
- package/utils/tag.js +42 -0
- package/utils/to-posix-path.js +35 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as acorn from "acorn";
|
|
2
|
+
import { tsPlugin } from "acorn-typescript";
|
|
3
|
+
import MagicString from "magic-string";
|
|
4
|
+
|
|
5
|
+
//#region src/utils/inject-tags.ts
|
|
6
|
+
const Parser = acorn.Parser.extend(tsPlugin());
|
|
7
|
+
const TEST_NAMES = /* @__PURE__ */ new Set(["test", "it"]);
|
|
8
|
+
function rootIdentifier(n) {
|
|
9
|
+
if (n.type === "Identifier") return n;
|
|
10
|
+
if (n.type === "MemberExpression") return rootIdentifier(n.object);
|
|
11
|
+
if (n.type === "CallExpression") return rootIdentifier(n.callee);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
function isTestCallee(callee) {
|
|
15
|
+
const root = rootIdentifier(callee);
|
|
16
|
+
return !!root && TEST_NAMES.has(root.name);
|
|
17
|
+
}
|
|
18
|
+
function hasTagsField(objectExpression) {
|
|
19
|
+
const props = objectExpression.properties ?? [];
|
|
20
|
+
for (const p of props) {
|
|
21
|
+
if (p.type !== "Property") continue;
|
|
22
|
+
const key = p.key;
|
|
23
|
+
if (key.type === "Identifier" && key.name === "tags") return true;
|
|
24
|
+
if (key.type === "Literal" && key.value === "tags") return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
function tagsLiteral(tags) {
|
|
29
|
+
return `[${tags.map((t) => JSON.stringify(t)).join(", ")}]`;
|
|
30
|
+
}
|
|
31
|
+
function walk(node, visit) {
|
|
32
|
+
visit(node);
|
|
33
|
+
for (const key of Object.keys(node)) {
|
|
34
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc") continue;
|
|
35
|
+
const v = node[key];
|
|
36
|
+
if (Array.isArray(v)) {
|
|
37
|
+
for (const item of v) if (item && typeof item === "object" && "type" in item) walk(item, visit);
|
|
38
|
+
} else if (v && typeof v === "object" && "type" in v) walk(v, visit);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function injectTags(source, tags) {
|
|
42
|
+
if (tags.length === 0) return null;
|
|
43
|
+
let ast;
|
|
44
|
+
try {
|
|
45
|
+
ast = Parser.parse(source, {
|
|
46
|
+
ecmaVersion: "latest",
|
|
47
|
+
sourceType: "module",
|
|
48
|
+
locations: true
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const ms = new MagicString(source);
|
|
54
|
+
let mutated = false;
|
|
55
|
+
walk(ast, (node) => {
|
|
56
|
+
if (node.type !== "CallExpression") return;
|
|
57
|
+
const callee = node.callee;
|
|
58
|
+
if (!isTestCallee(callee)) return;
|
|
59
|
+
const args = node.arguments ?? [];
|
|
60
|
+
if (args.length < 2) return;
|
|
61
|
+
const last = args[args.length - 1];
|
|
62
|
+
if (!(last.type === "FunctionExpression" || last.type === "ArrowFunctionExpression")) return;
|
|
63
|
+
const optsCandidate = args.length >= 3 ? args[args.length - 2] : null;
|
|
64
|
+
if (optsCandidate) {
|
|
65
|
+
if (optsCandidate.type === "ObjectExpression") {
|
|
66
|
+
if (hasTagsField(optsCandidate)) return;
|
|
67
|
+
const lastProp = (optsCandidate.properties ?? []).at(-1);
|
|
68
|
+
if (lastProp === void 0) {
|
|
69
|
+
const insertPoint = optsCandidate.end - 1;
|
|
70
|
+
ms.appendLeft(insertPoint, `tags: ${tagsLiteral(tags)} `);
|
|
71
|
+
} else ms.appendLeft(lastProp.end, `, tags: ${tagsLiteral(tags)}`);
|
|
72
|
+
mutated = true;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const optsStart = optsCandidate.start;
|
|
76
|
+
const optsEnd = optsCandidate.end;
|
|
77
|
+
const origExpr = source.slice(optsStart, optsEnd);
|
|
78
|
+
ms.overwrite(optsStart, optsEnd, `{ ...(${origExpr}), tags: ${tagsLiteral(tags)} }`);
|
|
79
|
+
mutated = true;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const nameArg = args[0];
|
|
83
|
+
ms.appendRight(nameArg.end, `, { tags: ${tagsLiteral(tags)} }`);
|
|
84
|
+
mutated = true;
|
|
85
|
+
});
|
|
86
|
+
if (!mutated) return null;
|
|
87
|
+
return {
|
|
88
|
+
code: ms.toString(),
|
|
89
|
+
map: ms.generateMap({ hires: true })
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
export { injectTags };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { computeFailureSignature, findFunctionBoundary } from "@vitest-agent/sdk";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/process-failure.ts
|
|
5
|
+
const FRAME_LINE_REGEX = /^\s*at\s+(?:([\w$.<>[\] ]+?)\s+)?\(?([^\n)]+):(\d+):(\d+)\)?\s*$/;
|
|
6
|
+
const isFrameworkPath = (filePath) => filePath.includes("/node_modules/") || filePath.includes("vitest/dist") || filePath.includes("@vitest/") || filePath.startsWith("node:");
|
|
7
|
+
const parseFramesFromStackString = (stack) => {
|
|
8
|
+
const frames = [];
|
|
9
|
+
let ordinal = 0;
|
|
10
|
+
for (const line of stack.split("\n")) {
|
|
11
|
+
const m = FRAME_LINE_REGEX.exec(line);
|
|
12
|
+
if (m === null) continue;
|
|
13
|
+
frames.push({
|
|
14
|
+
ordinal: ordinal++,
|
|
15
|
+
method: m[1] ?? null,
|
|
16
|
+
filePath: m[2],
|
|
17
|
+
line: Number.parseInt(m[3], 10),
|
|
18
|
+
col: Number.parseInt(m[4], 10),
|
|
19
|
+
sourceMapped: false
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return frames;
|
|
23
|
+
};
|
|
24
|
+
const readSourceSafe = (filePath) => {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(filePath, "utf-8");
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Convert a Vitest error into structured frame inputs (with source-map and
|
|
33
|
+
* function-boundary annotations) plus a stable failure signature.
|
|
34
|
+
*
|
|
35
|
+
* Returns `null` for the signature when no usable top frame is found
|
|
36
|
+
* (error has no stack, or every frame is in framework code). Frames may
|
|
37
|
+
* still be populated even when the signature is null.
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
const processFailure = (error) => {
|
|
41
|
+
const rawFrames = error.stacks !== void 0 && error.stacks.length > 0 ? error.stacks.map((f, ordinal) => ({
|
|
42
|
+
ordinal,
|
|
43
|
+
method: f.method ?? null,
|
|
44
|
+
filePath: f.file ?? "<unknown>",
|
|
45
|
+
line: f.line ?? 0,
|
|
46
|
+
col: f.column ?? 0,
|
|
47
|
+
sourceMapped: true
|
|
48
|
+
})) : error.stack !== void 0 ? parseFramesFromStackString(error.stack) : [];
|
|
49
|
+
const topFrame = rawFrames.find((f) => f.filePath !== "<unknown>" && !isFrameworkPath(f.filePath));
|
|
50
|
+
let topBoundaryLine = null;
|
|
51
|
+
let topFunctionName = null;
|
|
52
|
+
if (topFrame !== void 0) {
|
|
53
|
+
const lineForBoundary = topFrame.line;
|
|
54
|
+
const source = readSourceSafe(topFrame.filePath);
|
|
55
|
+
const boundary = source !== null ? findFunctionBoundary(source, lineForBoundary) : null;
|
|
56
|
+
if (boundary !== null) {
|
|
57
|
+
topBoundaryLine = boundary.line;
|
|
58
|
+
topFunctionName = boundary.name;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const frames = rawFrames.map((f) => {
|
|
62
|
+
return {
|
|
63
|
+
ordinal: f.ordinal,
|
|
64
|
+
method: f.method,
|
|
65
|
+
filePath: f.filePath,
|
|
66
|
+
line: f.line,
|
|
67
|
+
col: f.col,
|
|
68
|
+
...f.sourceMapped && { sourceMappedLine: f.line },
|
|
69
|
+
...topFrame !== void 0 && f.ordinal === topFrame.ordinal && topBoundaryLine !== null && { functionBoundaryLine: topBoundaryLine }
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
if (topFrame === void 0) return {
|
|
73
|
+
frames,
|
|
74
|
+
signatureHash: null
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
frames,
|
|
78
|
+
signatureHash: computeFailureSignature({
|
|
79
|
+
error_name: error.name ?? "Error",
|
|
80
|
+
assertion_message: error.message,
|
|
81
|
+
top_frame_function_name: topFunctionName ?? topFrame.method ?? "<anonymous>",
|
|
82
|
+
top_frame_function_boundary_line: topBoundaryLine,
|
|
83
|
+
top_frame_raw_line: topFrame.line
|
|
84
|
+
})
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { processFailure };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//#region src/utils/resolve-thresholds.ts
|
|
2
|
+
const METRIC_KEYS = /* @__PURE__ */ new Set([
|
|
3
|
+
"lines",
|
|
4
|
+
"functions",
|
|
5
|
+
"branches",
|
|
6
|
+
"statements"
|
|
7
|
+
]);
|
|
8
|
+
const RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
9
|
+
...METRIC_KEYS,
|
|
10
|
+
"100",
|
|
11
|
+
"perFile",
|
|
12
|
+
"autoUpdate"
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Parse Vitest `coverage.thresholds` format into a normalized `ResolvedThresholds`.
|
|
16
|
+
* @param input - The raw `coverage.thresholds` object from Vitest config
|
|
17
|
+
* @returns Normalized thresholds with global, perFile, and pattern entries
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
function resolveThresholds(input) {
|
|
21
|
+
if (!input) return {
|
|
22
|
+
global: {},
|
|
23
|
+
perFile: false,
|
|
24
|
+
patterns: []
|
|
25
|
+
};
|
|
26
|
+
const global = {};
|
|
27
|
+
const patterns = [];
|
|
28
|
+
let perFile = false;
|
|
29
|
+
if (input["100"] === true) {
|
|
30
|
+
global.lines = 100;
|
|
31
|
+
global.functions = 100;
|
|
32
|
+
global.branches = 100;
|
|
33
|
+
global.statements = 100;
|
|
34
|
+
}
|
|
35
|
+
for (const key of METRIC_KEYS) {
|
|
36
|
+
const value = input[key];
|
|
37
|
+
if (typeof value === "number") global[key] = value;
|
|
38
|
+
}
|
|
39
|
+
if (input.perFile === true) perFile = true;
|
|
40
|
+
for (const [key, value] of Object.entries(input)) {
|
|
41
|
+
if (RESERVED_KEYS.has(key)) continue;
|
|
42
|
+
if (typeof value !== "object" || value === null) continue;
|
|
43
|
+
const patternMetrics = {};
|
|
44
|
+
const obj = value;
|
|
45
|
+
if (obj["100"] === true) {
|
|
46
|
+
patternMetrics.lines = 100;
|
|
47
|
+
patternMetrics.functions = 100;
|
|
48
|
+
patternMetrics.branches = 100;
|
|
49
|
+
patternMetrics.statements = 100;
|
|
50
|
+
}
|
|
51
|
+
for (const mk of METRIC_KEYS) {
|
|
52
|
+
const mv = obj[mk];
|
|
53
|
+
if (typeof mv === "number") patternMetrics[mk] = mv;
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(patternMetrics).length > 0) patterns.push([key, patternMetrics]);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
global,
|
|
59
|
+
perFile,
|
|
60
|
+
patterns
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
export { resolveThresholds };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/route-rendered-output.ts
|
|
5
|
+
/**
|
|
6
|
+
* Route a {@link RenderedOutput} entry to its declared target.
|
|
7
|
+
*
|
|
8
|
+
* Centralizes the side-effects that user-supplied reporters get to stay
|
|
9
|
+
* pure about: writing to stdout, appending to a github-summary file, or
|
|
10
|
+
* writing to an arbitrary file path. The plugin's internal Vitest reporter
|
|
11
|
+
* calls this once per `RenderedOutput` returned by the user's reporter(s).
|
|
12
|
+
*
|
|
13
|
+
* `github-summary` writes go to `kit.config.githubSummaryFile` if set,
|
|
14
|
+
* otherwise `process.env.GITHUB_STEP_SUMMARY`. When neither is set the
|
|
15
|
+
* write is silently dropped — the typical case is "we're not under
|
|
16
|
+
* GitHub Actions and the user-supplied reporter shouldn't have produced
|
|
17
|
+
* one anyway."
|
|
18
|
+
*
|
|
19
|
+
* `file` outputs require an explicit path embedded in the output (a
|
|
20
|
+
* convention reporters should adopt; this helper currently treats `file`
|
|
21
|
+
* as a no-op until we settle on a path field). The default reporter
|
|
22
|
+
* never produces `file` outputs today, so this gap is theoretical.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
const routeRenderedOutput = (output, options) => {
|
|
27
|
+
switch (output.target) {
|
|
28
|
+
case "stdout":
|
|
29
|
+
process.stdout.write(`${output.content.replace(/\n+$/, "")}\n`);
|
|
30
|
+
return;
|
|
31
|
+
case "github-summary": {
|
|
32
|
+
const path = options.githubSummaryFile ?? process.env.GITHUB_STEP_SUMMARY;
|
|
33
|
+
if (!path) return;
|
|
34
|
+
try {
|
|
35
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
36
|
+
appendFileSync(path, output.content, "utf8");
|
|
37
|
+
} catch {}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
case "file": return;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { routeRenderedOutput };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/utils/stringify-failure-value.ts
|
|
2
|
+
/**
|
|
3
|
+
* Converts a raw Vitest assertion value (`.expected` / `.actual`) into a
|
|
4
|
+
* one-line string suitable for display in the stream renderer.
|
|
5
|
+
*
|
|
6
|
+
* The value stays within the Vitest-side error object; this helper
|
|
7
|
+
* converts it to a string representation that crosses the ReportError
|
|
8
|
+
* schema boundary. Returns `undefined` when there is no value to show
|
|
9
|
+
* (i.e., the input is `undefined`).
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Stringify a raw Vitest assertion `.expected` or `.actual` value into a
|
|
15
|
+
* single-line string.
|
|
16
|
+
*
|
|
17
|
+
* - `undefined` → `undefined` (signals "no value — omit the field")
|
|
18
|
+
* - `null` → `"null"`
|
|
19
|
+
* - primitives (string, number, boolean, bigint) → `String(value)`
|
|
20
|
+
* - objects / arrays → `JSON.stringify(value)`, falling back to
|
|
21
|
+
* `String(value)` for circular or otherwise un-serialisable values
|
|
22
|
+
*/
|
|
23
|
+
const stringifyFailureValue = (value) => {
|
|
24
|
+
if (value === void 0) return void 0;
|
|
25
|
+
if (value === null) return "null";
|
|
26
|
+
if (typeof value === "string") return value;
|
|
27
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
28
|
+
try {
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
} catch {
|
|
31
|
+
return String(value);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { stringifyFailureValue };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/utils/strip-console-reporters.ts
|
|
2
|
+
/**
|
|
3
|
+
* Built-in Vitest reporters that write to the console (stdout).
|
|
4
|
+
* These are the reporters suppressed when an agent takes over console output.
|
|
5
|
+
*
|
|
6
|
+
* @privateRemarks
|
|
7
|
+
* `"agent"` is the built-in Vitest reporter added in v4.1 that reduces
|
|
8
|
+
* console noise for AI agents. We strip it because our reporter replaces
|
|
9
|
+
* its functionality with structured markdown output.
|
|
10
|
+
*
|
|
11
|
+
* @see {@link https://vitest.dev/api/advanced/reporters.html | Vitest Reporter docs}
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
const CONSOLE_REPORTERS = /* @__PURE__ */ new Set([
|
|
15
|
+
"default",
|
|
16
|
+
"verbose",
|
|
17
|
+
"tree",
|
|
18
|
+
"dot",
|
|
19
|
+
"tap",
|
|
20
|
+
"tap-flat",
|
|
21
|
+
"hanging-process",
|
|
22
|
+
"agent"
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* Filter out built-in console reporters from a Vitest reporters array.
|
|
26
|
+
*
|
|
27
|
+
* Keeps custom reporters (class instances, file paths) and non-console
|
|
28
|
+
* built-in reporters (`json`, `junit`, `html`, `blob`, `github-actions`).
|
|
29
|
+
* Used by `AgentPlugin` in agent mode to suppress noisy console output.
|
|
30
|
+
*
|
|
31
|
+
* @param reporters - The Vitest `config.reporters` array
|
|
32
|
+
* @returns Filtered array with console reporters removed
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
function stripConsoleReporters(reporters) {
|
|
37
|
+
return reporters.filter((entry) => {
|
|
38
|
+
if (typeof entry === "string") return !CONSOLE_REPORTERS.has(entry);
|
|
39
|
+
if (Array.isArray(entry) && typeof entry[0] === "string") return !CONSOLE_REPORTERS.has(entry[0]);
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { CONSOLE_REPORTERS, stripConsoleReporters };
|
package/utils/tag.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/utils/tag.ts
|
|
2
|
+
const RESERVED = /* @__PURE__ */ new Set([
|
|
3
|
+
"and",
|
|
4
|
+
"or",
|
|
5
|
+
"not"
|
|
6
|
+
]);
|
|
7
|
+
const FORBIDDEN_CHAR = /[()&|!*\s]/;
|
|
8
|
+
function validateTagName(name) {
|
|
9
|
+
if (!name) throw new Error("Tag name is empty");
|
|
10
|
+
if (RESERVED.has(name)) throw new Error(`Tag name "${name}" is reserved (and/or/not)`);
|
|
11
|
+
if (FORBIDDEN_CHAR.test(name)) throw new Error(`Tag name "${name}" contains an invalid character (no spaces, ()&|!*)`);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A validated Vitest tag with its `name` string and a `TestTagDefinition` for registration.
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
var Tag = class Tag {
|
|
18
|
+
/** The tag name string (validated on construction). */
|
|
19
|
+
name;
|
|
20
|
+
/** The full `TestTagDefinition` object to pass to Vitest's `test.tags` config. */
|
|
21
|
+
definition;
|
|
22
|
+
constructor(name, options) {
|
|
23
|
+
this.name = name;
|
|
24
|
+
this.definition = {
|
|
25
|
+
name,
|
|
26
|
+
...options
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a validated `Tag`.
|
|
31
|
+
* @param name - Tag identifier; must not be empty, reserved, or contain forbidden characters
|
|
32
|
+
* @param options - Optional timeout, retry, and other Vitest tag settings
|
|
33
|
+
* @returns A new `Tag` instance
|
|
34
|
+
*/
|
|
35
|
+
static make(name, options = {}) {
|
|
36
|
+
validateTagName(name);
|
|
37
|
+
return new Tag(name, options);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { Tag };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/utils/to-posix-path.ts
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a filesystem path to forward-slash separators.
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for the POSIX-style path strings the plugin uses
|
|
6
|
+
* in three places where the path is compared against a slash-only pattern
|
|
7
|
+
* or surfaced to user code that documents slash semantics:
|
|
8
|
+
*
|
|
9
|
+
* - `buildModuleInfo.relativePath` — consumed by user-supplied `ClassifyFn`
|
|
10
|
+
* implementations and by the bundled `classifyByDirectory` helper.
|
|
11
|
+
* - `find-test-files` regex matching — `globToRegex` compiles patterns
|
|
12
|
+
* with `/` boundaries.
|
|
13
|
+
* - `discoverProjects` `addProject` relativePath — passed to
|
|
14
|
+
* `strategy.buildProject` and exposed via `DiscoverInput.relativePath`.
|
|
15
|
+
*
|
|
16
|
+
* Always folds backslashes to forward slashes regardless of the host
|
|
17
|
+
* platform. On POSIX the call is effectively a no-op for paths produced
|
|
18
|
+
* by `node:path` operations (they never contain backslashes there) but
|
|
19
|
+
* still defends against custom DiscoverStrategy implementations that
|
|
20
|
+
* pass through a Windows-style path string. On Windows, where `node:path`
|
|
21
|
+
* returns backslash separators, this folds them so glob matching and
|
|
22
|
+
* slash-bounded segment checks both work without platform-specific
|
|
23
|
+
* branching at every call site.
|
|
24
|
+
*
|
|
25
|
+
* Returns the input unchanged when no backslashes are present — only the
|
|
26
|
+
* separator characters are touched. Callers that need the platform-native
|
|
27
|
+
* form (e.g. for `readFile`) should pass the original path produced by
|
|
28
|
+
* `join` rather than the value returned here.
|
|
29
|
+
*/
|
|
30
|
+
function toPosixPath(p) {
|
|
31
|
+
return p.indexOf("\\") === -1 ? p : p.split("\\").join("/");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { toPosixPath };
|