diff --git a/docs/rules/index.md b/docs/rules/index.md
index 7828b58bb..cd9532af3 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -8,9 +8,9 @@ pageClass: rule-list
::: tip Legend
- :wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fix-problems) can automatically fix some of the reported problems.
+:wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fix-problems) can automatically fix some of the reported problems.
- :bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+:bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
:::
Mark indicating rule type:
@@ -25,10 +25,10 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
-| Rule ID | Description | | |
-|:--------|:------------|:--:|:--:|
-| [vue/comment-directive] | support comment-directives in `` | | :warning: |
-| [vue/jsx-uses-vars] | prevent variables used in JSX to be marked as unused | | :warning: |
+| Rule ID | Description | | |
+| :---------------------- | :--------------------------------------------------- | :-: | :-------: |
+| [vue/comment-directive] | support comment-directives in `` | | :warning: |
+| [vue/jsx-uses-vars] | prevent variables used in JSX to be marked as unused | | :warning: |
@@ -39,97 +39,97 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
-| Rule ID | Description | | |
-|:--------|:------------|:--:|:--:|
-| [vue/multi-word-component-names] | require component names to be always multi-word | | :three::two::hammer: |
-| [vue/no-arrow-functions-in-watch] | disallow using arrow functions to define watcher | | :three::two::warning: |
-| [vue/no-async-in-computed-properties] | disallow asynchronous actions in computed properties | | :three::two::warning: |
-| [vue/no-child-content] | disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` | :bulb: | :three::two::warning: |
-| [vue/no-computed-properties-in-data] | disallow accessing computed properties in `data` | | :three::two::warning: |
-| [vue/no-custom-modifiers-on-v-model] | disallow custom modifiers on v-model used on the component | | :two::warning: |
-| [vue/no-deprecated-data-object-declaration] | disallow using deprecated object declaration on data (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
-| [vue/no-deprecated-delete-set] | disallow using deprecated `$delete` and `$set` (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-destroyed-lifecycle] | disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
-| [vue/no-deprecated-dollar-listeners-api] | disallow using deprecated `$listeners` (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-dollar-scopedslots-api] | disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
-| [vue/no-deprecated-events-api] | disallow using deprecated events api (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-filter] | disallow using deprecated filters syntax (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-functional-template] | disallow using deprecated the `functional` template (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-html-element-is] | disallow using deprecated the `is` attribute on HTML elements (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-inline-template] | disallow using deprecated `inline-template` attribute (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-model-definition] | disallow deprecated `model` definition (in Vue.js 3.0.0+) | :bulb: | :three::warning: |
-| [vue/no-deprecated-props-default-this] | disallow deprecated `this` access in props default function (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-router-link-tag-prop] | disallow using deprecated `tag` property on `RouterLink` (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-scope-attribute] | disallow deprecated `scope` attribute (in Vue.js 2.5.0+) | :wrench: | :three::hammer: |
-| [vue/no-deprecated-slot-attribute] | disallow deprecated `slot` attribute (in Vue.js 2.6.0+) | :wrench: | :three::hammer: |
-| [vue/no-deprecated-slot-scope-attribute] | disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+) | :wrench: | :three::hammer: |
-| [vue/no-deprecated-v-bind-sync] | disallow use of deprecated `.sync` modifier on `v-bind` directive (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
-| [vue/no-deprecated-v-is] | disallow deprecated `v-is` directive (in Vue.js 3.1.0+) | | :three::hammer: |
-| [vue/no-deprecated-v-on-native-modifier] | disallow using deprecated `.native` modifiers (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-deprecated-v-on-number-modifiers] | disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
-| [vue/no-deprecated-vue-config-keycodes] | disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+) | | :three::warning: |
-| [vue/no-dupe-keys] | disallow duplication of field names | | :three::two::warning: |
-| [vue/no-dupe-v-else-if] | disallow duplicate conditions in `v-if` / `v-else-if` chains | | :three::two::warning: |
-| [vue/no-duplicate-attributes] | disallow duplication of attributes | | :three::two::warning: |
-| [vue/no-export-in-script-setup] | disallow `export` in `
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+
+
+```vue
+
+```
+
+
+
+This rule doesn't report components using `
+```
+
+
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [vue/require-default-export](./require-default-export.md)
+- [vue/require-direct-export](./require-direct-export.md)
+
+## :books: Further Reading
+
+- [Vue.js Guide - TypeScript with Composition API](https://vuejs.org/guide/typescript/composition-api.html#typing-component-props)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/prefer-define-component.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/prefer-define-component.js)
diff --git a/lib/index.js b/lib/index.js
index e511536fa..0ce94365c 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -205,6 +205,7 @@ const plugin = {
'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-component': require('./rules/prefer-define-component'),
'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'),
diff --git a/lib/rules/prefer-define-component.js b/lib/rules/prefer-define-component.js
new file mode 100644
index 000000000..2ef6246db
--- /dev/null
+++ b/lib/rules/prefer-define-component.js
@@ -0,0 +1,114 @@
+/**
+ * @author Kamogelo Moalusi
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// @ts-nocheck
+const utils = require('../utils')
+
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'enforce components to be defined using `defineComponent`',
+ categories: ['vue3-recommended', 'vue2-recommended'],
+ url: 'https://eslint.vuejs.org/rules/prefer-define-component.html'
+ },
+ fixable: null,
+ schema: [],
+ messages: {
+ 'prefer-define-component': 'Use `defineComponent` to define a component.'
+ }
+ },
+ /** @param {RuleContext} context */
+ create(context) {
+ const filePath = context.getFilename()
+ if (!utils.isVueFile(filePath)) return {}
+
+ const sourceCode = context.getSourceCode()
+ const documentFragment = sourceCode.parserServices.getDocumentFragment?.()
+
+ // Check if there's a non-setup script tag
+ const hasNormalScript =
+ documentFragment &&
+ documentFragment.children.some(
+ (e) =>
+ utils.isVElement(e) &&
+ e.name === 'script' &&
+ (!e.startTag.attributes ||
+ !e.startTag.attributes.some((attr) => attr.key.name === 'setup'))
+ )
+
+ // If no regular script tag, we don't need to check
+ if (!hasNormalScript) return {}
+
+ // Skip checking if there's only a setup script (no normal script)
+ if (utils.isScriptSetup(context) && !hasNormalScript) return {}
+
+ let hasDefineComponent = false
+ /** @type {ExportDefaultDeclaration | null} */
+ let exportDefaultNode = null
+ let hasVueExtend = false
+
+ return utils.compositingVisitors(utils.defineVueVisitor(context, {}), {
+ /** @param {ExportDefaultDeclaration} node */
+ 'Program > ExportDefaultDeclaration'(node) {
+ exportDefaultNode = node
+ },
+
+ /** @param {CallExpression} node */
+ 'Program > ExportDefaultDeclaration > CallExpression'(node) {
+ if (
+ node.callee.type === 'Identifier' &&
+ node.callee.name === 'defineComponent'
+ ) {
+ hasDefineComponent = true
+ return
+ }
+
+ // Support aliased imports
+ if (node.callee.type === 'Identifier') {
+ const variable = utils.findVariableByIdentifier(context, node.callee)
+ if (
+ variable &&
+ variable.defs &&
+ variable.defs.length > 0 &&
+ variable.defs[0].node.type === 'ImportSpecifier' &&
+ variable.defs[0].node.imported &&
+ variable.defs[0].node.imported.name === 'defineComponent'
+ ) {
+ hasDefineComponent = true
+ return
+ }
+ }
+
+ // Check for Vue.extend case
+ if (
+ node.callee.type === 'MemberExpression' &&
+ node.callee.object &&
+ node.callee.object.type === 'Identifier' &&
+ node.callee.object.name === 'Vue' &&
+ node.callee.property &&
+ node.callee.property.type === 'Identifier' &&
+ node.callee.property.name === 'extend'
+ ) {
+ hasVueExtend = true
+ }
+ },
+
+ 'Program > ExportDefaultDeclaration > ObjectExpression'() {
+ hasDefineComponent = false
+ },
+
+ 'Program:exit'() {
+ if (exportDefaultNode && (hasVueExtend || !hasDefineComponent)) {
+ context.report({
+ node: exportDefaultNode,
+ messageId: 'prefer-define-component'
+ })
+ }
+ }
+ })
+ }
+}
diff --git a/tests/lib/rules/prefer-define-component.js b/tests/lib/rules/prefer-define-component.js
new file mode 100644
index 000000000..800c561c8
--- /dev/null
+++ b/tests/lib/rules/prefer-define-component.js
@@ -0,0 +1,229 @@
+/**
+ * @author Kamogelo Moalusi
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/prefer-define-component')
+
+const tester = new RuleTester({
+ languageOptions: {
+ parser: require('vue-eslint-parser'),
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+})
+
+tester.run('prefer-define-component', rule, {
+ valid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `
+ }
+ ],
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: 'Use `defineComponent` to define a component.',
+ line: 3,
+ column: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: 'Use `defineComponent` to define a component.',
+ line: 3,
+ column: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: 'Use `defineComponent` to define a component.',
+ line: 3,
+ column: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ errors: [
+ {
+ message: 'Use `defineComponent` to define a component.',
+ line: 3,
+ column: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ errors: [
+ {
+ message: 'Use `defineComponent` to define a component.',
+ line: 7,
+ column: 7
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+ `,
+ errors: [
+ {
+ message: 'Use `defineComponent` to define a component.',
+ line: 3,
+ column: 7
+ }
+ ]
+ }
+ ]
+})