diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts index b0f230fcf261..054880abeaa0 100644 --- a/packages/coverage-v8/src/provider.ts +++ b/packages/coverage-v8/src/provider.ts @@ -13,7 +13,8 @@ import remapping from '@ampproject/remapping' import { normalize, resolve } from 'pathe' import c from 'picocolors' import { provider } from 'std-env' -import type { EncodedSourceMap } from 'vite-node' +import { cleanUrl } from 'vite-node/utils' +import type { EncodedSourceMap, FetchResult } from 'vite-node' import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config' import { BaseCoverageProvider } from 'vitest/coverage' import type { AfterSuiteRunMeta, CoverageProvider, CoverageV8Options, ReportContext, ResolvedCoverageOptions } from 'vitest' @@ -21,6 +22,7 @@ import type { Vitest } from 'vitest/node' // @ts-expect-error missing types import _TestExclude from 'test-exclude' +import type { ViteNodeServer } from 'vite-node/server' interface TestExclude { new(opts: { @@ -36,6 +38,7 @@ interface TestExclude { } type Options = ResolvedCoverageOptions<'v8'> +type TransformResults = Map // TODO: vite-node should export this const WRAPPER_LENGTH = 185 @@ -99,18 +102,19 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage if (provider === 'stackblitz') this.ctx.logger.log(c.blue(' % ') + c.yellow('@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.')) + const transformResults = normalizeTransformResults(this.ctx.projects.map(project => project.vitenode.fetchCache)) const merged = mergeProcessCovs(this.coverages) const scriptCoverages = merged.result.filter(result => this.testExclude.shouldInstrument(fileURLToPath(result.url))) if (this.options.all && allTestsRun) { const coveredFiles = Array.from(scriptCoverages.map(r => r.url)) - const untestedFiles = await this.getUntestedFiles(coveredFiles) + const untestedFiles = await this.getUntestedFiles(coveredFiles, transformResults) scriptCoverages.push(...untestedFiles) } const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => { - const sources = await this.getSources(url, functions) + const sources = await this.getSources(url, transformResults, functions) // If no source map was found from vite-node we can assume this file was not run in the wrapper const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0 @@ -177,14 +181,14 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage } } - private async getUntestedFiles(testedFiles: string[]): Promise { + private async getUntestedFiles(testedFiles: string[], transformResults: TransformResults): Promise { const includedFiles = await this.testExclude.glob(this.ctx.config.root) const uncoveredFiles = includedFiles .map(file => pathToFileURL(resolve(this.ctx.config.root, file))) .filter(file => !testedFiles.includes(file.href)) return await Promise.all(uncoveredFiles.map(async (uncoveredFile) => { - const { source } = await this.getSources(uncoveredFile.href) + const { source } = await this.getSources(uncoveredFile.href, transformResults) return { url: uncoveredFile.href, @@ -204,16 +208,14 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage })) } - private async getSources(url: string, functions: Profiler.FunctionCoverage[] = []): Promise<{ + private async getSources(url: string, transformResults: TransformResults, functions: Profiler.FunctionCoverage[] = []): Promise<{ source: string originalSource?: string sourceMap?: { sourcemap: EncodedSourceMap } }> { const filePath = normalize(fileURLToPath(url)) - const transformResult = this.ctx.projects - .map(project => project.vitenode.fetchCache.get(filePath)?.result) - .filter(Boolean) - .shift() + + const transformResult = transformResults.get(filePath) const map = transformResult?.map const code = transformResult?.code @@ -277,3 +279,18 @@ function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) { return Math.max(previous, maxEndOffset) }, 0) } + +function normalizeTransformResults(fetchCaches: ViteNodeServer['fetchCache'][]) { + const normalized: TransformResults = new Map() + + for (const fetchCache of fetchCaches) { + for (const [key, value] of fetchCache.entries()) { + const cleanEntry = cleanUrl(key) + + if (!normalized.has(cleanEntry)) + normalized.set(cleanEntry, value.result) + } + } + + return normalized +}