]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(compiler): test v-slot transform
authorEvan You <yyx990803@gmail.com>
Sat, 28 Sep 2019 18:05:10 +0000 (14:05 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 28 Sep 2019 18:05:10 +0000 (14:05 -0400)
packages/compiler-core/__tests__/parse.spec.ts
packages/compiler-core/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap [new file with mode: 0644]
packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts
packages/compiler-core/__tests__/transforms/vSlot.spec.ts
packages/compiler-core/src/errors.ts
packages/compiler-core/src/parse.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/transfromSlotOutlet.ts

index 08cc2260a5a8225f6ebb5e93b4832ed57f72b801..35e11f84e78655fe067cab19aad4f48cfe64b96a 100644 (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.', () => {
       const ast = parse('<div>hello</DIV>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 (file)
index 0000000..1ef1985
--- /dev/null
@@ -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)
+    ]
+  })
+}"
+`;
index f9fa59582f9b1ed8c2a599b87d051910a0b87334..5b16c27b6170dc4c60ad74fdc050bb6862c30115 100644 (file)
@@ -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 <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
+        }
+      }
+    })
+  })
 })
index 295d8d8d80ef45068f7c45228b47467455078838..cdc54fc8f11fa03bac7986c41c4232a7f81d2203 100644 (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 { 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<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
+        }
+      }
+    })
   })
 })
index 8efb901bc66b8a9e473bc41e154ecb68f24d6530..559a44fc681554fd416ec24a3f4b4540af89c993 100644 (file)
@@ -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 <template> 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.`
index 7343950c4cd851326101ba2e07583add1d5c07c3..128f70034e488527d0a0780b47b1347bb1ce6f86 100644 (file)
@@ -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,
index a076328affdb0a2a3d71a8f47a8569e265ea21bc..edfddca60635babdcb97742116e90ba7a540d124 100644 (file)
@@ -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)
   }
 
index 99f7354cd6aac489377146a6f452be570990dd52..1c4e07dd41abe8a7994fc53ea8b5f583adb9f625 100644 (file)
@@ -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) {