Skip to content

Commit a74dd59

Browse files
Add multi-word-component-names rule (#1661)
* Add `multi-word-component-names` rule * Fix review comments for `multi-word-component-names` rule
1 parent 928e0c6 commit a74dd59

File tree

5 files changed

+454
-0
lines changed

5 files changed

+454
-0
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ For example:
296296
| [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |
297297
| [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: |
298298
| [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | |
299+
| [vue/multi-word-component-names](./multi-word-component-names.md) | require component names to be always multi-word | |
299300
| [vue/new-line-between-multi-line-property](./new-line-between-multi-line-property.md) | enforce new lines between multi-line properties in Vue components | :wrench: |
300301
| [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: |
301302
| [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` | |
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/multi-word-component-names
5+
description: require component names to be always multi-word
6+
---
7+
# vue/multi-word-component-names
8+
9+
> require component names to be always multi-word
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule require component names to be always multi-word, except for root `App`
16+
components, and built-in components provided by Vue, such as `<transition>` or
17+
`<component>`. This prevents conflicts with existing and future HTML elements,
18+
since all HTML elements are a single word.
19+
20+
<eslint-code-block filename="src/TodoItem.js" language="javascript" :rules="{'vue/multi-word-component-names': ['error']}">
21+
22+
```js
23+
/* ✓ GOOD */
24+
Vue.component('todo-item', {
25+
// ...
26+
})
27+
28+
/* ✗ BAD */
29+
Vue.component('Todo', {
30+
// ...
31+
})
32+
```
33+
</eslint-code-block>
34+
35+
<eslint-code-block filename="src/TodoItem.js" :rules="{'vue/multi-word-component-names': ['error']}">
36+
37+
```vue
38+
<script>
39+
/* ✓ GOOD */
40+
export default {
41+
name: 'TodoItem',
42+
// ...
43+
}
44+
</script>
45+
```
46+
</eslint-code-block>
47+
48+
<eslint-code-block filename="src/Todo.vue" :rules="{'vue/multi-word-component-names': ['error']}">
49+
50+
```vue
51+
<script>
52+
/* ✗ BAD */
53+
export default {
54+
name: 'Todo',
55+
// ...
56+
}
57+
</script>
58+
```
59+
</eslint-code-block>
60+
61+
<eslint-code-block filename="src/Todo.vue" :rules="{'vue/multi-word-component-names': ['error']}">
62+
63+
```vue
64+
<script>
65+
/* ✗ BAD */
66+
export default {
67+
// ...
68+
}
69+
</script>
70+
```
71+
</eslint-code-block>
72+
73+
## :wrench: Options
74+
75+
Nothing.
76+
77+
## :books: Further Reading
78+
79+
- [Style guide - Multi-word component names](https://vuejs.org/v2/style-guide/#Multi-word-component-names-essential)
80+
81+
## :mag: Implementation
82+
83+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/multi-word-component-names.js)
84+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/multi-word-component-names.js)

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ module.exports = {
4747
'match-component-file-name': require('./rules/match-component-file-name'),
4848
'max-attributes-per-line': require('./rules/max-attributes-per-line'),
4949
'max-len': require('./rules/max-len'),
50+
'multi-word-component-names': require('./rules/multi-word-component-names'),
5051
'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'),
5152
'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
5253
'name-property-casing': require('./rules/name-property-casing'),
+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @author Marton Csordas
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const casing = require('../utils/casing')
12+
const utils = require('../utils')
13+
14+
const RESERVED_NAMES_IN_VUE3 = new Set(
15+
require('../utils/vue3-builtin-components')
16+
)
17+
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+
35+
// ------------------------------------------------------------------------------
36+
// Rule Definition
37+
// ------------------------------------------------------------------------------
38+
39+
module.exports = {
40+
meta: {
41+
type: 'suggestion',
42+
docs: {
43+
description: 'require component names to be always multi-word',
44+
categories: undefined,
45+
url: 'https://eslint.vuejs.org/rules/multi-word-component-names.html'
46+
},
47+
schema: [],
48+
messages: {
49+
unexpected: 'Component name "{{value}}" should always be multi-word.'
50+
}
51+
},
52+
/** @param {RuleContext} context */
53+
create(context) {
54+
const fileName = context.getFilename()
55+
let componentName = fileName.replace(/\.[^/.]+$/, '')
56+
57+
return utils.compositingVisitors(
58+
{
59+
/** @param {Program} node */
60+
Program(node) {
61+
if (
62+
!node.body.length &&
63+
utils.isVueFile(fileName) &&
64+
!isValidComponentName(componentName)
65+
) {
66+
context.report({
67+
messageId: 'unexpected',
68+
data: {
69+
value: componentName
70+
},
71+
loc: { line: 1, column: 0 }
72+
})
73+
}
74+
}
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+
})
113+
)
114+
}
115+
}

0 commit comments

Comments
 (0)