From 5bc0b7ceca21a8c55bfccb158ea564a2ebb84b1f Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:14:31 -0400 Subject: [PATCH] fix(@schematics/angular): add Vitest config generation and runner checks This commit expands the `config` schematic to support generating a `vitest-base.config.ts` file and includes runner checks to prevent misconfiguration. Changes include: - Added 'vitest' as a valid type for the `config` schematic in `schema.json`. - Created a new `vitest-base.config.ts.template` file for generating a basic Vitest configuration. - Implemented the `addVitestConfig` function in `packages/schematics/angular/config/index.ts`: - Verifies that the project's `test` target uses the `@angular/build:unit-test` builder. - Copies the `vitest-base.config.ts` template to the project root. - Sets the `runnerConfig` option to `true` in `angular.json` to enable automatic discovery. - Includes warning logic to notify users if the `runner` option in `angular.json` is explicitly set to `karma`, indicating that the generated Vitest config may not be used. --- .../files/vitest-base.config.ts.template | 9 +++ packages/schematics/angular/config/index.ts | 60 +++++++++++++++ .../schematics/angular/config/index_spec.ts | 74 +++++++++++++++++++ .../schematics/angular/config/schema.json | 2 +- 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 packages/schematics/angular/config/files/vitest-base.config.ts.template diff --git a/packages/schematics/angular/config/files/vitest-base.config.ts.template b/packages/schematics/angular/config/files/vitest-base.config.ts.template new file mode 100644 index 000000000000..1f5a2340af39 --- /dev/null +++ b/packages/schematics/angular/config/files/vitest-base.config.ts.template @@ -0,0 +1,9 @@ +// Learn more about Vitest configuration options at https://vitest.dev/config/ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // ... + }, +}); diff --git a/packages/schematics/angular/config/index.ts b/packages/schematics/angular/config/index.ts index 6bfd36f41b84..c4bafc39657e 100644 --- a/packages/schematics/angular/config/index.ts +++ b/packages/schematics/angular/config/index.ts @@ -30,11 +30,71 @@ export default createProjectSchematic((options, { project }) => { return addKarmaConfig(options); case ConfigType.Browserslist: return addBrowserslistConfig(project.root); + case ConfigType.Vitest: + return addVitestConfig(options); default: throw new SchematicsException(`"${options.type}" is an unknown configuration file type.`); } }); +function addVitestConfig(options: ConfigOptions): Rule { + return (tree, context) => + updateWorkspace((workspace) => { + const project = workspace.projects.get(options.project); + if (!project) { + throw new SchematicsException(`Project name "${options.project}" doesn't not exist.`); + } + + const testTarget = project.targets.get('test'); + if (!testTarget) { + throw new SchematicsException( + `No "test" target found for project "${options.project}".` + + ' A "test" target is required to generate a Vitest configuration.', + ); + } + + if (testTarget.builder !== AngularBuilder.BuildUnitTest) { + throw new SchematicsException( + `Cannot add a Vitest configuration as builder for "test" target in project does not` + + ` use "${AngularBuilder.BuildUnitTest}".`, + ); + } + + testTarget.options ??= {}; + testTarget.options.runnerConfig = true; + + // Check runner option. + if (testTarget.options.runner === 'karma') { + context.logger.warn( + `The "test" target is configured to use the "karma" runner in the main options.` + + ' The generated "vitest-base.config.ts" file may not be used.', + ); + } + + for (const [name, config] of Object.entries(testTarget.configurations ?? {})) { + if ( + config && + typeof config === 'object' && + 'runner' in config && + config.runner === 'karma' + ) { + context.logger.warn( + `The "test" target's "${name}" configuration is configured to use the "karma" runner.` + + ' The generated "vitest-base.config.ts" file may not be used for that configuration.', + ); + } + } + + return mergeWith( + apply(url('./files'), [ + filter((p) => p.endsWith('vitest-base.config.ts.template')), + applyTemplates({}), + move(project.root), + ]), + ); + }); +} + async function addBrowserslistConfig(projectRoot: string): Promise { return mergeWith( apply(url('./files'), [ diff --git a/packages/schematics/angular/config/index_spec.ts b/packages/schematics/angular/config/index_spec.ts index a63a58ab6887..bc1715c4866a 100644 --- a/packages/schematics/angular/config/index_spec.ts +++ b/packages/schematics/angular/config/index_spec.ts @@ -185,4 +185,78 @@ describe('Config Schematic', () => { expect(tree.exists('projects/foo/.browserslistrc')).toBeTrue(); }); }); + + describe(`when 'type' is 'vitest'`, () => { + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const angularJson = applicationTree.readJson('angular.json') as any; + angularJson.projects.foo.architect.test.builder = '@angular/build:unit-test'; + applicationTree.overwrite('angular.json', JSON.stringify(angularJson)); + }); + + it('should create a vitest-base.config.ts file', async () => { + const tree = await runConfigSchematic(ConfigType.Vitest); + expect(tree.exists('projects/foo/vitest-base.config.ts')).toBeTrue(); + }); + + it(`should set 'runnerConfig' in test builder`, async () => { + const tree = await runConfigSchematic(ConfigType.Vitest); + const config = JSON.parse(tree.readContent('/angular.json')); + const prj = config.projects.foo; + const { runnerConfig } = prj.architect.test.options; + expect(runnerConfig).toBe(true); + }); + + it('should throw an error if the builder is not "unit-test"', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const angularJson = applicationTree.readJson('angular.json') as any; + angularJson.projects.foo.architect.test.builder = '@angular/build:karma'; + applicationTree.overwrite('angular.json', JSON.stringify(angularJson)); + + await expectAsync(runConfigSchematic(ConfigType.Vitest)).toBeRejectedWithError( + /Cannot add a Vitest configuration as builder for "test" target/, + ); + }); + + it(`should warn when 'runner' is 'karma' in options`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const angularJson = applicationTree.readJson('angular.json') as any; + angularJson.projects.foo.architect.test.options ??= {}; + angularJson.projects.foo.architect.test.options.runner = 'karma'; + applicationTree.overwrite('angular.json', JSON.stringify(angularJson)); + + const logs: string[] = []; + schematicRunner.logger.subscribe(({ message }) => logs.push(message)); + await runConfigSchematic(ConfigType.Vitest); + expect( + logs.some((v) => + v.includes( + `The "test" target is configured to use the "karma" runner in the main options.`, + ), + ), + ).toBeTrue(); + }); + + it(`should warn when 'runner' is 'karma' in a configuration`, async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const angularJson = applicationTree.readJson('angular.json') as any; + angularJson.projects.foo.architect.test.configurations ??= {}; + angularJson.projects.foo.architect.test.configurations.ci = { runner: 'karma' }; + applicationTree.overwrite('angular.json', JSON.stringify(angularJson)); + + const logs: string[] = []; + schematicRunner.logger.subscribe(({ message }) => logs.push(message)); + await runConfigSchematic(ConfigType.Vitest); + expect( + logs.some((v) => v.includes(`"ci" configuration is configured to use the "karma" runner`)), + ).toBeTrue(); + }); + + it(`should not warn when 'runner' is not set`, async () => { + const logs: string[] = []; + schematicRunner.logger.subscribe(({ message }) => logs.push(message)); + await runConfigSchematic(ConfigType.Vitest); + expect(logs.length).toBe(0); + }); + }); }); diff --git a/packages/schematics/angular/config/schema.json b/packages/schematics/angular/config/schema.json index 14bb34f07260..dd755e5ac6ae 100644 --- a/packages/schematics/angular/config/schema.json +++ b/packages/schematics/angular/config/schema.json @@ -16,7 +16,7 @@ "type": { "type": "string", "description": "Specifies the type of configuration file to generate.", - "enum": ["karma", "browserslist"], + "enum": ["karma", "browserslist", "vitest"], "x-prompt": "Which type of configuration file would you like to create?", "$default": { "$source": "argv",