import { describe, expect, it } from "vitest"; import { eslint } from "./lintProbe.testkit"; // The layer rule (local/layers in eslint.config.js) is what makes the folders a structure rather // than a filing habit: lib/ → components/ui/ → features/ → modules/ → shell/, an import only goes // down, and a module's own files are reached from outside only through its index.ts or lazily. A // rule that matched nothing would look exactly like a clean codebase, so each edge is probed here. async function refused(source: string, filePath: string): Promise { const [result] = await eslint.lintText(source, { filePath }); return (result?.messages ?? []).filter((m) => m.ruleId === "local/layers").map((m) => m.message); } const imp = (...specs: string[]) => specs.map((s, i) => `import x${i} from "${s}";\n`).join("") + `export default [${specs.map((_, i) => `x${i}`).join(", ")}];\n`; const IN_FEED = "src/modules/feed/__probe.tsx"; const IN_FEATURE = "src/features/video/__probe.tsx"; describe("a module", () => { it("reaches another module only through its index.ts", async () => { expect(await refused(imp("../plex/PlexBrowse"), IN_FEED)).toHaveLength(1); expect(await refused(imp("../plex", "../plex/index"), IN_FEED)).toEqual([]); }); it("by every spelling: a type, a re-export, a type-only import", async () => { const src = 'import type { P } from "../plex/plexContext";\n' + 'export { usePlex } from "../plex/plexContext";\n' + 'export * from "../channels/channelsContext";\n' + "export type Q = P;\n"; expect(await refused(src, IN_FEED)).toHaveLength(3); }); it("may open another module's page lazily, and use its own files and anything below", async () => { const src = 'const P = () => import("../plex/PlexBrowse");\n' + 'import F from "./Feed";\nimport V from "../../features/video/VideoCard";\n' + 'import B from "../../components/ui/Button";\nimport { qk } from "../../lib/queryKeys";\n' + "export default [P, F, V, B, qk];\n"; expect(await refused(src, IN_FEED)).toEqual([]); }); it("never imports the shell", async () => { expect(await refused(imp("../../shell/Header"), IN_FEED)).toHaveLength(1); }); }); describe("a feature", () => { it("reads a module only through its face, and never the shell", async () => { expect(await refused(imp("../../modules/plex"), IN_FEATURE)).toEqual([]); const up = await refused(imp("../../modules/plex/plexContext", "../../shell/App"), IN_FEATURE); expect(up).toHaveLength(2); expect(up.every((m) => m.includes("sits below"))).toBe(true); }); it("uses other features freely", async () => { expect(await refused(imp("../player/ClipPanel", "../filters/SearchBar"), IN_FEATURE)).toEqual( [], ); }); }); describe("the platform and lib layers", () => { it("import nothing above them — not even a module's face", async () => { const ui = await refused( imp("../../features/video/VideoCard", "../../modules/feed", "../../shell/App"), "src/components/ui/__layers_probe.tsx", ); expect(ui).toHaveLength(3); const lib = await refused( imp("../features/navigation/navigationContext", "../modules/feed", "../shell/App"), "src/lib/__layers_probe.ts", ); expect(lib).toHaveLength(3); }); }); describe("the shell and the entry", () => { it("reach a module through its face, or lazily", async () => { const src = imp("../modules/feed/Feed", "../modules/feed") + 'export const L = () => import("../modules/feed/Feed");\n'; expect(await refused(src, "src/shell/__layers_probe.tsx")).toHaveLength(1); }); it("only the root entry mounts the shell", async () => { expect(await refused(imp("./shell/App"), "src/__layers_probe.tsx")).toEqual([]); expect(await refused(imp("./modules/feed/Feed"), "src/__layers_probe.tsx")).toHaveLength(1); }); });