Files
siftlode/frontend/src/lib/layers.contract.test.ts
T
peter d5867bacca feat(frontend): the folders are layers — an import only goes down, a module is reached through its index.ts
`local/layers` (eslint.config.js) resolves every import to its file and refuses one that goes up
(lib/ → components/ui/ → features/ → modules/ → shell/) or that reaches into another module's own
files. A module's index.ts is its public face: the stores, mounted at the root and read app-wide,
which features/ may read too; a page is still opened lazily by import(). Its contract test probes
every edge, and three mutants of the rule (off, no face check, a door for everyone) each turn it red.

Six modules got that face; the shared channel UI (about, tags, tag manager) moved to
features/channel, the filter chips and the saved-views context to features/filters, and the prefs
store's controller type moved next to the store instead of importing it from its own page. The
header's per-module controls and the eagerly loaded rails keep eight inline suppressions, each
naming why: they go when the header gets slots and every rail loads lazily.

`chunkGuard` fails the production build when a lazily imported module lands in a boot chunk (the
entry, the shell, what they import) — e.g. a page re-exported from an index.ts, which makes its
import() a silent no-op. Proved on a build with that mutant; today's build has none.
2026-10-22 23:30:27 +02:00

92 lines
3.8 KiB
TypeScript

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<string[]> {
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);
});
});