Skip to content

Commit dd96780

Browse files
authored
Add vue/prefer-define-options rule (#2167)
1 parent 6b3736b commit dd96780

File tree

5 files changed

+289
-0
lines changed

5 files changed

+289
-0
lines changed

Diff for: docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ For example:
255255
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
256256
| [vue/padding-line-between-tags](./padding-line-between-tags.md) | require or disallow newlines between sibling tags in template | :wrench: | :lipstick: |
257257
| [vue/padding-lines-in-component-definition](./padding-lines-in-component-definition.md) | require or disallow padding lines in component definition | :wrench: | :lipstick: |
258+
| [vue/prefer-define-options](./prefer-define-options.md) | enforce use of `defineOptions` instead of default export. | :wrench: | :warning: |
258259
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
259260
| [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
260261
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |

Diff for: docs/rules/prefer-define-options.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/prefer-define-options
5+
description: enforce use of `defineOptions` instead of default export.
6+
---
7+
# vue/prefer-define-options
8+
9+
> enforce use of `defineOptions` instead of default export.
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
13+
14+
## :book: Rule Details
15+
16+
This rule aims to enforce use of `defineOptions` instead of default export in `<script setup>`.
17+
18+
The [`defineOptions()`](https://vuejs.org/api/sfc-script-setup.html#defineoptions) macro was officially introduced in Vue 3.3.
19+
20+
<eslint-code-block fix :rules="{'vue/prefer-define-options': ['error']}">
21+
22+
```vue
23+
<script setup>
24+
/* ✓ GOOD */
25+
defineOptions({ name: 'Foo' })
26+
</script>
27+
```
28+
29+
</eslint-code-block>
30+
31+
<eslint-code-block fix :rules="{'vue/prefer-define-options': ['error']}">
32+
33+
```vue
34+
<script>
35+
/* ✗ BAD */
36+
export default { name: 'Foo' }
37+
</script>
38+
<script setup>
39+
/* ... */
40+
</script>
41+
```
42+
43+
</eslint-code-block>
44+
45+
## :wrench: Options
46+
47+
Nothing.
48+
49+
## :books: Further Reading
50+
51+
- [API - defineOptions()](https://vuejs.org/api/sfc-script-setup.html#defineoptions)
52+
53+
## :mag: Implementation
54+
55+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-define-options.js)
56+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-define-options.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ module.exports = {
166166
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
167167
'padding-line-between-tags': require('./rules/padding-line-between-tags'),
168168
'padding-lines-in-component-definition': require('./rules/padding-lines-in-component-definition'),
169+
'prefer-define-options': require('./rules/prefer-define-options'),
169170
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
170171
'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'),
171172
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),

Diff for: lib/rules/prefer-define-options.js

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @author Yosuke Ota <https://github.com/ota-meshi>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
9+
module.exports = {
10+
meta: {
11+
type: 'suggestion',
12+
docs: {
13+
description: 'enforce use of `defineOptions` instead of default export.',
14+
categories: undefined,
15+
url: 'https://eslint.vuejs.org/rules/prefer-define-options.html'
16+
},
17+
fixable: 'code',
18+
schema: [],
19+
messages: {
20+
preferDefineOptions: 'Use `defineOptions` instead of default export.'
21+
}
22+
},
23+
/**
24+
* @param {RuleContext} context
25+
* @returns {RuleListener}
26+
*/
27+
create(context) {
28+
const scriptSetup = utils.getScriptSetupElement(context)
29+
if (!scriptSetup) {
30+
return {}
31+
}
32+
33+
/** @type {CallExpression | null} */
34+
let defineOptionsNode = null
35+
/** @type {ExportDefaultDeclaration | null} */
36+
let exportDefaultDeclaration = null
37+
38+
return utils.compositingVisitors(
39+
utils.defineScriptSetupVisitor(context, {
40+
onDefineOptionsEnter(node) {
41+
defineOptionsNode = node
42+
}
43+
}),
44+
{
45+
ExportDefaultDeclaration(node) {
46+
exportDefaultDeclaration = node
47+
},
48+
'Program:exit'() {
49+
if (!exportDefaultDeclaration) {
50+
return
51+
}
52+
context.report({
53+
node: exportDefaultDeclaration,
54+
messageId: 'preferDefineOptions',
55+
fix: defineOptionsNode
56+
? null
57+
: buildFix(exportDefaultDeclaration, scriptSetup)
58+
})
59+
}
60+
}
61+
)
62+
63+
/**
64+
* @param {ExportDefaultDeclaration} node
65+
* @param {VElement} scriptSetup
66+
* @returns {(fixer: RuleFixer) => Fix[]}
67+
*/
68+
function buildFix(node, scriptSetup) {
69+
return (fixer) => {
70+
const sourceCode = context.getSourceCode()
71+
72+
// Calc remove range
73+
/** @type {Range} */
74+
let removeRange = [...node.range]
75+
76+
const script = scriptSetup.parent.children
77+
.filter(utils.isVElement)
78+
.find(
79+
(node) =>
80+
node.name === 'script' && !utils.hasAttribute(node, 'setup')
81+
)
82+
if (
83+
script &&
84+
script.endTag &&
85+
sourceCode
86+
.getTokensBetween(script.startTag, script.endTag, {
87+
includeComments: true
88+
})
89+
.every(
90+
(token) =>
91+
removeRange[0] <= token.range[0] &&
92+
token.range[1] <= removeRange[1]
93+
)
94+
) {
95+
removeRange = [...script.range]
96+
}
97+
const removeStartLoc = sourceCode.getLocFromIndex(removeRange[0])
98+
if (
99+
sourceCode.lines[removeStartLoc.line - 1]
100+
.slice(0, removeStartLoc.column)
101+
.trim() === ''
102+
) {
103+
removeRange[0] =
104+
removeStartLoc.line === 1
105+
? 0
106+
: sourceCode.getIndexFromLoc({
107+
line: removeStartLoc.line - 1,
108+
column: sourceCode.lines[removeStartLoc.line - 2].length
109+
})
110+
}
111+
112+
return [
113+
fixer.removeRange(removeRange),
114+
fixer.insertTextAfter(
115+
scriptSetup.startTag,
116+
`\ndefineOptions(${sourceCode.getText(node.declaration)})\n`
117+
)
118+
]
119+
}
120+
}
121+
}
122+
}

Diff for: tests/lib/rules/prefer-define-options.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* @author Yosuke Ota <https://github.com/ota-meshi>
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('eslint').RuleTester
8+
const rule = require('../../../lib/rules/prefer-define-options')
9+
10+
const tester = new RuleTester({
11+
parser: require.resolve('vue-eslint-parser'),
12+
parserOptions: {
13+
ecmaVersion: 2020,
14+
sourceType: 'module'
15+
}
16+
})
17+
18+
tester.run('prefer-define-options', rule, {
19+
valid: [
20+
{
21+
filename: 'test.vue',
22+
code: `
23+
<script setup>
24+
defineOptions({ name: 'Foo' })
25+
</script>
26+
`
27+
},
28+
{
29+
filename: 'test.vue',
30+
code: `
31+
<script>
32+
export default { name: 'Foo' }
33+
</script>
34+
`
35+
}
36+
],
37+
invalid: [
38+
{
39+
filename: 'test.vue',
40+
code: `
41+
<script>
42+
export default { name: 'Foo' }
43+
</script>
44+
<script setup>
45+
const props = defineProps(['foo'])
46+
</script>
47+
`,
48+
output: `
49+
<script setup>
50+
defineOptions({ name: 'Foo' })
51+
52+
const props = defineProps(['foo'])
53+
</script>
54+
`,
55+
errors: [
56+
{
57+
message: 'Use `defineOptions` instead of default export.',
58+
line: 3
59+
}
60+
]
61+
},
62+
{
63+
filename: 'test.vue',
64+
code: `
65+
<script>
66+
export default { name: 'Foo' }
67+
</script>
68+
<script setup>
69+
defineOptions({})
70+
</script>
71+
`,
72+
output: null,
73+
errors: [
74+
{
75+
message: 'Use `defineOptions` instead of default export.',
76+
line: 3
77+
}
78+
]
79+
},
80+
{
81+
filename: 'test.vue',
82+
code: `
83+
<script>
84+
export const A = 42
85+
export default { name: 'Foo' }
86+
</script>
87+
<script setup>
88+
const props = defineProps(['foo'])
89+
</script>
90+
`,
91+
output: `
92+
<script>
93+
export const A = 42
94+
</script>
95+
<script setup>
96+
defineOptions({ name: 'Foo' })
97+
98+
const props = defineProps(['foo'])
99+
</script>
100+
`,
101+
errors: [
102+
{
103+
message: 'Use `defineOptions` instead of default export.',
104+
line: 4
105+
}
106+
]
107+
}
108+
]
109+
})

0 commit comments

Comments
 (0)