···25252626const isWindows = process.platform === 'win32'
27272828+// Compiled scripts of inlined modules, shared across vm contexts: vm pools
2929+// evaluate every module again in each fresh context, but the compiled script
3030+// holds no per-context state (Vite rewrites dynamic imports to
3131+// `__vite_ssr_dynamic_import__`, so no per-context import callback is baked
3232+// in) — only its evaluation has to happen per context. Keyed by module id
3333+// (`mock:` ids stay distinct from their originals).
3434+const vmInlineScriptCache = new Map<string, vm.Script>()
3535+3636+function getVmInlineScript(
3737+ id: string,
3838+ wrappedCode: string,
3939+ options: vm.ScriptOptions,
4040+): vm.Script {
4141+ let script = vmInlineScriptCache.get(id)
4242+ if (!script) {
4343+ script = new vm.Script(wrappedCode, options)
4444+ vmInlineScriptCache.set(id, script)
4545+ }
4646+ return script
4747+}
4848+2849export interface VitestModuleEvaluatorOptions {
2950 evaluatedModules?: VitestEvaluatedModules
3051 metaEnv?: ModuleRunnerImportMeta['env']
···391412392413 try {
393414 const initModule = this.vm
394394- ? vm.runInContext(wrappedCode, this.vm.context, options)
415415+ ? getVmInlineScript(module.id, wrappedCode, options).runInContext(this.vm.context)
395416 : vm.runInThisContext(wrappedCode, options)
396417397418 await initModule(...argumentsValues)
+52
packages/vitest/src/runtime/vm/code-cache.ts
···11+interface CodeCacheEntry {
22+ source: string
33+ data: Buffer | undefined
44+}
55+66+/**
77+ * Worker-wide cache of V8 code cache buffers for externalized modules.
88+ *
99+ * vm pools create a fresh executor per test file, so every externalized
1010+ * module is compiled and evaluated again in each fresh context. The compiled
1111+ * code has no per-context state — reusing its V8 code cache skips the
1212+ * re-parse/re-compile while the evaluation still happens per context.
1313+ *
1414+ * Entries are keyed by the module identifier and guarded by the exact source
1515+ * text, so an invalidated module that produces different code simply replaces
1616+ * its entry.
1717+ */
1818+export class CodeCache {
1919+ private entries = new Map<string, CodeCacheEntry>()
2020+2121+ get(identifier: string, source: string): Buffer | undefined {
2222+ const entry = this.entries.get(identifier)
2323+ if (entry && entry.source === source) {
2424+ return entry.data
2525+ }
2626+ return undefined
2727+ }
2828+2929+ /**
3030+ * Stores the code cache produced by `produce` unless an entry for the same
3131+ * source already exists. A `produce` failure is recorded as an empty entry,
3232+ * so it is not retried on every fresh context.
3333+ */
3434+ store(identifier: string, source: string, produce: () => Buffer): void {
3535+ const entry = this.entries.get(identifier)
3636+ if (entry && entry.source === source) {
3737+ return
3838+ }
3939+ let data: Buffer | undefined
4040+ try {
4141+ data = produce()
4242+ }
4343+ catch {
4444+ data = undefined
4545+ }
4646+ this.entries.set(identifier, { source, data })
4747+ }
4848+4949+ delete(identifier: string): void {
5050+ this.entries.delete(identifier)
5151+ }
5252+}
···11+import type { CodeCache } from './code-cache'
12import type { FileMap } from './file-map'
23import type { ImportModuleDynamically, VMSyntheticModule } from './types'
34import { Module as _Module, createRequire, isBuiltin } from 'node:module'
···7889interface CommonjsExecutorOptions {
910 fileMap: FileMap
1111+ codeCache?: CodeCache
1012 interopDefault?: boolean
1113 context: vm.Context
1214 importModuleDynamically: ImportModuleDynamically
···3335 > = Object.create(null)
34363537 private fs: FileMap
3838+ private codeCache: CodeCache | undefined
3639 private Module: typeof _Module
3740 private interopDefault: boolean | undefined
38413942 constructor(options: CommonjsExecutorOptions) {
4043 this.context = options.context
4144 this.fs = options.fileMap
4545+ this.codeCache = options.codeCache
4246 this.interopDefault = options.interopDefault
43474448 const primitives = vm.runInContext(
···109113110114 _compile(code: string, filename: string) {
111115 const cjsModule = Module.wrap(code)
116116+ const codeCache = executor.codeCache
117117+ const cachedData = codeCache?.get(filename, cjsModule)
112118 const script = new vm.Script(cjsModule, {
113119 filename,
120120+ cachedData,
114121 importModuleDynamically: options.importModuleDynamically,
115122 } as any)
123123+ if (cachedData && script.cachedDataRejected) {
124124+ codeCache!.delete(filename)
125125+ }
116126 // @ts-expect-error mark script with current identifier
117127 script.identifier = filename
118128 const fn = script.runInContext(executor.context)
···124134 }
125135 finally {
126136 this.loaded = true
137137+ // store after execution so the code cache carries the compiled
138138+ // module body, not only the lazily-parsed wrapper
139139+ codeCache?.store(filename, cjsModule, () => script.createCachedData())
127140 }
128141 }
129142
+7
packages/vitest/src/runtime/vm/esm-executor.ts
···7373 this.moduleCache.set(fileURL, m)
7474 return m
7575 }
7676+ const codeCache = this.executor.codeCache
7777+ const cachedData = codeCache?.get(fileURL, code)
7678 const m = new SourceTextModule(code, {
7779 identifier: fileURL,
7880 context: this.context,
8181+ cachedData,
7982 importModuleDynamically: this.executor.importModuleDynamically,
8083 initializeImportMeta: (meta, mod) => {
8184 meta.url = mod.identifier
···9295 }
9396 },
9497 })
9898+ // the code cache of a SourceTextModule must be created before evaluation
9999+ if (!cachedData) {
100100+ codeCache?.store(fileURL, code, () => m.createCachedData())
101101+ }
95102 this.moduleCache.set(fileURL, m)
96103 return m
97104 }
+6
packages/vitest/src/runtime/vm/types.ts
···8787 * @param code JavaScript Module code to parse
8888 */
8989 constructor(code: string, options?: SourceTextModuleOptions)
9090+ /**
9191+ * Creates a code cache that can be used with the `SourceTextModule`
9292+ * constructor's `cachedData` option. Must be called before the module
9393+ * has been evaluated.
9494+ */
9595+ createCachedData(): Buffer
9096}
+19
packages/vitest/src/runtime/workers/vm.ts
···11import type { Context } from 'node:vm'
22import type { WorkerGlobalState, WorkerSetupContext } from '../../types/worker'
33import type { Traces } from '../../utils/traces'
44+import type { ModuleInformation } from '../external-executor'
45import { pathToFileURL } from 'node:url'
56import { isContext, runInContext } from 'node:vm'
67import { resolve } from 'pathe'
···1516import { startVitestModuleRunner, VITEST_VM_CONTEXT_SYMBOL } from '../moduleRunner/startVitestModuleRunner'
1617import { setupEnv } from '../setup-common'
1718import { provideWorkerState } from '../utils'
1919+import { CodeCache } from '../vm/code-cache'
1820import { FileMap } from '../vm/file-map'
19212022const entryFile = pathToFileURL(resolve(distDir, 'workers/runVmTests.js')).href
21232224const fileMap = new FileMap()
2325const packageCache = new Map<string, string>()
2626+const codeCache = new CodeCache()
2727+const resolveCache = new Map<string, string>()
2828+const moduleInfoCache = new Map<string, ModuleInformation>()
24292530export async function runVmTests(method: 'run' | 'collect', state: WorkerGlobalState, traces: Traces): Promise<void> {
2631 const { ctx, rpc } = state
···2833 const beforeEnvironmentTime = performance.now()
2934 const { environment } = await loadEnvironment(ctx.environment.name, ctx.config.root, rpc, traces, true)
3035 state.environment = environment
3636+3737+ // let the server transform this file's import graph while this worker is
3838+ // busy importing the environment package (jsdom takes ~0.5s per worker) —
3939+ // the server is otherwise idle during that window on a cold start. The
4040+ // transforms also land in the `fetchWarmModules` snapshot, so the worker's
4141+ // own fetches short-circuit to disk reads. Failures are ignored: the
4242+ // worker's own fetch reports them with the proper import context.
4343+ rpc.prewarmModuleGraph(
4444+ environment.viteEnvironment || environment.name,
4545+ ctx.files.map(file => file.filepath),
4646+ ).catch(() => {})
31473248 if (!environment.setupVM) {
3349 const envName = ctx.environment.name
···87103 const externalModulesExecutor = new ExternalModulesExecutor({
88104 context,
89105 fileMap,
106106+ codeCache,
107107+ resolveCache,
108108+ moduleInfoCache,
90109 packageCache,
91110 transform: rpc.transform,
92111 viteClientModule: stubs['/@vite/client'],
+7
packages/vitest/src/types/rpc.ts
···1919 * paying a `fetch` round-trip per module.
2020 */
2121 fetchWarmModules: (environment: string, files: string[]) => Promise<Record<string, FetchResult | FetchCachedFileSystemResult>>
2222+ /**
2323+ * Transforms the import graphs of the given test files ahead of the
2424+ * worker's own fetches. Fired by vm pool workers before their environment
2525+ * setup, so the server transforms modules while the worker is busy
2626+ * importing its environment package.
2727+ */
2828+ prewarmModuleGraph: (environment: string, files: string[]) => Promise<void>
2229 transform: (id: string) => Promise<{ code?: string }>
23302431 onUserConsoleLog: (log: UserConsoleLog) => void
+54
test/e2e/test/vm-threads.test.ts
···1616 expect(exitCode).toBe(0)
1717})
18181919+// compiled scripts of inlined modules are shared between vm contexts within
2020+// a worker — module state must still be re-evaluated per test file. With 4
2121+// files on 2 workers, at least one worker runs several files, so a leak of
2222+// evaluated state through the shared script would fail the second file.
2323+test.for(['vmThreads', 'vmForks'] as const)(
2424+ '%s re-evaluates inlined modules in every context',
2525+ async (pool) => {
2626+ const testFile = `
2727+ import { expect, test } from 'vitest'
2828+ import { increment } from './counter.js'
2929+3030+ test('module state is fresh for this file', () => {
3131+ expect(increment()).toBe(1)
3232+ })
3333+ `
3434+ const { stderr, exitCode } = await runInlineTests({
3535+ 'counter.js': `
3636+ let count = 0
3737+ export function increment() {
3838+ return ++count
3939+ }
4040+ `,
4141+ 'a.test.js': testFile,
4242+ 'b.test.js': testFile,
4343+ 'c.test.js': testFile,
4444+ 'd.test.js': testFile,
4545+ }, {
4646+ pool,
4747+ maxWorkers: 2,
4848+ })
4949+5050+ expect(stderr).toBe('')
5151+ expect(exitCode).toBe(0)
5252+ },
5353+)
5454+1955// vm pools resolve `isolate` to false (isolation comes from a fresh VM
2056// context per run request), which used to trigger the "single non-isolated
2157// worker receives all files at once" batching with `maxWorkers: 1` — all
···4581 expect(exitCode).toBe(0)
4682 },
4783)
8484+8585+// the graph prewarm triggered by vm workers swallows its own transform
8686+// errors — the worker's fetch must still report them with the import context
8787+test('vm pools report errors from modules covered by the graph prewarm', async () => {
8888+ const { stderr, exitCode } = await runInlineTests({
8989+ 'a.test.js': `
9090+ import './does-not-exist.js'
9191+ import { test } from 'vitest'
9292+9393+ test('never runs', () => {})
9494+ `,
9595+ }, {
9696+ pool: 'vmThreads',
9797+ })
9898+9999+ expect(exitCode).toBe(1)
100100+ expect(stderr).toContain('does-not-exist.js')
101101+})
4810249103// The module-sync condition was added in Node 22.12/20.19 when require(esm)
50104// was unflagged. The fix uses the _resolveFilename conditions option which
+63
test/unit/test/vm-code-cache.test.ts
···11+import { expect, test, vi } from 'vitest'
22+import { CodeCache } from 'vitest/src/runtime/vm/code-cache.js'
33+44+test('returns the stored data only for the exact same source', () => {
55+ const cache = new CodeCache()
66+ const data = Buffer.from('cached')
77+88+ expect(cache.get('/mod.js', 'source')).toBeUndefined()
99+1010+ cache.store('/mod.js', 'source', () => data)
1111+1212+ expect(cache.get('/mod.js', 'source')).toBe(data)
1313+ expect(cache.get('/mod.js', 'changed source')).toBeUndefined()
1414+ expect(cache.get('/other.js', 'source')).toBeUndefined()
1515+})
1616+1717+test('does not produce again for the same source', () => {
1818+ const cache = new CodeCache()
1919+ const produce = vi.fn(() => Buffer.from('cached'))
2020+2121+ cache.store('/mod.js', 'source', produce)
2222+ cache.store('/mod.js', 'source', produce)
2323+2424+ expect(produce).toHaveBeenCalledTimes(1)
2525+})
2626+2727+test('replaces the entry when the source changes', () => {
2828+ const cache = new CodeCache()
2929+ const first = Buffer.from('first')
3030+ const second = Buffer.from('second')
3131+3232+ cache.store('/mod.js', 'source', () => first)
3333+ cache.store('/mod.js', 'changed source', () => second)
3434+3535+ expect(cache.get('/mod.js', 'source')).toBeUndefined()
3636+ expect(cache.get('/mod.js', 'changed source')).toBe(second)
3737+})
3838+3939+test('records a failed produce and does not retry it', () => {
4040+ const cache = new CodeCache()
4141+ const produce = vi.fn<() => Buffer>(() => {
4242+ throw new Error('cannot create cached data')
4343+ })
4444+4545+ cache.store('/mod.js', 'source', produce)
4646+ cache.store('/mod.js', 'source', produce)
4747+4848+ expect(produce).toHaveBeenCalledTimes(1)
4949+ expect(cache.get('/mod.js', 'source')).toBeUndefined()
5050+})
5151+5252+test('delete removes the entry', () => {
5353+ const cache = new CodeCache()
5454+ const produce = vi.fn(() => Buffer.from('cached'))
5555+5656+ cache.store('/mod.js', 'source', produce)
5757+ cache.delete('/mod.js')
5858+5959+ expect(cache.get('/mod.js', 'source')).toBeUndefined()
6060+6161+ cache.store('/mod.js', 'source', produce)
6262+ expect(produce).toHaveBeenCalledTimes(2)
6363+})