Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250import MagicString from 'magic-string'import { extract_names as extractNames } from 'periscopic'import type { PluginContext } from 'rollup'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 overwriteconst viExportAllHelper = '__vitest_browser_runner__.exportAll'
const skipHijack = [ '/@vite/client', '/@vite/env', /vite\/dist\/client/,]
// this is basically copypaste from Vite SSR// this method transforms all import and export statements into `__vi_injected__` variable// to allow spying on them. this can be disabled by setting `slowHijackESM` to `false`export function injectVitestModule(code: string, id: string, parse: PluginContext['parse']) { if (skipHijack.some(skip => id.match(skip))) return
const s = new MagicString(code)
let ast: any try { ast = parse(code) } catch (err) { console.error(`Cannot parse ${id}:\n${(err as any).message}`) return }
let uid = 0 const idToImportMap = new Map<string, string>() const declaredConst = new Set<string>()
const hoistIndex = 0
// 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
if (skipHijack.some(skip => source.match(skip))) return null
const importId = `__vi_esm_${uid++}__` const hasSpecifiers = node.specifiers.length > 0 const code = hasSpecifiers ? `import { ${viInjectedKey} as ${importId} } from '${source}'\n` : `import '${source}'\n` return { code, id: importId, } }
function defineImport(node: ImportDeclaration) { const declaration = transformImportDeclaration(node) if (!declaration) return null s.appendLeft(hoistIndex, declaration.code) return declaration.id }
function defineImportAll(source: string) { const importId = `__vi_esm_${uid++}__` s.appendLeft(hoistIndex, `const { ${viInjectedKey}: ${importId} } = await import(${JSON.stringify(source)});\n`) return importId }
function defineExport(position: number, name: string, local = name) { s.appendLeft( position, `\nObject.defineProperty(${viInjectedKey}, "${name}", ` + `{ enumerable: true, configurable: true, get(){ return ${local} }});`, ) }
// 1. check all import statements and record id -> importName map for (const node of ast.body as Node[]) { // 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') { const importId = defineImport(node) if (!importId) continue s.remove(node.start, node.end) 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) } } } }
// 2. check all export statements and define exports for (const node of ast.body as Node[]) { // named exports if (node.type === 'ExportNamedDeclaration') { if (node.declaration) { if ( node.declaration.type === 'FunctionDeclaration' || node.declaration.type === 'ClassDeclaration' ) { // export function foo() {} defineExport(node.end, node.declaration.id!.name) } else { // export const foo = 1, bar = 2 for (const declaration of node.declaration.declarations) { const names = extractNames(declaration.id as any) for (const name of names) defineExport(node.end, name) } } s.remove(node.start, (node.declaration as Node).start) } else { s.remove(node.start, node.end) if (node.source) { // export { foo, bar } from './foo' const importId = defineImportAll(node.source.value as string) // hoist re-exports near the defined import so they are immediately exported for (const spec of node.specifiers) { defineExport( hoistIndex, spec.exported.name, `${importId}.${spec.local.name}`, ) } } else { // export { foo, bar } for (const spec of node.specifiers) { const local = spec.local.name const binding = idToImportMap.get(local) defineExport(node.end, spec.exported.name, binding || local) } } } }
// default export if (node.type === 'ExportDefaultDeclaration') { const expressionTypes = ['FunctionExpression', 'ClassExpression'] if ( 'id' in node.declaration && node.declaration.id && !expressionTypes.includes(node.declaration.type) ) { // named hoistable/class exports // export default function foo() {} // export default class A {} const { name } = node.declaration.id s.remove(node.start, node.start + 15 /* 'export default '.length */) s.append( `\nObject.defineProperty(${viInjectedKey}, "default", ` + `{ enumerable: true, configurable: true, value: ${name} });`, ) } else { // anonymous default exports s.update( node.start, node.start + 14 /* 'export default'.length */, `${viInjectedKey}.default =`, ) // keep export default for optimized dependencies s.append(`\nexport default { ${viInjectedKey}: ${viInjectedKey}.default };\n`) } }
// export * from './foo' if (node.type === 'ExportAllDeclaration') { s.remove(node.start, node.end) const importId = defineImportAll(node.source.value as string) // hoist re-exports near the defined import so they are immediately exported if (node.exported) defineExport(hoistIndex, node.exported.name, `${importId}`) else s.appendLeft(hoistIndex, `${viExportAllHelper}(${viInjectedKey}, ${importId});\n`) } }
// 3. convert references to import bindings & import.meta references 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) } }, // TODO: make env updatable onImportMeta() { // s.update(node.start, node.end, viImportMetaKey) }, onDynamicImport(node) { const replace = '__vitest_browser_runner__.wrapModule(import(' s.overwrite(node.start, (node.source as Positioned<Expression>).start, replace) s.overwrite(node.end - 1, node.end, '))') }, })
// make sure "__vi_injected__" is declared as soon as possible // prepend even if file doesn't export anything s.prepend(`const ${viInjectedKey} = { [Symbol.toStringTag]: "Module" };\n`) s.append(`\nexport { ${viInjectedKey} }`)
return { ast, code: s.toString(), map: s.generateMap({ hires: 'boundary', source: id }), }}