aab99abd28
fix #1594
380 lines
11 KiB
TypeScript
380 lines
11 KiB
TypeScript
import {
|
|
ElementNode,
|
|
ObjectExpression,
|
|
createObjectExpression,
|
|
NodeTypes,
|
|
createObjectProperty,
|
|
createSimpleExpression,
|
|
createFunctionExpression,
|
|
DirectiveNode,
|
|
ElementTypes,
|
|
ExpressionNode,
|
|
Property,
|
|
TemplateChildNode,
|
|
SourceLocation,
|
|
createConditionalExpression,
|
|
ConditionalExpression,
|
|
SimpleExpressionNode,
|
|
FunctionExpression,
|
|
CallExpression,
|
|
createCallExpression,
|
|
createArrayExpression,
|
|
SlotsExpression
|
|
} from '../ast'
|
|
import { TransformContext, NodeTransform } from '../transform'
|
|
import { createCompilerError, ErrorCodes } from '../errors'
|
|
import {
|
|
findDir,
|
|
isTemplateNode,
|
|
assert,
|
|
isVSlot,
|
|
hasScopeRef,
|
|
isStaticExp
|
|
} from '../utils'
|
|
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
|
|
import { parseForExpression, createForLoopParams } from './vFor'
|
|
import { SlotFlags } from '@vue/shared/src'
|
|
|
|
const defaultFallback = createSimpleExpression(`undefined`, false)
|
|
|
|
// A NodeTransform that:
|
|
// 1. Tracks scope identifiers for scoped slots so that they don't get prefixed
|
|
// by transformExpression. This is only applied in non-browser builds with
|
|
// { prefixIdentifiers: true }.
|
|
// 2. Track v-slot depths so that we know a slot is inside another slot.
|
|
// Note the exit callback is executed before buildSlots() on the same node,
|
|
// so only nested slots see positive numbers.
|
|
export const trackSlotScopes: NodeTransform = (node, context) => {
|
|
if (
|
|
node.type === NodeTypes.ELEMENT &&
|
|
(node.tagType === ElementTypes.COMPONENT ||
|
|
node.tagType === ElementTypes.TEMPLATE)
|
|
) {
|
|
// We are only checking non-empty v-slot here
|
|
// since we only care about slots that introduce scope variables.
|
|
const vSlot = findDir(node, 'slot')
|
|
if (vSlot) {
|
|
const slotProps = vSlot.exp
|
|
if (!__BROWSER__ && context.prefixIdentifiers) {
|
|
slotProps && context.addIdentifiers(slotProps)
|
|
}
|
|
context.scopes.vSlot++
|
|
return () => {
|
|
if (!__BROWSER__ && context.prefixIdentifiers) {
|
|
slotProps && context.removeIdentifiers(slotProps)
|
|
}
|
|
context.scopes.vSlot--
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A NodeTransform that tracks scope identifiers for scoped slots with v-for.
|
|
// This transform is only applied in non-browser builds with { prefixIdentifiers: true }
|
|
export const trackVForSlotScopes: NodeTransform = (node, context) => {
|
|
let vFor
|
|
if (
|
|
isTemplateNode(node) &&
|
|
node.props.some(isVSlot) &&
|
|
(vFor = findDir(node, 'for'))
|
|
) {
|
|
const result = (vFor.parseResult = parseForExpression(
|
|
vFor.exp as SimpleExpressionNode,
|
|
context
|
|
))
|
|
if (result) {
|
|
const { value, key, index } = result
|
|
const { addIdentifiers, removeIdentifiers } = context
|
|
value && addIdentifiers(value)
|
|
key && addIdentifiers(key)
|
|
index && addIdentifiers(index)
|
|
|
|
return () => {
|
|
value && removeIdentifiers(value)
|
|
key && removeIdentifiers(key)
|
|
index && removeIdentifiers(index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export type SlotFnBuilder = (
|
|
slotProps: ExpressionNode | undefined,
|
|
slotChildren: TemplateChildNode[],
|
|
loc: SourceLocation
|
|
) => FunctionExpression
|
|
|
|
const buildClientSlotFn: SlotFnBuilder = (props, children, loc) =>
|
|
createFunctionExpression(
|
|
props,
|
|
children,
|
|
false /* newline */,
|
|
true /* isSlot */,
|
|
children.length ? children[0].loc : loc
|
|
)
|
|
|
|
// Instead of being a DirectiveTransform, v-slot processing is called during
|
|
// transformElement to build the slots object for a component.
|
|
export function buildSlots(
|
|
node: ElementNode,
|
|
context: TransformContext,
|
|
buildSlotFn: SlotFnBuilder = buildClientSlotFn
|
|
): {
|
|
slots: SlotsExpression
|
|
hasDynamicSlots: boolean
|
|
} {
|
|
context.helper(WITH_CTX)
|
|
|
|
const { children, loc } = node
|
|
const slotsProperties: Property[] = []
|
|
const dynamicSlots: (ConditionalExpression | CallExpression)[] = []
|
|
|
|
const buildDefaultSlotProperty = (
|
|
props: ExpressionNode | undefined,
|
|
children: TemplateChildNode[]
|
|
) => createObjectProperty(`default`, buildSlotFn(props, children, loc))
|
|
|
|
// If the slot is inside a v-for or another v-slot, force it to be dynamic
|
|
// since it likely uses a scope variable.
|
|
let hasDynamicSlots = context.scopes.vSlot > 0 || context.scopes.vFor > 0
|
|
// with `prefixIdentifiers: true`, this can be further optimized to make
|
|
// it dynamic only when the slot actually uses the scope variables.
|
|
if (!__BROWSER__ && !context.ssr && context.prefixIdentifiers) {
|
|
hasDynamicSlots = hasScopeRef(node, context.identifiers)
|
|
}
|
|
|
|
// 1. Check for slot with slotProps on component itself.
|
|
// <Comp v-slot="{ prop }"/>
|
|
const onComponentSlot = findDir(node, 'slot', true)
|
|
if (onComponentSlot) {
|
|
const { arg, exp } = onComponentSlot
|
|
if (arg && !isStaticExp(arg)) {
|
|
hasDynamicSlots = true
|
|
}
|
|
slotsProperties.push(
|
|
createObjectProperty(
|
|
arg || createSimpleExpression('default', true),
|
|
buildSlotFn(exp, children, loc)
|
|
)
|
|
)
|
|
}
|
|
|
|
// 2. Iterate through children and check for template slots
|
|
// <template v-slot:foo="{ prop }">
|
|
let hasTemplateSlots = false
|
|
let hasNamedDefaultSlot = false
|
|
const implicitDefaultChildren: TemplateChildNode[] = []
|
|
const seenSlotNames = new Set<string>()
|
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
const slotElement = children[i]
|
|
let slotDir
|
|
|
|
if (
|
|
!isTemplateNode(slotElement) ||
|
|
!(slotDir = findDir(slotElement, 'slot', true))
|
|
) {
|
|
// not a <template v-slot>, skip.
|
|
if (slotElement.type !== NodeTypes.COMMENT) {
|
|
implicitDefaultChildren.push(slotElement)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if (onComponentSlot) {
|
|
// already has on-component slot - this is incorrect usage.
|
|
context.onError(
|
|
createCompilerError(ErrorCodes.X_V_SLOT_MIXED_SLOT_USAGE, slotDir.loc)
|
|
)
|
|
break
|
|
}
|
|
|
|
hasTemplateSlots = true
|
|
const { children: slotChildren, loc: slotLoc } = slotElement
|
|
const {
|
|
arg: slotName = createSimpleExpression(`default`, true),
|
|
exp: slotProps,
|
|
loc: dirLoc
|
|
} = slotDir
|
|
|
|
// check if name is dynamic.
|
|
let staticSlotName: string | undefined
|
|
if (isStaticExp(slotName)) {
|
|
staticSlotName = slotName ? slotName.content : `default`
|
|
} else {
|
|
hasDynamicSlots = true
|
|
}
|
|
|
|
const slotFunction = buildSlotFn(slotProps, slotChildren, slotLoc)
|
|
// check if this slot is conditional (v-if/v-for)
|
|
let vIf: DirectiveNode | undefined
|
|
let vElse: DirectiveNode | undefined
|
|
let vFor: DirectiveNode | undefined
|
|
if ((vIf = findDir(slotElement, 'if'))) {
|
|
hasDynamicSlots = true
|
|
dynamicSlots.push(
|
|
createConditionalExpression(
|
|
vIf.exp!,
|
|
buildDynamicSlot(slotName, slotFunction),
|
|
defaultFallback
|
|
)
|
|
)
|
|
} else if (
|
|
(vElse = findDir(slotElement, /^else(-if)?$/, true /* allowEmpty */))
|
|
) {
|
|
// find adjacent v-if
|
|
let j = i
|
|
let prev
|
|
while (j--) {
|
|
prev = children[j]
|
|
if (prev.type !== NodeTypes.COMMENT) {
|
|
break
|
|
}
|
|
}
|
|
if (prev && isTemplateNode(prev) && findDir(prev, 'if')) {
|
|
// remove node
|
|
children.splice(i, 1)
|
|
i--
|
|
__TEST__ && assert(dynamicSlots.length > 0)
|
|
// attach this slot to previous conditional
|
|
let conditional = dynamicSlots[
|
|
dynamicSlots.length - 1
|
|
] as ConditionalExpression
|
|
while (
|
|
conditional.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION
|
|
) {
|
|
conditional = conditional.alternate
|
|
}
|
|
conditional.alternate = vElse.exp
|
|
? createConditionalExpression(
|
|
vElse.exp,
|
|
buildDynamicSlot(slotName, slotFunction),
|
|
defaultFallback
|
|
)
|
|
: buildDynamicSlot(slotName, slotFunction)
|
|
} else {
|
|
context.onError(
|
|
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, vElse.loc)
|
|
)
|
|
}
|
|
} else if ((vFor = findDir(slotElement, 'for'))) {
|
|
hasDynamicSlots = true
|
|
const parseResult =
|
|
vFor.parseResult ||
|
|
parseForExpression(vFor.exp as SimpleExpressionNode, context)
|
|
if (parseResult) {
|
|
// Render the dynamic slots as an array and add it to the createSlot()
|
|
// args. The runtime knows how to handle it appropriately.
|
|
dynamicSlots.push(
|
|
createCallExpression(context.helper(RENDER_LIST), [
|
|
parseResult.source,
|
|
createFunctionExpression(
|
|
createForLoopParams(parseResult),
|
|
buildDynamicSlot(slotName, slotFunction),
|
|
true /* force newline */
|
|
)
|
|
])
|
|
)
|
|
} else {
|
|
context.onError(
|
|
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, vFor.loc)
|
|
)
|
|
}
|
|
} else {
|
|
// check duplicate static names
|
|
if (staticSlotName) {
|
|
if (seenSlotNames.has(staticSlotName)) {
|
|
context.onError(
|
|
createCompilerError(
|
|
ErrorCodes.X_V_SLOT_DUPLICATE_SLOT_NAMES,
|
|
dirLoc
|
|
)
|
|
)
|
|
continue
|
|
}
|
|
seenSlotNames.add(staticSlotName)
|
|
if (staticSlotName === 'default') {
|
|
hasNamedDefaultSlot = true
|
|
}
|
|
}
|
|
slotsProperties.push(createObjectProperty(slotName, slotFunction))
|
|
}
|
|
}
|
|
|
|
if (!onComponentSlot) {
|
|
if (!hasTemplateSlots) {
|
|
// implicit default slot (on component)
|
|
slotsProperties.push(buildDefaultSlotProperty(undefined, children))
|
|
} else if (implicitDefaultChildren.length) {
|
|
// implicit default slot (mixed with named slots)
|
|
if (hasNamedDefaultSlot) {
|
|
context.onError(
|
|
createCompilerError(
|
|
ErrorCodes.X_V_SLOT_EXTRANEOUS_DEFAULT_SLOT_CHILDREN,
|
|
implicitDefaultChildren[0].loc
|
|
)
|
|
)
|
|
} else {
|
|
slotsProperties.push(
|
|
buildDefaultSlotProperty(undefined, implicitDefaultChildren)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
const slotFlag = hasDynamicSlots
|
|
? SlotFlags.DYNAMIC
|
|
: hasForwardedSlots(node.children)
|
|
? SlotFlags.FORWARDED
|
|
: SlotFlags.STABLE
|
|
|
|
let slots = createObjectExpression(
|
|
slotsProperties.concat(
|
|
createObjectProperty(
|
|
`_`,
|
|
// 2 = compiled but dynamic = can skip normalization, but must run diff
|
|
// 1 = compiled and static = can skip normalization AND diff as optimized
|
|
createSimpleExpression('' + slotFlag, false)
|
|
)
|
|
),
|
|
loc
|
|
) as SlotsExpression
|
|
if (dynamicSlots.length) {
|
|
slots = createCallExpression(context.helper(CREATE_SLOTS), [
|
|
slots,
|
|
createArrayExpression(dynamicSlots)
|
|
]) as SlotsExpression
|
|
}
|
|
|
|
return {
|
|
slots,
|
|
hasDynamicSlots
|
|
}
|
|
}
|
|
|
|
function buildDynamicSlot(
|
|
name: ExpressionNode,
|
|
fn: FunctionExpression
|
|
): ObjectExpression {
|
|
return createObjectExpression([
|
|
createObjectProperty(`name`, name),
|
|
createObjectProperty(`fn`, fn)
|
|
])
|
|
}
|
|
|
|
function hasForwardedSlots(children: TemplateChildNode[]): boolean {
|
|
for (let i = 0; i < children.length; i++) {
|
|
const child = children[i]
|
|
if (child.type === NodeTypes.ELEMENT) {
|
|
if (
|
|
child.tagType === ElementTypes.SLOT ||
|
|
(child.tagType === ElementTypes.ELEMENT &&
|
|
hasForwardedSlots(child.children))
|
|
) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|