Skip to content

feat(define-macros-order): add defineExposeLast option #2349

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion docs/rules/define-macros-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -111,6 +113,36 @@ const slots = defineSlots()

</eslint-code-block>

### `{ "defineExposeLast": true }`

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {defineExposeLast: true}]}">

```vue
<!-- ✓ GOOD -->
<script setup>
defineProps(/* ... */)
defineEmits(/* ... */)
const slots = defineSlots()
defineExpose({/* ... */})
</script>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {defineExposeLast: true}]}">

```vue
<!-- ✗ BAD -->
<script setup>
defineProps(/* ... */)
defineEmits(/* ... */)
defineExpose({/* ... */})
const slots = defineSlots()
</script>
```

</eslint-code-block>

## :rocket: Version

This rule was introduced in eslint-plugin-vue v8.7.0
Expand Down
77 changes: 76 additions & 1 deletion lib/rules/define-macros-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ASTNode>} */
const macrosNodes = new Map()
/** @type {ASTNode} */
let defineExposeNode

return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
Expand All @@ -111,6 +115,9 @@ function create(context) {
},
onDefineSlotsExit(node) {
macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node))
},
onDefineExposeExit(node) {
defineExposeNode = getDefineMacrosStatement(node)
}
}),
{
Expand All @@ -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]

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -255,6 +322,7 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
},
fixable: 'code',
hasSuggestions: true,
schema: [
{
type: 'object',
Expand All @@ -266,14 +334,21 @@ module.exports = {
},
uniqueItems: true,
additionalItems: false
},
defineExposeLast: {
type: 'boolean'
}
},
additionalProperties: false
}
],
messages: {
macrosNotOnTop:
'{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).'
'{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).',
defineExposeNotTheLast:
'`defineExpose` should be the last statement in `<script setup>`.',
putExposeAtTheLast:
'Put `defineExpose` as the last statement in `<script setup>`.'
}
},
create
Expand Down
9 changes: 9 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,8 @@ module.exports = {
* - `onDefineOptionsExit` ... Event when defineOptions visit ends.
* - `onDefineSlotsEnter` ... Event when defineSlots is found.
* - `onDefineSlotsExit` ... Event when defineSlots visit ends.
* - `onDefineExposeEnter` ... Event when defineExpose is found.
* - `onDefineExposeExit` ... Event when defineExpose visit ends.
*
* @param {RuleContext} context The ESLint rule context object.
* @param {ScriptSetupVisitor} visitor The visitor to traverse the AST nodes.
Expand Down Expand Up @@ -1401,6 +1403,13 @@ module.exports = {
'onDefineSlotsExit',
(candidateMacro, node) => candidateMacro === node,
() => undefined
),
new MacroListener(
'defineExpose',
'onDefineExposeEnter',
'onDefineExposeExit',
(candidateMacro, node) => candidateMacro === node,
() => undefined
)
].filter((m) => m.hasListener)
if (macroListenerList.length > 0) {
Expand Down
156 changes: 156 additions & 0 deletions tests/lib/rules/define-macros-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,22 @@ const optionsPropsFirst = [
}
]

const optionsExposeLast = [
{
defineExposeLast: true
}
]

function message(macro) {
return `${macro} should be the first statement in \`<script setup>\` (after any potential import statements or type definitions).`
}

const defineExposeNotTheLast =
'`defineExpose` should be the last statement in `<script setup>`.'

const putExposeAtBottom =
'Put `defineExpose` as the last statement in `<script setup>`.'

tester.run('define-macros-order', rule, {
valid: [
{
Expand Down Expand Up @@ -170,6 +182,48 @@ tester.run('define-macros-order', rule, {
order: ['defineOptions', 'defineEmits', 'defineProps', 'defineSlots']
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
import Foo from 'foo'
/** props */
defineProps(['foo'])
/** options */
defineOptions({})
/** expose */
defineExpose({})
</script>
`,
options: optionsExposeLast
},
{
filename: 'test.vue',
code: `
<script setup lang="ts">
import Foo from 'foo'
/** props */
const props = defineProps({
test: Boolean
})
/** emits */
defineEmits(['update:foo'])
/** slots */
const slots = defineSlots()
/** expose */
defineExpose({})
</script>
`,
options: [
{
order: ['defineProps', 'defineEmits'],
defineExposeLast: true
}
],
parserOptions: {
parser: require.resolve('@typescript-eslint/parser')
}
}
],
invalid: [
Expand Down Expand Up @@ -622,6 +676,108 @@ tester.run('define-macros-order', rule, {
line: 6
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
/** emits */
defineEmits(['update:foo'])
/** expose */
defineExpose({})
/** slots */
const slots = defineSlots()
</script>
`,
output: null,
options: optionsExposeLast,
errors: [
{
message: defineExposeNotTheLast,
line: 6,
suggestions: [
{
desc: putExposeAtBottom,
output: `
<script setup>
/** emits */
defineEmits(['update:foo'])
/** slots */
const slots = defineSlots()
/** expose */
defineExpose({})
</script>
`
}
]
}
]
},
{
filename: 'test.vue',
code: `
<script setup>
/** emits */
defineEmits(['update:foo'])
/** expose */
defineExpose({})
/** options */
defineOptions({})
/** props */
const props = defineProps(['foo'])
/** slots */
const slots = defineSlots()
</script>
`,
output: `
<script setup>
/** options */
defineOptions({})
/** emits */
defineEmits(['update:foo'])
/** expose */
defineExpose({})
/** props */
const props = defineProps(['foo'])
/** slots */
const slots = defineSlots()
</script>
`,
options: [
{
order: ['defineOptions', 'defineEmits', 'defineProps'],
defineExposeLast: true
}
],
errors: [
{
message: defineExposeNotTheLast,
line: 6,
suggestions: [
{
desc: putExposeAtBottom,
output: `
<script setup>
/** emits */
defineEmits(['update:foo'])
/** options */
defineOptions({})
/** props */
const props = defineProps(['foo'])
/** slots */
const slots = defineSlots()
/** expose */
defineExpose({})
</script>
`
}
]
},
{
message: message('defineOptions'),
line: 8
}
]
}
]
})
2 changes: 2 additions & 0 deletions typings/eslint-plugin-vue/util-types/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down