diff --git a/docs/rules/define-macros-order.md b/docs/rules/define-macros-order.md
index 37074f05c..24d25c56e 100644
--- a/docs/rules/define-macros-order.md
+++ b/docs/rules/define-macros-order.md
@@ -20,12 +20,14 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they
```json
{
"vue/define-macros-order": ["error", {
- "order": ["defineProps", "defineEmits"]
+ "order": ["defineProps", "defineEmits"],
+ "defineExposeLast": false
}]
}
```
- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"` and `"defineSlots"`.
+- `defineExposeLast` (`boolean`) ... Force `defineExpose` at the end.
### `{ "order": ["defineProps", "defineEmits"] }` (default)
@@ -111,6 +113,36 @@ const slots = defineSlots()
+### `{ "defineExposeLast": true }`
+
+
+
+```vue
+
+
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
## :rocket: Version
This rule was introduced in eslint-plugin-vue v8.7.0
diff --git a/lib/rules/define-macros-order.js b/lib/rules/define-macros-order.js
index 5a678db42..b55c5e8c7 100644
--- a/lib/rules/define-macros-order.js
+++ b/lib/rules/define-macros-order.js
@@ -95,8 +95,12 @@ function create(context) {
const options = context.options
/** @type {[string, string]} */
const order = (options[0] && options[0].order) || DEFAULT_ORDER
+ /** @type {boolean} */
+ const defineExposeLast = (options[0] && options[0].defineExposeLast) || false
/** @type {Map} */
const macrosNodes = new Map()
+ /** @type {ASTNode} */
+ let defineExposeNode
return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
@@ -111,6 +115,9 @@ function create(context) {
},
onDefineSlotsExit(node) {
macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node))
+ },
+ onDefineExposeExit(node) {
+ defineExposeNode = getDefineMacrosStatement(node)
}
}),
{
@@ -131,6 +138,14 @@ function create(context) {
(data) => utils.isDef(data.node)
)
+ // check last node
+ if (defineExposeLast) {
+ const lastNode = program.body[program.body.length - 1]
+ if (defineExposeNode && lastNode !== defineExposeNode) {
+ reportExposeNotOnBottom(defineExposeNode, lastNode)
+ }
+ }
+
for (const [index, should] of orderedList.entries()) {
const targetStatement = program.body[firstStatementIndex + index]
@@ -172,6 +187,58 @@ function create(context) {
})
}
+ /**
+ * @param {ASTNode} node
+ * @param {ASTNode} lastNode
+ */
+ function reportExposeNotOnBottom(node, lastNode) {
+ context.report({
+ node,
+ loc: node.loc,
+ messageId: 'defineExposeNotTheLast',
+ suggest: [
+ {
+ messageId: 'putExposeAtTheLast',
+ fix(fixer) {
+ return moveNodeToLast(fixer, node, lastNode)
+ }
+ }
+ ]
+ })
+ }
+
+ /**
+ * Move all lines of "node" with its comments to after the "target"
+ * @param {RuleFixer} fixer
+ * @param {ASTNode} node
+ * @param {ASTNode} target
+ */
+ function moveNodeToLast(fixer, node, target) {
+ // get comments under tokens(if any)
+ const beforeNodeToken = sourceCode.getTokenBefore(node)
+ const nodeComment = sourceCode.getTokenAfter(beforeNodeToken, {
+ includeComments: true
+ })
+ const nextNodeComment = sourceCode.getTokenAfter(node, {
+ includeComments: true
+ })
+
+ // remove position: node (and comments) to next node (and comments)
+ const cutStart = getLineStartIndex(nodeComment, beforeNodeToken)
+ const cutEnd = getLineStartIndex(nextNodeComment, node)
+
+ // insert text: comment + node
+ const textNode = sourceCode.getText(
+ node,
+ node.range[0] - beforeNodeToken.range[1]
+ )
+
+ return [
+ fixer.insertTextAfter(target, textNode),
+ fixer.removeRange([cutStart, cutEnd])
+ ]
+ }
+
/**
* Move all lines of "node" with its comments to before the "target"
* @param {RuleFixer} fixer
@@ -255,6 +322,7 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
},
fixable: 'code',
+ hasSuggestions: true,
schema: [
{
type: 'object',
@@ -266,6 +334,9 @@ module.exports = {
},
uniqueItems: true,
additionalItems: false
+ },
+ defineExposeLast: {
+ type: 'boolean'
}
},
additionalProperties: false
@@ -273,7 +344,11 @@ module.exports = {
],
messages: {
macrosNotOnTop:
- '{{macro}} should be the first statement in `
+ `,
+ options: optionsExposeLast
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: [
+ {
+ order: ['defineProps', 'defineEmits'],
+ defineExposeLast: true
+ }
+ ],
+ parserOptions: {
+ parser: require.resolve('@typescript-eslint/parser')
+ }
}
],
invalid: [
@@ -622,6 +676,108 @@ tester.run('define-macros-order', rule, {
line: 6
}
]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: null,
+ options: optionsExposeLast,
+ errors: [
+ {
+ message: defineExposeNotTheLast,
+ line: 6,
+ suggestions: [
+ {
+ desc: putExposeAtBottom,
+ output: `
+
+ `
+ }
+ ]
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ output: `
+
+ `,
+ options: [
+ {
+ order: ['defineOptions', 'defineEmits', 'defineProps'],
+ defineExposeLast: true
+ }
+ ],
+ errors: [
+ {
+ message: defineExposeNotTheLast,
+ line: 6,
+ suggestions: [
+ {
+ desc: putExposeAtBottom,
+ output: `
+
+ `
+ }
+ ]
+ },
+ {
+ message: message('defineOptions'),
+ line: 8
+ }
+ ]
}
]
})
diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts
index b2704769f..4d8384d66 100644
--- a/typings/eslint-plugin-vue/util-types/utils.ts
+++ b/typings/eslint-plugin-vue/util-types/utils.ts
@@ -44,6 +44,8 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase {
onDefineOptionsExit?(node: CallExpression): void
onDefineSlotsEnter?(node: CallExpression): void
onDefineSlotsExit?(node: CallExpression): void
+ onDefineExposeEnter?(node: CallExpression): void
+ onDefineExposeExit?(node: CallExpression): void
[query: string]:
| ((node: VAST.ParamNode) => void)
| ((node: CallExpression, props: ComponentProp[]) => void)