Skip to content

Commit

Permalink
feat(define-macros-order): add defineExposeLast option (#2349)
Browse files Browse the repository at this point in the history
Co-authored-by: Flo Edelmann <git@flo-edelmann.de>
  • Loading branch information
waynzh and FloEdelmann committed Jan 8, 2024
1 parent a89dd10 commit e2f8b70
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 2 deletions.
34 changes: 33 additions & 1 deletion docs/rules/define-macros-order.md
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
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
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
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
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

0 comments on commit e2f8b70

Please sign in to comment.