vue3-yuanma/packages/compiler-core/src/transform.ts

261 lines
7.0 KiB
TypeScript
Raw Normal View History

import {
RootNode,
NodeTypes,
ParentNode,
ChildNode,
ElementNode,
2019-09-22 05:42:12 +08:00
DirectiveNode,
Property,
ExpressionNode,
createExpression,
JSChildNode
} from './ast'
import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
2019-09-25 03:49:02 +08:00
import { TO_STRING, COMMENT, CREATE_VNODE } from './runtimeConstants'
2019-09-22 05:42:12 +08:00
// 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: ChildNode,
context: TransformContext
) => void | (() => void) | (() => void)[]
2019-09-22 05:42:12 +08:00
// - 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 = (
2019-09-22 05:42:12 +08:00
dir: DirectiveNode,
context: TransformContext
) => {
props: Property | Property[]
needRuntime: boolean
}
// A structural directive transform is a techically 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 {
2019-09-22 05:42:12 +08:00
nodeTransforms?: NodeTransform[]
directiveTransforms?: { [name: string]: DirectiveTransform }
2019-09-24 01:29:41 +08:00
prefixIdentifiers?: boolean
onError?: (error: CompilerError) => void
}
2019-09-22 05:42:12 +08:00
export interface TransformContext extends Required<TransformOptions> {
2019-09-25 03:49:02 +08:00
root: RootNode
imports: Set<string>
statements: string[]
hoists: JSChildNode[]
2019-09-24 01:56:56 +08:00
identifiers: { [name: string]: number | undefined }
parent: ParentNode
childIndex: number
2019-09-20 03:41:17 +08:00
currentNode: ChildNode | null
helper(name: string): string
replaceNode(node: ChildNode): void
2019-09-20 03:41:17 +08:00
removeNode(node?: ChildNode): void
onNodeRemoved: () => void
addIdentifier(exp: ExpressionNode): void
removeIdentifier(exp: ExpressionNode): void
hoist(exp: JSChildNode): ExpressionNode
}
function createTransformContext(
root: RootNode,
{
2019-09-24 01:29:41 +08:00
prefixIdentifiers = false,
nodeTransforms = [],
directiveTransforms = {},
onError = defaultOnError
}: TransformOptions
): TransformContext {
const context: TransformContext = {
2019-09-25 03:49:02 +08:00
root,
imports: new Set(),
statements: [],
hoists: [],
identifiers: {},
2019-09-24 01:29:41 +08:00
prefixIdentifiers,
nodeTransforms,
directiveTransforms,
onError,
parent: root,
childIndex: 0,
2019-09-20 03:41:17 +08:00
currentNode: null,
helper(name) {
context.imports.add(name)
return prefixIdentifiers ? name : `_${name}`
},
replaceNode(node) {
2019-09-25 04:35:01 +08:00
/* istanbul ignore if */
2019-09-20 03:41:17 +08:00
if (__DEV__ && !context.currentNode) {
throw new Error(`node being replaced is already removed.`)
}
2019-09-20 03:41:17 +08:00
context.parent.children[context.childIndex] = context.currentNode = node
},
2019-09-20 03:41:17 +08:00
removeNode(node) {
const list = context.parent.children
const removalIndex = node
2019-09-23 14:52:54 +08:00
? list.indexOf(node as any)
2019-09-20 03:41:17 +08:00
: context.currentNode
? context.childIndex
: -1
2019-09-25 04:35:01 +08:00
/* istanbul ignore if */
2019-09-20 03:41:17 +08:00
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: () => {},
2019-09-24 01:56:56 +08:00
addIdentifier({ content }) {
const { identifiers } = context
if (identifiers[content] === undefined) {
identifiers[content] = 0
}
;(identifiers[content] as number)++
},
2019-09-24 01:56:56 +08:00
removeIdentifier({ content }) {
;(context.identifiers[content] as number)--
},
hoist(exp) {
context.hoists.push(exp)
return createExpression(
`_hoisted_${context.hoists.length}`,
false,
exp.loc
)
}
}
return context
}
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseChildren(root, context)
root.imports = [...context.imports]
root.statements = context.statements
root.hoists = context.hoists
}
export function traverseChildren(
parent: ParentNode,
context: TransformContext
) {
2019-09-20 03:41:17 +08:00
let i = 0
const nodeRemoved = () => {
i--
}
for (; i < parent.children.length; i++) {
2019-09-23 14:52:54 +08:00
const child = parent.children[i]
if (isString(child)) continue
context.currentNode = child
context.parent = parent
context.childIndex = i
2019-09-20 03:41:17 +08:00
context.onNodeRemoved = nodeRemoved
2019-09-23 14:52:54 +08:00
traverseNode(child, context)
}
}
export function traverseNode(node: ChildNode, context: TransformContext) {
// apply transform plugins
2019-09-22 05:42:12 +08:00
const { nodeTransforms } = context
const exitFns = []
2019-09-22 05:42:12 +08:00
for (let i = 0; i < nodeTransforms.length; i++) {
const plugin = nodeTransforms[i]
const onExit = plugin(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) {
2019-09-25 03:49:02 +08:00
case NodeTypes.COMMENT:
context.helper(CREATE_VNODE)
2019-09-25 03:49:02 +08:00
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(COMMENT)
2019-09-25 03:49:02 +08:00
break
case NodeTypes.EXPRESSION:
// no need to traverse, but we need to inject toString helper
if (node.isInterpolation) {
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:
traverseChildren(node, context)
break
}
// exit transforms
for (let i = 0; i < exitFns.length; i++) {
exitFns[i]()
}
}
2019-09-22 05:42:12 +08:00
export function createStructuralDirectiveTransform(
name: string | RegExp,
2019-09-22 05:42:12 +08:00
fn: StructuralDirectiveTransform
): NodeTransform {
const matches = isString(name)
? (n: string) => n === name
: (n: string) => name.test(n)
return (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
2019-09-22 05:42:12 +08:00
const { props } = node
const exitFns = []
2019-09-22 05:42:12 +08:00
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
2019-09-22 05:42:12 +08:00
props.splice(i, 1)
i--
const onExit = fn(node, prop, context)
if (onExit) exitFns.push(onExit)
}
}
return exitFns
}
}
}