FIT VERDICTexploded, then seated: every coupling is a published stud, on core or on the brick below — and the server takes none of core’s
the BRICK ASSEMBLY
ALTITUDE 2 — one baseplate, ten bricks, 72 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 I 2026-10-15: brick 10, ui-router-mobx, drops onto the far seat of stud C’s front rail, and its cap carries ringed stud K, which brick 3 peers from brick 1’s cap — brick 3 holds no hook of its own · source counted at origin/main @ eb1162da (2026-10-18)
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.1.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.1.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. 145 and 281 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 — 21, 95 and 68 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.1.0; and K, RouterStore.for(router), which brick 3 takes at ^0.1.0. Bricks 5, 7 and 3 find their router over G and then F on brick 1, and stand eight 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 three 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.16.1 and ui-router-server ^0.2.0 || ^0.3.0 || ^0.4.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, 21, 68, 95, 145, 171, 208 and 281 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.
Eleven published packages; ten bricks in this assembly. Every runtime companion is a brick, and every one is drawn. The remaining package, eslint-plugin-lit-ui-router 1.2.1 (12 files, 1,462 sloc), is not a brick at all: it takes no stud on any router plate, because it couples to ESLint and oxlint, not to @uirouter/. Every number on this sheet is read from www/ — counted at origin/main @ eb1162da (2026-10-18).
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 |