API / @aihu/router

@aihu/router

App & routing

File-based router for the aihu meta-framework.

version
0.5.0
exports
49
values
26
types
23
01

__router_registerAfterGuard

function
function __router_registerAfterGuard( fn: Parameters<Router['registerAfterGuard']>[0], ): () => void
02

__router_registerBeforeGuard

function
function __router_registerBeforeGuard( fn: Parameters<Router['registerBeforeGuard']>[0], ): () => void
03

bindRouteSignalWriter

function
function bindRouteSignalWriter( value: RouteContextValue, write: (m: MatchResult | null) => void, ): void
04

componentTagFor

function
function componentTagFor(name: string): string

Normalize a component name to its custom-element tag (O1b).

05

composeRouterMiddleware

function
function composeRouterMiddleware(...middlewares: RouterMiddleware[]): RouterMiddleware
06

createPrefetcher

function
function createPrefetcher(mode: PrefetchMode): { attach: (a: HTMLAnchorElement, getMatch: () => MatchResult | null) => void detach: (a: HTMLAnchorElement) => void }

Build the DOM hooks needed for a prefetch-enabled `<a>`.

07

createRouter

function
function createRouter(routes: RouteDefinition[]): Router
08

createRouteSignal

function
function 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()`.

09

createServerRouter

function
function createServerRouter( routes: RouteDefinition[], options?: ServerRouterOptions, ): ServerRouter

Construct a router with the server-side `handle(req)` request handler wired in.

10

defineRouterMiddleware

function
function defineRouterMiddleware(fn: RouterMiddleware): RouterMiddleware
11

escapeForJsSource

function
function escapeForJsSource(json: string): string
12

genSC

function
function 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> = [], ): string

Generate `virtual:aihu-server-components` — the SERVER-side child registry source, for `output: 'ssr'` builds.

13

jsSourceLiteral

function
function jsSourceLiteral(value: unknown): string

JSON-encode `value` as a JavaScript literal safe to concatenate into generated source.

14

layoutTagFor

function
function layoutTagFor(name: string): string

Runtime layout namespace convention (v0.7.5).

15function
async function navigate( href: string, opts: NavigateOptions = {}, ): Promise<'navigated' | 'cancelled' | 'no-router' | 'no-match'>

Programmatically navigate to `href` within the active router.

16

provideRouteContext

function
function provideRouteContext(value: RouteContextValue): void

Provide the route context (used by `<router>` SFC).

17

readAihuComponentTag

function
function readAihuComponentTag(f: string): string

O1b: Resolve the custom-element tag a component file registers under.

18

readRouteSidecar

function
function readRouteSidecar(f: string): RouteSidecar | null

v0.6.3: Read sibling .route.json sidecar.

19

scanComponents

function
function 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.

20

scanLayouts

function
function scanLayouts(d: string): LayoutMap

v0.6.8: Scan layouts dir for .aihu files.

21

scanPages

function
function scanPages(root: string, pd: string): MiddlewareScan
22

useRoute

function
function useRoute(): MatchResult | null

Read the current matched route from the nearest `<router>` context.

23

useRouter

function
function useRouter(): Router | null

Get the active `Router` instance from the nearest `<router>` context.

24

viteRouterPlugin

function
function viteRouterPlugin(opts?: RouterPluginOptions): VitePlugin
25

RouteContext

const
const RouteContext

Context token for the active routing context.

26

viteRouterIntegration

const
const viteRouterIntegration
27

LayoutMap

interface
interface LayoutMap {
  [name: string]: string
}

Layout name → absolute file path (v0.6.8).

28

LayoutModuleLike

interface
interface 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
}
29

MiddlewareScan

interfaceagent
interface 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.

30interface
interface NavigateOptions {
  /** Use `history.replaceState` instead of `pushState`. */
  replace?: boolean
  /** Wrap navigation in `document.startViewTransition()` when supported. */
  viewTransitions?: boolean
}
31

RouteContextValue

interface
interface 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`.

32

RouteMatchContext

interface
interface RouteMatchContext {
  url: URL
  params: Record<string, string>
  route: RouteDefinition
  signal: AbortSignal
  env?: Record<string, string>
}
33

RouterPluginOptions

interface
interface 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[]
}
34

RouteSidecar

interface
interface 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).

35

ServerRouterOptions

interfaceagent
interface 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.

36

AfterGuard

type
type AfterGuard = (to: MatchResult, from: MatchResult | null) => void

After-navigation callback.

37

BeforeGuard

type
type BeforeGuard = (
  to: MatchResult,
  from: MatchResult | null,
  next: NextFn,
) => void | Promise<void>

Before-navigation guard.

38

MatchResult

type
type MatchResult = {
  route: RouteDefinition
  params: Record<string, string>
  /** The matched pathname, e.g. '/posts/hello' */
  pathname: string
}
39

NextFn

type
type NextFn = (decision?: void | false | string) => void

Navigation guard `next` callback (RFC-A5-015).

40

PlatformContext

type
type PlatformContext
41

PrefetchMode

type
type PrefetchMode = 'none' | 'hover' | 'visible'
42

RouteDefinition

type
type 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
}
43

RouteHead

type
type 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).

44

RouteModule

type
type 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' }
}
45

Router

type
type 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
}
46

RouterMiddleware

type
type RouterMiddleware = (
  ctx: RouteMatchContext,
  next: () => Promise<RouterResult>,
) => Promise<RouterResult>
47

RouterResult

type
type RouterResult =
  | { kind: 'continue' }
  | { kind: 'redirect'; location: string; status?: number }
  | { kind: 'cancel'; status?: number; body?: string }
48

RouteSegment

type
type RouteSegment =
  | { kind: 'static'; path: string }
  | { kind: 'param'; name: string }
  | { kind: 'catchall' }
49

ServerRouter

type
type 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>
}