diff --git a/docs/rules/require-direct-export.md b/docs/rules/require-direct-export.md
index 63d9fb19e..e51751df0 100644
--- a/docs/rules/require-direct-export.md
+++ b/docs/rules/require-direct-export.md
@@ -51,7 +51,41 @@ export default ComponentA
## :wrench: Options
-Nothing.
+```json
+{
+ "vue/require-direct-export": ["error", {
+ "disallowFunctionalComponentFunction": false
+ }]
+}
+```
+
+- `"disallowFunctionalComponentFunction"` ... If `true`, disallow functional component functions, available in Vue 3.x. default `false`
+
+### `"disallowFunctionalComponentFunction": false`
+
+
+
+```vue
+
+```
+
+
+
+### `"disallowFunctionalComponentFunction": true`
+
+
+
+```vue
+
+```
+
+
## :mag: Implementation
diff --git a/lib/rules/require-direct-export.js b/lib/rules/require-direct-export.js
index 41e1a9824..2bc0abd12 100644
--- a/lib/rules/require-direct-export.js
+++ b/lib/rules/require-direct-export.js
@@ -6,6 +6,13 @@
const utils = require('../utils')
+/**
+ * @typedef {import('vue-eslint-parser').AST.ESLintExportDefaultDeclaration} ExportDefaultDeclaration
+ * @typedef {import('vue-eslint-parser').AST.ESLintDeclaration} Declaration
+ * @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
+ * @typedef {import('vue-eslint-parser').AST.ESLintReturnStatement} ReturnStatement
+ *
+ */
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
@@ -19,28 +26,84 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/require-direct-export.html'
},
fixable: null, // or "code" or "whitespace"
- schema: []
+ schema: [{
+ type: 'object',
+ properties: {
+ disallowFunctionalComponentFunction: { type: 'boolean' }
+ },
+ additionalProperties: false
+ }]
},
create (context) {
const filePath = context.getFilename()
+ if (!utils.isVueFile(filePath)) return {}
+
+ const disallowFunctional = (context.options[0] || {}).disallowFunctionalComponentFunction
+
+ let maybeVue3Functional
+ let scopeStack = null
return {
- 'ExportDefaultDeclaration:exit' (node) {
- if (!utils.isVueFile(filePath)) return
-
- const isObjectExpression = (
- node.type === 'ExportDefaultDeclaration' &&
- node.declaration.type === 'ObjectExpression'
- )
-
- if (!isObjectExpression) {
- context.report({
- node,
- message: `Expected the component literal to be directly exported.`
- })
+ /** @param {Declaration | Expression} node */
+ 'ExportDefaultDeclaration > *' (node) {
+ if (node.type === 'ObjectExpression') {
+ // OK
+ return
+ }
+ if (!disallowFunctional) {
+ if (node.type === 'ArrowFunctionExpression') {
+ if (node.body.type !== 'BlockStatement') {
+ // OK
+ return
+ }
+ maybeVue3Functional = {
+ body: node.body
+ }
+ return
+ }
+ if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
+ maybeVue3Functional = {
+ body: node.body
+ }
+ return
+ }
+ }
+
+ context.report({
+ node: node.parent,
+ message: `Expected the component literal to be directly exported.`
+ })
+ },
+ ...(disallowFunctional ? {} : {
+ ':function > BlockStatement' (node) {
+ if (!maybeVue3Functional) {
+ return
+ }
+ scopeStack = { upper: scopeStack, withinVue3FunctionalBody: maybeVue3Functional.body === node }
+ },
+ /** @param {ReturnStatement} node */
+ ReturnStatement (node) {
+ if (scopeStack && scopeStack.withinVue3FunctionalBody && node.argument) {
+ maybeVue3Functional.hasReturnArgument = true
+ }
+ },
+ ':function > BlockStatement:exit' (node) {
+ scopeStack = scopeStack && scopeStack.upper
+ },
+ /** @param {ExportDefaultDeclaration} node */
+ 'ExportDefaultDeclaration:exit' (node) {
+ if (!maybeVue3Functional) {
+ return
+ }
+ if (!maybeVue3Functional.hasReturnArgument) {
+ context.report({
+ node,
+ message: `Expected the component literal to be directly exported.`
+ })
+ }
}
- }
+ })
}
}
}
diff --git a/tests/lib/rules/require-direct-export.js b/tests/lib/rules/require-direct-export.js
index 72f71bf93..16b6aa96a 100644
--- a/tests/lib/rules/require-direct-export.js
+++ b/tests/lib/rules/require-direct-export.js
@@ -11,17 +11,17 @@
const rule = require('../../../lib/rules/require-direct-export')
const RuleTester = require('eslint').RuleTester
-const parserOptions = {
- ecmaVersion: 2018,
- sourceType: 'module',
- ecmaFeatures: { jsx: true }
-}
-
// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------
-const ruleTester = new RuleTester()
+const ruleTester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: { jsx: true }
+ }
+})
ruleTester.run('require-direct-export', rule, {
valid: [
@@ -29,28 +29,186 @@ ruleTester.run('require-direct-export', rule, {
filename: 'test.vue',
code: ''
},
+ {
+ filename: 'test.vue',
+ code: `export default {}`
+ },
+ {
+ filename: 'test.vue',
+ code: `export default {}`,
+ options: [{ disallowFunctionalComponentFunction: true }]
+ },
+ {
+ filename: 'test.js',
+ code: `export default Foo`
+ },
{
filename: 'test.vue',
code: `
- export default {}
- `,
- parserOptions
+ import { h } from 'vue'
+ export default function (props) {
+ return h('div', \`Hello! \${props.name}\`)
+ }
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ import { h } from 'vue'
+ export default function Component () {
+ return h('div')
+ }
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ import { h } from 'vue'
+ export default (props) => {
+ return h('div', \`Hello! \${props.name}\`)
+ }
+ `
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ import { h } from 'vue'
+ export default props => h('div', props.msg)
+ `
}
],
invalid: [
-
{
filename: 'test.vue',
code: `
- const A = {};
- export default A`,
- parserOptions,
+ const A = {};
+ export default A`,
errors: [{
message: 'Expected the component literal to be directly exported.',
type: 'ExportDefaultDeclaration',
line: 3
}]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ function A(props) {
+ return h('div', props.msg)
+ };
+ export default A`,
+ errors: [{
+ message: 'Expected the component literal to be directly exported.',
+ type: 'ExportDefaultDeclaration',
+ line: 5
+ }]
+ },
+ {
+ filename: 'test.vue',
+ code: `export default function NoReturn() {}`,
+ errors: [{
+ message: 'Expected the component literal to be directly exported.',
+ type: 'ExportDefaultDeclaration',
+ line: 1
+ }]
+ },
+ {
+ filename: 'test.vue',
+ code: `export default function () {}`,
+ errors: [{
+ message: 'Expected the component literal to be directly exported.',
+ type: 'ExportDefaultDeclaration',
+ line: 1
+ }]
+ },
+ {
+ filename: 'test.vue',
+ code: `export default () => {}`,
+ errors: [{
+ message: 'Expected the component literal to be directly exported.',
+ type: 'ExportDefaultDeclaration',
+ line: 1
+ }]
+ },
+ {
+ filename: 'test.vue',
+ code: `export default () => {
+ const foo = () => {
+ return b
+ }
+ }`,
+ errors: [{
+ message: 'Expected the component literal to be directly exported.',
+ type: 'ExportDefaultDeclaration',
+ line: 1
+ }]
+ },
+ {
+ filename: 'test.vue',
+ code: `export default () => {
+ return
+ }`,
+ errors: [{
+ message: 'Expected the component literal to be directly exported.',
+ type: 'ExportDefaultDeclaration',
+ line: 1
+ }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ function A(props) {
+ return h('div', props.msg)
+ };
+ export default A`,
+ options: [{ disallowFunctionalComponentFunction: true }],
+ errors: [{
+ message: 'Expected the component literal to be directly exported.',
+ type: 'ExportDefaultDeclaration',
+ line: 5
+ }]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ import { h } from 'vue'
+ export default function (props) {
+ return h('div', \`Hello! \${props.name}\`)
+ }
+ `,
+ options: [{ disallowFunctionalComponentFunction: true }],
+ errors: ['Expected the component literal to be directly exported.']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ import { h } from 'vue'
+ export default function Component () {
+ return h('div')
+ }
+ `,
+ options: [{ disallowFunctionalComponentFunction: true }],
+ errors: ['Expected the component literal to be directly exported.']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ import { h } from 'vue'
+ export default (props) => {
+ return h('div', \`Hello! \${props.name}\`)
+ }
+ `,
+ options: [{ disallowFunctionalComponentFunction: true }],
+ errors: ['Expected the component literal to be directly exported.']
+ },
+ {
+ filename: 'test.vue',
+ code: `
+ import { h } from 'vue'
+ export default props => h('div', props.msg)
+ `,
+ options: [{ disallowFunctionalComponentFunction: true }],
+ errors: ['Expected the component literal to be directly exported.']
}
]
})