feat(modules): client extension slots (phase 3, slice 2) #138
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/module-client-slots"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 3, slice 2 of
docs/website/MODULE_SYSTEM.md§2.7.1 — core only, and the one slice that adds rather than moves. No UO code leaves core here. Docs: docs#137.Why this slice exists
Two problems in the client half turned out to be the same missing mechanism:
UserDetail.jsxrenders six UO sections with nowhere to put them. The server half gotdeclareSlot/registerExtensionin Phase 2 PR 4 and the six/admin/users/:id/shard/*routes moved behind it. The client never got the twin —registrytakes routes, nav and feature providers, and nothing else.SiteFooterlinks to/site/shard, a URL the extraction deletes, and the footer is not nav so no registry answers for it.So core declares a slot, at most one module fills it, and core renders
<Slot>— nothing when unfilled.MODULE_API_VERSION1.1.0 → 1.2.0; contract is MODULE_API.md §3.7.The two decisions
A slot is named for a PLACE, never for a meaning. The footer slot began as "make shard status a hook any module can use", and the only refinement is that core must not learn what a game server's status is.
site.footer.statusis a position and a bit of styling; the label, the target, the data and whether anything renders at all belong to the module. A slot typed by its content puts game semantics back into core, which is the thing this phase removes — and the next module wanting that spot for something else would need a second mechanism.This slice inverts the phase's merge order, once. Everywhere else
module-uomerges beforewebsite. Here core must go first, because a chunk cannot callregistry.registerExtensionbefore the function exists. Harmless precisely because this slice only adds: core declares both slots and fills them with its own components under owner idcore— the same trickuseShardFlagsand the server'sregistries.registerCore()already use — so the rendered pages are unchanged and the mechanism is proved by core's own content before a line of it moves.Where it is not fail-open
An unknown slot, a non-component and a second fill all throw, matching
checkExtensionShapeserver-side. A dropped nav row costs a link the viewer can reach another way; a silently dropped extension is invisible to everyone including its author. And a throw is always a programming error and never a race — core declares in its own bundle, every module chunk is a deferred script injected after it.Reading stays fail-safe (undeclared and unfilled both read null), and a filling component renders inside an error boundary — core's only one. That asymmetry is where the client differs from the server: a module route that throws costs the module's own page, but an extension throws inside CORE's, and the whole reason core keeps ownership of that page is that it stays usable.
What the browser smoke found
§7.7, with a throwaway hand-written chunk filling both slots. The footer rendered the module's own label and target in core's
linkStyle, the admin page receiveduserId, a deliberate render failure was contained to its own spot with the slot named in the console — and the footer was left showingemail · · Admin.The separator was rendered beside the slot, guarded on
hasExtension. That is right when nothing is installed and wrong when something is installed and fails: the slot is filled, so the separator renders, and the component then throws into the boundary leaving the separator behind. Core now decorates through<Slot wrap>, inside the boundary, so the decoration shares the extension's fate — andhasExtensionis deleted rather than left as a trap for the next caller. Every unit test passed both before and after.Two more things the smoke settled, neither a defect:
core. Correct and temporary (the client half deletes core's fills in the change that registers the module's), now stated in the contract.Zero CSP reports throughout.
Verification
routes.manifest.json158 public + 2 internal, unchanged;routes.guards.jsonunchanged;swagger-output.jsonregenerates byte-identicalAI disclosure
Written with Claude Code.
Co-Authored-By: Claude noreply@anthropic.com