diff --git a/docs/rules/index.md b/docs/rules/index.md
index abc3419a4..5e9e91dbb 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -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: |
diff --git a/docs/rules/prefer-define-options.md b/docs/rules/prefer-define-options.md
new file mode 100644
index 000000000..5cb75d2a6
--- /dev/null
+++ b/docs/rules/prefer-define-options.md
@@ -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: ***This rule has not been released yet.***
+- :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 `
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+## :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)
diff --git a/lib/index.js b/lib/index.js
index c7b580868..a09e73019 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -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'),
diff --git a/lib/rules/prefer-define-options.js b/lib/rules/prefer-define-options.js
new file mode 100644
index 000000000..cd9240445
--- /dev/null
+++ b/lib/rules/prefer-define-options.js
@@ -0,0 +1,122 @@
+/**
+ * @author Yosuke Ota
+ * 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`
+ )
+ ]
+ }
+ }
+ }
+}
diff --git a/tests/lib/rules/prefer-define-options.js b/tests/lib/rules/prefer-define-options.js
new file mode 100644
index 000000000..2fd9bdfec
--- /dev/null
+++ b/tests/lib/rules/prefer-define-options.js
@@ -0,0 +1,109 @@
+/**
+ * @author Yosuke Ota
+ * 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: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ output: `
+
+ `,
+ errors: [
+ {
+ message: 'Use `defineOptions` instead of default export.',
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ output: null,
+ errors: [
+ {
+ message: 'Use `defineOptions` instead of default export.',
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ output: `
+
+
+ `,
+ errors: [
+ {
+ message: 'Use `defineOptions` instead of default export.',
+ line: 4
+ }
+ ]
+ }
+ ]
+})