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

434 lines
11 KiB
TypeScript
Raw Normal View History

import { TransformOptions } from './options'
import {
RootNode,
NodeTypes,
ParentNode,
TemplateChildNode,
ElementNode,
2019-09-21 21:42:12 +00:00
DirectiveNode,
Property,
ExpressionNode,
createSimpleExpression,
JSChildNode,
SimpleExpressionNode,
ElementTypes,
2019-10-19 01:51:34 +00:00
CacheExpression,
createCacheExpression,
TemplateLiteral,
createVNodeCall
} from './ast'
import {
isString,
isArray,
NOOP,
PatchFlags,
PatchFlagNames
} from '@vue/shared'
import { defaultOnError } from './errors'
import {
2020-01-26 22:35:21 +00:00
TO_DISPLAY_STRING,
FRAGMENT,
helperNameMap,
CREATE_BLOCK,
CREATE_COMMENT,
OPEN_BLOCK
} from './runtimeHelpers'
import { isVSlot } from './utils'
2019-10-07 21:12:22 +00:00
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
2019-09-21 21:42:12 +00: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: RootNode | TemplateChildNode,
context: TransformContext
) => void | (() => void) | (() => void)[]
2019-09-21 21:42:12 +00: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-21 21:42:12 +00:00
dir: DirectiveNode,
node: ElementNode,
2019-10-19 01:51:34 +00:00
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 {
2019-10-10 22:02:51 +00:00
props: Property[]
2020-02-04 17:20:51 +00:00
needRuntime?: boolean | symbol
ssrTagParts?: TemplateLiteral['elements']
2019-09-21 21:42:12 +00:00
}
// A structural directive transform is a technically a NodeTransform;
2019-09-21 21:42:12 +00:00
// Only v-if and v-for fall into this category.
export type StructuralDirectiveTransform = (
node: ElementNode,
dir: DirectiveNode,
context: TransformContext
) => void | (() => void)
export interface ImportItem {
exp: string | ExpressionNode
path: string
}
2019-09-21 21:42:12 +00:00
export interface TransformContext extends Required<TransformOptions> {
2019-09-24 19:49:02 +00:00
root: RootNode
2019-10-10 22:02:51 +00:00
helpers: Set<symbol>
components: Set<string>
directives: Set<string>
hoists: JSChildNode[]
imports: Set<ImportItem>
2020-02-07 06:06:51 +00:00
temps: number
2019-10-19 01:51:34 +00:00
cached: number
2019-09-23 17:56:56 +00:00
identifiers: { [name: string]: number | undefined }
scopes: {
vFor: number
vSlot: number
vPre: number
vOnce: number
}
parent: ParentNode | null
childIndex: number
currentNode: RootNode | TemplateChildNode | null
2019-10-10 22:02:51 +00:00
helper<T extends symbol>(name: T): T
helperString(name: symbol): string
replaceNode(node: TemplateChildNode): void
removeNode(node?: TemplateChildNode): void
2019-10-14 21:12:02 +00:00
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
}
export function createTransformContext(
root: RootNode,
{
2019-09-23 17:29:41 +00:00
prefixIdentifiers = false,
2019-10-04 13:03:00 +00:00
hoistStatic = false,
2019-10-19 01:51:34 +00:00
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
transformHoist = null,
isBuiltInComponent = NOOP,
2020-02-06 21:51:26 +00:00
scopeId = null,
2020-02-03 22:47:06 +00:00
ssr = false,
onError = defaultOnError
}: TransformOptions
): TransformContext {
const context: TransformContext = {
// options
prefixIdentifiers,
hoistStatic,
cacheHandlers,
nodeTransforms,
directiveTransforms,
transformHoist,
isBuiltInComponent,
2020-02-06 21:51:26 +00:00
scopeId,
2020-02-03 22:47:06 +00:00
ssr,
onError,
// state
2019-09-24 19:49:02 +00:00
root,
helpers: new Set(),
components: new Set(),
directives: new Set(),
hoists: [],
imports: new Set(),
2020-02-07 06:06:51 +00:00
temps: 0,
2019-10-19 01:51:34 +00:00
cached: 0,
identifiers: {},
scopes: {
vFor: 0,
vSlot: 0,
vPre: 0,
vOnce: 0
},
parent: null,
currentNode: root,
childIndex: 0,
// methods
helper(name) {
context.helpers.add(name)
return name
},
helperString(name) {
return (
(context.prefixIdentifiers ? `` : `_`) +
helperNameMap[context.helper(name)]
)
},
replaceNode(node) {
2019-09-24 20:35:01 +00:00
/* 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
},
2019-09-19 19:41:17 +00:00
removeNode(node) {
if (__DEV__ && !context.parent) {
throw new Error(`Cannot remove root node.`)
}
const list = context.parent!.children
2019-09-19 19:41:17 +00:00
const removalIndex = node
? list.indexOf(node)
2019-09-19 19:41:17 +00:00
: context.currentNode
? context.childIndex
: -1
2019-09-24 20:35:01 +00:00
/* istanbul ignore if */
2019-09-19 19:41:17 +00: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: () => {},
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)
}
2019-09-23 17:56:56 +00:00
}
},
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
)
2019-10-19 01:51:34 +00:00
},
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)
2019-10-04 13:03:00 +00:00
if (options.hoistStatic) {
hoistStatic(root, context)
2019-10-04 03:30:25 +00:00
}
2020-02-03 22:47:06 +00:00
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.temps = context.temps
2020-02-03 22:47:06 +00:00
root.cached = context.cached
}
2020-02-03 22:47:06 +00:00
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
2019-10-07 21:12:22 +00:00
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
if (codegenNode.type === NodeTypes.VNODE_CALL) {
codegenNode.isBlock = true
helper(OPEN_BLOCK)
helper(CREATE_BLOCK)
}
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 = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
`${PatchFlags.STABLE_FRAGMENT} /* ${
PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
} */`,
undefined,
undefined,
true
)
} else {
// no children = noop. codegen will return null.
}
}
export function traverseChildren(
parent: ParentNode,
context: TransformContext
) {
2019-09-19 19:41:17 +00:00
let i = 0
const nodeRemoved = () => {
i--
}
for (; i < parent.children.length; i++) {
2019-09-23 06:52:54 +00:00
const child = parent.children[i]
if (isString(child)) continue
context.parent = parent
context.childIndex = i
2019-09-19 19:41:17 +00:00
context.onNodeRemoved = nodeRemoved
2019-09-23 06:52:54 +00:00
traverseNode(child, context)
}
}
export function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
context.currentNode = node
// apply transform plugins
2019-09-21 21:42:12 +00:00
const { nodeTransforms } = context
const exitFns = []
2019-09-21 21:42:12 +00:00
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) {
2019-09-24 19:49:02 +00:00
case NodeTypes.COMMENT:
2020-02-03 22:47:06 +00:00
if (!context.ssr) {
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
2019-09-24 19:49:02 +00:00
break
case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper
2020-02-03 22:47:06 +00:00
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break
// for container types, further traverse downwards
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context)
}
break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context)
break
}
// exit transforms
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
2019-09-21 21:42:12 +00:00
export function createStructuralDirectiveTransform(
name: string | RegExp,
2019-09-21 21:42:12 +00: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-21 21:42:12 +00:00
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 = []
2019-09-21 21:42:12 +00: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-21 21:42:12 +00:00
props.splice(i, 1)
i--
const onExit = fn(node, prop, context)
if (onExit) exitFns.push(onExit)
}
}
return exitFns
}
}
}