--- /dev/null
+// 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)
+ ]
+ })
+}"
+`;
-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'
},
...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', () => {
- test('generate slot', () => {
- const ast = parseWithSlots(
- `
-<Comp>
- <Comp v-slot="{ dur }">
- hello {{ dur }}
- </Comp>
-</Comp>
-`,
+ test('implicit default slot', () => {
+ const { root, slots } = parseWithSlots(`<Comp><div/></Comp>`, {
+ 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(
+ `<Comp v-slot="{ foo }">{{ foo }}{{ bar }}</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.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 }
)
- const { code } = generate(ast, { prefixIdentifiers: true })
- console.log(code)
+ 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
+ }
+ }
+ })
})
})
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
]
// 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) {
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
// 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
}
}
}
- let propsExpression: PropsExpression
+ let propsExpression: PropsExpression | undefined = undefined
// has v-bind="object" or v-on="object", wrap with mergeProps
if (mergeArgs.length) {
// 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
}
// hoist the object if it's fully static
- if (isStatic) {
+ if (isStatic && propsExpression) {
propsExpression = context.hoist(propsExpression)
}