import { type ResponseResult } from "@nifrajs/core/server"; import type { BoundaryRegistration, BoundaryStates } from "../boundary.js"; import type { Loader as LayoutLoader, LinkDescriptor, Meta, MetaDescriptor, MetaInput, RouteModule, ScriptDescriptor, UnsafeScriptDescriptor } from "../manifest.js"; import type { RenderAdapter } from "../render-seam.js"; type MaybePromise = T | Promise; declare const RESPONSE_RESULT: unique symbol; /** A returned/thrown value that ends the request as-is: either shape of control-flow signal. */ export declare const isControlFlow: (value: unknown) => value is Response | ResponseResult; export interface RenderedPage { readonly [RESPONSE_RESULT]: true; toResponse(): Response; toNodeBody?(): { readonly status: number; readonly headers: Readonly> | undefined; readonly body: string | Uint8Array; }; } export interface RenderPageOptions { readonly adapter: RenderAdapter; /** The layout chain to render - outermost layout → page (opaque; the adapter renders it). */ readonly chain: readonly unknown[]; /** The loader output for this request. */ readonly data: unknown; /** An action's data return (POST only) - surfaced to the page as `actionData` + serialized * for the client so post-POST hydration matches. Omit on GETs. */ readonly actionData?: unknown; /** * URL of the built client entry (loaded as a module script). * * Optional ONLY on a `hydrate: false` page, where there is no client takeover to feed and this value * reaches no output at all. The union below is what enforces that: omitting it on a hydrating page * stays a compile error rather than rendering `src=""`. */ readonly clientEntry?: string; /** Chunk URLs for the **matched** route (its layout chain + own chunk) to `modulepreload` in the * shell - so the route code downloads in parallel with the entry instead of after it * (`buildClient`'s per-route map). Empty/omitted ⇒ only the entry is preloaded (unchanged). */ readonly preload?: readonly string[]; /** Stylesheet URLs for the **matched** route (its layout chain + own CSS, from `buildClient`'s * `BuildManifest.css`) - injected as `` in `` so styles arrive with the * first paint (no FOUC). Rendered even on non-hydrated pages. Empty/omitted ⇒ none (unchanged). */ readonly styles?: readonly string[]; /** SSG: the prerendered-path set, serialized to `window.__NIFRA_PRERENDERED__` so the client fetches * a static `_data.json` on soft-nav into a prerendered route. Empty/omitted ⇒ not injected. */ readonly prerenderedPaths?: readonly string[]; /** ISR: route freshness in seconds, emitted as the `x-nifra-isr-revalidate` header for a `withISR` * wrapper to read. Omit ⇒ no header (the wrapper's default TTL applies). */ readonly revalidate?: number; /** ISR invalidation tags emitted as a bounded `x-nifra-isr-tags` header. */ readonly revalidateTags?: readonly string[]; /** Matched route id; written to `window.__NIFRA_ROUTE__` so the client hydrates this chain. */ readonly routeId?: string; /** The matched route's decoded path params - surfaced to the page as `params` (via {@link RenderProps}) * so an adapter's `useParams` is SSR-correct. Omit ⇒ `{}` (a route with no dynamic segments, or an * error/404 render). */ readonly params?: Readonly>; /** The request's `pathname + search` - surfaced as `path` (via {@link RenderProps}) so an adapter's * `useLocation`/`useSearchParams` render the right URL server-side and hydrate without drift. Omit ⇒ * `""`. */ readonly path?: string; /** The route's validated search (`searchOf(mod.searchSchema, url.search)`, the same value handed to * the loader as `ctx.search`), surfaced as `search` (via {@link RenderProps}) so an adapter's * `useSearch` is SSR-correct. Omit ⇒ `{}` (a render with no search context). */ readonly search?: Record; /** Per-layout loader data, forwarded to the adapter as `RenderProps.layoutData`. Aligned with the * chain's layout prefix; omitted when no layout in the chain has a loader. */ readonly layoutData?: readonly unknown[]; /** Dynamic-boundary states, forwarded to the adapter and serialized for hydration when present. */ readonly boundaries?: BoundaryStates; /** HTTP status for the response (default 200; e.g. 404 for a not-found page). */ readonly status?: number; /** Extra response headers - e.g. the `cache-control` a terminal status page wants. `content-type` * is ignored (the document is always HTML) and the ISR freshness header is applied after these, * so neither can be overridden from here. */ readonly headers?: HeadersLike; /** Document title (fallback when `head.title` is unset). */ readonly title?: string; /** Resolved route head - `title` overrides `title` above; `meta`/`link` render as managed * (`data-nifra`) tags the client updates on navigation. * * **Head contract (the layout chain contributes).** `createWebApp` resolves this via * {@link mergeHeads}: a route's head is its **layout chain's** `meta`/`head` exports merged with the * page's. A `_layout.tsx` may `export const meta` (or `export function meta(args)`) - its tags land * on every page below it (the home for `hreflang`, `preconnect`, a section-default ``). The * merge is **nearest-wins for scalars** (the page's `title` overrides an inner layout's, which * overrides an outer one; an undefined page title keeps the layout's) and **concatenated for the * `meta`/`link` arrays** (outermost layout first, page last). `<meta>`/`<link>` attributes pass a * shared tag-specific allowlist (including inert `data-*` metadata) and values are escaped. */ readonly head?: Meta; /** Id of the container wrapping the app markup (default `"root"`). A non-default id also gets * {@link ROOT_ATTRIBUTE}, which is how the generated client entry finds the container to hydrate - * the entry is built once and cannot know what a given render chose. */ readonly rootId?: string; /** When `false`, emit a complete but **non-hydrated** document - no client entry script, data * globals, or modulepreloads. Used for server-rendered `_error` pages: a terminal state that needs no * client takeover, and it sidesteps an SSR/hydrate mismatch (the server rendered the boundary, not * the page the client manifest maps this route id to). Default `true`. */ readonly hydrate?: boolean; /** Island client bundles (`@nifrajs/web/islands`) to load as `<script type="module">` in the document * tail - emitted **regardless of `hydrate`**, so a static (`hydrate: false`) page can still mount * no-framework islands. URLs are attribute-escaped. Empty/omitted ⇒ none (unchanged output). */ readonly islandScripts?: readonly string[]; /** CSP nonce applied to every framework-owned executable script in this document. */ readonly nonce?: string; /** * Advanced: a per-route slot the renderer fills with the request-invariant document pieces (shell * prefix/suffix, tail statics) on the first render and reuses afterwards, skipping their re-assembly. * * Only pass a cache when every shell-shaping input is IDENTICAL across the requests sharing it: * same `head` content (a static meta chain - never a `meta(data)` function), `title`, `styles`, * `preload`, `islandScripts`, `clientEntry`, `rootId`, `hydrate`, and `prerenderedPaths`. Per-request * values (`data`, `params`, `search`, `actionData`, `layoutData`, deferred state) are always assembled * fresh and safe. A per-request `nonce` disables the cache automatically. `createWebApp` wires this * per route, gated on the route + layout metas being static. */ readonly assemblyCache?: RenderAssemblyCache; } /** The mutable per-route slot {@link RenderPageOptions.assemblyCache} fills. Opaque - create as `{}`. */ export interface RenderAssemblyCache { /** Shell up to (not including) the deferred-runtime insertion point. */ shellPre?: string; /** Shell from after the deferred-runtime insertion point through the open `#root` container. */ shellPost?: string; /** Tail opener: the inline script open tag + the route-id global (before the action global). */ tailPre?: string; /** Tail mid: the prerendered-paths global (between the action and layout-data globals). */ tailMid?: string; /** Tail data prefix: the `window.<data global>=` assignment head. */ tailData?: string; /** Tail from after the serialized data through `</html>`. */ tailPost?: string; } /** * `renderPage` input. A hydrating page (the default) must supply `clientEntry`, because the document * loads it as a module script; a `hydrate: false` page may omit it, because nothing in the emitted * document references it. Expressed as a union so the compiler enforces the pairing rather than the * renderer discovering an empty `src` at runtime. */ export type RenderPageInput = RenderPageOptions & ({ readonly hydrate?: true; readonly clientEntry: string; } | { readonly hydrate: false; }); /** * Server: render a full HTML document for a page - the adapter's hydration head + the SSR * markup (**streamed**) + the serialized loader data + the client module - as a `Response`. * The shell (`<head>` + the open container) flushes first, the adapter's app stream follows, * then the tail (data globals + client entry). Pure Web Standards, so it returns straight from * a nifra route handler and streams on any fetch runtime (Bun/Node/Deno/Workers). */ export declare function renderPage(options: RenderPageInput): MaybePromise<Response>; export declare function renderPageResult(options: RenderPageInput): MaybePromise<RenderedPage>; /** Options for {@link redirect}. */ export interface RedirectOptions { /** HTTP status (default 303 See Other; pass 307/308 to preserve the method). */ readonly status?: number; /** Allow an off-origin / absolute destination. Off by default: only a same-origin path (a single * leading `/`) is permitted, so an action can't be turned into an open redirect by passing * attacker-controlled input straight through. Set `true` for a deliberate external redirect. */ readonly external?: boolean; /** Extra response headers. A redirect is no longer a `Response`, so there is no `.headers` to * mutate after the fact - name them here. Cookies still ride `c.set`, as on any other response. */ readonly headers?: Readonly<Record<string, string>>; } /** A same-origin destination is an absolute path: one leading `/`, but NOT `//` (protocol-relative → * another origin), and nothing a URL parser resolves off-origin. Everything else (absolute URL with * a scheme, `//host`, `javascript:`, a bare relative `foo`) requires `external: true`. The predicate * lives in the kernel - see `isSameOriginPath` - so this gate and the auth guards' `redirectTo` * cannot drift apart. */ /** * Build a redirect - return it from a route `action` for the Post/Redirect/Get pattern (POST * mutates, 303 sends the browser to a fresh GET, so a reload doesn't re-submit). Defaults to 303 * (See Other); pass `{ status: 307 }` or `{ status: 308 }` to preserve the method. * * **Secure by default:** `location` must be a same-origin path (begins with `/`, not `//`). An * off-origin/absolute destination throws unless you pass `{ external: true }` - this closes the * open-redirect footgun of `return redirect(formData.get("next"))` on the no-JS (native-form) path, * which serves the action's control-flow value verbatim. * * Returns a plain render, not a `Response`. A redirect is a status line and one header - the most * body-less response there is - and building a `Response` for it costs the whole Web object plus, on * Node, a stream drained back out. As data it renders on the same lane a handler's return takes. * `redirect(...)` is still returned or thrown from exactly the same places; only `.status` / * `.headers` are gone from the value, replaced by `options.headers` and the request's `c.set`. * * @param options redirect status, extra headers, and whether an off-origin destination is intentional. */ export declare function redirect(location: string, options?: RedirectOptions): ResponseResult; /** * Brand marking a `Response` as a **terminal-status signal** rather than a response to serve verbatim. * * A loader that throws a bare `Response` has always been passed straight through - that is how * `throw redirect(...)` works, and apps rely on it. So "render the `_404` boundary at 404" cannot be * expressed by throwing a plain `Response`: the framework would have no way to tell it apart from a * hand-rolled body the app wants served exactly as written. The brand is that distinction, and * checking it *before* the pass-through is what keeps every existing throw working unchanged. * * `Symbol.for` matches `RESPONSE_RESULT` above: a registry symbol survives two copies of this module, * which a `unique symbol` would not - and a duplicate-install turning a 404 into a raw 404 body would * be a maddening bug to trace. */ /** Derived from the `Headers` constructor rather than the DOM lib's `HeadersInit`: `@nifrajs/web` * is consumed from DOM-free programs (the root tsconfig runs `types: ["bun"]`), and naming the DOM * type here would break their build. Same workaround as `SSEInit.headers` in core. */ export type HeadersLike = ConstructorParameters<typeof Headers>[0]; /** * Append the likely cause to an SSR error that carries a duplicate-instance signature. * * Deliberately does NOT claim the resolved paths: `@nifrajs/web` is framework-agnostic and does not * depend on React, so it cannot resolve the copies without guessing. It names the condition and the * command that reports the paths, which is the actionable part. Returns the error untouched when the * signature does not match, so an ordinary render error reads exactly as before. */ export declare function withDuplicateInstanceHint(err: unknown): unknown; /** * An action's control-flow value passes straight through - except a redirect on a client-submit data * request: fetch would follow the 3xx into HTML the client can't use, so the redirect rides the * X-Nifra-Redirect header on a 204 and the client navigates. One conversion shared by the returned- * and thrown- paths, so `return redirect()` and `throw redirect()` agree. * * Takes either shape: `redirect()` is a plain render, while a hand-rolled `new Response(...)` from an * action still arrives as a `Response`. The rewrite stays on the lane its input was on - a plain * redirect converts to a plain 204, and never materializes the `Response` it is replacing. */ export declare function actionResponse(result: Response | ResponseResult, isDataRequest: boolean): Response | ResponseResult; /** A loaded layout module. `loader`/`gate` are the layout-loader surface; `meta` predates it. */ export type LoadedLayoutModules = ReadonlyArray<{ default: unknown; meta?: MetaInput; loader?: LayoutLoader; action?: unknown; gate?: boolean; searchSchema?: RouteModule["searchSchema"]; boundaries?: readonly BoundaryRegistration[]; }>; /** * Narrow the route's params to the ones a layout owns. * * A layout wraps a URL prefix, so anything deeper belongs to a route beneath it. Handing it the full * set would let a layout read a param it does not own - which reads fine until the layout is reused * under a route that has no such param and the value silently becomes `undefined`. */ export declare function scopeParams(params: Record<string, string>, owned: readonly string[] | undefined): Record<string, string>; export declare const EMPTY_RETAIN: ReadonlySet<number>; export declare const tagLayoutError: (err: unknown, layoutId: string) => unknown; export declare const layoutErrorId: (err: unknown) => string | undefined; export declare const STATUS_SIGNAL: unique symbol; /** Reason phrases for the plain-text fallback, used only when the app authored no `_<status>` and no * `_404` page. Deliberately not exhaustive - anything unlisted falls back to "Error", which is more * useful than shipping a table of every RFC status for a body almost nobody will see. */ export declare const STATUS_TEXT: Readonly<Record<number, string>>; export interface StatusSignal extends Response { readonly [STATUS_SIGNAL]: { readonly status: number; readonly headers?: HeadersLike; }; } export declare function isStatusSignal(value: unknown): value is StatusSignal; /** Options shared by {@link notFound}, {@link gone}, and {@link statusPage}. */ export interface StatusPageOptions { /** * Extra headers for the rendered response - `cache-control` above all. * * The defaults differ by status for a reason: a 404 may be a race with publication, so it wants a * short TTL, while a 410 is a promise that the URL is permanently gone and can be cached hard. * Getting this wrong is the difference between a crawler dropping a dead URL and re-fetching it * for weeks. */ readonly headers?: HeadersLike; } /** * Render the nearest `_404` page at status **404**. `throw` it from a loader when the record does not * exist. * * This is the fix for the soft 404: a matched route whose loader finds nothing has otherwise no way to * answer 404, so the path of least resistance is to return empty data and render "not found" inside a * **200**. That looks correct in a browser and is invisible in review, which is why it ships and stays * shipped - while search engines penalise it and keep the dead URL indexed. * * Returns `never`: these throw, so a loader narrows without a redundant `return`, and the type states * the thing the "loaders `throw` redirect, actions `return` it" rule already trips people on. */ export declare function notFound(options?: StatusPageOptions): never; /** * Render a terminal page at status **410 Gone**. `throw` it from a loader for a record that existed and * was deliberately removed - a withdrawn listing, a deleted post. * * 410 is not a pedantic 404: it tells a crawler to **drop** the URL rather than re-fetch it for weeks * on the assumption the 404 was transient. Uses `_410.tsx` if the app has one, otherwise `_404`. */ export declare function gone(options?: StatusPageOptions): never; /** * Render a terminal page at any 4xx/5xx status - the escape hatch behind {@link notFound} and * {@link gone} (402, 451, …). Uses `_<status>.tsx` if present, otherwise `_404`. */ export declare function statusPage(status: number, options?: StatusPageOptions): never; /** The wrapper `revalidate()` returns: the action's `data` plus the paths it changed. A plain tagged * shape (not a class) so `@nifrajs/client`'s `ActionData` can unwrap it structurally without importing * from `@nifrajs/web`. `createWebApp` strips the wrapper - the client receives `data` as the body and * the paths via the `X-Nifra-Revalidate` header. */ export interface RevalidateResult<T> { readonly __nifraRevalidate: readonly string[]; readonly data: T; } /** * Return this from an action to declare which routes the mutation changed (alongside the action's * `data`). `createWebApp` sets the `X-Nifra-Revalidate` response header; after the submit the client * marks those cached routes stale - refetching the active one and any mounted fetcher showing them - * so a mutation can refresh views beyond the one that was submitted. `data` is still surfaced to the * component as `actionData` (the wrapper is transparent to `ActionData<typeof action>`). */ export declare function revalidate<T>(paths: readonly string[], data: T): RevalidateResult<T>; /** * Serialize loader data for embedding inside an inline `<script>`. `JSON.stringify` alone * is NOT safe there: a string containing `</script>` or `<!--` would break out of the * script element (an XSS vector). Escape `<`/`>` to `\uXXXX`, plus the U+2028/U+2029 * separators. */ export declare function serializeData(data: unknown): string; /** * A `<link rel="canonical">` descriptor for a route's `meta.link`. The canonical URL tells search * engines which URL is authoritative for a page (deduping query-string / tracking variants). * * ```ts * export const meta = (a) => ({ link: [canonical(`https://site.com/posts/${a.params.slug}`)] }) * ``` */ export declare function canonical(href: string): LinkDescriptor; /** Inputs for {@link openGraph} - the common Open Graph properties. All optional; only the provided * ones become tags. `type` defaults to `"website"`. */ export interface OpenGraphInput { readonly title?: string; readonly description?: string; /** Absolute URL of the share image (`og:image`). */ readonly image?: string; /** Canonical URL of the page (`og:url`). */ readonly url?: string; /** Object type (`og:type`) - `"website"`, `"article"`, … Default `"website"`. */ readonly type?: string; } /** * Build the Open Graph `<meta property="og:*">` entries for a route's `meta.meta`. Returns only the * properties you supplied (plus `og:type`, defaulting to `"website"`), so it composes with other meta. * * ```ts * export const meta = { meta: [...openGraph({ title: "Nifra", image: "https://site.com/og.png" })] } * ``` */ export declare function openGraph(input: OpenGraphInput): MetaDescriptor[]; /** * Build a JSON-LD `<script type="application/ld+json">` entry for a route's `meta.script` from a plain * object. `JSON.stringify` produces the body; the head renderer breakout-escapes it (see * `escapeScriptContent`), so a string field containing `</script>` is embedded safely. * * ```ts * export const meta = { * script: [jsonLd({ "@context": "https://schema.org", "@type": "Article", headline: "Hi" })], * } * ``` */ export declare function jsonLd(data: Record<string, unknown>): ScriptDescriptor; /** * Deliberately unsafe escape hatch for executable inline code. The required nonce keeps the result * compatible with a strict CSP and makes the security-sensitive choice visible at the call site. */ export declare function unsafeInlineScript(content: string, options: { readonly nonce: string; readonly type?: "module" | "text/javascript"; }): UnsafeScriptDescriptor; export {}; //# sourceMappingURL=render-document.d.ts.map