Skip to content

Add vue/prefer-define-options rule #2167

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 2 commits into from
May 13, 2023
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
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ For example:
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
| [vue/padding-line-between-tags](./padding-line-between-tags.md) | require or disallow newlines between sibling tags in template | :wrench: | :lipstick: |
| [vue/padding-lines-in-component-definition](./padding-lines-in-component-definition.md) | require or disallow padding lines in component definition | :wrench: | :lipstick: |
| [vue/prefer-define-options](./prefer-define-options.md) | enforce use of `defineOptions` instead of default export. | :wrench: | :warning: |
| [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
| [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: |
| [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
Expand Down
56 changes: 56 additions & 0 deletions docs/rules/prefer-define-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/prefer-define-options
description: enforce use of `defineOptions` instead of default export.
---
# vue/prefer-define-options

> enforce use of `defineOptions` instead of default export.

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :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.

## :book: Rule Details

This rule aims to enforce use of `defineOptions` instead of default export in `<script setup>`.

The [`defineOptions()`](https://vuejs.org/api/sfc-script-setup.html#defineoptions) macro was officially introduced in Vue 3.3.

<eslint-code-block fix :rules="{'vue/prefer-define-options': ['error']}">

```vue
<script setup>
/* ✓ GOOD */
defineOptions({ name: 'Foo' })
</script>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/prefer-define-options': ['error']}">

```vue
<script>
/* ✗ BAD */
export default { name: 'Foo' }
</script>
<script setup>
/* ... */
</script>
```

</eslint-code-block>

## :wrench: Options

Nothing.

## :books: Further Reading

- [API - defineOptions()](https://vuejs.org/api/sfc-script-setup.html#defineoptions)

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-define-options.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-define-options.js)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ module.exports = {
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
'padding-line-between-tags': require('./rules/padding-line-between-tags'),
'padding-lines-in-component-definition': require('./rules/padding-lines-in-component-definition'),
'prefer-define-options': require('./rules/prefer-define-options'),
'prefer-import-from-vue': require('./rules/prefer-import-from-vue'),
'prefer-prop-type-boolean-first': require('./rules/prefer-prop-type-boolean-first'),
'prefer-separate-static-class': require('./rules/prefer-separate-static-class'),
Expand Down
122 changes: 122 additions & 0 deletions lib/rules/prefer-define-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @author Yosuke Ota <https://github.com/ota-meshi>
* See LICENSE file in root directory for full license.
*/
'use strict'

const utils = require('../utils')

module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce use of `defineOptions` instead of default export.',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/prefer-define-options.html'
},
fixable: 'code',
schema: [],
messages: {
preferDefineOptions: 'Use `defineOptions` instead of default export.'
}
},
/**
* @param {RuleContext} context
* @returns {RuleListener}
*/
create(context) {
const scriptSetup = utils.getScriptSetupElement(context)
if (!scriptSetup) {
return {}
}

/** @type {CallExpression | null} */
let defineOptionsNode = null
/** @type {ExportDefaultDeclaration | null} */
let exportDefaultDeclaration = null

return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
onDefineOptionsEnter(node) {
defineOptionsNode = node
}
}),
{
ExportDefaultDeclaration(node) {
exportDefaultDeclaration = node
},
'Program:exit'() {
if (!exportDefaultDeclaration) {
return
}
context.report({
node: exportDefaultDeclaration,
messageId: 'preferDefineOptions',
fix: defineOptionsNode
? null
: buildFix(exportDefaultDeclaration, scriptSetup)
})
}
}
)

/**
* @param {ExportDefaultDeclaration} node
* @param {VElement} scriptSetup
* @returns {(fixer: RuleFixer) => Fix[]}
*/
function buildFix(node, scriptSetup) {
return (fixer) => {
const sourceCode = context.getSourceCode()

// Calc remove range
/** @type {Range} */
let removeRange = [...node.range]

const script = scriptSetup.parent.children
.filter(utils.isVElement)
.find(
(node) =>
node.name === 'script' && !utils.hasAttribute(node, 'setup')
)
if (
script &&
script.endTag &&
sourceCode
.getTokensBetween(script.startTag, script.endTag, {
includeComments: true
})
.every(
(token) =>
removeRange[0] <= token.range[0] &&
token.range[1] <= removeRange[1]
)
) {
removeRange = [...script.range]
}
const removeStartLoc = sourceCode.getLocFromIndex(removeRange[0])
if (
sourceCode.lines[removeStartLoc.line - 1]
.slice(0, removeStartLoc.column)
.trim() === ''
) {
removeRange[0] =
removeStartLoc.line === 1
? 0
: sourceCode.getIndexFromLoc({
line: removeStartLoc.line - 1,
column: sourceCode.lines[removeStartLoc.line - 2].length
})
}

return [
fixer.removeRange(removeRange),
fixer.insertTextAfter(
scriptSetup.startTag,
`\ndefineOptions(${sourceCode.getText(node.declaration)})\n`
)
]
}
}
}
}
109 changes: 109 additions & 0 deletions tests/lib/rules/prefer-define-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* @author Yosuke Ota <https://github.com/ota-meshi>
* See LICENSE file in root directory for full license.
*/
'use strict'

const RuleTester = require('eslint').RuleTester
const rule = require('../../../lib/rules/prefer-define-options')

const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module'
}
})

tester.run('prefer-define-options', rule, {
valid: [
{
filename: 'test.vue',
code: `
<script setup>
defineOptions({ name: 'Foo' })
</script>
`
},
{
filename: 'test.vue',
code: `
<script>
export default { name: 'Foo' }
</script>
`
}
],
invalid: [
{
filename: 'test.vue',
code: `
<script>
export default { name: 'Foo' }
</script>
<script setup>
const props = defineProps(['foo'])
</script>
`,
output: `
<script setup>
defineOptions({ name: 'Foo' })

const props = defineProps(['foo'])
</script>
`,
errors: [
{
message: 'Use `defineOptions` instead of default export.',
line: 3
}
]
},
{
filename: 'test.vue',
code: `
<script>
export default { name: 'Foo' }
</script>
<script setup>
defineOptions({})
</script>
`,
output: null,
errors: [
{
message: 'Use `defineOptions` instead of default export.',
line: 3
}
]
},
{
filename: 'test.vue',
code: `
<script>
export const A = 42
export default { name: 'Foo' }
</script>
<script setup>
const props = defineProps(['foo'])
</script>
`,
output: `
<script>
export const A = 42
</script>
<script setup>
defineOptions({ name: 'Foo' })

const props = defineProps(['foo'])
</script>
`,
errors: [
{
message: 'Use `defineOptions` instead of default export.',
line: 4
}
]
}
]
})