Skip to content

Commit 084e929

Browse files
authored
fix!: correctly interop nested default for external and inlined modules (#2512)
* fix: correctly interop nested default for external and inlined modules, if environment is not node * chore: cleanup * chore: cleanup * test: update default interop test * chore: update module interop tests * fix: add environment to enzym example * chore: update tests * test: don't run module tests without threads, because we don't clear ESM cache * chore: correctly reset current test environment
1 parent 58ee8e9 commit 084e929

File tree

19 files changed

+196
-90
lines changed

19 files changed

+196
-90
lines changed

examples/react-enzyme/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@vitest/ui": "latest",
1717
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.7",
1818
"enzyme": "^3.11.0",
19+
"jsdom": "^20.0.3",
1920
"vite": "latest",
2021
"vitest": "latest"
2122
},

examples/react-enzyme/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { defineConfig } from 'vitest/config'
66
export default defineConfig({
77
plugins: [react()],
88
test: {
9+
environment: 'jsdom',
910
setupFiles: ['./vitest.setup.ts'],
1011
},
1112
})

packages/vite-node/src/client.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,17 @@ export class ViteNodeRunner {
273273
enumerable: false,
274274
configurable: false,
275275
})
276-
// this prosxy is triggered only on exports.name and module.exports access
276+
// this prosxy is triggered only on exports.{name} and module.exports access
277277
const cjsExports = new Proxy(exports, {
278-
set(_, p, value) {
278+
set: (_, p, value) => {
279+
// treat "module.exports =" the same as "exports.default =" to not have nested "default.default",
280+
// so "exports.default" becomes the actual module
281+
if (p === 'default' && this.shouldInterop(url, { default: value })) {
282+
exportAll(cjsExports, value)
283+
exports.default = value
284+
return true
285+
}
286+
279287
if (!Reflect.has(exports, 'default'))
280288
exports.default = {}
281289

@@ -378,35 +386,45 @@ export class ViteNodeRunner {
378386
* Import a module and interop it
379387
*/
380388
async interopedImport(path: string) {
381-
const mod = await import(path)
382-
383-
if (this.shouldInterop(path, mod)) {
384-
const tryDefault = this.hasNestedDefault(mod)
385-
return new Proxy(mod, {
386-
get: proxyMethod('get', tryDefault),
387-
set: proxyMethod('set', tryDefault),
388-
has: proxyMethod('has', tryDefault),
389-
deleteProperty: proxyMethod('deleteProperty', tryDefault),
390-
})
391-
}
389+
const importedModule = await import(path)
392390

393-
return mod
394-
}
391+
if (!this.shouldInterop(path, importedModule))
392+
return importedModule
395393

396-
hasNestedDefault(target: any) {
397-
return '__esModule' in target && target.__esModule && 'default' in target.default
394+
const { mod, defaultExport } = interopModule(importedModule)
395+
396+
return new Proxy(mod, {
397+
get(mod, prop) {
398+
if (prop === 'default')
399+
return defaultExport
400+
return mod[prop] ?? defaultExport?.[prop]
401+
},
402+
has(mod, prop) {
403+
if (prop === 'default')
404+
return defaultExport !== undefined
405+
return prop in mod || (defaultExport && prop in defaultExport)
406+
},
407+
})
398408
}
399409
}
400410

401-
function proxyMethod(name: 'get' | 'set' | 'has' | 'deleteProperty', tryDefault: boolean) {
402-
return function (target: any, key: string | symbol, ...args: [any?, any?]): any {
403-
const result = Reflect[name](target, key, ...args)
404-
if (isPrimitive(target.default))
405-
return result
406-
if ((tryDefault && key === 'default') || typeof result === 'undefined')
407-
return Reflect[name](target.default, key, ...args)
408-
return result
411+
function interopModule(mod: any) {
412+
if (isPrimitive(mod)) {
413+
return {
414+
mod: { default: mod },
415+
defaultExport: mod,
416+
}
409417
}
418+
419+
let defaultExport = 'default' in mod ? mod.default : mod
420+
421+
if (!isPrimitive(defaultExport) && '__esModule' in defaultExport) {
422+
mod = defaultExport
423+
if ('default' in defaultExport)
424+
defaultExport = defaultExport.default
425+
}
426+
427+
return { mod, defaultExport }
410428
}
411429

412430
// keep consistency with Vite on how exports are defined

packages/vitest/src/integrations/chai/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import * as chai from 'chai'
22
import './setup'
33
import type { Test } from '../../types'
4-
import { getFullName, getWorkerState } from '../../utils'
4+
import { getCurrentEnvironment, getFullName } from '../../utils'
55
import type { MatcherState } from '../../types/chai'
66
import { getState, setState } from './jest-expect'
77
import { GLOBAL_EXPECT } from './constants'
88

9-
const workerState = getWorkerState()
10-
119
export function createExpect(test?: Test) {
1210
const expect = ((value: any, message?: string): Vi.Assertion => {
1311
const { assertionCalls } = getState(expect)
@@ -30,7 +28,7 @@ export function createExpect(test?: Test) {
3028
isExpectingAssertionsError: null,
3129
expectedAssertionsNumber: null,
3230
expectedAssertionsNumberErrorGen: null,
33-
environment: workerState.config.environment,
31+
environment: getCurrentEnvironment(),
3432
testPath: test?.suite.file?.filepath,
3533
currentTestName: test ? getFullName(test) : undefined,
3634
}, expect)

packages/vitest/src/runtime/entry.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export async function run(files: string[], config: ResolvedConfig): Promise<void
5353
if (!files || !files.length)
5454
continue
5555

56+
// @ts-expect-error untyped global
57+
globalThis.__vitest_environment__ = environment
58+
5659
const filesByOptions = groupBy(files, ({ envOptions }) => JSON.stringify(envOptions))
5760

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

6467
await withEnv(environment, files[0].envOptions || config.environmentOptions || {}, async () => {
6568
for (const { file } of files) {
66-
// it doesn't matter if running with --threads
67-
// if running with --no-threads, we usually want to reset everything before running a test
68-
// but we have --isolate option to disable this
69+
// it doesn't matter if running with --threads
70+
// if running with --no-threads, we usually want to reset everything before running a test
71+
// but we have --isolate option to disable this
6972
if (config.isolate) {
7073
workerState.mockMap.clear()
7174
resetModules(workerState.moduleCache, true)

packages/vitest/src/runtime/execute.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ViteNodeRunner } from 'vite-node/client'
22
import type { ViteNodeRunnerOptions } from 'vite-node'
33
import { normalizePath } from 'vite'
44
import type { MockMap } from '../types/mocker'
5-
import { getWorkerState } from '../utils'
5+
import { getCurrentEnvironment, getWorkerState } from '../utils'
66
import { VitestMocker } from './mocker'
77

88
export interface ExecuteOptions extends ViteNodeRunnerOptions {
@@ -60,4 +60,8 @@ export class VitestRunner extends ViteNodeRunner {
6060
__vitest_mocker__: this.mocker,
6161
})
6262
}
63+
64+
shouldInterop(path: string, mod: any) {
65+
return this.options.interopDefault ?? (getCurrentEnvironment() !== 'node' && super.shouldInterop(path, mod))
66+
}
6367
}

packages/vitest/src/runtime/worker.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async function startViteNode(ctx: WorkerContext) {
5050
},
5151
moduleCache,
5252
mockMap,
53-
interopDefault: config.deps.interopDefault ?? true,
53+
interopDefault: config.deps.interopDefault,
5454
root: config.root,
5555
base: config.base,
5656
}))[0]
@@ -70,6 +70,8 @@ function init(ctx: WorkerContext) {
7070
process.env.VITEST_WORKER_ID = String(workerId)
7171
process.env.VITEST_POOL_ID = String(poolId)
7272

73+
// @ts-expect-error untyped global
74+
globalThis.__vitest_environment__ = config.environment
7375
// @ts-expect-error I know what I am doing :P
7476
globalThis.__vitest_worker__ = {
7577
ctx,

packages/vitest/src/utils/global.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ export function getWorkerState(): WorkerGlobalState {
44
// @ts-expect-error untyped global
55
return globalThis.__vitest_worker__
66
}
7+
8+
export function getCurrentEnvironment(): string {
9+
// @ts-expect-error untyped global
10+
return globalThis.__vitest_environment__
11+
}

packages/vitest/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export function resetModules(modules: ModuleCacheMap, resetMocks = false) {
4949
const skipPaths = [
5050
// Vitest
5151
/\/vitest\/dist\//,
52+
/\/vite-node\/dist\//,
5253
// yarn's .store folder
5354
/vitest-virtual-\w+\/dist/,
5455
// cnpm

0 commit comments

Comments
 (0)