Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
vue/no-unregistered-components
rule (#1114)
* Add `vue/no-unregistered-components` rule * Remove rule from configs/essential Also remove text about where rule is included * Extend allowed ignore patterns * Add note about globally/mixins registered components + use strings for ingore patterns * (auto) update rules index * (auto) update rules lib index * Fix PR review's concerns - Correct rule categories (null) - Ignore `component :is="..."` component for well known HTML tags. - Ignore `component`, `suspense`, `teleport` as unknown components. * Add more rule tests to cover latest changes * Correct + add test for `suspense` and `teleport` * Restore not intended change in package.json * Progress with PR review * Handle edge case `<component is="div" />` * Progress with PR review - Remove no longer needed variable. - Handle edge case where a component is registered using kebab-case but later on is used using PascalCase. e.g: registered as `foo-bar` and used as `FooBar` is not valid. - Handle edge case `<component is />`, where `node.value` would be `null`. See https://github.com/mysticatea/vue-eslint-parser/blob/master/docs/ast.md#vattribute - Add more tests to prove all these changes * Remove unused block
- Loading branch information
Showing
5 changed files
with
858 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
})) | ||
} | ||
} |
Oops, something went wrong.