11import type { DevEnvironment , EnvironmentModuleNode , FetchResult } from 'vite'
2+ import type { FetchFunctionOptions } from 'vite/module-runner'
23import type { FetchCachedFileSystemResult } from '../../types/general'
34import type { RuntimeRPC } from '../../types/rpc'
5+ import type { OTELCarrier } from '../../utils/traces'
46import type { TestProject } from '../project'
57import type { ResolveSnapshotPathHandlerContext } from '../types/config'
68import { existsSync , mkdirSync } from 'node:fs'
@@ -41,6 +43,56 @@ export function createMethodsRPC(project: TestProject, methodsOptions: MethodsOp
4143 mkdirSync ( project . config . dumpDir , { recursive : true } )
4244 }
4345 project . vitest . state . metadata [ project . name ] . dumpDir = project . config . dumpDir
46+
47+ function getEnvironment ( environmentName : string ) : DevEnvironment {
48+ const environment = project . vite . environments [ environmentName ]
49+ if ( ! environment ) {
50+ throw new Error ( `The environment ${ environmentName } was not defined in the Vite config.` )
51+ }
52+ return environment
53+ }
54+
55+ async function fetchModule (
56+ url : string ,
57+ importer : string | undefined ,
58+ environment : DevEnvironment ,
59+ options ?: FetchFunctionOptions ,
60+ otelCarrier ?: OTELCarrier ,
61+ // per-module durations are only recorded for direct worker fetches: the
62+ // graph prewarm fetches whole levels concurrently, so its per-module wall
63+ // times measure the queue position, not the module's own transform cost
64+ accountModuleDuration = true ,
65+ ) : Promise < FetchResult | FetchCachedFileSystemResult > {
66+ const state = project . vitest . state
67+ const start = performance . now ( )
68+
69+ return await project . _fetcher ( url , importer , environment , cacheFs , options , otelCarrier ) . then ( ( result ) => {
70+ const metadata = state . metadata [ project . name ]
71+ if ( 'externalize' in result ) {
72+ metadata . externalized [ url ] = result . externalize
73+ // builtins and network urls are already resolved inside the worker
74+ // without a round-trip, only module externalizations are worth sharing
75+ if ( result . type === 'module' && url [ 0 ] === '/' ) {
76+ let externals = warmExternals . get ( environment )
77+ if ( ! externals ) {
78+ externals = Object . create ( null ) as Record < string , FetchResult >
79+ warmExternals . set ( environment , externals )
80+ }
81+ externals [ url ] = result
82+ }
83+ }
84+ if ( 'tmp' in result ) {
85+ metadata . tmps [ url ] = result . tmp
86+ }
87+ if ( accountModuleDuration ) {
88+ const duration = performance . now ( ) - start
89+ metadata . duration [ url ] ??= [ ]
90+ metadata . duration [ url ] . push ( duration )
91+ }
92+ return result
93+ } )
94+ }
95+
4496 return {
4597 async fetch (
4698 url ,
@@ -49,37 +101,7 @@ export function createMethodsRPC(project: TestProject, methodsOptions: MethodsOp
49101 options ,
50102 otelCarrier ,
51103 ) {
52- const environment = project . vite . environments [ environmentName ]
53- if ( ! environment ) {
54- throw new Error ( `The environment ${ environmentName } was not defined in the Vite config.` )
55- }
56-
57- const start = performance . now ( )
58-
59- return await project . _fetcher ( url , importer , environment , cacheFs , options , otelCarrier ) . then ( ( result ) => {
60- const duration = performance . now ( ) - start
61- project . vitest . state . transformTime += duration
62- const metadata = project . vitest . state . metadata [ project . name ]
63- if ( 'externalize' in result ) {
64- metadata . externalized [ url ] = result . externalize
65- // builtins and network urls are already resolved inside the worker
66- // without a round-trip, only module externalizations are worth sharing
67- if ( result . type === 'module' && url [ 0 ] === '/' ) {
68- let externals = warmExternals . get ( environment )
69- if ( ! externals ) {
70- externals = Object . create ( null ) as Record < string , FetchResult >
71- warmExternals . set ( environment , externals )
72- }
73- externals [ url ] = result
74- }
75- }
76- if ( 'tmp' in result ) {
77- metadata . tmps [ url ] = result . tmp
78- }
79- metadata . duration [ url ] ??= [ ]
80- metadata . duration [ url ] . push ( duration )
81- return result
82- } )
104+ return fetchModule ( url , importer , getEnvironment ( environmentName ) , options , otelCarrier )
83105 } ,
84106 async fetchWarmModules ( environmentName , files ) {
85107 const environment = project . vite . environments [ environmentName ]
@@ -150,6 +172,71 @@ export function createMethodsRPC(project: TestProject, methodsOptions: MethodsOp
150172
151173 return warm
152174 } ,
175+ async prewarmModuleGraph ( environmentName , files ) {
176+ const environment = getEnvironment ( environmentName )
177+ const moduleGraph = environment . moduleGraph
178+ const seen = new Set < string > ( )
179+
180+ async function walkNode ( node : EnvironmentModuleNode ) : Promise < void > {
181+ const children : Promise < void > [ ] = [ ]
182+ for ( const child of node . importedModules ) {
183+ if ( child . url == null || seen . has ( child . url ) ) {
184+ continue
185+ }
186+ if ( child . transformResult ) {
187+ seen . add ( child . url )
188+ children . push ( walkNode ( child ) )
189+ }
190+ else {
191+ children . push ( fetchNode ( child . url , node . id ?? undefined ) )
192+ }
193+ }
194+ if ( children . length ) {
195+ await Promise . all ( children )
196+ }
197+ }
198+
199+ async function fetchNode ( url : string , importer : string | undefined ) : Promise < void > {
200+ if ( seen . has ( url ) ) {
201+ return
202+ }
203+ seen . add ( url )
204+ try {
205+ await fetchModule ( url , importer , environment , undefined , undefined , false )
206+ }
207+ catch {
208+ // the worker's own fetch will surface the error with the proper
209+ // import context
210+ return
211+ }
212+ let node : EnvironmentModuleNode | undefined
213+ try {
214+ node = await moduleGraph . getModuleByUrl ( url ) ?? moduleGraph . getModuleById ( url ) ?? undefined
215+ }
216+ catch {
217+ node = moduleGraph . getModuleById ( url ) ?? undefined
218+ }
219+ if ( node ) {
220+ await walkNode ( node )
221+ }
222+ }
223+
224+ await Promise . all ( [ ...files , ...project . config . setupFiles ] . map ( async ( file ) => {
225+ const nodes = moduleGraph . getModulesByFile ( file )
226+ if ( nodes && nodes . size ) {
227+ await Promise . all ( Array . from ( nodes , ( node ) => {
228+ if ( node . transformResult ) {
229+ seen . add ( node . url )
230+ return walkNode ( node )
231+ }
232+ return fetchNode ( node . url , undefined )
233+ } ) )
234+ }
235+ else {
236+ await fetchNode ( file , undefined )
237+ }
238+ } ) )
239+ } ,
153240 async resolve ( id , importer , environmentName ) {
154241 const environment = project . vite . environments [ environmentName ]
155242 if ( ! environment ) {
0 commit comments