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!: correctly interop nested default for external and inlined modules #2512

Merged
merged 9 commits into from
Dec 16, 2022
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/react-enzyme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@vitest/ui": "latest",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
"enzyme": "^3.11.0",
"jsdom": "^20.0.3",
"vite": "latest",
"vitest": "latest"
},
Expand Down
1 change: 1 addition & 0 deletions examples/react-enzyme/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
},
})
68 changes: 43 additions & 25 deletions packages/vite-node/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,17 @@ export class ViteNodeRunner {
enumerable: false,
configurable: false,
})
// this prosxy is triggered only on exports.name and module.exports access
// this prosxy is triggered only on exports.{name} and module.exports access
const cjsExports = new Proxy(exports, {
set(_, p, value) {
set: (_, p, value) => {
// treat "module.exports =" the same as "exports.default =" to not have nested "default.default",
// so "exports.default" becomes the actual module
if (p === 'default' && this.shouldInterop(url, { default: value })) {
exportAll(cjsExports, value)
exports.default = value
return true
}

if (!Reflect.has(exports, 'default'))
exports.default = {}

Expand Down Expand Up @@ -396,35 +404,45 @@ export class ViteNodeRunner {
* Import a module and interop it
*/
async interopedImport(path: string) {
const mod = await import(path)

if (this.shouldInterop(path, mod)) {
const tryDefault = this.hasNestedDefault(mod)
return new Proxy(mod, {
get: proxyMethod('get', tryDefault),
set: proxyMethod('set', tryDefault),
has: proxyMethod('has', tryDefault),
deleteProperty: proxyMethod('deleteProperty', tryDefault),
})
}
const importedModule = await import(path)

return mod
}
if (!this.shouldInterop(path, importedModule))
return importedModule

hasNestedDefault(target: any) {
return '__esModule' in target && target.__esModule && 'default' in target.default
const { mod, defaultExport } = interopModule(importedModule)

return new Proxy(mod, {
get(mod, prop) {
if (prop === 'default')
return defaultExport
return mod[prop] ?? defaultExport?.[prop]
},
has(mod, prop) {
if (prop === 'default')
return defaultExport !== undefined
return prop in mod || (defaultExport && prop in defaultExport)
},
})
}
}

function proxyMethod(name: 'get' | 'set' | 'has' | 'deleteProperty', tryDefault: boolean) {
return function (target: any, key: string | symbol, ...args: [any?, any?]): any {
const result = Reflect[name](target, key, ...args)
if (isPrimitive(target.default))
return result
if ((tryDefault && key === 'default') || typeof result === 'undefined')
return Reflect[name](target.default, key, ...args)
return result
function interopModule(mod: any) {
if (isPrimitive(mod)) {
return {
mod: { default: mod },
defaultExport: mod,
}
}

let defaultExport = 'default' in mod ? mod.default : mod

if (!isPrimitive(defaultExport) && '__esModule' in defaultExport) {
mod = defaultExport
if ('default' in defaultExport)
defaultExport = defaultExport.default
}

return { mod, defaultExport }
}

// keep consistency with Vite on how exports are defined
Expand Down
6 changes: 2 additions & 4 deletions packages/vitest/src/integrations/chai/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import * as chai from 'chai'
import './setup'
import type { Test } from '../../types'
import { getFullName, getWorkerState } from '../../utils'
import { getCurrentEnvironment, getFullName } from '../../utils'
import type { MatcherState } from '../../types/chai'
import { getState, setState } from './jest-expect'
import { GLOBAL_EXPECT } from './constants'

const workerState = getWorkerState()

export function createExpect(test?: Test) {
const expect = ((value: any, message?: string): Vi.Assertion => {
const { assertionCalls } = getState(expect)
Expand All @@ -30,7 +28,7 @@ export function createExpect(test?: Test) {
isExpectingAssertionsError: null,
expectedAssertionsNumber: null,
expectedAssertionsNumberErrorGen: null,
environment: workerState.config.environment,
environment: getCurrentEnvironment(),
testPath: test?.suite.file?.filepath,
currentTestName: test ? getFullName(test) : undefined,
}, expect)
Expand Down
9 changes: 6 additions & 3 deletions packages/vitest/src/runtime/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export async function run(files: string[], config: ResolvedConfig): Promise<void
if (!files || !files.length)
continue

// @ts-expect-error untyped global
globalThis.__vitest_environment__ = environment

const filesByOptions = groupBy(files, ({ envOptions }) => JSON.stringify(envOptions))

for (const options of Object.keys(filesByOptions)) {
Expand All @@ -63,9 +66,9 @@ export async function run(files: string[], config: ResolvedConfig): Promise<void

await withEnv(environment, files[0].envOptions || config.environmentOptions || {}, async () => {
for (const { file } of files) {
// it doesn't matter if running with --threads
// if running with --no-threads, we usually want to reset everything before running a test
// but we have --isolate option to disable this
// it doesn't matter if running with --threads
// if running with --no-threads, we usually want to reset everything before running a test
// but we have --isolate option to disable this
if (config.isolate) {
workerState.mockMap.clear()
resetModules(workerState.moduleCache, true)
Expand Down
6 changes: 5 additions & 1 deletion packages/vitest/src/runtime/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ViteNodeRunner } from 'vite-node/client'
import type { ViteNodeRunnerOptions } from 'vite-node'
import { normalizePath } from 'vite'
import type { MockMap } from '../types/mocker'
import { getWorkerState } from '../utils'
import { getCurrentEnvironment, getWorkerState } from '../utils'
import { VitestMocker } from './mocker'

export interface ExecuteOptions extends ViteNodeRunnerOptions {
Expand Down Expand Up @@ -51,4 +51,8 @@ export class VitestRunner extends ViteNodeRunner {
__vitest_mocker__: mocker,
})
}

shouldInterop(path: string, mod: any) {
return this.options.interopDefault ?? (getCurrentEnvironment() !== 'node' && super.shouldInterop(path, mod))
}
}
4 changes: 3 additions & 1 deletion packages/vitest/src/runtime/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function startViteNode(ctx: WorkerContext) {
},
moduleCache,
mockMap,
interopDefault: config.deps.interopDefault ?? true,
interopDefault: config.deps.interopDefault,
root: config.root,
base: config.base,
}))[0]
Expand All @@ -70,6 +70,8 @@ function init(ctx: WorkerContext) {
process.env.VITEST_WORKER_ID = String(workerId)
process.env.VITEST_POOL_ID = String(poolId)

// @ts-expect-error untyped global
globalThis.__vitest_environment__ = config.environment
// @ts-expect-error I know what I am doing :P
globalThis.__vitest_worker__ = {
ctx,
Expand Down
5 changes: 5 additions & 0 deletions packages/vitest/src/utils/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export function getWorkerState(): WorkerGlobalState {
// @ts-expect-error untyped global
return globalThis.__vitest_worker__
}

export function getCurrentEnvironment(): string {
// @ts-expect-error untyped global
return globalThis.__vitest_environment__
}
1 change: 1 addition & 0 deletions packages/vitest/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function resetModules(modules: ModuleCacheMap, resetMocks = false) {
const skipPaths = [
// Vitest
/\/vitest\/dist\//,
/\/vite-node\/dist\//,
// yarn's .store folder
/vitest-virtual-\w+\/dist/,
// cnpm
Expand Down
Loading