diff --git a/docs/rules/index.md b/docs/rules/index.md
index e718603f8..64bd7b81d 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -267,6 +267,7 @@ For example:
| [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: |
+| [vue/require-default-export](./require-default-export.md) | require components to be the default export | | :warning: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | :hammer: |
| [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: |
| [vue/require-explicit-slots](./require-explicit-slots.md) | require slots to be explicitly defined | | :warning: |
diff --git a/docs/rules/one-component-per-file.md b/docs/rules/one-component-per-file.md
index 6fbede339..5e6b37518 100644
--- a/docs/rules/one-component-per-file.md
+++ b/docs/rules/one-component-per-file.md
@@ -49,6 +49,10 @@ export default {
Nothing.
+## :couple: Related Rules
+
+- [vue/require-default-export](./require-default-export.md)
+
## :books: Further Reading
- [Style guide - Component files](https://vuejs.org/style-guide/rules-strongly-recommended.html#component-files)
diff --git a/docs/rules/require-default-export.md b/docs/rules/require-default-export.md
new file mode 100644
index 000000000..1760c25fa
--- /dev/null
+++ b/docs/rules/require-default-export.md
@@ -0,0 +1,57 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/require-default-export
+description: require components to be the default export
+---
+
+# vue/require-default-export
+
+> require components to be the default export
+
+- :exclamation: _**This rule has not been released yet.**_
+
+## :book: Rule Details
+
+This rule reports when a Vue component does not have a default export, if the component is not defined as `
+```
+
+
+
+
+
+```vue
+
+
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [vue/one-component-per-file](./one-component-per-file.md)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-default-export.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-default-export.js)
diff --git a/lib/index.js b/lib/index.js
index 956eeaa4b..f5b0986a8 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -208,6 +208,7 @@ const plugin = {
'prop-name-casing': require('./rules/prop-name-casing'),
'quote-props': require('./rules/quote-props'),
'require-component-is': require('./rules/require-component-is'),
+ 'require-default-export': require('./rules/require-default-export'),
'require-default-prop': require('./rules/require-default-prop'),
'require-direct-export': require('./rules/require-direct-export'),
'require-emit-validator': require('./rules/require-emit-validator'),
diff --git a/lib/rules/require-default-export.js b/lib/rules/require-default-export.js
new file mode 100644
index 000000000..5c5402008
--- /dev/null
+++ b/lib/rules/require-default-export.js
@@ -0,0 +1,68 @@
+/**
+ * @author ItMaga
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'require components to be the default export',
+ categories: undefined,
+ url: 'https://eslint.vuejs.org/rules/require-default-export.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ missing: 'Missing default export.',
+ mustBeDefaultExport: 'Component must be the default export.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const sourceCode = context.getSourceCode()
+ const documentFragment = sourceCode.parserServices.getDocumentFragment?.()
+
+ const hasScript =
+ documentFragment &&
+ documentFragment.children.some(
+ (e) => utils.isVElement(e) && e.name === 'script'
+ )
+
+ if (utils.isScriptSetup(context) || !hasScript) {
+ return {}
+ }
+
+ let hasDefaultExport = false
+ let hasDefinedComponent = false
+
+ return utils.compositingVisitors(
+ utils.defineVueVisitor(context, {
+ onVueObjectExit() {
+ hasDefinedComponent = true
+ }
+ }),
+
+ {
+ 'Program > ExportDefaultDeclaration'() {
+ hasDefaultExport = true
+ },
+
+ /**
+ * @param {Program} node
+ */
+ 'Program:exit'(node) {
+ if (!hasDefaultExport && node.body.length > 0) {
+ context.report({
+ loc: node.tokens[node.tokens.length - 1].loc,
+ messageId: hasDefinedComponent ? 'mustBeDefaultExport' : 'missing'
+ })
+ }
+ }
+ }
+ )
+ }
+}
diff --git a/tests/lib/rules/require-default-export.js b/tests/lib/rules/require-default-export.js
new file mode 100644
index 000000000..279af4a38
--- /dev/null
+++ b/tests/lib/rules/require-default-export.js
@@ -0,0 +1,190 @@
+/**
+ * @author ItMaga
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/require-default-export')
+
+const tester = new RuleTester({
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+})
+
+tester.run('require-default-export', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+ Without script
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.js',
+ code: `
+ const foo = 'foo';
+ export const bar = 'bar';
+ `
+ },
+ {
+ filename: 'test.js',
+ code: `
+ import {defineComponent} from 'vue';
+ defineComponent({});
+ `
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'missing',
+ line: 4,
+ endLine: 4,
+ column: 7,
+ endColumn: 16
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'missing',
+ line: 4,
+ endLine: 4,
+ column: 7,
+ endColumn: 16
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'missing',
+ line: 6,
+ endLine: 6,
+ column: 7,
+ endColumn: 16
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'missing',
+ line: 5,
+ endLine: 5,
+ column: 7,
+ endColumn: 16
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'mustBeDefaultExport',
+ line: 6,
+ endLine: 6,
+ column: 7,
+ endColumn: 16
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ messageId: 'mustBeDefaultExport',
+ line: 6,
+ endLine: 6,
+ column: 7,
+ endColumn: 16
+ }
+ ]
+ }
+ ]
+})