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.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import type { Plugin } from "vite";
|
||||
import { bootLeaks, type ChunkLike } from "./src/lib/chunkLeaks";
|
||||
|
||||
// A lazily imported page, rail, dialog or player must stay out of the chunks loaded at boot (the
|
||||
// entry, the shell and what they import): one static import of it on the boot path — a module's
|
||||
// index.ts re-exporting it is the easy way — makes its import() a silent no-op. That rule used to be
|
||||
// a hope; this makes it a build failure, naming each module and the boot chunk it landed in.
|
||||
const SHELL = "src/shell/App.tsx";
|
||||
|
||||
export function chunkGuard(): Plugin {
|
||||
return {
|
||||
name: "siftlode-chunk-guard",
|
||||
apply: "build",
|
||||
generateBundle(_options, bundle) {
|
||||
const lazy = new Set(
|
||||
[...this.getModuleIds()].filter(
|
||||
(id) => (this.getModuleInfo(id)?.dynamicImporters.length ?? 0) > 0,
|
||||
),
|
||||
);
|
||||
const chunks: ChunkLike[] = Object.values(bundle).flatMap((o) =>
|
||||
o.type === "chunk"
|
||||
? [
|
||||
{
|
||||
fileName: o.fileName,
|
||||
isEntry: o.isEntry,
|
||||
imports: o.imports,
|
||||
moduleIds: o.moduleIds,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
const leaks = bootLeaks(chunks, lazy, SHELL);
|
||||
if (leaks.length)
|
||||
this.error(
|
||||
`lazily imported, yet loaded at boot: ${leaks.join(", ")}. ` +
|
||||
"Something on the boot path imports it statically (an index.ts re-export?): import it lazily there too, or not at all.",
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user