Repository: thepassle/eslint-plugin-barrel-files Branch: main Commit: 6ca8d7bf55f6 Files: 18 Total size: 27.2 KB Directory structure: gitextract_57vo26pf/ ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs/ │ └── rules/ │ ├── avoid-barrel-files.md │ ├── avoid-importing-barrel-files.md │ ├── avoid-namespace-import.md │ └── avoid-re-export-all.md ├── index.js ├── lib/ │ └── rules/ │ ├── avoid-barrel-files.js │ ├── avoid-importing-barrel-files.js │ ├── avoid-namespace-import.js │ └── avoid-re-export-all.js ├── package.json └── tests/ └── lib/ └── rules/ ├── avoid-barrel-files-ts.js ├── avoid-barrel-files.js ├── avoid-namespace-import.js └── avoid-re-export-all.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ## editors /.idea /.vscode *.iml ## system files .DS_Store ## dependencies /node_modules* /npm-debug.log ## testing /coverage/ ## temp folders /.tmp/ # build /_site/ /dist/ # generic logs *.log ================================================ FILE: CHANGELOG.md ================================================ # Change Log This project follows _Semantic Versioning_ (aka SemVer). Visit http://semver.org/ to learn more about it. ## Release 3.0.0 (2025-02-13) - Actually export the plugin ## Release 3.0.0 (2025-02-13) - Migrate to ESM and flatconfig format ## Release 1.0.0 (2024-01-05) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 modern-webdev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # eslint-plugin-barrel-files Barrel files are files that just re-export a bunch of things from other files. If you're using a bundler, bundlers usually apply treeshaking and dead code elimination algorithms to remove any unused code. In many environments however, like test runners, browsers, CDN environments or server side JavaScript runtimes, treeshaking does not get applied. This means that lots of modules get loaded unnecessarily, which can cause significant performance slowdowns. For more information, I recommend reading [Speeding up the JavaScript ecosystem - The barrel file debacle](https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/) ## Supported Rules - [barrel-files/avoid-barrel-files](./docs/rules/avoid-barrel-files.md) - [barrel-files/avoid-importing-barrel-files](./docs/rules/avoid-importing-barrel-files.md) - [barrel-files/avoid-namespace-import](./docs/rules/avoid-namespace-import.md) - [barrel-files/avoid-re-export-all](./docs/rules/avoid-re-export-all.md) ================================================ FILE: docs/rules/avoid-barrel-files.md ================================================ # Avoid barrel files ## Rule Details This rule disallows _authoring_ barrel files in your project. Examples of **incorrect** code for this rule: ```js export { foo } from 'foo'; export { bar } from 'bar'; export { baz } from 'baz'; ``` ## Configuration This rule takes an optional configuration: ```json { "rules": { "barrel-files/avoid-barrel-files": [ 2, { "amountOfExportsToConsiderModuleAsBarrel": 5 } ] } } ``` ================================================ FILE: docs/rules/avoid-importing-barrel-files.md ================================================ # Avoid importing barrel files ## Rule Details This rule aims to avoid importing barrel files that lead to loading large module graphs. This rule is different from the `avoid-barrel-files` rule, which lints against _authoring_ barrel files. This rule lints against _importing_ barrel files. Examples of **incorrect** code for this rule: ```js // If `foo` is a barrel file leading to a module graph of more than 20 modules export { foo } from 'foo'; ``` ## Configuration This rule takes an optional configuration: ```json { "rules": { "barrel-files/avoid-importing-barrel-files": [ 2, { "allowList": ["foo"], "maxModuleGraphSizeAllowed": 40, "amountOfExportsToConsiderModuleAsBarrel": 5, "exportConditions": ["node", "import"], "mainFields": ["module", "main", "browser"], "extensions": [".js", ".ts", ".json", ".node"], "tsconfig": { "configFile": "./tsconfig.json", "references": [] } } ] } } ``` ### Path Aliases The rule can accept an `alias` option whose value can be an object that matches Webpack's [resolve.alias](https://webpack.js.org/configuration/resolve/) configuration. ```js // .eslintrc.cjs const path = require('path') export default { // ... "rules": { "barrel-files/avoid-importing-barrel-files": [ 2, { alias: { // "@/foo/bar.js" => "./src/foo/bar.js" "@": [path.resolve(".", "src")] } } ] } } ``` ================================================ FILE: docs/rules/avoid-namespace-import.md ================================================ # Avoid namespace import ## Rule Details This rule aims to... Examples of **incorrect** code for this rule: ```js import * as foo from 'foo'; ``` ## Configuration This rule takes an optional configuration: ```json { "rules": { "barrel-files/avoid-namespace-import": [ 2, { "allowList": ["foo"] } ] } } ``` ================================================ FILE: docs/rules/avoid-re-export-all.md ================================================ # Avoid re-export all ## Rule Details This rule aims to... Examples of **incorrect** code for this rule: ```js export * from 'foo'; export * as foo from 'foo'; ``` ================================================ FILE: index.js ================================================ import { join, parse } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { dirname } from "node:path"; import { readFile, readdir } from "node:fs/promises"; const __dirname = dirname(fileURLToPath(import.meta.url)); const pkgJson = JSON.parse(await readFile(join(__dirname, "./package.json"))); const rules = {}; const filenames = await readdir(`${__dirname}/lib/rules`); for (const filename of filenames) { const rule = ( await import(pathToFileURL(`${__dirname}/lib/rules/${filename}`)) ).default; const ruleName = parse(filename).name; rules[ruleName] = rule; } const plugin = { meta: { name: pkgJson.name, version: pkgJson.version, }, processors: {}, rules, configs: {}, }; // assign configs here so we can reference `plugin` Object.assign(plugin.configs, { recommended: { plugins: { 'barrel-files': plugin, }, rules: { 'barrel-files/avoid-importing-barrel-files': 'error', 'barrel-files/avoid-barrel-files': 'error', 'barrel-files/avoid-namespace-import': 'error', 'barrel-files/avoid-re-export-all': 'error', }, }, }); export default plugin; ================================================ FILE: lib/rules/avoid-barrel-files.js ================================================ /** * @fileoverview avoid-barrel-files * @author Pascal Schilp */ //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ export default { meta: { type: 'suggestion', docs: { description: "avoid-barrel-files", category: 'Performance', recommended: true, url: '../../docs/rules/avoid-barrel-files.md', }, schema: [ { amountOfExportsToConsiderModuleAsBarrel: { type: 'number', description: 'Minimum amount of exports to consider module as barrelfile', default: 3, }, }, ], }, create: context => ({ //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- Program(node) { const options = (context.options && context.options[0]) || {}; const amountOfExportsToConsiderModuleAsBarrel = options?.amountOfExportsToConsiderModuleAsBarrel ?? 3; let declarations = 0; let exports = 0; node.body.forEach((n) => { if (n.type === 'VariableDeclaration') { declarations += n.declarations.length; } if ( n.type === 'FunctionDeclaration' || n.type === 'ClassDeclaration' || n.type === 'TSTypeAliasDeclaration' || n.type === 'TSInterfaceDeclaration' ) { declarations += 1; } if (n.type === 'ExportNamedDeclaration') { exports += n.specifiers.length; } if (n.type === 'ExportAllDeclaration' && n?.exportKind !== 'type') { exports += 1; } if (n.type === 'ExportDefaultDeclaration') { if ( n.declaration.type === 'FunctionDeclaration' || n.declaration.type === 'CallExpression' ) { declarations += 1; } else if (n.declaration.type === 'ObjectExpression') { exports += n.declaration.properties.length; } else { exports += 1; } } }); if ( exports > declarations && exports > amountOfExportsToConsiderModuleAsBarrel ) { context.report({ node, message: "Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.", }); } }, }), }; ================================================ FILE: lib/rules/avoid-importing-barrel-files.js ================================================ /** * @fileoverview avoid-barrel-files * @author Pascal Schilp */ //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ import { readFileSync } from 'fs'; import path from 'path'; import { builtinModules } from 'module'; import { count_module_graph_size, is_barrel_file, } from 'eslint-barrel-file-utils/index.cjs'; import { ResolverFactory } from 'oxc-resolver'; /** * @fileoverview Avoid importing barrel files * @author Pascal Schilp */ //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ const cache = {}; // custom error class to emulate oxc_resolver ResolveError enum. // `errorVariant` can be equal to a `ResolveError` enum variant. class ResolveError extends Error { constructor(errorVariant = null, message = "") { super(message); this.errorVariant = errorVariant; this.message = message; } } export default { meta: { type: 'problem', fixable: null, docs: { description: 'Avoid importing barrel files', recommended: true, url: '../../docs/rules/avoid-importing-barrel-files.md', }, schema: [ { allowList: { type: 'array', description: 'List of modules from which to allow barrel files', default: [], uniqueItems: true, items: { type: 'string', }, }, }, { maxModuleGraphSizeAllowed: { type: 'number', description: 'Maximum allowed module graph size', default: 20, }, }, { amountOfExportsToConsiderModuleAsBarrel: { type: 'number', description: 'Amount of exports to consider a module as barrel file', default: 3, }, }, { debug: { type: 'boolean', description: 'Enabling debug loggin', default: false, }, }, { exportConditions: { type: 'array', description: 'Export conditions to use to resolve bare module specifiers', default: [], uniqueItems: true, items: { type: 'string', }, }, }, { mainFields: { type: 'array', description: 'Main fields to use to resolve modules', default: [], uniqueItems: true, items: { type: 'string', }, }, }, { extensions: { type: 'array', description: 'Extensions to use to resolve modules', default: [], uniqueItems: true, items: { type: 'string', }, }, }, // schema to match oxc-resolver's TsconfigOptions { tsconfig: { type: 'object', description: 'Options to TsconfigOptions', properties: { configFile: { type: 'string', description: 'Relative path to the configuration file' }, references: { type: 'array', description: 'Typescript Project References', items: { type: 'string' } } } } }, // NapiResolveOptions.alias { alias: { type: 'object', description: 'Webpack aliases used in imports or requires' } } ], }, create(context) { const options = (context.options && context.options[0]) || {}; const maxModuleGraphSizeAllowed = options.maxModuleGraphSizeAllowed ?? 20; const debug = options.debug ?? false; const amountOfExportsToConsiderModuleAsBarrel = options.amountOfExportsToConsiderModuleAsBarrel ?? 3; const exportConditions = options.exportConditions ?? ["node", "import"]; const mainFields = options.mainFields ?? ["module", "browser", "main"]; const extensions = options.extensions ?? [".js", ".ts", ".tsx", ".jsx", ".json", ".node"]; const tsconfig = options.tsconfig; const alias = options.alias; const resolutionOptions = { exportConditions, mainFields, extensions, tsconfig, alias }; const resolver = new ResolverFactory({ tsconfig, alias, conditionNames: exportConditions, mainFields, extensions }); /** * @param {string} specifier * @returns {boolean} */ const isBareModuleSpecifier = specifier => !!specifier?.replace(/'/g, '')[0].match(/[@a-zA-Z]/g); /** * @param {number} amount * @returns {string} */ const message = (amount, specifier) => `The imported module "${specifier}" is a barrel file, which leads to importing a module graph of ${amount} modules, which exceeds the maximum allowed size of ${maxModuleGraphSizeAllowed} modules` return { ImportDeclaration(node) { const moduleSpecifier = node.source.value; const currentFileName = context.getFilename(); if (options?.allowList?.includes(moduleSpecifier)) { return; } if (node?.importKind === 'type') { return; } if (builtinModules.includes(moduleSpecifier.replace("node:", ""))) { return; } let resolvedPath; try { resolvedPath = resolver.sync(path.dirname(currentFileName), moduleSpecifier); if (resolvedPath.error) { // assuming ResolveError::NotFound if ResolveResult's path value is None if (!resolvedPath.path) { throw new ResolveError("NotFound", resolvedPath.error); } throw new ResolveError(null, resolvedPath.error); } } catch (e) { if (!debug) { return } if (e instanceof ResolveError) { switch (e.errorVariant) { case "NotFound": console.error(`Failed to resolve "${moduleSpecifier}" from "${currentFileName}": \n\n${e.stack}`); break default: console.error(`${e.message}: \n\n${e.stack}`); } } console.error(`${e}: \n\n${e.stack}`); return } const fileContent = readFileSync(resolvedPath.path, 'utf8'); let isBarrelFile; /** * Only cache bare module specifiers, as local files can change */ if (isBareModuleSpecifier(moduleSpecifier)) { /** * The module specifier is not cached yet, so we need to analyze and cache it */ if (!cache[moduleSpecifier]) { isBarrelFile = is_barrel_file(fileContent, amountOfExportsToConsiderModuleAsBarrel); const moduleGraphSize = isBarrelFile ? count_module_graph_size(resolvedPath.path, resolutionOptions) : -1; cache[moduleSpecifier] = { isBarrelFile, moduleGraphSize, }; if (moduleGraphSize > maxModuleGraphSizeAllowed) { context.report({ node: node.source, message: message(moduleGraphSize, moduleSpecifier) }); } } else { /** * It is a bare module specifier, but cached, so we can use the cached value */ if (cache[moduleSpecifier].moduleGraphSize > maxModuleGraphSizeAllowed) { context.report({ node: node.source, message: message(cache[moduleSpecifier].moduleGraphSize, moduleSpecifier) }); } } } else { /** * Its not a bare module specifier, but local module, so we need to analyze it */ const isBarrelFile = is_barrel_file(fileContent, amountOfExportsToConsiderModuleAsBarrel); const moduleGraphSize = isBarrelFile ? count_module_graph_size(resolvedPath.path, resolutionOptions) : -1; if (moduleGraphSize > maxModuleGraphSizeAllowed) { context.report({ node: node.source, message: message(moduleGraphSize, moduleSpecifier) }); } } }, }; }, }; ================================================ FILE: lib/rules/avoid-namespace-import.js ================================================ /** * @fileoverview avoid-namespace-import * @author Pascal Schilp */ //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ export default { meta: { type: 'suggestion', docs: { description: "avoid-namespace-import", category: 'Performance', recommended: true, url: '../../docs/rules/avoid-namespace-import.md', }, schema: [ { allowList: { type: 'array', description: 'List of namespace imports to allow', default: [], uniqueItems: true, items: { type: 'string', }, }, }, ], }, create: context => { const options = (context.options && context.options[0]) || {}; const allowList = options.allowList ?? []; return{ //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- ImportNamespaceSpecifier(node) { if (node.parent.importKind !== 'type' && !allowList.includes(node.parent.source.value)) { context.report({ node, message: "Avoid namespace imports, it leads to unused imports and prevents treeshaking.", }); } }, } }, }; ================================================ FILE: lib/rules/avoid-re-export-all.js ================================================ /** * @fileoverview avoid-re-export-all * @author Pascal Schilp */ //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ export default { meta: { type: 'suggestion', docs: { description: "avoid-re-export-all", category: 'Performance', recommended: true, url: '../../docs/rules/avoid-re-export-all.md', }, }, create: context => ({ //---------------------------------------------------------------------- // Public //---------------------------------------------------------------------- ExportAllDeclaration(node) { if (node?.exportKind !== 'type') { context.report({ node, message: "Avoid re-exporting * from a module, it leads to unused imports and prevents treeshaking.", }); } }, }), }; ================================================ FILE: package.json ================================================ { "name": "eslint-plugin-barrel-files", "version": "3.0.1", "description": "eslint plugin to avoid common errors with barrel files", "main": "index.js", "directories": { "doc": "docs", "lib": "lib", "test": "tests" }, "type": "module", "scripts": { "test": "npm run test:node", "test:node": "mocha tests --recursive", "test:single": "mocha tests/lib/rules/avoid-barrel-files-ts.js --watch" }, "keywords": [ "eslint", "eslintplugin", "eslint-plugin", "barrel", "barrelfiles" ], "peerDependencies": { "eslint": ">= 5" }, "repository": { "type": "git", "url": "https://github.com/thepassle/eslint-plugin-barrel-files" }, "author": "", "license": "MIT", "devDependencies": { "@typescript-eslint/parser": "^6.20.0", "@typescript-eslint/rule-tester": "^7.6.0", "mocha": "^10.2.0" }, "bundleDependencies": [], "dependencies": { "eslint-barrel-file-utils": "^0.0.11", "oxc-resolver": "^1.9.3" } } ================================================ FILE: tests/lib/rules/avoid-barrel-files-ts.js ================================================ /** * @fileoverview avoid-barrel-files-ts * @author Pascal Schilp */ //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ import { RuleTester } from '@typescript-eslint/rule-tester'; import { after } from 'node:test'; import rule from '../../../lib/rules/avoid-barrel-files.js'; RuleTester.afterAll = after; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ parserOptions: { sourceType: "module", ecmaVersion: "latest", }, }); ruleTester.run('avoid-barrel-files ts', rule, { valid: [ { code: ` type Money = string; export type { Money }; `, }, { code: ` type Money = { amount: string; currency: string; }; export type { Money }; `, }, { code: ` interface Money { amount: string; currency: string; }; type Country = string; type State = { name: string; }; const newSouthWales = { name: "New South Wales" }; export { newSouthWales } export type { Money, Country, State }; `, } ], invalid: [ { code: ` import { Country } from 'geo'; type Money = string; type State = { name: string; }; interface Person { name: string; age: number; } export type { Money, Country, Person, State }; `, errors: [{ message: 'Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.' }], } ], }); ================================================ FILE: tests/lib/rules/avoid-barrel-files.js ================================================ /** * @fileoverview avoid-barrel-files * @author Pascal Schilp */ //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ import { RuleTester } from 'eslint'; import rule from '../../../lib/rules/avoid-barrel-files.js'; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ parserOptions: { sourceType: "module", ecmaVersion: "latest", }, }); ruleTester.run('avoid-barrel-files', rule, { valid: [ { code: ` let foo; export { foo }; `, }, { code: ` let foo, bar; export { foo, bar }; `, }, { code: ` let foo, bar, baz; export { foo, bar, baz }; `, }, { code: ` let foo, bar, baz, qux; export { foo, bar, baz, qux }; `, }, { code: ` let foo, bar, baz, qux, quux; export { foo, bar, baz, qux }; `, }, { code: ` export default function Foo() { return 'bar'; } `, options: [ { amountOfExportsToConsiderModuleAsBarrel: 0, }, ], }, { code: ` export default function bar() {} `, options: [ { amountOfExportsToConsiderModuleAsBarrel: 0, }, ], }, { code: ` export default defineFoo({}); `, options: [ { amountOfExportsToConsiderModuleAsBarrel: 0, }, ], } ], invalid: [ { code: ` import { bar, baz, qux} from 'foo'; let foo; export { foo, bar, baz, qux, }; `, errors: [{ message: 'Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.' }], }, { code: ` export * from 'foo'; export * from 'bar'; export * from 'baz'; export * from 'qux'; `, errors: [{ message: 'Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.' }], }, { code: `export { foo, bar, baz } from 'foo';`, errors: [{ message: 'Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.' }], options: [ { amountOfExportsToConsiderModuleAsBarrel: 2, }, ], }, { code: 'export default { var1, var2, var3, var4 };', errors: [{ message: 'Avoid barrel files, they slow down performance, and cause large module graphs with modules that go unused.' }], }, ], }); ================================================ FILE: tests/lib/rules/avoid-namespace-import.js ================================================ /** * @fileoverview avoid-namespace-import * @author Pascal Schilp */ //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ import { RuleTester } from 'eslint'; import rule from '../../../lib/rules/avoid-namespace-import.js'; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ // parser: require.resolve('@typescript-eslint/parser'), parserOptions: { sourceType: "module", ecmaVersion: "latest", }, }); ruleTester.run('avoid-namespace-import', rule, { valid: [ 'import { foo } from "foo";', // 'import type { foo } from "foo";', // 'import type * as foo from "foo";' { code: 'import * as foo from "foo";', options: [ { allowList: ['foo'], }, ], }, ], invalid: [ { code: 'import * as foo from "foo";', errors: [{ message: 'Avoid namespace imports, it leads to unused imports and prevents treeshaking.' }], }, { code: 'import * as bar from "bar";', errors: [{ message: 'Avoid namespace imports, it leads to unused imports and prevents treeshaking.' }], options: [ { allowList: ['foo'], }, ], }, ], }); ================================================ FILE: tests/lib/rules/avoid-re-export-all.js ================================================ /** * @fileoverview avoid-re-export-all * @author Pascal Schilp */ //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ import { RuleTester } from 'eslint'; import rule from '../../../lib/rules/avoid-re-export-all.js'; //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ // parser: require.resolve('@typescript-eslint/parser'), parserOptions: { sourceType: "module", ecmaVersion: "latest", }, }); ruleTester.run('avoid-re-export-all', rule, { valid: [ // 'export type { foo } from "foo";', // 'export type * as foo from "foo";', 'export { foo } from "foo";', 'export { foo as bar } from "foo";', ], invalid: [ { code: 'export * from "foo";', errors: [{ message: 'Avoid re-exporting * from a module, it leads to unused imports and prevents treeshaking.' }], }, { code: 'export * as foo from "foo";', errors: [{ message: 'Avoid re-exporting * from a module, it leads to unused imports and prevents treeshaking.' }], }, ], });