feat(compiler): transform component slots

This commit is contained in:
Evan You 2019-09-27 22:25:32 -04:00
parent ee66ce78b7
commit 32666c7708
6 changed files with 194 additions and 19 deletions

View File

@ -3,7 +3,8 @@ import {
parse,
transform,
ElementNode,
NodeTypes
NodeTypes,
generate
} from '../../src'
import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn'
@ -320,4 +321,10 @@ describe('compiler: transform slots', () => {
]
})
})
test('generate slot', () => {
const ast = parseWithSlots(`<Comp><div/></Comp>`)
const { code } = generate(ast)
console.log(code)
})
})

View File

@ -27,7 +27,8 @@ export const enum NodeTypes {
JS_CALL_EXPRESSION,
JS_OBJECT_EXPRESSION,
JS_PROPERTY,
JS_ARRAY_EXPRESSION
JS_ARRAY_EXPRESSION,
JS_SLOT_FUNCTION
}
export const enum ElementTypes {
@ -157,6 +158,7 @@ export type JSChildNode =
| ObjectExpression
| ArrayExpression
| ExpressionNode
| SlotFunctionExpression
export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION
@ -180,6 +182,12 @@ export interface ArrayExpression extends Node {
elements: Array<string | JSChildNode>
}
export interface SlotFunctionExpression extends Node {
type: NodeTypes.JS_SLOT_FUNCTION
params: ExpressionNode | undefined
returns: ChildNode[]
}
export function createArrayExpression(
elements: ArrayExpression['elements'],
loc: SourceLocation
@ -264,3 +272,16 @@ export function createCallExpression(
arguments: args
}
}
export function createFunctionExpression(
params: ExpressionNode | undefined,
returns: ChildNode[],
loc: SourceLocation
): SlotFunctionExpression {
return {
type: NodeTypes.JS_SLOT_FUNCTION,
params,
returns,
loc
}
}

View File

@ -18,7 +18,8 @@ import {
InterpolationNode,
CompoundExpressionNode,
SimpleExpressionNode,
ElementTypes
ElementTypes,
SlotFunctionExpression
} from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map'
import {
@ -364,10 +365,17 @@ function genNode(node: CodegenNode, context: CodegenContext) {
case NodeTypes.JS_ARRAY_EXPRESSION:
genArrayExpression(node, context)
break
case NodeTypes.JS_SLOT_FUNCTION:
genSlotFunction(node, context)
break
default:
/* istanbul ignore next */
__DEV__ &&
/* istanbul ignore if */
if (__DEV__) {
assert(false, `unhandled codegen node type: ${(node as any).type}`)
// make sure we exhaust all possible types
const exhaustiveCheck: never = node
return exhaustiveCheck
}
}
}
@ -568,3 +576,14 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
function genArrayExpression(node: ArrayExpression, context: CodegenContext) {
genNodeListAsArray(node.elements, context)
}
function genSlotFunction(
node: SlotFunctionExpression,
context: CodegenContext
) {
context.push(`(`, node)
if (node.params) genNode(node.params, context)
context.push(`) => `)
// pre-normalized slots should always return arrays
genNodeListAsArray(node.returns, context)
}

View File

@ -69,6 +69,10 @@ export const enum ErrorCodes {
X_V_BIND_NO_EXPRESSION,
X_V_ON_NO_EXPRESSION,
X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
X_NAMED_SLOT_ON_COMPONENT,
X_MIXED_SLOT_USAGE,
X_DUPLICATE_SLOT_NAMES,
X_EXTRANEOUS_NON_SLOT_CHILDREN,
// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
@ -133,14 +137,24 @@ export const errorMessages: { [code: number]: string } = {
'Note that dynamic directive argument connot contain spaces.',
// transform errors
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if`,
[ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if`,
[ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for has no expression`,
[ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`,
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression`,
[ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression`,
[ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `unexpected custom directive on <slot> outlet`,
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if.`,
[ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if.`,
[ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for has no expression.`,
[ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression.`,
[ErrorCodes.X_V_BIND_NO_EXPRESSION]: `v-bind is missing expression.`,
[ErrorCodes.X_V_ON_NO_EXPRESSION]: `v-on is missing expression.`,
[ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET]: `Unexpected custom directive on <slot> outlet.`,
[ErrorCodes.X_NAMED_SLOT_ON_COMPONENT]:
`Named v-slot on component. ` +
`Named slots should use <template v-slot> syntax nested inside the component.`,
[ErrorCodes.X_MIXED_SLOT_USAGE]:
`Mixed v-slot usage on both the component and nested <template>.` +
`The default slot should also use <template> syntax when there are other ` +
`named slots to avoid scope ambiguity.`,
[ErrorCodes.X_DUPLICATE_SLOT_NAMES]: `Duplicate slot names found. `,
[ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN]:
`Extraneous children found when component has explicit slots. ` +
`These children will be ignored.`,
// generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`,
[ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`

View File

@ -141,6 +141,12 @@ export function buildProps(
// directives
isStatic = false
const { name, arg, exp, loc } = prop
// skip v-slot - it is handled by its dedicated transform.
if (name === 'slot') {
continue
}
// special case for v-bind and v-on with no argument
const isBind = name === 'bind'
if (!arg && (isBind || name === 'on')) {

View File

@ -6,22 +6,127 @@ import {
createCompoundExpression,
createCallExpression,
CompoundExpressionNode,
CallExpression
CallExpression,
createObjectProperty,
createSimpleExpression,
createFunctionExpression,
DirectiveNode,
ElementTypes,
ExpressionNode,
Property,
ChildNode,
SourceLocation
} from '../ast'
import { TransformContext } from '../transform'
import { buildProps } from './transformElement'
import { createCompilerError, ErrorCodes } from '../errors'
import { isSimpleIdentifier } from '../utils'
import { RENDER_SLOT } from '../runtimeConstants'
import { isString } from '@vue/shared'
const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
export function buildSlots(
{ loc, children }: ElementNode,
{ props, children, loc }: ElementNode,
context: TransformContext
): ObjectExpression {
const slots = createObjectExpression([], loc)
// TODO
const slots: Property[] = []
return slots
// 1. Check for default slot with slotProps on component itself.
// <Comp v-slot="{ prop }"/>
const explicitDefaultSlot = props.find(isVSlot)
if (explicitDefaultSlot) {
const { arg, exp, loc } = explicitDefaultSlot
if (arg) {
context.onError(
createCompilerError(ErrorCodes.X_NAMED_SLOT_ON_COMPONENT, loc)
)
}
slots.push(buildSlot(`default`, exp, children, loc))
}
// 2. Iterate through children and check for template slots
// <template v-slot:foo="{ prop }">
let hasTemplateSlots = false
const seenSlotNames = new Set<string>()
const nonSlotChildren: ChildNode[] = []
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.TEMPLATE
) {
const { props, children, loc: nodeLoc } = child
const slotDir = props.find(isVSlot)
if (slotDir) {
hasTemplateSlots = true
const { arg: slotName, exp: slotProps, loc: dirLoc } = slotDir
if (explicitDefaultSlot) {
// already has on-component default slot - this is incorrect usage.
context.onError(
createCompilerError(ErrorCodes.X_MIXED_SLOT_USAGE, dirLoc)
)
break
} else {
// check duplicate slot names
if (
!slotName ||
(slotName.type === NodeTypes.SIMPLE_EXPRESSION && slotName.isStatic)
) {
const name = slotName ? slotName.content : `default`
if (seenSlotNames.has(name)) {
context.onError(
createCompilerError(ErrorCodes.X_DUPLICATE_SLOT_NAMES, dirLoc)
)
continue
}
seenSlotNames.add(name)
}
slots.push(
buildSlot(slotName || `default`, slotProps, children, nodeLoc)
)
}
} else {
nonSlotChildren.push(child)
}
} else {
nonSlotChildren.push(child)
}
}
if (hasTemplateSlots && nonSlotChildren.length) {
context.onError(
createCompilerError(
ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
nonSlotChildren[0].loc
)
)
}
if (!explicitDefaultSlot && !hasTemplateSlots) {
// implicit default slot.
slots.push(buildSlot(`default`, undefined, children, loc))
}
return createObjectExpression(slots, loc)
}
function buildSlot(
name: string | ExpressionNode,
slotProps: ExpressionNode | undefined,
children: ChildNode[],
loc: SourceLocation
): Property {
return createObjectProperty(
isString(name) ? createSimpleExpression(name, true, loc) : name,
createFunctionExpression(
slotProps,
children,
children.length ? children[0].loc : loc
),
loc
)
}
export function buildSlotOutlet(node: ElementNode, context: TransformContext) {
@ -84,7 +189,10 @@ export function buildSlotOutlet(node: ElementNode, context: TransformContext) {
)
if (directives.length) {
context.onError(
createCompilerError(ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET)
createCompilerError(
ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
directives[0].loc
)
)
}
slotArgs.push(propsExpression)