diff --git a/packages/mocker/src/node/hoistMocksPlugin.ts b/packages/mocker/src/node/hoistMocksPlugin.ts index 5515b3b1f09b..04a4ce867d9f 100644 --- a/packages/mocker/src/node/hoistMocksPlugin.ts +++ b/packages/mocker/src/node/hoistMocksPlugin.ts @@ -6,23 +6,23 @@ import type { Expression, Identifier, ImportDeclaration, - ImportExpression, VariableDeclaration, } from 'estree' import type { SourceMap } from 'magic-string' +import type { RollupAstNode } from 'rollup' import type { Plugin, Rollup } from 'vite' import type { Node, Positioned } from './esmWalker' import { findNodeAround } from 'acorn-walk' import MagicString from 'magic-string' import { createFilter } from 'vite' -import { esmWalker, getArbitraryModuleIdentifier } from './esmWalker' +import { esmWalker } from './esmWalker' interface HoistMocksOptions { /** * List of modules that should always be imported before compiler hints. - * @default ['vitest'] + * @default 'vitest' */ - hoistedModules?: string[] + hoistedModule?: string /** * @default ["vi", "vitest"] */ @@ -106,11 +106,14 @@ function isIdentifier(node: any): node is Positioned { return node.type === 'Identifier' } -function getBetterEnd(code: string, node: Node) { +function getNodeTail(code: string, node: Node) { let end = node.end if (code[node.end] === ';') { end += 1 } + if (code[node.end] === '\n') { + return end + 1 + } if (code[node.end + 1] === '\n') { end += 1 } @@ -160,48 +163,43 @@ export function hoistMocks( dynamicImportMockMethodNames = ['mock', 'unmock', 'doMock', 'doUnmock'], hoistedMethodNames = ['hoisted'], utilsObjectNames = ['vi', 'vitest'], - hoistedModules = ['vitest'], + hoistedModule = 'vitest', } = options - const hoistIndex = code.match(hashbangRE)?.[0].length ?? 0 + // hoist at the start of the file, after the hashbang + let hoistIndex = hashbangRE.exec(code)?.[0].length ?? 0 let hoistedModuleImported = false let uid = 0 const idToImportMap = new Map() + const imports: { + node: RollupAstNode + id: string + }[] = [] + // this will transform import statements into dynamic ones, if there are imports // it will keep the import as is, if we don't need to mock anything // in browser environment it will wrap the module value with "vitest_wrap_module" function // that returns a proxy to the module so that named exports can be mocked - const transformImportDeclaration = (node: ImportDeclaration) => { - const source = node.source.value as string - - const importId = `__vi_import_${uid++}__` - const hasSpecifiers = node.specifiers.length > 0 - const code = hasSpecifiers - ? `const ${importId} = await import('${source}')\n` - : `await import('${source}')\n` - return { - code, - id: importId, - } - } - - function defineImport(node: Positioned) { + function defineImport( + importNode: ImportDeclaration & { + start: number + end: number + }, + ) { + const source = importNode.source.value as string // always hoist vitest import to top of the file, so // "vi" helpers can access it - if (hoistedModules.includes(node.source.value as string)) { + if (hoistedModule === source) { hoistedModuleImported = true return } + const importId = `__vi_import_${uid++}__` + imports.push({ id: importId, node: importNode }) - const declaration = transformImportDeclaration(node) - if (!declaration) { - return null - } - s.appendLeft(hoistIndex, declaration.code) - return declaration.id + return importId } // 1. check all import statements and record id -> importName map @@ -214,13 +212,20 @@ export function hoistMocks( if (!importId) { continue } - s.remove(node.start, getBetterEnd(code, node)) for (const spec of node.specifiers) { if (spec.type === 'ImportSpecifier') { - idToImportMap.set( - spec.local.name, - `${importId}.${getArbitraryModuleIdentifier(spec.imported)}`, - ) + if (spec.imported.type === 'Identifier') { + idToImportMap.set( + spec.local.name, + `${importId}.${spec.imported.name}`, + ) + } + else { + idToImportMap.set( + spec.local.name, + `${importId}[${JSON.stringify(spec.imported.value as string)}]`, + ) + } } else if (spec.type === 'ImportDefaultSpecifier') { idToImportMap.set(spec.local.name, `${importId}.default`) @@ -235,7 +240,7 @@ export function hoistMocks( const declaredConst = new Set() const hoistedNodes: Positioned< - CallExpression | VariableDeclaration | AwaitExpression + CallExpression | VariableDeclaration | AwaitExpression >[] = [] function createSyntaxError(node: Positioned, message: string) { @@ -300,6 +305,8 @@ export function hoistMocks( } } + const usedUtilityExports = new Set() + esmWalker(ast, { onIdentifier(id, info, parentStack) { const binding = idToImportMap.get(id.name) @@ -333,6 +340,7 @@ export function hoistMocks( && isIdentifier(node.callee.property) ) { const methodName = node.callee.property.name + usedUtilityExports.add(node.callee.object.name) if (hoistableMockMethodNames.includes(methodName)) { const method = `${node.callee.object.name}.${methodName}` @@ -347,6 +355,35 @@ export function hoistMocks( `Cannot export the result of "${method}". Remove export declaration because "${method}" doesn\'t return anything.`, ) } + // rewrite vi.mock(import('..')) into vi.mock('..') + if ( + node.type === 'CallExpression' + && node.callee.type === 'MemberExpression' + && dynamicImportMockMethodNames.includes((node.callee.property as Identifier).name) + ) { + const moduleInfo = node.arguments[0] as Positioned + // vi.mock(import('./path')) -> vi.mock('./path') + if (moduleInfo.type === 'ImportExpression') { + const source = moduleInfo.source as Positioned + s.overwrite( + moduleInfo.start, + moduleInfo.end, + s.slice(source.start, source.end), + ) + } + // vi.mock(await import('./path')) -> vi.mock('./path') + if ( + moduleInfo.type === 'AwaitExpression' + && moduleInfo.argument.type === 'ImportExpression' + ) { + const source = moduleInfo.argument.source as Positioned + s.overwrite( + moduleInfo.start, + moduleInfo.end, + s.slice(source.start, source.end), + ) + } + } hoistedNodes.push(node) } // vi.doMock(import('./path')) -> vi.doMock('./path') @@ -394,9 +431,8 @@ export function hoistMocks( 'AwaitExpression', )?.node as Positioned | undefined // hoist "await vi.hoisted(async () => {})" or "vi.hoisted(() => {})" - hoistedNodes.push( - awaitedExpression?.argument === node ? awaitedExpression : node, - ) + const moveNode = awaitedExpression?.argument === node ? awaitedExpression : node + hoistedNodes.push(moveNode) } } } @@ -446,24 +482,6 @@ export function hoistMocks( ) } - function rewriteMockDynamicImport( - nodeCode: string, - moduleInfo: Positioned, - expressionStart: number, - expressionEnd: number, - mockStart: number, - ) { - const source = moduleInfo.source as Positioned - const importPath = s.slice(source.start, source.end) - const nodeCodeStart = expressionStart - mockStart - const nodeCodeEnd = expressionEnd - mockStart - return ( - nodeCode.slice(0, nodeCodeStart) - + importPath - + nodeCode.slice(nodeCodeEnd) - ) - } - // validate hoistedNodes doesn't have nodes inside other nodes for (let i = 0; i < hoistedNodes.length; i++) { const node = hoistedNodes[i] @@ -479,61 +497,55 @@ export function hoistMocks( } } - // Wait for imports to be hoisted and then hoist the mocks - const hoistedCode = hoistedNodes - .map((node) => { - const end = getBetterEnd(code, node) - /** - * In the following case, we need to change the `user` to user: __vi_import_x__.user - * So we should get the latest code from `s`. - * - * import user from './user' - * vi.mock('./mock.js', () => ({ getSession: vi.fn().mockImplementation(() => ({ user })) })) - */ - let nodeCode = s.slice(node.start, end) - - // rewrite vi.mock(import('..')) into vi.mock('..') - if ( - node.type === 'CallExpression' - && node.callee.type === 'MemberExpression' - && dynamicImportMockMethodNames.includes((node.callee.property as Identifier).name) - ) { - const moduleInfo = node.arguments[0] as Positioned - // vi.mock(import('./path')) -> vi.mock('./path') - if (moduleInfo.type === 'ImportExpression') { - nodeCode = rewriteMockDynamicImport( - nodeCode, - moduleInfo, - moduleInfo.start, - moduleInfo.end, - node.start, - ) - } - // vi.mock(await import('./path')) -> vi.mock('./path') - if ( - moduleInfo.type === 'AwaitExpression' - && moduleInfo.argument.type === 'ImportExpression' - ) { - nodeCode = rewriteMockDynamicImport( - nodeCode, - moduleInfo.argument as Positioned, - moduleInfo.start, - moduleInfo.end, - node.start, - ) - } - } + // hoist vi.mock/vi.hoisted + for (const node of hoistedNodes) { + const end = getNodeTail(code, node) + if (hoistIndex === end) { + hoistIndex = end + } + // don't hoist into itself if it's already at the top + else if (hoistIndex !== node.start) { + s.move(node.start, end, hoistIndex) + } + } - s.remove(node.start, end) - return `${nodeCode}${nodeCode.endsWith('\n') ? '' : '\n'}` - }) - .join('') + // hoist actual dynamic imports last so they are inserted after all hoisted mocks + for (const { node: importNode, id: importId } of imports) { + const source = importNode.source.value as string - if (hoistedCode || hoistedModuleImported) { - s.prepend( - (!hoistedModuleImported && hoistedCode ? API_NOT_FOUND_CHECK(utilsObjectNames) : '') - + hoistedCode, + s.update( + importNode.start, + importNode.end, + `const ${importId} = await import(${JSON.stringify( + source, + )});\n`, ) + + if (importNode.start === hoistIndex) { + // no need to hoist, but update hoistIndex to keep the order + hoistIndex = importNode.end + } + else { + // There will be an error if the module is called before it is imported, + // so the module import statement is hoisted to the top + s.move(importNode.start, importNode.end, hoistIndex) + } + } + + if (!hoistedModuleImported && hoistedNodes.length) { + const utilityImports = [...usedUtilityExports] + // "vi" or "vitest" is imported from a module other than "vitest" + if (utilityImports.some(name => idToImportMap.has(name))) { + s.prepend(API_NOT_FOUND_CHECK(utilityImports)) + } + // if "vi" or "vitest" are not imported at all, import them + else if (utilityImports.length) { + s.prepend( + `import { ${[...usedUtilityExports].join(', ')} } from ${JSON.stringify( + hoistedModule, + )}\n`, + ) + } } return { diff --git a/test/core/test/injector-mock.test.ts b/test/core/test/injector-mock.test.ts index 80f8b2d35b77..b0b9fd4ae4af 100644 --- a/test/core/test/injector-mock.test.ts +++ b/test/core/test/injector-mock.test.ts @@ -19,7 +19,7 @@ const hoistMocksOptions: HoistMocksPluginOptions = { }, } -async function hoistSimple(code: string, url = '') { +function hoistSimple(code: string, url = '') { return hoistMocks(code, url, parse, hoistMocksOptions) } @@ -33,7 +33,7 @@ test('hoists mock, unmock, hoisted', () => { vi.unmock('path') vi.hoisted(() => {}) `)).toMatchInlineSnapshot(` - "if (typeof globalThis["vi"] === "undefined" && typeof globalThis["vitest"] === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } + "import { vi } from "vitest" vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {})" @@ -42,21 +42,18 @@ test('hoists mock, unmock, hoisted', () => { test('always hoists import from vitest', () => { expect(hoistSimpleCode(` - import { vi } from 'vitest' - vi.mock('path', () => {}) - vi.unmock('path') - vi.hoisted(() => {}) - import { test } from 'vitest' +import { vi } from 'vitest' +vi.mock('path', () => {}) +vi.unmock('path') +vi.hoisted(() => {}) +import { test } from 'vitest' `)).toMatchInlineSnapshot(` "vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {}) - import { vi } from 'vitest' - - - - import { test } from 'vitest'" + import { vi } from 'vitest' + import { test } from 'vitest'" `) }) @@ -73,16 +70,13 @@ test('always hoists all imports but they are under mocks', () => { "vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {}) - const __vi_import_0__ = await import('./path.js') - const __vi_import_1__ = await import('./path2.js') + const __vi_import_0__ = await import("./path.js"); + const __vi_import_1__ = await import("./path2.js"); import { vi } from 'vitest' - - - - import { test } from 'vitest'" + import { test } from 'vitest'" `) }) @@ -93,7 +87,7 @@ test('correctly mocks namespaced', () => { vi.mock('../src/add', () => {}) `)).toMatchInlineSnapshot(` "vi.mock('../src/add', () => {}) - const __vi_import_0__ = await import('../src/add') + const __vi_import_0__ = await import("../src/add"); import { vi } from 'vitest'" `) @@ -107,7 +101,7 @@ test('correctly access import', () => { vi.mock('../src/add', () => {}) `)).toMatchInlineSnapshot(` "vi.mock('../src/add', () => {}) - const __vi_import_0__ = await import('../src/add') + const __vi_import_0__ = await import("../src/add"); import { vi } from 'vitest' @@ -117,14 +111,14 @@ test('correctly access import', () => { describe('transform', () => { const hoistSimpleCodeWithoutMocks = (code: string) => { - return hoistMocks(`import {vi} from "vitest";\n${code}\nvi.mock('faker');`, '/test.js', parse, hoistMocksOptions)?.code.trim() + return hoistMocks(`import {vi} from "vitest";\n${code}\nvi.mock('faker');\n`, '/test.js', parse, hoistMocksOptions)?.code.trim() } - test('default import', async () => { + test('default import', () => { expect( hoistSimpleCodeWithoutMocks(`import foo from 'vue';console.log(foo.bar)`), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; console.log(__vi_import_0__.default.bar)" `) @@ -150,8 +144,8 @@ vi.mock('./mock.js', () => ({ admin: __vi_import_1__.admin, })) })) - const __vi_import_0__ = await import('./user') - const __vi_import_1__ = await import('./admin') + const __vi_import_0__ = await import("./user"); + const __vi_import_1__ = await import("./admin"); import { vi } from 'vitest'" `) @@ -190,34 +184,34 @@ vi.mock('./mock.js', () => { `) }) - test('named import', async () => { + test('named import', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import { ref } from 'vue';function foo() { return ref(0) }`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function foo() { return __vi_import_0__.ref(0) }" `) }) - test('namespace import', async () => { + test('namespace import', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import * as vue from 'vue';function foo() { return vue.ref(0) }`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function foo() { return __vi_import_0__.ref(0) }" `) }) - test('export function declaration', async () => { - expect(await hoistSimpleCodeWithoutMocks(`export function foo() {}`)) + test('export function declaration', () => { + expect(hoistSimpleCodeWithoutMocks(`export function foo() {}`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -225,8 +219,8 @@ vi.mock('./mock.js', () => { `) }) - test('export class declaration', async () => { - expect(await hoistSimpleCodeWithoutMocks(`export class foo {}`)) + test('export class declaration', () => { + expect(hoistSimpleCodeWithoutMocks(`export class foo {}`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -234,8 +228,8 @@ vi.mock('./mock.js', () => { `) }) - test('export var declaration', async () => { - expect(await hoistSimpleCodeWithoutMocks(`export const a = 1, b = 2`)) + test('export var declaration', () => { + expect(hoistSimpleCodeWithoutMocks(`export const a = 1, b = 2`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -243,9 +237,9 @@ vi.mock('./mock.js', () => { `) }) - test('export named', async () => { + test('export named', () => { expect( - await hoistSimpleCodeWithoutMocks(`const a = 1, b = 2; export { a, b as c }`), + hoistSimpleCodeWithoutMocks(`const a = 1, b = 2; export { a, b as c }`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -253,9 +247,9 @@ vi.mock('./mock.js', () => { `) }) - test('export named from', async () => { + test('export named from', () => { expect( - await hoistSimpleCodeWithoutMocks(`export { ref, computed as c } from 'vue'`), + hoistSimpleCodeWithoutMocks(`export { ref, computed as c } from 'vue'`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -263,22 +257,22 @@ vi.mock('./mock.js', () => { `) }) - test('named exports of imported binding', async () => { + test('named exports of imported binding', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import {createApp} from 'vue';export {createApp}`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; export {createApp}" `) }) - test('export * from', async () => { + test('export * from', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `export * from 'vue'\n` + `export * from 'react'`, ), ).toMatchInlineSnapshot(` @@ -289,8 +283,8 @@ vi.mock('./mock.js', () => { `) }) - test('export * as from', async () => { - expect(await hoistSimpleCodeWithoutMocks(`export * as foo from 'vue'`)) + test('export * as from', () => { + expect(hoistSimpleCodeWithoutMocks(`export * as foo from 'vue'`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -298,9 +292,9 @@ vi.mock('./mock.js', () => { `) }) - test('export default', async () => { + test('export default', () => { expect( - await hoistSimpleCodeWithoutMocks(`export default {}`), + hoistSimpleCodeWithoutMocks(`export default {}`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -308,35 +302,35 @@ vi.mock('./mock.js', () => { `) }) - test('export then import minified', async () => { + test('export then import minified', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `export * from 'vue';import {createApp} from 'vue';`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; export * from 'vue';" `) }) - test('hoist import to top', async () => { + test('hoist import to top', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `path.resolve('server.js');import path from 'node:path';`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('node:path') + const __vi_import_0__ = await import("node:path"); import {vi} from "vitest"; __vi_import_0__.default.resolve('server.js');" `) }) - test('import.meta', async () => { + test('import.meta', () => { expect( - await hoistSimpleCodeWithoutMocks(`console.log(import.meta.url)`), + hoistSimpleCodeWithoutMocks(`console.log(import.meta.url)`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -344,8 +338,8 @@ vi.mock('./mock.js', () => { `) }) - test('dynamic import', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('dynamic import', () => { + const result = hoistSimpleCodeWithoutMocks( `export const i = () => import('./foo')`, ) expect(result).toMatchInlineSnapshot(` @@ -355,115 +349,115 @@ vi.mock('./mock.js', () => { `) }) - test('do not rewrite method definition', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite method definition', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';class A { fn() { fn() } }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; class A { fn() { __vi_import_0__.fn() } }" `) }) - test('do not rewrite when variable is in scope', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite when variable is in scope', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A(){ const fn = () => {}; return { fn }; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A(){ const fn = () => {}; return { fn }; }" `) }) // #5472 - test('do not rewrite when variable is in scope with object destructuring', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite when variable is in scope with object destructuring', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }" `) }) // #5472 - test('do not rewrite when variable is in scope with array destructuring', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite when variable is in scope with array destructuring', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }" `) }) // #5727 - test('rewrite variable in string interpolation in function nested arguments', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('rewrite variable in string interpolation in function nested arguments', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A({foo = \`test\${fn}\`} = {}){ return {}; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A({foo = \`test\${__vi_import_0__.fn}\`} = {}){ return {}; }" `) }) // #6520 - test('rewrite variables in default value of destructuring params', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('rewrite variables in default value of destructuring params', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A({foo = fn}){ return {}; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A({foo = __vi_import_0__.fn}){ return {}; }" `) }) - test('do not rewrite when function declaration is in scope', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite when function declaration is in scope', () => { + const result = hoistSimpleCodeWithoutMocks( `import { fn } from 'vue';function A(){ function fn() {}; return { fn }; }`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; function A(){ function fn() {}; return { fn }; }" `) }) - test('do not rewrite catch clause', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('do not rewrite catch clause', () => { + const result = hoistSimpleCodeWithoutMocks( `import {error} from './dependency';try {} catch(error) {}`, ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./dependency') + const __vi_import_0__ = await import("./dependency"); import {vi} from "vitest"; try {} catch(error) {}" `) }) // #2221 - test('should declare variable for imported super class', async () => { + test('should declare variable for imported super class', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import { Foo } from './dependency';` + `class A extends Foo {}`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./dependency') + const __vi_import_0__ = await import("./dependency"); import {vi} from "vitest"; const Foo = __vi_import_0__.Foo; class A extends Foo {}" @@ -472,14 +466,14 @@ vi.mock('./mock.js', () => { // exported classes: should prepend the declaration at root level, before the // first class that uses the binding expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import { Foo } from './dependency';` + `export default class A extends Foo {}\n` + `export class B extends Foo {}`, ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./dependency') + const __vi_import_0__ = await import("./dependency"); import {vi} from "vitest"; const Foo = __vi_import_0__.Foo; export default class A extends Foo {} @@ -488,16 +482,16 @@ vi.mock('./mock.js', () => { }) // #4049 - test('should handle default export variants', async () => { + test('should handle default export variants', () => { // default anonymous functions - expect(await hoistSimpleCodeWithoutMocks(`export default function() {}\n`)) + expect(hoistSimpleCodeWithoutMocks(`export default function() {}\n`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; export default function() {}" `) // default anonymous class - expect(await hoistSimpleCodeWithoutMocks(`export default class {}\n`)) + expect(hoistSimpleCodeWithoutMocks(`export default class {}\n`)) .toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -505,7 +499,7 @@ vi.mock('./mock.js', () => { `) // default named functions expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `export default function foo() {}\n` + `foo.prototype = Object.prototype;`, ), @@ -517,7 +511,7 @@ vi.mock('./mock.js', () => { `) // default named classes expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `export default class A {}\n` + `export class B extends A {}`, ), ).toMatchInlineSnapshot(` @@ -528,9 +522,9 @@ vi.mock('./mock.js', () => { `) }) - test('sourcemap source', async () => { + test('sourcemap source', () => { const map = ( - (await hoistSimple( + (hoistSimple( `vi.mock(any); export const a = 1`, 'input.js', @@ -539,9 +533,9 @@ vi.mock('./mock.js', () => { expect(map?.sources).toStrictEqual(['input.js']) }) - test('overwrite bindings', async () => { + test('overwrite bindings', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `import { inject } from 'vue';` + `const a = { inject }\n` + `const b = { test: inject }\n` @@ -553,7 +547,7 @@ vi.mock('./mock.js', () => { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; const a = { inject: __vi_import_0__.inject } const b = { test: __vi_import_0__.inject } @@ -565,9 +559,9 @@ vi.mock('./mock.js', () => { `) }) - test('Empty array pattern', async () => { + test('Empty array pattern', () => { expect( - await hoistSimpleCodeWithoutMocks(`const [, LHS, RHS] = inMatch;`), + hoistSimpleCodeWithoutMocks(`const [, LHS, RHS] = inMatch;`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -575,9 +569,9 @@ vi.mock('./mock.js', () => { `) }) - test('function argument destructure', async () => { + test('function argument destructure', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { foo, bar } from 'foo' const a = ({ _ = foo() }) => {} @@ -587,7 +581,7 @@ function c({ _ = bar() + foo() }) {} ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; @@ -597,9 +591,9 @@ function c({ _ = bar() + foo() }) {} `) }) - test('object destructure alias', async () => { + test('object destructure alias', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { n } from 'foo' const a = () => { @@ -610,7 +604,7 @@ const a = () => { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; @@ -622,7 +616,7 @@ const a = () => { // #9585 expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { n, m } from 'foo' const foo = {} @@ -634,7 +628,7 @@ const foo = {} ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; @@ -646,9 +640,9 @@ const foo = {} `) }) - test('nested object destructure alias', async () => { + test('nested object destructure alias', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { remove, add, get, set, rest, objRest } from 'vue' @@ -678,10 +672,11 @@ objRest() ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; + function a() { const { o: { remove }, @@ -707,9 +702,9 @@ objRest() `) }) - test('object props and methods', async () => { + test('object props and methods', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import foo from 'foo' @@ -728,10 +723,11 @@ const obj = { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; + const bar = 'bar' const obj = { @@ -746,9 +742,9 @@ const obj = { `) }) - test('class props', async () => { + test('class props', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { remove, add } from 'vue' @@ -760,10 +756,11 @@ class A { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; + const add = __vi_import_0__.add; const remove = __vi_import_0__.remove; class A { @@ -773,9 +770,9 @@ class A { `) }) - test('class methods', async () => { + test('class methods', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import foo from 'foo' @@ -792,10 +789,11 @@ class A { ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foo') + const __vi_import_0__ = await import("foo"); import {vi} from "vitest"; + const bar = 'bar' class A { @@ -808,9 +806,9 @@ class A { `) }) - test('declare scope', async () => { + test('declare scope', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` import { aaa, bbb, ccc, ddd } from 'vue' @@ -838,10 +836,11 @@ bbb() ), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('vue') + const __vi_import_0__ = await import("vue"); import {vi} from "vitest"; + function foobar() { ddd() @@ -865,9 +864,9 @@ bbb() `) }) - test('continuous exports', async () => { + test('continuous exports', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( ` export function fn1() { }export function fn2() { @@ -885,7 +884,7 @@ export function fn1() { }) // https://github.com/vitest-dev/vitest/issues/1141 - test('export default expression', async () => { + test('export default expression', () => { // esbuild transform result of following TS code // export default function getRandom() { // return Math.random() @@ -896,7 +895,7 @@ export default (function getRandom() { }); `.trim() - expect(await hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` + expect(hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; export default (function getRandom() { @@ -905,7 +904,7 @@ export default (function getRandom() { `) expect( - await hoistSimpleCodeWithoutMocks(`export default (class A {});`), + hoistSimpleCodeWithoutMocks(`export default (class A {});`), ).toMatchInlineSnapshot(` "vi.mock('faker'); import {vi} from "vitest"; @@ -914,18 +913,18 @@ export default (function getRandom() { }) // #8002 - test('with hashbang', async () => { + test('with hashbang', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `#!/usr/bin/env node console.log("it can parse the hashbang")`, ), ).toMatchInlineSnapshot(`undefined`) }) - test('import hoisted after hashbang', async () => { + test('import hoisted after hashbang', () => { expect( - await hoistSimpleCodeWithoutMocks( + hoistSimpleCodeWithoutMocks( `#!/usr/bin/env node console.log(foo); import foo from "foo"`, @@ -934,7 +933,7 @@ import foo from "foo"`, }) // #10289 - test('track scope by class, function, condition blocks', async () => { + test('track scope by class, function, condition blocks', () => { const code = ` import { foo, bar } from 'foobar' if (false) { @@ -962,9 +961,9 @@ export class Test { } };`.trim() - expect(await hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` + expect(hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foobar') + const __vi_import_0__ = await import("foobar"); import {vi} from "vitest"; if (false) { @@ -995,9 +994,9 @@ export class Test { }) // #10386 - test('track var scope by function', async () => { + test('track var scope by function', () => { expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` import { foo, bar } from 'foobar' function test() { if (true) { @@ -1007,7 +1006,7 @@ function test() { }`), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foobar') + const __vi_import_0__ = await import("foobar"); import {vi} from "vitest"; @@ -1021,9 +1020,9 @@ function test() { }) // #11806 - test('track scope by blocks', async () => { + test('track scope by blocks', () => { expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` import { foo, bar, baz } from 'foobar' function test() { [foo]; @@ -1036,7 +1035,7 @@ function test() { }`), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('foobar') + const __vi_import_0__ = await import("foobar"); import {vi} from "vitest"; @@ -1052,9 +1051,9 @@ function test() { `) }) - test('track scope in for loops', async () => { + test('track scope in for loops', () => { expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` import { test } from './test.js' for (const test of tests) { @@ -1070,10 +1069,11 @@ for (const test in tests) { }`), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./test.js') + const __vi_import_0__ = await import("./test.js"); import {vi} from "vitest"; + for (const test of tests) { console.log(test) } @@ -1088,8 +1088,8 @@ for (const test in tests) { `) }) - test('avoid binding ClassExpression', async () => { - const result = await hoistSimpleCodeWithoutMocks( + test('avoid binding ClassExpression', () => { + const result = hoistSimpleCodeWithoutMocks( ` import Foo, { Bar } from './foo'; @@ -1103,10 +1103,11 @@ const Baz = class extends Foo {} ) expect(result).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./foo') + const __vi_import_0__ = await import("./foo"); import {vi} from "vitest"; + console.log(__vi_import_0__.default, __vi_import_0__.Bar); const obj = { foo: class Foo {}, @@ -1116,15 +1117,15 @@ const Baz = class extends Foo {} `) }) - test('import assertion attribute', async () => { + test('import assertion attribute', () => { expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` import * as foo from './foo.json' with { type: 'json' }; import('./bar.json', { with: { type: 'json' } }); `), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./foo.json') + const __vi_import_0__ = await import("./foo.json"); import {vi} from "vitest"; @@ -1132,12 +1133,12 @@ const Baz = class extends Foo {} `) }) - test('import and export ordering', async () => { + test('import and export ordering', () => { // Given all imported modules logs `mod ${mod}` on execution, // and `foo` is `bar`, the logging order should be: // "mod a", "mod foo", "mod b", "bar1", "bar2" expect( - await hoistSimpleCodeWithoutMocks(` + hoistSimpleCodeWithoutMocks(` console.log(foo + 1) export * from './a' import { foo } from './foo' @@ -1146,7 +1147,7 @@ console.log(foo + 2) `), ).toMatchInlineSnapshot(` "vi.mock('faker'); - const __vi_import_0__ = await import('./foo') + const __vi_import_0__ = await import("./foo"); import {vi} from "vitest"; console.log(__vi_import_0__.foo + 1) @@ -1157,7 +1158,7 @@ console.log(foo + 2) `) }) - test('handle single "await vi.hoisted"', async () => { + test('handle single "await vi.hoisted"', () => { expect( hoistSimpleCode(` import { vi } from 'vitest'; @@ -1210,7 +1211,7 @@ await vi vi.mock(await import(\`./path\`), () => {}); `), ).toMatchInlineSnapshot(` - "if (typeof globalThis["vi"] === "undefined" && typeof globalThis["vitest"] === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } + "import { vi } from "vitest" vi.mock('./path') vi.mock(somePath) vi.mock(\`./path\`) @@ -1271,6 +1272,58 @@ test('test', async () => { })" `) }) + + test('correctly hoists when import.meta is used', () => { + expect(hoistSimpleCode(` +import { calc } from './calc' +function sum(a, b) { + return calc("+", 1, 2); +} + +if (import.meta.vitest) { + const { vi, test, expect } = import.meta.vitest + vi.mock('faker') + test('sum', () => { + expect(sum(1, 2)).toBe(3) + }) +} + `)).toMatchInlineSnapshot(` + "import { vi } from "vitest" + vi.mock('faker') + const __vi_import_0__ = await import("./calc"); + + + function sum(a, b) { + return __vi_import_0__.calc("+", 1, 2); + } + + if (import.meta.vitest) { + const { vi, test, expect } = import.meta.vitest + test('sum', () => { + expect(sum(1, 2)).toBe(3) + }) + }" + `) + }) + + test('injects an error if a utility import is imported from an external module', () => { + expect(hoistSimpleCode(` +import { expect, test, vi } from './proxy-module' +vi.mock('vite') +test('hi', () => { + expect(1 + 1).toEqual(2) +}) + `)).toMatchInlineSnapshot(` + "if (typeof globalThis["vi"] === "undefined") { throw new Error("There are some problems in resolving the mocks API.\\nYou may encounter this issue when importing the mocks API from another module other than 'vitest'.\\nTo fix this issue you can either:\\n- import the mocks API directly from 'vitest'\\n- enable the 'globals' options") } + __vi_import_0__.vi.mock('vite') + const __vi_import_0__ = await import("./proxy-module"); + + + __vi_import_0__.test('hi', () => { + __vi_import_0__.expect(1 + 1).toEqual(2) + })" + `) + }) }) describe('throws an error when nodes are incompatible', () => { diff --git a/test/public-mocker/test/mocker.test.ts b/test/public-mocker/test/mocker.test.ts index 116c7838a5e1..a646d388f8e8 100644 --- a/test/public-mocker/test/mocker.test.ts +++ b/test/public-mocker/test/mocker.test.ts @@ -54,7 +54,7 @@ async function createTestServer(config: UserConfig) { globalThisAccessor: 'Symbol.for("vitest.mocker")', hoistMocks: { utilsObjectNames: ['mocker'], - hoistedModules: ['virtual:mocker'], + hoistedModule: 'virtual:mocker', hoistableMockMethodNames: ['customMock'], dynamicImportMockMethodNames: ['customMock'], hoistedMethodNames: ['customHoisted'], diff --git a/test/public-mocker/vite.config.ts b/test/public-mocker/vite.config.ts index 043a3e3221fd..7943f178b09e 100644 --- a/test/public-mocker/vite.config.ts +++ b/test/public-mocker/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ globalThisAccessor: 'Symbol.for("vitest.mocker")', hoistMocks: { utilsObjectNames: ['mocker'], - hoistedModules: ['virtual:mocker'], + hoistedModule: 'virtual:mocker', hoistableMockMethodNames: ['customMock'], dynamicImportMockMethodNames: ['customMock'], hoistedMethodNames: ['customHoisted'],