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 6e621ecc588b..22c5993eabe2 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 @@ -11,6 +11,7 @@ import { readFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { platform } from 'node:os'; import path from 'node:path'; +import type { ExistingRawSourceMap } from 'rolldown'; import type { BrowserConfigOptions, InlineConfig, @@ -178,9 +179,10 @@ export async function createVitestConfigPlugin( }; } +const textDecoder = new TextDecoder('utf-8'); async function loadResultFile(file: ResultFile): Promise { if (file.origin === 'memory') { - return new TextDecoder('utf-8').decode(file.contents); + return textDecoder.decode(file.contents); } return readFile(file.inputPath, 'utf-8'); @@ -215,20 +217,6 @@ export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins } } - 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; - } - } - // Determine the base directory for resolution. let baseDir: string; if (importer) { @@ -265,7 +253,7 @@ export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins if (entryPoint) { outputPath = entryPoint + '.js'; - if (vitestConfig.coverage.enabled) { + if (vitestConfig?.coverage?.enabled) { // To support coverage exclusion of the actual test file, the virtual // test entry point only references the built and bundled intermediate file. // If vitest supported an "excludeOnlyAfterRemap" option, this could be removed completely. @@ -286,26 +274,9 @@ export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins const sourceMapFile = buildResultFiles.get(sourceMapPath); const sourceMapText = sourceMapFile ? await loadResultFile(sourceMapFile) : 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']; - } else if (!vitestConfig.coverage.enabled && Array.isArray(map.sources)) { - map.sources = (map.sources as string[]).map((source) => { - if (source.startsWith('angular:')) { - return source; - } - - // source is relative to the workspace root because the output file is at the root of the output. - const absoluteSource = path.join(workspaceRoot, source); - - return toPosixPath(path.relative(path.dirname(id), absoluteSource)); - }); - } + adjustSourcemapSources(map, !vitestConfig?.coverage?.enabled, workspaceRoot, id); } return { @@ -338,6 +309,40 @@ export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins ]; } +/** + * Adjusts the sources field in a sourcemap to ensure correct source mapping and coverage reporting. + * + * @param map The raw sourcemap to adjust. + * @param rebaseSources Whether to rebase the source paths relative to the test file. + * @param workspaceRoot The root directory of the workspace. + * @param id The ID (path) of the file being loaded. + */ +function adjustSourcemapSources( + map: ExistingRawSourceMap, + rebaseSources: boolean, + workspaceRoot: string, + id: string, +): void { + if (!map.sources?.length && !map.sourcesContent?.length && !map.mappings) { + // 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. + map.sources = ['virtual:builder']; + } else if (rebaseSources && map.sources) { + map.sources = map.sources.map((source) => { + if (!source || source.startsWith('angular:')) { + return source; + } + + // source is relative to the workspace root because the output file is at the root of the output. + const absoluteSource = path.join(workspaceRoot, source); + + return toPosixPath(path.relative(path.dirname(id), absoluteSource)); + }); + } +} + function createSourcemapSupportPlugin(): VitestPlugins[0] { return { name: 'angular:source-map-support',