Status: Accepted (initial scope)
Owner: Geometra core maintainers
Geometra currently has strong rendering, input, and server/client protocol primitives, but no first-class routing package. Applications can switch views manually, yet they do not get:
- deterministic route matching
- nested layout routes with outlet composition
- navigation lifecycle primitives across canvas, terminal, and server/client runtimes
- a standard data-loading and mutation model tied to route transitions
To compete with modern router ecosystems, Geometra needs an official routing layer that preserves its north-star architecture:
Tree -> Yoga WASM -> Geometry -> Pixels
Ship a new package: @geometra/router.
This package will provide:
- route definition and matching primitives
- history adapters (browser + memory)
- navigation APIs (imperative and declarative)
- route transition state signals
- integration points for loaders/actions and error boundaries in later phases
The router must remain declarative and renderer-agnostic so the same route model works for canvas, terminal, and server/client protocol flows.
- Applications can define nested routes with static, dynamic, and splat segments.
- Route matching and ranking are deterministic and fully covered by unit tests.
- Apps can navigate with
navigate(to, { replace? }), back, and forward. - Browser runtime supports history sync; non-browser runtimes use memory history.
- Route state (location, params, matched branch, pending state) is exposed as reactive primitives compatible with
@geometra/core. - Initial docs and at least one working demo show router usage in Geometra.
- Full data-router parity in first release (loaders/actions are phased next).
- File-based routing conventions.
- DOM-only features that cannot be mapped to non-DOM renderers.
- Framework-specific compile-time transforms.
- Replacing existing
@geometra/coreprimitives.
- Keep protocol behavior explicit and version-safe for server/client mode.
- Do not introduce DOM coupling in core matching/navigation logic.
- Preserve focus/selection/text input correctness through route transitions.
- Maintain Bun-first compatibility and reliable CI behavior.
@geometra/router is required to be first-class across all primary Geometra runtimes.
| Runtime | Status | Required behavior |
|---|---|---|
Local canvas (createApp + CanvasRenderer) |
Required | Route transitions render as normal tree updates with no DOM dependency in matching/navigation core. |
Terminal (createApp + TerminalRenderer) |
Required | Same route definitions and matching behavior via memory history; keyboard navigation supported. |
Server/client protocol (@geometra/server + @geometra/client) |
Required | Initial route match works server-side and navigation behavior is explicit over protocol messages. |
Acceptance gate: no router release is considered complete unless all three runtimes are covered by integration tests and examples.
The router exposes a single history interface so runtime differences stay behind adapters.
- Uses
window.history.pushState/replaceStatefor navigations. - Subscribes to
popstatefor back/forward transitions. - Normalizes location as
{ pathname, search, hash }. - Treats trailing slash and base path handling consistently with memory history.
- Never requires DOM nodes for core navigation behavior (only
windowhistory APIs).
- Maintains an internal stack (
entries[],index) for non-browser runtimes and tests. - Supports
push,replace,back, andforwardwith browser-equivalent semantics. - Emits updates through the same subscription API used by browser history.
- Serves as the default adapter for terminal/runtime contexts without
window.
navigate(to)andnavigate(to, { replace: true })must yield equivalent final locations across adapters.back()andforward()behavior must match for the same sequence of operations.- Querystring and hash parsing must be adapter-neutral.
- Router state updates must be deterministic and emitted in the same order across adapters.
For @geometra/server + @geometra/client mode, routing transitions must be explicit protocol events.
navigate: request navigation to a destination.- shape:
{ type: 'navigate', to, replace?: boolean, requestId, protocolVersion?: 1 }
- shape:
history: request history movement.- shape:
{ type: 'history', delta, requestId, protocolVersion?: 1 }
- shape:
prefetch: request route module/data prefetch.- shape:
{ type: 'prefetch', to, requestId, protocolVersion?: 1 }
- shape:
navigationStart: acknowledges transition start.- shape:
{ type: 'navigationStart', requestId, to, protocolVersion?: 1 }
- shape:
frame/patch: existing render transport for resulting UI geometry.navigationEnd: confirms transition completion and canonical location.- shape:
{ type: 'navigationEnd', requestId, location, protocolVersion?: 1 }
- shape:
error: transition-level failure with request correlation.- shape:
{ type: 'error', requestId, message, code?, protocolVersion?: 1 }
- shape:
- Each client-initiated navigation includes a
requestIdfor deterministic correlation. - Server must emit
navigationEndorerrorfor every acceptednavigate/historyrequest. - Unknown newer protocol versions must fail explicitly (existing v1 behavior).
- Navigation events are ordered relative to emitted
frame/patchmessages to avoid ambiguous client state.
type RouteDefinition = {
path?: string
id?: string
children?: RouteDefinition[]
component?: () => UIElement
}
type Router = {
start(): void
dispose(): void
navigate(to: string, opts?: { replace?: boolean }): void
back(): void
forward(): void
state: Signal<{
location: { pathname: string; search: string; hash: string }
params: Record<string, string>
matches: Array<{ id?: string; path?: string }>
navigation: 'idle' | 'navigating'
}>
}
declare function createRouter(config: {
routes: RouteDefinition[]
history?: BrowserHistory | MemoryHistory
}): Router- Foundation router (matching + history + navigate + nested outlet composition).
- Data router (loaders/actions/redirect/cancel/revalidate).
- DX and optimization (prefetch/lazy routes/dev overlay/advanced diagnostics).
- Route transitions can regress focus and IME flows if lifecycle hooks are unclear.
- Server/client navigation protocol may drift without explicit message contracts.
- Ranking edge cases can cause surprising matches without strict tests.
Adopt this RFC as the baseline for checklist execution, starting with the foundation milestone.