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: ` + + ` + }, + { + 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 + } + ] + } + ] +})