Skip to content

Commit ecc1ae5

Browse files
authored
Add ignore option to vue/multi-word-component-names rule (#1681)
1 parent 166dfbf commit ecc1ae5

File tree

3 files changed

+143
-62
lines changed

3 files changed

+143
-62
lines changed

docs/rules/multi-word-component-names.md

+39-2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export default {
6565
<eslint-code-block filename="src/Todo.vue" :rules="{'vue/multi-word-component-names': ['error']}">
6666

6767
```vue
68+
<!-- filename: Todo.vue -->
6869
<script>
6970
/* ✗ BAD */
7071
export default {
@@ -77,11 +78,47 @@ export default {
7778

7879
## :wrench: Options
7980

80-
Nothing.
81+
```json
82+
{
83+
"vue/multi-word-component-names": ["error", {
84+
"ignores": []
85+
}]
86+
}
87+
```
88+
89+
- `ignores` (`string[]`) ... The component names to ignore. Sets the component name to allow.
90+
91+
### `ignores: ["Todo"]`
92+
93+
<eslint-code-block fix :rules="{'vue/multi-word-component-names': ['error', {ignores: ['Todo']}]}">
94+
95+
```vue
96+
<script>
97+
export default {
98+
/* ✓ GOOD */
99+
name: 'Todo'
100+
}
101+
</script>
102+
```
103+
104+
</eslint-code-block>
105+
106+
<eslint-code-block fix :rules="{'vue/multi-word-component-names': ['error', {ignores: ['Todo']}]}">
107+
108+
```vue
109+
<script>
110+
export default {
111+
/* ✗ BAD */
112+
name: 'Item'
113+
}
114+
</script>
115+
```
116+
117+
</eslint-code-block>
81118

82119
## :books: Further Reading
83120

84-
- [Style guide - Multi-word component names](https://vuejs.org/v2/style-guide/#Multi-word-component-names-essential)
121+
- [Style guide - Multi-word component names](https://v3.vuejs.org/style-guide/#multi-word-component-names-essential)
85122

86123
## :rocket: Version
87124

lib/rules/multi-word-component-names.js

+76-60
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,6 @@ const RESERVED_NAMES_IN_VUE3 = new Set(
1515
require('../utils/vue3-builtin-components')
1616
)
1717

18-
// ------------------------------------------------------------------------------
19-
// Helpers
20-
// ------------------------------------------------------------------------------
21-
22-
/**
23-
* Returns true if the given component name is valid, otherwise false.
24-
* @param {string} name
25-
* */
26-
function isValidComponentName(name) {
27-
if (name.toLowerCase() === 'app' || RESERVED_NAMES_IN_VUE3.has(name)) {
28-
return true
29-
} else {
30-
const elements = casing.kebabCase(name).split('-')
31-
return elements.length > 1
32-
}
33-
}
34-
3518
// ------------------------------------------------------------------------------
3619
// Rule Definition
3720
// ------------------------------------------------------------------------------
@@ -44,22 +27,92 @@ module.exports = {
4427
categories: ['vue3-essential', 'essential'],
4528
url: 'https://eslint.vuejs.org/rules/multi-word-component-names.html'
4629
},
47-
schema: [],
30+
schema: [
31+
{
32+
type: 'object',
33+
properties: {
34+
ignores: {
35+
type: 'array',
36+
items: { type: 'string' },
37+
uniqueItems: true,
38+
additionalItems: false
39+
}
40+
},
41+
additionalProperties: false
42+
}
43+
],
4844
messages: {
4945
unexpected: 'Component name "{{value}}" should always be multi-word.'
5046
}
5147
},
5248
/** @param {RuleContext} context */
5349
create(context) {
54-
const fileName = context.getFilename()
55-
let componentName = fileName.replace(/\.[^/.]+$/, '')
50+
/** @type {Set<string>} */
51+
const ignores = new Set()
52+
ignores.add('App')
53+
ignores.add('app')
54+
for (const ignore of (context.options[0] && context.options[0].ignores) ||
55+
[]) {
56+
ignores.add(ignore)
57+
if (casing.isPascalCase(ignore)) {
58+
// PascalCase
59+
ignores.add(casing.kebabCase(ignore))
60+
}
61+
}
62+
let hasVue = false
63+
let hasName = false
64+
65+
/**
66+
* Returns true if the given component name is valid, otherwise false.
67+
* @param {string} name
68+
* */
69+
function isValidComponentName(name) {
70+
if (ignores.has(name) || RESERVED_NAMES_IN_VUE3.has(name)) {
71+
return true
72+
}
73+
const elements = casing.kebabCase(name).split('-')
74+
return elements.length > 1
75+
}
76+
77+
/**
78+
* @param {Expression | SpreadElement} nameNode
79+
*/
80+
function validateName(nameNode) {
81+
if (nameNode.type !== 'Literal') return
82+
const componentName = `${nameNode.value}`
83+
if (!isValidComponentName(componentName)) {
84+
context.report({
85+
node: nameNode,
86+
messageId: 'unexpected',
87+
data: {
88+
value: componentName
89+
}
90+
})
91+
}
92+
}
5693

5794
return utils.compositingVisitors(
95+
utils.executeOnCallVueComponent(context, (node) => {
96+
hasVue = true
97+
if (node.arguments.length !== 2) return
98+
hasName = true
99+
validateName(node.arguments[0])
100+
}),
101+
utils.executeOnVue(context, (obj) => {
102+
hasVue = true
103+
const node = utils.findProperty(obj, 'name')
104+
if (!node) return
105+
hasName = true
106+
validateName(node.value)
107+
}),
58108
{
59109
/** @param {Program} node */
60-
Program(node) {
110+
'Program:exit'(node) {
111+
if (hasName) return
112+
if (!hasVue && node.body.length > 0) return
113+
const fileName = context.getFilename()
114+
const componentName = fileName.replace(/\.[^/.]+$/, '')
61115
if (
62-
!node.body.length &&
63116
utils.isVueFile(fileName) &&
64117
!isValidComponentName(componentName)
65118
) {
@@ -72,44 +125,7 @@ module.exports = {
72125
})
73126
}
74127
}
75-
},
76-
77-
utils.executeOnVue(context, (obj) => {
78-
const node = utils.findProperty(obj, 'name')
79-
80-
/** @type {SourceLocation | null} */
81-
let loc = null
82-
83-
// Check if the component has a name property.
84-
if (node) {
85-
const valueNode = node.value
86-
if (valueNode.type !== 'Literal') return
87-
88-
componentName = `${valueNode.value}`
89-
loc = node.loc
90-
} else if (
91-
obj.parent.type === 'CallExpression' &&
92-
obj.parent.arguments.length === 2
93-
) {
94-
// The component is registered globally with 'Vue.component', where
95-
// the first paremter is the component name.
96-
const argument = obj.parent.arguments[0]
97-
if (argument.type !== 'Literal') return
98-
99-
componentName = `${argument.value}`
100-
loc = argument.loc
101-
}
102-
103-
if (!isValidComponentName(componentName)) {
104-
context.report({
105-
messageId: 'unexpected',
106-
data: {
107-
value: componentName
108-
},
109-
loc: loc || { line: 1, column: 0 }
110-
})
111-
}
112-
})
128+
}
113129
)
114130
}
115131
}

tests/lib/rules/multi-word-component-names.js

+28
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ tester.run('multi-word-component-names', rule, {
158158
Vue.component('TheTest', {})
159159
</script>
160160
`
161+
},
162+
{
163+
filename: 'test.vue',
164+
options: [{ ignores: ['Todo'] }],
165+
code: `
166+
<script>
167+
export default {
168+
name: 'Todo'
169+
}
170+
</script>
171+
`
161172
}
162173
],
163174
invalid: [
@@ -248,6 +259,23 @@ tester.run('multi-word-component-names', rule, {
248259
line: 3
249260
}
250261
]
262+
},
263+
{
264+
filename: 'test.vue',
265+
options: [{ ignores: ['Todo'] }],
266+
code: `
267+
<script>
268+
export default {
269+
name: 'Item'
270+
}
271+
</script>
272+
`,
273+
errors: [
274+
{
275+
message: 'Component name "Item" should always be multi-word.',
276+
line: 4
277+
}
278+
]
251279
}
252280
]
253281
})

0 commit comments

Comments
 (0)