/** * Route manifest - the fs-free heart of file-based routing. Maps route file paths to nifra * router patterns + their nested layout chain. `discoverRoutes` (in `@nifrajs/web/fs`) scans * the filesystem and feeds `buildManifest`; everything here is pure logic, so it stays * portable (no fs, no DOM) and fully unit-testable. Edge deploys pre-build the manifest. */ import type { StandardSchemaV1 } from "@nifrajs/core/server"; import type { BoundaryDescriptor, BoundaryRegistration } from "./boundary.js"; /** Context passed to a route `loader`. The `api` + `env` are injected by `createWebApp` and typed * per-route via `@nifrajs/client`'s `LoaderArgs` (here they are opaque to the agnostic core). */ export interface LoaderContext { readonly params: Record; readonly request: Request; /** Alias of {@link request} - mirrors a route handler's `c.req` so the same name works in both. */ readonly req: Request; readonly api: unknown; /** Platform bindings forwarded from the request `c.env` (Workers env/KV/D1). Opaque here. */ readonly env: unknown; /** `true` when the request carries a valid draft/preview cookie (only when `createWebApp` is given a * `draftSecret`; otherwise always `false`). Branch on it to load unpublished content for editors. */ readonly draft: boolean; /** The URL search params, validated against the route's `searchSchema` (a Standard Schema) when it * declares one - failing closed to the schema's defaults - else the raw parsed query. Typed per-route * via `@nifrajs/client`'s `LoaderArgs`. */ readonly search: Record; } /** A route's optional data loader: params/request in, data out. */ export type Loader = (ctx: LoaderContext) => unknown | Promise; /** * A route's optional mutation, run on POST. Shares the loader context (params/request/api); * read the form/JSON body off `request`. Returns either a control-flow value (a `redirect()`, a * `status(...)` render, or a hand-rolled `Response` - all passed straight through) or data, * surfaced to the page component as `actionData`. */ export type Action = (ctx: LoaderContext) => unknown | Promise; /** The body a client action may prepare for the server action. The server must validate it again. */ export type ClientRequestBody = NonNullable; /** Safe, client-visible context for a client loader. It intentionally exposes no Request or headers. */ export interface ClientLoaderArgs { readonly url: string; readonly params: Readonly>; readonly signal: AbortSignal; /** Lazily obtains the server loader result; repeated calls share one per-navigation request. */ readonly serverLoader: () => Promise; } /** A client-only post-hydration data loader. Its return value replaces the route's rendered data. */ export type ClientLoader = (args: ClientLoaderArgs) => unknown | Promise; /** Safe, client-visible context for a client action. No secrets or raw request headers cross this seam. */ export interface ClientActionArgs extends ClientLoaderArgs { readonly body: ClientRequestBody; } /** Client action preparation. `body` is sent as untrusted input; `optimisticData` is never sent. */ export interface ClientActionResult { readonly body?: ClientRequestBody; readonly optimisticData?: unknown; } /** A client-only action wrapper; it never replaces the server action. */ export type ClientAction = (args: ClientActionArgs) => ClientActionResult | void | Promise; /** Client hooks populated by the generated route entry after a route chunk loads. */ export interface ClientRouteHooks { readonly clientLoader?: ClientLoader; readonly clientAction?: ClientAction; /** Neutral boundary descriptors needed for soft-navigation interception; no server loader crosses. */ readonly boundaries?: readonly BoundaryDescriptor[]; } /** * One `` tag's attributes for a route/layout's `meta.link`. The common HTML `` attributes * are spelled out and **optional** so a typed partial like `{ rel, href, hreflang }` is assignable - * the previous `Record` required *every* value to be a present string, which rejected * exactly that idiomatic shape (the bug this fixes). Standard attributes are explicit and the template * index signature admits inert `data-*` metadata without opening executable `on*` attributes. `boolean` * covers `disabled` (rendered bare when `true`, omitted when `false`), and `undefined` lets a caller * spread in a conditionally absent attribute. SSR and soft navigation apply one runtime allowlist too, * so a cast or untyped route cannot widen the injection surface. */ export interface LinkDescriptor { readonly rel?: string; readonly href?: string; readonly hreflang?: string; readonly crossorigin?: string; readonly media?: string; readonly nonce?: string; readonly sizes?: string; readonly type?: string; readonly as?: string; readonly integrity?: string; readonly referrerpolicy?: string; readonly fetchpriority?: string; readonly title?: string; readonly imagesrcset?: string; readonly imagesizes?: string; readonly color?: string; readonly disabled?: boolean; readonly [attr: `data-${string}`]: string | undefined; } /** One managed `` tag. Standard attributes and inert `data-*` metadata only. */ export interface MetaDescriptor { readonly charset?: string; readonly content?: string; readonly "http-equiv"?: string; readonly itemprop?: string; readonly media?: string; readonly name?: string; readonly property?: string; readonly scheme?: string; readonly [attr: `data-${string}`]: string | undefined; } /** One `