Repository: vuejs/eslint-plugin-vue (js)

Issue:
`vue/component-name-in-template-casing`: ignore `<component>` built-in element
<!--
  Before proposing rule changes, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**What rule do you want to change?**

- `vue/component-name-in-template-casing`

**Does this change cause the rule to produce more or fewer warnings?**

It will provide one fewer warning/error, because it will handle an edge-case.

**How will the change be implemented? (New option, new default behavior, etc.)?**

The change will treat `<component>` as an element, not a component.

The `vue/component-name-in-template-casing` rule enforces pascal-cased component names. This will also enforce `<component>` to be written as `<Component>`, which is not correct. Technically, `<component>` is not a component, but an element.

**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
The following code will be valid:
```vue
<template>
  <component is="div" />
</template>
```

**What does the rule currently do for this code?**

It will enforce `<component>` to be pascal-cased as `<Component>`:

```vue
<template>
  <Component is="div" />
</template>
```

**What will the rule do after it's changed?**

- Keep `<component>` as-is.

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->


As the [Vue docs state](https://vuejs.org/api/built-in-special-elements.html):

> [!NOTE]
> `<component>`, `<slot>` and `<template>` are component-like features and part of the template syntax. They are not true components and are compiled away during template compilation. As such, they are conventionally written with lowercase in templates.

The issue came up working with [`@antfu/eslint-config`](https://github.com/antfu/eslint-config). 
Related PR:
- https://github.com/antfu/eslint-config/pull/291

Pull request:
fix(component-name-in-template-casing): ignore vue template syntax
fix #2299

Patch:
diff --git a/lib/rules/component-name-in-template-casing.js b/lib/rules/component-name-in-template-casing.js
index fc1849b1..487af771 100644
--- a/lib/rules/component-name-in-template-casing.js
+++ b/lib/rules/component-name-in-template-casing.js
@@ -123,7 +123,8 @@ module.exports = {
       if (
         (!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
         utils.isHtmlWellKnownElementName(node.rawName) ||
-        utils.isSvgWellKnownElementName(node.rawName)
+        utils.isSvgWellKnownElementName(node.rawName) ||
+        utils.isVueBuiltInElementName(node.rawName)
       ) {
         return false
       }
diff --git a/lib/rules/require-toggle-inside-transition.js b/lib/rules/require-toggle-inside-transition.js
index 365fbc71..bdbb5c2d 100644
--- a/lib/rules/require-toggle-inside-transition.js
+++ b/lib/rules/require-toggle-inside-transition.js
@@ -47,7 +47,8 @@ module.exports = {
     return utils.defineTemplateBodyVisitor(context, {
       /** @param {VElement} node */
       "VElement[name='transition'] > VElement"(node) {
-        if (node.parent.children[0] !== node) {
+        const child = node.parent.children.find(utils.isVElement)
+        if (child !== node) {
           return
         }
         verifyInsideElement(node)
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 8702c626..22119ae9 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -54,6 +54,7 @@ const VUE2_BUILTIN_COMPONENT_NAMES = new Set(
 const VUE3_BUILTIN_COMPONENT_NAMES = new Set(
   require('./vue3-builtin-components')
 )
+const VUE_BUILTIN_ELEMENT_NAMES = new Set(require('./vue-builtin-elements'))
 const path = require('path')
 const vueEslintParser = require('vue-eslint-parser')
 const { traverseNodes, getFallbackKeys, NS } = vueEslintParser.AST
@@ -867,6 +868,15 @@ module.exports = {
     )
   },
 
+  /**
+   * Check whether the given name is Vue builtin element name or not.
+   * @param {string} name The name to check.
+   * @returns {boolean} `true` if the name is a builtin Vue element name
+   */
+  isVueBuiltInElementName(name) {
+    return VUE_BUILTIN_ELEMENT_NAMES.has(name.toLowerCase())
+  },
+
   /**
    * Check whether the given name is Vue builtin directive name or not.
    * @param {string} name The name to check.
diff --git a/lib/utils/vue-builtin-elements.js b/lib/utils/vue-builtin-elements.js
new file mode 100644
index 00000000..70ddb6b7
--- /dev/null
+++ b/lib/utils/vue-builtin-elements.js
@@ -0,0 +1,1 @@
+module.exports = ['template', 'slot', 'component']


Test patch:
diff --git a/tests/lib/rules/component-name-in-template-casing.js b/tests/lib/rules/component-name-in-template-casing.js
index 3aa4ee43..99206380 100644
--- a/tests/lib/rules/component-name-in-template-casing.js
+++ b/tests/lib/rules/component-name-in-template-casing.js
@@ -86,6 +86,10 @@ tester.run('component-name-in-template-casing', rule, {
       code: '<template><circle cx="0" cy="0" :d="radius"></template>',
       options: ['PascalCase', { registeredComponentsOnly: false }]
     },
+    {
+      code: '<template><component is="div" /></template>',
+      options: ['PascalCase', { registeredComponentsOnly: false }]
+    },
 
     // kebab-case
     {
@@ -108,6 +112,10 @@ tester.run('component-name-in-template-casing', rule, {
       code: '<template><math><mspace/></math></template>',
       options: ['kebab-case', { registeredComponentsOnly: false }]
     },
+    {
+      code: '<template><component is="div" /></template>',
+      options: ['kebab-case', { registeredComponentsOnly: false }]
+    },
 
     // ignores
     {
@@ -859,7 +867,7 @@ tester.run('component-name-in-template-casing', rule, {
       `,
       output: `
         <template>
-          <Component />
+          <component />
           <Suspense />
           <Teleport />
           <ClientOnly />
@@ -868,7 +876,6 @@ tester.run('component-name-in-template-casing', rule, {
       `,
       options: ['PascalCase', { registeredComponentsOnly: false }],
       errors: [
-        'Component name "component" is not PascalCase.',
         'Component name "suspense" is not PascalCase.',
         'Component name "teleport" is not PascalCase.',
         'Component name "client-only" is not PascalCase.',
@@ -1025,7 +1032,7 @@ tester.run('component-name-in-template-casing', rule, {
                 <HelloWorld3 />
                 <HelloWorld4 />
                 <HelloWorld5 />
-                <Component />
+                <component />
               </template>
             `,
             errors: [
@@ -1058,11 +1065,6 @@ tester.run('component-name-in-template-casing', rule, {
                 message: 'Component name "hello-world5" is not PascalCase.',
                 line: 18,
                 column: 17
-              },
-              {
-                message: 'Component name "component" is not PascalCase.',
-                line: 19,
-                column: 17
               }
             ]
           }
diff --git a/tests/lib/rules/require-toggle-inside-transition.js b/tests/lib/rules/require-toggle-inside-transition.js
index 73ec4720..0b64daaa 100644
--- a/tests/lib/rules/require-toggle-inside-transition.js
+++ b/tests/lib/rules/require-toggle-inside-transition.js
@@ -97,6 +97,11 @@ tester.run('require-toggle-inside-transition', rule, {
       filename: 'test.vue',
       code: '<template><transition><template v-for="e in list"><div /></template></transition></template>',
       errors: [{ messageId: 'expected' }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><Transition>  <div /></Transition></template>',
+      errors: [{ messageId: 'expected' }]
     }
   ]
 })