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( + ` + + + `, + { 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( + ` + + + `, + { 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( + ` + + `, + { 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 = `bar` + 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