Skip to content

Commit c6bbd95

Browse files
rodrigopedraota-meshi
authored andcommitted
⭐️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 <[email protected]> * Update match-component-file-name.js * Update match-component-file-name.js
1 parent 78bd936 commit c6bbd95

File tree

5 files changed

+1075
-0
lines changed

5 files changed

+1075
-0
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
237237

238238
| | Rule ID | Description |
239239
|:---|:--------|:------------|
240+
| | [vue/match-component-file-name](./docs/rules/match-component-file-name.md) | require component name property to match its file name |
240241
| :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `<script>` |
241242

242243
### Deprecated

Diff for: docs/rules/match-component-file-name.md

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# require component name property to match its file name (vue/match-component-file-name)
2+
3+
This rule reports if a component `name` property does not match its file name.
4+
5+
You can define an array of file extensions this rule should verify for
6+
the component's name.
7+
8+
## :book: Rule Details
9+
10+
This rule has some options.
11+
12+
```json
13+
{
14+
"vue/match-component-file-name": ["error", {
15+
"extensions": ["jsx"],
16+
"shouldMatchCase": false
17+
}]
18+
}
19+
```
20+
21+
By default this rule will only verify components in a file with a `.jsx`
22+
extension.
23+
24+
You can use any combination of `".jsx"`, `".vue"` and `".js"` extensions.
25+
26+
You can also enforce same case between the component's name and its file name.
27+
28+
If you are defining multiple components within the same file, this rule will be ignored.
29+
30+
:-1: Examples of **incorrect** code for this rule:
31+
32+
```jsx
33+
// file name: src/MyComponent.jsx
34+
export default {
35+
name: 'MComponent', // note the missing y
36+
render: () {
37+
return <h1>Hello world</h1>
38+
}
39+
}
40+
```
41+
42+
```vue
43+
// file name: src/MyComponent.vue
44+
// options: {extensions: ["vue"]}
45+
<script>
46+
export default {
47+
name: 'MComponent',
48+
template: '<div />'
49+
}
50+
</script>
51+
```
52+
53+
```js
54+
// file name: src/MyComponent.js
55+
// options: {extensions: ["js"]}
56+
new Vue({
57+
name: 'MComponent',
58+
template: '<div />'
59+
})
60+
```
61+
62+
```js
63+
// file name: src/MyComponent.js
64+
// options: {extensions: ["js"]}
65+
Vue.component('MComponent', {
66+
template: '<div />'
67+
})
68+
```
69+
70+
```jsx
71+
// file name: src/MyComponent.jsx
72+
// options: {shouldMatchCase: true}
73+
export default {
74+
name: 'my-component',
75+
render() { return <div /> }
76+
}
77+
```
78+
79+
```jsx
80+
// file name: src/my-component.jsx
81+
// options: {shouldMatchCase: true}
82+
export default {
83+
name: 'MyComponent',
84+
render() { return <div /> }
85+
}
86+
```
87+
88+
:+1: Examples of **correct** code for this rule:
89+
90+
```jsx
91+
// file name: src/MyComponent.jsx
92+
export default {
93+
name: 'MyComponent',
94+
render: () {
95+
return <h1>Hello world</h1>
96+
}
97+
}
98+
```
99+
100+
```jsx
101+
// file name: src/MyComponent.jsx
102+
// no name property defined
103+
export default {
104+
render: () {
105+
return <h1>Hello world</h1>
106+
}
107+
}
108+
```
109+
110+
```vue
111+
// file name: src/MyComponent.vue
112+
<script>
113+
export default {
114+
name: 'MyComponent',
115+
template: '<div />'
116+
}
117+
</script>
118+
```
119+
120+
```vue
121+
// file name: src/MyComponent.vue
122+
<script>
123+
export default {
124+
template: '<div />'
125+
}
126+
</script>
127+
```
128+
129+
```js
130+
// file name: src/MyComponent.js
131+
new Vue({
132+
name: 'MyComponent',
133+
template: '<div />'
134+
})
135+
```
136+
137+
```js
138+
// file name: src/MyComponent.js
139+
new Vue({
140+
template: '<div />'
141+
})
142+
```
143+
144+
```js
145+
// file name: src/MyComponent.js
146+
Vue.component('MyComponent', {
147+
template: '<div />'
148+
})
149+
```
150+
151+
```js
152+
// file name: src/components.js
153+
// defines multiple components, so this rule is ignored
154+
Vue.component('MyComponent', {
155+
template: '<div />'
156+
})
157+
158+
Vue.component('OtherComponent', {
159+
template: '<div />'
160+
})
161+
162+
new Vue({
163+
name: 'ThirdComponent',
164+
template: '<div />'
165+
})
166+
```
167+
168+
```jsx
169+
// file name: src/MyComponent.jsx
170+
// options: {shouldMatchCase: true}
171+
export default {
172+
name: 'MyComponent',
173+
render() { return <div /> }
174+
}
175+
```
176+
177+
```jsx
178+
// file name: src/my-component.jsx
179+
// options: {shouldMatchCase: true}
180+
export default {
181+
name: 'my-component',
182+
render() { return <div /> }
183+
}
184+
```
185+
186+
## :wrench: Options
187+
188+
```json
189+
{
190+
"vue/match-component-file-name": ["error", {
191+
"extensions": ["jsx"],
192+
"shouldMatchCase": false
193+
}]
194+
}
195+
```
196+
197+
- `"extensions": []` ... array of file extensions to be verified. Default is set to `["jsx"]`.
198+
- `"shouldMatchCase": false` ... boolean indicating if component's name
199+
should also match its file name case. Default is set to `false`.
200+
201+
## :books: Further reading
202+
203+
- [Style guide - Single-file component filename casing](https://vuejs.org/v2/style-guide/#Single-file-component-filename-casing-strongly-recommended)
204+

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = {
1818
'html-quotes': require('./rules/html-quotes'),
1919
'html-self-closing': require('./rules/html-self-closing'),
2020
'jsx-uses-vars': require('./rules/jsx-uses-vars'),
21+
'match-component-file-name': require('./rules/match-component-file-name'),
2122
'max-attributes-per-line': require('./rules/max-attributes-per-line'),
2223
'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'),
2324
'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),

Diff for: lib/rules/match-component-file-name.js

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
* @fileoverview Require component name property to match its file name
3+
* @author Rodrigo Pedra Brum <[email protected]>
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
const casing = require('../utils/casing')
13+
const path = require('path')
14+
15+
// ------------------------------------------------------------------------------
16+
// Rule Definition
17+
// ------------------------------------------------------------------------------
18+
19+
module.exports = {
20+
meta: {
21+
docs: {
22+
description: 'require component name property to match its file name',
23+
category: undefined,
24+
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.5/docs/rules/match-component-file-name.md'
25+
},
26+
fixable: null,
27+
schema: [
28+
{
29+
type: 'object',
30+
properties: {
31+
extensions: {
32+
type: 'array',
33+
items: {
34+
type: 'string'
35+
},
36+
uniqueItems: true,
37+
additionalItems: false
38+
},
39+
shouldMatchCase: {
40+
type: 'boolean'
41+
}
42+
},
43+
additionalProperties: false
44+
}
45+
]
46+
},
47+
48+
create (context) {
49+
const options = context.options[0]
50+
const shouldMatchCase = (options && options.shouldMatchCase) || false
51+
const extensionsArray = options && options.extensions
52+
const allowedExtensions = Array.isArray(extensionsArray) ? extensionsArray : ['jsx']
53+
54+
const extension = path.extname(context.getFilename())
55+
const filename = path.basename(context.getFilename(), extension)
56+
57+
const errors = []
58+
let componentCount = 0
59+
60+
if (!allowedExtensions.includes(extension.replace(/^\./, ''))) {
61+
return {}
62+
}
63+
64+
// ----------------------------------------------------------------------
65+
// Private
66+
// ----------------------------------------------------------------------
67+
68+
function compareNames (name, filename) {
69+
if (shouldMatchCase) {
70+
return name === filename
71+
}
72+
73+
return casing.pascalCase(name) === filename || casing.kebabCase(name) === filename
74+
}
75+
76+
function verifyName (node) {
77+
let name
78+
if (node.type === 'TemplateLiteral') {
79+
const quasis = node.quasis[0]
80+
name = quasis.value.cooked
81+
} else {
82+
name = node.value
83+
}
84+
85+
if (!compareNames(name, filename)) {
86+
errors.push({
87+
node: node,
88+
message: 'Component name `{{name}}` should match file name `{{filename}}`.',
89+
data: { filename, name }
90+
})
91+
}
92+
}
93+
94+
function canVerify (node) {
95+
return node.type === 'Literal' || (
96+
node.type === 'TemplateLiteral' &&
97+
node.expressions.length === 0 &&
98+
node.quasis.length === 1
99+
)
100+
}
101+
102+
return Object.assign({},
103+
{
104+
"CallExpression > MemberExpression > Identifier[name='component']" (node) {
105+
const parent = node.parent.parent
106+
const calleeObject = utils.unwrapTypes(parent.callee.object)
107+
108+
if (calleeObject.type === 'Identifier' && calleeObject.name === 'Vue') {
109+
if (parent.arguments && parent.arguments.length === 2) {
110+
const argument = parent.arguments[0]
111+
if (canVerify(argument)) {
112+
verifyName(argument)
113+
}
114+
}
115+
}
116+
}
117+
},
118+
utils.executeOnVue(context, (object) => {
119+
const node = object.properties
120+
.find(item => (
121+
item.type === 'Property' &&
122+
item.key.name === 'name' &&
123+
canVerify(item.value)
124+
))
125+
126+
componentCount++
127+
128+
if (!node) return
129+
verifyName(node.value)
130+
}),
131+
{
132+
'Program:exit' () {
133+
if (componentCount > 1) return
134+
135+
errors.forEach((error) => context.report(error))
136+
}
137+
}
138+
)
139+
}
140+
}

0 commit comments

Comments
 (0)