Skip to content

Commit 926064c

Browse files
doug-wadeota-meshi
andauthored
Fix #1819: Enforce order between script and script setup (#1825)
* Fix #1819: Enforce order between script and script setup * update defaults to match documentation * Use CSS selector syntax * remove incorrect documentation of default behavior for script setup * Fix bug with multiple not pseudo-selectors * revert component tags order docs * update docs * Update error message * update headers * update docs more better * Update tests/lib/rules/component-tags-order.js Co-authored-by: Yosuke Ota <[email protected]> * Update tests/lib/rules/component-tags-order.js Co-authored-by: Yosuke Ota <[email protected]> * Update tests/lib/rules/component-tags-order.js Co-authored-by: Yosuke Ota <[email protected]> * Update tests/lib/rules/component-tags-order.js Co-authored-by: Yosuke Ota <[email protected]> * Update docs/rules/component-tags-order.md Co-authored-by: Yosuke Ota <[email protected]> * don't move the message block Co-authored-by: Yosuke Ota <[email protected]>
1 parent 83290b7 commit 926064c

File tree

4 files changed

+297
-28
lines changed

4 files changed

+297
-28
lines changed

docs/rules/component-tags-order.md

+74-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ since: v6.1.0
1414

1515
## :book: Rule Details
1616

17-
This rule warns about the order of the `<script>`, `<template>` & `<style>` tags.
17+
This rule warns about the order of the top-level tags, such as `<script>`, `<template>` & `<style>`.
1818

1919
## :wrench: Options
2020

