diff --git a/examples/mocks/src/set-foo.ts b/examples/mocks/src/set-foo.ts new file mode 100644 index 000000000000..1d72c27cffe4 --- /dev/null +++ b/examples/mocks/src/set-foo.ts @@ -0,0 +1,12 @@ +import { afterEach, beforeEach } from 'vitest' + +// eslint-disable-next-line import/no-mutable-exports +export let foo: number + +beforeEach(() => { + foo = 1 +}) + +afterEach(() => { + foo = 2 +}) diff --git a/examples/mocks/src/squared.js b/examples/mocks/src/squared.js new file mode 100644 index 000000000000..e870bc0da611 --- /dev/null +++ b/examples/mocks/src/squared.js @@ -0,0 +1,3 @@ +export function squared() { + +} diff --git a/examples/mocks/test/destructured.test.ts b/examples/mocks/test/destructured.test.ts new file mode 100644 index 000000000000..e57c77b3f4fb --- /dev/null +++ b/examples/mocks/test/destructured.test.ts @@ -0,0 +1,14 @@ +import * as squaredModule from '../src/squared.js' +import { squared } from '../src/squared.js' +import { foo } from '../src/set-foo.js' + +vi.mock('any') + +test('spyOn entire module', () => { + vi.spyOn(squaredModule, 'squared') + expect(squared).not.toHaveBeenCalled() +}) + +test('foo should be 1', () => { + expect(foo).toBe(1) +}) diff --git a/packages/browser/package.json b/packages/browser/package.json index 0581c5def86e..e79fa6f0fd10 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -67,12 +67,11 @@ } }, "dependencies": { - "estree-walker": "^3.0.3", + "@vitest/utils": "workspace:*", "magic-string": "^0.30.5", "sirv": "^2.0.4" }, "devDependencies": { - "@types/estree": "^1.0.5", "@types/ws": "^8.5.9", "@vitest/runner": "workspace:*", "@vitest/ui": "workspace:*", diff --git a/packages/browser/src/node/esmInjector.ts b/packages/browser/src/node/esmInjector.ts index a1e9fa7c67fc..775a29003c69 100644 --- a/packages/browser/src/node/esmInjector.ts +++ b/packages/browser/src/node/esmInjector.ts @@ -1,9 +1,8 @@ import MagicString from 'magic-string' import { extract_names as extractNames } from 'periscopic' -import type { Expression, ImportDeclaration } from 'estree' import type { PluginContext } from 'rollup' -import type { Node, Positioned } from './esmWalker' -import { esmWalker, isInDestructuringAssignment, isNodeInPattern, isStaticProperty } from './esmWalker' +import { esmWalker } from '@vitest/utils/ast' +import type { Expression, ImportDeclaration, Node, Positioned } from '@vitest/utils/ast' const viInjectedKey = '__vi_inject__' // const viImportMetaKey = '__vi_import_meta__' // to allow overwrite @@ -209,26 +208,16 @@ export function injectVitestModule(code: string, id: string, parse: PluginContex // 3. convert references to import bindings & import.meta references esmWalker(ast, { - onIdentifier(id, parent, parentStack) { - const grandparent = parentStack[1] + onIdentifier(id, info, parentStack) { const binding = idToImportMap.get(id.name) if (!binding) return - if (isStaticProperty(parent) && parent.shorthand) { - // let binding used in a property shorthand - // { foo } -> { foo: __import_x__.foo } - // skip for destructuring patterns - if ( - !isNodeInPattern(parent) - || isInDestructuringAssignment(parent, parentStack) - ) - s.appendLeft(id.end, `: ${binding}`) + if (info.hasBindingShortcut) { + s.appendLeft(id.end, `: ${binding}`) } else if ( - (parent.type === 'PropertyDefinition' - && grandparent?.type === 'ClassBody') - || (parent.type === 'ClassDeclaration' && id === parent.superClass) + info.classDeclaration ) { if (!declaredConst.has(id.name)) { declaredConst.add(id.name) @@ -239,7 +228,7 @@ export function injectVitestModule(code: string, id: string, parse: PluginContex } else if ( // don't transform class name identifier - !(parent.type === 'ClassExpression' && id === parent.id) + !info.classExpression ) { s.update(id.start, id.end, binding) } diff --git a/packages/utils/package.json b/packages/utils/package.json index 72217896808c..74cc0826dc0c 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -24,6 +24,10 @@ "types": "./dist/diff.d.ts", "default": "./dist/diff.js" }, + "./ast": { + "types": "./dist/ast.d.ts", + "default": "./dist/ast.js" + }, "./error": { "types": "./dist/error.d.ts", "default": "./dist/error.js" @@ -59,10 +63,12 @@ }, "dependencies": { "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" }, "devDependencies": { - "@jridgewell/trace-mapping": "^0.3.20" + "@jridgewell/trace-mapping": "^0.3.20", + "@types/estree": "^1.0.5" } } diff --git a/packages/utils/rollup.config.js b/packages/utils/rollup.config.js index 8630b787fc85..9c2b1b8e89a3 100644 --- a/packages/utils/rollup.config.js +++ b/packages/utils/rollup.config.js @@ -12,6 +12,7 @@ const entries = { 'index': 'src/index.ts', 'helpers': 'src/helpers.ts', 'diff': 'src/diff/index.ts', + 'ast': 'src/ast/index.ts', 'error': 'src/error.ts', 'source-map': 'src/source-map.ts', 'types': 'src/types.ts', diff --git a/packages/browser/src/node/esmWalker.ts b/packages/utils/src/ast/esmWalker.ts similarity index 100% rename from packages/browser/src/node/esmWalker.ts rename to packages/utils/src/ast/esmWalker.ts diff --git a/packages/utils/src/ast/index.ts b/packages/utils/src/ast/index.ts new file mode 100644 index 000000000000..7c2af7b11197 --- /dev/null +++ b/packages/utils/src/ast/index.ts @@ -0,0 +1,339 @@ +import type { + CallExpression, + Function as FunctionNode, + Identifier, + ImportExpression, + Pattern, + Property, + VariableDeclaration, + Node as _Node, +} from 'estree' +import { walk as eswalk } from 'estree-walker' + +export type * from 'estree' + +export type Positioned = T & { + start: number + end: number +} + +export type Node = Positioned<_Node> + +interface IdentifierInfo { + /** + * If the identifier is used in a property shorthand + * { foo } -> { foo: __import_x__.foo } + */ + hasBindingShortcut: boolean + /** + * The identifier is used in a class declaration + */ + classDeclaration: boolean + /** + * The identifier is a name for a class expression + */ + classExpression: boolean +} + +interface Visitors { + onIdentifier?: ( + node: Positioned, + info: IdentifierInfo, + parentStack: Node[], + ) => void + onImportMeta?: (node: Node) => void + onDynamicImport?: (node: Positioned) => void + onCallExpression?: (node: Positioned) => void +} + +const isNodeInPatternWeakSet = new WeakSet<_Node>() +export function setIsNodeInPattern(node: Property) { + return isNodeInPatternWeakSet.add(node) +} +export function isNodeInPattern(node: _Node): node is Property { + return isNodeInPatternWeakSet.has(node) +} + +/** + * Same logic from \@vue/compiler-core & \@vue/compiler-sfc + * Except this is using acorn AST + */ +export function esmWalker( + root: Node, + { onIdentifier, onImportMeta, onDynamicImport, onCallExpression }: Visitors, +) { + const parentStack: Node[] = [] + const varKindStack: VariableDeclaration['kind'][] = [] + const scopeMap = new WeakMap<_Node, Set>() + const identifiers: [id: any, stack: Node[]][] = [] + + const setScope = (node: _Node, name: string) => { + let scopeIds = scopeMap.get(node) + if (scopeIds && scopeIds.has(name)) + return + + if (!scopeIds) { + scopeIds = new Set() + scopeMap.set(node, scopeIds) + } + scopeIds.add(name) + } + + function isInScope(name: string, parents: Node[]) { + return parents.some(node => node && scopeMap.get(node)?.has(name)) + } + function handlePattern(p: Pattern, parentScope: _Node) { + if (p.type === 'Identifier') { + setScope(parentScope, p.name) + } + else if (p.type === 'RestElement') { + handlePattern(p.argument, parentScope) + } + else if (p.type === 'ObjectPattern') { + p.properties.forEach((property) => { + if (property.type === 'RestElement') + setScope(parentScope, (property.argument as Identifier).name) + + else + handlePattern(property.value, parentScope) + }) + } + else if (p.type === 'ArrayPattern') { + p.elements.forEach((element) => { + if (element) + handlePattern(element, parentScope) + }) + } + else if (p.type === 'AssignmentPattern') { + handlePattern(p.left, parentScope) + } + else { + setScope(parentScope, (p as any).name) + } + } + + eswalk(root, { + enter(node, parent) { + if (node.type === 'ImportDeclaration') + return this.skip() + + // track parent stack, skip for "else-if"/"else" branches as acorn nests + // the ast within "if" nodes instead of flattening them + if ( + parent + && !(parent.type === 'IfStatement' && node === parent.alternate) + ) + parentStack.unshift(parent as Node) + + // track variable declaration kind stack used by VariableDeclarator + if (node.type === 'VariableDeclaration') + varKindStack.unshift(node.kind) + + if (node.type === 'CallExpression') + onCallExpression?.(node as Positioned) + + if (node.type === 'MetaProperty' && node.meta.name === 'import') + onImportMeta?.(node as Node) + + else if (node.type === 'ImportExpression') + onDynamicImport?.(node as Positioned) + + if (node.type === 'Identifier') { + if ( + !isInScope(node.name, parentStack) + && isRefIdentifier(node, parent!, parentStack) + ) { + // record the identifier, for DFS -> BFS + identifiers.push([node, parentStack.slice(0)]) + } + } + else if (isFunctionNode(node)) { + // If it is a function declaration, it could be shadowing an import + // Add its name to the scope so it won't get replaced + if (node.type === 'FunctionDeclaration') { + const parentScope = findParentScope(parentStack) + if (parentScope) + setScope(parentScope, node.id!.name) + } + // walk function expressions and add its arguments to known identifiers + // so that we don't prefix them + node.params.forEach((p) => { + if (p.type === 'ObjectPattern' || p.type === 'ArrayPattern') { + handlePattern(p, node) + return + } + (eswalk as any)(p.type === 'AssignmentPattern' ? p.left : p, { + enter(child: Node, parent: Node) { + // skip params default value of destructure + if ( + parent?.type === 'AssignmentPattern' + && parent?.right === child + ) + return this.skip() + + if (child.type !== 'Identifier') + return + // do not record as scope variable if is a destructuring keyword + if (isStaticPropertyKey(child, parent)) + return + // do not record if this is a default value + // assignment of a destructuring variable + if ( + (parent?.type === 'TemplateLiteral' + && parent?.expressions.includes(child)) + || (parent?.type === 'CallExpression' && parent?.callee === child) + ) + return + + setScope(node, child.name) + }, + }) + }) + } + else if (node.type === 'Property' && parent!.type === 'ObjectPattern') { + // mark property in destructuring pattern + setIsNodeInPattern(node) + } + else if (node.type === 'VariableDeclarator') { + const parentFunction = findParentScope( + parentStack, + varKindStack[0] === 'var', + ) + if (parentFunction) + handlePattern(node.id, parentFunction) + } + else if (node.type === 'CatchClause' && node.param) { + handlePattern(node.param, node) + } + }, + + leave(node, parent) { + // untrack parent stack from above + if ( + parent + && !(parent.type === 'IfStatement' && node === parent.alternate) + ) + parentStack.shift() + + if (node.type === 'VariableDeclaration') + varKindStack.shift() + }, + }) + + // emit the identifier events in BFS so the hoisted declarations + // can be captured correctly + identifiers.forEach(([node, stack]) => { + if (!isInScope(node.name, stack)) { + const parent = stack[0] + const grandparent = stack[1] + const hasBindingShortcut = isStaticProperty(parent) && parent.shorthand + && (!isNodeInPattern(parent) || isInDestructuringAssignment(parent, parentStack)) + + const classDeclaration = (parent.type === 'PropertyDefinition' + && grandparent?.type === 'ClassBody') || (parent.type === 'ClassDeclaration' && node === parent.superClass) + + const classExpression = parent.type === 'ClassExpression' && node === parent.id + + onIdentifier?.(node, { + hasBindingShortcut, + classDeclaration, + classExpression, + }, stack) + } + }) +} + +function isRefIdentifier(id: Identifier, parent: _Node, parentStack: _Node[]) { + // declaration id + if ( + parent.type === 'CatchClause' + || ((parent.type === 'VariableDeclarator' + || parent.type === 'ClassDeclaration') + && parent.id === id) + ) + return false + + if (isFunctionNode(parent)) { + // function declaration/expression id + if ((parent as any).id === id) + return false + + // params list + if (parent.params.includes(id)) + return false + } + + // class method name + if (parent.type === 'MethodDefinition' && !parent.computed) + return false + + // property key + if (isStaticPropertyKey(id, parent)) + return false + + // object destructuring pattern + if (isNodeInPattern(parent) && parent.value === id) + return false + + // non-assignment array destructuring pattern + if ( + parent.type === 'ArrayPattern' + && !isInDestructuringAssignment(parent, parentStack) + ) + return false + + // member expression property + if ( + parent.type === 'MemberExpression' + && parent.property === id + && !parent.computed + ) + return false + + if (parent.type === 'ExportSpecifier') + return false + + // is a special keyword but parsed as identifier + if (id.name === 'arguments') + return false + + return true +} + +export function isStaticProperty(node: _Node): node is Property { + return node && node.type === 'Property' && !node.computed +} + +export function isStaticPropertyKey(node: _Node, parent: _Node) { + return isStaticProperty(parent) && parent.key === node +} + +const functionNodeTypeRE = /Function(?:Expression|Declaration)$|Method$/ +export function isFunctionNode(node: _Node): node is FunctionNode { + return functionNodeTypeRE.test(node.type) +} + +const blockNodeTypeRE = /^BlockStatement$|^For(?:In|Of)?Statement$/ +function isBlock(node: _Node) { + return blockNodeTypeRE.test(node.type) +} + +function findParentScope( + parentStack: _Node[], + isVar = false, +): _Node | undefined { + return parentStack.find(isVar ? isFunctionNode : isBlock) +} + +export function isInDestructuringAssignment( + parent: _Node, + parentStack: _Node[], +): boolean { + if ( + parent + && (parent.type === 'Property' || parent.type === 'ArrayPattern') + ) + return parentStack.some(i => i.type === 'AssignmentExpression') + + return false +} diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index c68f2c989c80..ec507afb7eae 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -66,6 +66,7 @@ const external = [ 'vite-node/constants', 'vite-node/utils', '@vitest/utils/diff', + '@vitest/utils/ast', '@vitest/utils/error', '@vitest/utils/source-map', '@vitest/runner/utils', diff --git a/packages/vitest/src/node/hoistMocks.ts b/packages/vitest/src/node/hoistMocks.ts index b0e0371cccb2..d279196ba3c3 100644 --- a/packages/vitest/src/node/hoistMocks.ts +++ b/packages/vitest/src/node/hoistMocks.ts @@ -1,7 +1,8 @@ import MagicString from 'magic-string' -import type { CallExpression, Identifier, ImportDeclaration, ImportNamespaceSpecifier, VariableDeclaration, Node as _Node } from 'estree' -import { findNodeAround, simple as simpleWalk } from 'acorn-walk' +import type { CallExpression, Identifier, ImportDeclaration, VariableDeclaration, Node as _Node } from 'estree' +import { findNodeAround } from 'acorn-walk' import type { PluginContext } from 'rollup' +import { esmWalker } from '@vitest/utils/ast' export type Positioned = T & { start: number @@ -45,6 +46,15 @@ function transformImportSpecifiers(node: ImportDeclaration) { return `{ ${dynamicImports} }` } +export function getBetterEnd(code: string, node: Node) { + let end = node.end + if (code[node.end] === ';') + end += 1 + if (code[node.end + 1] === '\n') + end += 1 + return end +} + const regexpHoistable = /^[ \t]*\b(vi|vitest)\s*\.\s*(mock|unmock|hoisted)\(/m const regexpAssignedHoisted = /=[ \t]*(\bawait|)[ \t]*\b(vi|vitest)\s*\.\s*hoisted\(/ const hashbangRE = /^#!.*\n/ @@ -71,47 +81,42 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse let hoistedCode = '' let hoistedVitestImports = '' - // this will tranfrom import statements into dynamic ones, if there are imports + let uid = 0 + const idToImportMap = new Map() + + // 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 namespace = node.specifiers.find(specifier => specifier.type === 'ImportNamespaceSpecifier') as ImportNamespaceSpecifier | undefined - - let code = '' - if (namespace) - code += `const ${namespace.local.name} = await import('${source}')\n` - - // if we don't hijack ESM and process this file, then we definetly have mocks, - // so we need to transform imports into dynamic ones, so "vi.mock" can be executed before - const specifiers = transformImportSpecifiers(node) - - if (specifiers) { - if (namespace) - code += `const ${specifiers} = ${namespace.local.name}\n` - else - code += `const ${specifiers} = await import('${source}')\n` + 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, } - else if (!namespace) { - code += `await import('${source}')\n` - } - return code } - function hoistImport(node: Positioned) { + function defineImport(node: Positioned) { // always hoist vitest import to top of the file, so // "vi" helpers can access it - s.remove(node.start, node.end) - if (node.source.value === 'vitest') { const code = `const ${transformImportSpecifiers(node)} = await import('vitest')\n` hoistedVitestImports += code + s.remove(node.start, getBetterEnd(code, node)) return } - const code = transformImportDeclaration(node) - s.appendLeft(hoistIndex, code) + + const declaration = transformImportDeclaration(node) + if (!declaration) + return null + s.appendLeft(hoistIndex, declaration.code) + return declaration.id } // 1. check all import statements and record id -> importName map @@ -119,13 +124,58 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse // import foo from 'foo' --> foo -> __import_foo__.default // import { baz } from 'foo' --> baz -> __import_foo__.baz // import * as ok from 'foo' --> ok -> __import_foo__ - if (node.type === 'ImportDeclaration') - hoistImport(node) + if (node.type === 'ImportDeclaration') { + const importId = defineImport(node) + 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}.${spec.imported.name}`, + ) + } + else if (spec.type === 'ImportDefaultSpecifier') { + idToImportMap.set(spec.local.name, `${importId}.default`) + } + else { + // namespace specifier + idToImportMap.set(spec.local.name, importId) + } + } + } } - simpleWalk(ast, { - CallExpression(_node) { - const node = _node as any as Positioned + const declaredConst = new Set() + + esmWalker(ast, { + onIdentifier(id, info, parentStack) { + const binding = idToImportMap.get(id.name) + if (!binding) + return + + if (info.hasBindingShortcut) { + s.appendLeft(id.end, `: ${binding}`) + } + else if ( + info.classDeclaration + ) { + if (!declaredConst.has(id.name)) { + declaredConst.add(id.name) + // locate the top-most node containing the class declaration + const topNode = parentStack[parentStack.length - 2] + s.prependRight(topNode.start, `const ${id.name} = ${binding};\n`) + } + } + else if ( + // don't transform class name identifier + !info.classExpression + ) { + s.update(id.start, id.end, binding) + } + }, + onCallExpression(node) { if ( node.callee.type === 'MemberExpression' && isIdentifier(node.callee.object) @@ -135,8 +185,10 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse const methodName = node.callee.property.name if (methodName === 'mock' || methodName === 'unmock') { - hoistedCode += `${code.slice(node.start, node.end)}\n` - s.remove(node.start, node.end) + const end = getBetterEnd(code, node) + const nodeCode = code.slice(node.start, end) + hoistedCode += `${nodeCode}${nodeCode.endsWith('\n') ? '' : '\n'}` + s.remove(node.start, end) } if (methodName === 'hoisted') { @@ -160,13 +212,17 @@ export function hoistMocks(code: string, id: string, parse: PluginContext['parse if (canMoveDeclaration) { // hoist "const variable = vi.hoisted(() => {})" - hoistedCode += `${code.slice(declarationNode.start, declarationNode.end)}\n` - s.remove(declarationNode.start, declarationNode.end) + const end = getBetterEnd(code, declarationNode) + const nodeCode = code.slice(declarationNode.start, end) + hoistedCode += `${nodeCode}${nodeCode.endsWith('\n') ? '' : '\n'}` + s.remove(declarationNode.start, end) } else { // hoist "vi.hoisted(() => {})" - hoistedCode += `${code.slice(node.start, node.end)}\n` - s.remove(node.start, node.end) + const end = getBetterEnd(code, node) + const nodeCode = code.slice(node.start, end) + hoistedCode += `${nodeCode}${nodeCode.endsWith('\n') ? '' : '\n'}` + s.remove(node.start, end) } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e5d783c641e..ae3d4523d6e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -865,9 +865,9 @@ importers: packages/browser: dependencies: - estree-walker: - specifier: ^3.0.3 - version: 3.0.3 + '@vitest/utils': + specifier: workspace:* + version: link:../utils magic-string: specifier: ^0.30.5 version: 0.30.5 @@ -875,9 +875,6 @@ importers: specifier: ^2.0.4 version: 2.0.4 devDependencies: - '@types/estree': - specifier: ^1.0.5 - version: 1.0.5 '@types/ws': specifier: ^8.5.9 version: 8.5.9 @@ -1216,6 +1213,9 @@ importers: diff-sequences: specifier: ^29.6.3 version: 29.6.3 + estree-walker: + specifier: ^3.0.3 + version: 3.0.3 loupe: specifier: ^2.3.7 version: 2.3.7 @@ -1226,6 +1226,9 @@ importers: '@jridgewell/trace-mapping': specifier: ^0.3.20 version: 0.3.20 + '@types/estree': + specifier: ^1.0.5 + version: 1.0.5 packages/vite-node: dependencies: diff --git a/test/core/test/injector-mock.test.ts b/test/core/test/injector-mock.test.ts index ca365d221c51..705c6e9a2c09 100644 --- a/test/core/test/injector-mock.test.ts +++ b/test/core/test/injector-mock.test.ts @@ -1,11 +1,16 @@ import { parseAst } from 'rollup/parseAst' import { expect, test } from 'vitest' +import { describe } from 'node:test' import { hoistMocks } from '../../../packages/vitest/src/node/hoistMocks' function parse(code: string, options: any) { return parseAst(code, options) } +async function hoistSimple(code: string, url = '') { + return hoistMocks(code, url, parse) +} + function hoistSimpleCode(code: string) { return hoistMocks(code, '/test.js', parse)?.code.trim() } @@ -54,8 +59,8 @@ test('always hoists all imports but they are under mocks', () => { vi.mock('path', () => {}) vi.unmock('path') vi.hoisted(() => {}) - const { someValue } = await import('./path.js') - const { someValue2 } = await import('./path2.js')" + const __vi_import_0__ = await import('./path.js') + const __vi_import_1__ = await import('./path2.js')" `) }) @@ -67,7 +72,1056 @@ test('correctly mocks namespaced', () => { `)).toMatchInlineSnapshot(` "const { vi } = await import('vitest') vi.mock('../src/add', () => {}) - const AddModule = await import('../src/add') - const { default: add } = AddModule" + const __vi_import_0__ = await import('../src/add')" + `) +}) + +test('correctly access import', () => { + expect(hoistSimpleCode(` + import { vi } from 'vitest' + import add from '../src/add' + add(); + vi.mock('../src/add', () => {}) + `)).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('../src/add', () => {}) + const __vi_import_0__ = await import('../src/add') + + + + __vi_import_0__.default();" `) }) + +describe('transform', () => { + const hoistSimpleCodeWithoutMocks = (code: string) => { + return hoistMocks(`import {vi} from "vitest";\n${code}\nvi.mock('faker');`, '/test.js', parse)?.code.trim() + } + test('default import', async () => { + expect( + await hoistSimpleCodeWithoutMocks(`import foo from 'vue';console.log(foo.bar)`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + console.log(__vi_import_0__.default.bar)" + `) + }) + + test('named import', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `import { ref } from 'vue';function foo() { return ref(0) }`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + function foo() { return __vi_import_0__.ref(0) }" + `) + }) + + test('namespace import', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `import * as vue from 'vue';function foo() { return vue.ref(0) }`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + function foo() { return __vi_import_0__.ref(0) }" + `) + }) + + test('export function declaration', async () => { + expect(await hoistSimpleCodeWithoutMocks(`export function foo() {}`)) + .toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export function foo() {}" + `) + }) + + test('export class declaration', async () => { + expect(await hoistSimpleCodeWithoutMocks(`export class foo {}`)) + .toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export class foo {}" + `) + }) + + test('export var declaration', async () => { + expect(await hoistSimpleCodeWithoutMocks(`export const a = 1, b = 2`)) + .toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export const a = 1, b = 2" + `) + }) + + test('export named', async () => { + expect( + await hoistSimpleCodeWithoutMocks(`const a = 1, b = 2; export { a, b as c }`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + const a = 1, b = 2; export { a, b as c }" + `) + }) + + test('export named from', async () => { + expect( + await hoistSimpleCodeWithoutMocks(`export { ref, computed as c } from 'vue'`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export { ref, computed as c } from 'vue'" + `) + }) + + test('named exports of imported binding', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `import {createApp} from 'vue';export {createApp}`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + export {createApp}" + `) + }) + + test('export * from', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `export * from 'vue'\n` + `export * from 'react'`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export * from 'vue' + export * from 'react'" + `) + }) + + test('export * as from', async () => { + expect(await hoistSimpleCodeWithoutMocks(`export * as foo from 'vue'`)) + .toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export * as foo from 'vue'" + `) + }) + + test('export default', async () => { + expect( + await hoistSimpleCodeWithoutMocks(`export default {}`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export default {}" + `) + }) + + test('export then import minified', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `export * from 'vue';import {createApp} from 'vue';`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + export * from 'vue';" + `) + }) + + test('hoist import to top', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `path.resolve('server.js');import path from 'node:path';`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('node:path') + + __vi_import_0__.default.resolve('server.js');" + `) + }) + + test('import.meta', async () => { + expect( + await hoistSimpleCodeWithoutMocks(`console.log(import.meta.url)`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + console.log(import.meta.url)" + `) + }) + + test('dynamic import', async () => { + const result = await hoistSimpleCodeWithoutMocks( + `export const i = () => import('./foo')`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export const i = () => import('./foo')" + `) + }) + + test('do not rewrite method definition', async () => { + const result = await hoistSimpleCodeWithoutMocks( + `import { fn } from 'vue';class A { fn() { fn() } }`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + class A { fn() { __vi_import_0__.fn() } }" + `) + }) + + test('do not rewrite when variable is in scope', async () => { + const result = await hoistSimpleCodeWithoutMocks( + `import { fn } from 'vue';function A(){ const fn = () => {}; return { fn }; }`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + function A(){ const fn = () => {}; return { fn }; }" + `) + }) + + // #5472 + test('do not rewrite when variable is in scope with object destructuring', async () => { + const result = await hoistSimpleCodeWithoutMocks( + `import { fn } from 'vue';function A(){ let {fn, test} = {fn: 'foo', test: 'bar'}; return { fn }; }`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + 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( + `import { fn } from 'vue';function A(){ let [fn, test] = ['foo', 'bar']; return { fn }; }`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + 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( + `import { fn } from 'vue';function A({foo = \`test\${fn}\`} = {}){ return {}; }`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + function A({foo = \`test\${__vi_import_0__.fn}\`} = {}){ return {}; }" + `) + }) + + // #6520 + test('rewrite variables in default value of destructuring params', async () => { + const result = await hoistSimpleCodeWithoutMocks( + `import { fn } from 'vue';function A({foo = fn}){ return {}; }`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + function A({foo = __vi_import_0__.fn}){ return {}; }" + `) + }) + + test('do not rewrite when function declaration is in scope', async () => { + const result = await hoistSimpleCodeWithoutMocks( + `import { fn } from 'vue';function A(){ function fn() {}; return { fn }; }`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + function A(){ function fn() {}; return { fn }; }" + `) + }) + + test('do not rewrite catch clause', async () => { + const result = await hoistSimpleCodeWithoutMocks( + `import {error} from './dependency';try {} catch(error) {}`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('./dependency') + + try {} catch(error) {}" + `) + }) + + // #2221 + test('should declare variable for imported super class', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `import { Foo } from './dependency';` + `class A extends Foo {}`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('./dependency') + + const Foo = __vi_import_0__.Foo; + class A extends Foo {}" + `) + + // exported classes: should prepend the declaration at root level, before the + // first class that uses the binding + expect( + await hoistSimpleCodeWithoutMocks( + `import { Foo } from './dependency';` + + `export default class A extends Foo {}\n` + + `export class B extends Foo {}`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('./dependency') + + const Foo = __vi_import_0__.Foo; + export default class A extends Foo {} + export class B extends Foo {}" + `) + }) + + // #4049 + test('should handle default export variants', async () => { + // default anonymous functions + expect(await hoistSimpleCodeWithoutMocks(`export default function() {}\n`)) + .toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export default function() {}" + `) + // default anonymous class + expect(await hoistSimpleCodeWithoutMocks(`export default class {}\n`)) + .toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export default class {}" + `) + // default named functions + expect( + await hoistSimpleCodeWithoutMocks( + `export default function foo() {}\n` + + `foo.prototype = Object.prototype;`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export default function foo() {} + foo.prototype = Object.prototype;" + `) + // default named classes + expect( + await hoistSimpleCodeWithoutMocks( + `export default class A {}\n` + `export class B extends A {}`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export default class A {} + export class B extends A {}" + `) + }) + + test('sourcemap source', async () => { + const map = ( + (await hoistSimple( + `vi.mock(any); + export const a = 1`, + 'input.js', + ))?.map + ) + expect(map?.sources).toStrictEqual(['input.js']) + }) + + test('overwrite bindings', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `import { inject } from 'vue';` + + `const a = { inject }\n` + + `const b = { test: inject }\n` + + `function c() { const { test: inject } = { test: true }; console.log(inject) }\n` + + `const d = inject\n` + + `function f() { console.log(inject) }\n` + + `function e() { const { inject } = { inject: true } }\n` + + `function g() { const f = () => { const inject = true }; console.log(inject) }\n`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + const a = { inject: __vi_import_0__.inject } + const b = { test: __vi_import_0__.inject } + function c() { const { test: inject } = { test: true }; console.log(inject) } + const d = __vi_import_0__.inject + function f() { console.log(__vi_import_0__.inject) } + function e() { const { inject } = { inject: true } } + function g() { const f = () => { const inject = true }; console.log(__vi_import_0__.inject) }" + `) + }) + + test('Empty array pattern', async () => { + expect( + await hoistSimpleCodeWithoutMocks(`const [, LHS, RHS] = inMatch;`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + const [, LHS, RHS] = inMatch;" + `) + }) + + test('function argument destructure', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + ` +import { foo, bar } from 'foo' +const a = ({ _ = foo() }) => {} +function b({ _ = bar() }) {} +function c({ _ = bar() + foo() }) {} +`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('foo') + + + + const a = ({ _ = __vi_import_0__.foo() }) => {} + function b({ _ = __vi_import_0__.bar() }) {} + function c({ _ = __vi_import_0__.bar() + __vi_import_0__.foo() }) {}" + `) + }) + + test('object destructure alias', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + ` +import { n } from 'foo' +const a = () => { + const { type: n = 'bar' } = {} + console.log(n) +} +`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('foo') + + + + const a = () => { + const { type: n = 'bar' } = {} + console.log(n) + }" + `) + + // #9585 + expect( + await hoistSimpleCodeWithoutMocks( + ` +import { n, m } from 'foo' +const foo = {} + +{ + const { [n]: m } = foo +} +`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('foo') + + + + const foo = {} + + { + const { [__vi_import_0__.n]: m } = foo + }" + `) + }) + + test('nested object destructure alias', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + ` +import { remove, add, get, set, rest, objRest } from 'vue' + +function a() { + const { + o: { remove }, + a: { b: { c: [ add ] }}, + d: [{ get }, set, ...rest], + ...objRest + } = foo + + remove() + add() + get() + set() + rest() + objRest() +} + +remove() +add() +get() +set() +rest() +objRest() +`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + + + function a() { + const { + o: { remove }, + a: { b: { c: [ add ] }}, + d: [{ get }, set, ...rest], + ...objRest + } = foo + + remove() + add() + get() + set() + rest() + objRest() + } + + __vi_import_0__.remove() + __vi_import_0__.add() + __vi_import_0__.get() + __vi_import_0__.set() + __vi_import_0__.rest() + __vi_import_0__.objRest()" + `) + }) + + test('object props and methods', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + ` +import foo from 'foo' + +const bar = 'bar' + +const obj = { + foo() {}, + [foo]() {}, + [bar]() {}, + foo: () => {}, + [foo]: () => {}, + [bar]: () => {}, + bar(foo) {} +} +`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('foo') + + + + const bar = 'bar' + + const obj = { + foo() {}, + [__vi_import_0__.default]() {}, + [bar]() {}, + foo: () => {}, + [__vi_import_0__.default]: () => {}, + [bar]: () => {}, + bar(foo) {} + }" + `) + }) + + test('class props', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + ` +import { remove, add } from 'vue' + +class A { + remove = 1 + add = null +} +`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + + + const add = __vi_import_0__.add; + const remove = __vi_import_0__.remove; + class A { + remove = 1 + add = null + }" + `) + }) + + test('class methods', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + ` +import foo from 'foo' + +const bar = 'bar' + +class A { + foo() {} + [foo]() {} + [bar]() {} + #foo() {} + bar(foo) {} +} +`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('foo') + + + + const bar = 'bar' + + class A { + foo() {} + [__vi_import_0__.default]() {} + [bar]() {} + #foo() {} + bar(foo) {} + }" + `) + }) + + test('declare scope', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + ` +import { aaa, bbb, ccc, ddd } from 'vue' + +function foobar() { + ddd() + + const aaa = () => { + bbb(ccc) + ddd() + } + const bbb = () => { + console.log('hi') + } + const ccc = 1 + function ddd() {} + + aaa() + bbb() + ccc() +} + +aaa() +bbb() +`, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('vue') + + + + function foobar() { + ddd() + + const aaa = () => { + bbb(ccc) + ddd() + } + const bbb = () => { + console.log('hi') + } + const ccc = 1 + function ddd() {} + + aaa() + bbb() + ccc() + } + + __vi_import_0__.aaa() + __vi_import_0__.bbb()" + `) + }) + + test('continuous exports', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + ` +export function fn1() { +}export function fn2() { +} + `, + ), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + + export function fn1() { + }export function fn2() { + }" + `) + }) + + // https://github.com/vitest-dev/vitest/issues/1141 + test('export default expression', async () => { + // esbuild transform result of following TS code + // export default function getRandom() { + // return Math.random() + // } + const code = ` +export default (function getRandom() { + return Math.random(); +}); +`.trim() + + expect(await hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export default (function getRandom() { + return Math.random(); + });" + `) + + expect( + await hoistSimpleCodeWithoutMocks(`export default (class A {});`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + + export default (class A {});" + `) + }) + + // #8002 + test('with hashbang', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `#!/usr/bin/env node +console.log("it can parse the hashbang")`, + ), + ).toMatchInlineSnapshot(`undefined`) + }) + + test('import hoisted after hashbang', async () => { + expect( + await hoistSimpleCodeWithoutMocks( + `#!/usr/bin/env node +console.log(foo); +import foo from "foo"`, + ), + ).toMatchInlineSnapshot(`undefined`) + }) + + // #10289 + test('track scope by class, function, condition blocks', async () => { + const code = ` +import { foo, bar } from 'foobar' +if (false) { + const foo = 'foo' + console.log(foo) +} else if (false) { + const [bar] = ['bar'] + console.log(bar) +} else { + console.log(foo) + console.log(bar) +} +export class Test { + constructor() { + if (false) { + const foo = 'foo' + console.log(foo) + } else if (false) { + const [bar] = ['bar'] + console.log(bar) + } else { + console.log(foo) + console.log(bar) + } + } +};`.trim() + + expect(await hoistSimpleCodeWithoutMocks(code)).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('foobar') + + + if (false) { + const foo = 'foo' + console.log(foo) + } else if (false) { + const [bar] = ['bar'] + console.log(bar) + } else { + console.log(__vi_import_0__.foo) + console.log(__vi_import_0__.bar) + } + export class Test { + constructor() { + if (false) { + const foo = 'foo' + console.log(foo) + } else if (false) { + const [bar] = ['bar'] + console.log(bar) + } else { + console.log(__vi_import_0__.foo) + console.log(__vi_import_0__.bar) + } + } + };" + `) + }) + + // #10386 + test('track var scope by function', async () => { + expect( + await hoistSimpleCodeWithoutMocks(` +import { foo, bar } from 'foobar' +function test() { + if (true) { + var foo = () => { var why = 'would' }, bar = 'someone' + } + return [foo, bar] +}`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('foobar') + + + + function test() { + if (true) { + var foo = () => { var why = 'would' }, bar = 'someone' + } + return [foo, bar] + }" + `) + }) + + // #11806 + test('track scope by blocks', async () => { + expect( + await hoistSimpleCodeWithoutMocks(` +import { foo, bar, baz } from 'foobar' +function test() { + [foo]; + { + let foo = 10; + let bar = 10; + } + try {} catch (baz){ baz }; + return bar; +}`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('foobar') + + + + function test() { + [__vi_import_0__.foo]; + { + let foo = 10; + let bar = 10; + } + try {} catch (baz){ baz }; + return __vi_import_0__.bar; + }" + `) + }) + + test('track scope in for loops', async () => { + expect( + await hoistSimpleCodeWithoutMocks(` +import { test } from './test.js' + +for (const test of tests) { + console.log(test) +} + +for (let test = 0; test < 10; test++) { + console.log(test) +} + +for (const test in tests) { + console.log(test) +}`), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('./test.js') + + + + for (const test of tests) { + console.log(test) + } + + for (let test = 0; test < 10; test++) { + console.log(test) + } + + for (const test in tests) { + console.log(test) + }" + `) + }) + + test('avoid binding ClassExpression', async () => { + const result = await hoistSimpleCodeWithoutMocks( + ` +import Foo, { Bar } from './foo'; + +console.log(Foo, Bar); +const obj = { + foo: class Foo {}, + bar: class Bar {} +} +const Baz = class extends Foo {} +`, + ) + expect(result).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('./foo') + + + + console.log(__vi_import_0__.default, __vi_import_0__.Bar); + const obj = { + foo: class Foo {}, + bar: class Bar {} + } + const Baz = class extends __vi_import_0__.default {}" + `) + }) + + test('import assertion attribute', async () => { + expect( + await hoistSimpleCodeWithoutMocks(` + import * as foo from './foo.json' with { type: 'json' }; + import('./bar.json', { with: { type: 'json' } }); + `), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('./foo.json') + + + + import('./bar.json', { with: { type: 'json' } });" + `) + }) + + test('import and export ordering', async () => { + // 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(` +console.log(foo + 1) +export * from './a' +import { foo } from './foo' +export * from './b' +console.log(foo + 2) + `), + ).toMatchInlineSnapshot(` + "const { vi } = await import('vitest') + vi.mock('faker'); + const __vi_import_0__ = await import('./foo') + + + console.log(__vi_import_0__.foo + 1) + export * from './a' + + export * from './b' + console.log(__vi_import_0__.foo + 2)" + `) + }) +})