Skip to content

vmThreads pool: custom require() matches module-sync condition but cannot load ESM #9650

@lesleh

Description

@lesleh

Describe the bug

When using the vmThreads pool, Vitest's host-thread require() matches the module-sync exports condition but then fails to load the resulting .mjs file, throwing Cannot use import statement outside a module.

This is related to but distinct from #7692 — that issue covers Vite's ESM resolver not picking up module-sync. This issue is about vmThreads' custom require() implementation, which has its own condition matching that bypasses Vite's resolve config entirely.

Reproduction

Any package using module-sync in its exports map will trigger this. The simplest case is async-function (v1.0.0), which has:

{
  "exports": {
    ".": [
      {
        "module-sync": "./require.mjs",
        "import": "./index.mjs",
        "default": "./index.js"
      },
      "./index.js"
    ]
  }
}

require.mjs is a valid ESM file designed for Node 22.12+'s native require() (which can load ESM synchronously). vmThreads' custom require() resolves to this file via the module-sync condition, then tries to parse it as CJS and fails.

vitest.config.ts:

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    pool: "vmThreads",
  },
});

Error:

SyntaxError: Cannot use import statement outside a module
 ❯ node_modules/async-function/require.mjs:1

The forks pool does not have this problem — it uses Node's native require() which handles module-sync correctly.

What's been tried

  • resolve.conditions: ["module-sync"] — No effect. This only controls Vite's ESM resolver, not vmThreads' host require().
  • poolOptions.vmThreads.execArgv: ["--experimental-require-module"] — No effect. The flag applies to Node's native module loader, but vmThreads builds its own require() using V8's vm API, bypassing it entirely.
  • Yarn patches converting require.mjs to CJS — Works, but is a workaround.

Expected behavior

vmThreads' custom require() should either:

  1. Not advertise the module-sync condition if it cannot load ESM, or
  2. Handle ESM when module-sync resolves to an .mjs file

Affected packages

This affects any package using module-sync exports. In practice, the most common are ljharb's ecosystem packages:

  • async-function (via is-async-functionwhich-builtin-typereflect.getprototypeof)
  • generator-function (via is-generator-functionwhich-builtin-typereflect.getprototypeof)
  • async-generator-function (via is-async-generator-functionwhich-builtin-typereflect.getprototypeof)

These are transitive dependencies — they're pulled in by common packages like es-abstract and deep-equal.

See also: ljharb/async-function#1

System Info

System:
  OS: macOS
  Node: v22.x

Package:
  vitest: 4.0.18

Config:
  pool: vmThreads

Used Package Manager

yarn

Validations

Metadata

Metadata

Assignees

No one assigned

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions