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:
2026-10-22 23:30:27 +02:00
parent 63a0fecc8b
commit d5867bacca
66 changed files with 453 additions and 109 deletions
+4 -4
View File
@@ -1,7 +1,7 @@
# `components/ui/` — the platform layer
Everything in this directory is a **platform component**: generic, reusable, and **domain-free**.
Everything in `components/` above it is a **module component**: it knows about videos, channels,
Everything above it — `features/`, `modules/` and `shell/` under `src/` — knows about videos, channels,
playlists, Plex or downloads.
That boundary is the whole point of this directory. Before E10 S1 these 33 modules sat flat among
@@ -18,9 +18,9 @@ A component belongs here **iff its props and imports name no domain concept**. I
- generic helpers from `lib/` (`layerStack`, `history`, `columnSort`, `useScrollFade`, …),
- i18n, React, and third-party UI libraries.
It may **not** import a feature component (`../Feed`) or a domain API slice (`lib/api/*`). This is
enforced by `no-restricted-imports` in `eslint.config.js` — not by this README, which is only the
explanation. If a primitive needs domain data, take it as a prop or a render-prop and let the module
It may **not** import anything from `features/`, `modules/` or `shell/`, or a domain API slice
(`lib/api/*`). This is enforced in `eslint.config.js` (`no-restricted-imports` and `local/layers`) — not
by this README, which is only the explanation. If a primitive needs domain data, take it as a prop or a render-prop and let the module
supply it. The same goes for its words: a primitive translates only from the platform i18n
namespaces (`common`, `errors`, `datatable`, `keys`, `palette`, `sidebar`, `notifications`, `time` —
`uiI18n.contract.test.ts`); what a value means where it is used arrives as a prop (`PriorityStepper`'s