@vitest-agent/plugin 2.3.1 → 2.4.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/index.d.ts +98 -2
- package/index.js +2 -1
- package/package.json +7 -7
- package/plugin.js +1 -1
- package/tsdoc-metadata.json +1 -1
- package/utils/discover-projects.js +24 -23
- package/utils/discover-strategy.js +8 -11
- package/utils/find-test-files.js +6 -6
- package/utils/is-test-shaped-package.js +4 -11
- package/utils/walker-fs.js +27 -0
package/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Context, Effect, Layer, LogLevel, Option } from "effect";
|
|
|
4
4
|
import { TestProjectInlineConfiguration } from "vitest/config";
|
|
5
5
|
import { ResolvedConfig, VitestPluginContext } from "vitest/node";
|
|
6
6
|
import { SourceMap } from "magic-string";
|
|
7
|
+
import { WorkspacesSyncOptions } from "@effected/workspaces/node-sync";
|
|
7
8
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
8
9
|
import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
|
|
9
10
|
//#region src/utils/tag.d.ts
|
|
@@ -31,6 +32,85 @@ declare class Tag {
|
|
|
31
32
|
static make(name: string, options?: TagOptions): Tag;
|
|
32
33
|
}
|
|
33
34
|
//#endregion
|
|
35
|
+
//#region src/utils/walker-fs.d.ts
|
|
36
|
+
/**
|
|
37
|
+
* The filesystem operations the discovery walkers need, supplied by the
|
|
38
|
+
* caller. Node's built-ins satisfy it directly via {@link nodeWalkerFs}, which
|
|
39
|
+
* is what every production call site uses.
|
|
40
|
+
*
|
|
41
|
+
* The port exists because the walkers used to call `node:fs/promises` outright,
|
|
42
|
+
* which made them untestable against anything but a real temporary directory —
|
|
43
|
+
* a virtual filesystem cannot intercept a direct `node:fs` call. Every test in
|
|
44
|
+
* this package that exercised discovery therefore had to build, populate and
|
|
45
|
+
* tear down a real tmp tree.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately NOT `fs.promises`-shaped: the walkers need exactly two
|
|
48
|
+
* operations, and a wider surface would invite call sites to reach past the
|
|
49
|
+
* port. It is also NOT shaped like `@effected/workspaces`'s `SyncFileSystem`
|
|
50
|
+
* (which this package consumes separately, in `discover-projects.ts`) — that
|
|
51
|
+
* port answers entry *names*, and the walkers need the entry *type* that
|
|
52
|
+
* `readdir({ withFileTypes: true })` returns in the same syscall. Reading names
|
|
53
|
+
* and then stat-ing each one is the syscall-doubling this module exists to
|
|
54
|
+
* avoid; see {@link WalkerEntry}.
|
|
55
|
+
*
|
|
56
|
+
* @packageDocumentation
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* One directory entry, carrying the name and type together.
|
|
60
|
+
*
|
|
61
|
+
* A structural subset of Node's `Dirent`, so a real `Dirent` satisfies it
|
|
62
|
+
* verbatim and `readdir({ withFileTypes: true })` needs no adaptation.
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
interface WalkerEntry {
|
|
66
|
+
/** The entry's own name, not a path. */
|
|
67
|
+
readonly name: string;
|
|
68
|
+
/** Whether this entry is a regular file. */
|
|
69
|
+
readonly isFile: () => boolean;
|
|
70
|
+
/** Whether this entry is a directory. */
|
|
71
|
+
readonly isDirectory: () => boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The filesystem operations the discovery walkers need.
|
|
75
|
+
*
|
|
76
|
+
* Both operations may reject; every walker absorbs a rejection as "this path
|
|
77
|
+
* contributes nothing" rather than propagating it, matching the behavior the
|
|
78
|
+
* `node:fs` calls had when they were inline.
|
|
79
|
+
* @public
|
|
80
|
+
*/
|
|
81
|
+
interface WalkerFileSystem {
|
|
82
|
+
/**
|
|
83
|
+
* The entries inside the directory at `dir`, with their types. May reject —
|
|
84
|
+
* a rejection reads as an unreadable directory and skips it.
|
|
85
|
+
*/
|
|
86
|
+
readonly readDirectory: (dir: string) => Promise<ReadonlyArray<WalkerEntry>>;
|
|
87
|
+
/**
|
|
88
|
+
* The type of the entry at `path`, or `null` when it does not exist or
|
|
89
|
+
* cannot be read. Never rejects.
|
|
90
|
+
*/
|
|
91
|
+
readonly statEntry: (path: string) => Promise<WalkerEntryStat | null>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* What the walkers read off a `stat`: the entry's type, plus the modification
|
|
95
|
+
* time the discovery cache's directory signature is built from.
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
interface WalkerEntryStat {
|
|
99
|
+
/** Whether the path holds a regular file. */
|
|
100
|
+
readonly isFile: boolean;
|
|
101
|
+
/** Whether the path holds a directory. */
|
|
102
|
+
readonly isDirectory: boolean;
|
|
103
|
+
/** Modification time in milliseconds since the epoch. */
|
|
104
|
+
readonly mtimeMs: number;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* {@link WalkerFileSystem} over `node:fs/promises` — the binding every
|
|
108
|
+
* production call site uses, and the default when a walker is called without
|
|
109
|
+
* an explicit port.
|
|
110
|
+
* @public
|
|
111
|
+
*/
|
|
112
|
+
declare const nodeWalkerFs: WalkerFileSystem;
|
|
113
|
+
//#endregion
|
|
34
114
|
//#region src/utils/discover-strategy.d.ts
|
|
35
115
|
/**
|
|
36
116
|
* Resolved metadata about a discovered test module.
|
|
@@ -73,6 +153,12 @@ interface DiscoverInput {
|
|
|
73
153
|
readonly workspaceRoot: string;
|
|
74
154
|
/** Parsed `package.json` contents, when available. */
|
|
75
155
|
readonly packageJson?: PackageJson;
|
|
156
|
+
/**
|
|
157
|
+
* Filesystem port the walkers read through. Defaults to `node:fs` when
|
|
158
|
+
* omitted, which is what every production call site does; tests inject a
|
|
159
|
+
* virtual volume instead of building a real temporary directory.
|
|
160
|
+
*/
|
|
161
|
+
readonly fs?: WalkerFileSystem;
|
|
76
162
|
}
|
|
77
163
|
/**
|
|
78
164
|
* Context object passed to a `ClassifyFn`.
|
|
@@ -938,6 +1024,16 @@ interface DiscoverProjectsOptions {
|
|
|
938
1024
|
readonly name: string;
|
|
939
1025
|
readonly path: string;
|
|
940
1026
|
}>;
|
|
1027
|
+
/**
|
|
1028
|
+
* Filesystem port the test-file and signature walks read through. Defaults
|
|
1029
|
+
* to `node:fs`; tests inject a virtual volume instead of a real tmp tree.
|
|
1030
|
+
*/
|
|
1031
|
+
readonly fs?: WalkerFileSystem;
|
|
1032
|
+
/**
|
|
1033
|
+
* Sync operations `@effected/workspaces` resolves the workspace root and
|
|
1034
|
+
* package list through. Defaults to its `nodeSyncOps` binding.
|
|
1035
|
+
*/
|
|
1036
|
+
readonly syncOps?: WorkspacesSyncOptions;
|
|
941
1037
|
}
|
|
942
1038
|
/**
|
|
943
1039
|
* Scan all workspace packages and additional entries through the active strategy and return projects + tags.
|
|
@@ -1006,7 +1102,7 @@ declare function combineClassifiers(...fns: ReadonlyArray<ClassifyFn>): Classify
|
|
|
1006
1102
|
* @returns Absolute paths of matched test files
|
|
1007
1103
|
* @public
|
|
1008
1104
|
*/
|
|
1009
|
-
declare function findTestFiles(dir: string, patterns: ReadonlyArray<string
|
|
1105
|
+
declare function findTestFiles(dir: string, patterns: ReadonlyArray<string>, fs?: WalkerFileSystem): Promise<ReadonlyArray<string>>;
|
|
1010
1106
|
//#endregion
|
|
1011
1107
|
//#region src/layers/ReporterLive.d.ts
|
|
1012
1108
|
/**
|
|
@@ -1247,5 +1343,5 @@ declare const COVERAGE_AUTOUPDATE: Readonly<{
|
|
|
1247
1343
|
lenient: (n: number) => number;
|
|
1248
1344
|
}>;
|
|
1249
1345
|
//#endregion
|
|
1250
|
-
export { type AddProjectInput, AgentPlugin, type AgentPluginConstructorOptions, AgentReporter, type AgentReporterConstructorOptions, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, type ClassifyContext, type ClassifyFn, ConfigValidation, ConfigValidationLive, ConfigValidationTest, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, type CoverageInput, CoverageLevel, type CoverageLevelName, type CoverageLevelPreset, type CoverageOptions, DefaultDiscoverStrategy, type DiscoverBuilder, type DiscoverInput, type PackageJson as DiscoverPackageJson, type DiscoverProjectsOptions, type DiscoverProjectsResult, type DiscoverResult, DiscoverStrategy, type DiscoverStrategyCreateOptions, type DiscoverStrategyExtendOptions, type InjectTagsResult, type ModuleInfo, ReporterLive, Tag, type TagOptions, type ValidationError, type ValidationInfo, type ValidationInput, type ValidationResult, type ValidationWarning, type VitestErrorLike, type VitestStackFrameLike, type VitestThresholdsInput, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
|
|
1346
|
+
export { type AddProjectInput, AgentPlugin, type AgentPluginConstructorOptions, AgentReporter, type AgentReporterConstructorOptions, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, type ClassifyContext, type ClassifyFn, ConfigValidation, ConfigValidationLive, ConfigValidationTest, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, type CoverageInput, CoverageLevel, type CoverageLevelName, type CoverageLevelPreset, type CoverageOptions, DefaultDiscoverStrategy, type DiscoverBuilder, type DiscoverInput, type PackageJson as DiscoverPackageJson, type DiscoverProjectsOptions, type DiscoverProjectsResult, type DiscoverResult, DiscoverStrategy, type DiscoverStrategyCreateOptions, type DiscoverStrategyExtendOptions, type InjectTagsResult, type ModuleInfo, ReporterLive, Tag, type TagOptions, type ValidationError, type ValidationInfo, type ValidationInput, type ValidationResult, type ValidationWarning, type VitestErrorLike, type VitestStackFrameLike, type VitestThresholdsInput, type WalkerEntry, type WalkerEntryStat, type WalkerFileSystem, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, nodeWalkerFs, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
|
|
1251
1347
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { captureEnvVars } from "./utils/capture-env.js";
|
|
|
8
8
|
import { captureSettings, hashSettings } from "./utils/capture-settings.js";
|
|
9
9
|
import { processFailure } from "./utils/process-failure.js";
|
|
10
10
|
import { AgentReporter } from "./reporter.js";
|
|
11
|
+
import { nodeWalkerFs } from "./utils/walker-fs.js";
|
|
11
12
|
import { findTestFiles } from "./utils/find-test-files.js";
|
|
12
13
|
import { Tag } from "./utils/tag.js";
|
|
13
14
|
import { DefaultDiscoverStrategy, DiscoverStrategy } from "./utils/discover-strategy.js";
|
|
@@ -28,4 +29,4 @@ const COVERAGE_LEVELS_PER_FILE = AgentPlugin.COVERAGE_LEVELS_PER_FILE;
|
|
|
28
29
|
const COVERAGE_AUTOUPDATE = AgentPlugin.COVERAGE_AUTOUPDATE;
|
|
29
30
|
|
|
30
31
|
//#endregion
|
|
31
|
-
export { AgentPlugin, AgentReporter, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, ConfigValidation, ConfigValidationLive, ConfigValidationTest, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, CoverageLevel, DefaultDiscoverStrategy, DiscoverStrategy, ReporterLive, Tag, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
|
|
32
|
+
export { AgentPlugin, AgentReporter, CONSOLE_REPORTERS, COVERAGE_AUTOUPDATE, COVERAGE_LEVELS, COVERAGE_LEVELS_PER_FILE, CURRENT_PLUGIN_VERSION, ConfigValidation, ConfigValidationLive, ConfigValidationTest, CoverageAnalyzer, CoverageAnalyzerLive, CoverageAnalyzerTest, CoverageLevel, DefaultDiscoverStrategy, DiscoverStrategy, ReporterLive, Tag, captureEnvVars, captureSettings, classifyByDirectory, classifyByFilename, combineClassifiers, discoverProjects, findTestFiles, hashSettings, nodeWalkerFs, processFailure, resolveCoverageInput, resolveThresholds, stripConsoleReporters, validateCoverageConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Vitest plugin for the vitest-agent ecosystem: owns persistence, classification, baselines, trends, and dispatches rendering to a configurable reporter.",
|
|
6
6
|
"keywords": [
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@effect/platform-node": "4.0.0-rc.109",
|
|
43
43
|
"@effect/sql-sqlite-node": "4.0.0-rc.109",
|
|
44
|
-
"@effected/workspaces": "^0.
|
|
45
|
-
"@vitest-agent/cli": "2.2.
|
|
46
|
-
"@vitest-agent/mcp": "2.3.
|
|
47
|
-
"@vitest-agent/reporter": "2.1.
|
|
48
|
-
"@vitest-agent/sdk": "2.4.
|
|
44
|
+
"@effected/workspaces": "^0.16.0",
|
|
45
|
+
"@vitest-agent/cli": "2.2.3",
|
|
46
|
+
"@vitest-agent/mcp": "2.3.3",
|
|
47
|
+
"@vitest-agent/reporter": "2.1.3",
|
|
48
|
+
"@vitest-agent/sdk": "2.4.3",
|
|
49
49
|
"effect": "4.0.0-rc.109",
|
|
50
|
-
"magic-string": "^1.2.
|
|
50
|
+
"magic-string": "^1.2.1"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"@vitest/coverage-istanbul": "^4.1.0",
|
package/plugin.js
CHANGED
|
@@ -100,7 +100,7 @@ const aggregatedReporterByVitest = /* @__PURE__ */ new WeakSet();
|
|
|
100
100
|
*
|
|
101
101
|
* @public
|
|
102
102
|
*/
|
|
103
|
-
const CURRENT_PLUGIN_VERSION = "2.
|
|
103
|
+
const CURRENT_PLUGIN_VERSION = "2.4.0";
|
|
104
104
|
const TEST_FILE_DIR_RE = new RegExp(`/(?:${SRC_DIR}|${TEST_DIR})/`);
|
|
105
105
|
const isTestFile = (id) => isTestFileName(id) && TEST_FILE_DIR_RE.test(id);
|
|
106
106
|
/**
|
package/tsdoc-metadata.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { toPosixPath } from "./to-posix-path.js";
|
|
2
|
+
import { nodeWalkerFs } from "./walker-fs.js";
|
|
2
3
|
import { DefaultDiscoverStrategy } from "./discover-strategy.js";
|
|
3
4
|
import { isTestShapedPackage } from "./is-test-shaped-package.js";
|
|
4
5
|
import { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR } from "@vitest-agent/sdk";
|
|
5
6
|
import { isAbsolute, join, normalize, relative } from "node:path";
|
|
6
|
-
import { readdir, stat } from "node:fs/promises";
|
|
7
7
|
import { findWorkspaceRootSync, getWorkspacePackagesSync } from "@effected/workspaces";
|
|
8
8
|
import { nodeSyncOps } from "@effected/workspaces/node-sync";
|
|
9
9
|
|
|
10
10
|
//#region src/utils/discover-projects.ts
|
|
11
11
|
const DISCOVERY_LAST_SCAN_SYMBOL = Symbol.for("vitest-agent:discovery:last-scan-at");
|
|
12
12
|
const warnedDeclinedPackagePaths = /* @__PURE__ */ new Set();
|
|
13
|
-
async function warnIfDeclinedPackageIsTestShaped(pkg) {
|
|
13
|
+
async function warnIfDeclinedPackageIsTestShaped(pkg, fs) {
|
|
14
14
|
const normPath = normalize(pkg.path);
|
|
15
15
|
if (warnedDeclinedPackagePaths.has(normPath)) return;
|
|
16
16
|
warnedDeclinedPackagePaths.add(normPath);
|
|
17
17
|
let testShaped;
|
|
18
18
|
try {
|
|
19
|
-
testShaped = await isTestShapedPackage(pkg.path);
|
|
19
|
+
testShaped = await isTestShapedPackage(pkg.path, fs);
|
|
20
20
|
} catch (error) {
|
|
21
21
|
warnedDeclinedPackagePaths.delete(normPath);
|
|
22
22
|
throw error;
|
|
@@ -44,16 +44,16 @@ const _cache = /* @__PURE__ */ new Map();
|
|
|
44
44
|
* subprocess-e2e fixture that installs deps), and Node's recursive `readdir`
|
|
45
45
|
* follows symlinked directories, so an unguarded call would walk into it.
|
|
46
46
|
*/
|
|
47
|
-
async function computeDirSignature(dirPath) {
|
|
47
|
+
async function computeDirSignature(dirPath, fs) {
|
|
48
48
|
const parts = [];
|
|
49
|
-
await walkDirSignature(dirPath, dirPath, parts);
|
|
49
|
+
await walkDirSignature(dirPath, dirPath, parts, fs);
|
|
50
50
|
parts.sort();
|
|
51
51
|
return parts.join("|");
|
|
52
52
|
}
|
|
53
|
-
async function walkDirSignature(root, dir, parts) {
|
|
53
|
+
async function walkDirSignature(root, dir, parts, fs) {
|
|
54
54
|
let entries;
|
|
55
55
|
try {
|
|
56
|
-
entries = await
|
|
56
|
+
entries = await fs.readDirectory(dir);
|
|
57
57
|
} catch {
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
@@ -61,14 +61,11 @@ async function walkDirSignature(root, dir, parts) {
|
|
|
61
61
|
const fullPath = join(dir, ent.name);
|
|
62
62
|
if (ent.isDirectory()) {
|
|
63
63
|
if (NON_DISCOVERABLE_DIRS.has(ent.name)) continue;
|
|
64
|
-
await walkDirSignature(root, fullPath, parts);
|
|
64
|
+
await walkDirSignature(root, fullPath, parts, fs);
|
|
65
65
|
} else if (ent.isFile()) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} catch {
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
66
|
+
const info = await fs.statEntry(fullPath);
|
|
67
|
+
if (info === null) continue;
|
|
68
|
+
const mtimeMs = info.mtimeMs;
|
|
72
69
|
const relPath = toPosixPath(relative(root, fullPath));
|
|
73
70
|
parts.push(`${relPath}:${mtimeMs}`);
|
|
74
71
|
}
|
|
@@ -85,11 +82,11 @@ async function walkDirSignature(root, dir, parts) {
|
|
|
85
82
|
* test file anywhere else, so nothing else can change the emitted project list
|
|
86
83
|
* (issue #227).
|
|
87
84
|
*/
|
|
88
|
-
async function computeWorkspaceSignature(packages) {
|
|
85
|
+
async function computeWorkspaceSignature(packages, fs) {
|
|
89
86
|
const parts = [];
|
|
90
87
|
for (const pkg of packages) {
|
|
91
|
-
const srcSig = await computeDirSignature(join(pkg.path, SRC_DIR));
|
|
92
|
-
const testSig = await computeDirSignature(join(pkg.path, TEST_DIR));
|
|
88
|
+
const srcSig = await computeDirSignature(join(pkg.path, SRC_DIR), fs);
|
|
89
|
+
const testSig = await computeDirSignature(join(pkg.path, TEST_DIR), fs);
|
|
93
90
|
parts.push(`${pkg.path}::src=${srcSig}::__test__=${testSig}`);
|
|
94
91
|
}
|
|
95
92
|
return parts.join("\n");
|
|
@@ -104,14 +101,16 @@ async function discoverProjects(options) {
|
|
|
104
101
|
const strategy = options?.strategy;
|
|
105
102
|
const cwd = options?.cwd;
|
|
106
103
|
const additionalEntries = options?.additionalEntries ?? [];
|
|
107
|
-
const
|
|
104
|
+
const fs = options?.fs ?? nodeWalkerFs;
|
|
105
|
+
const syncOps = options?.syncOps ?? nodeSyncOps;
|
|
106
|
+
const root = findWorkspaceRootSync(cwd ?? process.cwd(), syncOps);
|
|
108
107
|
if (!root) throw new Error(`[vitest-agent] Could not find workspace root from ${cwd ?? process.cwd()}. Ensure a pnpm-workspace.yaml or package.json with "workspaces" exists.`);
|
|
109
108
|
const useCache = strategy === void 0 && additionalEntries.length === 0;
|
|
110
109
|
const resolvedStrategy = strategy ?? new DefaultDiscoverStrategy();
|
|
111
|
-
const packages = getWorkspacePackagesSync(root,
|
|
110
|
+
const packages = getWorkspacePackagesSync(root, syncOps);
|
|
112
111
|
let signature;
|
|
113
112
|
if (useCache) {
|
|
114
|
-
signature = await computeWorkspaceSignature(packages);
|
|
113
|
+
signature = await computeWorkspaceSignature(packages, fs);
|
|
115
114
|
const cached = _cache.get(root);
|
|
116
115
|
if (cached && cached.signature === signature) return cached.result;
|
|
117
116
|
}
|
|
@@ -123,10 +122,11 @@ async function discoverProjects(options) {
|
|
|
123
122
|
name: pkg.name,
|
|
124
123
|
path: pkg.path,
|
|
125
124
|
relativePath: toPosixPath(pkg.relativePath),
|
|
126
|
-
workspaceRoot: root
|
|
125
|
+
workspaceRoot: root,
|
|
126
|
+
fs
|
|
127
127
|
});
|
|
128
128
|
if (config !== null) configs.push(config);
|
|
129
|
-
else await warnIfDeclinedPackageIsTestShaped(pkg);
|
|
129
|
+
else await warnIfDeclinedPackageIsTestShaped(pkg, fs);
|
|
130
130
|
workspaceNames.add(pkg.name);
|
|
131
131
|
workspacePaths.add(normalize(pkg.path));
|
|
132
132
|
}
|
|
@@ -140,7 +140,8 @@ async function discoverProjects(options) {
|
|
|
140
140
|
name: entry.name,
|
|
141
141
|
path: normPath,
|
|
142
142
|
relativePath,
|
|
143
|
-
workspaceRoot: root
|
|
143
|
+
workspaceRoot: root,
|
|
144
|
+
fs
|
|
144
145
|
});
|
|
145
146
|
if (config === null) {
|
|
146
147
|
const strategyName = resolvedStrategy.constructor.name;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { nodeWalkerFs } from "./walker-fs.js";
|
|
1
2
|
import { findTestFiles } from "./find-test-files.js";
|
|
2
3
|
import { Tag } from "./tag.js";
|
|
3
4
|
import { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS } from "@vitest-agent/sdk";
|
|
4
5
|
import { join, sep } from "node:path";
|
|
5
|
-
import { stat } from "node:fs/promises";
|
|
6
6
|
import { configDefaults } from "vitest/config";
|
|
7
7
|
|
|
8
8
|
//#region src/utils/discover-strategy.ts
|
|
@@ -12,15 +12,11 @@ const SETUP_EXTS = [
|
|
|
12
12
|
"js",
|
|
13
13
|
"jsx"
|
|
14
14
|
];
|
|
15
|
-
async function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return false;
|
|
15
|
+
async function detectSetupFile(pkgPath, fs) {
|
|
16
|
+
for (const ext of SETUP_EXTS) {
|
|
17
|
+
const candidate = join(pkgPath, `vitest.setup.${ext}`);
|
|
18
|
+
if ((await fs.statEntry(candidate))?.isFile === true) return `vitest.setup.${ext}`;
|
|
20
19
|
}
|
|
21
|
-
}
|
|
22
|
-
async function detectSetupFile(pkgPath) {
|
|
23
|
-
for (const ext of SETUP_EXTS) if (await isFile(join(pkgPath, `vitest.setup.${ext}`))) return `vitest.setup.${ext}`;
|
|
24
20
|
return null;
|
|
25
21
|
}
|
|
26
22
|
/**
|
|
@@ -112,8 +108,9 @@ var DefaultDiscoverStrategy = class extends DiscoverStrategy {
|
|
|
112
108
|
return ["unit"];
|
|
113
109
|
}
|
|
114
110
|
async buildProject(input) {
|
|
111
|
+
const fs = input.fs ?? nodeWalkerFs;
|
|
115
112
|
const srcPrefix = join(input.path, SRC_DIR);
|
|
116
|
-
const allFiles = await findTestFiles(input.path, [`${SRC_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`, `${TEST_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`]);
|
|
113
|
+
const allFiles = await findTestFiles(input.path, [`${SRC_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`, `${TEST_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`], fs);
|
|
117
114
|
if (allFiles.length === 0) return null;
|
|
118
115
|
const hasSrcTests = allFiles.some((f) => f.startsWith(`${srcPrefix}${sep}`) || f === srcPrefix);
|
|
119
116
|
const hasTestDirTests = allFiles.some((f) => !(f.startsWith(`${srcPrefix}${sep}`) || f === srcPrefix));
|
|
@@ -125,7 +122,7 @@ var DefaultDiscoverStrategy = class extends DiscoverStrategy {
|
|
|
125
122
|
...[SRC_DIR, TEST_DIR].flatMap((root) => [...NON_DISCOVERABLE_DIRS].map((d) => join(input.path, root, "**", d, "**"))),
|
|
126
123
|
...hasTestDirTests ? TEST_HELPER_DIRS.map((d) => join(input.path, TEST_DIR, "**", d, "**")) : []
|
|
127
124
|
];
|
|
128
|
-
const setupFile = await detectSetupFile(input.path);
|
|
125
|
+
const setupFile = await detectSetupFile(input.path, fs);
|
|
129
126
|
return {
|
|
130
127
|
extends: true,
|
|
131
128
|
test: {
|
package/utils/find-test-files.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { toPosixPath } from "./to-posix-path.js";
|
|
2
|
+
import { nodeWalkerFs } from "./walker-fs.js";
|
|
2
3
|
import { NON_DISCOVERABLE_DIRS } from "@vitest-agent/sdk";
|
|
3
4
|
import { join, relative } from "node:path";
|
|
4
|
-
import { readdir } from "node:fs/promises";
|
|
5
5
|
|
|
6
6
|
//#region src/utils/find-test-files.ts
|
|
7
7
|
function globToRegex(pattern) {
|
|
@@ -70,17 +70,17 @@ function toRegexFragment(glob) {
|
|
|
70
70
|
* @returns Absolute paths of matched test files
|
|
71
71
|
* @public
|
|
72
72
|
*/
|
|
73
|
-
async function findTestFiles(dir, patterns) {
|
|
73
|
+
async function findTestFiles(dir, patterns, fs = nodeWalkerFs) {
|
|
74
74
|
if (patterns.length === 0) return [];
|
|
75
75
|
const matchers = patterns.map(globToRegex);
|
|
76
76
|
const results = [];
|
|
77
|
-
await walkDir(dir, dir, matchers, results);
|
|
77
|
+
await walkDir(dir, dir, matchers, results, fs);
|
|
78
78
|
return results;
|
|
79
79
|
}
|
|
80
|
-
async function walkDir(root, dir, matchers, results) {
|
|
80
|
+
async function walkDir(root, dir, matchers, results, fs) {
|
|
81
81
|
let entries;
|
|
82
82
|
try {
|
|
83
|
-
entries = await
|
|
83
|
+
entries = await fs.readDirectory(dir);
|
|
84
84
|
} catch {
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
@@ -88,7 +88,7 @@ async function walkDir(root, dir, matchers, results) {
|
|
|
88
88
|
for (const ent of entries) {
|
|
89
89
|
if (NON_DISCOVERABLE_DIRS.has(ent.name)) continue;
|
|
90
90
|
const fullPath = join(dir, ent.name);
|
|
91
|
-
if (ent.isDirectory()) await walkDir(root, fullPath, matchers, results);
|
|
91
|
+
if (ent.isDirectory()) await walkDir(root, fullPath, matchers, results, fs);
|
|
92
92
|
else if (ent.isFile()) {
|
|
93
93
|
const rel = toPosixPath(relative(root, fullPath));
|
|
94
94
|
if (matchers.some((re) => re.test(rel))) results.push(fullPath);
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
+
import { nodeWalkerFs } from "./walker-fs.js";
|
|
1
2
|
import { findTestFiles } from "./find-test-files.js";
|
|
2
3
|
import { SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX } from "@vitest-agent/sdk";
|
|
3
4
|
import { join } from "node:path";
|
|
4
|
-
import { stat } from "node:fs/promises";
|
|
5
5
|
|
|
6
6
|
//#region src/utils/is-test-shaped-package.ts
|
|
7
|
-
async function isDirectory(p) {
|
|
8
|
-
try {
|
|
9
|
-
return (await stat(p)).isDirectory();
|
|
10
|
-
} catch {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
7
|
/**
|
|
15
8
|
* True when `pkgPath` looks like it was meant to hold tests: a `__test__/`
|
|
16
9
|
* directory exists at the package root (regardless of what's inside it), or
|
|
@@ -31,9 +24,9 @@ async function isDirectory(p) {
|
|
|
31
24
|
* it needs to catch.
|
|
32
25
|
* @internal
|
|
33
26
|
*/
|
|
34
|
-
async function isTestShapedPackage(pkgPath) {
|
|
35
|
-
if (await
|
|
36
|
-
return (await findTestFiles(pkgPath, [`${SRC_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`])).length > 0;
|
|
27
|
+
async function isTestShapedPackage(pkgPath, fs = nodeWalkerFs) {
|
|
28
|
+
if ((await fs.statEntry(join(pkgPath, TEST_DIR)))?.isDirectory === true) return true;
|
|
29
|
+
return (await findTestFiles(pkgPath, [`${SRC_DIR}/**/${TEST_FILE_GLOB_SUFFIX}`], fs)).length > 0;
|
|
37
30
|
}
|
|
38
31
|
|
|
39
32
|
//#endregion
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/walker-fs.ts
|
|
4
|
+
/**
|
|
5
|
+
* {@link WalkerFileSystem} over `node:fs/promises` — the binding every
|
|
6
|
+
* production call site uses, and the default when a walker is called without
|
|
7
|
+
* an explicit port.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
const nodeWalkerFs = {
|
|
11
|
+
readDirectory: async (dir) => await readdir(dir, { withFileTypes: true }),
|
|
12
|
+
statEntry: async (path) => {
|
|
13
|
+
try {
|
|
14
|
+
const info = await stat(path);
|
|
15
|
+
return {
|
|
16
|
+
isFile: info.isFile(),
|
|
17
|
+
isDirectory: info.isDirectory(),
|
|
18
|
+
mtimeMs: info.mtimeMs
|
|
19
|
+
};
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { nodeWalkerFs };
|