]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-core/compat): fix is prop usage on components
authorEvan You <yyx990803@gmail.com>
Mon, 21 Jun 2021 20:16:49 +0000 (16:16 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 21 Jun 2021 20:16:49 +0000 (16:16 -0400)
also fix v-bind:is usage on plain element in compat mode

fix #3934

packages/compiler-core/__tests__/transforms/transformElement.spec.ts
packages/compiler-core/src/parse.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/vue-compat/__tests__/compiler.spec.ts

index 33fdb7dba19720083f188a8bc38b41d1b9f94c14..a00cf0c5da99a20ac8a409d3f757d0dfee2f9e71 100644 (file)
@@ -57,8 +57,9 @@ function parseWithElementTransform(
   }
 }
 
-function parseWithBind(template: string) {
+function parseWithBind(template: string, options?: CompilerOptions) {
   return parseWithElementTransform(template, {
+    ...options,
     directiveTransforms: {
       bind: transformBind
     }
@@ -914,6 +915,18 @@ describe('compiler: element transform', () => {
         directives: undefined
       })
     })
+
+    // #3934
+    test('normal component with is prop', () => {
+      const { node, root } = parseWithBind(`<custom-input is="foo" />`, {
+        isNativeTag: () => false
+      })
+      expect(root.helpers).toContain(RESOLVE_COMPONENT)
+      expect(root.helpers).not.toContain(RESOLVE_DYNAMIC_COMPONENT)
+      expect(node).toMatchObject({
+        tag: '_component_custom_input'
+      })
+    })
   })
 
   test('<svg> should be forced into blocks', () => {
index 9580bdea232d35eb9e35944ffaca22b20f63d295..ebae0ce214d886966bf9675113e4071ce91ef8d4 100644 (file)
@@ -10,7 +10,8 @@ import {
   assert,
   advancePositionWithMutation,
   advancePositionWithClone,
-  isCoreComponent
+  isCoreComponent,
+  isBindKey
 } from './utils'
 import {
   Namespaces,
@@ -596,53 +597,21 @@ function parseTag(
   }
 
   let tagType = ElementTypes.ELEMENT
-  const options = context.options
-  if (!context.inVPre && !options.isCustomElement(tag)) {
-    const hasVIs = props.some(p => {
-      if (p.name !== 'is') return
-      // v-is="xxx" (TODO: deprecate)
-      if (p.type === NodeTypes.DIRECTIVE) {
-        return true
-      }
-      // is="vue:xxx"
-      if (p.value && p.value.content.startsWith('vue:')) {
-        return true
-      }
-      // in compat mode, any is usage is considered a component
+  if (!context.inVPre) {
+    if (tag === 'slot') {
+      tagType = ElementTypes.SLOT
+    } else if (tag === 'template') {
       if (
-        __COMPAT__ &&
-        checkCompatEnabled(
-          CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
-          context,
-          p.loc
+        props.some(
+          p =>
+            p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
         )
       ) {
-        return true
+        tagType = ElementTypes.TEMPLATE
       }
-    })
-    if (options.isNativeTag && !hasVIs) {
-      if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
-    } else if (
-      hasVIs ||
-      isCoreComponent(tag) ||
-      (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
-      /^[A-Z]/.test(tag) ||
-      tag === 'component'
-    ) {
+    } else if (isComponent(tag, props, context)) {
       tagType = ElementTypes.COMPONENT
     }
-
-    if (tag === 'slot') {
-      tagType = ElementTypes.SLOT
-    } else if (
-      tag === 'template' &&
-      props.some(
-        p =>
-          p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
-      )
-    ) {
-      tagType = ElementTypes.TEMPLATE
-    }
   }
 
   return {
@@ -658,6 +627,65 @@ function parseTag(
   }
 }
 
+function isComponent(
+  tag: string,
+  props: (AttributeNode | DirectiveNode)[],
+  context: ParserContext
+) {
+  const options = context.options
+  if (options.isCustomElement(tag)) {
+    return false
+  }
+  if (
+    tag === 'component' ||
+    /^[A-Z]/.test(tag) ||
+    isCoreComponent(tag) ||
+    (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
+    (options.isNativeTag && !options.isNativeTag(tag))
+  ) {
+    return true
+  }
+  // at this point the tag should be a native tag, but check for potential "is"
+  // casting
+  for (let i = 0; i < props.length; i++) {
+    const p = props[i]
+    if (p.type === NodeTypes.ATTRIBUTE) {
+      if (p.name === 'is' && p.value) {
+        if (p.value.content.startsWith('vue:')) {
+          return true
+        } else if (
+          __COMPAT__ &&
+          checkCompatEnabled(
+            CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
+            context,
+            p.loc
+          )
+        ) {
+          return true
+        }
+      }
+    } else {
+      // directive
+      // v-is (TODO Deprecate)
+      if (p.name === 'is') {
+        return true
+      } else if (
+        // :is on plain element - only treat as component in compat mode
+        p.name === 'bind' &&
+        isBindKey(p.arg, 'is') &&
+        __COMPAT__ &&
+        checkCompatEnabled(
+          CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
+          context,
+          p.loc
+        )
+      ) {
+        return true
+      }
+    }
+  }
+}
+
 function parseAttributes(
   context: ParserContext,
   type: TagType
index dba1a99b487d13bbdefb4773209edfecb3817876..36396c5d7d1a2770928d95bece432c717c8633a6 100644 (file)
@@ -240,16 +240,16 @@ export function resolveComponentType(
 
   // 1. dynamic component
   const isExplicitDynamic = isComponentTag(tag)
-  const isProp =
-    findProp(node, 'is') || (!isExplicitDynamic && findDir(node, 'is'))
+  const isProp = findProp(node, 'is')
   if (isProp) {
-    if (!isExplicitDynamic && isProp.type === NodeTypes.ATTRIBUTE) {
-      // <button is="vue:xxx">
-      // if not <component>, only is value that starts with "vue:" will be
-      // treated as component by the parse phase and reach here, unless it's
-      // compat mode where all is values are considered components
-      tag = isProp.value!.content.replace(/^vue:/, '')
-    } else {
+    if (
+      isExplicitDynamic ||
+      (__COMPAT__ &&
+        isCompatEnabled(
+          CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
+          context
+        ))
+    ) {
       const exp =
         isProp.type === NodeTypes.ATTRIBUTE
           ? isProp.value && createSimpleExpression(isProp.value.content, true)
@@ -259,9 +259,26 @@ export function resolveComponentType(
           exp
         ])
       }
+    } else if (
+      isProp.type === NodeTypes.ATTRIBUTE &&
+      isProp.value!.content.startsWith('vue:')
+    ) {
+      // <button is="vue:xxx">
+      // if not <component>, only is value that starts with "vue:" will be
+      // treated as component by the parse phase and reach here, unless it's
+      // compat mode where all is values are considered components
+      tag = isProp.value!.content.slice(4)
     }
   }
 
+  // 1.5 v-is (TODO: Deprecate)
+  const isDir = !isExplicitDynamic && findDir(node, 'is')
+  if (isDir && isDir.exp) {
+    return createCallExpression(context.helper(RESOLVE_DYNAMIC_COMPONENT), [
+      isDir.exp
+    ])
+  }
+
   // 2. built-in components (Teleport, Transition, KeepAlive, Suspense...)
   const builtIn = isCoreComponent(tag) || context.isBuiltInComponent(tag)
   if (builtIn) {
@@ -433,7 +450,13 @@ export function buildProps(
       // skip is on <component>, or is="vue:xxx"
       if (
         name === 'is' &&
-        (isComponentTag(tag) || (value && value.content.startsWith('vue:')))
+        (isComponentTag(tag) ||
+          (value && value.content.startsWith('vue:')) ||
+          (__COMPAT__ &&
+            isCompatEnabled(
+              CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
+              context
+            )))
       ) {
         continue
       }
@@ -473,7 +496,14 @@ export function buildProps(
       // skip v-is and :is on <component>
       if (
         name === 'is' ||
-        (isVBind && isComponentTag(tag) && isBindKey(arg, 'is'))
+        (isVBind &&
+          isBindKey(arg, 'is') &&
+          (isComponentTag(tag) ||
+            (__COMPAT__ &&
+              isCompatEnabled(
+                CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
+                context
+              ))))
       ) {
         continue
       }
index f06db1d30904291413890e18899801bd1f503a82..2b13233e3e6b0ad497648be39aab653d6f3f9914 100644 (file)
@@ -35,6 +35,22 @@ test('COMPILER_IS_ON_ELEMENT', () => {
   expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned()
 })
 
+test('COMPILER_IS_ON_ELEMENT (dynamic)', () => {
+  const MyButton = {
+    template: `<div><slot/></div>`
+  }
+
+  const vm = new Vue({
+    template: `<button :is="'MyButton'">text</button>`,
+    components: {
+      MyButton
+    }
+  }).$mount()
+
+  expect(vm.$el.outerHTML).toBe(`<div>text</div>`)
+  expect(CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT).toHaveBeenWarned()
+})
+
 test('COMPILER_V_BIND_SYNC', async () => {
   const MyButton = {
     props: ['foo'],