Repository: vuejs/eslint-plugin-vue
Pull request #581 (2018-09-22 03:02:35)

⭐️New: Add `vue/require-direct-export` rule
Vue SFC only allows default export where the default export must be a component object. A lot of the linting rules in this package only work when the component is directly exported like so (this is why I mark is as "essential"):
```
export default {
     name: 'ComponentName'
};
```

My team follows the Airbnb JS style, and sometimes the components are [assigned to a variable before getting exported](https://github.com/airbnb/javascript#naming--filename-matches-export).

This rule is to deter components from being assigned to a variable before being exported so that all linting rules can be correctly applied.

hirokiosame: Hey @michalsnik Apologies for the delayed changes, but made the fixes now. Let me know if there's anything else you'd like me to do!
michalsnik: The category @hirokiosame is still not `undefined` as pointed in the comment.

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 513827d0..103e9419 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -145,6 +145,7 @@ For example:
 | [vue/key-spacing](./key-spacing.md) | enforce consistent spacing between keys and values in object literal properties | :wrench: |
 | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name |  |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
+| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  |
 | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
 | [vue/space-infix-ops](./space-infix-ops.md) | require spacing around infix operators | :wrench: |
 | [vue/space-unary-ops](./space-unary-ops.md) | enforce consistent spacing before or after unary operators | :wrench: |
diff --git a/docs/rules/require-direct-export.md b/docs/rules/require-direct-export.md
new file mode 100644
index 00000000..778c6630
--- /dev/null
+++ b/docs/rules/require-direct-export.md
@@ -0,0 +1,45 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/require-direct-export
+description: require the component to be directly exported
+---
+# vue/require-direct-export
+> require the component to be directly exported
+
+## Rule Details
+
+This rule aims to require that the component object be directly exported.
+
+:-1: Examples of **incorrect** code:
+
+```js
+const ComponentA = {
+	name: 'ComponentA',
+	data() {
+		return {
+			state: 1
+		}
+	}
+}
+
+export default ComponentA
+```
+
+:+1: Examples of **correct** code:
+
+```js
+export default {
+	name: 'ComponentA',
+	data() {
+		return {
+			state: 1
+		}
+	}
+}
+```
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-direct-export.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-direct-export.js)
diff --git a/lib/index.js b/lib/index.js
index 2cc622b0..df9a21b3 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -48,6 +48,7 @@ module.exports = {
     'prop-name-casing': require('./rules/prop-name-casing'),
     'require-component-is': require('./rules/require-component-is'),
     'require-default-prop': require('./rules/require-default-prop'),
+    'require-direct-export': require('./rules/require-direct-export'),
     'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
     'require-prop-types': require('./rules/require-prop-types'),
     'require-render-return': require('./rules/require-render-return'),
diff --git a/lib/rules/require-direct-export.js b/lib/rules/require-direct-export.js
new file mode 100644
index 00000000..3fac553d
--- /dev/null
+++ b/lib/rules/require-direct-export.js
@@ -0,0 +1,46 @@
+/**
+ * @fileoverview require the component to be directly exported
+ * @author Hiroki Osame <hiroki.osame@gmail.com>
+ */
+'use strict'
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'require the component to be directly exported',
+      category: undefined,
+      url: 'https://eslint.vuejs.org/rules/require-direct-export.html'
+    },
+    fixable: null,  // or "code" or "whitespace"
+    schema: []
+  },
+
+  create (context) {
+    const filePath = context.getFilename()
+
+    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.`
+          })
+        }
+      }
+    }
+  }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index c36d0cda..e2af0257 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -536,6 +536,10 @@ module.exports = {
       })
   },
 
+  isVueFile (path) {
+    return path.endsWith('.vue') || path.endsWith('.jsx')
+  },
+
   /**
    * Check whether the given node is a Vue component based
    * on the filename and default export type
@@ -545,8 +549,7 @@ module.exports = {
    * @returns {boolean}
    */
   isVueComponentFile (node, path) {
-    const isVueFile = path.endsWith('.vue') || path.endsWith('.jsx')
-    return isVueFile &&
+    return this.isVueFile(path) &&
       node.type === 'ExportDefaultDeclaration' &&
       node.declaration.type === 'ObjectExpression'
   },


Test patch:
diff --git a/tests/lib/rules/require-direct-export.js b/tests/lib/rules/require-direct-export.js
new file mode 100644
index 00000000..72f71bf9
--- /dev/null
+++ b/tests/lib/rules/require-direct-export.js
@@ -0,0 +1,56 @@
+/**
+ * @fileoverview require the component to be directly exported
+ * @author Hiroki Osame <hiroki.osame@gmail.com>
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+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()
+ruleTester.run('require-direct-export', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: `
+            export default {}
+          `,
+      parserOptions
+    }
+  ],
+
+  invalid: [
+
+    {
+      filename: 'test.vue',
+      code: `
+          const A = {};
+          export default A`,
+      parserOptions,
+      errors: [{
+        message: 'Expected the component literal to be directly exported.',
+        type: 'ExportDefaultDeclaration',
+        line: 3
+      }]
+    }
+  ]
+})