Skip to content

Commit 9fff64d

Browse files
authored
New rule order-in-components (#42)
* Add new rule "order-in-components" * Add initial implementation of `order-in-components` rule * Update test scripts * Improve order-in-components rule, add more test scenarios * Update readme * Update order-in-components docs * Update rule logic and fix tests * Fix order logic * Check for arguments existance * Apply order-in-components rule only to exported ObjectExpressions in .vue and .jsx files * Disable recommended setting in `order-in-components` rule
1 parent a800d96 commit 9fff64d

File tree

7 files changed

+518
-3
lines changed

7 files changed

+518
-3
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ The `--fix` option on the command line automatically fixes problems reported by
6969
| :white_check_mark: | [no-confusing-v-for-v-if](./docs/rules/no-confusing-v-for-v-if.md) | disallow confusing `v-for` and `v-if` on the same element. |
7070
| | [no-duplicate-attributes](./docs/rules/no-duplicate-attributes.md) | disallow duplicate arguments. |
7171
| :white_check_mark: | [no-textarea-mustache](./docs/rules/no-textarea-mustache.md) | disallow mustaches in `<textarea>`. |
72+
| | [order-in-components](./docs/rules/order-in-components.md) | Keep order of properties in components |
7273
| :white_check_mark: | [require-component-is](./docs/rules/require-component-is.md) | require `v-bind:is` of `<component>` elements. |
7374
| :white_check_mark: | [require-v-for-key](./docs/rules/require-v-for-key.md) | require `v-bind:key` with `v-for` directives. |
7475

Diff for: docs/rules/order-in-components.md

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Keep proper order of properties in your components (order-in-components)
2+
3+
This rule makes sure you keep declared order of properties in components.
4+
5+
## :book: Rule Details
6+
7+
Recommended order of properties is as follows:
8+
9+
1. Options / Misc (`name`, `delimiters`, `functional`, `model`)
10+
2. Options / Assets (`components`, `directives`, `filters`)
11+
3. Options / Composition (`parent`, `mixins`, `extends`, `provide`, `inject`)
12+
4. `el`
13+
5. `template`
14+
6. `props`
15+
7. `propsData`
16+
8. `data`
17+
9. `computed`
18+
10. `watch`
19+
11. `lifecycleHooks`
20+
12. `methods`
21+
13. `render`
22+
14. `renderError`
23+
24+
Note that `lifecycleHooks` is not a regular property - it indicates the group of all lifecycle hooks just to simplify the configuration.
25+
26+
Examples of **incorrect** code for this rule:
27+
28+
```js
29+
30+
export default {
31+
name: 'app',
32+
data () {
33+
return {
34+
msg: 'Welcome to Your Vue.js App'
35+
}
36+
},
37+
props: {
38+
propA: Number,
39+
},
40+
}
41+
42+
```
43+
44+
Examples of **correct** code for this rule:
45+
46+
```js
47+
48+
export default {
49+
name: 'app',
50+
props: {
51+
propA: Number,
52+
},
53+
data () {
54+
return {
55+
msg: 'Welcome to Your Vue.js App'
56+
}
57+
},
58+
}
59+
60+
```
61+
62+
### Options
63+
64+
If you want you can change the order providing the optional configuration in your `.eslintrc` file. Setting responsible for the above order looks like this:
65+
66+
```
67+
vue/order-in-components: [2, {
68+
order: [
69+
['name', 'delimiters', 'functional', 'model'],
70+
['components', 'directives', 'filters'],
71+
['parent', 'mixins', 'extends', 'provide', 'inject'],
72+
'el',
73+
'template',
74+
'props',
75+
'propsData',
76+
'data',
77+
'computed',
78+
'watch',
79+
'lifecycle_hooks',
80+
'methods',
81+
'render',
82+
'renderError'
83+
]
84+
}]
85+
```
86+
87+
If you want some of properties to be treated equally in order you can group them into arrays, like we did with `name`, `delimiters`, `funcitonal` and `model`.

Diff for: lib/recommended-rules.js

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626
"vue/no-invalid-v-text": "error",
2727
"vue/no-parsing-error": "error",
2828
"vue/no-textarea-mustache": "error",
29+
"vue/order-in-components": "off",
2930
"vue/require-component-is": "error",
3031
"vue/require-v-for-key": "error",
3132
"vue/v-bind-style": "off",

Diff for: lib/rules/order-in-components.js

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* @fileoverview Keep order of properties in components
3+
* @author Michał Sajnóg
4+
*/
5+
'use strict'
6+
7+
const defaultOrder = [
8+
['name', 'delimiters', 'functional', 'model'],
9+
['components', 'directives', 'filters'],
10+
['parent', 'mixins', 'extends', 'provide', 'inject'],
11+
'el',
12+
'template',
13+
'props',
14+
'propsData',
15+
'data',
16+
'computed',
17+
'watch',
18+
'LIFECYCLE_HOOKS',
19+
'methods',
20+
'render',
21+
'renderError'
22+
]
23+
24+
const groups = {
25+
LIFECYCLE_HOOKS: [
26+
'beforeCreate',
27+
'created',
28+
'beforeMount',
29+
'mounted',
30+
'beforeUpdate',
31+
'updated',
32+
'activated',
33+
'deactivated',
34+
'beforeDestroy',
35+
'destroyed'
36+
]
37+
}
38+
39+
function isComponentFile (node, path) {
40+
const isVueFile = path.endsWith('.vue') || path.endsWith('.jsx')
41+
return isVueFile && node.declaration.type === 'ObjectExpression'
42+
}
43+
44+
function isVueComponent (node) {
45+
const callee = node.callee
46+
47+
const isFullVueComponent = node.type === 'CallExpression' &&
48+
callee.type === 'MemberExpression' &&
49+
callee.object.type === 'Identifier' &&
50+
callee.object.name === 'Vue' &&
51+
callee.property.type === 'Identifier' &&
52+
callee.property.name === 'component' &&
53+
node.arguments.length &&
54+
node.arguments.slice(-1)[0].type === 'ObjectExpression'
55+
56+
const isDestructedVueComponent = callee.type === 'Identifier' &&
57+
callee.name === 'component'
58+
59+
return isFullVueComponent || isDestructedVueComponent
60+
}
61+
62+
function isVueInstance (node) {
63+
const callee = node.callee
64+
return node.type === 'NewExpression' &&
65+
callee.type === 'Identifier' &&
66+
callee.name === 'Vue' &&
67+
node.arguments.length &&
68+
node.arguments[0].type === 'ObjectExpression'
69+
}
70+
71+
function getOrderMap (order) {
72+
const orderMap = new Map()
73+
74+
order.forEach((property, i) => {
75+
if (Array.isArray(property)) {
76+
property.forEach(p => orderMap.set(p, i))
77+
} else {
78+
orderMap.set(property, i)
79+
}
80+
})
81+
82+
return orderMap
83+
}
84+
85+
function checkOrder (propertiesNodes, orderMap, context) {
86+
const properties = propertiesNodes.map(property => property.key)
87+
88+
properties.forEach((property, i) => {
89+
const propertiesAbove = properties.slice(0, i)
90+
const unorderedProperties = propertiesAbove
91+
.filter(p => orderMap.get(p.name) > orderMap.get(property.name))
92+
.sort((p1, p2) => orderMap.get(p1.name) > orderMap.get(p2.name))
93+
94+
const firstUnorderedProperty = unorderedProperties[0]
95+
96+
if (firstUnorderedProperty) {
97+
const line = firstUnorderedProperty.loc.start.line
98+
context.report({
99+
node: property,
100+
message: `The "${property.name}" property should be above the "${firstUnorderedProperty.name}" property on line ${line}.`
101+
})
102+
}
103+
})
104+
}
105+
106+
function create (context) {
107+
const options = context.options[0] || {}
108+
const order = options.order || defaultOrder
109+
const filePath = context.getFilename()
110+
111+
const extendedOrder = order.map(property => groups[property] || property)
112+
const orderMap = getOrderMap(extendedOrder)
113+
114+
return {
115+
ExportDefaultDeclaration (node) {
116+
// export default {} in .vue || .jsx
117+
if (!isComponentFile(node, filePath)) return
118+
checkOrder(node.declaration.properties, orderMap, context)
119+
},
120+
CallExpression (node) {
121+
// Vue.component('xxx', {}) || component('xxx', {})
122+
if (!isVueComponent(node)) return
123+
checkOrder(node.arguments.slice(-1)[0].properties, orderMap, context)
124+
},
125+
NewExpression (node) {
126+
// new Vue({})
127+
if (!isVueInstance(node)) return
128+
checkOrder(node.arguments[0].properties, orderMap, context)
129+
}
130+
}
131+
}
132+
133+
// ------------------------------------------------------------------------------
134+
// Rule Definition
135+
// ------------------------------------------------------------------------------
136+
137+
module.exports = {
138+
create,
139+
meta: {
140+
docs: {
141+
description: 'Keep order of properties in components',
142+
category: 'Best Practices',
143+
recommended: false
144+
},
145+
fixable: null,
146+
schema: []
147+
}
148+
}

Diff for: package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
"main": "lib/index.js",
66
"scripts": {
77
"start": "npm run test:simple -- --watch --growl",
8-
"test:base": "mocha \"tests/lib/**/*.js\" \"tests/integrations/*.js\" --timeout 60000",
8+
"test:base": "mocha \"tests/lib/**/*.js\"",
99
"test:simple": "npm run test:base -- --reporter nyan",
10-
"test": "nyc npm run test:base",
10+
"test": "nyc npm run test:base -- \"tests/integrations/*.js\" --timeout 60000",
1111
"lint": "eslint .",
1212
"pretest": "npm run lint",
1313
"preversion": "npm run update && npm test",

0 commit comments

Comments
 (0)