From 731eda75c75df5e285f23d36838ad190b7cf009c Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 19 Jul 2026 16:55:51 +0200 Subject: [PATCH 1/7] Add `no-barrel-files` rule Fixes #3520 --- docs/rules/no-barrel-files.md | 48 +++++++ eslint.dogfooding.config.js | 2 + readme.md | 1 + rules/index.js | 1 + rules/no-barrel-files.js | 100 ++++++++++++++ test/no-barrel-files.js | 55 ++++++++ test/snapshots/no-barrel-files.js.md | 183 +++++++++++++++++++++++++ test/snapshots/no-barrel-files.js.snap | Bin 0 -> 707 bytes 8 files changed, 390 insertions(+) create mode 100644 docs/rules/no-barrel-files.md create mode 100644 rules/no-barrel-files.js create mode 100644 test/no-barrel-files.js create mode 100644 test/snapshots/no-barrel-files.js.md create mode 100644 test/snapshots/no-barrel-files.js.snap diff --git a/docs/rules/no-barrel-files.md b/docs/rules/no-barrel-files.md new file mode 100644 index 0000000000..b49596a566 --- /dev/null +++ b/docs/rules/no-barrel-files.md @@ -0,0 +1,48 @@ +# no-barrel-files + +📝 Disallow barrel files. + +🚫 This rule is _disabled_ in the following [configs](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config): ✅ `recommended`, ☑️ `unopinionated`. + + + + +A barrel file only imports and re-exports bindings from other modules. Barrel files make it less clear where a dependency comes from and can cause consumers to load more modules than they need. + +This rule treats any file whose top-level body consists only of imports and re-exports as a barrel, including a file that re-exports from only one module. It is intentionally syntax-only: it does not resolve modules or try to distinguish package entry points from internal barrel files. Disable it in intentional entry points. + +Side-effect-only imports are not part of a barrel file, so a file that combines one with re-exports is ignored. + +## Examples + +Examples of **incorrect** code for this rule: + +```js +export {foo} from './foo.js'; +``` + +```js +import {foo} from './foo.js'; +export {foo}; +``` + +```js +export * from './foo.js'; +export * from './bar.js'; +``` + +Examples of **correct** code for this rule: + +```js +export const foo = 1; +``` + +```js +import {foo} from './foo.js'; +export const bar = foo; +``` + +```js +export {foo} from './foo.js'; +export const bar = 1; +``` diff --git a/eslint.dogfooding.config.js b/eslint.dogfooding.config.js index 734b420a48..39e8d45b92 100644 --- a/eslint.dogfooding.config.js +++ b/eslint.dogfooding.config.js @@ -53,6 +53,8 @@ const config = [ 'unicorn/prefer-simplified-conditions': 'off', // Many existing internal utilities intentionally export declarations separately. 'unicorn/default-export-style': 'off', + // The plugin intentionally keeps a few internal barrel files for shared utilities and rule exports. + 'unicorn/no-barrel-files': 'off', // TODO: Enable when targeting Node.js 26. 'unicorn/prefer-iterator-concat': 'off', 'unicorn/prefer-temporal': 'off', diff --git a/readme.md b/readme.md index 84b04b5e6b..abb6410af2 100644 --- a/readme.md +++ b/readme.md @@ -147,6 +147,7 @@ export default defineConfig([ | [no-async-promise-finally](docs/rules/no-async-promise-finally.md) | Disallow async functions as `Promise#finally()` callbacks. | ✅ ☑️ | | | | | [no-await-expression-member](docs/rules/no-await-expression-member.md) | Disallow member access from await expression. | ✅ | 🔧 | | | | [no-await-in-promise-methods](docs/rules/no-await-in-promise-methods.md) | Disallow using `await` in `Promise` method parameters. | ✅ ☑️ | | 💡 | | +| [no-barrel-files](docs/rules/no-barrel-files.md) | Disallow barrel files. | | | | | | [no-blob-to-file](docs/rules/no-blob-to-file.md) | Disallow unnecessary `Blob` to `File` conversion. | ✅ ☑️ | | 💡 | | | [no-boolean-sort-comparator](docs/rules/no-boolean-sort-comparator.md) | Disallow boolean-returning sort comparators. | ✅ ☑️ | | 💡 | | | [no-break-in-nested-loop](docs/rules/no-break-in-nested-loop.md) | Disallow `break` and `continue` in nested loops and switches inside loops. | ✅ | | | | diff --git a/rules/index.js b/rules/index.js index 8a518376fb..b6dc1bf7af 100644 --- a/rules/index.js +++ b/rules/index.js @@ -56,6 +56,7 @@ export {default as 'no-asterisk-prefix-in-documentation-comments'} from './no-as export {default as 'no-async-promise-finally'} from './no-async-promise-finally.js'; export {default as 'no-await-expression-member'} from './no-await-expression-member.js'; export {default as 'no-await-in-promise-methods'} from './no-await-in-promise-methods.js'; +export {default as 'no-barrel-files'} from './no-barrel-files.js'; export {default as 'no-blob-to-file'} from './no-blob-to-file.js'; export {default as 'no-boolean-sort-comparator'} from './no-boolean-sort-comparator.js'; export {default as 'no-break-in-nested-loop'} from './no-break-in-nested-loop.js'; diff --git a/rules/no-barrel-files.js b/rules/no-barrel-files.js new file mode 100644 index 0000000000..d748270e69 --- /dev/null +++ b/rules/no-barrel-files.js @@ -0,0 +1,100 @@ +/** @import * as ESLint from 'eslint'; */ + +const MESSAGE_ID = 'no-barrel-files'; +const messages = { + [MESSAGE_ID]: 'Barrel files are not allowed.', +}; + +const getImportedBindings = program => { + const importedBindings = new Set(); + + for (const node of program.body) { + if (node.type !== 'ImportDeclaration') { + continue; + } + + for (const specifier of node.specifiers) { + importedBindings.add(specifier.local.name); + } + } + + return importedBindings; +}; + +const isBarrelFile = program => { + const importedBindings = getImportedBindings(program); + let hasExport = false; + + for (const node of program.body) { + if (node.type === 'ImportDeclaration') { + if (node.specifiers.length === 0) { + return false; + } + + continue; + } + + if (node.type === 'ExportAllDeclaration') { + hasExport = true; + continue; + } + + if (node.type === 'ExportNamedDeclaration') { + if ( + node.declaration + || (!node.source && node.specifiers.some(specifier => !importedBindings.has(specifier.local.name))) + ) { + return false; + } + + hasExport ||= node.specifiers.length > 0; + continue; + } + + if ( + node.type === 'ExportDefaultDeclaration' + && node.declaration.type === 'Identifier' + && importedBindings.has(node.declaration.name) + ) { + hasExport = true; + continue; + } + + return false; + } + + return hasExport; +}; + +/** @param {ESLint.Rule.RuleContext} context */ +const create = context => { + context.on('Program', node => { + if (!isBarrelFile(node)) { + return; + } + + return { + node, + messageId: MESSAGE_ID, + }; + }); +}; + +/** @type {ESLint.Rule.RuleModule} */ +const config = { + create, + meta: { + type: 'suggestion', + docs: { + description: 'Disallow barrel files.', + recommended: false, + }, + schema: [], + messages, + languages: [ + 'js/js', + ], + }, +}; + +export default config; diff --git a/test/no-barrel-files.js b/test/no-barrel-files.js new file mode 100644 index 0000000000..0a35d68ee0 --- /dev/null +++ b/test/no-barrel-files.js @@ -0,0 +1,55 @@ +import outdent from 'outdent'; +import {getTester, parsers} from './utils/test.js'; + +const {test} = getTester(import.meta); + +test.snapshot({ + valid: [ + 'import "foo";', + 'import {} from "foo";', + 'import {foo} from "foo"; export const bar = foo;', + 'export {};', + 'export {} from "foo";', + 'const foo = 1; export {foo};', + 'export {foo} from "foo"; const bar = 1;', + 'export const foo = 1;', + 'export default foo;', + 'export default function foo() {}', + 'import "foo"; export {bar} from "bar";', + outdent` + import {foo} from './foo.js'; + const bar = foo; + export {bar}; + `, + ], + invalid: [ + 'export {foo} from "foo";', + 'export * from "foo";', + 'export * as namespace from "foo";', + 'export {}; export {foo} from "foo";', + 'import {foo} from "foo"; export {foo};', + 'import foo from "foo"; export default foo;', + outdent` + import {foo} from './foo.js'; + export {foo}; + export {bar} from './bar.js'; + `, + outdent` + // This is still a barrel file. + export {}; + export {foo} from './foo.js'; + export {bar} from './bar.js'; + `, + { + code: 'export type {Foo} from "foo";', + languageOptions: {parser: parsers.typescript}, + }, + { + code: outdent` + import type {Foo} from './foo.js'; + export type {Foo}; + `, + languageOptions: {parser: parsers.typescript}, + }, + ], +}); diff --git a/test/snapshots/no-barrel-files.js.md b/test/snapshots/no-barrel-files.js.md new file mode 100644 index 0000000000..5958bb8c78 --- /dev/null +++ b/test/snapshots/no-barrel-files.js.md @@ -0,0 +1,183 @@ +# Snapshot report for `test/no-barrel-files.js` + +The actual snapshot is saved in `no-barrel-files.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## invalid(1): export {foo} from "foo"; + +> Input + + `␊ + 1 | export {foo} from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export {foo} from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(2): export * from "foo"; + +> Input + + `␊ + 1 | export * from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export * from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(3): export * as namespace from "foo"; + +> Input + + `␊ + 1 | export * as namespace from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export * as namespace from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(4): export {}; export {foo} from "foo"; + +> Input + + `␊ + 1 | export {}; export {foo} from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export {}; export {foo} from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(5): import {foo} from "foo"; export {foo}; + +> Input + + `␊ + 1 | import {foo} from "foo"; export {foo};␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import {foo} from "foo"; export {foo};␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(6): import foo from "foo"; export default foo; + +> Input + + `␊ + 1 | import foo from "foo"; export default foo;␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import foo from "foo"; export default foo;␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(7): import {foo} from './foo.js'; export {foo}; export {bar} from './bar.js'; + +> Input + + `␊ + 1 | import {foo} from './foo.js';␊ + 2 | export {foo};␊ + 3 | export {bar} from './bar.js';␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import {foo} from './foo.js';␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^␊ + > 2 | export {foo};␊ + | ^^^^^^^^^^^^^␊ + > 3 | export {bar} from './bar.js';␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(8): // This is still a barrel file. export {}; export {foo} from './foo.js'; export {bar} from './bar.js'; + +> Input + + `␊ + 1 | // This is still a barrel file.␊ + 2 | export {};␊ + 3 | export {foo} from './foo.js';␊ + 4 | export {bar} from './bar.js';␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | // This is still a barrel file.␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^␊ + > 2 | export {};␊ + | ^^^^^^^^^^␊ + > 3 | export {foo} from './foo.js';␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^␊ + > 4 | export {bar} from './bar.js';␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(9): export type {Foo} from "foo"; + +> Input + + `␊ + 1 | export type {Foo} from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export type {Foo} from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(10): import type {Foo} from './foo.js'; export type {Foo}; + +> Input + + `␊ + 1 | import type {Foo} from './foo.js';␊ + 2 | export type {Foo};␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import type {Foo} from './foo.js';␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^␊ + > 2 | export type {Foo};␊ + | ^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` diff --git a/test/snapshots/no-barrel-files.js.snap b/test/snapshots/no-barrel-files.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..153af3e45f75bd4f0aca3d1283aa70319ef0bba3 GIT binary patch literal 707 zcmV;!0zCaeRzV@Hq#p@OqPrch4|J4qq2MUy{1{@vhp-ZJzOtRjlw@+BD|vDnMMRM|{#EC? zb9S|^IX7D10(4+(eBPyugp|?eX?Ccl3JNL2?h7gk77H8S!rYS!CgSyDkxYOT~I$J=u>%oa-U zUZr7bq;#|s=S?|h;Qw3+K zY|Uc8wjRIfHxh|-P#^jKl|E)pz7$NLpgcgcAj6V^^i3y{P`aCF?!1+4Ixv6#I zsvpFh`yb?LrPLR{LL!Q_Ara?mXK5cG+V6~pEp6xmbSv+>vwOYLJ?-YY!u&&p`D@{C z97tm=+<`XOndjVrMFJN4Em&@mm;0gDhaiAg`-y}^f>a4(Kp-qs{rYLCbB3iCzN2D% zb*5%$2&ZC)884Vhx3+*^-nQ0JjCWd9!!!qL(Y_VaXdF|q%QE(C=5Tqo^K#p56{iIC zIiqm&tnqx%LzCk+J|E~sbQ9nUlF1Aw1B2e>k=eQU%6L!=;A#|_UMrSpkiA5?KEcDb pMjW?TX$u|DKd3`~n?`E=c6RSL+$ZI@ObT;W<3Ac@a!36Q0017tN|FEo literal 0 HcmV?d00001 From ccc2261dcea05026fc32b40fb8b77fea4359aad3 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 19 Jul 2026 17:03:26 +0200 Subject: [PATCH 2/7] `no-barrel-files`: Improve detection and coverage Fixes #3520 --- docs/rules/no-barrel-files.md | 2 +- rules/no-barrel-files.js | 10 +++---- test/no-barrel-files.js | 3 ++ test/snapshots/no-barrel-files.js.md | 40 ++++++++++++++++++++++--- test/snapshots/no-barrel-files.js.snap | Bin 707 -> 774 bytes 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/docs/rules/no-barrel-files.md b/docs/rules/no-barrel-files.md index b49596a566..9607035071 100644 --- a/docs/rules/no-barrel-files.md +++ b/docs/rules/no-barrel-files.md @@ -11,7 +11,7 @@ A barrel file only imports and re-exports bindings from other modules. Barrel fi This rule treats any file whose top-level body consists only of imports and re-exports as a barrel, including a file that re-exports from only one module. It is intentionally syntax-only: it does not resolve modules or try to distinguish package entry points from internal barrel files. Disable it in intentional entry points. -Side-effect-only imports are not part of a barrel file, so a file that combines one with re-exports is ignored. +Side-effect-only imports, including `import 'foo'` and `import {} from 'foo'`, are not part of a barrel file, so a file that combines one with re-exports is ignored. ## Examples diff --git a/rules/no-barrel-files.js b/rules/no-barrel-files.js index d748270e69..ce3657225d 100644 --- a/rules/no-barrel-files.js +++ b/rules/no-barrel-files.js @@ -23,7 +23,7 @@ const getImportedBindings = program => { const isBarrelFile = program => { const importedBindings = getImportedBindings(program); - let hasExport = false; + let hasReExport = false; for (const node of program.body) { if (node.type === 'ImportDeclaration') { @@ -35,7 +35,7 @@ const isBarrelFile = program => { } if (node.type === 'ExportAllDeclaration') { - hasExport = true; + hasReExport = true; continue; } @@ -47,7 +47,7 @@ const isBarrelFile = program => { return false; } - hasExport ||= node.specifiers.length > 0; + hasReExport ||= node.specifiers.length > 0; continue; } @@ -56,14 +56,14 @@ const isBarrelFile = program => { && node.declaration.type === 'Identifier' && importedBindings.has(node.declaration.name) ) { - hasExport = true; + hasReExport = true; continue; } return false; } - return hasExport; + return hasReExport; }; /** @param {ESLint.Rule.RuleContext} context */ diff --git a/test/no-barrel-files.js b/test/no-barrel-files.js index 0a35d68ee0..81cd75db21 100644 --- a/test/no-barrel-files.js +++ b/test/no-barrel-files.js @@ -7,6 +7,7 @@ test.snapshot({ valid: [ 'import "foo";', 'import {} from "foo";', + 'import {} from "foo"; export {bar} from "bar";', 'import {foo} from "foo"; export const bar = foo;', 'export {};', 'export {} from "foo";', @@ -29,6 +30,8 @@ test.snapshot({ 'export {}; export {foo} from "foo";', 'import {foo} from "foo"; export {foo};', 'import foo from "foo"; export default foo;', + 'export default foo; import foo from "foo";', + 'import * as namespace from "foo"; export {namespace as default};', outdent` import {foo} from './foo.js'; export {foo}; diff --git a/test/snapshots/no-barrel-files.js.md b/test/snapshots/no-barrel-files.js.md index 5958bb8c78..2ae025b7c8 100644 --- a/test/snapshots/no-barrel-files.js.md +++ b/test/snapshots/no-barrel-files.js.md @@ -100,7 +100,39 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(7): import {foo} from './foo.js'; export {foo}; export {bar} from './bar.js'; +## invalid(7): export default foo; import foo from "foo"; + +> Input + + `␊ + 1 | export default foo; import foo from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export default foo; import foo from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(8): import * as namespace from "foo"; export {namespace as default}; + +> Input + + `␊ + 1 | import * as namespace from "foo"; export {namespace as default};␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import * as namespace from "foo"; export {namespace as default};␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(9): import {foo} from './foo.js'; export {foo}; export {bar} from './bar.js'; > Input @@ -122,7 +154,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(8): // This is still a barrel file. export {}; export {foo} from './foo.js'; export {bar} from './bar.js'; +## invalid(10): // This is still a barrel file. export {}; export {foo} from './foo.js'; export {bar} from './bar.js'; > Input @@ -147,7 +179,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(9): export type {Foo} from "foo"; +## invalid(11): export type {Foo} from "foo"; > Input @@ -163,7 +195,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(10): import type {Foo} from './foo.js'; export type {Foo}; +## invalid(12): import type {Foo} from './foo.js'; export type {Foo}; > Input diff --git a/test/snapshots/no-barrel-files.js.snap b/test/snapshots/no-barrel-files.js.snap index 153af3e45f75bd4f0aca3d1283aa70319ef0bba3..c63d5bad4b13524621ecb81b69d635fc9eae1060 100644 GIT binary patch literal 774 zcmV+h1NrkUoz zO~cG9W_=^dx|7bDDHyRKS3cY1`3P1}5{)bZoe;(26OtNhd1y8BZE;dkPDE-k)g^mt zB^qWGC3rp4Fga2btpg-B(e+NAsv)n+#M%+DrA8OLAsqz@kAQX?j0ZZ5ChBESo(9g9 zq<_* zTz$>51v%9+J&Ve;O3vVU+i|6|->CV_fQx_iS&+QjXTkBiv-R3W^?Icp)2Rh5fOh7T zcXY3lx<@Wf74a`B;va*ljsq#IkkuoDo%zn{86;q^Ud3{&_$Ys~(FMnWr(2!^Pk{h)FMB0Lp6Unv%L>DN;)o`Ldk0U1DF!HMfDH> E00n(>x&QzG literal 707 zcmV;!0zCaeRzV@Hq#p@OqPrch4|J4qq2MUy{1{@vhp-ZJzOtRjlw@+BD|vDnMMRM|{#EC? zb9S|^IX7D10(4+(eBPyugp|?eX?Ccl3JNL2?h7gk77H8S!rYS!CgSyDkxYOT~I$J=u>%oa-U zUZr7bq;#|s=S?|h;Qw3+K zY|Uc8wjRIfHxh|-P#^jKl|E)pz7$NLpgcgcAj6V^^i3y{P`aCF?!1+4Ixv6#I zsvpFh`yb?LrPLR{LL!Q_Ara?mXK5cG+V6~pEp6xmbSv+>vwOYLJ?-YY!u&&p`D@{C z97tm=+<`XOndjVrMFJN4Em&@mm;0gDhaiAg`-y}^f>a4(Kp-qs{rYLCbB3iCzN2D% zb*5%$2&ZC)884Vhx3+*^-nQ0JjCWd9!!!qL(Y_VaXdF|q%QE(C=5Tqo^K#p56{iIC zIiqm&tnqx%LzCk+J|E~sbQ9nUlF1Aw1B2e>k=eQU%6L!=;A#|_UMrSpkiA5?KEcDb pMjW?TX$u|DKd3`~n?`E=c6RSL+$ZI@ObT;W<3Ac@a!36Q0017tN|FEo From b18b38eceb8d6e624f42cf5837e3f4d02b1b0535 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 19 Jul 2026 17:05:51 +0200 Subject: [PATCH 3/7] `no-barrel-files`: Add AST coverage Fixes #3520 --- docs/rules/no-barrel-files.md | 2 +- test/no-barrel-files.js | 5 +++ test/snapshots/no-barrel-files.js.md | 46 +++++++++++++++++++++---- test/snapshots/no-barrel-files.js.snap | Bin 774 -> 842 bytes 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/docs/rules/no-barrel-files.md b/docs/rules/no-barrel-files.md index 9607035071..56c3e8f588 100644 --- a/docs/rules/no-barrel-files.md +++ b/docs/rules/no-barrel-files.md @@ -7,7 +7,7 @@ -A barrel file only imports and re-exports bindings from other modules. Barrel files make it less clear where a dependency comes from and can cause consumers to load more modules than they need. +A barrel file only imports and re-exports bindings, including type-only bindings, from other modules. Barrel files make it less clear where a dependency comes from and can cause consumers to load more modules than they need. This rule treats any file whose top-level body consists only of imports and re-exports as a barrel, including a file that re-exports from only one module. It is intentionally syntax-only: it does not resolve modules or try to distinguish package entry points from internal barrel files. Disable it in intentional entry points. diff --git a/test/no-barrel-files.js b/test/no-barrel-files.js index 81cd75db21..fdefb58d96 100644 --- a/test/no-barrel-files.js +++ b/test/no-barrel-files.js @@ -29,6 +29,7 @@ test.snapshot({ 'export * as namespace from "foo";', 'export {}; export {foo} from "foo";', 'import {foo} from "foo"; export {foo};', + 'import {foo as bar} from "foo"; export {bar};', 'import foo from "foo"; export default foo;', 'export default foo; import foo from "foo";', 'import * as namespace from "foo"; export {namespace as default};', @@ -47,6 +48,10 @@ test.snapshot({ code: 'export type {Foo} from "foo";', languageOptions: {parser: parsers.typescript}, }, + { + code: 'export type * from "foo";', + languageOptions: {parser: parsers.typescript}, + }, { code: outdent` import type {Foo} from './foo.js'; diff --git a/test/snapshots/no-barrel-files.js.md b/test/snapshots/no-barrel-files.js.md index 2ae025b7c8..5e766bc203 100644 --- a/test/snapshots/no-barrel-files.js.md +++ b/test/snapshots/no-barrel-files.js.md @@ -84,7 +84,23 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(6): import foo from "foo"; export default foo; +## invalid(6): import {foo as bar} from "foo"; export {bar}; + +> Input + + `␊ + 1 | import {foo as bar} from "foo"; export {bar};␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import {foo as bar} from "foo"; export {bar};␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(7): import foo from "foo"; export default foo; > Input @@ -100,7 +116,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(7): export default foo; import foo from "foo"; +## invalid(8): export default foo; import foo from "foo"; > Input @@ -116,7 +132,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(8): import * as namespace from "foo"; export {namespace as default}; +## invalid(9): import * as namespace from "foo"; export {namespace as default}; > Input @@ -132,7 +148,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(9): import {foo} from './foo.js'; export {foo}; export {bar} from './bar.js'; +## invalid(10): import {foo} from './foo.js'; export {foo}; export {bar} from './bar.js'; > Input @@ -154,7 +170,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(10): // This is still a barrel file. export {}; export {foo} from './foo.js'; export {bar} from './bar.js'; +## invalid(11): // This is still a barrel file. export {}; export {foo} from './foo.js'; export {bar} from './bar.js'; > Input @@ -179,7 +195,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(11): export type {Foo} from "foo"; +## invalid(12): export type {Foo} from "foo"; > Input @@ -195,7 +211,23 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(12): import type {Foo} from './foo.js'; export type {Foo}; +## invalid(13): export type * from "foo"; + +> Input + + `␊ + 1 | export type * from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export type * from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(14): import type {Foo} from './foo.js'; export type {Foo}; > Input diff --git a/test/snapshots/no-barrel-files.js.snap b/test/snapshots/no-barrel-files.js.snap index c63d5bad4b13524621ecb81b69d635fc9eae1060..bdf0e5756aa563988b5a4f1d18176ae6f693ec09 100644 GIT binary patch literal 842 zcmV-Q1GW4?RzVC7CR}-s|IYm8zcaVr?5>d8 zJLvF*#4u*T)M8QJ}w)L1R#a{BZ)k&8E02srT=7M$R zx?F(m2(Ey+90t+@CWL&=dzDEX&OO(aF4U^EfC2;*>Jf!qb0lreMH!xf-%ULxP6Z6 zojWQd!5JzWv&)ZfHNdxw@%bB|%Un7-{{ca915s>EQ_L2nV17j~Kc&GmSYQ^W^Pyc1 z(5^&i{TPYZdfaAv;`A}W^)BLC$6O5*DX3o%)MFh~6ZKh%&WrPQ)EeV)XhJr{X|9+D z=M3uq3Fo5#=W66m{n!gPQS_9lARVd~Y$C`HuxE3xL!tkL(ErdmIsckDI>x^%(`NYF ze2biKOO>iEW#yGxN(>Lw_QH3&%sqW~x?nq~U@x>IFt(rtP*40p`}Z2Ld*tF&75|_r z{x+DHIFQT|SsgOisqd_gK>`Nr<*c^~>+PSDs#SQ}cN91Zl;;Qm%pY%oY$g2%jIxHI z89t(FyemxHkP%M84^w8))vlStFwcuE6ysts>6q+b4YqZ{HH2jnewoLg*&NQ!cjeG` zv6HFyX2{{-e&(5@fqKU^JW**8st3G9GDFP_)##lan#B-JBY`I4kUoz zO~cG9W_=^dx|7bDDHyRKS3cY1`3P1}5{)bZoe;(26OtNhd1y8BZE;dkPDE-k)g^mt zB^qWGC3rp4Fga2btpg-B(e+NAsv)n+#M%+DrA8OLAsqz@kAQX?j0ZZ5ChBESo(9g9 zq<_* zTz$>51v%9+J&Ve;O3vVU+i|6|->CV_fQx_iS&+QjXTkBiv-R3W^?Icp)2Rh5fOh7T zcXY3lx<@Wf74a`B;va*ljsq#IkkuoDo%zn{86;q^Ud3{&_$Ys~(FMnWr(2!^Pk{h)FMB0Lp6Unv%L>DN;)o`Ldk0U1DF!HMfDH> E00n(>x&QzG From 72467f46478b34fa66a3fc84d572689095ad8565 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 19 Jul 2026 17:14:04 +0200 Subject: [PATCH 4/7] `no-barrel-files`: Handle TypeScript default exports Fixes #3520 --- docs/rules/no-barrel-files.md | 2 + rules/no-barrel-files.js | 27 ++--- test/no-barrel-files.js | 22 ++++ test/snapshots/no-barrel-files.js.md | 138 ++++++++++++++++++++++--- test/snapshots/no-barrel-files.js.snap | Bin 842 -> 1076 bytes 5 files changed, 163 insertions(+), 26 deletions(-) diff --git a/docs/rules/no-barrel-files.md b/docs/rules/no-barrel-files.md index 56c3e8f588..aa04300ddd 100644 --- a/docs/rules/no-barrel-files.md +++ b/docs/rules/no-barrel-files.md @@ -11,6 +11,8 @@ A barrel file only imports and re-exports bindings, including type-only bindings This rule treats any file whose top-level body consists only of imports and re-exports as a barrel, including a file that re-exports from only one module. It is intentionally syntax-only: it does not resolve modules or try to distinguish package entry points from internal barrel files. Disable it in intentional entry points. +At least one binding must be re-exported; files containing only empty exports are ignored. The rule recognizes ECMAScript module syntax, including TypeScript's type-only extensions. + Side-effect-only imports, including `import 'foo'` and `import {} from 'foo'`, are not part of a barrel file, so a file that combines one with re-exports is ignored. ## Examples diff --git a/rules/no-barrel-files.js b/rules/no-barrel-files.js index ce3657225d..1e52ed30e4 100644 --- a/rules/no-barrel-files.js +++ b/rules/no-barrel-files.js @@ -1,12 +1,14 @@ /** @import * as ESLint from 'eslint'; */ +import {unwrapTypeScriptExpression} from './utils/index.js'; + const MESSAGE_ID = 'no-barrel-files'; const messages = { [MESSAGE_ID]: 'Barrel files are not allowed.', }; -const getImportedBindings = program => { - const importedBindings = new Set(); +const getImportedBindingNames = program => { + const importedBindingNames = new Set(); for (const node of program.body) { if (node.type !== 'ImportDeclaration') { @@ -14,15 +16,15 @@ const getImportedBindings = program => { } for (const specifier of node.specifiers) { - importedBindings.add(specifier.local.name); + importedBindingNames.add(specifier.local.name); } } - return importedBindings; + return importedBindingNames; }; const isBarrelFile = program => { - const importedBindings = getImportedBindings(program); + const importedBindingNames = getImportedBindingNames(program); let hasReExport = false; for (const node of program.body) { @@ -42,7 +44,7 @@ const isBarrelFile = program => { if (node.type === 'ExportNamedDeclaration') { if ( node.declaration - || (!node.source && node.specifiers.some(specifier => !importedBindings.has(specifier.local.name))) + || (!node.source && node.specifiers.some(specifier => !importedBindingNames.has(specifier.local.name))) ) { return false; } @@ -51,13 +53,12 @@ const isBarrelFile = program => { continue; } - if ( - node.type === 'ExportDefaultDeclaration' - && node.declaration.type === 'Identifier' - && importedBindings.has(node.declaration.name) - ) { - hasReExport = true; - continue; + if (node.type === 'ExportDefaultDeclaration') { + const declaration = unwrapTypeScriptExpression(node.declaration); + if (declaration.type === 'Identifier' && importedBindingNames.has(declaration.name)) { + hasReExport = true; + continue; + } } return false; diff --git a/test/no-barrel-files.js b/test/no-barrel-files.js index fdefb58d96..706309bf13 100644 --- a/test/no-barrel-files.js +++ b/test/no-barrel-files.js @@ -25,6 +25,8 @@ test.snapshot({ ], invalid: [ 'export {foo} from "foo";', + 'export {foo as bar} from "foo";', + 'export {default} from "foo";', 'export * from "foo";', 'export * as namespace from "foo";', 'export {}; export {foo} from "foo";', @@ -52,6 +54,26 @@ test.snapshot({ code: 'export type * from "foo";', languageOptions: {parser: parsers.typescript}, }, + { + code: 'export type * as Namespace from "foo";', + languageOptions: {parser: parsers.typescript}, + }, + { + code: 'import foo from "foo"; export default foo as Foo;', + languageOptions: {parser: parsers.typescript}, + }, + { + code: 'import foo from "foo"; export default foo satisfies Foo;', + languageOptions: {parser: parsers.typescript}, + }, + { + code: 'import foo from "foo"; export default foo!;', + languageOptions: {parser: parsers.typescript}, + }, + { + code: 'import foo from "foo"; export default foo;', + languageOptions: {parser: parsers.typescript}, + }, { code: outdent` import type {Foo} from './foo.js'; diff --git a/test/snapshots/no-barrel-files.js.md b/test/snapshots/no-barrel-files.js.md index 5e766bc203..9f3d029cae 100644 --- a/test/snapshots/no-barrel-files.js.md +++ b/test/snapshots/no-barrel-files.js.md @@ -20,7 +20,39 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(2): export * from "foo"; +## invalid(2): export {foo as bar} from "foo"; + +> Input + + `␊ + 1 | export {foo as bar} from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export {foo as bar} from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(3): export {default} from "foo"; + +> Input + + `␊ + 1 | export {default} from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export {default} from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(4): export * from "foo"; > Input @@ -36,7 +68,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(3): export * as namespace from "foo"; +## invalid(5): export * as namespace from "foo"; > Input @@ -52,7 +84,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(4): export {}; export {foo} from "foo"; +## invalid(6): export {}; export {foo} from "foo"; > Input @@ -68,7 +100,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(5): import {foo} from "foo"; export {foo}; +## invalid(7): import {foo} from "foo"; export {foo}; > Input @@ -84,7 +116,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(6): import {foo as bar} from "foo"; export {bar}; +## invalid(8): import {foo as bar} from "foo"; export {bar}; > Input @@ -100,7 +132,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(7): import foo from "foo"; export default foo; +## invalid(9): import foo from "foo"; export default foo; > Input @@ -116,7 +148,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(8): export default foo; import foo from "foo"; +## invalid(10): export default foo; import foo from "foo"; > Input @@ -132,7 +164,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(9): import * as namespace from "foo"; export {namespace as default}; +## invalid(11): import * as namespace from "foo"; export {namespace as default}; > Input @@ -148,7 +180,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(10): import {foo} from './foo.js'; export {foo}; export {bar} from './bar.js'; +## invalid(12): import {foo} from './foo.js'; export {foo}; export {bar} from './bar.js'; > Input @@ -170,7 +202,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(11): // This is still a barrel file. export {}; export {foo} from './foo.js'; export {bar} from './bar.js'; +## invalid(13): // This is still a barrel file. export {}; export {foo} from './foo.js'; export {bar} from './bar.js'; > Input @@ -195,7 +227,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(12): export type {Foo} from "foo"; +## invalid(14): export type {Foo} from "foo"; > Input @@ -211,7 +243,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(13): export type * from "foo"; +## invalid(15): export type * from "foo"; > Input @@ -227,7 +259,87 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(14): import type {Foo} from './foo.js'; export type {Foo}; +## invalid(16): export type * as Namespace from "foo"; + +> Input + + `␊ + 1 | export type * as Namespace from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export type * as Namespace from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(17): import foo from "foo"; export default foo as Foo; + +> Input + + `␊ + 1 | import foo from "foo"; export default foo as Foo;␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import foo from "foo"; export default foo as Foo;␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(18): import foo from "foo"; export default foo satisfies Foo; + +> Input + + `␊ + 1 | import foo from "foo"; export default foo satisfies Foo;␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import foo from "foo"; export default foo satisfies Foo;␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(19): import foo from "foo"; export default foo!; + +> Input + + `␊ + 1 | import foo from "foo"; export default foo!;␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import foo from "foo"; export default foo!;␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(20): import foo from "foo"; export default foo; + +> Input + + `␊ + 1 | import foo from "foo"; export default foo;␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import foo from "foo"; export default foo;␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(21): import type {Foo} from './foo.js'; export type {Foo}; > Input diff --git a/test/snapshots/no-barrel-files.js.snap b/test/snapshots/no-barrel-files.js.snap index bdf0e5756aa563988b5a4f1d18176ae6f693ec09..82bce791faf79f077adef1499b2dd3610b725d75 100644 GIT binary patch literal 1076 zcmV-41k3wDRzVv2T2Ax zD=SO4*;x<+7{kE>F;NpuG#-qY=*6pEJo|t6ANVhrn(lPp_Ra3>ROqokzBBWgw>$gb zleOWvo7>{;DfeyP;rl~&V~;s@eWW-#1AM>fdOjRju6qQQ=kCCu<+_8jwrH@X*m8Yw zd|IzFpWVw}$Xv>_UVRrcm-`?KP=rI}@3Y)t8{FZy8_iw6o#3McE4#vlU=R6BwiX`+ z0B{KFiWe*}&*KhQw!;N5k3+-tfjN%*nAh{|dDu&y=Xy{q6a$5^K%s;cfC<=OUINA> zTFP)Y1{Fh@9HSsV#)M3ar|C^-BoHcxgzDU4yN;j0Q$nj6ittbwl+7VZX{c<-xMUbZ z6$n%&Ix4w%jT~CX6|9jtLA7n#f`R#7C08Ucw(X`trHY>sJe#YQZ>Dd#Y%x# zWpoDY9eNgMdIZ`WT4%nErk6&!y$jOe@5h+e~ zD<+3hm>&`5t2j)J0n(WEMmrVIRy(wL;3brbs_0&lTxe?9`|F;FFh8=Tjzv^-y<%HKcqCSFQ0l zl>IMc|3g*F=3^S%GydN`t;u~Vktf`6zF@g-{*f4t93SY$C%?PFyno;A-mq=7VGopt zU}(TBKq=CDwcaa--XjO6cJU9|#orodCZ0&*7G-PXWV?Qst!XSkV?4wC7IME^ol~Iz zceiW-wgBPVjsvW9zy-1S_(w3RG&GChCEAT=nRYcKOeazgUB#fBVc z(TYi)tj1*>sT!hWBK6WseI`4cUESqm-GzEn|4Ea>!E$2PQAYQUtN2KzL#TVeQxwxw z9HNT6v!l5tX(l5y3B7v(t!IE>No?TvP{dXoiEbC)3q@H{Om$H(0jqiqJ>75VrJ3|w zn#t$TZ`Gt zP3Po4AjY*#=ayZ!M*oRn3At8Mxyo3|_A|15V`$qM!l-}^jTfYGs6&oEd=Ssrov#qm ucaZcVmW~O>kJh0fDD|I6{hLZXemFg7{CpgqmntFUZu|usnYF`$8~^}1t_srt literal 842 zcmV-Q1GW4?RzVC7CR}-s|IYm8zcaVr?5>d8 zJLvF*#4u*T)M8QJ}w)L1R#a{BZ)k&8E02srT=7M$R zx?F(m2(Ey+90t+@CWL&=dzDEX&OO(aF4U^EfC2;*>Jf!qb0lreMH!xf-%ULxP6Z6 zojWQd!5JzWv&)ZfHNdxw@%bB|%Un7-{{ca915s>EQ_L2nV17j~Kc&GmSYQ^W^Pyc1 z(5^&i{TPYZdfaAv;`A}W^)BLC$6O5*DX3o%)MFh~6ZKh%&WrPQ)EeV)XhJr{X|9+D z=M3uq3Fo5#=W66m{n!gPQS_9lARVd~Y$C`HuxE3xL!tkL(ErdmIsckDI>x^%(`NYF ze2biKOO>iEW#yGxN(>Lw_QH3&%sqW~x?nq~U@x>IFt(rtP*40p`}Z2Ld*tF&75|_r z{x+DHIFQT|SsgOisqd_gK>`Nr<*c^~>+PSDs#SQ}cN91Zl;;Qm%pY%oY$g2%jIxHI z89t(FyemxHkP%M84^w8))vlStFwcuE6ysts>6q+b4YqZ{HH2jnewoLg*&NQ!cjeG` zv6HFyX2{{-e&(5@fqKU^JW**8st3G9GDFP_)##lan#B-JBY`I4 Date: Sun, 19 Jul 2026 17:19:26 +0200 Subject: [PATCH 5/7] `no-barrel-files`: Handle TypeScript instantiation expressions Fixes #3520 --- rules/no-barrel-files.js | 6 +++++- test/no-barrel-files.js | 4 ++++ test/snapshots/no-barrel-files.js.md | 20 ++++++++++++++++++-- test/snapshots/no-barrel-files.js.snap | Bin 1076 -> 1113 bytes 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/rules/no-barrel-files.js b/rules/no-barrel-files.js index 1e52ed30e4..4de5199e28 100644 --- a/rules/no-barrel-files.js +++ b/rules/no-barrel-files.js @@ -54,7 +54,11 @@ const isBarrelFile = program => { } if (node.type === 'ExportDefaultDeclaration') { - const declaration = unwrapTypeScriptExpression(node.declaration); + let declaration = unwrapTypeScriptExpression(node.declaration); + while (declaration.type === 'TSInstantiationExpression') { + declaration = unwrapTypeScriptExpression(declaration.expression); + } + if (declaration.type === 'Identifier' && importedBindingNames.has(declaration.name)) { hasReExport = true; continue; diff --git a/test/no-barrel-files.js b/test/no-barrel-files.js index 706309bf13..bd9a5848ba 100644 --- a/test/no-barrel-files.js +++ b/test/no-barrel-files.js @@ -70,6 +70,10 @@ test.snapshot({ code: 'import foo from "foo"; export default foo!;', languageOptions: {parser: parsers.typescript}, }, + { + code: 'import foo from "foo"; export default foo;', + languageOptions: {parser: parsers.typescript}, + }, { code: 'import foo from "foo"; export default foo;', languageOptions: {parser: parsers.typescript}, diff --git a/test/snapshots/no-barrel-files.js.md b/test/snapshots/no-barrel-files.js.md index 9f3d029cae..4447ef4216 100644 --- a/test/snapshots/no-barrel-files.js.md +++ b/test/snapshots/no-barrel-files.js.md @@ -323,7 +323,23 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(20): import foo from "foo"; export default foo; +## invalid(20): import foo from "foo"; export default foo; + +> Input + + `␊ + 1 | import foo from "foo"; export default foo;␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | import foo from "foo"; export default foo;␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(21): import foo from "foo"; export default foo; > Input @@ -339,7 +355,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(21): import type {Foo} from './foo.js'; export type {Foo}; +## invalid(22): import type {Foo} from './foo.js'; export type {Foo}; > Input diff --git a/test/snapshots/no-barrel-files.js.snap b/test/snapshots/no-barrel-files.js.snap index 82bce791faf79f077adef1499b2dd3610b725d75..e1f5e558ab36ca43b3f3f11066ddba2001560cb4 100644 GIT binary patch literal 1113 zcmV-f1g85zRzVsFnf1~9ScR~IrtP@o(6H?zVmS6L88mEraMt7v+T`1| z%a2d%b?VZ)*$atFiS{dbF>yIbQiSA5OZxj1v*-r1*sVr$&+R0*Ai+qjFwW@~tE6h- zQG^iEBI}YDnWv7!EMk}zdc^fmK(uS0IM1b@L(C3O$($nWHxAA zG>oPS@l+rH0iw77 z6sLL>qeC&w4*>H;7^cDiaZLN8o%U$UUE1~>k-2paq4vj!>l|<`Kv#uC4D~HQJ&~cR zq{kK2r;O4iOZ<_DdpaJu)Dz;P+o>r zt33{8{|ngvkkzvOm>Tzt|F=(TVxJ1=2{)3>8Md8$z()ed2e|Qx?`}}%-*>ZPChjCMG?y32^V^Yy0gqb4JVOOahi5!^ej!Xp(A!R`T% zK}=I}h)VL#j%HZUOa*8ndiNYyPa0u~Y~c4n#C8~oY8T%RMNv>p_fSwyR@EAMyx&qw z)9JSqlh2{wstLbRoUWif=Owf`xCtoBahn4#7*qWMRNv_L2x1V;2PsYKMPWS?%bHWy zV(q>{S820j7y(Jwci8nXZqS{ z4PZ^v>*5~}6LCzd+;z;xRt=$D2DFv9v?3W3{tATO=?iy9(XC`n@2c?5D|=U$Y`cd4 zNoNsYmttXySWNm8kbb2p-5o-US<`r482dWi;KLr$({#UrNZ$t13s5>F9JVsPAu#oy fK>eFcJ$yJdXxNYV=Os&sxf_20Evmh%@*MyG(YG8% literal 1076 zcmV-41k3wDRzVv2T2Ax zD=SO4*;x<+7{kE>F;NpuG#-qY=*6pEJo|t6ANVhrn(lPp_Ra3>ROqokzBBWgw>$gb zleOWvo7>{;DfeyP;rl~&V~;s@eWW-#1AM>fdOjRju6qQQ=kCCu<+_8jwrH@X*m8Yw zd|IzFpWVw}$Xv>_UVRrcm-`?KP=rI}@3Y)t8{FZy8_iw6o#3McE4#vlU=R6BwiX`+ z0B{KFiWe*}&*KhQw!;N5k3+-tfjN%*nAh{|dDu&y=Xy{q6a$5^K%s;cfC<=OUINA> zTFP)Y1{Fh@9HSsV#)M3ar|C^-BoHcxgzDU4yN;j0Q$nj6ittbwl+7VZX{c<-xMUbZ z6$n%&Ix4w%jT~CX6|9jtLA7n#f`R#7C08Ucw(X`trHY>sJe#YQZ>Dd#Y%x# zWpoDY9eNgMdIZ`WT4%nErk6&!y$jOe@5h+e~ zD<+3hm>&`5t2j)J0n(WEMmrVIRy(wL;3brbs_0&lTxe?9`|F;FFh8=Tjzv^-y<%HKcqCSFQ0l zl>IMc|3g*F=3^S%GydN`t;u~Vktf`6zF@g-{*f4t93SY$C%?PFyno;A-mq=7VGopt zU}(TBKq=CDwcaa--XjO6cJU9|#orodCZ0&*7G-PXWV?Qst!XSkV?4wC7IME^ol~Iz zceiW-wgBPVjsvW9zy-1S_(w3RG&GChCEAT=nRYcKOeazgUB#fBVc z(TYi)tj1*>sT!hWBK6WseI`4cUESqm-GzEn|4Ea>!E$2PQAYQUtN2KzL#TVeQxwxw z9HNT6v!l5tX(l5y3B7v(t!IE>No?TvP{dXoiEbC)3q@H{Om$H(0jqiqJ>75VrJ3|w zn#t$TZ`Gt zP3Po4AjY*#=ayZ!M*oRn3At8Mxyo3|_A|15V`$qM!l-}^jTfYGs6&oEd=Ssrov#qm ucaZcVmW~O>kJh0fDD|I6{hLZXemFg7{CpgqmntFUZu|usnYF`$8~^}1t_srt From 22aba38d7e1b35041f5ab7887ce363bca54099a3 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 19 Jul 2026 22:07:16 +0200 Subject: [PATCH 6/7] `no-barrel-files`: Cover exports before imports Fixes #3520 --- test/no-barrel-files.js | 1 + test/snapshots/no-barrel-files.js.md | 38 ++++++++++++++++++------- test/snapshots/no-barrel-files.js.snap | Bin 1113 -> 1135 bytes 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/test/no-barrel-files.js b/test/no-barrel-files.js index bd9a5848ba..a57b940de8 100644 --- a/test/no-barrel-files.js +++ b/test/no-barrel-files.js @@ -35,6 +35,7 @@ test.snapshot({ 'import foo from "foo"; export default foo;', 'export default foo; import foo from "foo";', 'import * as namespace from "foo"; export {namespace as default};', + 'export {foo}; import {foo} from "foo";', outdent` import {foo} from './foo.js'; export {foo}; diff --git a/test/snapshots/no-barrel-files.js.md b/test/snapshots/no-barrel-files.js.md index 4447ef4216..3f1097a7fc 100644 --- a/test/snapshots/no-barrel-files.js.md +++ b/test/snapshots/no-barrel-files.js.md @@ -180,7 +180,23 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(12): import {foo} from './foo.js'; export {foo}; export {bar} from './bar.js'; +## invalid(12): export {foo}; import {foo} from "foo"; + +> Input + + `␊ + 1 | export {foo}; import {foo} from "foo";␊ + ` + +> Error 1/1 + + `␊ + Message:␊ + > 1 | export {foo}; import {foo} from "foo";␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ + ` + +## invalid(13): import {foo} from './foo.js'; export {foo}; export {bar} from './bar.js'; > Input @@ -202,7 +218,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(13): // This is still a barrel file. export {}; export {foo} from './foo.js'; export {bar} from './bar.js'; +## invalid(14): // This is still a barrel file. export {}; export {foo} from './foo.js'; export {bar} from './bar.js'; > Input @@ -227,7 +243,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(14): export type {Foo} from "foo"; +## invalid(15): export type {Foo} from "foo"; > Input @@ -243,7 +259,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(15): export type * from "foo"; +## invalid(16): export type * from "foo"; > Input @@ -259,7 +275,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(16): export type * as Namespace from "foo"; +## invalid(17): export type * as Namespace from "foo"; > Input @@ -275,7 +291,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(17): import foo from "foo"; export default foo as Foo; +## invalid(18): import foo from "foo"; export default foo as Foo; > Input @@ -291,7 +307,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(18): import foo from "foo"; export default foo satisfies Foo; +## invalid(19): import foo from "foo"; export default foo satisfies Foo; > Input @@ -307,7 +323,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(19): import foo from "foo"; export default foo!; +## invalid(20): import foo from "foo"; export default foo!; > Input @@ -323,7 +339,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(20): import foo from "foo"; export default foo; +## invalid(21): import foo from "foo"; export default foo; > Input @@ -339,7 +355,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(21): import foo from "foo"; export default foo; +## invalid(22): import foo from "foo"; export default foo; > Input @@ -355,7 +371,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Barrel files are not allowed.␊ ` -## invalid(22): import type {Foo} from './foo.js'; export type {Foo}; +## invalid(23): import type {Foo} from './foo.js'; export type {Foo}; > Input diff --git a/test/snapshots/no-barrel-files.js.snap b/test/snapshots/no-barrel-files.js.snap index e1f5e558ab36ca43b3f3f11066ddba2001560cb4..0b8ce7e821c3ded14aefc9e772289fd121344074 100644 GIT binary patch literal 1135 zcmV-#1d#hdRzVcvElk+ z|FF?uKD$*skvo-Z|Mi{Bo$iA?KpA$Gzt3}rt#OB6Z?^9Hodh2xSotL`1Y74b`Fe5` z0KhJ+D*oUK^E~c=WjkB|^EfnJADH8~4|${5>4!bs5t9L38wmqdp&udLlY%CZNf-xM;DvJei<|1~D2Z zPz6yQPf^si(nFc4^~@Mcl>({B$Wya7n_;1yMgts+Jv<3fl-2d>)I9=i4vn+eLDNg4++IfW`uB5jIL%~jJ9ED2fNu)(QTv7LUHbz<@hVcB z=vGV*r7%Ar%oj)wRF(4-yKC&tD)$lH9ZoOJWQWsCK877`Mfz3A6T(QbWVvqf zo*0R3zi8u--(6$gzwdSvZzAG5%0on?!8|}Ej+F zLrQmI_0Uxes=2#hP%yXq&})eP;l6mqq<2>1;f+-dQ8KZ5>7_o?9geQ<3bO7Zw_N{G zi^I-B>hfGgYsXoc`?IBdnVk`2A*q<2Nzdv3vX>+~zJd4yfdge_w!=}$=dm7#Rlgb}l$@fB$tx#`9a zd&He4{0bp`14&=S(h1@CDidjfQvZq6zp2!do702F_mOD7R0$bp<1g9gw5Bp2002Q6 BEXn`? literal 1113 zcmV-f1g85zRzVsFnf1~9ScR~IrtP@o(6H?zVmS6L88mEraMt7v+T`1| z%a2d%b?VZ)*$atFiS{dbF>yIbQiSA5OZxj1v*-r1*sVr$&+R0*Ai+qjFwW@~tE6h- zQG^iEBI}YDnWv7!EMk}zdc^fmK(uS0IM1b@L(C3O$($nWHxAA zG>oPS@l+rH0iw77 z6sLL>qeC&w4*>H;7^cDiaZLN8o%U$UUE1~>k-2paq4vj!>l|<`Kv#uC4D~HQJ&~cR zq{kK2r;O4iOZ<_DdpaJu)Dz;P+o>r zt33{8{|ngvkkzvOm>Tzt|F=(TVxJ1=2{)3>8Md8$z()ed2e|Qx?`}}%-*>ZPChjCMG?y32^V^Yy0gqb4JVOOahi5!^ej!Xp(A!R`T% zK}=I}h)VL#j%HZUOa*8ndiNYyPa0u~Y~c4n#C8~oY8T%RMNv>p_fSwyR@EAMyx&qw z)9JSqlh2{wstLbRoUWif=Owf`xCtoBahn4#7*qWMRNv_L2x1V;2PsYKMPWS?%bHWy zV(q>{S820j7y(Jwci8nXZqS{ z4PZ^v>*5~}6LCzd+;z;xRt=$D2DFv9v?3W3{tATO=?iy9(XC`n@2c?5D|=U$Y`cd4 zNoNsYmttXySWNm8kbb2p-5o-US<`r482dWi;KLr$({#UrNZ$t13s5>F9JVsPAu#oy fK>eFcJ$yJdXxNYV=Os&sxf_20Evmh%@*MyG(YG8% From f2c6b8fd20c821f13f91b136644a06f2bd927c28 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Mon, 20 Jul 2026 00:39:09 +0200 Subject: [PATCH 7/7] `no-barrel-files`: Document how to fix violations Fixes #3520 --- docs/rules/no-barrel-files.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/rules/no-barrel-files.md b/docs/rules/no-barrel-files.md index aa04300ddd..d5d3db0259 100644 --- a/docs/rules/no-barrel-files.md +++ b/docs/rules/no-barrel-files.md @@ -15,6 +15,8 @@ At least one binding must be re-exported; files containing only empty exports ar Side-effect-only imports, including `import 'foo'` and `import {} from 'foo'`, are not part of a barrel file, so a file that combines one with re-exports is ignored. +Fix violations by removing the barrel file and importing directly from its source modules, for example, replace `import {foo} from './index.js'` with `import {foo} from './foo.js'`. Disable this rule for intentional package entry points. + ## Examples Examples of **incorrect** code for this rule: