From aa88d76ba7f3fcc547a20a86c75a8941433eb7e4 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:18:09 -0400 Subject: [PATCH] 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. --- .../unit-test/runners/vitest/configuration.ts | 55 +++++++++++++++++++ .../unit-test/runners/vitest/executor.ts | 8 ++- .../tests/options/runner-config_spec.ts | 48 +++++++++++++++- 3 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts new file mode 100644 index 000000000000..6df583350e07 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/configuration.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * @fileoverview + * This file contains utility functions for finding the Vitest base configuration file. + */ + +import { readdir } from 'node:fs/promises'; +import path from 'node:path'; + +/** + * A list of potential Vitest configuration filenames. + * The order of the files is important as the first one found will be used. + */ +const POTENTIAL_CONFIGS = [ + 'vitest-base.config.ts', + 'vitest-base.config.mts', + 'vitest-base.config.cts', + 'vitest-base.config.js', + 'vitest-base.config.mjs', + 'vitest-base.config.cjs', +]; + +/** + * Finds the Vitest configuration file in the given search directories. + * + * @param searchDirs An array of directories to search for the configuration file. + * @returns The path to the configuration file, or `false` if no file is found. + * Returning `false` is used to disable Vitest's default configuration file search. + */ +export async function findVitestBaseConfig(searchDirs: string[]): Promise { + const uniqueDirs = new Set(searchDirs); + for (const dir of uniqueDirs) { + try { + const entries = await readdir(dir, { withFileTypes: true }); + const files = new Set(entries.filter((e) => e.isFile()).map((e) => e.name)); + + for (const potential of POTENTIAL_CONFIGS) { + if (files.has(potential)) { + return path.join(dir, potential); + } + } + } catch { + // Ignore directories that cannot be read + } + } + + return false; +} diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts index 4ffebb0ad28b..dee43338338f 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -22,6 +22,7 @@ import { import { NormalizedUnitTestBuilderOptions } from '../../options'; import type { TestExecutor } from '../api'; import { setupBrowserConfiguration } from './browser-provider'; +import { findVitestBaseConfig } from './configuration'; import { createVitestPlugins } from './plugins'; type VitestCoverageOption = Exclude; @@ -207,11 +208,16 @@ export class VitestExecutor implements TestExecutor { } : {}; + const runnerConfig = this.options.runnerConfig; + return startVitest( 'test', undefined, { - config: this.options.runnerConfig === true ? undefined : this.options.runnerConfig, + config: + runnerConfig === true + ? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot]) + : runnerConfig, root: workspaceRoot, project: ['base', this.projectName], name: 'base', diff --git a/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts index 54e4d9b21d13..b061b88e990c 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/runner-config_spec.ts @@ -44,7 +44,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { }); it('should search for a config file when `true`', async () => { - harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT); harness.useTarget('test', { ...BASE_OPTIONS, runnerConfig: true, @@ -57,7 +57,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { }); it('should ignore config file when `false`', async () => { - harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT); harness.useTarget('test', { ...BASE_OPTIONS, runnerConfig: false, @@ -70,9 +70,53 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { }); it('should ignore config file by default', async () => { + harness.writeFile('vitest-base.config.ts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toNotExist(); + }); + + it('should find and use a `vitest-base.config.mts` in the project root', async () => { + harness.writeFile('vitest-base.config.mts', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toExist(); + }); + + it('should find and use a `vitest-base.config.js` in the workspace root', async () => { + // This file should be ignored because the new logic looks for `vitest-base.config.*`. + harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); + // The workspace root is the directory containing the project root in the test harness. + harness.writeFile('vitest-base.config.js', VITEST_CONFIG_CONTENT); + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + harness.expectFile('vitest-results.xml').toExist(); + }); + + it('should fallback to in-memory config when no base config is found', async () => { + // This file should be ignored because the new logic looks for `vitest-base.config.*` + // and when `runnerConfig` is true, it should not fall back to the default search. harness.writeFile('vitest.config.ts', VITEST_CONFIG_CONTENT); harness.useTarget('test', { ...BASE_OPTIONS, + runnerConfig: true, }); const { result } = await harness.executeOnce();