@@ -26,7 +26,7 @@ This rule warns about the order of the `<script>`, `<template>` & `<style>` tags
2626
}
2727
```
2828

29-
- `order` (`(string|string[])[]`) ... The order of top-level element names. default `[ [ "script", "template" ], "style" ]`.
29+
- `order` (`(string|string[])[]`) ... The order of top-level element names. default `[ [ "script", "template" ], "style" ]`. May also be CSS selectors, such as `script[setup]` and `i18n:not([lang=en])`.
3030

3131
### `{ "order": [ [ "script", "template" ], "style" ] }` (default)
3232

@@ -113,6 +113,78 @@ This rule warns about the order of the `<script>`, `<template>` & `<style>` tags
113113

114114
</eslint-code-block>
115115

116+
### `{ 'order': ['template', 'script:not([setup])', 'script[setup]'] }`
117+
118+
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'script:not([setup])', 'script[setup]'] }]}">
119+
120+
```vue
121+
<!-- ✓ GOOD -->
122+
<template>...</template>
123+
<script>/* ... */</script>
124+
<script setup>/* ... */</script>
125+
```
126+
127+
</eslint-code-block>
128+
129+
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'script:not([setup])', 'script[setup]'] }]}">
130+
131+
```vue
132+
<!-- ✗ BAD -->
133+
<template>...</template>
134+
<script setup>/* ... */</script>
135+
<script>/* ... */</script>
136+
```
137+
138+
</eslint-code-block>
139+
140+
### `{ 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }`
141+
142+
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }]}">
143+
144+
```vue
145+
<!-- ✓ GOOD -->
146+
<template>...</template>
147+
<style>/* ... */</style>
148+
<style scoped>/* ... */</style>
149+
```
150+
151+
</eslint-code-block>
152+
153+
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }]}">
154+
155+
```vue
156+
<!-- ✗ BAD -->
157+
<template>...</template>
158+
<style scoped>/* ... */</style>
159+
<style>/* ... */</style>
160+
```
161+
162+
</eslint-code-block>
163+
164+
### `{ 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }`
165+
166+
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }]}">
167+
168+
```vue
169+
<!-- ✓ GOOD -->
170+
<template>...</template>
171+
<i18n lang="ja">/* ... */</i18n>
172+
<i18n lang="en">/* ... */</i18n>
173+
```
174+
175+
</eslint-code-block>
176+
177+
<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }]}">
178+
179+
```vue
180+
<!-- ✗ BAD -->
181+
<template>...</template>
182+
<i18n lang="en">/* ... */</i18n>
183+
<i18n lang="ja">/* ... */</i18n>
184+
```
185+
186+
</eslint-code-block>
187+
116188
## :books: Further Reading
117189

118190
- [Style guide - Single-file component top-level element order](https://vuejs.org/style-guide/rules-recommended.html#single-file-component-top-level-element-order)

lib/rules/component-tags-order.js

+81-11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// ------------------------------------------------------------------------------
1010

1111
const utils = require('../utils')
12+
const parser = require('postcss-selector-parser')
1213

1314
const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style'])
1415

@@ -46,7 +47,7 @@ module.exports = {
4647
],
4748
messages: {
4849
unexpected:
49-
'The <{{name}}> should be above the <{{firstUnorderedName}}> on line {{line}}.'
50+
'<{{elementName}}{{elementAttributes}}> should be above <{{firstUnorderedName}}{{firstUnorderedAttributes}}> on line {{line}}.'
5051
}
5152
},
5253
/**
@@ -70,11 +71,73 @@ module.exports = {
7071
})
7172

7273
/**
73-
* @param {string} name
74+
* @param {VElement} element
75+
* @return {String}
7476
*/
75-
function getOrderPosition(name) {
76-
const num = orderMap.get(name)
77-
return num == null ? -1 : num
77+
function getAttributeString(element) {
78+
return element.startTag.attributes
79+
.map((attribute) => {
80+
if (attribute.value && attribute.value.type !== 'VLiteral') {
81+
return ''
82+
}
83+
84+
return `${attribute.key.name}${
85+
attribute.value && attribute.value.value
86+
? '=' + attribute.value.value
87+
: ''
88+
}`
89+
})
90+
.join(' ')
91+
}
92+
93+
/**
94+
* @param {String} ordering
95+
* @param {VElement} element
96+
* @return {Boolean} true if the element matches the selector, false otherwise
97+
*/
98+
function matches(ordering, element) {
99+
let attributeMatches = true
100+
let isNegated = false
101+
let tagMatches = true
102+
103+
parser((selectors) => {
104+
selectors.walk((selector) => {
105+
switch (selector.type) {
106+
case 'tag':
107+
tagMatches = selector.value === element.name
108+
break
109+
case 'pseudo':
110+
isNegated = selector.value === ':not'
111+
break
112+
case 'attribute':
113+
attributeMatches = utils.hasAttribute(
114+
element,
115+
selector.qualifiedAttribute,
116+
selector.value
117+
)
118+
break
119+
}
120+
})
121+
}).processSync(ordering)
122+
123+
if (isNegated) {
124+
return tagMatches && !attributeMatches
125+
} else {
126+
return tagMatches && attributeMatches
127+
}
128+
}
129+
130+
/**
131+
* @param {VElement} element
132+
*/
133+
function getOrderPosition(element) {
134+
for (const [ordering, index] of orderMap.entries()) {
135+
if (matches(ordering, element)) {
136+
return index
137+
}
138+
}
139+
140+
return -1
78141
}
79142
const documentFragment =
80143
context.parserServices.getDocumentFragment &&
@@ -95,24 +158,31 @@ module.exports = {
95158
const elements = getTopLevelHTMLElements()
96159
const sourceCode = context.getSourceCode()
97160
elements.forEach((element, index) => {
98-
const expectedIndex = getOrderPosition(element.name)
161+
const expectedIndex = getOrderPosition(element)
99162
if (expectedIndex < 0) {
100163
return
101164
}
102165
const firstUnordered = elements
103166
.slice(0, index)
104-
.filter((e) => expectedIndex < getOrderPosition(e.name))
105-
.sort(
106-
(e1, e2) => getOrderPosition(e1.name) - getOrderPosition(e2.name)
107-
)[0]
167+
.filter((e) => expectedIndex < getOrderPosition(e))
168+
.sort((e1, e2) => getOrderPosition(e1) - getOrderPosition(e2))[0]
108169
if (firstUnordered) {
170+
const firstUnorderedttributes = getAttributeString(firstUnordered)
171+
const elementAttributes = getAttributeString(element)
172+
109173
context.report({
110174
node: element,
111175
loc: element.loc,
112176
messageId: 'unexpected',
113177
data: {
114-
name: element.name,
178+
elementName: element.name,
179+
elementAttributes: elementAttributes
180+
? ' ' + elementAttributes
181+
: '',
115182
firstUnorderedName: firstUnordered.name,
183+
firstUnorderedAttributes: firstUnorderedttributes
184+
? ' ' + firstUnorderedttributes
185+
: '',
116186
line: firstUnordered.loc.start.line
117187
},
118188
*fix(fixer) {

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"dependencies": {
5757
"eslint-utils": "^3.0.0",
5858
"natural-compare": "^1.4.0",
59+
"postcss-selector-parser": "^6.0.9",
5960
"semver": "^7.3.5",
6061
"vue-eslint-parser": "^8.0.1"
6162
},

0 commit comments

Comments
 (0)