# Architecture
> High-level system design for contributors. See [Glossary](https://github.com/greydragon888/real-router/wiki/glossary) for project-specific terminology.
## Bird's Eye View
Real-Router is a **named, hierarchical, state-driven router** for JavaScript applications. Routes form a dot-notation tree (`users.profile.edit`), navigation is guarded by lifecycle functions, and the entire lifecycle is driven by a single finite state machine — no boolean flags, no ad-hoc state.
Key technical choices:
- **Segment Trie** for URL matching — O(segments) traversal, O(1) for static routes
- **Facade + Namespaces** — thin Router class delegates to single-responsibility namespace modules
- **Optimistic sync execution** — navigation runs synchronously unless a guard returns a Promise
- **Plugin interception** — plugins wrap router methods (onion-layer), they cannot block transitions
- **Deeply frozen state** — all `State` objects are `Object.freeze()`'d, never mutated
## Package Map
```
real-router/
├── packages/
│ ├── core/ # Router implementation (facade + namespaces)
│ ├── core-types/ # @real-router/types — shared TypeScript types
│ ├── react/ # React integration (triple entry: main for 19.2+, /legacy for 18+, /ink for Ink 7+ terminal UIs)
│ ├── preact/ # Preact integration (hooks, components, Suspense)
│ ├── solid/ # Solid.js integration (hooks, components, directives)
│ ├── vue/ # Vue 3 integration (composables, components, directives)
│ ├── svelte/ # Svelte 5 integration (composables, components, actions)
│ ├── angular/ # Angular 21+ integration (signals, inject* functions, components, directives, zoneless)
│ ├── sources/ # Subscription layer for UI bindings: cached getTransitionSource / createDismissableError / createActiveNameSelector, canonicalJson params
│ ├── rx/ # Reactive Observable API (state$, events$, operators)
│ ├── browser-plugin/ # Browser History API synchronization
│ ├── hash-plugin/ # Hash-based routing (#/path)
│ ├── logger-plugin/ # Development logging with timing and param diffs
│ ├── persistent-params-plugin/ # Parameter persistence across navigations
│ ├── ssr-data-plugin/ # SSR per-route data loading via start() interceptor
│ ├── rsc-server-plugin/ # RSC per-route ReactNode loading via start() interceptor (bundler-agnostic)
│ ├── lifecycle-plugin/ # Route-level lifecycle hooks: onEnter, onStay, onLeave
│ ├── preload-plugin/ # Preload on navigation intent (hover, touch) via event delegation
│ ├── memory-plugin/ # In-memory history stack: back/forward/go without browser History API
│ ├── navigation-plugin/ # Navigation API browser synchronization + route-level history
│ ├── validation-plugin/ # Opt-in argument validation (DX-only, 100% tree-shakeable)
│ ├── search-schema-plugin/ # Runtime search param validation via Standard Schema (Zod, Valibot, ArkType)
│ ├── route-utils/ # Route tree queries and segment testing
│ ├── logger/ # Isomorphic structured logging
│ ├── fsm/ # Finite state machine engine (internal, published by accident)
│ ├── event-emitter/ # Generic typed event emitter (internal)
│ ├── route-tree/ # Route tree building, validation, matcher facade (internal)
│ ├── path-matcher/ # Segment Trie URL matching and path building (internal)
│ ├── search-params/ # Query string handling (internal)
│ └── type-guards/ # Runtime type validation (internal)
├── shared/ # Bare source files shared across packages via src/ symlinks (minimal workspace entry)
│ ├── package.json # Minimal: name, type:commonjs, devDeps on @real-router/core + type-guards
│ ├── dom-utils/ # Shared DOM utilities for adapters: route announcer, scroll restoration, scroll spy (#575), view transitions, direction tracker, link helpers
│ ├── browser-env/ # Shared browser abstractions for URL plugins: history API, popstate, SSR fallback
│ └── ssr/ # Shared SSR plugin scaffolding: createSsrLoaderPlugin generic factory + createLoadersValidator
├── examples/
│ ├── shared/ # Shared store, API, abilities, styles
│ ├── web/
│ │ ├── react/ (28 vite apps) # React 19.2+ (incl. animation-examples × 4 + ssr-examples × 5 [ssr, ssr-streaming, ssr-mixed, ssg, ssr-rsc]); 59 e2e specs
│ │ ├── preact/ (21 vite apps) # Preact 10 (incl. animation-examples × 4 + ssr-examples × 4); 54 e2e specs
│ │ ├── solid/ (24 vite apps) # Solid.js (incl. animation-examples × 4 + ssr-examples × 4); 54 e2e specs
│ │ ├── vue/ (24 vite apps) # Vue 3 SFC (incl. animation-examples × 4 + ssr-examples × 4); 55 e2e specs
│ │ ├── svelte/ (25 vite apps) # Svelte 5 (incl. animation-examples × 4 + ssr-examples × 4); 54 e2e specs
│ │ └── angular/ (16 vite apps) # Angular 21+ (incl. animation-examples × 4 + ssr-examples × 4 using provideRealRouterFactory); 49 e2e specs
│ ├── console/
│ │ └── react-ink/ (1 app) # CLI demo via @real-router/react/ink + memory-plugin
│ └── desktop/
│ ├── electron/ (3 apps) # Electron: browser-plugin (app://), hash-plugin (file://), navigation-plugin
│ └── tauri/ (2 apps) # Tauri v2: browser-plugin, navigation-plugin
```
**Public packages** (published to npm): `core`, `core-types`, `react`, `preact`, `solid`, `vue`, `svelte`, `angular`, `sources`, `rx`, `browser-plugin`, `hash-plugin`, `logger-plugin`, `persistent-params-plugin`, `ssr-data-plugin`, `rsc-server-plugin`, `lifecycle-plugin`, `preload-plugin`, `memory-plugin`, `navigation-plugin`, `validation-plugin`, `search-schema-plugin`, `route-utils`, `logger`
**Internal packages** (bundled into consumers, not on npm): `route-tree`, `path-matcher`, `search-params`, `type-guards`, `event-emitter`
**Shared sources** (bundled via per-package `src/*` symlinks; `shared/` is a minimal workspace entry with no source files of its own, only a `package.json` declaring workspace devDeps for transitive resolution): `shared/dom-utils`, `shared/browser-env`, `shared/ssr`
## Package Dependencies
```mermaid
graph TD
subgraph standalone [Standalone — zero deps]
PM[path-matcher]
SP[search-params]
EE[event-emitter]
FSM["fsm"]
LOG["logger"]
TYPES["core-types"]
end
subgraph internal [Internal packages]
TG[type-guards] -->|dep| TYPES
RT[route-tree] -->|dep| PM
RT -->|dep| SP
end
subgraph core [Core]
CORE["core"]
end
CORE -->|dep| TYPES
CORE -->|dep| LOG
CORE -->|dep| FSM
CORE -.->|bundles| RT
CORE -.->|bundles| TG
CORE -.->|bundles| EE
subgraph consumers [Consumer packages]
BP["browser-plugin"]
HP["hash-plugin"]
SOURCES["sources"]
REACT["react"]
RX["rx"]
LP["logger-plugin"]
PPP["persistent-params-plugin"]
NP["navigation-plugin"]
ROUTEUTILS["route-utils"]
end
BROWSERENV["shared/browser-env
(shared sources)"]
DOMUTILS["shared/dom-utils
(shared sources)"]
SSRSHARED["shared/ssr
(shared sources)"]
BP -->|dep| CORE
BP -->|dep| LOG
BP -.->|bundles| TG
BP -.->|symlink| BROWSERENV
HP -->|dep| CORE
HP -.->|bundles| TG
HP -.->|symlink| BROWSERENV
NP -->|dep| CORE
NP -.->|bundles| TG
NP -.->|symlink| BROWSERENV
LP -->|dep| CORE
LP -->|dep| LOG
SOURCES -->|dep| ROUTEUTILS
SOURCES -->|dep| CORE
REACT["react
(main + /legacy)"]
REACT -->|dep| CORE
REACT -->|dep| SOURCES
REACT -->|dep| ROUTEUTILS
REACT -.->|symlink| DOMUTILS
PREACT["preact"]
PREACT -->|dep| CORE
PREACT -->|dep| SOURCES
PREACT -->|dep| ROUTEUTILS
PREACT -.->|symlink| DOMUTILS
SOLID["solid"]
SOLID -->|dep| CORE
SOLID -->|dep| SOURCES
SOLID -->|dep| ROUTEUTILS
SOLID -.->|symlink| DOMUTILS
VUE["vue"]
VUE -->|dep| CORE
VUE -->|dep| SOURCES
VUE -->|dep| ROUTEUTILS
VUE -.->|symlink| DOMUTILS
SVELTE["svelte"]
SVELTE -->|dep| CORE
SVELTE -->|dep| SOURCES
SVELTE -->|dep| ROUTEUTILS
SVELTE -.->|symlink| DOMUTILS
ANGULAR["angular"]
ANGULAR -->|dep| CORE
ANGULAR -->|dep| SOURCES
ANGULAR -->|dep| ROUTEUTILS
ANGULAR -.->|copy| DOMUTILS
RX -->|dep| CORE
PPP -->|dep| CORE
PPP -.->|bundles| TG
SDP["ssr-data-plugin"]
SDP -->|dep| CORE
SDP -.->|symlink| SSRSHARED
RSP["rsc-server-plugin"]
RSP -->|dep| CORE
RSP -.->|symlink| SSRSHARED
LCP["lifecycle-plugin"]
LCP -->|dep| CORE
PLP["preload-plugin"]
PLP -->|dep| CORE
MP["memory-plugin"]
MP -->|dep| CORE
VP["validation-plugin"]
VP -->|dep| CORE
SSP["search-schema-plugin"]
SSP -->|dep| CORE
ROUTEUTILS -->|dep| TYPES
```
Solid arrows = runtime `dependencies`. Dashed arrows = bundled at build time (consumer's bundle includes the internal package). The `angular` adapter uses a git-tracked **copy** of `shared/dom-utils/` (not a symlink) because ng-packagr does not follow symlinks the same way tsdown does — `prebundle` re-materializes the copy before every build.
## Core Architecture
The `core` package uses a **facade + namespaces** pattern:
```
Router.ts (facade) ─────────────────────────────────────────────────
│
├── RouterFSM — finite state machine (lifecycle + navigation state)
│
├── RoutesNamespace — route tree, path operations, forwarding
├── StateNamespace — current/previous state storage
├── NavigationNamespace — navigate(), transition pipeline
├── OptionsNamespace — router configuration
├── DependenciesStore — dependency injection container (plain store)
├── EventBusNamespace — FSM + EventEmitter encapsulation, events, subscribe
├── PluginsNamespace — plugin lifecycle management
├── RouteLifecycleNamespace — canActivate/canDeactivate guards
└── RouterLifecycleNamespace — start/stop operations
api/ (standalone functions — tree-shakeable, access router via WeakMap)
├── getRoutesApi(router) — route CRUD
├── getDependenciesApi(router) — dependency CRUD
├── getLifecycleApi(router) — guard management
├── getPluginApi(router) — plugin infrastructure, interception, router extension
└── cloneRouter(router, deps) — SSR cloning
wiring/ (construction-time, Builder+Director pattern)
├── RouterWiringBuilder — namespace dependency wiring
└── wireRouter — calls wire methods in correct order
```
Router.ts is a thin facade — validates inputs and delegates to namespaces. All business logic lives in namespaces. Standalone API functions in `api/` access router internals via a `WeakMap` registry — this enables tree-shaking.
## Router FSM
All router lifecycle and navigation state is managed by a single finite state machine:
```mermaid
stateDiagram-v2
[*] --> IDLE
IDLE --> STARTING : START
IDLE --> DISPOSED : DISPOSE
STARTING --> READY : STARTED
STARTING --> IDLE : FAIL
STARTING --> DISPOSED : DISPOSE
READY --> TRANSITION_STARTED : NAVIGATE
READY --> READY : FAIL
READY --> IDLE : STOP
READY --> DISPOSED : DISPOSE
TRANSITION_STARTED --> TRANSITION_STARTED : NAVIGATE
TRANSITION_STARTED --> LEAVE_APPROVED : LEAVE_APPROVE
TRANSITION_STARTED --> READY : CANCEL
TRANSITION_STARTED --> READY : FAIL
TRANSITION_STARTED --> DISPOSED : DISPOSE
LEAVE_APPROVED --> READY : COMPLETE
LEAVE_APPROVED --> READY : CANCEL
LEAVE_APPROVED --> READY : FAIL
LEAVE_APPROVED --> TRANSITION_STARTED : NAVIGATE
LEAVE_APPROVED --> DISPOSED : DISPOSE
DISPOSED --> [*]
```
`DISPOSE` is wired from every non-DISPOSED state so `router.dispose()` always settles the FSM at `DISPOSED`. For healthy flows the facade still orchestrates cleanup through `IDLE` (`STOP` → `IDLE` → `DISPOSE`); the direct transitions are a safety net for cases where the FSM cannot be returned to `IDLE` first (e.g. `dispose()` mid-`STARTING` after a start-pipeline throw).
| State | Description |
| -------------------- | ----------------------------------------------------- |
| `IDLE` | Router not started or stopped |
| `STARTING` | Initializing (synchronous window before first await) |
| `READY` | Ready for navigation |
| `TRANSITION_STARTED` | Navigation in progress |
| `LEAVE_APPROVED` | Deactivation guards passed, activation guards pending |
| `DISPOSED` | Terminal state, no transitions out |
FSM events trigger observable emissions through two paths:
**Via `fsm.on(from, event, action)`** — events that go through the FSM's `send()` dispatch:
- `STARTED` → `emitRouterStart()`
- `STOP` → `emitRouterStop()`
- `CANCEL` (from `TRANSITION_STARTED` or `LEAVE_APPROVED`) → `emitTransitionCancel()`
- `FAIL` (from any state) → `emitTransitionError()`
**Via direct `forceState()` + emit** — hot-path navigation transitions bypass FSM dispatch for performance (no Map lookup, no action call); the emit follows the state transition in `EventBusNamespace.send*()` helpers:
- `NAVIGATE` (`sendNavigate`) → `forceState(TRANSITION_STARTED)` + `emitTransitionStart()`
- `LEAVE_APPROVE` (`sendLeaveApprove`) → `forceState(LEAVE_APPROVED)` + `emitTransitionLeaveApprove()`
- `COMPLETE` (`sendComplete`) → `forceState(READY)` + `emitTransitionSuccess()`
### Route-tree mutation channel — `TREE_CHANGED` (orthogonal to the FSM)
The seven events above are all about **transitions** (FSM state changes). A separate, **non-FSM** channel signals **structural route-tree mutations** (`add` / `remove` / `update` / `replace` / `clear` via `getRoutesApi`). It reuses the same `EventEmitter` through an **internal-only** key — `TREE_CHANGED` lives in `RouterEventMap` but **not** in the public `EventName` union / `events.*` registry / `Plugin` interface — and is observed only via `getRoutesApi(router).subscribeChanges(handler)`:
- **Post-commit, fire-and-forget** — emitted from the five `getRoutesApi` wrappers after the atomic commit, never from the shared internals that `dispose()`/`cloneRouter()`/`setRootPath()` reuse, so teardown and cloning stay silent.
- **Discriminated payload** (`TreeChangedEvent`, keyed by `op`); `update` emits only on structural fields (guard-only patches are silent).
- Depth tracking (`maxEventDepth`) and per-listener error isolation come for free from the shared emitter; `RecursionDepthError` is the one error that propagates to the CRUD caller.
Tree mutations are an **infrastructural** concern (DevTools, microfrontend coordinators, file-routes watch, caches keyed by route name), not an app-level event — there is deliberately no `router.subscribeTree()` facade and no `addEventListener` path. See [packages/core/CLAUDE.md](packages/core/CLAUDE.md) for the consumer pattern.
## Navigation Pipeline
All navigation methods return `Promise`. The pipeline uses **optimistic sync execution** — guards run synchronously until one returns a Promise, then switches to the async path.
```mermaid
flowchart TD
NAV["router.navigate(name, params, options)"] --> BUILD
BUILD["buildNavigateState()
forwardState + buildPath + makeState"] --> DEACTIVATE
DEACTIVATE["Deactivation guards
inner→outer"]
DEACTIVATE --> LEAVE["[LEAVE_APPROVED]
emit TRANSITION_LEAVE_APPROVE
subscribeLeave() callbacks"]
LEAVE --> ACTIVATE["Activation guards
outer→inner"]
ACTIVATE -->|all guards returned boolean| SYNC
ACTIVATE -->|a guard returned Promise| ASYNC
SYNC["Complete inline
no await, controller released (not aborted)"]
ASYNC["await remaining guards
under AbortController"]
SYNC --> COMPLETE
ASYNC --> COMPLETE
COMPLETE["completeTransition()
setState + freeze → FSM READY"]
COMPLETE --> RESOLVE["Promise‹State› resolves"]
DEACTIVATE -.->|error at any step| ERR["emitTransitionError()
Promise rejects with RouterError"]
ACTIVATE -.->|error at any step| ERR
ASYNC -.->|abort / cancel| ERR
```
On error at any step: `emitTransitionError()`, Promise rejects with `RouterError`.
**`navigateToNotFound()`** bypasses this pipeline entirely — sets state directly and emits only `TRANSITION_SUCCESS` (no guards, no AbortController, no `TRANSITION_START`). Always uses `replace: true`.
**Cancellation sources:** external AbortController (`opts.signal`), concurrent navigation (aborts previous), `stop()`, `dispose()`. The internal AbortController is created **synchronously** whenever the navigation has guards or `subscribeLeave` listeners (they receive `signal` before it is known whether they run async); only the pure hot path — no guards, no leave listeners — allocates none. It is aborted solely on cancellation/error, never on success (#722).
## Extension Points
| Extension | Purpose | Scope | Can Block |
| ----------- | ------------------------------ | --------- | --------- |
| **Guards** | Route access control | Per-route | Yes |
| **Plugins** | React to events, extend router | Global | No |
### Plugin Interception
Plugins intercept router methods via `addInterceptor()` on `PluginApi`. `InterceptableMethodMap` is fixed at compile time (`core-types/src/api.ts`):
| Method | Signature | Used by |
| -------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| `start` | `(path?: string) => Promise` | browser-plugin, hash-plugin, navigation-plugin (via `createStartInterceptor` from `shared/browser-env`); ssr-data-plugin, rsc-server-plugin (via `createSsrLoaderPlugin` from `shared/ssr`) |
| `buildPath` | `(route: string, params?: Params) => string` | persistent-params-plugin |
| `forwardState` | `(routeName: string, routeParams: Params) => SimpleState` | persistent-params-plugin, search-schema-plugin |
Multiple interceptors per method execute in **LIFO** order (last-registered wraps first). Each receives `next` (original or previously-wrapped function) plus the method's arguments. Applied via `createInterceptable()` in `RouterInternals`.
### Router Extension
Plugins extend the router instance with new properties via `extendRouter()` on `PluginApi`. Throws `RouterError(PLUGIN_CONFLICT)` if any key already exists (atomic validation). Extensions are tracked in `RouterInternals.routerExtensions` and cleaned up on unsubscribe or `dispose()`.
### Context Namespace Claims
Plugins publish per-route data via `claimContextNamespace()` on `PluginApi`. Each plugin claims a unique namespace key at registration time (O(1) collision detection via `Set`), receives a `{ write, release }` object, and publishes data to `state.context.` from lifecycle hooks. Mirrors the `extendRouter()` ownership model: closure-based tracking, manual `release()` in `teardown()`, dispose safety net for orphaned claims. Six plugins use this — 8 claims in total:
| Plugin | Namespace key(s) | Published fields (examples) |
| ------------------------ | ---------------------- | ---------------------------------------------------------- |
| browser-plugin | `browser` + `url` | source, fullUrl |
| navigation-plugin | `navigation` | direction, sourceElement |
| memory-plugin | `memory` | direction, historyIndex |
| persistent-params-plugin | `persistentParams` | persisted query param snapshot |
| ssr-data-plugin | `data` | per-route loader result (via `createSsrLoaderPlugin`) |
| rsc-server-plugin | `rsc` + `rscAction` | per-route ReactNode (via `createSsrLoaderPlugin`) + server-action results |
### Validator Slot
`@real-router/validation-plugin` uses a unique extension mechanism — not interceptors, not event listeners, but a **nullable validator slot** in `RouterInternals`:
```typescript
ctx.validator?.routes.validateBuildPathArgs(route); // no-op when null
```
The slot is typed as `RouterValidator | null`. The plugin sets it on registration, clears it on teardown. All core call sites use optional chaining — zero overhead when absent.
## Invariants
These are deliberately designed constraints. Violating them will break the system in subtle ways.
### State & Immutability
- **All `State` objects are deeply frozen** (`Object.freeze`). Never mutate — always create new.
- **Router options are immutable** — deep-frozen at construction time.
### FSM & Events
- **All transition events are consequences of FSM transitions** — never manual calls. No boolean flags (`#started`, `#active`, `#navigating` — all removed). (The `TREE_CHANGED` channel is the one deliberate exception — it is orthogonal to the FSM, emitted by `getRoutesApi` mutations, not by state changes.)
- **`TREE_CHANGED` is internal-only and wrapper-emitted** — never in the public `EventName` union, and emitted strictly from the five `getRoutesApi` CRUD wrappers, never from shared internals (`adoptRouteArtifacts`/`commitTreeChanges`/`resetStore`). This keeps `dispose()`, `cloneRouter()`, and `setRootPath()` from emitting it.
- **`dispose()` is terminal** — DISPOSED state has no outbound transitions. All mutating methods throw `RouterError(ROUTER_DISPOSED)` after disposal.
### Guards & Plugins
- **Guards return `boolean | Promise` only** — no redirects, no state modification, no `State` return.
- **Plugins are observers** — they react to events but cannot block or modify the transition pipeline.
- **Guard execution order is fixed**: deactivation innermost → outermost, then activation outermost → innermost.
- **`navigateToNotFound()` bypasses both** — no guards run, plugins only see `onTransitionSuccess`.
### Navigation
- **Concurrent navigation cancels previous** — the previous internal AbortController is aborted, promise rejects with `TRANSITION_CANCELLED`.
- **Navigating FROM `UNKNOWN_ROUTE` auto-forces `replace: true`** — prevents browser history pollution with 404 entries.
- **Fire-and-forget is safe** — `navigate()`, `navigateToDefault()`, and the `navigateToState()` plugin primitive internally suppress unhandled rejections for expected errors (`SAME_STATES`, `TRANSITION_CANCELLED`, `ROUTER_NOT_STARTED`, `ROUTE_NOT_FOUND`, `CANNOT_ACTIVATE`, `CANNOT_DEACTIVATE`). Guard blocks are an expected outcome, not an internal error — `await` the call (or use an `onTransitionError` plugin) to observe a guard rejection.
### Packages
- **Internal packages are never imported by end users** — they are bundled into consumer packages at build time.
- **`core` never depends on browser APIs** — platform-agnostic. The `start(path)` method requires a path; browser-plugin makes it optional by injecting `browser.getLocation()` via interceptor.
## Boundaries
### Layer Rules
```
┌──────────────────────────────────────────────────────────────────┐
│ Consumer Packages │
├──────────────────────────────────────────────────────────────────┤
│ react │ preact │ solid │ vue │ svelte │ angular │ browser-plugin │
├──────────────────────────────────────────────────────────────────┤
│ Core │
├──────────────────────────────────────────────────────────────────┤
│ core + core-types │
├──────────────────────────────────────────────────────────────────┤
│ Foundation (internal) │
├──────────────────────────────────────────────────────────────────┤
│ route-tree │ path-matcher │ search-params │ event-emitter │ ... │
└──────────────────────────────────────────────────────────────────┘
```
**ALLOWED:**
- Consumer packages depend on `core` and `core-types`
- Consumer packages bundle internal packages as needed (`type-guards`)
- Consumer packages import shared sources via git-tracked symlinks (`src/dom-utils` → `shared/dom-utils`, `src/browser-env` → `shared/browser-env`, `src/shared-ssr` → `shared/ssr`)
- Foundation packages depend on each other (`route-tree` → `path-matcher`, `search-params`)
- `shared/browser-env` is the **only** location that touches `window`, `history`, `addEventListener` (enforced by convention, not by package boundary)
**FORBIDDEN:**
- Foundation packages must not depend on `core`
- Exception: `shared/browser-env` files import `Router`, `PluginApi`, `RouterError` types from `@real-router/core` — resolved via the consumer's `node_modules` when accessed through the symlink
- Consumer packages must not depend on each other's internals
- No package may bypass the plugin system to mutate router state directly
- No circular dependencies between packages
### Extension Boundaries
- Plugins extend the router **only** via `extendRouter()` and publish per-route data **only** via `claimContextNamespace()` — never by mutating the router prototype or internals
- Interceptors wrap methods **only** from `InterceptableMethodMap` — the set is fixed at compile time
- Guards registered via route config are tracked separately from guards registered via `addActivateGuard()` — `replace()` clears only definition-sourced guards
- **`/ssr` subpath isolation** — every adapter ships a distinct `@real-router/{adapter}/ssr` entry-point for server-only types and components (``, ``, ``, ``, ``, `useDeferred`). The main entry never re-exports SSR helpers; the `/ssr` entry never depends on history/navigation plugins. This guarantees client bundles cannot accidentally pull server-only types, enables RSC `react-server` export-condition composition, and makes ESLint rules like "no `*/ssr` import in client component" mechanically enforceable. See [IMPLEMENTATION_NOTES.md › Subpath isolation for SSR/RSC concerns](IMPLEMENTATION_NOTES.md)
## Cross-Cutting Concerns
### Error Handling
All navigation errors are `RouterError` instances with typed `code` from `errorCodes`. Common rejections (`SAME_STATES`, `ROUTER_NOT_STARTED`, `ROUTE_NOT_FOUND`) return **pre-allocated** `Promise.reject()` instances — zero allocation per rejection.
### Testing Strategy
- **100% code coverage** enforced in CI across all packages
- **Property-based testing** — 2000+ property test cases via fast-check across 31 packages: URL encoding, parameter serialization, route tree operations, reactive subscription ordering, canonical params, link helpers
- **Stress testing** — 700+ stress test cases across 183 `.stress.ts` files in 14 packages (core, plugins, all 6 framework adapters): concurrent navigations, guard removal mid-execution, route CRUD under load, heap snapshots confirming zero memory leaks, mount/unmount lifecycle, subscription fanout granularity, full SPA simulations
- **Playwright e2e testing** — 1800+ test cases across 330+ spec files (100+ playwright projects) covering all 6 framework adapters (React, Preact, Solid, Vue, Svelte, Angular). Tests verify real browser behavior: navigation, guards, data loading, error handling, hash routing, nested routes, dynamic routes, async guards, SSR/streaming/SSG/RSC pipelines, animations. Turbo-cached via `test:e2e` task.
- **Mutation testing** (Stryker) validates test suite quality beyond line coverage
- **`lint:e2e`** pre-commit check — verifies every example with `playwright.config.ts` has at least one spec file
### Build System
pnpm monorepo with Turborepo for task orchestration. Dual ESM/CJS output via tsdown (Rolldown-based bundler). Internal packages are bundled into consumers — not separate npm artifacts. `workspace:^` protocol for inter-package dependencies. All turbo tasks use `outputLogs: "errors-only"` — silent on success, full output on failure. `build:verbose`/`test:verbose` scripts override to full output for debugging. Turbo `test:e2e` task caches Playwright results based on source + spec + config inputs.
### Performance Hot Path
The navigate path is heavily optimized:
- **Optimistic sync execution** — no `await`/microtask on the sync path; AbortController allocated only when guards or `subscribeLeave` listeners exist (none on the pure hot path)
- **FSM `forceState()`** — bypasses `send()` dispatch for NAVIGATE/COMPLETE transitions
- **EventEmitter explicit params** — `emit(name, a?, b?, c?, d?)` avoids V8 rest-param array allocation
- **Cached error rejections** — pre-allocated for common error codes
- **Single-pass freeze** — `freezeStateInPlace` in one recursive traversal
## See Also
- `packages/core/CLAUDE.md` — detailed core internals for AI agents
- `IMPLEMENTATION_NOTES.md` — infrastructure and tooling decisions
- [Wiki](https://github.com/greydragon888/real-router/wiki) — full user documentation
- [Glossary](https://github.com/greydragon888/real-router/wiki/glossary) — project-specific terminology