Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: don't crash when using --experimental-vm-threads, interop CJS default inside node_modules #3876

Merged
merged 3 commits into from Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/vitest/src/runtime/execute.ts
Expand Up @@ -146,7 +146,11 @@ export class VitestExecutor extends ViteNodeRunner {
}

constructor(public options: ExecuteOptions) {
super(options)
super({
...options,
// interop is done inside the external executor instead
interopDefault: options.context ? false : options.interopDefault,
})

this.mocker = new VitestMocker(this)

Expand All @@ -169,6 +173,7 @@ export class VitestExecutor extends ViteNodeRunner {
}
else {
this.externalModules = new ExternalModulesExecutor({
...options,
context: options.context,
packageCache: options.packageCache,
})
Expand Down
66 changes: 56 additions & 10 deletions packages/vitest/src/runtime/external-executor.ts
Expand Up @@ -6,9 +6,10 @@ import { dirname } from 'node:path'
import { Module as _Module, createRequire } from 'node:module'
import { readFileSync, statSync } from 'node:fs'
import { basename, extname, join, normalize } from 'pathe'
import { getCachedData, isNodeBuiltin, setCacheData } from 'vite-node/utils'
import { getCachedData, isNodeBuiltin, isPrimitive, setCacheData } from 'vite-node/utils'
import { CSS_LANGS_RE, KNOWN_ASSET_RE } from 'vite-node/constants'
import { getColors } from '@vitest/utils'
import type { ExecuteOptions } from './execute'

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

interface ExternalModulesExecutorOptions {
export interface ExternalModulesExecutorOptions extends ExecuteOptions {
context: vm.Context
packageCache: Map<string, any>
}
Expand Down Expand Up @@ -273,7 +274,7 @@ export class ExternalModulesExecutor {
return buffer
}

private findNearestPackageData(basedir: string) {
private findNearestPackageData(basedir: string): { type?: 'module' | 'commonjs' } {
const originalBasedir = basedir
const packageCache = this.options.packageCache
while (basedir) {
Expand All @@ -300,12 +301,11 @@ export class ExternalModulesExecutor {
basedir = nextBasedir
}

return null
return {}
}

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

private interopCommonJsModule(mod: any) {
if (isPrimitive(mod) || Array.isArray(mod) || mod instanceof Promise) {
return {
keys: [],
moduleExports: {},
defaultExport: mod,
}
}

if (this.options.interopDefault !== false && '__esModule' in mod && !isPrimitive(mod.default)) {
return {
keys: Array.from(new Set(Object.keys(mod.default).concat(Object.keys(mod)).filter(key => key !== 'default'))),
moduleExports: new Proxy(mod, {
get(mod, prop) {
return mod[prop] ?? mod.default?.[prop]
},
}),
defaultExport: mod,
}
}

return {
keys: Object.keys(mod).filter(key => key !== 'default'),
moduleExports: mod,
defaultExport: mod,
}
}

private wrapCommonJsSynteticModule(identifier: string, exports: Record<string, unknown>) {
// TODO: technically module should be parsed to find static exports, implement for strict mode in #2854
const { keys, moduleExports, defaultExport } = this.interopCommonJsModule(exports)
const m: any = new SyntheticModule(
[...keys, 'default'],
() => {
for (const key of keys)
m.setExport(key, moduleExports[key])
m.setExport('default', defaultExport)
},
{
context: this.context,
identifier,
},
)
return m
}

private async evaluateModule<T extends VMModule>(m: T): Promise<T> {
if (m.status === 'unlinked') {
this.esmLinkMap.set(
Expand Down Expand Up @@ -582,7 +628,7 @@ c.green(`export default {

if (extension === '.node' || isNodeBuiltin(identifier)) {
const exports = this.requireCoreModule(identifier)
return this.wrapSynteticModule(identifier, exports)
return this.wrapCoreSynteticModule(identifier, exports)
}

const isFileUrl = identifier.startsWith('file://')
Expand All @@ -600,7 +646,7 @@ c.green(`export default {
if (extension === '.cjs') {
const module = this.createCommonJSNodeModule(pathUrl)
const exports = this.loadCommonJSModule(module, pathUrl)
return this.wrapSynteticModule(fileUrl, exports)
return this.wrapCommonJsSynteticModule(fileUrl, exports)
}

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

const module = this.createCommonJSNodeModule(pathUrl)
const exports = this.loadCommonJSModule(module, pathUrl)
return this.wrapSynteticModule(fileUrl, exports)
return this.wrapCommonJsSynteticModule(fileUrl, exports)
}

async import(identifier: string) {
Expand Down