Description
When pool: 'vmThreads' (or pool: 'vmForks') is combined with deps.optimizer.client.enabled: true, every test worker crashes immediately with:
Error: Cannot find module '/path/to/node_modules/vitest/dist/spy.js?v=d0e9b0ac'
Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }
The ?v=<hash> suffix is appended by the Vite optimizer as a cache-buster for pre-bundled modules. Node's VM module executor (used by both VM pool types) resolves modules via the filesystem and does not strip query strings from paths, so the file is never found.
The two features are therefore mutually exclusive in their current form — enabling the optimizer to speed up cold-cache CI runs prevents using VM pools to speed up local/warm runs.
Reproduction
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'happy-dom',
pool: 'vmThreads', // also fails with 'vmForks'
deps: {
optimizer: {
client: {
enabled: true,
include: ['react', 'react-dom'],
},
},
},
},
});
// example.test.ts
import { expect, test } from 'vitest';
test('basic', () => { expect(1 + 1).toBe(2); });
Run:
Error output
Error: Cannot find module '/absolute/path/node_modules/vitest/dist/spy.js?v=d0e9b0ac'
at ExternalModulesExecutor.createModule (vitest/dist/chunks/vm.D3epNOPZ.js:621:34)
at ExternalModulesExecutor.import (vitest/dist/chunks/vm.D3epNOPZ.js:553:29)
Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }
The hash suffix (?v=d0e9b0ac) is non-deterministic — it changes per run.
Root cause analysis
The Vite optimizer appends ?v=<hash> to pre-bundled module specifiers as a cache-invalidation mechanism (see Vite source: packages/vite/src/node/optimizer/index.ts). When vitest's VM executor (ExternalModulesExecutor) tries to load vitest's own internal modules (e.g. vitest/dist/spy.js) that were caught up in the optimizer's module graph, it receives the path with the query string appended. Node's vm.Module / createRequire resolves strictly against the filesystem — spy.js?v=d0e9b0ac does not exist as a file, so resolution fails.
The forks pool is unaffected because it uses standard Node.js require/import in a subprocess, which goes through Vite's dev server transform pipeline where the ?v= suffix is handled correctly.
Expected behaviour
Either:
- The VM executor strips query strings before filesystem resolution (matching how the Vite dev server handles them), or
- Vitest's internal modules are excluded from the optimizer's module graph so they are never given a
?v= suffix, or
- The combination is detected at startup and a clear error is thrown explaining the incompatibility
Environment
|
|
| vitest |
4.0.18 |
| vite |
7.3.1 |
| node |
24.12.0 |
| OS |
macOS |
| pool |
vmThreads / vmForks (both affected) |
| environment |
happy-dom (also reproducible with jsdom) |
Workaround
Use pool: 'forks' (the default) when deps.optimizer.client.enabled: true. The two features cannot be used together until this is resolved.
Description
When
pool: 'vmThreads'(orpool: 'vmForks') is combined withdeps.optimizer.client.enabled: true, every test worker crashes immediately with:The
?v=<hash>suffix is appended by the Vite optimizer as a cache-buster for pre-bundled modules. Node's VM module executor (used by both VM pool types) resolves modules via the filesystem and does not strip query strings from paths, so the file is never found.The two features are therefore mutually exclusive in their current form — enabling the optimizer to speed up cold-cache CI runs prevents using VM pools to speed up local/warm runs.
Reproduction
Run:
Error output
The hash suffix (
?v=d0e9b0ac) is non-deterministic — it changes per run.Root cause analysis
The Vite optimizer appends
?v=<hash>to pre-bundled module specifiers as a cache-invalidation mechanism (see Vite source:packages/vite/src/node/optimizer/index.ts). When vitest's VM executor (ExternalModulesExecutor) tries to load vitest's own internal modules (e.g.vitest/dist/spy.js) that were caught up in the optimizer's module graph, it receives the path with the query string appended. Node'svm.Module/createRequireresolves strictly against the filesystem —spy.js?v=d0e9b0acdoes not exist as a file, so resolution fails.The
forkspool is unaffected because it uses standard Node.jsrequire/importin a subprocess, which goes through Vite's dev server transform pipeline where the?v=suffix is handled correctly.Expected behaviour
Either:
?v=suffix, orEnvironment
Workaround
Use
pool: 'forks'(the default) whendeps.optimizer.client.enabled: true. The two features cannot be used together until this is resolved.