Skip to content

Commit

Permalink
Fix #1819: Enforce order between script and script setup (#1825)
Browse files Browse the repository at this point in the history
* 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 <otameshiyo23@gmail.com>

* Update tests/lib/rules/component-tags-order.js

Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>

* Update tests/lib/rules/component-tags-order.js

Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>

* Update tests/lib/rules/component-tags-order.js

Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>

* Update docs/rules/component-tags-order.md

Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>

* don't move the message block

Co-authored-by: Yosuke Ota <otameshiyo23@gmail.com>
  • Loading branch information
doug-wade and ota-meshi committed Apr 11, 2022
1 parent 83290b7 commit 926064c
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 28 deletions.
76 changes: 74 additions & 2 deletions docs/rules/component-tags-order.md
Expand Up @@ -14,7 +14,7 @@ since: v6.1.0

## :book: Rule Details

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

## :wrench: Options

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

- `order` (`(string|string[])[]`) ... The order of top-level element names. default `[ [ "script", "template" ], "style" ]`.
- `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])`.

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

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

</eslint-code-block>

### `{ 'order': ['template', 'script:not([setup])', 'script[setup]'] }`

<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'script:not([setup])', 'script[setup]'] }]}">

```vue
<!-- ✓ GOOD -->
<template>...</template>
<script>/* ... */</script>
<script setup>/* ... */</script>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'script:not([setup])', 'script[setup]'] }]}">

```vue
<!-- ✗ BAD -->
<template>...</template>
<script setup>/* ... */</script>
<script>/* ... */</script>
```

</eslint-code-block>

### `{ 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }`

<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }]}">

```vue
<!-- ✓ GOOD -->
<template>...</template>
<style>/* ... */</style>
<style scoped>/* ... */</style>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }]}">

```vue
<!-- ✗ BAD -->
<template>...</template>
<style scoped>/* ... */</style>
<style>/* ... */</style>
```

</eslint-code-block>

### `{ 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }`

<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }]}">

```vue
<!-- ✓ GOOD -->
<template>...</template>
<i18n lang="ja">/* ... */</i18n>
<i18n lang="en">/* ... */</i18n>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/component-tags-order': ['error', { 'order': ['template', 'i18n:not([lang=en])', 'i18n[lang=en]'] }]}">

```vue
<!-- ✗ BAD -->
<template>...</template>
<i18n lang="en">/* ... */</i18n>
<i18n lang="ja">/* ... */</i18n>
```

</eslint-code-block>

## :books: Further Reading

- [Style guide - Single-file component top-level element order](https://vuejs.org/style-guide/rules-recommended.html#single-file-component-top-level-element-order)
Expand Down
92 changes: 81 additions & 11 deletions lib/rules/component-tags-order.js
Expand Up @@ -9,6 +9,7 @@
// ------------------------------------------------------------------------------

const utils = require('../utils')
const parser = require('postcss-selector-parser')

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

Expand Down Expand Up @@ -46,7 +47,7 @@ module.exports = {
],
messages: {
unexpected:
'The <{{name}}> should be above the <{{firstUnorderedName}}> on line {{line}}.'
'<{{elementName}}{{elementAttributes}}> should be above <{{firstUnorderedName}}{{firstUnorderedAttributes}}> on line {{line}}.'
}
},
/**
Expand All @@ -70,11 +71,73 @@ module.exports = {
})

/**
* @param {string} name
* @param {VElement} element
* @return {String}
*/
function getOrderPosition(name) {
const num = orderMap.get(name)
return num == null ? -1 : num
function getAttributeString(element) {
return element.startTag.attributes
.map((attribute) => {
if (attribute.value && attribute.value.type !== 'VLiteral') {
return ''
}

return `${attribute.key.name}${
attribute.value && attribute.value.value
? '=' + attribute.value.value
: ''
}`
})
.join(' ')
}

/**
* @param {String} ordering
* @param {VElement} element
* @return {Boolean} true if the element matches the selector, false otherwise
*/
function matches(ordering, element) {
let attributeMatches = true
let isNegated = false
let tagMatches = true

parser((selectors) => {
selectors.walk((selector) => {
switch (selector.type) {
case 'tag':
tagMatches = selector.value === element.name
break
case 'pseudo':
isNegated = selector.value === ':not'
break
case 'attribute':
attributeMatches = utils.hasAttribute(
element,
selector.qualifiedAttribute,
selector.value
)
break
}
})
}).processSync(ordering)

if (isNegated) {
return tagMatches && !attributeMatches
} else {
return tagMatches && attributeMatches
}
}

/**
* @param {VElement} element
*/
function getOrderPosition(element) {
for (const [ordering, index] of orderMap.entries()) {
if (matches(ordering, element)) {
return index
}
}

return -1
}
const documentFragment =
context.parserServices.getDocumentFragment &&
Expand All @@ -95,24 +158,31 @@ module.exports = {
const elements = getTopLevelHTMLElements()
const sourceCode = context.getSourceCode()
elements.forEach((element, index) => {
const expectedIndex = getOrderPosition(element.name)
const expectedIndex = getOrderPosition(element)
if (expectedIndex < 0) {
return
}
const firstUnordered = elements
.slice(0, index)
.filter((e) => expectedIndex < getOrderPosition(e.name))
.sort(
(e1, e2) => getOrderPosition(e1.name) - getOrderPosition(e2.name)
)[0]
.filter((e) => expectedIndex < getOrderPosition(e))
.sort((e1, e2) => getOrderPosition(e1) - getOrderPosition(e2))[0]
if (firstUnordered) {
const firstUnorderedttributes = getAttributeString(firstUnordered)
const elementAttributes = getAttributeString(element)

context.report({
node: element,
loc: element.loc,
messageId: 'unexpected',
data: {
name: element.name,
elementName: element.name,
elementAttributes: elementAttributes
? ' ' + elementAttributes
: '',
firstUnorderedName: firstUnordered.name,
firstUnorderedAttributes: firstUnorderedttributes
? ' ' + firstUnorderedttributes
: '',
line: firstUnordered.loc.start.line
},
*fix(fixer) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -56,6 +56,7 @@
"dependencies": {
"eslint-utils": "^3.0.0",
"natural-compare": "^1.4.0",
"postcss-selector-parser": "^6.0.9",
"semver": "^7.3.5",
"vue-eslint-parser": "^8.0.1"
},
Expand Down

0 comments on commit 926064c

Please sign in to comment.