Skip to content

Commit aa88d76

Browse files
committed
fix(@angular/build): introduce vitest-base.config for test configuration
When using the Vitest runner, a standard `vitest.config.js` file can cause conflicts with other tools or IDE extensions that might assume it represents a complete, standalone configuration. However, the builder uses this file only as a *base* configuration, which is then merged with its own internal setup. To avoid this ambiguity, the builder now searches for a `vitest-base.config.(js|ts|...etc)` file when the `runnerConfig` option is set to `true`. This makes the intent clear that the file provides a base configuration specifically for the Angular CLI builder. The search order is as follows: 1. Project Root 2. Workspace Root If no `vitest-base.config.*` file is found, the builder proceeds with its default in-memory configuration. Vitest's default behavior of searching for `vitest.config.*` is explicitly disabled in this mode to ensure predictable and consistent test execution.
1 parent cc16b10 commit aa88d76

File tree

3 files changed

+108
-3
lines changed

3 files changed

+108
-3
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
/**
10+
* @fileoverview
11+
* This file contains utility functions for finding the Vitest base configuration file.
12+
*/
13+
14+
import { readdir } from 'node:fs/promises';
15+
import path from 'node:path';
16+
17+
/**
18+
* A list of potential Vitest configuration filenames.
19+
* The order of the files is important as the first one found will be used.
20+
*/
21+
const POTENTIAL_CONFIGS = [
22+
'vitest-base.config.ts',
23+
'vitest-base.config.mts',
24+
'vitest-base.config.cts',
25+
'vitest-base.config.js',
26+
'vitest-base.config.mjs',
27+
'vitest-base.config.cjs',
28+
];
29+
30+
/**
31+
* Finds the Vitest configuration file in the given search directories.
32+
*
33+
* @param searchDirs An array of directories to search for the configuration file.
34+
* @returns The path to the configuration file, or `false` if no file is found.
35+
* Returning `false` is used to disable Vitest's default configuration file search.
36+
*/
37+
export async function findVitestBaseConfig(searchDirs: string[]): Promise<string | false> {
38+
const uniqueDirs = new Set(searchDirs);
39+
for (const dir of uniqueDirs) {
40+
try {
41+
const entries = await readdir(dir, { withFileTypes: true });
42+
const files = new Set(entries.filter((e) => e.isFile()).map((e) => e.name));
43+
44+
for (const potential of POTENTIAL_CONFIGS) {
45+
if (files.has(potential)) {
46+
return path.join(dir, potential);
47+
}
48+
}
49+
} catch {
50+
// Ignore directories that cannot be read
51+
}
52+
}
53+
54+
return false;
55+
}

packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { NormalizedUnitTestBuilderOptions } from '../../options';
2323
import type { TestExecutor } from '../api';
2424
import { setupBrowserConfiguration } from './browser-provider';
25+
import { findVitestBaseConfig } from './configuration';
2526
import { createVitestPlugins } from './plugins';
2627

2728
type VitestCoverageOption = Exclude<InlineConfig['coverage'], undefined>;
@@ -207,11 +208,16 @@ export class VitestExecutor implements TestExecutor {
207208
}
208209
: {};
209210

211+
const runnerConfig = this.options.runnerConfig;
212+
210213
return startVitest(
211214
'test',
212215
undefined,
213216
{
214-
config: this.options.runnerConfig === true ? undefined : this.options.runnerConfig,
217+
config:
218+
runnerConfig === true
219+
? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot])
220+
: runnerConfig,
215221
root: workspaceRoot,
216222
project: ['base', this.projectName],
217223
name: 'base',

packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
4444
});
4545

4646
it('should search for a config file when `true`', async () => {
47-
harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT);
47+
harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT);
4848
harness.useTarget('test', {
4949
...BASE_OPTIONS,
5050
runnerConfig: true,
@@ -57,7 +57,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
5757
});
5858

5959
it('should ignore config file when `false`', async () => {
60-
harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT);
60+
harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT);
6161
harness.useTarget('test', {
6262
...BASE_OPTIONS,
6363
runnerConfig: false,
@@ -70,9 +70,53 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
7070
});
7171

7272
it('should ignore config file by default', async () => {
73+
harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT);
74+
harness.useTarget('test', {
75+
...BASE_OPTIONS,
76+
});
77+
78+
const { result } = await harness.executeOnce();
79+
80+
expect(result?.success).toBeTrue();
81+
harness.expectFile('vitest-results.xml').toNotExist();
82+
});
83+
84+
it('should find and use a `vitest-base.config.mts` in the project root', async () => {
85+
harness.writeFile('vitest-base.config.mts', VITEST_CONFIG_CONTENT);
86+
harness.useTarget('test', {
87+
...BASE_OPTIONS,
88+
runnerConfig: true,
89+
});
90+
91+
const { result } = await harness.executeOnce();
92+
93+
expect(result?.success).toBeTrue();
94+
harness.expectFile('vitest-results.xml').toExist();
95+
});
96+
97+
it('should find and use a `vitest-base.config.js` in the workspace root', async () => {
98+
// This file should be ignored because the new logic looks for `vitest-base.config.*`.
99+
harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT);
100+
// The workspace root is the directory containing the project root in the test harness.
101+
harness.writeFile('vitest-base.config.js', VITEST_CONFIG_CONTENT);
102+
harness.useTarget('test', {
103+
...BASE_OPTIONS,
104+
runnerConfig: true,
105+
});
106+
107+
const { result } = await harness.executeOnce();
108+
109+
expect(result?.success).toBeTrue();
110+
harness.expectFile('vitest-results.xml').toExist();
111+
});
112+
113+
it('should fallback to in-memory config when no base config is found', async () => {
114+
// This file should be ignored because the new logic looks for `vitest-base.config.*`
115+
// and when `runnerConfig` is true, it should not fall back to the default search.
73116
harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT);
74117
harness.useTarget('test', {
75118
...BASE_OPTIONS,
119+
runnerConfig: true,
76120
});
77121

78122
const { result } = await harness.executeOnce();

0 commit comments

Comments
 (0)