the BRICK ASSEMBLY
ALTITUDE 2 — one baseplate, ten bricks, 89 authored files · an exploded LEGO assembly with every coupling named to the API call that makes it, brick and stud faces drawn opaque so nothing reads through a mass in front of it · REV J 2026-10-24: ui-router-registry takes schedule row 11, a compile-time part beside the lint plugin — types only, no stud, not drawn · source counted at origin/main @ 3f55e419 (2026-10-31)
STRUCTURE SCHEDULE — one row per brick · quantized plan · courses · the exact coupling, with its source
| # | package | version | files | sloc | mould | studs | coupling, with its source |
|---|---|---|---|---|---|---|---|
| 0 | @uirouter/core | 6.1.2 | 80 | 5,272 | baseplate, not massed — a plate is ground | A–E, its published API | every brick on this sheet declares it as a peerDependency; the only joints from one brick to another are 3 → 1, 5 → 1, 5 → 8, 6 → 1, 6 → 4, 7 → 1, 7 → 9 and 3 → 10 |
| 1 | lit-ui-router | 1.19.0 | 25 | 3,262 | 2×4 · 9 courses | studs A · B · D · carries F · G | class UIRouterLit extends UIRouter — moulded, not snapped; everything lit-specific enters through published seams: this.plugin(servicesPlugin) · this.stateRegistry.decorator(‘views’, litViewsBuilder) · urlService.listen()/sync() · viewService._pluginapi._viewConfigFactory(‘lit’, …) (internal) [src/core.ts] and publishes two of its own: F UIRouterLitElement.seekRouter(host), the ui-router-context event; G lit-ui-router/context, the context-request protocol [src/ui-router.ts, src/context.ts] |
| 2 | ui-router-navigation-location-plugin | 1.2.0 | 2 | 214 | 1×2 · 1 course | stud A (LOCATION SEAT) | navigationLocationPlugin = locationPluginFactory(‘vanilla.navigationLocation’, true, NavigationLocationService, BrowserLocationConfig) a router holds exactly one location plugin, so this brick SWAPS core’s pushStateLocation; it peers core only — it has never heard of Lit [src/index.ts] |
| 3 | lit-ui-router-mobx | 1.4.0 | 5 | 144 | 1×1 · 2 courses | seats on brick 1 (F) · peers brick 10 (K) | RouterStore.for(router) comes from ui-router-mobx ^0.2.0, stud K on brick 10, which registers the one onSuccess per router — this brick holds no hook of its own; src/router-store.ts re-exports it [src/router-reaction-controller.ts, src/router-store.ts] RouterReactionController → requestRouter(host, { subscribe: true }) over stud G, then UIRouterLitElement.seekRouter(host), the ui-router-context event — stud F on brick 1 [src/router-reaction-controller.ts] |
| 4 | ui-router-server | 0.7.1 | 21 | 3,006 | 2×4 · 7 courses | NO STUD on this plate · carries H | peerDependenciesMeta: { ‘@uirouter/core’: { optional: true } } — the ‘matcher’ tier is dependency-free pattern matching the ‘simulate’ tier reaches plates of its own behind a lazy import(): createHeadlessRouter(states, config) — new UIRouter() + plugin(servicesPlugin) + plugin(serverLocationPlugin), the mount’s strict / caseInsensitive / defaultSquashPolicy replayed [src/simulate.ts] a pool of them per mount, each held by one request at a time — prewarm builds them ahead, release() resets one for the next request [src/index.ts, src/router-pool.ts] |
| 5 | lit-ui-router-effect | 0.6.0 | 4 | 285 | 1×2 · 2 courses | seats on brick 1 (F) · peers brick 8 (I) | routeRef(router) and snapshotRoute() come from ui-router-effect ^0.2.0, stud I on brick 8, which registers the one onSuccess per router — this brick holds no hook of its own [src/router-ref-controller.ts, src/index.ts] RouterRefController → getScopedRouter() under a server render, else requestRouter(host, { subscribe: true }) over stud G, then seekRouter(host) on stud F; RefController forks one Effect fiber per host · peers effect ^3.22.0 || ^4.0.2 [src/router-ref-controller.ts, src/ref-controller.ts] |
| 6 | lit-ui-router-ssr | 0.7.0 | 18 | 1,485 | 2×4 · 6 courses | seats on brick 1 (G) AND on brick 4 (H) — the one part with a drop line onto each plate’s assembly | prerender() → createServerRouter({ mounts }).resolve(path) per path · provideRouter(root, router) + withRouterSync(router, …) around @lit-labs/ssr render() [src/prerender.ts] UiViewRenderer → getScopedRouter() + router.viewService.registerUIView(); withServedRender(UiView) extends brick 1’s view, which wakes on requestContext(adoptUiViewContext) [src/ui-view-renderer.ts, src/served-view.ts, src/register.ts] client half: hydrateRoot() → provideContext(container, adoptUiViewContext, adopt) + hydrate(); uiViewSlot() marks each view’s hole · peers @lit-labs/ssr ^4.1.0 and ssr-client ^1.1.7 — a render table, not a router plate [src/client.ts] |
| 7 | lit-ui-router-signals | 0.3.0 | 4 | 170 | 1×2 · 2 courses | seats on brick 1 (F) · peers brick 9 (J) | RouterSignalController → getScopedRouter() under a server render, else requestRouter(host, { subscribe: true }) over stud G, then UIRouterLitElement.seekRouter(host), stud F on brick 1 [src/router-signal-controller.ts] RouterSignals.for(router) + watchSelection() from ui-router-signals ^0.2.0 — it holds no hook of its own; brick 9 registers the one onSuccess per router [src/router-signal-controller.ts, src/signal-controller.ts] |
| 8 | ui-router-effect | 0.2.0 | 3 | 30 | 1×1 · 1 course | studs C · E on the plate · carries I · peers core and nothing in this repo | routeRef(router) → router.transitionService.onSuccess({}, …), one SubscriptionRef per router; snapshotRoute() reads globals.current/.params/.successfulTransitions — no Lit host, no brick under it · peers effect ^3.22.0 || ^4.0.2 [src/route-ref.ts] |
| 9 | ui-router-signals | 0.2.0 | 4 | 104 | 1×1 · 2 courses | studs C · E on the plate · carries J · peers core and nothing in this repo | RouterSignals.for(router) → attach(): one transitionService.onSuccess({}, …) hook writes a Signal.State; route · current · params · transition are Signal.Computed over it — no Lit host, no brick under it · peers signal-polyfill ^0.2.0 [src/router-signals.ts] |
| 10 | ui-router-mobx | 0.2.0 | 3 | 117 | 1×1 · 1 course | the C rail’s far seat · carries K · peers core and nothing in this repo | RouterStore.for(router) → attach(): one transitionService.onSuccess({}, update) hook writes MobX observables; update() reads globals.current/.params/.successfulTransitions — no Lit host, no brick under it · peers mobx ^6.0.0 || ^7.0.0 [src/router-store.ts] |
| 11 | ui-router-registry | 0.3.0 | 5 | 362 | not a brick | no stud on any router plate · peers core ^6.0.8 for its types | a types package: an app fills in interface Register by module augmentation, and StateName · ParamsOf · TypedStateService narrow to its states — it emits no JavaScript [src/registry.ts] |
| 12 | eslint-plugin-lit-ui-router | 1.3.0 | 12 | 1,481 | not a brick | no stud on any router plate | a lint package: it plugs into ESLint/oxlint hosts, never into @uirouter/core — rows 11 and 12 are the two published parts of twelve that live on a different table — types and lint |
TOTAL — 10 bricks drawn · 89 authored files · 8,817 sloc, standing on one 80-file, 5,272-line baseplate · counted at origin/main @ 3f55e419 (2026-10-31), same basis as sheets 3 and 4
STUD SCHEDULE — the published surface each brick seats on
| stud | published on | seam | who seats on it |
|---|---|---|---|
| A | core — the plate | router.plugin(factory) | the back rail, eight seats — brick 1 registers servicesPlugin on it the red ring: ONE location plugin per router — brick 2 SWAPS core’s out |
| B | core — the plate | stateRegistry.decorator(…) | brick 1 — decorator(‘views’, litViewsBuilder) is the whole Lit graft |
| C | core — the plate | transitionService.onSuccess() — a front rail, a hook per caller | bricks 8, 9 and 10 — one hook per router each; 8 and 10 seat on it |
| D | core — the plate | urlService.listen() / .sync() / .rules | brick 1 starts the client; brick 4 replays rules on its own plate |
| E | core — the plate | globals.current / .params | bricks 8, 9 and 10 read them into observables — none writes them |
| F | a brick, for the ones above it | UIRouterLitElement.seekRouter(host) | brick 1’s ui-router-context event — bricks 3, 5 and 7 seat here |
| G | a brick, for the ones above it | lit-ui-router/context — context-request | brick 1’s provideRouter · withRouterSync · getScopedRouter, requestContext · provideContext — brick 6 seats here; 3, 5 and 7 ask here first |
| H | a brick, for the ones above it | createServerRouter({ mounts }).resolve(path) | brick 4’s verdict table — brick 6 prerenders through it |
| I | a brick, for the ones above it | routeRef(router) · snapshotRoute() | brick 8’s SubscriptionRef — brick 5 forks it per host |
| J | a brick, for the ones above it | RouterSignals.for(router) · watchSelection() | brick 9’s signal state — brick 7 binds it to a Lit host |
| K | a brick, for the ones above it | RouterStore.for(router) | brick 10’s observable store — brick 3 reacts to it on a Lit host |
TOTAL — 11 seams: 5 on core, 6 on the bricks
GENERAL NOTES
Why bricks. The mechanism these packages share is a standardised coupling: each companion attaches through a published extension point — on @uirouter/, or on a brick below it that publishes one — and any brick nothing stands on can be left in the box without disturbing the rest. That is a stud, and a stud is worth drawing. So this is an exploded isometric — the LEGO instruction manual's own idiom — with a numbered part per package, a drop line onto the exact stud it takes, and a parts callout. The exploded view seats nothing, because a seated assembly hides the undersides, and the undersides are the argument; the finished model under it is drawn seated for the other half of the reading — the shape the parts make — in three steps at the drawing's own corner and then whole from two more, on a landscape of two levels: browser ground under the client plate, and a server shelf under the headless one.
The plate is core, not this package. Sheet 4's finding decides it: every limb in the family declares @uirouter/ as a peer. lit-ui-router is therefore brick 1, not the ground — and the drawing is honest about the one place the metaphor strains: class UIRouterLit extends UIRouter is moulded onto the plate, not snapped to it. What the drawing then shows is that the graft is thin anyway. Everything Lit-specific arrives through two published seams — this.plugin(servicesPlugin) and this.stateRegistry.decorator('views', litViewsBuilder) — plus urlService.listen()/ to start the thing. Three calls, and one internal seam — viewService._pluginapi._viewConfigFactory('lit', …) — that core does not publish. That is the whole renderer coupling.
One stud is a seat, not a socket. router.plugin() is the back rail: a single method that anything may queue on, which is why the spare-parts box is drawn at all — @uirouter/, sticky-states, dsr and rx all register through it and none of them knows this repo exists. One seat on that rail is ringed red, because it behaves differently: a router holds exactly one location plugin, so ui-router-navigation-location-plugin is a swap for core's pushStateLocation, never an addition. That package peers @uirouter/ and nothing else — it has never heard of Lit, and would work identically under the React or Angular adapters.
Brick 1 publishes two studs of its own, and four bricks take them. lit-ui-router-mobx and lit-ui-router-effect both ask first over stud G, requestRouter(host, { subscribe: true }), so a placeholder router's upgrade reaches them, and seat through UIRouterLitElement.seekRouter(host) — stud F, a bubbling ui-router-context event, published precisely as the dependency-injection primitive for external reactivity systems — when no provider answers. Brick 3 holds no hook of its own: it takes RouterStore.for() from ui-router-mobx ^0.2.0, stud K on brick 10, and runs a MobX reaction per connected host over the store it returns. Brick 5 holds no hook of its own: it takes routeRef() and snapshotRoute() from ui-router-effect ^0.2.0, stud I on brick 8, and forks one Effect fiber per connected host over the SubscriptionRef it returns. Both observe; neither writes router state. 144 and 285 lines. lit-ui-router-ssr takes stud G alone: lit-ui-router/, the community context-request protocol spoken without @lit/. prerender() provides the router on the render root with provideRouter() and scopes each render with withRouterSync(), its UiViewRenderer reads that scope with getScopedRouter(), and on the client the served view it defines — withServedRender(UiView), brick 1's own view class extended — wakes on requestContext(adoptUiViewContext), which hydrateRoot() answers with provideContext().
The agnostic bricks. ui-router-effect (brick 8), ui-router-signals (brick 9) and ui-router-mobx (brick 10) peer @uirouter/ and nothing in this repo — no Lit, no brick 1: each holds one transitionService.onSuccess({}, …) hook per router and reads globals, into an Effect SubscriptionRef through routeRef(), a Signal.State through RouterSignals.for() or MobX observables through RouterStore.for(). So all three drop straight onto the plate, brick 8 onto stud C, brick 9 onto stud E and brick 10 onto the far seat of stud C's front rail — onSuccess() is one method that queues a hook per caller — 30, 104 and 117 lines with no brick under them. Each one-stud cap is ringed, because a Lit binding on brick 1's cap peers it: I, routeRef(), which brick 5 takes; J, RouterSignals.for(router) and watchSelection(), which lit-ui-router-signals (brick 7) takes at ^0.2.0; and K, RouterStore.for(router), which brick 3 takes at ^0.2.0. Bricks 5, 7 and 3 find their router over G and then F on brick 1, and stand nine courses above the plate, so they peer I, J and K without sitting on them: those joints are lettered at both ends, not dropped, and no binding holds a hook of its own.
The fourth brick has no stud on the client plate, and that is the design. ui-router-server declares @uirouter/ as an optional peer (peerDependenciesMeta); its default 'matcher' tier is dependency-free pattern matching and never loads core, and its 'simulate' tier reaches plates of its own behind a lazy import() — a pool of headless routers per mount, each a new UIRouter() with servicesPlugin and the package’s own serverLocationPlugin, the mount’s matcher config replayed onto it, built on first use or up front by prewarm, reset between requests and dropped after a failed transition, because core mutates registrations and no two requests may hold one at the same time. Drawing it over a dashed second plate is the only truthful placement: it is the same mould, a different assembly, and the tie back to the client plate is crossed out. Seated, that plate stands on a shelf of its own: server ground, a request runtime with no DOM (node >=22.12; Connect, Vite, fetch and Hono adapters), drawn two courses above the browser ground because brick 6 must meet brick 4's cap at the height of brick 1's. The plate on it is dashed — an optional peer, reached by lazy import — and the ground is not. It does carry a stud of its own, H — createServerRouter({ mounts }), whose resolve(path) returns a verdict.
Brick 6 is the bridge. lit-ui-router-ssr peers lit-ui-router ^1.19.0 and ui-router-server ^0.7.0, and it is the one part in the family that takes a drop line onto each of this sheet's two assemblies: G on brick 1, H on brick 4. prerender() compiles the mount table with createServerRouter(), asks it for a verdict per path, and turns shell verdicts into pages, redirects into host rules and the otherwise projection into the 404 document. It never reaches the second plate's studs itself — that plate is ui-router-server's to load or not. Its other two peers, @lit-labs/ ^4.1.0 and @lit-labs/ ^1.1.7, are a render table, not a router plate, so they take no stud here.
Massing, quantized. Continuous mass would have made these bricks unbuildable shapes, so the census is rounded to LEGO: plan is one stud per 150 sloc rounded up to the next standard shape (1×1, 1×2, 2×2, 2×3, 2×4), height is one course per three authored files. The result is legible and it is a finding — the three bricks that carry a renderer, a server and the prerender bridge between them are all 2×4s; the seven that plug the router into something are four 1×1 and three 1×2 tiles, 30, 104, 117, 144, 170, 214 and 285 lines. A plug that needed to be a 2×4 would be lit-ui-router's problem to absorb, not a package. The baseplate is deliberately not massed: 80 files and 5,272 lines of core is ground, and ground has no height.
Twelve published packages; ten bricks in this assembly. Every runtime companion is a brick, and every one is drawn. The remaining two packages are compile-time parts, and neither is a brick: they take no stud on any router plate. ui-router-registry 0.3.0 (5 files, 362 sloc) is types only — it peers @uirouter/ for the declarations it narrows, and an app's module augmentation of Register is the whole of its coupling, so nothing of it reaches a running router. eslint-plugin-lit-ui-router 1.3.0 (12 files, 1,481 sloc) couples to ESLint and oxlint, not to @uirouter/. Every number on this sheet is read from www/ — counted at origin/main @ 3f55e419 (2026-10-31).
KEY
| a published package — plan ∝ quantized sloc, courses ∝ files | |
| a stud — a published extension point, on core or on a brick | |
| the location seat — exactly one per router, so it swaps | |
| a ringed stud on a plate brick — a brick on the cap peers it, lettered at both ends, not dropped | |
| drop line — the brick falls onto that stud | |
| a second plate — optional peer, reached by lazy import | |
| no coupling — the server takes no stud on this plate | |
| spare part — same stud, not in this set (sheet 4) | |
| ground — the browser, and a server shelf above the no-DOM line |