Skip to content

Commit 9edbf2f

Browse files
committed
Revert "Revert "⭐️New: Add vue/match-component-file-name rule (#668)""
This reverts commit f4eef78.
1 parent b48dc16 commit 9edbf2f

File tree

5 files changed

+1087
-0
lines changed

5 files changed

+1087
-0
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ For example:
135135
| Rule ID | Description | |
136136
|:--------|:------------|:---|
137137
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
138+
| [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | |
138139
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
139140

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

lib/index.js

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

0 commit comments

Comments
 (0)