vite
Version:
Native-ESM powered web dev build tool
442 lines (405 loc) • 12.9 kB
text/typescript
import MagicString from 'magic-string'
import { SourceMap } from 'rollup'
import { TransformResult } from '../server/transformRequest'
import { parser } from '../server/pluginContainer'
import {
Identifier,
Node as _Node,
Property,
Function as FunctionNode
} from 'estree'
import { extract_names as extractNames } from 'periscopic'
import { walk as eswalk } from 'estree-walker'
import { combineSourcemaps } from '../utils'
import { RawSourceMap } from '@ampproject/remapping/dist/types/types'
type Node = _Node & {
start: number
end: number
}
export const ssrModuleExportsKey = `__vite_ssr_exports__`
export const ssrImportKey = `__vite_ssr_import__`
export const ssrDynamicImportKey = `__vite_ssr_dynamic_import__`
export const ssrExportAllKey = `__vite_ssr_exportAll__`
export const ssrImportMetaKey = `__vite_ssr_import_meta__`
export async function ssrTransform(
code: string,
inMap: SourceMap | null,
url: string
): Promise<TransformResult | null> {
const s = new MagicString(code)
const ast = parser.parse(code, {
sourceType: 'module',
ecmaVersion: 2021,
locations: true
}) as any
let uid = 0
const deps = new Set<string>()
const idToImportMap = new Map<string, string>()
const declaredConst = new Set<string>()
function defineImport(node: Node, source: string) {
deps.add(source)
const importId = `__vite_ssr_import_${uid++}__`
s.appendLeft(
node.start,
`const ${importId} = ${ssrImportKey}(${JSON.stringify(source)})\n`
)
return importId
}
function defineExport(name: string, local = name) {
s.append(
`\nObject.defineProperty(${ssrModuleExportsKey}, "${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, node.source.value as string)
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)
}
}
s.remove(node.start, node.end)
}
}
// 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.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(name)
}
}
}
s.remove(node.start, (node.declaration as Node).start)
} else if (node.source) {
// export { foo, bar } from './foo'
const importId = defineImport(node, node.source.value as string)
for (const spec of node.specifiers) {
defineExport(spec.exported.name, `${importId}.${spec.local.name}`)
}
s.remove(node.start, node.end)
} else {
// export { foo, bar }
for (const spec of node.specifiers) {
const local = spec.local.name
const binding = idToImportMap.get(local)
defineExport(spec.exported.name, binding || local)
}
s.remove(node.start, node.end)
}
}
// default export
if (node.type === 'ExportDefaultDeclaration') {
if ('id' in node.declaration && node.declaration.id) {
// 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(${ssrModuleExportsKey}, "default", ` +
`{ enumerable: true, value: ${name} })`
)
} else {
// anonymous default exports
s.overwrite(
node.start,
node.start + 14 /* 'export default'.length */,
`${ssrModuleExportsKey}.default =`
)
}
}
// export * from './foo'
if (node.type === 'ExportAllDeclaration') {
if ((node as any).exported) {
const importId = defineImport(node, node.source.value as string)
defineExport((node as any).exported.name, `${importId}`)
s.remove(node.start, node.end)
} else {
const importId = defineImport(node, node.source.value as string)
s.remove(node.start, node.end)
s.append(`\n${ssrExportAllKey}(${importId})`)
}
}
}
// 3. convert references to import bindings & import.meta references
walk(ast, {
onIdentifier(id, parent, 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 (
!(parent as any).inPattern ||
isInDestructuringAssignment(parent, parentStack)
) {
s.appendLeft(id.end, `: ${binding}`)
}
} else if (
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[1]
s.prependRight(topNode.start, `const ${id.name} = ${binding};\n`)
}
} else {
s.overwrite(id.start, id.end, binding)
}
},
onImportMeta(node) {
s.overwrite(node.start, node.end, ssrImportMetaKey)
},
onDynamicImport(node) {
s.overwrite(node.start, node.start + 6, ssrDynamicImportKey)
}
})
let map = s.generateMap({ hires: true })
if (inMap && inMap.mappings && inMap.sources.length > 0) {
map = combineSourcemaps(url, [
{
...map,
sources: inMap.sources,
sourcesContent: inMap.sourcesContent
} as RawSourceMap,
inMap as RawSourceMap
]) as SourceMap
} else {
map.sources = [url]
map.sourcesContent = [code]
}
return {
code: s.toString(),
map,
deps: [...deps]
}
}
interface Visitors {
onIdentifier: (
node: Identifier & {
start: number
end: number
},
parent: Node,
parentStack: Node[]
) => void
onImportMeta: (node: Node) => void
onDynamicImport: (node: Node) => void
}
/**
* Same logic from \@vue/compiler-core & \@vue/compiler-sfc
* Except this is using acorn AST
*/
function walk(
root: Node,
{ onIdentifier, onImportMeta, onDynamicImport }: Visitors
) {
const parentStack: Node[] = []
const scope: Record<string, number> = Object.create(null)
const scopeMap = new WeakMap<_Node, Set<string>>()
const setScope = (node: FunctionNode, name: string) => {
let scopeIds = scopeMap.get(node)
if (scopeIds && scopeIds.has(name)) {
return
}
if (name in scope) {
scope[name]++
} else {
scope[name] = 1
}
if (!scopeIds) {
scopeIds = new Set()
scopeMap.set(node, scopeIds)
}
scopeIds.add(name)
}
;(eswalk as any)(root, {
enter(node: Node, parent: Node | null) {
if (node.type === 'ImportDeclaration') {
return this.skip()
}
parent && parentStack.push(parent)
if (node.type === 'MetaProperty' && node.meta.name === 'import') {
onImportMeta(node)
} else if (node.type === 'ImportExpression') {
onDynamicImport(node)
}
if (node.type === 'Identifier') {
if (!scope[node.name] && isRefIdentifier(node, parent!, parentStack)) {
onIdentifier(node, parent!, parentStack)
}
} else if (isFunction(node)) {
// walk function expressions and add its arguments to known identifiers
// so that we don't prefix them
node.params.forEach((p) =>
(eswalk as any)(p, {
enter(child: Node, parent: Node) {
if (
child.type === 'Identifier' &&
// do not record as scope variable if is a destructuring key
!isStaticPropertyKey(child, parent) &&
// do not record if this is a default value
// assignment of a destructuring variable
!(
parent &&
parent.type === 'AssignmentPattern' &&
parent.right === child
)
) {
setScope(node, child.name)
}
}
})
)
} else if (node.type === 'Property' && parent!.type === 'ObjectPattern') {
// mark property in destructuring pattern
;(node as any).inPattern = true
} else if (node.type === 'VariableDeclarator') {
const parentFunction = findParentFunction(parentStack)
if (parentFunction) {
if (node.id.type === 'ObjectPattern') {
node.id.properties.forEach((property) => {
if (property.type === 'RestElement') {
setScope(parentFunction, (property.argument as Identifier).name)
} else {
setScope(parentFunction, (property.value as Identifier).name)
}
})
} else {
setScope(parentFunction, (node.id as Identifier).name)
}
}
}
},
leave(node: Node, parent: Node | null) {
parent && parentStack.pop()
const scopeIds = scopeMap.get(node)
if (scopeIds) {
scopeIds.forEach((id: string) => {
scope[id]--
if (scope[id] === 0) {
delete scope[id]
}
})
}
}
})
}
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 (isFunction(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') {
return false
}
// property key
// this also covers object destructuring pattern
if (isStaticPropertyKey(id, parent) || (parent as any).inPattern) {
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
}
const isStaticProperty = (node: _Node): node is Property =>
node && node.type === 'Property' && !node.computed
const isStaticPropertyKey = (node: _Node, parent: _Node) =>
isStaticProperty(parent) && parent.key === node
function isFunction(node: _Node): node is FunctionNode {
return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
}
function findParentFunction(parentStack: _Node[]): FunctionNode | undefined {
for (let i = parentStack.length - 1; i >= 0; i--) {
const node = parentStack[i]
if (isFunction(node)) {
return node
}
}
}
function isInDestructuringAssignment(
parent: _Node,
parentStack: _Node[]
): boolean {
if (
parent &&
(parent.type === 'Property' || parent.type === 'ArrayPattern')
) {
let i = parentStack.length
while (i--) {
const p = parentStack[i]
if (p.type === 'AssignmentExpression') {
return true
} else if (p.type !== 'Property' && !p.type.endsWith('Pattern')) {
break
}
}
}
return false
}