diff --git a/packages/compiler-core/__tests__/parse.spec.ts b/packages/compiler-core/__tests__/parse.spec.ts
index 08cc2260a..35e11f84e 100644
--- a/packages/compiler-core/__tests__/parse.spec.ts
+++ b/packages/compiler-core/__tests__/parse.spec.ts
@@ -1234,6 +1234,50 @@ describe('compiler: parse', () => {
})
})
+ test('v-slot shorthand', () => {
+ const ast = parse('')
+ 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.', () => {
const ast = parse('
hello
after')
const element = ast.children[0] as ElementNode
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
new file mode 100644
index 000000000..1ef19853b
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
@@ -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)
+ ]
+ })
+}"
+`;
diff --git a/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts b/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts
index f9fa59582..5b16c27b6 100644
--- a/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts
@@ -3,7 +3,8 @@ import {
parse,
transform,
ElementNode,
- NodeTypes
+ NodeTypes,
+ ErrorCodes
} from '../../src'
import { transformElement } from '../../src/transforms/transformElement'
import { transformOn } from '../../src/transforms/vOn'
@@ -321,4 +322,27 @@ describe('compiler: transform outlets', () => {
]
})
})
+
+ test(`error on unexpected custom directive on `, () => {
+ const onError = jest.fn()
+ const source = ``
+ 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
+ }
+ }
+ })
+ })
})
diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
index 295d8d8d8..cdc54fc8f 100644
--- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts
@@ -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 { transformOn } from '../../src/transforms/vOn'
import { transformBind } from '../../src/transforms/vBind'
@@ -20,22 +28,411 @@ function parseWithSlots(template: string, options: CompilerOptions = {}) {
},
...options
})
- return ast
+ return {
+ root: ast,
+ slots: (ast.children[0] as ElementNode).codegenNode!.arguments[2]
+ }
+}
+
+function createSlotMatcher(obj: Record) {
+ 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', () => {
- test('generate slot', () => {
- const ast = parseWithSlots(
- `
-
-
- hello {{ dur }}
-
-
-`,
+ test('implicit default slot', () => {
+ const { root, slots } = parseWithSlots(``, {
+ prefixIdentifiers: true
+ })
+ expect(slots).toMatchObject(
+ createSlotMatcher({
+ default: {
+ 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(
+ `{{ foo }}{{ bar }}`,
{ prefixIdentifiers: true }
)
- const { code } = generate(ast, { prefixIdentifiers: true })
- console.log(code)
+ expect(slots).toMatchObject(
+ 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(
+ `
+
+ {{ foo }}{{ bar }}
+
+
+ {{ foo }}{{ bar }}
+
+ `,
+ { 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(
+ `
+
+ {{ foo }}{{ bar }}
+
+
+ {{ foo }}{{ bar }}
+
+ `,
+ { 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(
+ `
+
+
+ {{ foo }}{{ bar }}{{ baz }}
+
+ {{ foo }}{{ bar }}{{ baz }}
+
+ `,
+ { 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 = `foobar`
+ 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 = ``
+ 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 = ``
+ 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 = ``
+ 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 = `foo`
+ 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
+ }
+ }
+ })
})
})
diff --git a/packages/compiler-core/src/errors.ts b/packages/compiler-core/src/errors.ts
index 8efb901bc..559a44fc6 100644
--- a/packages/compiler-core/src/errors.ts
+++ b/packages/compiler-core/src/errors.ts
@@ -73,6 +73,7 @@ export const enum ErrorCodes {
X_MIXED_SLOT_USAGE,
X_DUPLICATE_SLOT_NAMES,
X_EXTRANEOUS_NON_SLOT_CHILDREN,
+ X_MISPLACED_V_SLOT,
// generic errors
X_PREFIX_ID_NOT_SUPPORTED,
@@ -155,6 +156,8 @@ export const errorMessages: { [code: number]: string } = {
[ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN]:
`Extraneous children found when component has explicit slots. ` +
`These children will be ignored.`,
+ [ErrorCodes.X_MISPLACED_V_SLOT]: `v-slot can only be used on components or tags.`,
+
// 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.`
diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts
index 7343950c4..128f70034 100644
--- a/packages/compiler-core/src/parse.ts
+++ b/packages/compiler-core/src/parse.ts
@@ -384,6 +384,11 @@ function parseTag(
const props = []
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)
advanceSpaces(context)
@@ -427,12 +432,6 @@ function parseTag(
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 {
type: NodeTypes.ELEMENT,
ns,
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index a076328af..edfddca60 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -39,7 +39,7 @@ export const transformElement: NodeTransform = (node, context) => {
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
let runtimeDirectives: DirectiveNode[] | undefined
let componentIdentifier: string | undefined
@@ -58,9 +58,18 @@ export const transformElement: NodeTransform = (node, context) => {
]
// props
if (hasProps) {
- const { props, directives } = buildProps(node.props, node.loc, context)
- args.push(props)
+ const { props, directives } = buildProps(
+ node.props,
+ node.loc,
+ context,
+ isComponent
+ )
runtimeDirectives = directives
+ if (!props) {
+ hasProps = false
+ } else {
+ args.push(props)
+ }
}
// children
if (hasChildren) {
@@ -104,9 +113,10 @@ type PropsExpression = ObjectExpression | CallExpression | ExpressionNode
export function buildProps(
props: ElementNode['props'],
elementLoc: SourceLocation,
- context: TransformContext
+ context: TransformContext,
+ isComponent: boolean = false
): {
- props: PropsExpression
+ props: PropsExpression | undefined
directives: DirectiveNode[]
} {
let isStatic = true
@@ -141,6 +151,11 @@ export function buildProps(
// skip v-slot - it is handled by its dedicated transform.
if (name === 'slot') {
+ if (!isComponent) {
+ context.onError(
+ createCompilerError(ErrorCodes.X_MISPLACED_V_SLOT, loc)
+ )
+ }
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
if (mergeArgs.length) {
@@ -216,7 +231,7 @@ export function buildProps(
// single v-bind with nothing else - no need for a mergeProps call
propsExpression = mergeArgs[0]
}
- } else {
+ } else if (properties.length) {
propsExpression = createObjectExpression(
dedupeProperties(properties),
elementLoc
@@ -224,7 +239,7 @@ export function buildProps(
}
// hoist the object if it's fully static
- if (isStatic) {
+ if (isStatic && propsExpression) {
propsExpression = context.hoist(propsExpression)
}
diff --git a/packages/compiler-core/src/transforms/transfromSlotOutlet.ts b/packages/compiler-core/src/transforms/transfromSlotOutlet.ts
index 99f7354cd..1c4e07dd4 100644
--- a/packages/compiler-core/src/transforms/transfromSlotOutlet.ts
+++ b/packages/compiler-core/src/transforms/transfromSlotOutlet.ts
@@ -64,7 +64,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
nameIndex > -1
? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1))
: props
- const hasProps = propsWithoutName.length
+ let hasProps = propsWithoutName.length > 0
if (hasProps) {
const { props: propsExpression, directives } = buildProps(
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) {