From 922819f448634ae81f7a1a59304dfc09066b612a Mon Sep 17 00:00:00 2001 From: Aziz Abdullaev Date: Wed, 23 Nov 2022 20:25:24 -0500 Subject: [PATCH] [New] `prefer-default-export`: add "target" option Fixes #2600. --- CHANGELOG.md | 2 + README.md | 2 +- docs/rules/prefer-default-export.md | 128 ++++++++++++++- src/rules/prefer-default-export.js | 29 +++- tests/src/rules/prefer-default-export.js | 189 ++++++++++++++++++++++- 5 files changed, 337 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f87541e9c..66d466537 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 ed1e4f822..640929c06 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. | | | | | | | diff --git a/docs/rules/prefer-default-export.md b/docs/rules/prefer-default-export.md index 38ec166e6..5d335f4c1 100644 --- a/docs/rules/prefer-default-export.md +++ b/docs/rules/prefer-default-export.md @@ -2,10 +2,44 @@ -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 d1b134cfc..32ef5004f 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); } }, }; diff --git a/tests/src/rules/prefer-default-export.js b/tests/src/rules/prefer-default-export.js index 6ecd2e3af..ae7c16a40 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 = {