From f8e6a72fbaf01c404d833aebe0b4f33fa1fe6ab1 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 30 Oct 2025 14:39:50 -0400 Subject: [PATCH] refactor(@angular/build): refactor Vitest configuration to use project-based setup Refactors the Vitest executor and plugin creation to streamline configuration. This moves project-specific settings into the `startVitest` call and simplifies the `createVitestPlugins` function for better modularity. --- .../unit-test/runners/vitest/executor.ts | 40 +++- .../unit-test/runners/vitest/plugins.ts | 213 ++++++++---------- 2 files changed, 118 insertions(+), 135 deletions(-) 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 700e9d4de23b..f7ff75c82d70 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 @@ -190,7 +190,7 @@ export class VitestExecutor implements TestExecutor { ); const testSetupFiles = this.prepareSetupFiles(); - const plugins = createVitestPlugins(this.options, testSetupFiles, browserOptions, { + const plugins = createVitestPlugins({ workspaceRoot, projectSourceRoot: this.options.projectSourceRoot, projectName: this.projectName, @@ -209,36 +209,54 @@ export class VitestExecutor implements TestExecutor { : {}; const runnerConfig = this.options.runnerConfig; + const externalConfigPath = + runnerConfig === true + ? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot]) + : runnerConfig; + const projectName = this.projectName; return startVitest( 'test', undefined, { - config: - runnerConfig === true - ? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot]) - : runnerConfig, + config: externalConfigPath, root: workspaceRoot, - project: ['base', this.projectName], - name: 'base', - include: [], + project: projectName, + outputFile, testNamePattern: this.options.filter, watch, ui, + ...debugOptions, }, { test: { coverage: await generateCoverageOption(coverage, this.projectName), - outputFile, - ...debugOptions, ...(reporters ? { reporters } : {}), + projects: [ + { + extends: externalConfigPath || true, + test: { + name: projectName, + globals: true, + setupFiles: testSetupFiles, + ...(this.options.exclude ? { exclude: this.options.exclude } : {}), + browser: browserOptions.browser, + // Use `jsdom` if no browsers are explicitly configured. + ...(browserOptions.browser ? {} : { environment: 'jsdom' }), + ...(this.options.include ? { include: this.options.include } : {}), + }, + optimizeDeps: { + noDiscovery: true, + }, + plugins, + }, + ], }, server: { // Disable the actual file watcher. The boolean watch option above should still // be enabled as it controls other internal behavior related to rerunning tests. watch: null, }, - plugins, }, ); } diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts index 649032e3b4fc..fd06475e6980 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts @@ -28,143 +28,108 @@ interface PluginOptions { testFileToEntryPoint: ReadonlyMap; } -export function createVitestPlugins( - options: NormalizedUnitTestBuilderOptions, - testSetupFiles: string[], - browserOptions: BrowserConfiguration, - pluginOptions: PluginOptions, -): VitestPlugins { - const { workspaceRoot, projectName, buildResultFiles, testFileToEntryPoint } = pluginOptions; +export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins { + const { workspaceRoot, buildResultFiles, testFileToEntryPoint } = pluginOptions; return [ { - name: 'angular:project-init', - // Type is incorrect. This allows a Promise. - // eslint-disable-next-line @typescript-eslint/no-misused-promises - configureVitest: async (context) => { - // Create a subproject that can be configured with plugins for browser mode. - // Plugins defined directly in the vite overrides will not be present in the - // browser specific Vite instance. - await context.injectTestProjects({ - test: { - name: projectName, - root: workspaceRoot, - globals: true, - setupFiles: testSetupFiles, - include: options.include, - ...(options.exclude ? { exclude: options.exclude } : {}), - browser: browserOptions.browser, - // Use `jsdom` if no browsers are explicitly configured. - ...(browserOptions.browser ? {} : { environment: 'jsdom' }), - }, - plugins: [ - { - name: 'angular:test-in-memory-provider', - enforce: 'pre', - resolveId: (id, importer) => { - if (importer && (id[0] === '.' || id[0] === '/')) { - let fullPath; - if (testFileToEntryPoint.has(importer)) { - fullPath = toPosixPath(path.join(workspaceRoot, id)); - } else { - fullPath = toPosixPath(path.join(path.dirname(importer), id)); - } + name: 'angular:test-in-memory-provider', + enforce: 'pre', + resolveId: (id, importer) => { + if (importer && (id[0] === '.' || id[0] === '/')) { + let fullPath; + if (testFileToEntryPoint.has(importer)) { + fullPath = toPosixPath(path.join(workspaceRoot, id)); + } else { + fullPath = toPosixPath(path.join(path.dirname(importer), id)); + } - const relativePath = path.relative(workspaceRoot, fullPath); - if (buildResultFiles.has(toPosixPath(relativePath))) { - return fullPath; - } - } + const relativePath = path.relative(workspaceRoot, fullPath); + if (buildResultFiles.has(toPosixPath(relativePath))) { + return fullPath; + } + } - if (testFileToEntryPoint.has(id)) { - return id; - } + if (testFileToEntryPoint.has(id)) { + return id; + } - assert(buildResultFiles.size > 0, 'buildResult must be available for resolving.'); - const relativePath = path.relative(workspaceRoot, id); - if (buildResultFiles.has(toPosixPath(relativePath))) { - return id; - } - }, - load: async (id) => { - assert( - buildResultFiles.size > 0, - 'buildResult must be available for in-memory loading.', - ); + assert(buildResultFiles.size > 0, 'buildResult must be available for resolving.'); + const relativePath = path.relative(workspaceRoot, id); + if (buildResultFiles.has(toPosixPath(relativePath))) { + return id; + } + }, + load: async (id) => { + assert(buildResultFiles.size > 0, 'buildResult must be available for in-memory loading.'); - // Attempt to load as a source test file. - const entryPoint = testFileToEntryPoint.get(id); - let outputPath; - if (entryPoint) { - outputPath = entryPoint + '.js'; + // Attempt to load as a source test file. + const entryPoint = testFileToEntryPoint.get(id); + let outputPath; + if (entryPoint) { + outputPath = entryPoint + '.js'; - // To support coverage exclusion of the actual test file, the virtual - // test entry point only references the built and bundled intermediate file. - return { - code: `import "./${outputPath}";`, - }; - } else { - // Attempt to load as a built artifact. - const relativePath = path.relative(workspaceRoot, id); - outputPath = toPosixPath(relativePath); - } + // To support coverage exclusion of the actual test file, the virtual + // test entry point only references the built and bundled intermediate file. + return { + code: `import "./${outputPath}";`, + }; + } else { + // Attempt to load as a built artifact. + const relativePath = path.relative(workspaceRoot, id); + outputPath = toPosixPath(relativePath); + } - const outputFile = buildResultFiles.get(outputPath); - if (outputFile) { - const sourceMapPath = outputPath + '.map'; - const sourceMapFile = buildResultFiles.get(sourceMapPath); - const code = - outputFile.origin === 'memory' - ? Buffer.from(outputFile.contents).toString('utf-8') - : await readFile(outputFile.inputPath, 'utf-8'); - const sourceMapText = sourceMapFile - ? sourceMapFile.origin === 'memory' - ? Buffer.from(sourceMapFile.contents).toString('utf-8') - : await readFile(sourceMapFile.inputPath, 'utf-8') - : undefined; + const outputFile = buildResultFiles.get(outputPath); + if (outputFile) { + const sourceMapPath = outputPath + '.map'; + const sourceMapFile = buildResultFiles.get(sourceMapPath); + const code = + outputFile.origin === 'memory' + ? Buffer.from(outputFile.contents).toString('utf-8') + : await readFile(outputFile.inputPath, 'utf-8'); + const sourceMapText = sourceMapFile + ? sourceMapFile.origin === 'memory' + ? Buffer.from(sourceMapFile.contents).toString('utf-8') + : await readFile(sourceMapFile.inputPath, 'utf-8') + : undefined; - // Vitest will include files in the coverage report if the sourcemap contains no sources. - // For builder-internal generated code chunks, which are typically helper functions, - // a virtual source is added to the sourcemap to prevent them from being incorrectly - // included in the final coverage report. - const map = sourceMapText ? JSON.parse(sourceMapText) : undefined; - if (map) { - if (!map.sources?.length && !map.sourcesContent?.length && !map.mappings) { - map.sources = ['virtual:builder']; - } - } + // Vitest will include files in the coverage report if the sourcemap contains no sources. + // For builder-internal generated code chunks, which are typically helper functions, + // a virtual source is added to the sourcemap to prevent them from being incorrectly + // included in the final coverage report. + const map = sourceMapText ? JSON.parse(sourceMapText) : undefined; + if (map) { + if (!map.sources?.length && !map.sourcesContent?.length && !map.mappings) { + map.sources = ['virtual:builder']; + } + } - return { - code, - map, - }; - } - }, - configureServer: (server) => { - server.middlewares.use( - createBuildAssetsMiddleware(server.config.base, buildResultFiles), - ); - }, - }, + return { + code, + map, + }; + } + }, + configureServer: (server) => { + server.middlewares.use(createBuildAssetsMiddleware(server.config.base, buildResultFiles)); + }, + }, + { + name: 'angular:html-index', + transformIndexHtml: () => { + // Add all global stylesheets + if (buildResultFiles.has('styles.css')) { + return [ { - name: 'angular:html-index', - transformIndexHtml: () => { - // Add all global stylesheets - if (buildResultFiles.has('styles.css')) { - return [ - { - tag: 'link', - attrs: { href: 'styles.css', rel: 'stylesheet' }, - injectTo: 'head', - }, - ]; - } - - return []; - }, + tag: 'link', + attrs: { href: 'styles.css', rel: 'stylesheet' }, + injectTo: 'head', }, - ], - }); + ]; + } + + return []; }, }, ];