This is done by adding the `slotted: false` option to: - compiler-dom - compiler-ssr - compiler-sfc (forwarded to template compiler) At runtime, only slotted component will render slot fragments with slot scope Ids. For SSR, only slotted component will add slot scope Ids to rendered slot content. This should improve both runtime performance and reduce SSR rendered markup size. Note: requires SFC tooling (e.g. `vue-loader` and `vite`) to pass on the `slotted` option from the SFC descriptoer to the `compileTemplate` call.
468 lines
12 KiB
TypeScript
468 lines
12 KiB
TypeScript
import { TransformOptions } from './options'
|
|
import {
|
|
RootNode,
|
|
NodeTypes,
|
|
ParentNode,
|
|
TemplateChildNode,
|
|
ElementNode,
|
|
DirectiveNode,
|
|
Property,
|
|
ExpressionNode,
|
|
createSimpleExpression,
|
|
JSChildNode,
|
|
SimpleExpressionNode,
|
|
ElementTypes,
|
|
CacheExpression,
|
|
createCacheExpression,
|
|
TemplateLiteral,
|
|
createVNodeCall,
|
|
ConstantTypes
|
|
} from './ast'
|
|
import {
|
|
isString,
|
|
isArray,
|
|
NOOP,
|
|
PatchFlags,
|
|
PatchFlagNames,
|
|
EMPTY_OBJ,
|
|
capitalize,
|
|
camelize
|
|
} from '@vue/shared'
|
|
import { defaultOnError } from './errors'
|
|
import {
|
|
TO_DISPLAY_STRING,
|
|
FRAGMENT,
|
|
helperNameMap,
|
|
CREATE_BLOCK,
|
|
CREATE_COMMENT,
|
|
OPEN_BLOCK
|
|
} from './runtimeHelpers'
|
|
import { isVSlot } 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
|
|
ssrTagParts?: TemplateLiteral['elements']
|
|
}
|
|
|
|
// 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 ImportItem {
|
|
exp: string | ExpressionNode
|
|
path: string
|
|
}
|
|
|
|
export interface TransformContext
|
|
extends Required<Omit<TransformOptions, 'filename'>> {
|
|
selfName: string | null
|
|
root: RootNode
|
|
helpers: Set<symbol>
|
|
components: Set<string>
|
|
directives: Set<string>
|
|
hoists: (JSChildNode | null)[]
|
|
imports: ImportItem[]
|
|
temps: number
|
|
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
|
|
constantCache: Map<TemplateChildNode, ConstantTypes>
|
|
}
|
|
|
|
export function createTransformContext(
|
|
root: RootNode,
|
|
{
|
|
filename = '',
|
|
prefixIdentifiers = false,
|
|
hoistStatic = false,
|
|
cacheHandlers = false,
|
|
nodeTransforms = [],
|
|
directiveTransforms = {},
|
|
transformHoist = null,
|
|
isBuiltInComponent = NOOP,
|
|
isCustomElement = NOOP,
|
|
expressionPlugins = [],
|
|
scopeId = null,
|
|
slotted = true,
|
|
ssr = false,
|
|
ssrCssVars = ``,
|
|
bindingMetadata = EMPTY_OBJ,
|
|
inline = false,
|
|
isTS = false,
|
|
onError = defaultOnError
|
|
}: TransformOptions
|
|
): TransformContext {
|
|
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
|
|
const context: TransformContext = {
|
|
// options
|
|
selfName: nameMatch && capitalize(camelize(nameMatch[1])),
|
|
prefixIdentifiers,
|
|
hoistStatic,
|
|
cacheHandlers,
|
|
nodeTransforms,
|
|
directiveTransforms,
|
|
transformHoist,
|
|
isBuiltInComponent,
|
|
isCustomElement,
|
|
expressionPlugins,
|
|
scopeId,
|
|
slotted,
|
|
ssr,
|
|
ssrCssVars,
|
|
bindingMetadata,
|
|
inline,
|
|
isTS,
|
|
onError,
|
|
|
|
// state
|
|
root,
|
|
helpers: new Set(),
|
|
components: new Set(),
|
|
directives: new Set(),
|
|
hoists: [],
|
|
imports: [],
|
|
constantCache: new Map(),
|
|
temps: 0,
|
|
cached: 0,
|
|
identifiers: Object.create(null),
|
|
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 `_${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)
|
|
const identifier = createSimpleExpression(
|
|
`_hoisted_${context.hoists.length}`,
|
|
false,
|
|
exp.loc,
|
|
ConstantTypes.CAN_HOIST
|
|
)
|
|
identifier.hoisted = exp
|
|
return identifier
|
|
},
|
|
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)
|
|
}
|
|
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
|
|
root.cached = context.cached
|
|
}
|
|
|
|
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) {
|
|
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.
|
|
let patchFlag = PatchFlags.STABLE_FRAGMENT
|
|
let patchFlagText = PatchFlagNames[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
|
|
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
|
|
}
|
|
root.codegenNode = createVNodeCall(
|
|
context,
|
|
helper(FRAGMENT),
|
|
undefined,
|
|
root.children,
|
|
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
|
|
undefined,
|
|
undefined,
|
|
true
|
|
)
|
|
} else {
|
|
// no children = noop. codegen will return null.
|
|
}
|
|
}
|
|
|
|
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.parent = parent
|
|
context.childIndex = i
|
|
context.onNodeRemoved = nodeRemoved
|
|
traverseNode(child, context)
|
|
}
|
|
}
|
|
|
|
export function traverseNode(
|
|
node: RootNode | TemplateChildNode,
|
|
context: TransformContext
|
|
) {
|
|
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
|
|
}
|
|
}
|
|
}
|