Skip to content

Commit 0c53e09

Browse files
authored
fix: don't crash when using --experimental-vm-threads, interop CJS default inside node_modules (#3876)
1 parent 3e1e7a1 commit 0c53e09

File tree

15 files changed

+792
-630
lines changed

15 files changed

+792
-630
lines changed

packages/vitest/src/runtime/execute.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ export class VitestExecutor extends ViteNodeRunner {
146146
}
147147

148148
constructor(public options: ExecuteOptions) {
149-
super(options)
149+
super({
150+
...options,
151+
// interop is done inside the external executor instead
152+
interopDefault: options.context ? false : options.interopDefault,
153+
})
150154

151155
this.mocker = new VitestMocker(this)
152156

@@ -169,6 +173,7 @@ export class VitestExecutor extends ViteNodeRunner {
169173
}
170174
else {
171175
this.externalModules = new ExternalModulesExecutor({
176+
...options,
172177
context: options.context,
173178
packageCache: options.packageCache,
174179
})

packages/vitest/src/runtime/external-executor.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import { dirname } from 'node:path'
66
import { Module as _Module, createRequire } from 'node:module'
77
import { readFileSync, statSync } from 'node:fs'
88
import { basename, extname, join, normalize } from 'pathe'
9-
import { getCachedData, isNodeBuiltin, setCacheData } from 'vite-node/utils'
9+
import { getCachedData, isNodeBuiltin, isPrimitive, setCacheData } from 'vite-node/utils'
1010
import { CSS_LANGS_RE, KNOWN_ASSET_RE } from 'vite-node/constants'
1111
import { getColors } from '@vitest/utils'
12+
import type { ExecuteOptions } from './execute'
1213

1314
// need to copy paste types for vm
1415
// because they require latest @types/node which we don't bundle
@@ -95,7 +96,7 @@ const nativeResolve = import.meta.resolve!
9596
const dataURIRegex
9697
= /^data:(?<mime>text\/javascript|application\/json|application\/wasm)(?:;(?<encoding>charset=utf-8|base64))?,(?<code>.*)$/
9798

98-
interface ExternalModulesExecutorOptions {
99+
export interface ExternalModulesExecutorOptions extends ExecuteOptions {
99100
context: vm.Context
100101
packageCache: Map<string, any>
101102
}
@@ -273,7 +274,7 @@ export class ExternalModulesExecutor {
273274
return buffer
274275
}
275276

276-
private findNearestPackageData(basedir: string) {
277+
private findNearestPackageData(basedir: string): { type?: 'module' | 'commonjs' } {
277278
const originalBasedir = basedir
278279
const packageCache = this.options.packageCache
279280
while (basedir) {
@@ -300,12 +301,11 @@ export class ExternalModulesExecutor {
300301
basedir = nextBasedir
301302
}
302303

303-
return null
304+
return {}
304305
}
305306

306-
private wrapSynteticModule(identifier: string, exports: Record<string, unknown>) {
307-
// TODO: technically module should be parsed to find static exports, implement for strict mode in #2854
308-
const moduleKeys = Object.keys(exports).filter(key => key !== 'default')
307+
private wrapCoreSynteticModule(identifier: string, exports: Record<string, unknown>) {
308+
const moduleKeys = Object.keys(exports)
309309
const m: any = new SyntheticModule(
310310
[...moduleKeys, 'default'],
311311
() => {
@@ -321,6 +321,52 @@ export class ExternalModulesExecutor {
321321
return m
322322
}
323323

324+
private interopCommonJsModule(mod: any) {
325+
if (isPrimitive(mod) || Array.isArray(mod) || mod instanceof Promise) {
326+
return {
327+
keys: [],
328+
moduleExports: {},
329+
defaultExport: mod,
330+
}
331+
}
332+
333+
if (this.options.interopDefault !== false && '__esModule' in mod && !isPrimitive(mod.default)) {
334+
return {
335+
keys: Array.from(new Set(Object.keys(mod.default).concat(Object.keys(mod)).filter(key => key !== 'default'))),
336+
moduleExports: new Proxy(mod, {
337+
get(mod, prop) {
338+
return mod[prop] ?? mod.default?.[prop]
339+
},
340+
}),
341+
defaultExport: mod,
342+
}
343+
}
344+
345+
return {
346+
keys: Object.keys(mod).filter(key => key !== 'default'),
347+
moduleExports: mod,
348+
defaultExport: mod,
349+
}
350+
}
351+
352+
private wrapCommonJsSynteticModule(identifier: string, exports: Record<string, unknown>) {
353+
// TODO: technically module should be parsed to find static exports, implement for strict mode in #2854
354+
const { keys, moduleExports, defaultExport } = this.interopCommonJsModule(exports)
355+
const m: any = new SyntheticModule(
356+
[...keys, 'default'],
357+
() => {
358+
for (const key of keys)
359+
m.setExport(key, moduleExports[key])
360+
m.setExport('default', defaultExport)
361+
},
362+
{
363+
context: this.context,
364+
identifier,
365+
},
366+
)
367+
return m
368+
}
369+
324370
private async evaluateModule<T extends VMModule>(m: T): Promise<T> {
325371
if (m.status === 'unlinked') {
326372
this.esmLinkMap.set(
@@ -582,7 +628,7 @@ c.green(`export default {
582628

583629
if (extension === '.node' || isNodeBuiltin(identifier)) {
584630
const exports = this.requireCoreModule(identifier)
585-
return this.wrapSynteticModule(identifier, exports)
631+
return this.wrapCoreSynteticModule(identifier, exports)
586632
}
587633

588634
const isFileUrl = identifier.startsWith('file://')
@@ -600,7 +646,7 @@ c.green(`export default {
600646
if (extension === '.cjs') {
601647
const module = this.createCommonJSNodeModule(pathUrl)
602648
const exports = this.loadCommonJSModule(module, pathUrl)
603-
return this.wrapSynteticModule(fileUrl, exports)
649+
return this.wrapCommonJsSynteticModule(fileUrl, exports)
604650
}
605651

606652
if (extension === '.mjs')
@@ -613,7 +659,7 @@ c.green(`export default {
613659

614660
const module = this.createCommonJSNodeModule(pathUrl)
615661
const exports = this.loadCommonJSModule(module, pathUrl)
616-
return this.wrapSynteticModule(fileUrl, exports)
662+
return this.wrapCommonJsSynteticModule(fileUrl, exports)
617663
}
618664

619665
async import(identifier: string) {

0 commit comments

Comments
 (0)