Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/rules/no-barrel-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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`.

<!-- end auto-generated rule header -->
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->

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.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to tell how to fix this issue: don't create files that exist solely to import and re-exports; import directly from the source.


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:

```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;
```
2 changes: 2 additions & 0 deletions eslint.dogfooding.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
'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.

Check warning on line 58 in eslint.dogfooding.config.js

View workflow job for this annotation

GitHub Actions / update-snapshots

Unexpected 'todo' comment: 'TODO: Enable when targeting Node.js 26.'

Check warning on line 58 in eslint.dogfooding.config.js

View workflow job for this annotation

GitHub Actions / lint

Unexpected 'todo' comment: 'TODO: Enable when targeting Node.js 26.'
'unicorn/prefer-iterator-concat': 'off',
'unicorn/prefer-temporal': 'off',
},
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. | ✅ | | | |
Expand Down
1 change: 1 addition & 0 deletions rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
105 changes: 105 additions & 0 deletions rules/no-barrel-files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/** @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 getImportedBindingNames = program => {
const importedBindingNames = new Set();

for (const node of program.body) {
if (node.type !== 'ImportDeclaration') {
continue;
}

for (const specifier of node.specifiers) {
importedBindingNames.add(specifier.local.name);
}
}

return importedBindingNames;
};

const isBarrelFile = program => {
const importedBindingNames = getImportedBindingNames(program);
let hasReExport = false;

for (const node of program.body) {
if (node.type === 'ImportDeclaration') {
if (node.specifiers.length === 0) {
return false;
}

continue;
}

if (node.type === 'ExportAllDeclaration') {
hasReExport = true;
continue;
}

if (node.type === 'ExportNamedDeclaration') {
if (
node.declaration
|| (!node.source && node.specifiers.some(specifier => !importedBindingNames.has(specifier.local.name)))
) {
return false;
}

hasReExport ||= node.specifiers.length > 0;
continue;
}

if (node.type === 'ExportDefaultDeclaration') {
let declaration = unwrapTypeScriptExpression(node.declaration);
while (declaration.type === 'TSInstantiationExpression') {
declaration = unwrapTypeScriptExpression(declaration.expression);
}

if (declaration.type === 'Identifier' && importedBindingNames.has(declaration.name)) {
hasReExport = true;
continue;
}
}

return false;
}

return hasReExport;
};

/** @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;
90 changes: 90 additions & 0 deletions test/no-barrel-files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 {} from "foo"; export {bar} from "bar";',
'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 {foo as bar} from "foo";',
'export {default} from "foo";',
'export * from "foo";',
'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};',
'export {foo}; import {foo} from "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: '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<string>;',
languageOptions: {parser: parsers.typescript},
},
{
code: 'import foo from "foo"; export default <Foo>foo;',
languageOptions: {parser: parsers.typescript},
},
{
code: outdent`
import type {Foo} from './foo.js';
export type {Foo};
`,
languageOptions: {parser: parsers.typescript},
},
],
});
Loading
Loading