mirror of
https://github.com/vitest-dev/vitest.git
synced 2026-01-18 16:31:32 +00:00
274 lines
9.0 KiB
TypeScript
274 lines
9.0 KiB
TypeScript
import MagicString from 'magic-string'
|
|
import { extract_names as extractNames } from 'periscopic'
|
|
import type { Expression, ImportDeclaration } from 'estree'
|
|
import type { AcornNode } from 'rollup'
|
|
import type { Node, Positioned } from './esmWalker'
|
|
import { esmWalker, isInDestructuringAssignment, isNodeInPattern, isStaticProperty } from './esmWalker'
|
|
|
|
const viInjectedKey = '__vi_inject__'
|
|
// const viImportMetaKey = '__vi_import_meta__' // to allow overwrite
|
|
const viExportAllHelper = '__vi_export_all__'
|
|
|
|
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: (code: string, options: any) => AcornNode) {
|
|
if (skipHijack.some(skip => id.match(skip)))
|
|
return
|
|
|
|
const s = new MagicString(code)
|
|
|
|
let ast: any
|
|
try {
|
|
ast = parse(code, {
|
|
sourceType: 'module',
|
|
ecmaVersion: 'latest',
|
|
locations: true,
|
|
})
|
|
}
|
|
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
|
|
|
|
let hasInjected = false
|
|
|
|
// this will tranfrom 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) {
|
|
hasInjected = true
|
|
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 {}
|
|
hasInjected = true
|
|
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
|
|
hasInjected = true
|
|
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 {
|
|
hasInjected = true
|
|
s.appendLeft(hoistIndex, `${viExportAllHelper}(${viInjectedKey}, ${importId});\n`)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. convert references to import bindings & import.meta references
|
|
esmWalker(ast, {
|
|
onIdentifier(id, parent, parentStack) {
|
|
const grandparent = parentStack[1]
|
|
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}`)
|
|
}
|
|
else if (
|
|
(parent.type === 'PropertyDefinition'
|
|
&& grandparent?.type === 'ClassBody')
|
|
|| (parent.type === 'ClassDeclaration' && id === parent.superClass)
|
|
) {
|
|
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
|
|
!(parent.type === 'ClassExpression' && id === parent.id)
|
|
) {
|
|
s.update(id.start, id.end, binding)
|
|
}
|
|
},
|
|
// TODO: make env updatable
|
|
onImportMeta() {
|
|
// s.update(node.start, node.end, viImportMetaKey)
|
|
},
|
|
onDynamicImport(node) {
|
|
const replace = '__vi_wrap_module__(import('
|
|
s.overwrite(node.start, (node.source as Positioned<Expression>).start, replace)
|
|
s.overwrite(node.end - 1, node.end, '))')
|
|
},
|
|
})
|
|
|
|
if (hasInjected) {
|
|
// make sure "__vi_injected__" is declared as soon as possible
|
|
s.prepend(`const ${viInjectedKey} = { [Symbol.toStringTag]: "Module" };\n`)
|
|
s.append(`\nexport { ${viInjectedKey} }`)
|
|
}
|
|
|
|
return {
|
|
ast,
|
|
code: s.toString(),
|
|
map: s.generateMap({ hires: 'boundary', source: id }),
|
|
}
|
|
}
|