Skip to content

Add new vue/prefer-define-component rule #2738

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
[vue/padding-line-between-blocks]: ./padding-line-between-blocks.md
[vue/padding-line-between-tags]: ./padding-line-between-tags.md
[vue/padding-lines-in-component-definition]: ./padding-lines-in-component-definition.md
[vue/prefer-define-component]: ./prefer-define-component.md
[vue/prefer-define-options]: ./prefer-define-options.md
[vue/prefer-import-from-vue]: ./prefer-import-from-vue.md
[vue/prefer-prop-type-boolean-first]: ./prefer-prop-type-boolean-first.md
Expand Down Expand Up @@ -644,4 +645,4 @@ The following rules extend the rules provided by ESLint itself and apply them to
[v9.0.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.0.0
[v9.16.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.16.0
[v9.17.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.17.0
[v9.7.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.7.0
[v9.7.0]: https://github.com/vuejs/eslint-plugin-vue/releases/tag/v9.7.0
96 changes: 96 additions & 0 deletions docs/rules/prefer-define-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/prefer-define-component
description: require components to be defined using `defineComponent`
---

# vue/prefer-define-component

> require components to be defined using `defineComponent`

## :book: Rule Details

This rule enforces the use of `defineComponent` when defining Vue components. Using `defineComponent` provides proper typing in Vue 3 and IDE support for object properties.

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

```vue
<script>
import { defineComponent } from 'vue'

/* ✓ GOOD */
export default defineComponent({
name: 'ComponentA',
props: {
message: String
}
})
</script>
```

</eslint-code-block>

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

```vue
<script>
/* ✗ BAD */
export default {
name: 'ComponentA',
props: {
message: String
}
}
</script>
```

</eslint-code-block>

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

```vue
<script>
/* ✗ BAD */
export default Vue.extend({
name: 'ComponentA',
props: {
message: String
}
})
</script>
```

</eslint-code-block>

This rule doesn't report components using `<script setup>` without a normal `<script>` tag, as those don't require `defineComponent`.

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

```vue
<script setup>
/* ✓ GOOD - script setup doesn't need defineComponent */
import { ref } from 'vue'
const count = ref(0)
</script>
```

</eslint-code-block>

## :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)
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
114 changes: 114 additions & 0 deletions lib/rules/prefer-define-component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @author Kamogelo Moalusi <github.com/thesheppard>
* 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'
})
}
}
})
}
}
Loading
Loading