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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions