408 lines
11 KiB
TypeScript
408 lines
11 KiB
TypeScript
import {
|
|
RootNode,
|
|
NodeTypes,
|
|
ParentNode,
|
|
TemplateChildNode,
|
|
ElementNode,
|
|
DirectiveNode,
|
|
Property,
|
|
ExpressionNode,
|
|
createSimpleExpression,
|
|
JSChildNode,
|
|
SimpleExpressionNode,
|
|
ElementTypes,
|
|
ElementCodegenNode,
|
|
ComponentCodegenNode,
|
|
createCallExpression,
|
|
CacheExpression,
|
|
createCacheExpression
|
|
} from './ast'
|
|
import { isString, isArray } from '@vue/shared'
|
|
import { CompilerError, defaultOnError } from './errors'
|
|
import {
|
|
TO_STRING,
|
|
FRAGMENT,
|
|
helperNameMap,
|
|
WITH_DIRECTIVES,
|
|
CREATE_BLOCK,
|
|
CREATE_COMMENT
|
|
} from './runtimeHelpers'
|
|
import { isVSlot, createBlockExpression } from './utils'
|
|
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
|
|
|
|
// There are two types of transforms:
|
|
//
|
|
// - NodeTransform:
|
|
// Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
|
|
// replace or remove the node being processed.
|
|
export type NodeTransform = (
|
|
node: RootNode | TemplateChildNode,
|
|
context: TransformContext
|
|
) => void | (() => void) | (() => void)[]
|
|
|
|
// - DirectiveTransform:
|
|
// Transforms that handles a single directive attribute on an element.
|
|
// It translates the raw directive into actual props for the VNode.
|
|
export type DirectiveTransform = (
|
|
dir: DirectiveNode,
|
|
node: ElementNode,
|
|
context: TransformContext,
|
|
// a platform specific compiler can import the base transform and augment
|
|
// it by passing in this optional argument.
|
|
augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult
|
|
) => DirectiveTransformResult
|
|
|
|
export interface DirectiveTransformResult {
|
|
props: Property[]
|
|
needRuntime: boolean | symbol
|
|
}
|
|
|
|
// A structural directive transform is a technically a NodeTransform;
|
|
// Only v-if and v-for fall into this category.
|
|
export type StructuralDirectiveTransform = (
|
|
node: ElementNode,
|
|
dir: DirectiveNode,
|
|
context: TransformContext
|
|
) => void | (() => void)
|
|
|
|
export interface TransformOptions {
|
|
nodeTransforms?: NodeTransform[]
|
|
directiveTransforms?: { [name: string]: DirectiveTransform }
|
|
prefixIdentifiers?: boolean
|
|
hoistStatic?: boolean
|
|
cacheHandlers?: boolean
|
|
onError?: (error: CompilerError) => void
|
|
}
|
|
|
|
export interface TransformContext extends Required<TransformOptions> {
|
|
root: RootNode
|
|
helpers: Set<symbol>
|
|
components: Set<string>
|
|
directives: Set<string>
|
|
hoists: JSChildNode[]
|
|
cached: number
|
|
identifiers: { [name: string]: number | undefined }
|
|
scopes: {
|
|
vFor: number
|
|
vSlot: number
|
|
vPre: number
|
|
vOnce: number
|
|
}
|
|
parent: ParentNode | null
|
|
childIndex: number
|
|
currentNode: RootNode | TemplateChildNode | null
|
|
helper<T extends symbol>(name: T): T
|
|
helperString(name: symbol): string
|
|
replaceNode(node: TemplateChildNode): void
|
|
removeNode(node?: TemplateChildNode): void
|
|
onNodeRemoved(): void
|
|
addIdentifiers(exp: ExpressionNode | string): void
|
|
removeIdentifiers(exp: ExpressionNode | string): void
|
|
hoist(exp: JSChildNode): SimpleExpressionNode
|
|
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
|
|
}
|
|
|
|
function createTransformContext(
|
|
root: RootNode,
|
|
{
|
|
prefixIdentifiers = false,
|
|
hoistStatic = false,
|
|
cacheHandlers = false,
|
|
nodeTransforms = [],
|
|
directiveTransforms = {},
|
|
onError = defaultOnError
|
|
}: TransformOptions
|
|
): TransformContext {
|
|
const context: TransformContext = {
|
|
root,
|
|
helpers: new Set(),
|
|
components: new Set(),
|
|
directives: new Set(),
|
|
hoists: [],
|
|
cached: 0,
|
|
identifiers: {},
|
|
scopes: {
|
|
vFor: 0,
|
|
vSlot: 0,
|
|
vPre: 0,
|
|
vOnce: 0
|
|
},
|
|
prefixIdentifiers,
|
|
hoistStatic,
|
|
cacheHandlers,
|
|
nodeTransforms,
|
|
directiveTransforms,
|
|
onError,
|
|
parent: null,
|
|
currentNode: root,
|
|
childIndex: 0,
|
|
helper(name) {
|
|
context.helpers.add(name)
|
|
return name
|
|
},
|
|
helperString(name) {
|
|
return (
|
|
(context.prefixIdentifiers ? `` : `_`) +
|
|
helperNameMap[context.helper(name)]
|
|
)
|
|
},
|
|
replaceNode(node) {
|
|
/* istanbul ignore if */
|
|
if (__DEV__) {
|
|
if (!context.currentNode) {
|
|
throw new Error(`Node being replaced is already removed.`)
|
|
}
|
|
if (!context.parent) {
|
|
throw new Error(`Cannot replace root node.`)
|
|
}
|
|
}
|
|
context.parent!.children[context.childIndex] = context.currentNode = node
|
|
},
|
|
removeNode(node) {
|
|
if (__DEV__ && !context.parent) {
|
|
throw new Error(`Cannot remove root node.`)
|
|
}
|
|
const list = context.parent!.children
|
|
const removalIndex = node
|
|
? list.indexOf(node)
|
|
: context.currentNode
|
|
? context.childIndex
|
|
: -1
|
|
/* istanbul ignore if */
|
|
if (__DEV__ && removalIndex < 0) {
|
|
throw new Error(`node being removed is not a child of current parent`)
|
|
}
|
|
if (!node || node === context.currentNode) {
|
|
// current node removed
|
|
context.currentNode = null
|
|
context.onNodeRemoved()
|
|
} else {
|
|
// sibling node removed
|
|
if (context.childIndex > removalIndex) {
|
|
context.childIndex--
|
|
context.onNodeRemoved()
|
|
}
|
|
}
|
|
context.parent!.children.splice(removalIndex, 1)
|
|
},
|
|
onNodeRemoved: () => {},
|
|
addIdentifiers(exp) {
|
|
// identifier tracking only happens in non-browser builds.
|
|
if (!__BROWSER__) {
|
|
if (isString(exp)) {
|
|
addId(exp)
|
|
} else if (exp.identifiers) {
|
|
exp.identifiers.forEach(addId)
|
|
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
|
|
addId(exp.content)
|
|
}
|
|
}
|
|
},
|
|
removeIdentifiers(exp) {
|
|
if (!__BROWSER__) {
|
|
if (isString(exp)) {
|
|
removeId(exp)
|
|
} else if (exp.identifiers) {
|
|
exp.identifiers.forEach(removeId)
|
|
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
|
|
removeId(exp.content)
|
|
}
|
|
}
|
|
},
|
|
hoist(exp) {
|
|
context.hoists.push(exp)
|
|
return createSimpleExpression(
|
|
`_hoisted_${context.hoists.length}`,
|
|
false,
|
|
exp.loc,
|
|
true
|
|
)
|
|
},
|
|
cache(exp, isVNode = false) {
|
|
return createCacheExpression(++context.cached, exp, isVNode)
|
|
}
|
|
}
|
|
|
|
function addId(id: string) {
|
|
const { identifiers } = context
|
|
if (identifiers[id] === undefined) {
|
|
identifiers[id] = 0
|
|
}
|
|
identifiers[id]!++
|
|
}
|
|
|
|
function removeId(id: string) {
|
|
context.identifiers[id]!--
|
|
}
|
|
|
|
return context
|
|
}
|
|
|
|
export function transform(root: RootNode, options: TransformOptions) {
|
|
const context = createTransformContext(root, options)
|
|
traverseNode(root, context)
|
|
if (options.hoistStatic) {
|
|
hoistStatic(root, context)
|
|
}
|
|
finalizeRoot(root, context)
|
|
}
|
|
|
|
function finalizeRoot(root: RootNode, context: TransformContext) {
|
|
const { helper } = context
|
|
const { children } = root
|
|
const child = children[0]
|
|
if (children.length === 1) {
|
|
// if the single child is an element, turn it into a block.
|
|
if (isSingleElementRoot(root, child) && child.codegenNode) {
|
|
// single element root is never hoisted so codegenNode will never be
|
|
// SimpleExpressionNode
|
|
const codegenNode = child.codegenNode as
|
|
| ElementCodegenNode
|
|
| ComponentCodegenNode
|
|
| CacheExpression
|
|
if (codegenNode.type !== NodeTypes.JS_CACHE_EXPRESSION) {
|
|
if (codegenNode.callee === WITH_DIRECTIVES) {
|
|
codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
|
|
} else {
|
|
codegenNode.callee = helper(CREATE_BLOCK)
|
|
}
|
|
root.codegenNode = createBlockExpression(codegenNode, context)
|
|
} else {
|
|
root.codegenNode = codegenNode
|
|
}
|
|
} else {
|
|
// - single <slot/>, IfNode, ForNode: already blocks.
|
|
// - single text node: always patched.
|
|
// root codegen falls through via genNode()
|
|
root.codegenNode = child
|
|
}
|
|
} else if (children.length > 1) {
|
|
// root has multiple nodes - return a fragment block.
|
|
root.codegenNode = createBlockExpression(
|
|
createCallExpression(helper(CREATE_BLOCK), [
|
|
helper(FRAGMENT),
|
|
`null`,
|
|
root.children
|
|
]),
|
|
context
|
|
)
|
|
} else {
|
|
// no children = noop. codegen will return null.
|
|
}
|
|
// finalize meta information
|
|
root.helpers = [...context.helpers]
|
|
root.components = [...context.components]
|
|
root.directives = [...context.directives]
|
|
root.hoists = context.hoists
|
|
root.cached = context.cached
|
|
}
|
|
|
|
export function traverseChildren(
|
|
parent: ParentNode,
|
|
context: TransformContext
|
|
) {
|
|
let i = 0
|
|
const nodeRemoved = () => {
|
|
i--
|
|
}
|
|
for (; i < parent.children.length; i++) {
|
|
const child = parent.children[i]
|
|
if (isString(child)) continue
|
|
context.currentNode = child
|
|
context.parent = parent
|
|
context.childIndex = i
|
|
context.onNodeRemoved = nodeRemoved
|
|
traverseNode(child, context)
|
|
}
|
|
}
|
|
|
|
export function traverseNode(
|
|
node: RootNode | TemplateChildNode,
|
|
context: TransformContext
|
|
) {
|
|
// apply transform plugins
|
|
const { nodeTransforms } = context
|
|
const exitFns = []
|
|
for (let i = 0; i < nodeTransforms.length; i++) {
|
|
const onExit = nodeTransforms[i](node, context)
|
|
if (onExit) {
|
|
if (isArray(onExit)) {
|
|
exitFns.push(...onExit)
|
|
} else {
|
|
exitFns.push(onExit)
|
|
}
|
|
}
|
|
if (!context.currentNode) {
|
|
// node was removed
|
|
return
|
|
} else {
|
|
// node may have been replaced
|
|
node = context.currentNode
|
|
}
|
|
}
|
|
|
|
switch (node.type) {
|
|
case NodeTypes.COMMENT:
|
|
// inject import for the Comment symbol, which is needed for creating
|
|
// comment nodes with `createVNode`
|
|
context.helper(CREATE_COMMENT)
|
|
break
|
|
case NodeTypes.INTERPOLATION:
|
|
// no need to traverse, but we need to inject toString helper
|
|
context.helper(TO_STRING)
|
|
break
|
|
|
|
// for container types, further traverse downwards
|
|
case NodeTypes.IF:
|
|
for (let i = 0; i < node.branches.length; i++) {
|
|
traverseChildren(node.branches[i], context)
|
|
}
|
|
break
|
|
case NodeTypes.FOR:
|
|
case NodeTypes.ELEMENT:
|
|
case NodeTypes.ROOT:
|
|
traverseChildren(node, context)
|
|
break
|
|
}
|
|
|
|
// exit transforms
|
|
let i = exitFns.length
|
|
while (i--) {
|
|
exitFns[i]()
|
|
}
|
|
}
|
|
|
|
export function createStructuralDirectiveTransform(
|
|
name: string | RegExp,
|
|
fn: StructuralDirectiveTransform
|
|
): NodeTransform {
|
|
const matches = isString(name)
|
|
? (n: string) => n === name
|
|
: (n: string) => name.test(n)
|
|
|
|
return (node, context) => {
|
|
if (node.type === NodeTypes.ELEMENT) {
|
|
const { props } = node
|
|
// structural directive transforms are not concerned with slots
|
|
// as they are handled separately in vSlot.ts
|
|
if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
|
|
return
|
|
}
|
|
const exitFns = []
|
|
for (let i = 0; i < props.length; i++) {
|
|
const prop = props[i]
|
|
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
|
|
// structural directives are removed to avoid infinite recursion
|
|
// also we remove them *before* applying so that it can further
|
|
// traverse itself in case it moves the node around
|
|
props.splice(i, 1)
|
|
i--
|
|
const onExit = fn(node, prop, context)
|
|
if (onExit) exitFns.push(onExit)
|
|
}
|
|
}
|
|
return exitFns
|
|
}
|
|
}
|
|
}
|