Skip to content

Commit bc20534

Browse files
committed
feat(define-macros-order): add defineExposeLast option
1 parent 77af4b4 commit bc20534

File tree

4 files changed

+150
-1
lines changed

4 files changed

+150
-1
lines changed

lib/rules/define-macros-order.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,12 @@ function create(context) {
9595
const options = context.options
9696
/** @type {[string, string]} */
9797
const order = (options[0] && options[0].order) || DEFAULT_ORDER
98+
/** @type {boolean} */
99+
const defineExposeLast = (options[0] && options[0].defineExposeLast) || false
98100
/** @type {Map<string, ASTNode>} */
99101
const macrosNodes = new Map()
102+
/** @type {ASTNode} */
103+
let defineExposeNode
100104

101105
return utils.compositingVisitors(
102106
utils.defineScriptSetupVisitor(context, {
@@ -111,6 +115,9 @@ function create(context) {
111115
},
112116
onDefineSlotsExit(node) {
113117
macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node))
118+
},
119+
onDefineExposeExit(node) {
120+
defineExposeNode = getDefineMacrosStatement(node)
114121
}
115122
}),
116123
{
@@ -131,6 +138,14 @@ function create(context) {
131138
(data) => utils.isDef(data.node)
132139
)
133140

141+
// check last node
142+
if (defineExposeLast) {
143+
const lastNode = program.body[program.body.length - 1]
144+
if (defineExposeNode && lastNode !== defineExposeNode) {
145+
reportExposeNotOnBottom(defineExposeNode, lastNode)
146+
}
147+
}
148+
134149
for (const [index, should] of orderedList.entries()) {
135150
const targetStatement = program.body[firstStatementIndex + index]
136151

@@ -172,6 +187,58 @@ function create(context) {
172187
})
173188
}
174189

