Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
⭐️New: Add
vue/match-component-file-name
rule (#668)
* match-component-file-name rule * add tests when there is no name attribute and improve error message * apply suggestions from @armano2 * refactor to have file extensions in options * revert using the spread operator in tests * revert using the spread operator in invalid tests * refactor to handle Vue.component(...) and improve options * improved documentation with usage example * added tests recommended by @armano2 * ignore rule when file has multiple components * accept mixed cases between component name and file name * apply suggestions from @mysticatea * add quotes to file name in error message * improve docs * add shouldMatchCase option * Improve docs Co-Authored-By: rodrigopedra <rodrigo.pedra@gmail.com> * Update match-component-file-name.js * Update match-component-file-name.js
- Loading branch information
1 parent
78bd936
commit c6bbd95
Showing
5 changed files
with
1,075 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,204 @@ | ||
# require component name property to match its file name (vue/match-component-file-name) | ||
|
||
This rule reports if a component `name` property does not match its file name. | ||
|
||
You can define an array of file extensions this rule should verify for | ||
the component's name. | ||
|
||
## :book: Rule Details | ||
|
||
This rule has some options. | ||
|
||
```json | ||
{ | ||
"vue/match-component-file-name": ["error", { | ||
"extensions": ["jsx"], | ||
"shouldMatchCase": false | ||
}] | ||
} | ||
``` | ||
|
||
By default this rule will only verify components in a file with a `.jsx` | ||
extension. | ||
|
||
You can use any combination of `".jsx"`, `".vue"` and `".js"` extensions. | ||
|
||
You can also enforce same case between the component's name and its file name. | ||
|
||
If you are defining multiple components within the same file, this rule will be ignored. | ||
|
||
:-1: Examples of **incorrect** code for this rule: | ||
|
||
```jsx | ||
// file name: src/MyComponent.jsx | ||
export default { | ||
name: 'MComponent', // note the missing y | ||
render: () { | ||
return <h1>Hello world</h1> | ||
} | ||
} | ||
``` | ||
|
||
```vue | ||
// file name: src/MyComponent.vue | ||
// options: {extensions: ["vue"]} | ||
<script> | ||
export default { | ||
name: 'MComponent', | ||
template: '<div />' | ||
} | ||
</script> | ||
``` | ||
|
||
```js | ||
// file name: src/MyComponent.js | ||
// options: {extensions: ["js"]} | ||
new Vue({ | ||
name: 'MComponent', | ||
template: '<div />' | ||
}) | ||
``` | ||
|
||
```js | ||
// file name: src/MyComponent.js | ||
// options: {extensions: ["js"]} | ||
Vue.component('MComponent', { | ||
template: '<div />' | ||
}) | ||
``` | ||
|
||
```jsx | ||
// file name: src/MyComponent.jsx | ||
// options: {shouldMatchCase: true} | ||
export default { | ||
name: 'my-component', | ||
render() { return <div /> } | ||
} | ||
``` | ||
|
||
```jsx | ||
// file name: src/my-component.jsx | ||
// options: {shouldMatchCase: true} | ||
export default { | ||
name: 'MyComponent', | ||
render() { return <div /> } | ||
} | ||
``` | ||
|
||
:+1: Examples of **correct** code for this rule: | ||
|
||
```jsx | ||
// file name: src/MyComponent.jsx | ||
export default { | ||
name: 'MyComponent', | ||
render: () { | ||
return <h1>Hello world</h1> | ||
} | ||
} | ||
``` | ||
|
||
```jsx | ||
// file name: src/MyComponent.jsx | ||
// no name property defined | ||
export default { | ||
render: () { | ||
return <h1>Hello world</h1> | ||
} | ||
} | ||
``` | ||
|
||
```vue | ||
// file name: src/MyComponent.vue | ||
<script> | ||
export default { | ||
name: 'MyComponent', | ||
template: '<div />' | ||
} | ||
</script> | ||
``` | ||
|
||
```vue | ||
// file name: src/MyComponent.vue | ||
<script> | ||
export default { | ||
template: '<div />' | ||
} | ||
</script> | ||
``` | ||
|
||
```js | ||
// file name: src/MyComponent.js | ||
new Vue({ | ||
name: 'MyComponent', | ||
template: '<div />' | ||
}) | ||
``` | ||
|
||
```js | ||
// file name: src/MyComponent.js | ||
new Vue({ | ||
template: '<div />' | ||
}) | ||
``` | ||
|
||
```js | ||
// file name: src/MyComponent.js | ||
Vue.component('MyComponent', { | ||
template: '<div />' | ||
}) | ||
``` | ||
|
||
```js | ||
// file name: src/components.js | ||
// defines multiple components, so this rule is ignored | ||
Vue.component('MyComponent', { | ||
template: '<div />' | ||
}) | ||
|
||
Vue.component('OtherComponent', { | ||
template: '<div />' | ||
}) | ||
|
||
new Vue({ | ||
name: 'ThirdComponent', | ||
template: '<div />' | ||
}) | ||
``` | ||
|
||
```jsx | ||
// file name: src/MyComponent.jsx | ||
// options: {shouldMatchCase: true} | ||
export default { | ||
name: 'MyComponent', | ||
render() { return <div /> } | ||
} | ||
``` | ||
|
||
```jsx | ||
// file name: src/my-component.jsx | ||
// options: {shouldMatchCase: true} | ||
export default { | ||
name: 'my-component', | ||
render() { return <div /> } | ||
} | ||
``` | ||
|
||
## :wrench: Options | ||
|
||
```json | ||
{ | ||
"vue/match-component-file-name": ["error", { | ||
"extensions": ["jsx"], | ||
"shouldMatchCase": false | ||
}] | ||
} | ||
``` | ||
|
||
- `"extensions": []` ... array of file extensions to be verified. Default is set to `["jsx"]`. | ||
- `"shouldMatchCase": false` ... boolean indicating if component's name | ||
should also match its file name case. Default is set to `false`. | ||
|
||
## :books: Further reading | ||
|
||
- [Style guide - Single-file component filename casing](https://vuejs.org/v2/style-guide/#Single-file-component-filename-casing-strongly-recommended) | ||
|
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,140 @@ | ||
/** | ||
* @fileoverview Require component name property to match its file name | ||
* @author Rodrigo Pedra Brum <rodrigo.pedra@gmail.com> | ||
*/ | ||
'use strict' | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Requirements | ||
// ------------------------------------------------------------------------------ | ||
|
||
const utils = require('../utils') | ||
const casing = require('../utils/casing') | ||
const path = require('path') | ||
|
||
// ------------------------------------------------------------------------------ | ||
// Rule Definition | ||
// ------------------------------------------------------------------------------ | ||
|
||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: 'require component name property to match its file name', | ||
category: undefined, | ||
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.5/docs/rules/match-component-file-name.md' | ||
}, | ||
fixable: null, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
extensions: { | ||
type: 'array', | ||
items: { | ||
type: 'string' | ||
}, | ||
uniqueItems: true, | ||
additionalItems: false | ||
}, | ||
shouldMatchCase: { | ||
type: 'boolean' | ||
} | ||
}, | ||
additionalProperties: false | ||
} | ||
] | ||
}, | ||
|
||
create (context) { | ||
const options = context.options[0] | ||
const shouldMatchCase = (options && options.shouldMatchCase) || false | ||
const extensionsArray = options && options.extensions | ||
const allowedExtensions = Array.isArray(extensionsArray) ? extensionsArray : ['jsx'] | ||
|
||
const extension = path.extname(context.getFilename()) | ||
const filename = path.basename(context.getFilename(), extension) | ||
|
||
const errors = [] | ||
let componentCount = 0 | ||
|
||
if (!allowedExtensions.includes(extension.replace(/^\./, ''))) { | ||
return {} | ||
} | ||
|
||
// ---------------------------------------------------------------------- | ||
// Private | ||
// ---------------------------------------------------------------------- | ||
|
||
function compareNames (name, filename) { | ||
if (shouldMatchCase) { | ||
return name === filename | ||
} | ||
|
||
return casing.pascalCase(name) === filename || casing.kebabCase(name) === filename | ||
} | ||
|
||
function verifyName (node) { | ||
let name | ||
if (node.type === 'TemplateLiteral') { | ||
const quasis = node.quasis[0] | ||
name = quasis.value.cooked | ||
} else { | ||
name = node.value | ||
} | ||
|
||
if (!compareNames(name, filename)) { | ||
errors.push({ | ||
node: node, | ||
message: 'Component name `{{name}}` should match file name `{{filename}}`.', | ||
data: { filename, name } | ||
}) | ||
} | ||
} | ||
|
||
function canVerify (node) { | ||
return node.type === 'Literal' || ( | ||
node.type === 'TemplateLiteral' && | ||
node.expressions.length === 0 && | ||
node.quasis.length === 1 | ||
) | ||
} | ||
|
||
return Object.assign({}, | ||
{ | ||
"CallExpression > MemberExpression > Identifier[name='component']" (node) { | ||
const parent = node.parent.parent | ||
const calleeObject = utils.unwrapTypes(parent.callee.object) | ||
|
||
if (calleeObject.type === 'Identifier' && calleeObject.name === 'Vue') { | ||
if (parent.arguments && parent.arguments.length === 2) { | ||
const argument = parent.arguments[0] | ||
if (canVerify(argument)) { | ||
verifyName(argument) | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
utils.executeOnVue(context, (object) => { | ||
const node = object.properties | ||
.find(item => ( | ||
item.type === 'Property' && | ||
item.key.name === 'name' && | ||
canVerify(item.value) | ||
)) | ||
|
||
componentCount++ | ||
|
||
if (!node) return | ||
verifyName(node.value) | ||
}), | ||
{ | ||
'Program:exit' () { | ||
if (componentCount > 1) return | ||
|
||
errors.forEach((error) => context.report(error)) | ||
} | ||
} | ||
) | ||
} | ||
} |
Oops, something went wrong.