# rules/eslint/closure-barrel-must-be-glob.eslint.rule.ts

> 89 lines of code and 20 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-closure-barrel-must-be-glob-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/closure-barrel-must-be-glob.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/closure-allowlist-entries-are-wired.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-allowlist-entries-are-wired.eslint.rule.ts.md) and before [rules/eslint/closure-build-script-unimported.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-build-script-unimported.eslint.rule.ts.md).

## Definitions

- `isGlobCall` (lexical_declaration, line 18)
- `isRelativeSideEffectImport` (lexical_declaration, line 46)
- `create` (method_definition, line 55, exported)
- `BARREL_MARKER` (lexical_declaration, line 8)
- `GLOB_METHOD` (lexical_declaration, line 9)
- `MIN_HAND_IMPORTS` (lexical_declaration, line 10)
- `basenameOf` (lexical_declaration, line 12)
- `norm` (lexical_declaration, line 13)
- `idx` (lexical_declaration, line 14)
- `init` (lexical_declaration, line 23)
- `callee` (lexical_declaration, line 27)
- `property` (lexical_declaration, line 31)
- `EXPORT_TYPES` (lexical_declaration, line 36)
- `isExport` (lexical_declaration, line 42)
- `target` (lexical_declaration, line 50)
- `basename` (lexical_declaration, line 56, exported)
- `program` (method_definition, line 61, exported)
- `body` (lexical_declaration, line 62, exported)
- `count` (lexical_declaration, line 69, exported)
- `payload` (lexical_declaration, line 73, exported)

## Uses

- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/selectors/syntax.selector.ts](https://banes-lab.com/source/governance/shared/selectors/syntax.selector.ts.md)

## Records this file checks

- [Runtime Discovery](https://banes-lab.com/records/architecture/runtime-discovery.md)
- [Open/Closed Principle](https://banes-lab.com/records/architecture/open-closed.md)
- [Hand-Kept Index](https://banes-lab.com/records/architecture/hand-kept-index.md)

## Source

```typescript
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { isType, literalString, nameOf, nodeAt, nodesAt } from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";
import { normalizePath } from "../../shared/resolvers/anchor.resolver.ts";

const BARREL_MARKER = ".barrel.ts";
const GLOB_METHOD = "glob";
const MIN_HAND_IMPORTS = 2;

const basenameOf = function basenameOf(path: string): string {
    const norm = normalizePath(path);
    const idx = norm.lastIndexOf("/");
    return idx === -1 ? norm : norm.slice(idx + 1);
};

const isGlobCall = function isGlobCall(statement: AstNode): boolean {
    if (statement.type !== "VariableDeclaration") {
        return false;
    }
    return nodesAt(statement, "declarations").some((decl) => {
        const init = nodeAt(decl, "init");
        if (!isType(init, "CallExpression")) {
            return false;
        }
        const callee = nodeAt(init, "callee");
        if (!isType(callee, "MemberExpression") || !isType(nodeAt(callee, "object"), "MetaProperty")) {
            return false;
        }
        const property = nodeAt(callee, "property");
        return isType(property, "Identifier") && nameOf(property) === GLOB_METHOD;
    });
};

const EXPORT_TYPES: ReadonlySet<string> = new Set([
    "ExportAllDeclaration",
    "ExportDefaultDeclaration",
    "ExportNamedDeclaration",
]);

const isExport = function isExport(statement: AstNode): boolean {
    return EXPORT_TYPES.has(statement.type);
};

const isRelativeSideEffectImport = function isRelativeSideEffectImport(statement: AstNode): boolean {
    if (statement.type !== "ImportDeclaration" || nodesAt(statement, "specifiers").length > 0) {
        return false;
    }
    const target = literalString(nodeAt(statement, "source"));
    return target?.startsWith(".") === true;
};

export default {
    create(context: RuleContext): RuleListener {
        const basename = basenameOf(context.filename);
        if (!basename.endsWith(BARREL_MARKER)) {
            return {};
        }
        return listener({
            program(view, node) {
                const body = nodesAt(view, "body");
                for (const statement of body.filter(isExport)) {
                    context.report({ data: { basename }, messageId: "barrelExports", node: statement });
                }
                if (body.some(isGlobCall)) {
                    return;
                }
                const count = body.filter(isRelativeSideEffectImport).length;
                if (count < MIN_HAND_IMPORTS) {
                    return;
                }
                const payload = { basename, count: String(count) };
                context.report({ data: payload, messageId: "barrelMustGlob", node });
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({
                detects: ["architecture:hand-kept-index"],
                enforces: ["architecture:runtime-discovery", "architecture:open-closed"],
            }),
            description:
                "Files matching `*.barrel.ts` must use `import.meta.glob` for discovery rather than enumerate sibling imports by hand, and must export nothing. Hand-imported barrels require an edit every time a new variant is added; glob barrels discover variants automatically. Operationalizes the self-registration mandate at the build layer.",
        },
        messages: {
            barrelExports:
                "Barrel file '{{ basename }}' exports a value. A discovery barrel only loads its variants for their registration side effect and exports nothing; a consumer that needs the discovered set reads it from the registry the variants register into, or derives it in its own module.",
            barrelMustGlob:
                "Barrel file '{{ basename }}' uses {{ count }} hand-written side-effect imports. Replace with `import.meta.glob(\"./*.<role>.ts\", { eager: true });` so new variants are discovered automatically. The pattern is the folder's declared role tag.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