190+
/**
191+
* @param {ASTNode} node
192+
* @param {ASTNode} lastNode
193+
*/
194+
function reportExposeNotOnBottom(node, lastNode) {
195+
context.report({
196+
node,
197+
loc: node.loc,
198+
messageId: 'defineExposeNotTheLast',
199+
fix(fixer) {
200+
return moveNodeToLast(fixer, node, lastNode)
201+
}
202+
})
203+
}
204+
205+
/**
206+
* Move all lines of "node" with its comments to after the "target"
207+
* @param {RuleFixer} fixer
208+
* @param {ASTNode} node
209+
* @param {ASTNode} target
210+
*/
211+
function moveNodeToLast(fixer, node, target) {
212+
// get comments under tokens(if any)
213+
const beforeNodeToken = sourceCode.getTokenBefore(node)
214+
const nodeComment = sourceCode.getTokenAfter(beforeNodeToken, {
215+
includeComments: true
216+
})
217+
const nextNodeComment = sourceCode.getTokenAfter(node, {
218+
includeComments: true
219+
})
220+
221+
// remove position: node (and comments) to next node (and comments)
222+
const cutStart = getLineStartIndex(nodeComment, beforeNodeToken)
223+
const cutEnd = getLineStartIndex(nextNodeComment, node)
224+
225+
// insert text: comment + node
226+
const textNode = sourceCode.getText(
227+
node,
228+
node.range[0] - beforeNodeToken.range[1]
229+
)
230+
231+
// insert position: after target and comments (if any)
232+
const afterTargetComment = sourceCode.getTokenAfter(target, {
233+
includeComments: true
234+
})
235+
236+
return [
237+
fixer.insertTextAfter(afterTargetComment, textNode),
238+
fixer.removeRange([cutStart, cutEnd])
239+
]
240+
}
241+
175242
/**
176243
* Move all lines of "node" with its comments to before the "target"
177244
* @param {RuleFixer} fixer
@@ -266,14 +333,19 @@ module.exports = {
266333
},
267334
uniqueItems: true,
268335
additionalItems: false
336+
},
337+
defineExposeLast: {
338+
type: 'boolean'
269339
}
270340
},
271341
additionalProperties: false
272342
}
273343
],
274344
messages: {
275345
macrosNotOnTop:
276-
'{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).'
346+
'{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).',
347+
defineExposeNotTheLast:
348+
'`defineExpose` should be the last statement in `<script setup>`.'
277349
}
278350
},
279351
create

lib/utils/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,8 @@ module.exports = {
13141314
* - `onDefineOptionsExit` ... Event when defineOptions visit ends.
13151315
* - `onDefineSlotsEnter` ... Event when defineSlots is found.
13161316
* - `onDefineSlotsExit` ... Event when defineSlots visit ends.
1317+
* - `onDefineExposeEnter` ... Event when defineExpose is found.
1318+
* - `onDefineExposeExit` ... Event when defineExpose visit ends.
13171319
*
13181320
* @param {RuleContext} context The ESLint rule context object.
13191321
* @param {ScriptSetupVisitor} visitor The visitor to traverse the AST nodes.
@@ -1401,6 +1403,13 @@ module.exports = {
14011403
'onDefineSlotsExit',
14021404
(candidateMacro, node) => candidateMacro === node,
14031405
() => undefined
1406+
),
1407+
new MacroListener(
1408+
'defineExpose',
1409+
'onDefineExposeEnter',
1410+
'onDefineExposeExit',
1411+
(candidateMacro, node) => candidateMacro === node,
1412+
() => undefined
14041413
)
14051414
].filter((m) => m.hasListener)
14061415
if (macroListenerList.length > 0) {

tests/lib/rules/define-macros-order.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,19 @@ const optionsPropsFirst = [
2727
}
2828
]
2929

30+
const optionsExposeLast = [
31+
{
32+
defineExposeLast: true
33+
}
34+
]
35+
3036
function message(macro) {
3137
return `${macro} should be the first statement in \`<script setup>\` (after any potential import statements or type definitions).`
3238
}
3339

40+
const defineExposeNotTheLast =
41+
'`defineExpose` should be the last statement in `<script setup>`.'
42+
3443
tester.run('define-macros-order', rule, {
3544
valid: [
3645
{
@@ -170,6 +179,21 @@ tester.run('define-macros-order', rule, {
170179
order: ['defineOptions', 'defineEmits', 'defineProps', 'defineSlots']
171180
}
172181
]
182+
},
183+
{
184+
filename: 'test.vue',
185+
code: `
186+
<script setup>
187+
defineProps({
188+
test: Boolean
189+
})
190+
console.log('test')
191+
defineExpose({
192+
a: 1
193+
})
194+
</script>
195+
`,
196+
options: optionsExposeLast
173197
}
174198
],
175199
invalid: [
@@ -622,6 +646,48 @@ tester.run('define-macros-order', rule, {
622646
line: 6
623647
}
624648
]
649+
},
650+
{
651+
filename: 'test.vue',
652+
code: `
653+
<script setup>
654+
defineProps({
655+
test: Boolean
656+
})
657+
658+
// expose
659+
defineExpose({
660+
a: 1
661+
})
662+
663+
// console start
664+
console.log('test')
665+
// console end
666+
</script>
667+
`,
668+
output: `
669+
<script setup>
670+
defineProps({
671+
test: Boolean
672+
})
673+
674+
// console start
675+
console.log('test')
676+
// console end
677+
678+
// expose
679+
defineExpose({
680+
a: 1
681+
})
682+
</script>
683+
`,
684+
options: optionsExposeLast,
685+
errors: [
686+
{
687+
message: defineExposeNotTheLast,
688+
line: 8
689+
}
690+
]
625691
}
626692
]
627693
})

typings/eslint-plugin-vue/util-types/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase {
4444
onDefineOptionsExit?(node: CallExpression): void
4545
onDefineSlotsEnter?(node: CallExpression): void
4646
onDefineSlotsExit?(node: CallExpression): void
47+
onDefineExposeEnter?(node: CallExpression): void
48+
onDefineExposeExit?(node: CallExpression): void
4749
[query: string]:
4850
| ((node: VAST.ParamNode) => void)
4951
| ((node: CallExpression, props: ComponentProp[]) => void)

0 commit comments

Comments
 (0)