rules/eslint/closure-no-cross-member-relative.eslint.rule.ts
rules/eslint/closure-no-cross-member-relative.eslint.rule.ts is a file in GovLab Extension Host. 80 lines of code and 19 definitions.
import type { LocalRule, NodeHandler, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { WORKSPACE_ROOT, collapsePath, normalizePath } from "../../shared/resolvers/anchor.resolver.ts";
import { literalString, locOf, nodeAt } from "../../shared/selectors/syntax.selector.ts";
import { manifestAt, memberRels } from "../../shared/loaders/manifest.loader.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";
import { noPackageName } from "../../shared/strings/manifest.strings.ts";
import path from "node:path";
interface Member {
name: string;
rel: string;
}
const packageNameOf = function packageNameOf(memberRel: string): string {
const { name } = manifestAt(path.join(WORKSPACE_ROOT, memberRel));
if (typeof name !== "string" || name.length === 0) {
throw new TypeError(noPackageName(memberRel));
}
return name;
};
const buildMembers = function buildMembers(): Member[] {
return memberRels()
.map((memberRel) => ({ name: packageNameOf(memberRel), rel: memberRel }))
.toSorted((a, b) => b.rel.length - a.rel.length);
};
const MEMBERS = buildMembers();
const ROOT_POSIX = normalizePath(WORKSPACE_ROOT);
const memberOf = function memberOf(absolute: string): Member | null {
const posix = normalizePath(absolute);
if (!posix.startsWith(`${ROOT_POSIX}/`)) {
return null;
}
const relative = posix.slice(ROOT_POSIX.length + 1);
return MEMBERS.find((member) => relative === member.rel || relative.startsWith(`${member.rel}/`)) ?? null;
};
export default {
create(context: RuleContext): RuleListener {
const check: NodeHandler = function check(view, node) {
const source = nodeAt(view, "source");
const specifier = literalString(source);
if (specifier === null) {
return;
}
if (!specifier.startsWith(".")) {
return;
}
const filename = normalizePath(context.filename);
const fromDir = filename.slice(0, filename.lastIndexOf("/"));
const target = collapsePath(`${fromDir}/${specifier}`);
const from = memberOf(filename);
const to = memberOf(target);
if (from === null || to === null || from.rel === to.rel) {
return;
}
const payload = { specifier: to.name.length > 0 ? to.name : to.rel, target: to.rel };
if (source === null) {
context.report({ data: payload, messageId: "crossMember", node });
return;
}
context.report({ data: payload, loc: locOf(source), messageId: "crossMember" });
};
return listener({ exportAllDeclaration: check, exportNamedDeclaration: check, importDeclaration: check });
},
meta: {
docs: {
checks: defineCheck({
detects: ["architecture:boundary-leakage"],
enforces: ["architecture:explicit-boundaries"],
}),
description:
"A relative import addresses a sibling by hop count, which is only stable inside one workspace member. Escaping the member with `../` encodes the distance between two trees that move independently: nothing fails when the count goes stale, the specifier just resolves somewhere else or stops resolving. Cross-member references go through the declared package specifier, which is the module SSOT and survives either end moving.",
workspaceWide: true,
},
messages: {
crossMember:
"This relative import leaves its workspace member and reaches into `{{ target }}` by hop count, so it breaks silently the moment either tree moves. Import from `{{ specifier }}` instead, adding the subpath to that package's `exports` map if it is not published yet.",
},
schema: [],
type: "problem",
},
} satisfies LocalRule;