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

517 lines
14 KiB
TypeScript

import type { TransformOptions } from './options'
import {
type ArrayExpression,
type CacheExpression,
ConstantTypes,
type DirectiveNode,
type ElementNode,
ElementTypes,
type ExpressionNode,
type JSChildNode,
NodeTypes,
type ParentNode,
type Property,
type RootNode,
type SimpleExpressionNode,
type TemplateChildNode,
type TemplateLiteral,
convertToBlock,
createCacheExpression,
createSimpleExpression,
createVNodeCall,
} from './ast'
import {
EMPTY_OBJ,
NOOP,
PatchFlags,
camelize,
capitalize,
isArray,
isString,
} from '@vue/shared'
import { defaultOnError, defaultOnWarn } from './errors'
import {
CREATE_COMMENT,
FRAGMENT,
TO_DISPLAY_STRING,
helperNameMap,
} from './runtimeHelpers'
import { isVSlot } from './utils'
import { cacheStatic, isSingleElementRoot } from './transforms/cacheStatic'
import type { CompilerCompatOptions } from './compat/compatConfig'
// 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
ssrTagParts?: TemplateLiteral['elements']
}
// A structural directive transform is technically also 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 ImportItem {
exp: string | ExpressionNode
path: string
}
export interface TransformContext
extends Required<Omit<TransformOptions, keyof CompilerCompatOptions>>,
CompilerCompatOptions {
selfName: string | null
root: RootNode
helpers: Map<symbol, number>
components: Set<string>
directives: Set<string>
hoists: (JSChildNode | null)[]
imports: ImportItem[]
temps: number
cached: (CacheExpression | null)[]
identifiers: { [name: string]: number | undefined }
scopes: {
vFor: number
vSlot: number
vPre: number
vOnce: number
}
parent: ParentNode | null
// we could use a stack but in practice we've only ever needed two layers up
// so this is more efficient
grandParent: ParentNode | null
childIndex: number
currentNode: RootNode | TemplateChildNode | null
inVOnce: boolean
helper<T extends symbol>(name: T): T
removeHelper<T extends symbol>(name: T): void
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: string | JSChildNode | ArrayExpression): SimpleExpressionNode
cache(exp: JSChildNode, isVNode?: boolean): CacheExpression
constantCache: WeakMap<TemplateChildNode, ConstantTypes>
// 2.x Compat only
filters?: Set<string>
}
export function createTransformContext(
root: RootNode,
{
filename = '',
prefixIdentifiers = false,
hoistStatic = false,
hmr = false,
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
transformHoist = null,
isBuiltInComponent = NOOP,
isCustomElement = NOOP,
expressionPlugins = [],
scopeId = null,
slotted = true,
ssr = false,
inSSR = false,
ssrCssVars = ``,
bindingMetadata = EMPTY_OBJ,
inline = false,
isTS = false,
onError = defaultOnError,
onWarn = defaultOnWarn,
compatConfig,
}: TransformOptions,
): TransformContext {
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
const context: TransformContext = {
// options
filename,
selfName: nameMatch && capitalize(camelize(nameMatch[1])),
prefixIdentifiers,
hoistStatic,
hmr,
cacheHandlers,
nodeTransforms,
directiveTransforms,
transformHoist,
isBuiltInComponent,
isCustomElement,
expressionPlugins,
scopeId,
slotted,
ssr,
inSSR,
ssrCssVars,
bindingMetadata,
inline,
isTS,
onError,
onWarn,
compatConfig,
// state
root,
helpers: new Map(),
components: new Set(),
directives: new Set(),
hoists: [],
imports: [],
cached: [],
constantCache: new WeakMap(),
temps: 0,
identifiers: Object.create(null),
scopes: {
vFor: 0,
vSlot: 0,
vPre: 0,
vOnce: 0,
},
parent: null,
grandParent: null,
currentNode: root,
childIndex: 0,
inVOnce: false,
// methods
helper(name) {
const count = context.helpers.get(name) || 0
context.helpers.set(name, count + 1)
return name
},
removeHelper(name) {
const count = context.helpers.get(name)
if (count) {
const currentCount = count - 1
if (!currentCount) {
context.helpers.delete(name)
} else {
context.helpers.set(name, currentCount)
}
}
},
helperString(name) {
return `_${helperNameMap[context.helper(name)]}`
},
replaceNode(node) {
/* v8 ignore start */
if (__DEV__) {
if (!context.currentNode) {
throw new Error(`Node being replaced is already removed.`)
}
if (!context.parent) {
throw new Error(`Cannot replace root node.`)
}
}
/* v8 ignore stop */
context.parent!.children[context.childIndex] = context.currentNode = node
},
removeNode(node) {
/* v8 ignore next 3 */
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
/* v8 ignore next 3 */
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: NOOP,
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) {
if (isString(exp)) exp = createSimpleExpression(exp)
context.hoists.push(exp)
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
false,
exp.loc,
ConstantTypes.CAN_CACHE,
)
identifier.hoisted = exp
return identifier
},
cache(exp, isVNode = false) {
const cacheExp = createCacheExpression(
context.cached.length,
exp,
isVNode,
)
context.cached.push(cacheExp)
return cacheExp
},
}
if (__COMPAT__) {
context.filters = new Set()
}
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): void {
const context = createTransformContext(root, options)
traverseNode(root, context)
if (options.hoistStatic) {
cacheStatic(root, context)
}
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = new Set([...context.helpers.keys()])
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = context.imports
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
root.transformed = true
if (__COMPAT__) {
root.filters = [...context.filters!]
}
}
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
if (children.length === 1) {
const child = children[0]
// 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) {
convertToBlock(codegenNode, context)
}
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.
let patchFlag = PatchFlags.STABLE_FRAGMENT
// check if the fragment actually contains a single valid child with
// the rest being comments
if (
__DEV__ &&
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
}
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
patchFlag,
undefined,
undefined,
true,
undefined,
false /* isComponent */,
)
} else {
// no children = noop. codegen will return null.
}
}
export function traverseChildren(
parent: ParentNode,
context: TransformContext,
): void {
let i = 0
const nodeRemoved = () => {
i--
}
for (; i < parent.children.length; i++) {
const child = parent.children[i]
if (isString(child)) continue
context.grandParent = context.parent
context.parent = parent
context.childIndex = i
context.onNodeRemoved = nodeRemoved
traverseNode(child, context)
}
}
export function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext,
): void {
context.currentNode = node
// 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:
if (!context.ssr) {
// 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
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
context.currentNode = node
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
}
}
}