Skip to content

Commit 1e9f80e

Browse files
perf(vm): don't prewarm modules the worker never requests (#11033)
Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>
1 parent c3ba16b commit 1e9f80e

7 files changed

Lines changed: 248 additions & 55 deletions

File tree

packages/mocker/src/node/hoistMocks.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import type {
2+
ArrowFunctionExpression,
23
AwaitExpression,
34
CallExpression,
45
ExportDefaultDeclaration,
56
ExportNamedDeclaration,
67
Expression,
8+
FunctionExpression,
79
Identifier,
810
ImportDeclaration,
11+
SpreadElement,
912
VariableDeclaration,
1013
} from 'estree'
1114
import type { Rollup } from 'vite'
@@ -16,7 +19,16 @@ import MagicString from 'magic-string'
1619
import { relative } from 'pathe'
1720
import { esmWalker } from './esmWalker'
1821

22+
export interface StaticMockCall {
23+
method: string
24+
specifier: string
25+
hasFactory: boolean
26+
/** the factory uses `importOriginal`/`importActual` */
27+
factoryLoadsOriginal: boolean
28+
}
29+
1930
export interface HoistMocksOptions {
31+
onStaticMock?: (call: StaticMockCall) => void
2032
/**
2133
* List of modules that should always be imported before compiler hints.
2234
* @default 'vitest'
@@ -340,6 +352,22 @@ export function hoistMocks(
340352
`Cannot export the result of "${method}". Remove export declaration because "${method}" doesn\'t return anything.`,
341353
)
342354
}
355+
if (options.onStaticMock) {
356+
const specifier = getStaticSpecifier(node.arguments[0])
357+
if (specifier != null) {
358+
// anything but an inline function may still load the original
359+
const factory = node.arguments[1]?.type === 'ArrowFunctionExpression' || node.arguments[1]?.type === 'FunctionExpression'
360+
? node.arguments[1] as Positioned<ArrowFunctionExpression | FunctionExpression>
361+
: undefined
362+
options.onStaticMock({
363+
method: methodName,
364+
specifier,
365+
hasFactory: factory != null,
366+
factoryLoadsOriginal: factory != null
367+
&& (factory.params.length > 0 || code.slice(factory.start, factory.end).includes('importActual')),
368+
})
369+
}
370+
}
343371
// rewrite vi.mock(import('..')) into vi.mock('..')
344372
if (
345373
node.type === 'CallExpression'
@@ -610,3 +638,18 @@ function createIndexLocationsMap(source: string): Map<number, { line: number; co
610638
}
611639
return map
612640
}
641+
642+
function getStaticSpecifier(node: Expression | SpreadElement | undefined): string | undefined {
643+
if (node?.type === 'AwaitExpression') {
644+
node = node.argument
645+
}
646+
if (node?.type === 'ImportExpression') {
647+
node = node.source
648+
}
649+
if (node?.type === 'Literal' && typeof node.value === 'string') {
650+
return node.value
651+
}
652+
if (node?.type === 'TemplateLiteral' && node.expressions.length === 0) {
653+
return node.quasis[0].value.cooked ?? undefined
654+
}
655+
}

packages/mocker/src/node/hoistMocksPlugin.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { SourceMap } from 'magic-string'
22
import type { Plugin, Rollup } from 'vite'
3-
import type { HoistMocksOptions } from './hoistMocks'
3+
import type { HoistMocksOptions, StaticMockCall } from './hoistMocks'
44
import { createFilter } from 'vite'
55
import { cleanUrl } from '../utils'
66
import { hoistMocks } from './hoistMocks'
@@ -46,6 +46,7 @@ export function hoistMocksPlugin(options: HoistMocksPluginOptions = {}): Plugin
4646
if (!filter(id)) {
4747
return
4848
}
49+
const staticMocks: StaticMockCall[] = []
4950
const s = hoistMocks(code, id, this.parse, {
5051
regexpHoistable,
5152
hoistableMockMethodNames,
@@ -55,12 +56,19 @@ export function hoistMocksPlugin(options: HoistMocksPluginOptions = {}): Plugin
5556
root,
5657
getMap: () => this.getCombinedSourcemap(),
5758
...options,
59+
onStaticMock(call) {
60+
staticMocks.push(call)
61+
options.onStaticMock?.(call)
62+
},
5863
})
59-
if (s) {
60-
return {
61-
code: s.toString(),
62-
map: s.generateMap({ hires: 'boundary', source: cleanUrl(id) }),
63-
}
64+
// vite keeps `meta` across re-transforms, so always reset it
65+
if (!s) {
66+
return { meta: { vitestStaticMocks: null } }
67+
}
68+
return {
69+
code: s.toString(),
70+
map: s.generateMap({ hires: 'boundary', source: cleanUrl(id) }),
71+
meta: { vitestStaticMocks: staticMocks },
6472
}
6573
},
6674
}

packages/mocker/src/node/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export { automockModule } from './automock'
33
export type { AutomockPluginOptions } from './automockPlugin'
44
export { automockPlugin } from './automockPlugin'
55
export { dynamicImportPlugin } from './dynamicImportPlugin'
6+
export type { HoistMocksOptions, StaticMockCall } from './hoistMocks'
67
export { hoistMockAndResolve as hoistMocks, hoistMocksPlugin } from './hoistMocksPlugin'
78
export type { HoistMocksPluginOptions, HoistMocksResult } from './hoistMocksPlugin'
89
export { interceptorPlugin } from './interceptorPlugin'

packages/vitest/src/node/cache/fsModuleCache.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { DevEnvironment } from 'vite'
1+
import type { StaticMockCall } from '@vitest/mocker/node'
2+
import type { DevEnvironment, TransformResult } from 'vite'
23
import type { ModuleType, VitestFetchResult } from '../../types/general'
34
import type { Vitest } from '../core'
45
import type { ResolvedConfig } from '../types/config'
@@ -38,7 +39,7 @@ export class FileSystemModuleCache {
3839
private rootCache: string
3940
private metadataFilePath: string
4041

41-
private version = '1.0.0-beta.6'
42+
private version = '1.0.0-beta.7'
4243
private fsCacheRoots = new WeakMap<ResolvedConfig, string>()
4344
private fsEnvironmentHashMap = new WeakMap<DevEnvironment, string>()
4445
private fsCacheKeyGenerators = new Set<CacheKeyIdGenerator>()
@@ -136,12 +137,16 @@ export class FileSystemModuleCache {
136137
importedUrls: meta.importedUrls,
137138
mappings: meta.mappings,
138139
moduleType: meta.moduleType,
140+
deps: meta.deps,
141+
dynamicDeps: meta.dynamicDeps,
142+
staticMocks: meta.staticMocks,
139143
}
140144
}
141145

142146
async saveCachedModule(
143147
cachedFilePath: string,
144148
fetchResult: VitestFetchResult,
149+
transformResult: TransformResult | null,
145150
importedUrls: string[] = [],
146151
mappings: boolean = false,
147152
): Promise<void> {
@@ -153,6 +158,9 @@ export class FileSystemModuleCache {
153158
importedUrls,
154159
mappings,
155160
moduleType: fetchResult.moduleType,
161+
deps: transformResult?.deps,
162+
dynamicDeps: transformResult?.dynamicDeps,
163+
staticMocks: transformResult?.__vitestStaticMocks,
156164
} satisfies Omit<CachedInlineModuleMeta, 'code'>
157165
debugFs?.(`${c.yellow('[write]')} ${fetchResult.id} is cached in ${cachedFilePath}`)
158166
await atomicWriteFile(cachedFilePath, `${fetchResult.code}${cacheComment}${this.toBase64(result)}`)
@@ -406,6 +414,9 @@ export interface CachedInlineModuleMeta {
406414
mappings: boolean
407415
importedUrls: string[]
408416
moduleType?: ModuleType
417+
deps?: string[]
418+
dynamicDeps?: string[]
419+
staticMocks?: StaticMockCall[] | null
409420
}
410421

411422
/**

packages/vitest/src/node/environments/fetchModule.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Span } from '@opentelemetry/api'
2+
import type { StaticMockCall } from '@vitest/mocker/node'
23
import type { DevEnvironment, EnvironmentModuleNode, Rollup, TransformResult } from 'vite'
34
import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner'
45
import type { FetchCachedFileSystemResult, ModuleType, VitestFetchResult } from '../../types/general'
@@ -122,7 +123,7 @@ class ModuleFetcher {
122123
}
123124

124125
const tmpFile = join(tmpDir, hash('sha1', result.id, 'hex'))
125-
return this.cacheResult(result, tmpFile).then((result) => {
126+
return this.cacheResult(result, tmpFile, transformResult).then((result) => {
126127
if (transformResult) {
127128
transformResult.__vitestTmp = tmpFile
128129
}
@@ -148,7 +149,13 @@ class ModuleFetcher {
148149
const map = moduleGraphModule.transformResult?.map
149150
const mappings = map && !('version' in map) && map.mappings === ''
150151

151-
const cachedResult = await this.cacheResult(result, cachePath, importedUrls, !!mappings)
152+
const cachedResult = await this.cacheResult(
153+
result,
154+
cachePath,
155+
moduleGraphModule.transformResult,
156+
importedUrls,
157+
!!mappings,
158+
)
152159
// remember where the code is stored on disk so that repeat fetches and the
153160
// `fetchWarmModules` snapshot can point at it in this session already, not
154161
// only after the cache is read back in the next one
@@ -287,8 +294,11 @@ class ModuleFetcher {
287294
code: cachedModule.code,
288295
map,
289296
ssr: true,
297+
deps: cachedModule.deps,
298+
dynamicDeps: cachedModule.dynamicDeps,
290299
__vitestTmp: cachePath,
291300
__vitestModuleType: moduleType,
301+
__vitestStaticMocks: cachedModule.staticMocks,
292302
}
293303

294304
// we populate the module graph to make the watch mode work because it relies on importers
@@ -339,6 +349,10 @@ class ModuleFetcher {
339349
if ('code' in result) {
340350
result.moduleType = await this.cachedModuleType(result.file, result.code, moduleGraphModule.transformResult)
341351
}
352+
const transformResult = moduleGraphModule.transformResult
353+
if (transformResult && moduleGraphModule.id) {
354+
transformResult.__vitestStaticMocks ??= environment.pluginContainer.getModuleInfo(moduleGraphModule.id)?.meta?.vitestStaticMocks ?? null
355+
}
342356
return result
343357
}
344358

@@ -374,6 +388,7 @@ class ModuleFetcher {
374388
private async cacheResult(
375389
result: FetchResult,
376390
cachePath: string,
391+
transformResult: TransformResult | null,
377392
importedUrls: string[] = [],
378393
mappings = false,
379394
): Promise<FetchResult | FetchCachedFileSystemResult> {
@@ -386,7 +401,7 @@ class ModuleFetcher {
386401
}
387402

388403
const savePromise = this.fsCache
389-
.saveCachedModule(cachePath, result, importedUrls, mappings)
404+
.saveCachedModule(cachePath, result, transformResult, importedUrls, mappings)
390405
.then(() => returnResult)
391406
.catch((error) => {
392407
debugFs?.(`failed to cache ${cachePath}, serving it inline: ${error}`)
@@ -588,5 +603,7 @@ declare module 'vite' {
588603
// `experimental.fsModuleCache` store or the forks pool's tmp copies
589604
__vitestTmp?: string
590605
__vitestModuleType?: ModuleType
606+
// set by the hoistMocks plugin; null when the file was not hoisted
607+
__vitestStaticMocks?: StaticMockCall[] | null
591608
}
592609
}

0 commit comments

Comments
 (0)