Skip to content

Commit

Permalink
fix: don't crash when using --experimental-vm-threads, interop CJS de…
Browse files Browse the repository at this point in the history
…fault inside node_modules (#3876)
  • Loading branch information
sheremet-va authored Aug 3, 2023
1 parent 3e1e7a1 commit 0c53e09
Show file tree
Hide file tree
Showing 15 changed files with 792 additions and 630 deletions.
7 changes: 6 additions & 1 deletion packages/vitest/src/runtime/execute.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 0c53e09

Please sign in to comment.