test(compiler): test v-slot transform

This commit is contained in:
Evan You 2019-09-28 14:05:10 -04:00
parent 96749e0178
commit 1c410205de
8 changed files with 613 additions and 30 deletions

View File

@ -1234,6 +1234,50 @@ describe('compiler: parse', () => {
}) })
}) })
test('v-slot shorthand', () => {
const ast = parse('<Comp #a="{ b }" />')
const directive = (ast.children[0] as ElementNode).props[0]
expect(directive).toStrictEqual({
type: NodeTypes.DIRECTIVE,
name: 'slot',
arg: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'a',
isStatic: true,
loc: {
source: 'a',
start: {
column: 8,
line: 1,
offset: 7
},
end: {
column: 9,
line: 1,
offset: 8
}
}
},
modifiers: [],
exp: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: '{ b }',
isStatic: false,
loc: {
start: { offset: 10, line: 1, column: 11 },
end: { offset: 15, line: 1, column: 16 },
source: '{ b }'
}
},
loc: {
start: { offset: 6, line: 1, column: 7 },
end: { offset: 16, line: 1, column: 17 },
source: '#a="{ b }"'
}
})
})
test('end tags are case-insensitive.', () => { test('end tags are case-insensitive.', () => {
const ast = parse('<div>hello</DIV>after') const ast = parse('<div>hello</DIV>after')
const element = ast.children[0] as ElementNode const element = ast.children[0] as ElementNode

View File

@ -0,0 +1,97 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`compiler: transform component slots dynamically named slots 1`] = `
"const { resolveComponent, createVNode, toString } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return createVNode(_component_Comp, 0, {
[_ctx.one]: ({ foo }) => [
toString(foo),
toString(_ctx.bar)
],
[_ctx.two]: ({ bar }) => [
toString(_ctx.foo),
toString(bar)
]
})
}"
`;
exports[`compiler: transform component slots explicit default slot 1`] = `
"const { resolveComponent, createVNode, toString } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return createVNode(_component_Comp, 0, {
default: ({ foo }) => [
toString(foo),
toString(_ctx.bar)
]
})
}"
`;
exports[`compiler: transform component slots implicit default slot 1`] = `
"const { resolveComponent, createVNode } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return createVNode(_component_Comp, 0, {
default: () => [
createVNode(\\"div\\")
]
})
}"
`;
exports[`compiler: transform component slots named slots 1`] = `
"const { resolveComponent, createVNode, toString } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
return createVNode(_component_Comp, 0, {
one: ({ foo }) => [
toString(foo),
toString(_ctx.bar)
],
two: ({ bar }) => [
toString(_ctx.foo),
toString(bar)
]
})
}"
`;
exports[`compiler: transform component slots nested slots scoping 1`] = `
"const { resolveComponent, createVNode, toString } = Vue
return function render() {
const _ctx = this
const _component_Comp = resolveComponent(\\"Comp\\")
const _component_Inner = resolveComponent(\\"Inner\\")
return createVNode(_component_Comp, 0, {
default: ({ foo }) => [
createVNode(_component_Inner, 0, {
default: ({ bar }) => [
toString(foo),
toString(bar),
toString(_ctx.baz)
]
}),
toString(foo),
toString(_ctx.bar),
toString(_ctx.baz)
]
})
}"
`;

View File

@ -3,7 +3,8 @@ import {
parse, parse,
transform, transform,
ElementNode, ElementNode,
NodeTypes NodeTypes,
ErrorCodes
} from '../../src' } from '../../src'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
@ -321,4 +322,27 @@ describe('compiler: transform <slot> outlets', () => {
] ]
}) })
}) })
test(`error on unexpected custom directive on <slot>`, () => {
const onError = jest.fn()
const source = `<slot v-foo />`
parseWithSlots(source, { onError })
const index = source.indexOf('v-foo')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET,
loc: {
source: `v-foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 5,
line: 1,
column: index + 6
}
}
})
})
}) })

View File

@ -1,4 +1,12 @@
import { CompilerOptions, parse, transform, generate } from '../../src' import {
CompilerOptions,
parse,
transform,
generate,
ElementNode,
NodeTypes,
ErrorCodes
} from '../../src'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
import { transformBind } from '../../src/transforms/vBind' import { transformBind } from '../../src/transforms/vBind'
@ -20,22 +28,411 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
}, },
...options ...options
}) })
return ast return {
root: ast,
slots: (ast.children[0] as ElementNode).codegenNode!.arguments[2]
}
}
function createSlotMatcher(obj: Record<string, any>) {
return {
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: Object.keys(obj).map(key => {
return {
type: NodeTypes.JS_PROPERTY,
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: !/^\[/.test(key),
content: key.replace(/^\[|\]$/g, '')
},
value: obj[key]
}
})
}
} }
describe('compiler: transform component slots', () => { describe('compiler: transform component slots', () => {
test('generate slot', () => { test('implicit default slot', () => {
const ast = parseWithSlots( const { root, slots } = parseWithSlots(`<Comp><div/></Comp>`, {
` prefixIdentifiers: true
<Comp> })
<Comp v-slot="{ dur }"> expect(slots).toMatchObject(
hello {{ dur }} createSlotMatcher({
</Comp> default: {
</Comp> type: NodeTypes.JS_SLOT_FUNCTION,
`, params: undefined,
returns: [
{
type: NodeTypes.ELEMENT,
tag: `div`
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('explicit default slot', () => {
const { root, slots } = parseWithSlots(
`<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</Comp>`,
{ prefixIdentifiers: true } { prefixIdentifiers: true }
) )
const { code } = generate(ast, { prefixIdentifiers: true }) expect(slots).toMatchObject(
console.log(code) createSlotMatcher({
default: {
type: NodeTypes.JS_SLOT_FUNCTION,
params: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{ foo }`,
isStatic: false
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('named slots', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template v-slot:one="{ foo }">
{{ foo }}{{ bar }}
</template>
<template #two="{ bar }">
{{ foo }}{{ bar }}
</template>
</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject(
createSlotMatcher({
one: {
type: NodeTypes.JS_SLOT_FUNCTION,
params: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{ foo }`,
isStatic: false
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
}
]
},
two: {
type: NodeTypes.JS_SLOT_FUNCTION,
params: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{ bar }`,
isStatic: false
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `bar`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('dynamically named slots', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template v-slot:[one]="{ foo }">
{{ foo }}{{ bar }}
</template>
<template #[two]="{ bar }">
{{ foo }}{{ bar }}
</template>
</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject(
createSlotMatcher({
'[_ctx.one]': {
type: NodeTypes.JS_SLOT_FUNCTION,
params: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{ foo }`,
isStatic: false
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
}
]
},
'[_ctx.two]': {
type: NodeTypes.JS_SLOT_FUNCTION,
params: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{ bar }`,
isStatic: false
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `bar`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('nested slots scoping', () => {
const { root, slots } = parseWithSlots(
`<Comp>
<template #default="{ foo }">
<Inner v-slot="{ bar }">
{{ foo }}{{ bar }}{{ baz }}
</Inner>
{{ foo }}{{ bar }}{{ baz }}
</template>
</Comp>`,
{ prefixIdentifiers: true }
)
expect(slots).toMatchObject(
createSlotMatcher({
default: {
type: NodeTypes.JS_SLOT_FUNCTION,
params: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{ foo }`,
isStatic: false
},
returns: [
{
type: NodeTypes.ELEMENT,
codegenNode: {
type: NodeTypes.JS_CALL_EXPRESSION,
arguments: [
`_component_Inner`,
`0`,
createSlotMatcher({
default: {
type: NodeTypes.JS_SLOT_FUNCTION,
params: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `{ bar }`,
isStatic: false
},
returns: [
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `bar`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.baz`
}
}
]
}
})
]
}
},
// test scope
{
type: NodeTypes.INTERPOLATION,
content: {
content: `foo`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.bar`
}
},
{
type: NodeTypes.INTERPOLATION,
content: {
content: `_ctx.baz`
}
}
]
}
})
)
expect(generate(root, { prefixIdentifiers: true }).code).toMatchSnapshot()
})
test('error on extraneous children w/ named slots', () => {
const onError = jest.fn()
const source = `<Comp><template #default>foo</template>bar</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('bar')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
loc: {
source: `bar`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 3,
line: 1,
column: index + 4
}
}
})
})
test('error on duplicated slot names', () => {
const onError = jest.fn()
const source = `<Comp><template #foo></template><template #foo></template></Comp>`
parseWithSlots(source, { onError })
const index = source.lastIndexOf('#foo')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_DUPLICATE_SLOT_NAMES,
loc: {
source: `#foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 4,
line: 1,
column: index + 5
}
}
})
})
test('error on invalid mixed slot usage', () => {
const onError = jest.fn()
const source = `<Comp v-slot="foo"><template #foo></template></Comp>`
parseWithSlots(source, { onError })
const index = source.lastIndexOf('#foo')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_MIXED_SLOT_USAGE,
loc: {
source: `#foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 4,
line: 1,
column: index + 5
}
}
})
})
test('error on v-slot usage on plain elements', () => {
const onError = jest.fn()
const source = `<div v-slot/>`
parseWithSlots(source, { onError })
const index = source.indexOf('v-slot')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_MISPLACED_V_SLOT,
loc: {
source: `v-slot`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 6,
line: 1,
column: index + 7
}
}
})
})
test('error on named slot on component', () => {
const onError = jest.fn()
const source = `<Comp v-slot:foo>foo</Comp>`
parseWithSlots(source, { onError })
const index = source.indexOf('v-slot')
expect(onError.mock.calls[0][0]).toMatchObject({
code: ErrorCodes.X_NAMED_SLOT_ON_COMPONENT,
loc: {
source: `v-slot:foo`,
start: {
offset: index,
line: 1,
column: index + 1
},
end: {
offset: index + 10,
line: 1,
column: index + 11
}
}
})
}) })
}) })

View File

@ -73,6 +73,7 @@ export const enum ErrorCodes {
X_MIXED_SLOT_USAGE, X_MIXED_SLOT_USAGE,
X_DUPLICATE_SLOT_NAMES, X_DUPLICATE_SLOT_NAMES,
X_EXTRANEOUS_NON_SLOT_CHILDREN, X_EXTRANEOUS_NON_SLOT_CHILDREN,
X_MISPLACED_V_SLOT,
// generic errors // generic errors
X_PREFIX_ID_NOT_SUPPORTED, X_PREFIX_ID_NOT_SUPPORTED,
@ -155,6 +156,8 @@ export const errorMessages: { [code: number]: string } = {
[ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN]: [ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN]:
`Extraneous children found when component has explicit slots. ` + `Extraneous children found when component has explicit slots. ` +
`These children will be ignored.`, `These children will be ignored.`,
[ErrorCodes.X_MISPLACED_V_SLOT]: `v-slot can only be used on components or <template> tags.`,
// generic errors // generic errors
[ErrorCodes.X_PREFIX_ID_NOT_SUPPORTED]: `"prefixIdentifiers" option is not supported in this build of compiler.`, [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.` [ErrorCodes.X_MODULE_MODE_NOT_SUPPORTED]: `ES module mode is not supported in this build of compiler.`

View File

@ -384,6 +384,11 @@ function parseTag(
const props = [] const props = []
const ns = context.options.getNamespace(tag, parent) const ns = context.options.getNamespace(tag, parent)
let tagType = ElementTypes.ELEMENT
if (tag === 'slot') tagType = ElementTypes.SLOT
else if (tag === 'template') tagType = ElementTypes.TEMPLATE
else if (/[A-Z-]/.test(tag)) tagType = ElementTypes.COMPONENT
advanceBy(context, match[0].length) advanceBy(context, match[0].length)
advanceSpaces(context) advanceSpaces(context)
@ -427,12 +432,6 @@ function parseTag(
advanceBy(context, isSelfClosing ? 2 : 1) advanceBy(context, isSelfClosing ? 2 : 1)
} }
let tagType = ElementTypes.ELEMENT
if (tag === 'slot') tagType = ElementTypes.SLOT
else if (tag === 'template') tagType = ElementTypes.TEMPLATE
else if (/[A-Z-]/.test(tag)) tagType = ElementTypes.COMPONENT
return { return {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
ns, ns,

View File

@ -39,7 +39,7 @@ export const transformElement: NodeTransform = (node, context) => {
node.tagType === ElementTypes.COMPONENT node.tagType === ElementTypes.COMPONENT
) { ) {
const isComponent = node.tagType === ElementTypes.COMPONENT const isComponent = node.tagType === ElementTypes.COMPONENT
const hasProps = node.props.length > 0 let hasProps = node.props.length > 0
const hasChildren = node.children.length > 0 const hasChildren = node.children.length > 0
let runtimeDirectives: DirectiveNode[] | undefined let runtimeDirectives: DirectiveNode[] | undefined
let componentIdentifier: string | undefined let componentIdentifier: string | undefined
@ -58,9 +58,18 @@ export const transformElement: NodeTransform = (node, context) => {
] ]
// props // props
if (hasProps) { if (hasProps) {
const { props, directives } = buildProps(node.props, node.loc, context) const { props, directives } = buildProps(
args.push(props) node.props,
node.loc,
context,
isComponent
)
runtimeDirectives = directives runtimeDirectives = directives
if (!props) {
hasProps = false
} else {
args.push(props)
}
} }
// children // children
if (hasChildren) { if (hasChildren) {
@ -104,9 +113,10 @@ type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
export function buildProps( export function buildProps(
props: ElementNode['props'], props: ElementNode['props'],
elementLoc: SourceLocation, elementLoc: SourceLocation,
context: TransformContext context: TransformContext,
isComponent: boolean = false
): { ): {
props: PropsExpression props: PropsExpression | undefined
directives: DirectiveNode[] directives: DirectiveNode[]
} { } {
let isStatic = true let isStatic = true
@ -141,6 +151,11 @@ export function buildProps(
// skip v-slot - it is handled by its dedicated transform. // skip v-slot - it is handled by its dedicated transform.
if (name === 'slot') { if (name === 'slot') {
if (!isComponent) {
context.onError(
createCompilerError(ErrorCodes.X_MISPLACED_V_SLOT, loc)
)
}
continue continue
} }
@ -197,7 +212,7 @@ export function buildProps(
} }
} }
let propsExpression: PropsExpression let propsExpression: PropsExpression | undefined = undefined
// has v-bind="object" or v-on="object", wrap with mergeProps // has v-bind="object" or v-on="object", wrap with mergeProps
if (mergeArgs.length) { if (mergeArgs.length) {
@ -216,7 +231,7 @@ export function buildProps(
// single v-bind with nothing else - no need for a mergeProps call // single v-bind with nothing else - no need for a mergeProps call
propsExpression = mergeArgs[0] propsExpression = mergeArgs[0]
} }
} else { } else if (properties.length) {
propsExpression = createObjectExpression( propsExpression = createObjectExpression(
dedupeProperties(properties), dedupeProperties(properties),
elementLoc elementLoc
@ -224,7 +239,7 @@ export function buildProps(
} }
// hoist the object if it's fully static // hoist the object if it's fully static
if (isStatic) { if (isStatic && propsExpression) {
propsExpression = context.hoist(propsExpression) propsExpression = context.hoist(propsExpression)
} }

View File

@ -64,7 +64,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
nameIndex > -1 nameIndex > -1
? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1)) ? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
: props : props
const hasProps = propsWithoutName.length let hasProps = propsWithoutName.length > 0
if (hasProps) { if (hasProps) {
const { props: propsExpression, directives } = buildProps( const { props: propsExpression, directives } = buildProps(
propsWithoutName, propsWithoutName,
@ -79,7 +79,11 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
) )
) )
} }
slotArgs.push(propsExpression) if (propsExpression) {
slotArgs.push(propsExpression)
} else {
hasProps = false
}
} }
if (children.length) { if (children.length) {