Skip to content

Commit d900c09

Browse files
authored
Add no-barrel-files rule (#3570)
1 parent 0096a18 commit d900c09

8 files changed

Lines changed: 642 additions & 0 deletions

File tree

docs/rules/no-barrel-files.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# no-barrel-files
2+
3+
📝 Disallow barrel files.
4+
5+
🚫 This rule is _disabled_ in the following [configs](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config): ✅ `recommended`, ☑️ `unopinionated`.
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
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.
11+
12+
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.
13+
14+
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.
15+
16+
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.
17+
18+
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.
19+
20+
## Examples
21+
22+
Examples of **incorrect** code for this rule:
23+
24+
```js
25+
export {foo} from './foo.js';
26+
```
27+
28+
```js
29+
import {foo} from './foo.js';
30+
export {foo};
31+
```
32+
33+
```js
34+
export * from './foo.js';
35+
export * from './bar.js';
36+
```
37+
38+
Examples of **correct** code for this rule:
39+
40+
```js
41+
export const foo = 1;
42+
```
43+
44+
```js
45+
import {foo} from './foo.js';
46+
export const bar = foo;
47+
```
48+
49+
```js
50+
export {foo} from './foo.js';
51+
export const bar = 1;
52+
```

eslint.dogfooding.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ const config = [
5353
'unicorn/prefer-simplified-conditions': 'off',
5454
// Many existing internal utilities intentionally export declarations separately.
5555
'unicorn/default-export-style': 'off',
56+
// The plugin intentionally keeps a few internal barrel files for shared utilities and rule exports.
57+
'unicorn/no-barrel-files': 'off',
5658
// TODO: Enable when targeting Node.js 26.
5759
'unicorn/prefer-iterator-concat': 'off',
5860
'unicorn/prefer-temporal': 'off',

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export default defineConfig([
147147
| [no-async-promise-finally](docs/rules/no-async-promise-finally.md) | Disallow async functions as `Promise#finally()` callbacks. | ✅ ☑️ | | | |
148148
| [no-await-expression-member](docs/rules/no-await-expression-member.md) | Disallow member access from await expression. || 🔧 | | |
149149
| [no-await-in-promise-methods](docs/rules/no-await-in-promise-methods.md) | Disallow using `await` in `Promise` method parameters. | ✅ ☑️ | | 💡 | |
150+
| [no-barrel-files](docs/rules/no-barrel-files.md) | Disallow barrel files. | | | | |
150151
| [no-blob-to-file](docs/rules/no-blob-to-file.md) | Disallow unnecessary `Blob` to `File` conversion. | ✅ ☑️ | | 💡 | |
151152
| [no-boolean-sort-comparator](docs/rules/no-boolean-sort-comparator.md) | Disallow boolean-returning sort comparators. | ✅ ☑️ | | 💡 | |
152153
| [no-break-in-nested-loop](docs/rules/no-break-in-nested-loop.md) | Disallow `break` and `continue` in nested loops and switches inside loops. || | | |

rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export {default as 'no-asterisk-prefix-in-documentation-comments'} from './no-as
5656
export {default as 'no-async-promise-finally'} from './no-async-promise-finally.js';
5757
export {default as 'no-await-expression-member'} from './no-await-expression-member.js';
5858
export {default as 'no-await-in-promise-methods'} from './no-await-in-promise-methods.js';
59+
export {default as 'no-barrel-files'} from './no-barrel-files.js';
5960
export {default as 'no-blob-to-file'} from './no-blob-to-file.js';
6061
export {default as 'no-boolean-sort-comparator'} from './no-boolean-sort-comparator.js';
6162
export {default as 'no-break-in-nested-loop'} from './no-break-in-nested-loop.js';

rules/no-barrel-files.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/** @import * as ESLint from 'eslint'; */
2+
3+
import {unwrapTypeScriptExpression} from './utils/index.js';
4+
5+
const MESSAGE_ID = 'no-barrel-files';
6+
const messages = {
7+
[MESSAGE_ID]: 'Barrel files are not allowed.',
8+
};
9+
10+
const getImportedBindingNames = program => {
11+
const importedBindingNames = new Set();
12+
13+
for (const node of program.body) {
14+
if (node.type !== 'ImportDeclaration') {
15+
continue;
16+
}
17+
18+
for (const specifier of node.specifiers) {
19+
importedBindingNames.add(specifier.local.name);
20+
}
21+
}
22+
23+
return importedBindingNames;
24+
};
25+
26+
const isBarrelFile = program => {
27+
const importedBindingNames = getImportedBindingNames(program);
28+
let hasReExport = false;
29+
30+
for (const node of program.body) {
31+
if (node.type === 'ImportDeclaration') {
32+
if (node.specifiers.length === 0) {
33+
return false;
34+
}
35+
36+
continue;
37+
}
38+
39+
if (node.type === 'ExportAllDeclaration') {
40+
hasReExport = true;
41+
continue;
42+
}
43+
44+
if (node.type === 'ExportNamedDeclaration') {
45+
if (
46+
node.declaration
47+
|| (!node.source && node.specifiers.some(specifier => !importedBindingNames.has(specifier.local.name)))
48+
) {
49+
return false;
50+
}
51+
52+
hasReExport ||= node.specifiers.length > 0;
53+
continue;
54+
}
55+
56+
if (node.type === 'ExportDefaultDeclaration') {
57+
let declaration = unwrapTypeScriptExpression(node.declaration);
58+
while (declaration.type === 'TSInstantiationExpression') {
59+
declaration = unwrapTypeScriptExpression(declaration.expression);
60+
}
61+
62+
if (declaration.type === 'Identifier' && importedBindingNames.has(declaration.name)) {
63+
hasReExport = true;
64+
continue;
65+
}
66+
}
67+
68+
return false;
69+
}
70+
71+
return hasReExport;
72+
};
73+
74+
/** @param {ESLint.Rule.RuleContext} context */
75+
const create = context => {
76+
context.on('Program', node => {
77+
if (!isBarrelFile(node)) {
78+
return;
79+
}
80+
81+
return {
82+
node,
83+
messageId: MESSAGE_ID,
84+
};
85+
});
86+
};
87+
88+
/** @type {ESLint.Rule.RuleModule} */
89+
const config = {
90+
create,
91+
meta: {
92+
type: 'suggestion',
93+
docs: {
94+
description: 'Disallow barrel files.',
95+
recommended: false,
96+
},
97+
schema: [],
98+
messages,
99+
languages: [
100+
'js/js',
101+
],
102+
},
103+
};
104+
105+
export default config;

test/no-barrel-files.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import outdent from 'outdent';
2+
import {getTester, parsers} from './utils/test.js';
3+
4+
const {test} = getTester(import.meta);
5+
6+
test.snapshot({
7+
valid: [
8+
'import "foo";',
9+
'import {} from "foo";',
10+
'import {} from "foo"; export {bar} from "bar";',
11+
'import {foo} from "foo"; export const bar = foo;',
12+
'export {};',
13+
'export {} from "foo";',
14+
'const foo = 1; export {foo};',
15+
'export {foo} from "foo"; const bar = 1;',
16+
'export const foo = 1;',
17+
'export default foo;',
18+
'export default function foo() {}',
19+
'import "foo"; export {bar} from "bar";',
20+
outdent`
21+
import {foo} from './foo.js';
22+
const bar = foo;
23+
export {bar};
24+
`,
25+
],
26+
invalid: [
27+
'export {foo} from "foo";',
28+
'export {foo as bar} from "foo";',
29+
'export {default} from "foo";',
30+
'export * from "foo";',
31+
'export * as namespace from "foo";',
32+
'export {}; export {foo} from "foo";',
33+
'import {foo} from "foo"; export {foo};',
34+
'import {foo as bar} from "foo"; export {bar};',
35+
'import foo from "foo"; export default foo;',
36+
'export default foo; import foo from "foo";',
37+
'import * as namespace from "foo"; export {namespace as default};',
38+
'export {foo}; import {foo} from "foo";',
39+
outdent`
40+
import {foo} from './foo.js';
41+
export {foo};
42+
export {bar} from './bar.js';
43+
`,
44+
outdent`
45+
// This is still a barrel file.
46+
export {};
47+
export {foo} from './foo.js';
48+
export {bar} from './bar.js';
49+
`,
50+
{
51+
code: 'export type {Foo} from "foo";',
52+
languageOptions: {parser: parsers.typescript},
53+
},
54+
{
55+
code: 'export type * from "foo";',
56+
languageOptions: {parser: parsers.typescript},
57+
},
58+
{
59+
code: 'export type * as Namespace from "foo";',
60+
languageOptions: {parser: parsers.typescript},
61+
},
62+
{
63+
code: 'import foo from "foo"; export default foo as Foo;',
64+
languageOptions: {parser: parsers.typescript},
65+
},
66+
{
67+
code: 'import foo from "foo"; export default foo satisfies Foo;',
68+
languageOptions: {parser: parsers.typescript},
69+
},
70+
{
71+
code: 'import foo from "foo"; export default foo!;',
72+
languageOptions: {parser: parsers.typescript},
73+
},
74+
{
75+
code: 'import foo from "foo"; export default foo<string>;',
76+
languageOptions: {parser: parsers.typescript},
77+
},
78+
{
79+
code: 'import foo from "foo"; export default <Foo>foo;',
80+
languageOptions: {parser: parsers.typescript},
81+
},
82+
{
83+
code: outdent`
84+
import type {Foo} from './foo.js';
85+
export type {Foo};
86+
`,
87+
languageOptions: {parser: parsers.typescript},
88+
},
89+
],
90+
});

0 commit comments

Comments
 (0)