Repository: import-js/eslint-plugin-import
Pull request #2602 (2022-11-24 19:13:32)

[New] `prefer-default-export`: add "target" option
Solution for #2600.

I have added new option for the `prefer-default-export` that will check any exporting file to have a default export.

###### schema:

```
"import/prefer-default-export": [
 	( "off" | "warn" | "error" ),
	{ "target": "single" | "any" } // default is "single"
]
```
###### Config options

* **single** (current implementation of the rule, I did not change anything here): When there is only a single export from a module, prefer using default export over named export.

All examples from below are from the [rule descriptions](https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/prefer-default-export.md).

Considered warnings:

```
// bad.js

// There is only a single module export and it's a named export.
export const foo = 'foo';

```

The following patterns are not warnings:

```
// good1.js

// There is a default export.
export const foo = 'foo';
const bar = 'bar';
export default bar;
```

```
// good2.js

// There is more than one named export in the module.
export const foo = 'foo';
export const bar = 'bar';
```

```
// good3.js

// There is more than one named export in the module
const foo = 'foo';
const bar = 'bar';
export { foo, bar }
```

```
// good4.js

// There is a default export.
const foo = 'foo';
export { foo as default }
```

```
// export-star.js

// Any batch export will disable this rule. The remote module is not inspected.
export * from './other-module'
```

newly added config option:

* **any**: any exporting file must contain default export.

No warnings:

```
//has default export
export default function bar() {};
```

```
// has default export
let foo;
export { foo as default }
```

```
//contains multiple exports AND default export﻿
export const a = 5;
export function bar(){};
let foo;
export { foo as default }`
```

```
// does not contain any exports => file is not checked by the rule
import * as foo from './foo';﻿
```

```
// export-star.js

// Any batch export will disable this rule. The remote module is not inspected.
export * from './other-module'
```

Following are considered as warnings:

```
//has 2 named exports, but no default export
export const foo = 'foo';
export const bar = 'bar';
```

```
// does not have default export
let foo, bar;
export { foo, bar }
```

```
// does not have default export
export { a, b } from "foo.js"﻿
```

```
// does not have default export
let item;
export const foo = item;
export { item };`
```

### Questions

I am not sure how to migrate the test cases below to new option "any". I do not understand what's going on here and what is exported.

```
export const { foo, bar } = item;
```

```
export const { foo, bar: baz } = item;
```

```
export const { foo: { bar, baz } } = item;
```

```
export const [a, b] = item;
```

I will update docs after your review.






Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f87541e9..66d46653 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 - [`order`]: new `alphabetize.orderImportKind` option to sort imports with same path based on their kind (`type`, `typeof`) ([#2544], thanks [@stropho])
 - [`consistent-type-specifier-style`]: add rule ([#2473], thanks [@bradzacher])
 - Add [`no-empty-named-blocks`] rule ([#2568], thanks [@guilhermelimak])
+- [`prefer-default-export`]: add "target" option ([#2602], thanks [@azyzz228])
 
 ### Fixed
 - [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311])
@@ -1025,6 +1026,7 @@ for info on changes for earlier releases.
 [`memo-parser`]: ./memo-parser/README.md
 
 [#2605]: https://github.com/import-js/eslint-plugin-import/pull/2605
+[#2602]: https://github.com/import-js/eslint-plugin-import/pull/2602
 [#2598]: https://github.com/import-js/eslint-plugin-import/pull/2598
 [#2589]: https://github.com/import-js/eslint-plugin-import/pull/2589
 [#2588]: https://github.com/import-js/eslint-plugin-import/pull/2588
diff --git a/README.md b/README.md
index ed1e4f82..640929c0 100644
--- a/README.md
+++ b/README.md
@@ -89,7 +89,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
 | [no-namespace](docs/rules/no-namespace.md)                                       | Forbid namespace (a.k.a. "wildcard" `*`) imports.                          |    |       |    | 🔧 |    |    |
 | [no-unassigned-import](docs/rules/no-unassigned-import.md)                       | Forbid unassigned imports                                                  |    |       |    |    |    |    |
 | [order](docs/rules/order.md)                                                     | Enforce a convention in module import order.                               |    |       |    | 🔧 |    |    |
-| [prefer-default-export](docs/rules/prefer-default-export.md)                     | Prefer a default export if module exports a single name.                   |    |       |    |    |    |    |
+| [prefer-default-export](docs/rules/prefer-default-export.md)                     | Prefer a default export if module exports a single name or multiple names. |    |       |    |    |    |    |
 
 <!-- end auto-generated rules list -->
 
diff --git a/docs/rules/prefer-default-export.md b/docs/rules/prefer-default-export.md
index 38ec166e..5d335f4c 100644
--- a/docs/rules/prefer-default-export.md
+++ b/docs/rules/prefer-default-export.md
@@ -2,10 +2,44 @@
 
 <!-- end auto-generated rule header -->
 
-When there is only a single export from a module, prefer using default export over named export.
+In exporting files, this rule checks if there is default export or not.
 
 ## Rule Details
 
+##### rule schema:
+
+```javascript
+"import/prefer-default-export": [
+    ( "off" | "warn" | "error" ),
+	{ "target": "single" | "any" } // default is "single"
+]
+```
+
+### Config Options
+
+There are two options available: `single` and `any`. By default, if you do not specify the option, rule will assume it is `single`.
+
+#### single
+
+**Definition**: When there is only a single export from a module, prefer using default export over named export.
+
+How to setup config file for this rule:
+
+```javascript
+// you can manually specify it
+"rules": {
+    "import/prefer-default-export": [
+        ( "off" | "warn" | "error" ),
+        { "target": "single" }
+    ]
+}
+
+// config setup below will also work
+"rules": {
+    "import/prefer-default-export": "off" | "warn" | "error"
+}
+```
+
 The following patterns are considered warnings:
 
 ```javascript
@@ -58,3 +92,95 @@ export { foo as default }
 // Any batch export will disable this rule. The remote module is not inspected.
 export * from './other-module'
 ```
+
+#### any
+
+**Definition**: any exporting file must contain a default export.
+
+How to setup config file for this rule:
+
+```javascript
+// you have to manually specify it
+"rules": {
+    "import/prefer-default-export": [
+        ( "off" | "warn" | "error" ),
+        { "target": "any" }
+    ]
+}
+```
+
+
+The following patterns are *not* considered warnings:
+
+```javascript
+// good1.js
+
+//has default export
+export default function bar() {};
+```
+
+```javascript
+// good2.js
+
+// has default export
+let foo;
+export { foo as default }
+```
+
+```javascript
+// good3.js
+
+//contains multiple exports AND default export
+export const a = 5;
+export function bar(){};
+let foo;
+export { foo as default }
+```
+
+```javascript
+// good4.js
+
+// does not contain any exports => file is not checked by the rule
+import * as foo from './foo';﻿
+```
+
+```javascript
+// export-star.js
+
+// Any batch export will disable this rule. The remote module is not inspected.
+export * from './other-module'
+```
+
+The following patterns are considered warnings:
+
+```javascript
+// bad1.js
+
+//has 2 named exports, but no default export
+export const foo = 'foo';
+export const bar = 'bar';
+```
+
+```javascript
+// bad2.js
+
+// does not have default export
+let foo, bar;
+export { foo, bar }
+```
+
+```javascript
+// bad3.js
+
+// does not have default export
+export { a, b } from "foo.js"﻿
+```
+
+```javascript
+// bad4.js
+
+// does not have default export
+let item;
+export const foo = item;
+export { item };
+```
diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.js
index d1b134cf..32ef5004 100644
--- a/src/rules/prefer-default-export.js
+++ b/src/rules/prefer-default-export.js
@@ -2,15 +2,28 @@
 
 import docsUrl from '../docsUrl';
 
+const SINGLE_EXPORT_ERROR_MESSAGE = 'Prefer default export on a file with single export.';
+const ANY_EXPORT_ERROR_MESSAGE = 'Prefer default export to be present on every file that has export.';
+
 module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
       category: 'Style guide',
-      description: 'Prefer a default export if module exports a single name.',
+      description: 'Prefer a default export if module exports a single name or multiple names.',
       url: docsUrl('prefer-default-export'),
     },
-    schema: [],
+    schema: [{
+      type: 'object',
+      properties:{
+        target: {
+          type: 'string',
+          enum: ['single', 'any'],
+          default: 'single',
+        },
+      },
+      additionalProperties: false,
+    }],
   },
 
   create(context) {
@@ -19,7 +32,8 @@ module.exports = {
     let hasStarExport = false;
     let hasTypeExport = false;
     let namedExportNode = null;
-
+    // get options. by default we look into files with single export
+    const { target = 'single' } =  context.options[0] || {};
     function captureDeclaration(identifierOrPattern) {
       if (identifierOrPattern && identifierOrPattern.type === 'ObjectPattern') {
         // recursively capture
@@ -88,8 +102,13 @@ module.exports = {
       },
 
       'Program:exit': function () {
-        if (specifierExportCount === 1 && !hasDefaultExport && !hasStarExport && !hasTypeExport) {
-          context.report(namedExportNode, 'Prefer default export.');
+        if (hasDefaultExport || hasStarExport || hasTypeExport) {
+          return;
+        }
+        if (target === 'single' && specifierExportCount === 1) {
+          context.report(namedExportNode, SINGLE_EXPORT_ERROR_MESSAGE);
+        } else if (target === 'any' && specifierExportCount > 0) {
+          context.report(namedExportNode, ANY_EXPORT_ERROR_MESSAGE);
         }
       },
     };


Test patch:
diff --git a/tests/src/rules/prefer-default-export.js b/tests/src/rules/prefer-default-export.js
index 6ecd2e3a..ae7c16a4 100644
--- a/tests/src/rules/prefer-default-export.js
+++ b/tests/src/rules/prefer-default-export.js
@@ -7,6 +7,10 @@ import { version as tsEslintVersion } from 'typescript-eslint-parser/package.jso
 const ruleTester = new RuleTester();
 const rule = require('../../../src/rules/prefer-default-export');
 
+const SINGLE_EXPORT_ERROR_MESSAGE = 'Prefer default export on a file with single export.';
+const ANY_EXPORT_ERROR_MESSAGE = 'Prefer default export to be present on every file that has export.';
+
+// test cases for default option { target: 'single' }
 ruleTester.run('prefer-default-export', rule, {
   valid: [].concat(
     test({
@@ -108,7 +112,7 @@ ruleTester.run('prefer-default-export', rule, {
         export function bar() {};`,
       errors: [{
         type: 'ExportNamedDeclaration',
-        message: 'Prefer default export.',
+        message: SINGLE_EXPORT_ERROR_MESSAGE,
       }],
     }),
     test({
@@ -116,7 +120,7 @@ ruleTester.run('prefer-default-export', rule, {
         export const foo = 'foo';`,
       errors: [{
         type: 'ExportNamedDeclaration',
-        message: 'Prefer default export.',
+        message: SINGLE_EXPORT_ERROR_MESSAGE,
       }],
     }),
     test({
@@ -125,7 +129,7 @@ ruleTester.run('prefer-default-export', rule, {
         export { foo };`,
       errors: [{
         type: 'ExportSpecifier',
-        message: 'Prefer default export.',
+        message: SINGLE_EXPORT_ERROR_MESSAGE,
       }],
     }),
     test({
@@ -133,7 +137,7 @@ ruleTester.run('prefer-default-export', rule, {
         export const { foo } = { foo: "bar" };`,
       errors: [{
         type: 'ExportNamedDeclaration',
-        message: 'Prefer default export.',
+        message: SINGLE_EXPORT_ERROR_MESSAGE,
       }],
     }),
     test({
@@ -141,7 +145,7 @@ ruleTester.run('prefer-default-export', rule, {
         export const { foo: { bar } } = { foo: { bar: "baz" } };`,
       errors: [{
         type: 'ExportNamedDeclaration',
-        message: 'Prefer default export.',
+        message: SINGLE_EXPORT_ERROR_MESSAGE,
       }],
     }),
     test({
@@ -149,12 +153,185 @@ ruleTester.run('prefer-default-export', rule, {
         export const [a] = ["foo"]`,
       errors: [{
         type: 'ExportNamedDeclaration',
-        message: 'Prefer default export.',
+        message: SINGLE_EXPORT_ERROR_MESSAGE,
       }],
     }),
   ],
 });
 
+// test cases for { target: 'any' }
+ruleTester.run('prefer-default-export', rule, {
+  // Any exporting file must contain default export
+  valid: [].concat(
+    test({
+      code: `
+          export default function bar() {};`,
+      options: [{
+        target: 'any',
+      }],
+    }),
+    test({
+      code: `
+              export const foo = 'foo';
+              export const bar = 'bar';
+              export default 42;`,
+      options: [{
+        target: 'any',
+      }],
+    }),
+    test({
+      code: `
+            export default a = 2;`,
+      options: [{
+        target: 'any',
+      }],
+    }),
+    test({
+      code: `
+            export const a = 2;
+            export default function foo() {};`,
+      options: [{
+        target: 'any',
+      }],
+    }),
+    test({
+      code: `
+          export const a = 5;
+          export function bar(){};
+          let foo;
+          export { foo as default }`,
+      options: [{
+        target: 'any',
+      }],
+    }),
+    test({
+      code: `
+          export * from './foo';`,
+      options: [{
+        target: 'any',
+      }],
+    }),
+    test({
+      code: `export Memory, { MemoryValue } from './Memory'`,
+      parser: parsers.BABEL_OLD,
+      options: [{
+        target: 'any',
+      }],
+    }),
+    // no exports at all
+    test({
+      code: `
+            import * as foo from './foo';`,
+      options: [{
+        target: 'any',
+      }],
+    }),
+    test({
+      code: `const a = 5;`,
+      options: [{
+        target: 'any',
+      }],
+    }),
+    // es2022: Arbitrary module namespae identifier names
+    testVersion('>= 8.7', () => ({
+      code: 'export const a = 4; let foo; export { foo as "default" };',
+      options: [{
+        target: 'any',
+      }],
+      parserOptions: { ecmaVersion: 2022 },
+    })),
+  ),
+  // { target: 'any' } invalid cases when any exporting file must contain default export but does not
+  invalid: [].concat(
+    test({
+      code: `
+        export const foo = 'foo';
+        export const bar = 'bar';`,
+      options: [{
+        target: 'any',
+      }],
+      errors: [{
+        message: ANY_EXPORT_ERROR_MESSAGE,
+      }],
+    }),
+    test({
+      code: `
+        export const foo = 'foo';
+        export function bar() {};`,
+      options: [{
+        target: 'any',
+      }],
+      errors: [{
+        message: ANY_EXPORT_ERROR_MESSAGE,
+      }],
+    }),
+    test({
+      code: `
+        let foo, bar;
+        export { foo, bar }`,
+      options: [{
+        target: 'any',
+      }],
+      errors: [{
+        message: ANY_EXPORT_ERROR_MESSAGE,
+      }],
+    }),
+    test({
+      code: `
+        let item;
+        export const foo = item;
+        export { item };`,
+      options: [{
+        target: 'any',
+      }],
+      errors: [{
+        message: ANY_EXPORT_ERROR_MESSAGE,
+      }],
+    }),
+    test({
+      code: 'export { a, b } from "foo.js"',
+      parser: parsers.BABEL_OLD,
+      options: [{
+        target: 'any',
+      }],
+      errors: [{
+        message: ANY_EXPORT_ERROR_MESSAGE,
+      }],
+    }),
+    test({
+      code: `
+        const foo = 'foo';
+        export { foo };`,
+      options: [{
+        target: 'any',
+      }],
+      errors: [{
+        message: ANY_EXPORT_ERROR_MESSAGE,
+      }],
+    }),
+    test({
+      code: `
+        export const { foo } = { foo: "bar" };`,
+      options: [{
+        target: 'any',
+      }],
+      errors: [{
+        message: ANY_EXPORT_ERROR_MESSAGE,
+      }],
+    }),
+    test({
+      code: `
+        export const { foo: { bar } } = { foo: { bar: "baz" } };`,
+      options: [{
+        target: 'any',
+      }],
+      errors: [{
+        message: ANY_EXPORT_ERROR_MESSAGE,
+      }],
+    }),
+  ),
+});
+
 context('TypeScript', function () {
   getNonDefaultParsers().forEach((parser) => {
     const parserConfig = {