@savvy-web/vitest 1.4.0 → 1.5.1

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.
Files changed (4) hide show
  1. package/index.d.ts +523 -556
  2. package/index.js +783 -517
  3. package/package.json +49 -59
  4. package/tsdoc-metadata.json +11 -11
package/index.js CHANGED
@@ -3,520 +3,786 @@ import { cpus } from "node:os";
3
3
  import { join, relative } from "node:path";
4
4
  import { AgentPlugin } from "vitest-agent-reporter";
5
5
  import { getWorkspaceManagerRoot, getWorkspacePackagePaths } from "workspace-tools";
6
- class VitestProject {
7
- #name;
8
- #kind;
9
- #config;
10
- #coverageExcludes = [];
11
- constructor(options, defaults){
12
- this.#name = options.name;
13
- this.#kind = options.kind ?? "unit";
14
- const { test: defaultTest, ...defaultRest } = defaults;
15
- const { test: overrideTest, ...overrideRest } = options.overrides ?? {};
16
- this.#config = {
17
- extends: true,
18
- ...defaultRest,
19
- ...overrideRest,
20
- test: {
21
- ...defaultTest,
22
- ...overrideTest,
23
- name: options.name,
24
- include: options.include
25
- }
26
- };
27
- }
28
- get name() {
29
- return this.#name;
30
- }
31
- get kind() {
32
- return this.#kind;
33
- }
34
- get coverageExcludes() {
35
- return this.#coverageExcludes;
36
- }
37
- toConfig() {
38
- return this.#config;
39
- }
40
- clone() {
41
- const { test, ...rest } = this.#config;
42
- const cloned = new VitestProject({
43
- name: this.#name,
44
- include: test?.include ?? [],
45
- kind: this.#kind
46
- }, {});
47
- cloned.#config = {
48
- ...rest,
49
- test: test ? {
50
- ...test
51
- } : void 0
52
- };
53
- cloned.#coverageExcludes.push(...this.#coverageExcludes);
54
- return cloned;
55
- }
56
- override(config) {
57
- const { test: overrideTest, ...overrideRest } = config;
58
- const { test: existingTest, ...existingRest } = this.#config;
59
- this.#config = {
60
- ...existingRest,
61
- ...overrideRest,
62
- test: {
63
- ...existingTest,
64
- ...overrideTest,
65
- name: this.#name,
66
- include: existingTest?.include
67
- }
68
- };
69
- return this;
70
- }
71
- addInclude(...patterns) {
72
- const { test: existingTest, ...rest } = this.#config;
73
- this.#config = {
74
- ...rest,
75
- test: {
76
- ...existingTest,
77
- include: [
78
- ...existingTest?.include ?? [],
79
- ...patterns
80
- ]
81
- }
82
- };
83
- return this;
84
- }
85
- addExclude(...patterns) {
86
- const { test: existingTest, ...rest } = this.#config;
87
- this.#config = {
88
- ...rest,
89
- test: {
90
- ...existingTest,
91
- exclude: [
92
- ...existingTest?.exclude ?? [],
93
- ...patterns
94
- ]
95
- }
96
- };
97
- return this;
98
- }
99
- addCoverageExclude(...patterns) {
100
- this.#coverageExcludes.push(...patterns);
101
- return this;
102
- }
103
- static unit(options) {
104
- return new VitestProject({
105
- ...options,
106
- kind: "unit"
107
- }, {
108
- test: {
109
- environment: "node"
110
- }
111
- });
112
- }
113
- static e2e(options) {
114
- const concurrency = Math.max(1, Math.min(8, Math.floor(cpus().length / 2)));
115
- return new VitestProject({
116
- ...options,
117
- kind: "e2e"
118
- }, {
119
- test: {
120
- environment: "node",
121
- testTimeout: 120000,
122
- hookTimeout: 60000,
123
- maxConcurrency: concurrency
124
- }
125
- });
126
- }
127
- static int(options) {
128
- const concurrency = Math.max(1, Math.min(8, Math.floor(cpus().length / 2)));
129
- return new VitestProject({
130
- ...options,
131
- kind: "int"
132
- }, {
133
- test: {
134
- environment: "node",
135
- testTimeout: 60000,
136
- hookTimeout: 30000,
137
- maxConcurrency: concurrency
138
- }
139
- });
140
- }
141
- static custom(kind, options) {
142
- return new VitestProject({
143
- ...options,
144
- kind
145
- }, {});
146
- }
147
- }
148
- class VitestConfig {
149
- static DEFAULT_COVERAGE_EXCLUDE = [
150
- "**/*.{test,spec}.{ts,tsx,js,jsx}",
151
- "**/__test__/**",
152
- "**/generated/**"
153
- ];
154
- static COVERAGE_LEVELS = Object.freeze({
155
- none: {
156
- lines: 0,
157
- branches: 0,
158
- functions: 0,
159
- statements: 0
160
- },
161
- basic: {
162
- lines: 50,
163
- branches: 50,
164
- functions: 50,
165
- statements: 50
166
- },
167
- standard: {
168
- lines: 70,
169
- branches: 65,
170
- functions: 70,
171
- statements: 70
172
- },
173
- strict: {
174
- lines: 80,
175
- branches: 75,
176
- functions: 80,
177
- statements: 80
178
- },
179
- full: {
180
- lines: 90,
181
- branches: 85,
182
- functions: 90,
183
- statements: 90
184
- }
185
- });
186
- static cachedProjects = null;
187
- static cachedVitestProjects = null;
188
- static async create(options, postProcess) {
189
- const specificProjects = VitestConfig.getSpecificProjects();
190
- const { projects, vitestProjects } = VitestConfig.discoverWorkspaceProjects();
191
- const thresholds = VitestConfig.resolveThresholds(options);
192
- const coverageConfig = VitestConfig.getCoverageConfig(specificProjects, projects, options);
193
- const workingProjects = vitestProjects.map((p)=>p.clone());
194
- VitestConfig.applyKindOverrides(workingProjects, options);
195
- const isCI = Boolean(process.env.GITHUB_ACTIONS);
196
- const reporters = isCI ? [
197
- "default",
198
- "github-actions"
199
- ] : [
200
- "default"
201
- ];
202
- let config = {
203
- test: {
204
- reporters,
205
- projects: workingProjects.map((p)=>p.toConfig()),
206
- ...options?.pool ? {
207
- pool: options.pool
208
- } : {},
209
- coverage: {
210
- provider: "v8",
211
- ...coverageConfig,
212
- enabled: true
213
- }
214
- }
215
- };
216
- if (options?.agentReporter !== false) {
217
- const agentOpts = "object" == typeof options?.agentReporter ? options.agentReporter : {};
218
- const targets = VitestConfig.resolveTargets(options);
219
- const hasExplicitTargets = agentOpts.reporter?.coverageTargets !== void 0;
220
- if (hasExplicitTargets && options?.coverageTargets !== void 0) console.warn("[@savvy-web/vitest] Both top-level coverageTargets and agentReporter.reporter.coverageTargets are set. Using agentReporter.reporter.coverageTargets.");
221
- const plugin = AgentPlugin({
222
- strategy: "own",
223
- ...agentOpts,
224
- reporter: {
225
- coverageThresholds: {
226
- ...thresholds
227
- },
228
- ...!hasExplicitTargets ? {
229
- coverageTargets: {
230
- ...targets
231
- }
232
- } : {},
233
- ...agentOpts.reporter
234
- }
235
- });
236
- config.plugins = [
237
- plugin
238
- ];
239
- }
240
- if (postProcess) {
241
- const result = postProcess(config);
242
- if (void 0 !== result) config = result;
243
- }
244
- return config;
245
- }
246
- static applyKindOverrides(vitestProjects, options) {
247
- if (!options) return;
248
- const kindOptions = {
249
- unit: options.unit,
250
- e2e: options.e2e,
251
- int: options.int
252
- };
253
- for (const [kind, override] of Object.entries(kindOptions)){
254
- if (void 0 === override) continue;
255
- const projectsOfKind = vitestProjects.filter((p)=>p.kind === kind);
256
- if ("function" == typeof override) {
257
- const map = new Map();
258
- for (const p of projectsOfKind)map.set(p.name, p);
259
- override(map);
260
- } else for (const p of projectsOfKind)p.override({
261
- test: override
262
- });
263
- }
264
- }
265
- static getSpecificProjects() {
266
- const args = process.argv;
267
- const projects = [];
268
- for(let i = 0; i < args.length; i++){
269
- const arg = args[i];
270
- if (arg.startsWith("--project=")) {
271
- const value = arg.split("=")[1];
272
- if (value) projects.push(value);
273
- } else if ("--project" === arg && i + 1 < args.length) {
274
- const value = args[i + 1];
275
- if (value) projects.push(value);
276
- i++;
277
- }
278
- }
279
- return projects;
280
- }
281
- static getPackageNameFromPath(packagePath) {
282
- try {
283
- const content = readFileSync(join(packagePath, "package.json"), "utf8");
284
- return JSON.parse(content).name ?? null;
285
- } catch {
286
- return null;
287
- }
288
- }
289
- static isDirectory(dirPath) {
290
- try {
291
- return statSync(dirPath).isDirectory();
292
- } catch {
293
- return false;
294
- }
295
- }
296
- static SETUP_FILE_EXTENSIONS = [
297
- "ts",
298
- "tsx",
299
- "js",
300
- "jsx"
301
- ];
302
- static detectSetupFile(packagePath) {
303
- for (const ext of VitestConfig.SETUP_FILE_EXTENSIONS){
304
- const candidate = join(packagePath, `vitest.setup.${ext}`);
305
- try {
306
- const stat = statSync(candidate);
307
- if (stat.isFile()) return `vitest.setup.${ext}`;
308
- } catch {}
309
- }
310
- return null;
311
- }
312
- static scanForTestFiles(dirPath) {
313
- let hasUnit = false;
314
- let hasE2e = false;
315
- let hasInt = false;
316
- try {
317
- const entries = readdirSync(dirPath, {
318
- withFileTypes: true
319
- });
320
- for (const entry of entries){
321
- if (entry.isDirectory()) {
322
- const sub = VitestConfig.scanForTestFiles(join(dirPath, entry.name));
323
- hasUnit = hasUnit || sub.hasUnit;
324
- hasE2e = hasE2e || sub.hasE2e;
325
- hasInt = hasInt || sub.hasInt;
326
- } else if (entry.isFile()) {
327
- if (/\.e2e\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name)) hasE2e = true;
328
- else if (/\.int\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name)) hasInt = true;
329
- else if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name)) hasUnit = true;
330
- }
331
- if (hasUnit && hasE2e && hasInt) break;
332
- }
333
- } catch {}
334
- return {
335
- hasUnit,
336
- hasE2e,
337
- hasInt
338
- };
339
- }
340
- static buildIncludes(srcGlob, testGlob, pattern) {
341
- const includes = [
342
- `${srcGlob}/**/${pattern}`
343
- ];
344
- if (testGlob) includes.push(`${testGlob}/**/${pattern}`);
345
- return includes;
346
- }
347
- static TEST_DIR_EXCLUSIONS = [
348
- "__test__/fixtures/**",
349
- "__test__/utils/**",
350
- "__test__/unit/fixtures/**",
351
- "__test__/unit/utils/**",
352
- "__test__/e2e/fixtures/**",
353
- "__test__/e2e/utils/**",
354
- "__test__/int/fixtures/**",
355
- "__test__/int/utils/**",
356
- "__test__/integration/fixtures/**",
357
- "__test__/integration/utils/**"
358
- ];
359
- static buildTestDirExclusions(prefix) {
360
- return VitestConfig.TEST_DIR_EXCLUSIONS.map((pattern)=>`${prefix}${pattern}`);
361
- }
362
- static discoverWorkspaceProjects() {
363
- if (VitestConfig.cachedProjects && VitestConfig.cachedVitestProjects) return {
364
- projects: VitestConfig.cachedProjects,
365
- vitestProjects: VitestConfig.cachedVitestProjects
366
- };
367
- const cwd = process.cwd();
368
- const workspaceRoot = getWorkspaceManagerRoot(cwd) ?? cwd;
369
- const workspacePaths = getWorkspacePackagePaths(workspaceRoot) ?? [];
370
- const projects = {};
371
- const vitestProjects = [];
372
- for (const pkgPath of workspacePaths){
373
- const packageName = VitestConfig.getPackageNameFromPath(pkgPath);
374
- if (!packageName) continue;
375
- const srcDirPath = join(pkgPath, "src");
376
- if (!VitestConfig.isDirectory(srcDirPath)) continue;
377
- const relativePath = relative(workspaceRoot, pkgPath) || ".";
378
- projects[packageName] = relativePath;
379
- const testDirPath = join(pkgPath, "__test__");
380
- const hasTestDir = VitestConfig.isDirectory(testDirPath);
381
- const srcScan = VitestConfig.scanForTestFiles(srcDirPath);
382
- const testScan = hasTestDir ? VitestConfig.scanForTestFiles(testDirPath) : {
383
- hasUnit: false,
384
- hasE2e: false,
385
- hasInt: false
386
- };
387
- const hasUnit = srcScan.hasUnit || testScan.hasUnit;
388
- const hasE2e = srcScan.hasE2e || testScan.hasE2e;
389
- const hasInt = srcScan.hasInt || testScan.hasInt;
390
- const kindCount = [
391
- hasUnit,
392
- hasE2e,
393
- hasInt
394
- ].filter(Boolean).length;
395
- const shouldSuffix = kindCount >= 2;
396
- const prefix = "." === relativePath ? "" : `${relativePath}/`;
397
- const srcGlob = `${prefix}src`;
398
- const testGlob = hasTestDir ? `${prefix}__test__` : null;
399
- const testDirExcludes = hasTestDir ? VitestConfig.buildTestDirExclusions(prefix) : [];
400
- const setupFile = VitestConfig.detectSetupFile(pkgPath);
401
- const setupFiles = setupFile ? [
402
- `${prefix}${setupFile}`
403
- ] : void 0;
404
- if (hasUnit) vitestProjects.push(VitestProject.unit({
405
- name: shouldSuffix ? `${packageName}:unit` : packageName,
406
- include: VitestConfig.buildIncludes(srcGlob, testGlob, "*.{test,spec}.{ts,tsx,js,jsx}"),
407
- overrides: {
408
- test: {
409
- ...setupFiles ? {
410
- setupFiles
411
- } : {},
412
- exclude: [
413
- "**/*.e2e.{test,spec}.*",
414
- "**/*.int.{test,spec}.*",
415
- ...testDirExcludes
416
- ]
417
- }
418
- }
419
- }));
420
- if (hasE2e) vitestProjects.push(VitestProject.e2e({
421
- name: shouldSuffix ? `${packageName}:e2e` : packageName,
422
- include: VitestConfig.buildIncludes(srcGlob, testGlob, "*.e2e.{test,spec}.{ts,tsx,js,jsx}"),
423
- overrides: {
424
- test: {
425
- ...setupFiles ? {
426
- setupFiles
427
- } : {},
428
- exclude: [
429
- ...testDirExcludes
430
- ]
431
- }
432
- }
433
- }));
434
- if (hasInt) vitestProjects.push(VitestProject.int({
435
- name: shouldSuffix ? `${packageName}:int` : packageName,
436
- include: VitestConfig.buildIncludes(srcGlob, testGlob, "*.int.{test,spec}.{ts,tsx,js,jsx}"),
437
- overrides: {
438
- test: {
439
- ...setupFiles ? {
440
- setupFiles
441
- } : {},
442
- exclude: [
443
- ...testDirExcludes
444
- ]
445
- }
446
- }
447
- }));
448
- if (!hasUnit && !hasE2e && !hasInt) vitestProjects.push(VitestProject.unit({
449
- name: packageName,
450
- include: VitestConfig.buildIncludes(srcGlob, testGlob, "*.{test,spec}.{ts,tsx,js,jsx}"),
451
- overrides: {
452
- test: {
453
- ...setupFiles ? {
454
- setupFiles
455
- } : {},
456
- exclude: [
457
- ...testDirExcludes
458
- ]
459
- }
460
- }
461
- }));
462
- }
463
- VitestConfig.cachedProjects = projects;
464
- VitestConfig.cachedVitestProjects = vitestProjects;
465
- return {
466
- projects,
467
- vitestProjects
468
- };
469
- }
470
- static resolveThresholds(options) {
471
- if (options?.coverage === void 0) return {
472
- ...VitestConfig.COVERAGE_LEVELS.none
473
- };
474
- if ("string" == typeof options.coverage) return {
475
- ...VitestConfig.COVERAGE_LEVELS[options.coverage]
476
- };
477
- return {
478
- ...options.coverage
479
- };
480
- }
481
- static resolveTargets(options) {
482
- if (options?.coverageTargets === void 0) return {
483
- ...VitestConfig.COVERAGE_LEVELS.basic
484
- };
485
- if ("string" == typeof options.coverageTargets) return {
486
- ...VitestConfig.COVERAGE_LEVELS[options.coverageTargets]
487
- };
488
- return {
489
- ...options.coverageTargets
490
- };
491
- }
492
- static getCoverageConfig(specificProjects, projects, options) {
493
- const exclude = [
494
- ...VitestConfig.DEFAULT_COVERAGE_EXCLUDE,
495
- ...options?.coverageExclude ?? []
496
- ];
497
- const thresholds = VitestConfig.resolveThresholds(options);
498
- const toSrcGlob = (relPath)=>{
499
- const prefix = "." === relPath ? "" : `${relPath}/`;
500
- return `${prefix}src/**/*.ts`;
501
- };
502
- if (specificProjects.length > 0) {
503
- const includes = [];
504
- for (const sp of specificProjects){
505
- const baseName = sp.replace(/:(unit|e2e|int)$/, "");
506
- const relPath = projects[baseName];
507
- if (void 0 !== relPath) includes.push(toSrcGlob(relPath));
508
- }
509
- if (includes.length > 0) return {
510
- include: includes,
511
- exclude,
512
- thresholds
513
- };
514
- }
515
- return {
516
- include: Object.values(projects).map(toSrcGlob),
517
- exclude,
518
- thresholds
519
- };
520
- }
521
- }
522
- export { VitestConfig, VitestProject };
6
+
7
+ //#region src/index.ts
8
+ /**
9
+ * Vitest utility functions for automatic project configuration discovery
10
+ * in pnpm monorepo workspaces.
11
+ *
12
+ * @remarks
13
+ * This package provides two main classes:
14
+ *
15
+ * - {@link VitestProject} - Represents a single Vitest project with sensible
16
+ * defaults per test kind (unit, e2e, or custom).
17
+ *
18
+ * - {@link VitestConfig} - Orchestrates workspace discovery, coverage
19
+ * configuration, reporter selection, and callback invocation.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { VitestConfig } from "@savvy-web/vitest";
24
+ *
25
+ * export default VitestConfig.create();
26
+ * ```
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ /**
31
+ * Represents a single Vitest project with sensible defaults per test kind.
32
+ *
33
+ * @remarks
34
+ * Instances are created through static factory methods. The private constructor
35
+ * enforces that all projects are built with validated merge semantics.
36
+ *
37
+ * Override merge precedence (highest wins):
38
+ * 1. `name` and `include` from options (always win)
39
+ * 2. `overrides.test` fields
40
+ * 3. Factory defaults for `test`
41
+ * 4. Top-level: `overrides` rest spreads over factory defaults rest
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { VitestProject } from "@savvy-web/vitest";
46
+ *
47
+ * const unitProject = VitestProject.unit({
48
+ * name: "@savvy-web/my-lib",
49
+ * include: ["src/**\/*.test.ts"],
50
+ * });
51
+ *
52
+ * const e2eProject = VitestProject.e2e({
53
+ * name: "@savvy-web/my-lib:e2e",
54
+ * include: ["test/e2e/**\/*.test.ts"],
55
+ * });
56
+ *
57
+ * const config = unitProject.toConfig();
58
+ * ```
59
+ *
60
+ * @public
61
+ */
62
+ var VitestProject = class VitestProject {
63
+ #name;
64
+ #kind;
65
+ #config;
66
+ #coverageExcludes = [];
67
+ constructor(options, defaults) {
68
+ this.#name = options.name;
69
+ this.#kind = options.kind ?? "unit";
70
+ const { test: defaultTest, ...defaultRest } = defaults;
71
+ const { test: overrideTest, ...overrideRest } = options.overrides ?? {};
72
+ this.#config = {
73
+ extends: true,
74
+ ...defaultRest,
75
+ ...overrideRest,
76
+ test: {
77
+ ...defaultTest,
78
+ ...overrideTest,
79
+ name: options.name,
80
+ include: options.include
81
+ }
82
+ };
83
+ }
84
+ /**
85
+ * The project name.
86
+ * @see {@link VitestProjectOptions.name}
87
+ */
88
+ get name() {
89
+ return this.#name;
90
+ }
91
+ /**
92
+ * The test kind (e.g., `"unit"`, `"e2e"`, or a custom string).
93
+ * @see {@link VitestProjectKind}
94
+ */
95
+ get kind() {
96
+ return this.#kind;
97
+ }
98
+ /**
99
+ * Coverage exclusion patterns accumulated via {@link addCoverageExclude}.
100
+ *
101
+ * @remarks
102
+ * These patterns are not embedded in the inline project config but are
103
+ * made available for the workspace-level coverage configuration to consume.
104
+ */
105
+ get coverageExcludes() {
106
+ return this.#coverageExcludes;
107
+ }
108
+ /**
109
+ * Returns the vitest-native inline configuration object.
110
+ *
111
+ * @returns A {@link https://vitest.dev/config/ | TestProjectInlineConfiguration}
112
+ * with all defaults and overrides merged
113
+ */
114
+ toConfig() {
115
+ return this.#config;
116
+ }
117
+ /**
118
+ * Creates a clone of this project with independent config state.
119
+ *
120
+ * @remarks
121
+ * The clone has its own config object so mutations via
122
+ * {@link override}, {@link addInclude}, {@link addExclude}, and
123
+ * {@link addCoverageExclude} do not affect the original.
124
+ *
125
+ * @returns A new {@link VitestProject} with the same configuration
126
+ */
127
+ clone() {
128
+ const { test, ...rest } = this.#config;
129
+ const cloned = new VitestProject({
130
+ name: this.#name,
131
+ include: test?.include ?? [],
132
+ kind: this.#kind
133
+ }, {});
134
+ cloned.#config = {
135
+ ...rest,
136
+ test: test ? { ...test } : void 0
137
+ };
138
+ cloned.#coverageExcludes.push(...this.#coverageExcludes);
139
+ return cloned;
140
+ }
141
+ /**
142
+ * Merges additional configuration over the current config.
143
+ *
144
+ * @remarks
145
+ * The {@link VitestProjectOptions.name | name} and
146
+ * {@link VitestProjectOptions.include | include} fields are preserved
147
+ * and cannot be overridden.
148
+ *
149
+ * @param config - Partial configuration to merge
150
+ * @returns `this` for chaining
151
+ */
152
+ override(config) {
153
+ const { test: overrideTest, ...overrideRest } = config;
154
+ const { test: existingTest, ...existingRest } = this.#config;
155
+ this.#config = {
156
+ ...existingRest,
157
+ ...overrideRest,
158
+ test: {
159
+ ...existingTest,
160
+ ...overrideTest,
161
+ name: this.#name,
162
+ include: existingTest?.include
163
+ }
164
+ };
165
+ return this;
166
+ }
167
+ /**
168
+ * Appends glob patterns to the test include list.
169
+ *
170
+ * @param patterns - Glob patterns to add
171
+ * @returns `this` for chaining
172
+ */
173
+ addInclude(...patterns) {
174
+ const { test: existingTest, ...rest } = this.#config;
175
+ this.#config = {
176
+ ...rest,
177
+ test: {
178
+ ...existingTest,
179
+ include: [...existingTest?.include ?? [], ...patterns]
180
+ }
181
+ };
182
+ return this;
183
+ }
184
+ /**
185
+ * Appends glob patterns to the test exclude list.
186
+ *
187
+ * @param patterns - Glob patterns to add
188
+ * @returns `this` for chaining
189
+ */
190
+ addExclude(...patterns) {
191
+ const { test: existingTest, ...rest } = this.#config;
192
+ this.#config = {
193
+ ...rest,
194
+ test: {
195
+ ...existingTest,
196
+ exclude: [...existingTest?.exclude ?? [], ...patterns]
197
+ }
198
+ };
199
+ return this;
200
+ }
201
+ /**
202
+ * Appends glob patterns to the coverage exclusion list.
203
+ *
204
+ * @remarks
205
+ * These patterns are exposed via {@link coverageExcludes} for the
206
+ * workspace-level coverage configuration to consume.
207
+ *
208
+ * @param patterns - Glob patterns to exclude from coverage
209
+ * @returns `this` for chaining
210
+ */
211
+ addCoverageExclude(...patterns) {
212
+ this.#coverageExcludes.push(...patterns);
213
+ return this;
214
+ }
215
+ /**
216
+ * Creates a unit test project with sensible defaults.
217
+ *
218
+ * @remarks
219
+ * Defaults applied: `extends: true`, `environment: "node"`.
220
+ *
221
+ * @param options - Project options (the `kind` field is forced to `"unit"`)
222
+ * @returns A new {@link VitestProject} configured for unit tests
223
+ *
224
+ * @example
225
+ * ```typescript
226
+ * import { VitestProject } from "@savvy-web/vitest";
227
+ *
228
+ * const project = VitestProject.unit({
229
+ * name: "@savvy-web/my-lib",
230
+ * include: ["src/**\/*.test.ts"],
231
+ * });
232
+ * ```
233
+ */
234
+ static unit(options) {
235
+ return new VitestProject({
236
+ ...options,
237
+ kind: "unit"
238
+ }, { test: { environment: "node" } });
239
+ }
240
+ /**
241
+ * Creates an e2e test project with sensible defaults.
242
+ *
243
+ * @remarks
244
+ * Defaults applied: `extends: true`, `environment: "node"`,
245
+ * `testTimeout: 120_000`, `hookTimeout: 60_000`,
246
+ * `maxConcurrency: clamp(floor(cpus / 2), 1, 8)`.
247
+ *
248
+ * @param options - Project options (the `kind` field is forced to `"e2e"`)
249
+ * @returns A new {@link VitestProject} configured for e2e tests
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * import { VitestProject } from "@savvy-web/vitest";
254
+ *
255
+ * const project = VitestProject.e2e({
256
+ * name: "@savvy-web/my-lib:e2e",
257
+ * include: ["test/e2e/**\/*.test.ts"],
258
+ * });
259
+ * ```
260
+ */
261
+ static e2e(options) {
262
+ const concurrency = Math.max(1, Math.min(8, Math.floor(cpus().length / 2)));
263
+ return new VitestProject({
264
+ ...options,
265
+ kind: "e2e"
266
+ }, { test: {
267
+ environment: "node",
268
+ testTimeout: 12e4,
269
+ hookTimeout: 6e4,
270
+ maxConcurrency: concurrency
271
+ } });
272
+ }
273
+ /**
274
+ * Creates an integration test project with sensible defaults.
275
+ *
276
+ * @remarks
277
+ * Defaults applied: `extends: true`, `environment: "node"`,
278
+ * `testTimeout: 60_000`, `hookTimeout: 30_000`,
279
+ * `maxConcurrency: clamp(floor(cpus / 2), 1, 8)`.
280
+ *
281
+ * @param options - Project options (the `kind` field is forced to `"int"`)
282
+ * @returns A new {@link VitestProject} configured for integration tests
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * import { VitestProject } from "@savvy-web/vitest";
287
+ *
288
+ * const project = VitestProject.int({
289
+ * name: "@savvy-web/my-lib:int",
290
+ * include: ["__test__/integration/**\/*.int.test.ts"],
291
+ * });
292
+ * ```
293
+ */
294
+ static int(options) {
295
+ const concurrency = Math.max(1, Math.min(8, Math.floor(cpus().length / 2)));
296
+ return new VitestProject({
297
+ ...options,
298
+ kind: "int"
299
+ }, { test: {
300
+ environment: "node",
301
+ testTimeout: 6e4,
302
+ hookTimeout: 3e4,
303
+ maxConcurrency: concurrency
304
+ } });
305
+ }
306
+ /**
307
+ * Creates a custom test project with no preset defaults beyond `extends: true`.
308
+ *
309
+ * @remarks
310
+ * Use this factory when the built-in `unit()` and `e2e()` presets do not
311
+ * match your needs. The `kind` string is stored on the instance but does
312
+ * not influence any default configuration.
313
+ *
314
+ * @param kind - A custom kind string (e.g., `"integration"`, `"smoke"`)
315
+ * @param options - Project options
316
+ * @returns A new {@link VitestProject} with no preset defaults
317
+ *
318
+ * @example
319
+ * ```typescript
320
+ * import { VitestProject } from "@savvy-web/vitest";
321
+ *
322
+ * const project = VitestProject.custom("integration", {
323
+ * name: "@savvy-web/my-lib:integration",
324
+ * include: ["test/integration/**\/*.test.ts"],
325
+ * });
326
+ * ```
327
+ */
328
+ static custom(kind, options) {
329
+ return new VitestProject({
330
+ ...options,
331
+ kind
332
+ }, {});
333
+ }
334
+ };
335
+ /**
336
+ * Utility class for generating Vitest configuration in monorepo workspaces.
337
+ *
338
+ * @remarks
339
+ * This class automatically discovers packages in a workspace that contain a
340
+ * `src/` directory and generates appropriate {@link VitestProject} configurations.
341
+ * Tests are discovered by filename convention:
342
+ *
343
+ * | Pattern | Kind |
344
+ * | --- | --- |
345
+ * | `*.test.ts` / `*.spec.ts` | unit |
346
+ * | `*.e2e.test.ts` / `*.e2e.spec.ts` | e2e |
347
+ *
348
+ * It supports both running all tests and targeting specific projects via the
349
+ * `--project` command line argument.
350
+ *
351
+ * Results are cached in static properties so that repeated config evaluations
352
+ * during watch mode or HMR do not re-scan the filesystem.
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * import { VitestConfig } from "@savvy-web/vitest";
357
+ *
358
+ * export default VitestConfig.create();
359
+ * ```
360
+ *
361
+ * @public
362
+ */
363
+ var VitestConfig = class VitestConfig {
364
+ /** Default glob patterns excluded from coverage reporting. */
365
+ static DEFAULT_COVERAGE_EXCLUDE = [
366
+ "**/*.{test,spec}.{ts,tsx,js,jsx}",
367
+ "**/__test__/**",
368
+ "**/generated/**"
369
+ ];
370
+ /**
371
+ * Named coverage level presets.
372
+ *
373
+ * @remarks
374
+ * Use a level name with the `coverage` option in {@link VitestConfig.create}
375
+ * to apply a preset. The object is frozen and cannot be mutated.
376
+ *
377
+ * | Level | lines | branches | functions | statements |
378
+ * | -------- | ----- | -------- | --------- | ---------- |
379
+ * | none | 0 | 0 | 0 | 0 |
380
+ * | basic | 50 | 50 | 50 | 50 |
381
+ * | standard | 70 | 65 | 70 | 70 |
382
+ * | strict | 80 | 75 | 80 | 80 |
383
+ * | full | 90 | 85 | 90 | 90 |
384
+ */
385
+ static COVERAGE_LEVELS = Object.freeze({
386
+ none: {
387
+ lines: 0,
388
+ branches: 0,
389
+ functions: 0,
390
+ statements: 0
391
+ },
392
+ basic: {
393
+ lines: 50,
394
+ branches: 50,
395
+ functions: 50,
396
+ statements: 50
397
+ },
398
+ standard: {
399
+ lines: 70,
400
+ branches: 65,
401
+ functions: 70,
402
+ statements: 70
403
+ },
404
+ strict: {
405
+ lines: 80,
406
+ branches: 75,
407
+ functions: 80,
408
+ statements: 80
409
+ },
410
+ full: {
411
+ lines: 90,
412
+ branches: 85,
413
+ functions: 90,
414
+ statements: 90
415
+ }
416
+ });
417
+ static cachedProjects = null;
418
+ static cachedVitestProjects = null;
419
+ /**
420
+ * Creates a complete Vitest configuration by discovering workspace projects
421
+ * and generating appropriate settings.
422
+ *
423
+ * @param options - Optional declarative configuration
424
+ * @param postProcess - Optional escape-hatch callback for full config control
425
+ * @returns The assembled Vitest configuration
426
+ *
427
+ * @see {@link VitestConfigOptions} for available options
428
+ * @see {@link PostProcessCallback} for the post-process callback signature
429
+ */
430
+ static async create(options, postProcess) {
431
+ const specificProjects = VitestConfig.getSpecificProjects();
432
+ const { projects, vitestProjects } = VitestConfig.discoverWorkspaceProjects();
433
+ const thresholds = VitestConfig.resolveThresholds(options);
434
+ const coverageConfig = VitestConfig.getCoverageConfig(specificProjects, projects, options);
435
+ const workingProjects = vitestProjects.map((p) => p.clone());
436
+ VitestConfig.applyKindOverrides(workingProjects, options);
437
+ let config = { test: {
438
+ reporters: Boolean(process.env.GITHUB_ACTIONS) ? ["default", "github-actions"] : ["default"],
439
+ projects: workingProjects.map((p) => p.toConfig()),
440
+ ...options?.pool ? { pool: options.pool } : {},
441
+ coverage: {
442
+ provider: "v8",
443
+ ...coverageConfig,
444
+ enabled: true
445
+ }
446
+ } };
447
+ if (options?.agentReporter !== false) {
448
+ const agentOpts = typeof options?.agentReporter === "object" ? options.agentReporter : {};
449
+ const targets = VitestConfig.resolveTargets(options);
450
+ const hasExplicitTargets = agentOpts.reporter?.coverageTargets !== void 0;
451
+ if (hasExplicitTargets && options?.coverageTargets !== void 0) console.warn("[@savvy-web/vitest] Both top-level coverageTargets and agentReporter.reporter.coverageTargets are set. Using agentReporter.reporter.coverageTargets.");
452
+ const plugin = AgentPlugin({
453
+ strategy: "own",
454
+ ...agentOpts,
455
+ reporter: {
456
+ coverageThresholds: { ...thresholds },
457
+ ...!hasExplicitTargets ? { coverageTargets: { ...targets } } : {},
458
+ ...agentOpts.reporter
459
+ }
460
+ });
461
+ config.plugins = [plugin];
462
+ }
463
+ if (postProcess) {
464
+ const result = postProcess(config);
465
+ if (result !== void 0) config = result;
466
+ }
467
+ return config;
468
+ }
469
+ /**
470
+ * Applies kind-specific overrides to discovered projects.
471
+ *
472
+ * @privateRemarks
473
+ * When the override is an object, it is merged into every project of the
474
+ * matching kind. When it is a callback, it receives a Map of project name
475
+ * to {@link VitestProject} for fine-grained per-project mutation.
476
+ */
477
+ static applyKindOverrides(vitestProjects, options) {
478
+ if (!options) return;
479
+ const kindOptions = {
480
+ unit: options.unit,
481
+ e2e: options.e2e,
482
+ int: options.int
483
+ };
484
+ for (const [kind, override] of Object.entries(kindOptions)) {
485
+ if (override === void 0) continue;
486
+ const projectsOfKind = vitestProjects.filter((p) => p.kind === kind);
487
+ if (typeof override === "function") {
488
+ const map = /* @__PURE__ */ new Map();
489
+ for (const p of projectsOfKind) map.set(p.name, p);
490
+ override(map);
491
+ } else for (const p of projectsOfKind) p.override({ test: override });
492
+ }
493
+ }
494
+ /**
495
+ * Extracts all specific project names from command line arguments.
496
+ *
497
+ * @privateRemarks
498
+ * Supports both `--project=value` and `--project value` formats to match
499
+ * Vitest's own argument parsing behavior. All `--project` flags are
500
+ * collected to support multi-project coverage scoping.
501
+ */
502
+ static getSpecificProjects() {
503
+ const args = process.argv;
504
+ const projects = [];
505
+ for (let i = 0; i < args.length; i++) {
506
+ const arg = args[i];
507
+ if (arg.startsWith("--project=")) {
508
+ const value = arg.split("=")[1];
509
+ if (value) projects.push(value);
510
+ } else if (arg === "--project" && i + 1 < args.length) {
511
+ const value = args[i + 1];
512
+ if (value) projects.push(value);
513
+ i++;
514
+ }
515
+ }
516
+ return projects;
517
+ }
518
+ /**
519
+ * Reads the `name` field from a package's `package.json`.
520
+ *
521
+ * @privateRemarks
522
+ * Uses try/catch because the package.json may not exist or may be malformed.
523
+ * Returns `null` to signal the caller should skip this package.
524
+ */
525
+ static getPackageNameFromPath(packagePath) {
526
+ try {
527
+ const content = readFileSync(join(packagePath, "package.json"), "utf8");
528
+ return JSON.parse(content).name ?? null;
529
+ } catch {
530
+ return null;
531
+ }
532
+ }
533
+ /**
534
+ * Checks whether a path is an existing directory.
535
+ *
536
+ * @privateRemarks
537
+ * Consolidates the repeated `statSync` + `isDirectory()` + try/catch
538
+ * pattern used throughout workspace discovery.
539
+ */
540
+ static isDirectory(dirPath) {
541
+ try {
542
+ return statSync(dirPath).isDirectory();
543
+ } catch {
544
+ return false;
545
+ }
546
+ }
547
+ /** Extensions probed (in order) when detecting a setup file. */
548
+ static SETUP_FILE_EXTENSIONS = [
549
+ "ts",
550
+ "tsx",
551
+ "js",
552
+ "jsx"
553
+ ];
554
+ /**
555
+ * Detects a `vitest.setup.{ts,tsx,js,jsx}` file at the package root.
556
+ *
557
+ * @privateRemarks
558
+ * First match wins. Returns just the filename (e.g. `"vitest.setup.ts"`)
559
+ * so the caller can prepend the relative prefix as needed.
560
+ */
561
+ static detectSetupFile(packagePath) {
562
+ for (const ext of VitestConfig.SETUP_FILE_EXTENSIONS) {
563
+ const candidate = join(packagePath, `vitest.setup.${ext}`);
564
+ try {
565
+ if (statSync(candidate).isFile()) return `vitest.setup.${ext}`;
566
+ } catch {}
567
+ }
568
+ return null;
569
+ }
570
+ /**
571
+ * Recursively scans a directory for test files and classifies them by kind.
572
+ *
573
+ * @privateRemarks
574
+ * Short-circuits as soon as all three kinds (unit, e2e, and int) are
575
+ * found, avoiding unnecessary filesystem traversal.
576
+ */
577
+ static scanForTestFiles(dirPath) {
578
+ let hasUnit = false;
579
+ let hasE2e = false;
580
+ let hasInt = false;
581
+ try {
582
+ const entries = readdirSync(dirPath, { withFileTypes: true });
583
+ for (const entry of entries) {
584
+ if (entry.isDirectory()) {
585
+ const sub = VitestConfig.scanForTestFiles(join(dirPath, entry.name));
586
+ hasUnit = hasUnit || sub.hasUnit;
587
+ hasE2e = hasE2e || sub.hasE2e;
588
+ hasInt = hasInt || sub.hasInt;
589
+ } else if (entry.isFile()) {
590
+ if (/\.e2e\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name)) hasE2e = true;
591
+ else if (/\.int\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name)) hasInt = true;
592
+ else if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(entry.name)) hasUnit = true;
593
+ }
594
+ if (hasUnit && hasE2e && hasInt) break;
595
+ }
596
+ } catch {}
597
+ return {
598
+ hasUnit,
599
+ hasE2e,
600
+ hasInt
601
+ };
602
+ }
603
+ /**
604
+ * Builds include glob patterns for a given relative path and optional
605
+ * test directory.
606
+ */
607
+ static buildIncludes(srcGlob, testGlob, pattern) {
608
+ const includes = [`${srcGlob}/**/${pattern}`];
609
+ if (testGlob) includes.push(`${testGlob}/**/${pattern}`);
610
+ return includes;
611
+ }
612
+ /**
613
+ * Conventional subdirectories under `__test__/` that hold helpers, not
614
+ * test files, and should be excluded from test discovery.
615
+ */
616
+ static TEST_DIR_EXCLUSIONS = [
617
+ "__test__/fixtures/**",
618
+ "__test__/utils/**",
619
+ "__test__/unit/fixtures/**",
620
+ "__test__/unit/utils/**",
621
+ "__test__/e2e/fixtures/**",
622
+ "__test__/e2e/utils/**",
623
+ "__test__/int/fixtures/**",
624
+ "__test__/int/utils/**",
625
+ "__test__/integration/fixtures/**",
626
+ "__test__/integration/utils/**"
627
+ ];
628
+ /**
629
+ * Returns exclusion patterns for fixture/utils directories under
630
+ * `__test__/`, scoped to the given package prefix.
631
+ *
632
+ * @param prefix - Either `"<relativePath>/"` for non-root packages or
633
+ * `""` for the workspace root.
634
+ */
635
+ static buildTestDirExclusions(prefix) {
636
+ return VitestConfig.TEST_DIR_EXCLUSIONS.map((pattern) => `${prefix}${pattern}`);
637
+ }
638
+ /**
639
+ * Discovers all packages in the workspace that contain a `src/` directory
640
+ * and generates {@link VitestProject} instances based on filename conventions.
641
+ *
642
+ * @privateRemarks
643
+ * When a package has both unit and e2e test files, projects are suffixed
644
+ * with `:unit` and `:e2e` to disambiguate. Packages with `src/` but no
645
+ * test files still get a unit project entry as a forward-looking placeholder.
646
+ */
647
+ static discoverWorkspaceProjects() {
648
+ if (VitestConfig.cachedProjects && VitestConfig.cachedVitestProjects) return {
649
+ projects: VitestConfig.cachedProjects,
650
+ vitestProjects: VitestConfig.cachedVitestProjects
651
+ };
652
+ const cwd = process.cwd();
653
+ const workspaceRoot = getWorkspaceManagerRoot(cwd) ?? cwd;
654
+ const workspacePaths = getWorkspacePackagePaths(workspaceRoot) ?? [];
655
+ const projects = {};
656
+ const vitestProjects = [];
657
+ for (const pkgPath of workspacePaths) {
658
+ const packageName = VitestConfig.getPackageNameFromPath(pkgPath);
659
+ if (!packageName) continue;
660
+ const srcDirPath = join(pkgPath, "src");
661
+ if (!VitestConfig.isDirectory(srcDirPath)) continue;
662
+ const relativePath = relative(workspaceRoot, pkgPath) || ".";
663
+ projects[packageName] = relativePath;
664
+ const testDirPath = join(pkgPath, "__test__");
665
+ const hasTestDir = VitestConfig.isDirectory(testDirPath);
666
+ const srcScan = VitestConfig.scanForTestFiles(srcDirPath);
667
+ const testScan = hasTestDir ? VitestConfig.scanForTestFiles(testDirPath) : {
668
+ hasUnit: false,
669
+ hasE2e: false,
670
+ hasInt: false
671
+ };
672
+ const hasUnit = srcScan.hasUnit || testScan.hasUnit;
673
+ const hasE2e = srcScan.hasE2e || testScan.hasE2e;
674
+ const hasInt = srcScan.hasInt || testScan.hasInt;
675
+ const shouldSuffix = [
676
+ hasUnit,
677
+ hasE2e,
678
+ hasInt
679
+ ].filter(Boolean).length >= 2;
680
+ const prefix = relativePath === "." ? "" : `${relativePath}/`;
681
+ const srcGlob = `${prefix}src`;
682
+ const testGlob = hasTestDir ? `${prefix}__test__` : null;
683
+ const testDirExcludes = hasTestDir ? VitestConfig.buildTestDirExclusions(prefix) : [];
684
+ const setupFile = VitestConfig.detectSetupFile(pkgPath);
685
+ const setupFiles = setupFile ? [`${prefix}${setupFile}`] : void 0;
686
+ if (hasUnit) vitestProjects.push(VitestProject.unit({
687
+ name: shouldSuffix ? `${packageName}:unit` : packageName,
688
+ include: VitestConfig.buildIncludes(srcGlob, testGlob, "*.{test,spec}.{ts,tsx,js,jsx}"),
689
+ overrides: { test: {
690
+ ...setupFiles ? { setupFiles } : {},
691
+ exclude: [
692
+ "**/*.e2e.{test,spec}.*",
693
+ "**/*.int.{test,spec}.*",
694
+ ...testDirExcludes
695
+ ]
696
+ } }
697
+ }));
698
+ if (hasE2e) vitestProjects.push(VitestProject.e2e({
699
+ name: shouldSuffix ? `${packageName}:e2e` : packageName,
700
+ include: VitestConfig.buildIncludes(srcGlob, testGlob, "*.e2e.{test,spec}.{ts,tsx,js,jsx}"),
701
+ overrides: { test: {
702
+ ...setupFiles ? { setupFiles } : {},
703
+ exclude: [...testDirExcludes]
704
+ } }
705
+ }));
706
+ if (hasInt) vitestProjects.push(VitestProject.int({
707
+ name: shouldSuffix ? `${packageName}:int` : packageName,
708
+ include: VitestConfig.buildIncludes(srcGlob, testGlob, "*.int.{test,spec}.{ts,tsx,js,jsx}"),
709
+ overrides: { test: {
710
+ ...setupFiles ? { setupFiles } : {},
711
+ exclude: [...testDirExcludes]
712
+ } }
713
+ }));
714
+ if (!hasUnit && !hasE2e && !hasInt) vitestProjects.push(VitestProject.unit({
715
+ name: packageName,
716
+ include: VitestConfig.buildIncludes(srcGlob, testGlob, "*.{test,spec}.{ts,tsx,js,jsx}"),
717
+ overrides: { test: {
718
+ ...setupFiles ? { setupFiles } : {},
719
+ exclude: [...testDirExcludes]
720
+ } }
721
+ }));
722
+ }
723
+ VitestConfig.cachedProjects = projects;
724
+ VitestConfig.cachedVitestProjects = vitestProjects;
725
+ return {
726
+ projects,
727
+ vitestProjects
728
+ };
729
+ }
730
+ /**
731
+ * Resolves coverage thresholds from options.
732
+ *
733
+ * @privateRemarks
734
+ * Priority: `options.coverage` (name or object) \> `COVERAGE_LEVELS.none`.
735
+ */
736
+ static resolveThresholds(options) {
737
+ if (options?.coverage === void 0) return { ...VitestConfig.COVERAGE_LEVELS.none };
738
+ if (typeof options.coverage === "string") return { ...VitestConfig.COVERAGE_LEVELS[options.coverage] };
739
+ return { ...options.coverage };
740
+ }
741
+ /**
742
+ * Resolves coverage targets from options.
743
+ *
744
+ * @privateRemarks
745
+ * Priority: `options.coverageTargets` (name or object) \> `COVERAGE_LEVELS.basic`.
746
+ */
747
+ static resolveTargets(options) {
748
+ if (options?.coverageTargets === void 0) return { ...VitestConfig.COVERAGE_LEVELS.basic };
749
+ if (typeof options.coverageTargets === "string") return { ...VitestConfig.COVERAGE_LEVELS[options.coverageTargets] };
750
+ return { ...options.coverageTargets };
751
+ }
752
+ /**
753
+ * Generates coverage configuration including thresholds.
754
+ *
755
+ * @privateRemarks
756
+ * Strips `:unit`/`:e2e`/`:int` suffix when looking up project paths for
757
+ * `--project` filtering, since coverage applies to the entire package
758
+ * regardless of test kind. When multiple `--project` flags are provided,
759
+ * coverage includes are unioned across all matched packages.
760
+ */
761
+ static getCoverageConfig(specificProjects, projects, options) {
762
+ const exclude = [...VitestConfig.DEFAULT_COVERAGE_EXCLUDE, ...options?.coverageExclude ?? []];
763
+ const thresholds = VitestConfig.resolveThresholds(options);
764
+ const toSrcGlob = (relPath) => {
765
+ return `${relPath === "." ? "" : `${relPath}/`}src/**/*.ts`;
766
+ };
767
+ if (specificProjects.length > 0) {
768
+ const includes = [];
769
+ for (const sp of specificProjects) {
770
+ const relPath = projects[sp.replace(/:(unit|e2e|int)$/, "")];
771
+ if (relPath !== void 0) includes.push(toSrcGlob(relPath));
772
+ }
773
+ if (includes.length > 0) return {
774
+ include: includes,
775
+ exclude,
776
+ thresholds
777
+ };
778
+ }
779
+ return {
780
+ include: Object.values(projects).map(toSrcGlob),
781
+ exclude,
782
+ thresholds
783
+ };
784
+ }
785
+ };
786
+
787
+ //#endregion
788
+ export { VitestConfig, VitestProject };