File-based router for the aihu meta-framework.
__router_registerAfterGuardfunction __router_registerAfterGuard( fn: Parameters<Router['registerAfterGuard']>[0], ): () => void__router_registerBeforeGuardfunction __router_registerBeforeGuard( fn: Parameters<Router['registerBeforeGuard']>[0], ): () => voidbindRouteSignalWriterfunction bindRouteSignalWriter( value: RouteContextValue, write: (m: MatchResult | null) => void, ): voidcomponentTagForfunction componentTagFor(name: string): stringNormalize a component name to its custom-element tag (O1b).
composeRouterMiddlewarefunction composeRouterMiddleware(...middlewares: RouterMiddleware[]): RouterMiddlewarecreatePrefetcherfunction createPrefetcher(mode: PrefetchMode): { attach: (a: HTMLAnchorElement, getMatch: () => MatchResult | null) => void detach: (a: HTMLAnchorElement) => void }Build the DOM hooks needed for a prefetch-enabled `<a>`.
createRouterfunction createRouter(routes: RouteDefinition[]): RoutercreateRouteSignalfunction createRouteSignal( router: Router, initialPathname?: string, ): { read: () => MatchResult | null write: (m: MatchResult | null) => void dispose: () => void }Wrap a `Router` in a reactive `Signal<MatchResult | null>` that updates on `popstate` events and on calls to `navigate()`.
createServerRouterfunction createServerRouter( routes: RouteDefinition[], options?: ServerRouterOptions, ): ServerRouterConstruct a router with the server-side `handle(req)` request handler wired in.
defineRouterMiddlewarefunction defineRouterMiddleware(fn: RouterMiddleware): RouterMiddlewareescapeForJsSourcefunction escapeForJsSource(json: string): stringgenSCfunction genSC( pageFiles: ReadonlyArray<string>, componentsDir: string, deriveChildTags?: (source: string, id: string) => string[], /** * Layout files, walked as ROOTS alongside the pages. * * Layouts are where a site's nav, header and footer live, and * `@aihu/router/server` now composes them into every live SSR response. * Rooting the walk at pages alone excluded every component a layout * references, so the shell would server-render with all of them as empty * elements — this module's own failure mode, relocated into the part of the * page that appears on EVERY route. * * Defaulted to empty so the pre-layout call shape keeps its exact behaviour. */ layoutFiles: ReadonlyArray<string> = [], ): stringGenerate `virtual:aihu-server-components` — the SERVER-side child registry source, for `output: 'ssr'` builds.
jsSourceLiteralfunction jsSourceLiteral(value: unknown): stringJSON-encode `value` as a JavaScript literal safe to concatenate into generated source.
layoutTagForfunction layoutTagFor(name: string): stringRuntime layout namespace convention (v0.7.5).
navigateasync function navigate( href: string, opts: NavigateOptions = {}, ): Promise<'navigated' | 'cancelled' | 'no-router' | 'no-match'>Programmatically navigate to `href` within the active router.
provideRouteContextfunction provideRouteContext(value: RouteContextValue): voidProvide the route context (used by `<router>` SFC).
readAihuComponentTagfunction readAihuComponentTag(f: string): stringO1b: Resolve the custom-element tag a component file registers under.
readRouteSidecarfunction readRouteSidecar(f: string): RouteSidecar | nullv0.6.3: Read sibling .route.json sidecar.
scanComponentsfunction scanComponents( d: string, listDir: ( dir: string, ) => ReadonlyArray<{ name: string; isFile(): boolean; isDirectory(): boolean }> = (dir) => readdirSync(dir, { withFileTypes: true }), ): Record<string, string>O1b: Recursively scan a components dir for `.aihu` files, mapping each file's normalized custom-element tag → absolute POSIX path.
scanLayoutsfunction scanLayouts(d: string): LayoutMapv0.6.8: Scan layouts dir for .aihu files.
scanPagesfunction scanPages(root: string, pd: string): MiddlewareScanuseRoutefunction useRoute(): MatchResult | nullRead the current matched route from the nearest `<router>` context.
useRouterfunction useRouter(): Router | nullGet the active `Router` instance from the nearest `<router>` context.
viteRouterPluginfunction viteRouterPlugin(opts?: RouterPluginOptions): VitePluginRouteContextconst RouteContextContext token for the active routing context.
viteRouterIntegrationconst viteRouterIntegrationLayoutMapinterface LayoutMap {
[name: string]: string
}Layout name → absolute file path (v0.6.8).
LayoutModuleLikeinterface LayoutModuleLike {
/** The renderable — `() => arbor-tree` or `{ toHtml() }`. */
readonly default?: unknown
/** LDF §10 step 3 — the compiler-assigned `data-a` scope id, for first-paint CSS. */
readonly __aihu_light_scope__?: string
/** The layout's registered custom-element tag, so SSR wraps what the client builds. */
readonly __aihu_tag__?: string
}MiddlewareScaninterface MiddlewareScan {
/** Route files (non-underscore page files). */
routes: string[]
/**
* Map from directory (absolute path) to its `_middleware.(ts|js)` file.
* When the runtime composes a route, all middleware files from the route
* file's ancestor directories (innermost first) should be applied.
*/
middlewareByDir: Record<string, string>
}v0.7.2: File-convention middleware discovered alongside routes.
NavigateOptionsinterface NavigateOptions {
/** Use `history.replaceState` instead of `pushState`. */
replace?: boolean
/** Wrap navigation in `document.startViewTransition()` when supported. */
viewTransitions?: boolean
}RouteContextValueinterface RouteContextValue {
router: Router
/** Reactive read accessor for the current match. */
current: () => MatchResult | null
/** @internal — view transitions opt-in (read by `<a>`). */
viewTransitions?: boolean
}Value carried by `RouteContext`.
RouteMatchContextinterface RouteMatchContext {
url: URL
params: Record<string, string>
route: RouteDefinition
signal: AbortSignal
env?: Record<string, string>
}RouterPluginOptionsinterface RouterPluginOptions {
pagesDir?: string
/** Directory to scan for layout files. Default: 'src/layouts' */
layoutsDir?: string
/** Directory to scan for component files. Default: 'src/components' */
componentsDir?: string
/**
* Build-time route-metadata extractor — `@aihu/compiler`'s `compileRouteMeta`.
* When provided, `genR` uses it to recover FULL `@route` metadata
* (`head`/`middleware`/`params`/`ssr`/`layout`) for `.aihu` pages, since the
* stdin compile path writes no `.route.json` sidecar to disk. Wired by
* `@aihu/app` (which pairs the compiler + router plugins). When absent (e.g.
* standalone `viteRouterIntegration` without the compiler), `genR` falls back
* to reading `name`+`layout` from the `@route` block via regex.
*/
compileRouteMeta?: (source: string, id: string) => RouteSidecar | null
/**
* Build-time child-tag derivation — `@aihu/compiler`'s `_deriveChildTags`
* applied to the SERVER-target compile of an SFC. Wired by `@aihu/app`
* (which pairs the compiler + router plugins), exactly like
* `compileRouteMeta` above and for the same reason: the router keeps zero
* compiler dependency.
*
* Consumed ONLY by `genSC` (`virtual:aihu-server-components`). When absent,
* `genSC` cannot prune and falls back to the whole component directory with
* a warning — see its docblock.
*/
deriveChildTags?: (source: string, id: string) => string[]
}RouteSidecarinterface RouteSidecar {
name?: string
middleware?: string[]
ssr?: boolean
layout?: string
/** Declared route param names, e.g. ["slug"]. Emitted by the Rust compiler from $prop declarations. */
params?: string[]
/**
* B2: per-route `<head>` metadata (compiler `head:` block). Omitted entirely
* when a route declares no `head:`. Threaded through to RouteDefinition.head
* and the generated `virtual:aihu-routes` module.
*/
head?: RouteHead
/**
* O1a: normalized custom-element tags this route's template references
* (e.g. `["hn-comment", "user-card"]`). Emitted by the compiler's
* `route.json`; threaded through to RouteDefinition.components so O1c can
* register a route's components on navigation instead of eager imports.
*/
components?: string[]
/**
* GX Phase 3 (#437-GX): the compiled `extract` policy (Phase 1 fan-out —
* always present in a v0.1.12+ `.route.json`, the default recorded, never
* implied by absence). Values stay `unknown` here: the compiler validated
* them (C483), but sidecars can be hand-edited, so consumers normalize
* fail-closed via `@aihu/server`'s `deriveReadPolicy`.
*/
extract?: { read?: unknown; call?: unknown }
/**
* GX Phase 4 (#466): the compiled `data:` declaration — the governed
* resource type binding (`type` keys the provider registry) and the
* declared locked-state `preview:` fields. INTEGRATION SEAM (Builder A):
* the Rust compiler parses the `@route` `data:` block and fans it into
* `.route.json` beside `extract`; this field threads it (sidecar or
* `compileRouteMeta`) into `RouteDefinition.data`, where
* `createServerRouter` normalizes it fail-closed (`normalizeGovernedData`).
*/
data?: { type?: string; preview?: string[] }
}Fields from a .route.json compiler sidecar (v0.6.3).
ServerRouterOptionsinterface ServerRouterOptions {
/**
* The governed registry (`createGovernedRegistry()` from `@aihu/server`).
* Passing it activates the governed pipeline for every route with a
* compiled `data:` declaration, boot-validates the registry against the
* route census (spec §2.3 — a typo is a boot refusal, not a 500 at first
* request), and enables the E3 governed-data endpoint. Pass the SAME
* instance to `AgentServiceOptions.entitlements` so both axes share one
* live meaning per scope (§4.6).
*/
readonly governed?: GovernedRegistry
/**
* Credential material for principal resolution on the SSR path: the
* verifying auth plugin (Bearer JWTs), the anonymous-UA classifier, and a
* host-verified session resolver (cookie path). Same injection posture as
* `AgentServiceOptions.resolveAuth` / `PrincipalGateDeps`.
*/
readonly auth?: GovernedRequestAuth
/**
* §2a — the pre-resolved child-component registry, forwarded to
* `renderToString` as `SsrOptions.children` on BOTH render paths.
*
* Typed as `buildChildRegistry`'s own return type so the intended
* construction is the obvious one:
*
* ```ts
* const children = buildChildRegistry(discovered)
* export default createServerRouter(routes, { children })
* ```
*
* A RESOLVED map, not a loader — `__aihu_schild` runs inside the compiled
* string fast path, which is synchronous, so every module must already be
* in hand before a render begins.
*
* CORRECTED: this used to end "Awaiting belongs at module init, once." It no
* longer does, and could not: module-scope `await` makes `@aihu/app`'s
* generated Worker entry an ESM async module, which deadlocks inside the
* bundler's chunk cycle. `@aihu/app` now resolves the whole registry graph
* once on the first request and memoises it, strictly before `handle()` runs
* — which satisfies this resolved-map contract exactly as module-scope
* resolution did.
*
* Omitting it is byte-identical to not passing it, matching this
* interface's existing contract: a component reference then renders as an
* empty element exactly as it does today.
*
* SCOPE, deliberately stated because the plan text overstated it: this
* closes the forwarding hole in THIS file. It does not, by itself, give any
* shipped adapter non-empty children. `@aihu/adapter-cloudflare` and
* `-vercel` emit their entry as a raw string at `closeBundle`, wire
* `createRequestRouter` rather than this function, and give every route a
* `notFound` placeholder — they render nothing at all today. A consumer
* still needs a way to BUILD this map on the server, which is §2b (a
* server-target virtual module plus a Vite-worker-environment example).
* See §2 of `docs/plans/2026-08-06-ssr-child-followups.md`.
*/
readonly children?: ReadonlyMap<string, ChildModuleLike>
/**
* Resolved layout modules, keyed by the NAME a route's `@route { layout }`
* declares (not by tag, not by file path) — that is the key the compiled
* `RouteDefinition.layout` carries.
*
* ## The divergence this closes
*
* `@aihu/app`'s SSG prerender composes layouts; this file did not, at all
* (`grep -c layout` over it returned 0 before this option existed). So an
* app that looked right prerendered lost its ENTIRE shell — nav, footer,
* grid — the moment the same route was served from a Worker, and nothing
* warned. That is a silent, visible-in-production difference between two
* render paths that are supposed to produce the same document.
*
* The composition RULE itself is not reimplemented here: the outlet splice
* is `@aihu/server`'s `injectIntoOutlet`, which the prerender calls too.
* What this file reproduces is the surrounding SEQUENCE (resolve → render
* shell → probe for a marker → inject, warning and falling back to the bare
* page at each step), because the two paths resolve layout MODULES
* differently and always will: the prerender scans the layouts directory off
* disk with a live Vite SSR loader, while a Worker has no filesystem and
* gets its modules from `virtual:aihu-layouts` inside the bundle.
*
* Omitting it leaves `handle` byte-identical to before — a route with a
* `layout` renders bare, exactly as it did.
*/
readonly layouts?: ReadonlyMap<string, LayoutModuleLike>
}GX Phase 4 (#466, 70-governed-data-access): options for the server router.
AfterGuardtype AfterGuard = (to: MatchResult, from: MatchResult | null) => voidAfter-navigation callback.
BeforeGuardtype BeforeGuard = (
to: MatchResult,
from: MatchResult | null,
next: NextFn,
) => void | Promise<void>Before-navigation guard.
MatchResulttype MatchResult = {
route: RouteDefinition
params: Record<string, string>
/** The matched pathname, e.g. '/posts/hello' */
pathname: string
}NextFntype NextFn = (decision?: void | false | string) => voidNavigation guard `next` callback (RFC-A5-015).
PlatformContexttype PlatformContextPrefetchModetype PrefetchMode = 'none' | 'hover' | 'visible'RouteDefinitiontype RouteDefinition = {
pattern: string
segments: RouteSegment[]
module: () => Promise<RouteModule>
// v0.6.3: fields from .route.json compiler sidecars
name?: string
middleware?: string[]
ssr?: boolean
layout?: string
// B2: per-route <head> metadata from the .route.json `head:` block
head?: RouteHead
/** Normalized custom-element tags this route references; used for route-scoped registration (O1). */
components?: readonly string[]
/**
* GX Phase 3 (#437-GX): the route's compiled `extract` policy from the
* `.route.json` sidecar (`{ read, call }`, the Phase 1 fan-out). Values are
* `unknown` — consumers normalize fail-closed (`deriveReadPolicy` in
* `@aihu/server`). Drives the compliance-tier noindex signal in
* `createServerRouter.handle` and the derived robots/discovery listings in
* `@aihu-plugin/agent-readiness`. Absent on hand-built routes → the
* resolved default (`read: 'agents'`, `call: 'anonymous'`) applies.
*/
extract?: { readonly read?: unknown; readonly call?: unknown }
/**
* GX Phase 4 (#466): the route's compiled `data:` declaration from the
* `.route.json` sidecar — `{ type, preview? }`, naming the governed
* resource type (the provider key) and the fields renderable in the locked
* state. Values are `unknown`: consumers normalize FAIL-CLOSED
* (`normalizeGovernedData` in `@aihu/server` — malformed is a boot
* refusal, never rounded to ungoverned). Presence makes the route governed:
* `createServerRouter.handle` replaces the ungated `mod.loader` path with
* the generated loader (70-governed-data-access §3).
*/
data?: unknown
}RouteHeadtype RouteHead = {
title?: string
description?: string
canonical?: string
og?: {
title?: string
description?: string
image?: string
type?: string
url?: string
}
twitter?: {
card?: string
title?: string
description?: string
image?: string
site?: string
}
jsonld?: unknown
}Per-route `<head>` metadata, threaded from the compiler's `.route.json` sidecar (`head:` block).
RouteModuletype RouteModule = {
default: unknown
/**
* The route's data loader. Either a plain server loader (the shipped,
* ungoverned contract — called with the matched params and, since bindings
* landed, a {@link LoaderContext}), or — GX Phase 4 (#466) — the
* `defineGovernedFetch` escape hatch (structurally typed here so this
* browser-eligible file keeps zero `@aihu/server` imports): a route-local
* provider the generated loader gates. On a `data:`-declared route a PLAIN
* loader is a C486 build error (one data source per route).
*/
loader?:
| ((params: Record<string, string>, ctx: LoaderContext) => Promise<unknown>)
| { readonly _brand: 'DefinedGovernedFetch' }
}Routertype Router = {
match(pathname: string): MatchResult | null
/**
* Register a guard that runs before each navigation. Multiple guards run
* in registration order. The first `next(false)` cancels navigation; the
* first `next('/x')` redirects. Returns a dispose fn.
*/
registerBeforeGuard(fn: BeforeGuard): () => void
/**
* Register a callback that runs after each navigation completes (after the
* outlet has updated). Returns a dispose fn.
*/
registerAfterGuard(fn: AfterGuard): () => void
/**
* Run the registered before-guard chain. Returns the final navigation
* decision: 'continue', 'cancel', or { redirect: string }.
* @internal — used by `<a>` and `<navigate>`.
*/
runBeforeGuards(
to: MatchResult,
from: MatchResult | null,
): Promise<'continue' | 'cancel' | { redirect: string }>
/**
* Run the registered after-guard chain.
* @internal — used by `<router>`.
*/
runAfterGuards(to: MatchResult, from: MatchResult | null): void
}RouterMiddlewaretype RouterMiddleware = (
ctx: RouteMatchContext,
next: () => Promise<RouterResult>,
) => Promise<RouterResult>RouterResulttype RouterResult =
| { kind: 'continue' }
| { kind: 'redirect'; location: string; status?: number }
| { kind: 'cancel'; status?: number; body?: string }RouteSegmenttype RouteSegment =
| { kind: 'static'; path: string }
| { kind: 'param'; name: string }
| { kind: 'catchall' }ServerRoutertype ServerRouter = Router & {
/**
* Serve one request.
*
* `platform` is the host runtime's per-request ambient state — on Cloudflare
* Workers, `fetch`'s `env` (KV, D1, R2, Durable Object stubs, secrets) and
* `ctx` (`waitUntil`); on another host, whatever that host's adapter chooses
* to pass. The framework NEVER reads inside it; it forwards it, unread and
* untyped, to route loaders, the governed provider, the live entitlement
* resolver and the session resolver.
*
* OMITTING IT IS BYTE-IDENTICAL to the pre-bindings behaviour: every
* consumer of `platform` receives `undefined` and every one of them treats
* that as "the host offered none", which is the state they were all in
* before this parameter existed.
*/
handle(req: Request, platform?: PlatformContext): Promise<Response>
}