@@ -5,7 +5,7 @@ import v8ToIstanbul from 'v8-to-istanbul'
55import { mergeProcessCovs } from '@bcoe/v8-coverage'
66import libReport from 'istanbul-lib-report'
77import reports from 'istanbul-reports'
8- import type { CoverageMap } from 'istanbul-lib-coverage'
8+ import type { CoverageMap , CoverageMapData } from 'istanbul-lib-coverage'
99import libCoverage from 'istanbul-lib-coverage'
1010import libSourceMaps from 'istanbul-lib-source-maps'
1111import MagicString from 'magic-string'
@@ -39,20 +39,24 @@ interface TestExclude {
3939
4040type Options = ResolvedCoverageOptions < 'v8' >
4141type TransformResults = Map < string , FetchResult >
42+ type RawCoverage = Profiler . TakePreciseCoverageReturnType
43+ type CoverageByTransformMode = Record < AfterSuiteRunMeta [ 'transformMode' ] , RawCoverage [ ] >
44+ type ProjectName = NonNullable < AfterSuiteRunMeta [ 'projectName' ] > | typeof DEFAULT_PROJECT
4245
4346// TODO: vite-node should export this
4447const WRAPPER_LENGTH = 185
4548
4649// Note that this needs to match the line ending as well
4750const VITE_EXPORTS_LINE_PATTERN = / O b j e c t \. d e f i n e P r o p e r t y \( _ _ v i t e _ s s r _ e x p o r t s _ _ .* \n / g
51+ const DEFAULT_PROJECT = Symbol . for ( 'default-project' )
4852
4953export class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider {
5054 name = 'v8'
5155
5256 ctx ! : Vitest
5357 options ! : Options
5458 testExclude ! : InstanceType < TestExclude >
55- coverages : Profiler . TakePreciseCoverageReturnType [ ] = [ ]
59+ coverages = new Map < ProjectName , CoverageByTransformMode > ( )
5660
5761 initialize ( ctx : Vitest ) {
5862 const config : CoverageV8Options = ctx . config . coverage
@@ -92,54 +96,52 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
9296 if ( clean && existsSync ( this . options . reportsDirectory ) )
9397 await fs . rm ( this . options . reportsDirectory , { recursive : true , force : true , maxRetries : 10 } )
9498
95- this . coverages = [ ]
99+ this . coverages = new Map ( )
96100 }
97101
98- onAfterSuiteRun ( { coverage } : AfterSuiteRunMeta ) {
99- this . coverages . push ( coverage as Profiler . TakePreciseCoverageReturnType )
102+ /*
103+ * Coverage and meta information passed from Vitest runners.
104+ * Note that adding new entries here and requiring on those without
105+ * backwards compatibility is a breaking change.
106+ */
107+ onAfterSuiteRun ( { coverage, transformMode, projectName } : AfterSuiteRunMeta ) {
108+ if ( transformMode !== 'web' && transformMode !== 'ssr' )
109+ throw new Error ( `Invalid transform mode: ${ transformMode } ` )
110+
111+ let entry = this . coverages . get ( projectName || DEFAULT_PROJECT )
112+
113+ if ( ! entry ) {
114+ entry = { web : [ ] , ssr : [ ] }
115+ this . coverages . set ( projectName || DEFAULT_PROJECT , entry )
116+ }
117+
118+ entry [ transformMode ] . push ( coverage as RawCoverage )
100119 }
101120
102121 async reportCoverage ( { allTestsRun } : ReportContext = { } ) {
103122 if ( provider === 'stackblitz' )
104123 this . ctx . logger . log ( c . blue ( ' % ' ) + c . yellow ( '@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.' ) )
105124
106- const transformResults = normalizeTransformResults ( this . ctx . projects . map ( project => project . vitenode . fetchCache ) )
107- const merged = mergeProcessCovs ( this . coverages )
108- const scriptCoverages = merged . result . filter ( result => this . testExclude . shouldInstrument ( fileURLToPath ( result . url ) ) )
125+ const coverageMaps = await Promise . all (
126+ Array . from ( this . coverages . entries ( ) ) . map ( ( [ projectName , coverages ] ) => [
127+ this . mergeAndTransformCoverage ( coverages . ssr , projectName , 'ssr' ) ,
128+ this . mergeAndTransformCoverage ( coverages . web , projectName , 'web' ) ,
129+ ] ) . flat ( ) ,
130+ )
109131
110132 if ( this . options . all && allTestsRun ) {
111- const coveredFiles = Array . from ( scriptCoverages . map ( r => r . url ) )
112- const untestedFiles = await this . getUntestedFiles ( coveredFiles , transformResults )
133+ const coveredFiles = coverageMaps . map ( map => map . files ( ) ) . flat ( )
134+ const untestedCoverage = await this . getUntestedFiles ( coveredFiles )
135+ const untestedCoverageResults = untestedCoverage . map ( files => ( { result : [ files ] } ) )
113136
114- scriptCoverages . push ( ... untestedFiles )
137+ coverageMaps . push ( await this . mergeAndTransformCoverage ( untestedCoverageResults ) )
115138 }
116139
117- const converted = await Promise . all ( scriptCoverages . map ( async ( { url, functions } ) => {
118- const sources = await this . getSources ( url , transformResults , functions )
119-
120- // If no source map was found from vite-node we can assume this file was not run in the wrapper
121- const wrapperLength = sources . sourceMap ? WRAPPER_LENGTH : 0
122-
123- const converter = v8ToIstanbul ( url , wrapperLength , sources )
124- await converter . load ( )
125-
126- converter . applyCoverage ( functions )
127- return converter . toIstanbul ( )
128- } ) )
129-
130- const mergedCoverage = converted . reduce ( ( coverage , previousCoverageMap ) => {
131- const map = libCoverage . createCoverageMap ( coverage )
132- map . merge ( previousCoverageMap )
133- return map
134- } , libCoverage . createCoverageMap ( { } ) )
135-
136- const sourceMapStore = libSourceMaps . createSourceMapStore ( )
137- const coverageMap : CoverageMap = await sourceMapStore . transformCoverage ( mergedCoverage )
140+ const coverageMap = mergeCoverageMaps ( ...coverageMaps )
138141
139142 const context = libReport . createContext ( {
140143 dir : this . options . reportsDirectory ,
141144 coverageMap,
142- sourceFinder : sourceMapStore . sourceFinder ,
143145 watermarks : this . options . watermarks ,
144146 } )
145147
@@ -185,11 +187,13 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
185187 }
186188 }
187189
188- private async getUntestedFiles ( testedFiles : string [ ] , transformResults : TransformResults ) : Promise < Profiler . ScriptCoverage [ ] > {
190+ private async getUntestedFiles ( testedFiles : string [ ] ) : Promise < Profiler . ScriptCoverage [ ] > {
191+ const transformResults = normalizeTransformResults ( this . ctx . vitenode . fetchCache )
192+
189193 const includedFiles = await this . testExclude . glob ( this . ctx . config . root )
190194 const uncoveredFiles = includedFiles
191195 . map ( file => pathToFileURL ( resolve ( this . ctx . config . root , file ) ) )
192- . filter ( file => ! testedFiles . includes ( file . href ) )
196+ . filter ( file => ! testedFiles . includes ( file . pathname ) )
193197
194198 return await Promise . all ( uncoveredFiles . map ( async ( uncoveredFile ) => {
195199 const { source } = await this . getSources ( uncoveredFile . href , transformResults )
@@ -247,6 +251,41 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
247251 } ,
248252 }
249253 }
254+
255+ private async mergeAndTransformCoverage ( coverages : RawCoverage [ ] , projectName ?: ProjectName , transformMode ?: 'web' | 'ssr' ) {
256+ const viteNode = this . ctx . projects . find ( project => project . getName ( ) === projectName ) ?. vitenode || this . ctx . vitenode
257+ const fetchCache = transformMode ? viteNode . fetchCaches [ transformMode ] : viteNode . fetchCache
258+ const transformResults = normalizeTransformResults ( fetchCache )
259+
260+ const merged = mergeProcessCovs ( coverages )
261+ const scriptCoverages = merged . result . filter ( result => this . testExclude . shouldInstrument ( fileURLToPath ( result . url ) ) )
262+
263+ const converted = await Promise . all ( scriptCoverages . map ( async ( { url, functions } ) => {
264+ const sources = await this . getSources ( url , transformResults , functions )
265+
266+ // If no source map was found from vite-node we can assume this file was not run in the wrapper
267+ const wrapperLength = sources . sourceMap ? WRAPPER_LENGTH : 0
268+
269+ const converter = v8ToIstanbul ( url , wrapperLength , sources )
270+ await converter . load ( )
271+
272+ converter . applyCoverage ( functions )
273+ return converter . toIstanbul ( )
274+ } ) )
275+
276+ const mergedCoverage = mergeCoverageMaps ( ...converted )
277+
278+ const sourceMapStore = libSourceMaps . createSourceMapStore ( )
279+ return sourceMapStore . transformCoverage ( mergedCoverage )
280+ }
281+ }
282+
283+ function mergeCoverageMaps ( ...coverageMaps : ( CoverageMap | CoverageMapData ) [ ] ) {
284+ return coverageMaps . reduce < CoverageMap > ( ( coverage , previousCoverageMap ) => {
285+ const map = libCoverage . createCoverageMap ( coverage )
286+ map . merge ( previousCoverageMap )
287+ return map
288+ } , libCoverage . createCoverageMap ( { } ) )
250289}
251290
252291/**
@@ -284,16 +323,14 @@ function findLongestFunctionLength(functions: Profiler.FunctionCoverage[]) {
284323 } , 0 )
285324}
286325
287- function normalizeTransformResults ( fetchCaches : Map < string , { result : FetchResult } > [ ] ) {
326+ function normalizeTransformResults ( fetchCache : Map < string , { result : FetchResult } > ) {
288327 const normalized : TransformResults = new Map ( )
289328
290- for ( const fetchCache of fetchCaches ) {
291- for ( const [ key , value ] of fetchCache . entries ( ) ) {
292- const cleanEntry = cleanUrl ( key )
329+ for ( const [ key , value ] of fetchCache . entries ( ) ) {
330+ const cleanEntry = cleanUrl ( key )
293331
294- if ( ! normalized . has ( cleanEntry ) )
295- normalized . set ( cleanEntry , value . result )
296- }
332+ if ( ! normalized . has ( cleanEntry ) )
333+ normalized . set ( cleanEntry , value . result )
297334 }
298335
299336 return normalized
0 commit comments