Skip to content

Add vue/no-unregistered-components rule #1114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 15, 2020
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ For example:
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
| [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | |
| [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | |
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
Expand Down
137 changes: 137 additions & 0 deletions docs/rules/no-unregistered-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/no-unregistered-components
description: disallow using components that are not registered inside templates
---
# vue/no-unregistered-components
> disallow using components that are not registered inside templates

## :book: Rule Details

This rule reports components that haven't been registered and are being used in the template.

::: warning Note
This rule cannot check globally registered components and components registered in mixins
unless you add them as part of the ignored patterns. `component`, `suspense` and `teleport`
are ignored by default.
:::

<eslint-code-block :rules="{'vue/no-unregistered-components': ['error']}">

```vue
<!-- ✓ GOOD -->
<template>
<div>
<h2>Lorem ipsum</h2>
<the-modal>
<component is="TheInput" />
<component :is="'TheDropdown'" />
<TheButton>CTA</TheButton>
</the-modal>
</div>
</template>

<script>
import TheButton from 'components/TheButton.vue'
import TheModal from 'components/TheModal.vue'
import TheInput from 'components/TheInput.vue'
import TheDropdown from 'components/TheDropdown.vue'

export default {
components: {
TheButton,
TheModal,
TheInput,
TheDropdown,
}
}
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/no-unregistered-components': ['error']}">

```vue
<!-- ✗ BAD -->
<template>
<div>
<h2>Lorem ipsum</h2>
<TheModal />
</div>
</template>

<script>
export default {
components: {

}
}
</script>
```

</eslint-code-block>

## :wrench: Options

```json
{
"vue/no-unregistered-components": ["error", {
"ignorePatterns": []
}]
}
```

- `ignorePatterns` Suppresses all errors if component name matches one or more patterns.

### `ignorePatterns: ['custom(\\-\\w+)+']`

<eslint-code-block :rules="{'vue/no-unregistered-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">

```vue
<!-- ✓ GOOD -->
<template>
<div>
<h2>Lorem ipsum</h2>
<CustomComponent />
</div>
</template>

<script>
export default {
components: {

},
}
</script>
```

</eslint-code-block>

<eslint-code-block :rules="{'vue/no-unregistered-components': ['error', { 'ignorePatterns': ['custom(\\-\\w+)+'] }]}">

```vue
<!-- ✗ BAD -->
<template>
<div>
<h2>Lorem ipsum</h2>
<WarmButton />
</div>
</template>

<script>
export default {
components: {

},
}
</script>
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unregistered-components.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unregistered-components.js)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ module.exports = {
'no-template-shadow': require('./rules/no-template-shadow'),
'no-template-target-blank': require('./rules/no-template-target-blank'),
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
'no-unregistered-components': require('./rules/no-unregistered-components'),
'no-unsupported-features': require('./rules/no-unsupported-features'),
'no-unused-components': require('./rules/no-unused-components'),
'no-unused-vars': require('./rules/no-unused-vars'),
Expand Down
153 changes: 153 additions & 0 deletions lib/rules/no-unregistered-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @fileoverview Report used components that are not registered
* @author Jesús Ángel González Novez
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('eslint-plugin-vue/lib/utils')
const casing = require('eslint-plugin-vue/lib/utils/casing')

// ------------------------------------------------------------------------------
// Rule helpers
// ------------------------------------------------------------------------------

const VUE_BUILT_IN_COMPONENTS = [
'component',
'suspense',
'teleport',
'transition',
'transition-group',
'keep-alive',
'slot'
]
/**
* Check whether the given node is a built-in component or not.
*
* Includes `suspense` and `teleport` from Vue 3.
*
* @param {ASTNode} node The start tag node to check.
* @returns {boolean} `true` if the node is a built-in component.
*/
const isBuiltInComponent = (node) => {
const rawName = node && casing.kebabCase(node.rawName)
return utils.isHtmlElementNode(node) &&
!utils.isHtmlWellKnownElementName(node.rawName) &&
VUE_BUILT_IN_COMPONENTS.indexOf(rawName) > -1
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow using components that are not registered inside templates',
categories: null,
recommended: false,
url: 'https://eslint.vuejs.org/rules/no-unregistered-components.html'
},
fixable: null,
schema: [{
type: 'object',
properties: {
ignorePatterns: {
type: 'array'
}
},
additionalProperties: false
}]
},

create (context) {
const options = context.options[0] || {}
const ignorePatterns = options.ignorePatterns || []
const usedComponentNodes = []
const registeredComponents = []

return utils.defineTemplateBodyVisitor(context, {
VElement (node) {
if (
(!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
utils.isHtmlWellKnownElementName(node.rawName) ||
utils.isSvgWellKnownElementName(node.rawName) ||
isBuiltInComponent(node)
) {
return
}

usedComponentNodes.push({ node, name: node.rawName })
},
"VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']" (node) {
if (
!node.value ||
node.value.type !== 'VExpressionContainer' ||
!node.value.expression
) return

if (node.value.expression.type === 'Literal') {
if (utils.isHtmlWellKnownElementName(node.value.expression.value)) return
usedComponentNodes.push({ node, name: node.value.expression.value })
}
},
"VAttribute[directive=false][key.name='is']" (node) {
if (
!node.value || // `<component is />`
utils.isHtmlWellKnownElementName(node.value.value)
) return
usedComponentNodes.push({ node, name: node.value.value })
},
"VElement[name='template']:exit" () {
// All registered components, transformed to kebab-case
const registeredComponentNames = registeredComponents
.map(({ name }) => casing.kebabCase(name))

// All registered components using kebab-case syntax
const componentsRegisteredAsKebabCase = registeredComponents
.filter(({ name }) => name === casing.kebabCase(name))
.map(({ name }) => name)

usedComponentNodes
.filter(({ name }) => {
const kebabCaseName = casing.kebabCase(name)

// Check ignored patterns in first place
if (ignorePatterns.find(pattern => {
const regExp = new RegExp(pattern)
return regExp.test(kebabCaseName) ||
regExp.test(casing.pascalCase(name)) ||
regExp.test(casing.camelCase(name)) ||
regExp.test(casing.snakeCase(name)) ||
regExp.test(name)
})) return false

// Component registered as `foo-bar` cannot be used as `FooBar`
if (
name.indexOf('-') === -1 &&
name === casing.pascalCase(name) &&
componentsRegisteredAsKebabCase.indexOf(kebabCaseName) !== -1
) {
return true
}

// Otherwise
return registeredComponentNames.indexOf(kebabCaseName) === -1
})
.forEach(({ node, name }) => context.report({
node,
message: 'The "{{name}}" component has been used but not registered.',
data: {
name
}
}))
}
}, utils.executeOnVue(context, (obj) => {
registeredComponents.push(...utils.getRegisteredComponents(obj))
}))
}
}
Loading