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

rules/eslint/closure-barrel-must-be-glob.eslint.rule.ts is a file in GovLab Extension Host. 89 lines of code and 20 definitions.

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;