docs(website): client extension slots, and the client half is one slice (phase 3, slice 2) #137
@@ -782,6 +782,19 @@ those props — or **nothing at all** when the slot is unfilled. An instance wit
|
||||
therefore renders exactly what it renders today, which is the same "untouched path" guarantee
|
||||
`withModuleNav` makes for nav.
|
||||
|
||||
**Core decorates a slot with `<Slot wrap>`, never by asking whether it is filled.** `wrap` is called
|
||||
with the extension's element and rendered *inside* the boundary, so core's own markup around an
|
||||
extension — a separator, a heading, a rule — shares the extension's fate:
|
||||
|
||||
```jsx
|
||||
<Slot name="site.footer.status" linkStyle={LINK_STYLE} wrap={(link) => <> · {link}</>} />
|
||||
```
|
||||
|
||||
There is deliberately no `hasExtension` for a layout to branch on. Branching is right about the
|
||||
unfilled case and wrong about the failed one: the slot *is* filled, so the separator renders, and
|
||||
the component then throws into the boundary and leaves the separator behind on its own. Found in a
|
||||
browser with this exact footer separator, which is the only place it could have been found.
|
||||
|
||||
The slots in 1.2.0:
|
||||
|
||||
| Slot | Rendered in | Props core passes |
|
||||
@@ -830,10 +843,15 @@ already uses for the feature seam. It means the mechanism is exercised by core's
|
||||
moment it lands, and the extraction becomes a deletion rather than a rewrite made under extraction
|
||||
pressure.
|
||||
|
||||
`declareSlot` and `hasExtension` are module exports of `modules/registry.js` and deliberately **not**
|
||||
**While core fills a slot, no module can** — first fill wins, and core registers first. In 1.2.0 that
|
||||
means both slots are occupied and a module's fill is rejected, loudly, naming `core`. This is
|
||||
scaffolding, not the steady state: core's two fills and the two files behind them are deleted by the
|
||||
client half of Phase 3, in the same change that registers the module's. Neither slot is one core
|
||||
intends to fill permanently, and a slot core *does* intend to fill is a slot that should not exist.
|
||||
|
||||
`declareSlot` and `extensionFor` are module exports of `modules/registry.js` and deliberately **not**
|
||||
members of the `registry` object handed to modules, for the same reason `featureProviders()` is not:
|
||||
declaring is core's, and a module asks about a slot it knows the name of rather than enumerating what
|
||||
everyone else filled.
|
||||
declaring is core's, and so is reading back who filled what.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -882,6 +882,35 @@ Two decisions inside it, both settled with the org lead 2026-08-11:
|
||||
`registries.registerCore()` already use — so the rendered page is unchanged and the mechanism is
|
||||
proved by core's own content before a line of it moves.
|
||||
|
||||
#### Slice 2 — the slots (website#138, 2026-08-11)
|
||||
|
||||
`modules/registry.js` grows `declareSlot` / `registerExtension` / `extensionFor`; `modules/Slot.jsx`
|
||||
is the read side and core's only error boundary. `SiteFooter`'s link becomes `ShardStatusLink.jsx`
|
||||
and `UserDetail`'s six UO sections become `UserShardSections.jsx`, both registered by `main.jsx`
|
||||
under owner id `core`. 616 server + **169 client** tests (+9), manifest still 158, swagger
|
||||
byte-identical.
|
||||
|
||||
**The §7.7 browser smoke earned its place again, and this time by finding something no test would
|
||||
have.** A throwaway module filled both slots: the footer rendered the module's own label and target
|
||||
in core's `linkStyle`, the admin page received `userId`, and a deliberate render failure was
|
||||
contained to its own spot with the slot named in the console — but the footer was left showing
|
||||
`email · · Admin`. The separator was rendered *beside* the slot, guarded on `hasExtension`, which is
|
||||
right when nothing is installed and wrong when something is installed and fails. Core now decorates
|
||||
through `<Slot wrap>`, inside the boundary, and `hasExtension` is gone rather than kept as a trap for
|
||||
the next caller. Every unit test passed both before and after.
|
||||
|
||||
Two more things the smoke settled, neither of them a defect:
|
||||
|
||||
- **Core's own fill occupies the slot, so a module cannot fill it** — first fill wins and core
|
||||
registers first, so the smoke module's call was rejected naming `core`. That is correct and
|
||||
temporary: the client half deletes core's two fills in the change that registers the module's. It
|
||||
is worth stating in the contract, because a module written against 1.2.0 today cannot use either
|
||||
slot.
|
||||
- **Core's UO sections on the user-detail page now fail to load, and that is slice 1's doing, not
|
||||
this slice's** — core stopped serving `/admin/users/:id/shard/*` when the server half left, and
|
||||
with no module installed there is nothing at the other end. Identical before and after this change;
|
||||
the client half is what puts a live module behind it.
|
||||
|
||||
**Criterion 1 is a grep over code, not over prose** — see [API §5.2](MODULE_API.md#52-zero-uo-identifiers-in-core-ci-website-repo)
|
||||
for what that means precisely. Core's marketing copy says "shard" in a dozen places, and a literal
|
||||
word grep would have made every one of them a CI failure while proving nothing about the boundary.
|
||||
|
||||
Reference in New Issue
Block a user