···25252626This makes tests run faster, but the VM module is unstable when running [ESM code](https://github.com/nodejs/node/issues/37648). Your tests will [leak memory](https://github.com/nodejs/node/issues/33439) - to battle that, consider manually editing [`vmMemoryLimit`](/config/vmmemorylimit) value.
27272828+On Node.js 24.9 and later, `require()` of an ES module is supported inside vm pools, mirroring [Node's own `require(esm)`](https://nodejs.org/api/modules.html#loading-ecmascript-modules-using-require). Calling `require()` on an ES module whose graph contains top-level `await` throws `ERR_REQUIRE_ASYNC_MODULE` - use `await import()` for those files.
2929+2830::: warning
2931Running code in a sandbox has some advantages (faster tests), but also comes with a number of disadvantages.
3032
+177-5
test/e2e/test/vm-threads.test.ts
···104104// was unflagged. The fix uses the _resolveFilename conditions option which
105105// is only available on Node 22.12+. Node 20 is unfixable and reaches EOL
106106// April 2026.
107107-const nodeMajor = Number(process.versions.node.split('.')[0])
107107+// On Node 24.9+ the vm pools support require(esm), so the condition resolves
108108+// to the ESM entry there — matching Node's own require() behaviour.
109109+const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number)
110110+const supportsRequireEsm = nodeMajor > 24 || (nodeMajor === 24 && nodeMinor >= 9)
111111+const moduleSyncEntry = supportsRequireEsm ? 'esm' : 'cjs'
108112test.skipIf(nodeMajor < 22)('can require package with module-sync exports condition', async () => {
109113 const { stderr, exitCode } = await runInlineTests({
110114 // .mjs module-sync entry
···138142139143 const require = createRequire(import.meta.url)
140144141141- test('require loads cjs entry for module-sync package (.mjs)', () => {
145145+ test('require loads the right entry for module-sync package (.mjs)', () => {
142146 const mod = require('module-sync-mjs')
143143- expect(mod.value).toBe('cjs')
147147+ expect(mod.value).toBe('${moduleSyncEntry}')
144148 })
145149146146- test('require loads cjs entry for module-sync package (.js with type: module)', () => {
150150+ test('require loads the right entry for module-sync package (.js with type: module)', () => {
147151 const mod = require('module-sync-js')
148148- expect(mod.value).toBe('cjs')
152152+ expect(mod.value).toBe('${moduleSyncEntry}')
149153 })
150154 `,
151155 }, {
···155159 expect(stderr).toBe('')
156160 expect(exitCode).toBe(0)
157161})
162162+163163+// https://nodejs.org/api/esm.html#commonjs-namespaces
164164+test.for(['vmThreads', 'vmForks'] as const)(
165165+ '%s provides the "module.exports" export on CommonJS namespaces',
166166+ async (pool) => {
167167+ const { stderr, exitCode } = await runVitest({
168168+ root: './fixtures/vm-cjs-namespace',
169169+ pool,
170170+ })
171171+172172+ expect(stderr).toBe('')
173173+ expect(exitCode).toBe(0)
174174+ },
175175+)
176176+177177+// require(esm) needs the synchronous module graph APIs added in Node 24.9
178178+// (SourceTextModule#hasAsyncGraph/linkRequests/instantiate)
179179+test.skipIf(!supportsRequireEsm).for(['vmThreads', 'vmForks'] as const)(
180180+ '%s supports require() of ES modules on Node 24.9+',
181181+ async (pool) => {
182182+ const { stderr, exitCode } = await runInlineTests({
183183+ 'package.json': '{}',
184184+ 'esm-scope/package.json': JSON.stringify({ type: 'module' }),
185185+ 'esm-scope/dep.mjs': 'export const dep = "dep"',
186186+ 'esm-scope/dep.cjs': 'module.exports = { fromCjs: "cjs" }',
187187+ 'esm-scope/entry.mjs': [
188188+ 'import { sep } from "node:path"',
189189+ 'import { dep } from "./dep.mjs"',
190190+ 'import cjs from "./dep.cjs"',
191191+ 'export const value = ["entry", dep, cjs.fromCjs].join(":")',
192192+ 'export const hasBuiltin = typeof sep === "string"',
193193+ 'export default "default-export"',
194194+ ].join('\n'),
195195+ 'esm-scope/scoped.js': 'export const value = "js-in-esm-scope"',
196196+ 'esm-scope/tla.mjs': 'export const value = await Promise.resolve("tla")',
197197+ 'esm-scope/tla-dep.mjs': [
198198+ 'import { value } from "./tla.mjs"',
199199+ 'export const wrapped = value',
200200+ ].join('\n'),
201201+ 'esm-scope/module-exports.mjs': [
202202+ 'const answer = 42',
203203+ 'export { answer as "module.exports" }',
204204+ ].join('\n'),
205205+ 'esm-scope/cjs-wrapper.mjs': 'export * from "./dep.cjs"',
206206+ 'esm-scope/cycle-a.mjs': [
207207+ 'import { b } from "./cycle-b.mjs"',
208208+ 'export const a = "a"',
209209+ 'export const seenB = b',
210210+ ].join('\n'),
211211+ 'esm-scope/cycle-b.mjs': [
212212+ 'import { a } from "./cycle-a.mjs"',
213213+ 'export const b = "b"',
214214+ 'export function readA() { return a }',
215215+ ].join('\n'),
216216+ 'esm-scope/data.json': '{"answer": 42}',
217217+ 'esm-scope/imports-json.mjs': [
218218+ 'import data from "./data.json" with { type: "json" }',
219219+ 'export const answer = data.answer',
220220+ ].join('\n'),
221221+ 'cjs-scope/package.json': '{}',
222222+ 'cjs-scope/esm-syntax.js': 'export const value = "detected"',
223223+ 'node_modules/esm-pkg/package.json': JSON.stringify({
224224+ name: 'esm-pkg',
225225+ exports: './index.mjs',
226226+ }),
227227+ 'node_modules/esm-pkg/index.mjs': 'export const state = { name: "esm-pkg" }',
228228+ 'node_modules/esm-pkg-2/package.json': JSON.stringify({
229229+ name: 'esm-pkg-2',
230230+ exports: './index.mjs',
231231+ }),
232232+ 'node_modules/esm-pkg-2/index.mjs': 'export const state = { name: "esm-pkg-2" }',
233233+ 'require-esm.test.js': `
234234+ import { createRequire } from 'node:module'
235235+ import { expect, test } from 'vitest'
236236+237237+ const require = createRequire(import.meta.url)
238238+239239+ test('loads an ES module graph with esm, cjs and builtin dependencies', () => {
240240+ const ns = require('./esm-scope/entry.mjs')
241241+ expect(ns.value).toBe('entry:dep:cjs')
242242+ expect(ns.hasBuiltin).toBe(true)
243243+ expect(ns.default).toBe('default-export')
244244+ })
245245+246246+ test('require() of the same module is cached', () => {
247247+ expect(require('./esm-scope/entry.mjs')).toBe(require('./esm-scope/entry.mjs'))
248248+ })
249249+250250+ test('loads a .js file from a "type": "module" scope', () => {
251251+ expect(require('./esm-scope/scoped.js').value).toBe('js-in-esm-scope')
252252+ })
253253+254254+ test('supports circular imports', () => {
255255+ const ns = require('./esm-scope/cycle-a.mjs')
256256+ expect(ns.a).toBe('a')
257257+ expect(ns.seenB).toBe('b')
258258+ })
259259+260260+ test('an export named "module.exports" defines the require() result', () => {
261261+ expect(require('./esm-scope/module-exports.mjs')).toBe(42)
262262+ })
263263+264264+ test('export * from a cjs file carries "module.exports" through require()', () => {
265265+ // the cjs namespace exposes its raw exports as a "module.exports"
266266+ // named export, which export * re-exports (unlike default) - so
267267+ // requiring the ESM wrapper returns the raw cjs exports object
268268+ expect(require('./esm-scope/cjs-wrapper.mjs')).toBe(require('./esm-scope/dep.cjs'))
269269+ })
270270+271271+ test('require() of json keeps returning the plain object', () => {
272272+ expect(require('./esm-scope/data.json')).toEqual({ answer: 42 })
273273+ })
274274+275275+ test('static json imports work inside a required ES module', () => {
276276+ expect(require('./esm-scope/imports-json.mjs').answer).toBe(42)
277277+ })
278278+279279+ test('top-level await throws ERR_REQUIRE_ASYNC_MODULE', () => {
280280+ let error
281281+ try {
282282+ require('./esm-scope/tla.mjs')
283283+ }
284284+ catch (caught) {
285285+ error = caught
286286+ }
287287+ expect(error).toBeDefined()
288288+ expect(error.code).toBe('ERR_REQUIRE_ASYNC_MODULE')
289289+ expect(error.message).toContain('top-level await')
290290+ })
291291+292292+ test('top-level await in a dependency throws ERR_REQUIRE_ASYNC_MODULE', () => {
293293+ let error
294294+ try {
295295+ require('./esm-scope/tla-dep.mjs')
296296+ }
297297+ catch (caught) {
298298+ error = caught
299299+ }
300300+ expect(error).toBeDefined()
301301+ expect(error.code).toBe('ERR_REQUIRE_ASYNC_MODULE')
302302+ })
303303+304304+ test('falls back to ESM for a .js file with ESM syntax in a CJS scope', () => {
305305+ const ns = require('./cjs-scope/esm-syntax.js')
306306+ expect(ns.value).toBe('detected')
307307+ expect(require('./cjs-scope/esm-syntax.js')).toBe(ns)
308308+ })
309309+310310+ test('import() after require() reuses the same module', async () => {
311311+ const required = require('esm-pkg')
312312+ const imported = await import('esm-pkg')
313313+ expect(imported.state).toBe(required.state)
314314+ })
315315+316316+ test('require() after import() reuses the same module', async () => {
317317+ const imported = await import('esm-pkg-2')
318318+ const required = require('esm-pkg-2')
319319+ expect(required.state).toBe(imported.state)
320320+ })
321321+ `,
322322+ }, {
323323+ pool,
324324+ })
325325+326326+ expect(stderr).toBe('')
327327+ expect(exitCode).toBe(0)
328328+ },
329329+)
+3-1
test/unit/test/interop.test.ts
···1313 "test": "hello",
1414 }
1515 `)
1616- if (task.file.projectName === 'vmThreads' || nodeMajor < 23) {
1616+ // vm pools always provide the 'module.exports' export on CJS namespaces;
1717+ // native Node.js added it in v23.0.0
1818+ if (task.file.projectName !== 'vmThreads' && nodeMajor < 23) {
1719 expect(esModuleFalse).toMatchInlineSnapshot(`
1820 {
1921 "__esModule": false,
···11+import { createRequire } from 'node:module'
22+import * as fs from 'node:fs'
33+import { expect, test } from 'vitest'
44+import * as raw from './src/external/raw-cjs.cjs'
55+import * as collision from './src/external/collision-cjs.cjs'
66+77+const require = createRequire(import.meta.url)
88+99+// https://nodejs.org/api/esm.html#commonjs-namespaces
1010+test('cjs namespace provides the raw exports as "module.exports"', () => {
1111+ expect(raw['module.exports']).toEqual({ a: 1 })
1212+ expect(raw['module.exports']).toBe(require('./src/external/raw-cjs.cjs'))
1313+ expect(raw.default).toBe(raw['module.exports'])
1414+ expect(raw.a).toBe(1)
1515+})
1616+1717+test('dynamic import provides the same "module.exports" export', async () => {
1818+ const ns = await import('./src/external/raw-cjs.cjs')
1919+ expect(ns['module.exports']).toBe(require('./src/external/raw-cjs.cjs'))
2020+})
2121+2222+test('synthetic "module.exports" shadows a real property of that name', () => {
2323+ expect(collision['module.exports']).toBe(require('./src/external/collision-cjs.cjs'))
2424+ expect(collision['module.exports']['module.exports']).toBe('shadowed')
2525+ expect(collision.x).toBe(1)
2626+})
2727+2828+test('builtin namespaces do not provide "module.exports"', () => {
2929+ expect('module.exports' in fs).toBe(false)
3030+})