Skip to content

vmThreads/vmForks pool crashes with ERR_MODULE_NOT_FOUND when deps.optimizer.client.enabled is true #9899

@csvan

Description

@csvan

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:

vitest 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:

  1. The VM executor strips query strings before filesystem resolution (matching how the Vite dev server handles them), or
  2. Vitest's internal modules are excluded from the optimizer's module graph so they are never given a ?v= suffix, or
  3. 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.

Metadata

Metadata

Assignees

Labels

p3-minor-bugAn edge case that only affects very specific usage (priority)

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions