/** * Pre-render handler definition for build-time rendering of route segments. * * Prerender wraps a handler so that in production (phase 2) * it can be pre-rendered at build time and served as a static Flight payload. * In dev mode (phase 1), it behaves as a normal handler — the handler runs * on every request just like a regular path() handler. * * The $$id is auto-generated by the Vite exposeInternalIds plugin * based on file path and export name. No manual naming required. * * @example * ```ts * // Static page — no params * export const DocsPage = Prerender(async (ctx) => { * return
Documentation
; * }); * * // Dynamic page — params first, handler second * export const DocsArticle = Prerender( * async () => [{ slug: "getting-started" }, { slug: "api-reference" }], * async (ctx) => { * return
{ctx.params.slug}
; * } * ); * ``` */ import type { ReactNode } from "react"; import type { Handler, HandlerContext, DefaultEnv, ExtractParams, } from "./types.js"; import type { Handle } from "./handle.js"; import type { ContextVar } from "./context-var.js"; import type { ReverseFunction } from "./reverse.js"; import type { DefaultReverseRouteMap } from "./types/global-namespace.js"; import type { UseItems, HandlerUseItem } from "./route-types.js"; import { isCachedFunction } from "./cache/taint.js"; // -- Named route resolution types ------------------------------------------- /** * Reverse function for build contexts (BuildContext, StaticBuildContext, GetParamsContext). * Global names get full autocomplete and param validation from the generated route map. * Local `.name` calls are accepted but not validated (the include() scope is unknown * at the type level). */ type BuildReverseFunction = [DefaultReverseRouteMap] extends [ Record, ] ? // No generated route map — permissive fallback ( name: string, params?: Record, search?: Record, ) => string : // Generated route map available — typed globals + permissive locals ReverseFunction & { ( name: `.${string}`, params?: Record, search?: Record, ): string; }; /** * Default route map for Prerender named route resolution. * Uses GeneratedRouteMap (from gen file) to avoid circular dependencies. */ type DefaultPrerenderRouteMap = keyof Rango.GeneratedRouteMap extends never ? {} : Rango.GeneratedRouteMap; /** Extract params from a route map entry (string pattern or { path } object). */ type ExtractParamsFromEntry = TEntry extends string ? ExtractParams : TEntry extends { readonly path: infer P extends string } ? ExtractParams

: Record; /** * Resolve params from Prerender's type parameter. * Accepts named routes (global or .local) and explicit param objects. * * Resolution order: * 1. ".local" string → look up in TRouteMap * 2. Global route name string → look up in DefaultPrerenderRouteMap * 3. Record object → use as-is (explicit params) * 4. Fallback → {} */ type ResolvePrerenderParams< T, TRouteMap extends {} = DefaultPrerenderRouteMap, > = T extends `.${infer Local}` ? Local extends keyof TRouteMap ? ExtractParamsFromEntry : Record : T extends keyof DefaultPrerenderRouteMap ? ExtractParamsFromEntry : T extends Record ? T : {}; // -- Types ------------------------------------------------------------------ export interface PrerenderOptions { /** * Maximum number of param sets to render in parallel (default: 1). * Only applies to dynamic Prerender handlers with getParams(). * Set to higher values to speed up builds with many routes. * * @example * ```typescript * export const BlogPost = Prerender( * async () => allPosts.map(p => ({ slug: p.slug })), * async (ctx) => , * { concurrency: 4 }, * ); * ``` */ concurrency?: number; } /** * Context passed to Prerender() handlers at build time. * Has a synthetic URL from getParams, params, pathname, and optionally env. * No request, headers, cookies. */ export interface BuildContext { /** Params extracted from the route pattern (populated from getParams). */ params: TParams; /** True during build-time pre-rendering, false during passthrough live render. */ build: true; /** * True when running in Vite dev mode (on-demand prerender), false during * production `vite build`. Use this to branch on runtime mode without * changing build semantics. */ dev: boolean; /** * Build-time environment bindings (KV, D1, etc.) supplied by the Vite plugin. * Only available when `buildEnv` is configured in rango() options. * Throws with a clear error if not configured. * * This is NOT the live request env — it is shared across all prerender * invocations for the build. */ env: DefaultEnv; /** Read a variable set by getParams or a parent handler. */ get: { (contextVar: ContextVar): T | undefined; (key: string): any; }; /** Set a variable readable by child layouts and parallels. */ set: { (contextVar: ContextVar, value: T): void; (key: string, value: any): void; }; /** Push handle data (frozen into pre-rendered output at build time). */ use: (handle: Handle) => (data: T) => void; /** Synthetic URL built from pattern + params (no real request). */ url: URL; /** Pathname portion of the synthetic URL. */ pathname: string; /** URLSearchParams from the synthetic URL (always empty for prerender). */ searchParams: URLSearchParams; /** Typed search params — always {} for prerender (no real query string). */ search: {}; /** URL generation by route name. */ reverse: BuildReverseFunction; /** * Signal that this param set should not produce a local prerender artifact. * At runtime the live handler runs instead. Only valid on routes wrapped * with `Passthrough()`. */ passthrough: () => PrerenderPassthroughResult; } /** * Context passed to Static() handlers at build time. * No URL, no params, no pathname — just renders content. */ export interface StaticBuildContext { /** Always true for Static handlers at build time. */ build: true; /** * True when running in Vite dev mode, false during production build. */ dev: boolean; /** * Build-time environment bindings supplied by the Vite plugin. * Only available when `buildEnv` is configured in rango() options. */ env: DefaultEnv; /** Read a variable (available for type consistency with BuildContext). */ get: { (contextVar: ContextVar): T | undefined; (key: string): any; }; /** Set a variable (available for type consistency with BuildContext). */ set: { (contextVar: ContextVar, value: T): void; (key: string, value: any): void; }; /** Push handle data (frozen into pre-rendered output at build time). */ use: (handle: Handle) => (data: T) => void; /** URL generation by route name. */ reverse: BuildReverseFunction; } /** * Context passed to getParams() at build time. * Allows sharing data with handler invocations via set(). */ export interface GetParamsContext { /** Always true during build-time getParams execution. */ build: true; /** * True when running in Vite dev mode, false during production build. */ dev: boolean; /** * Build-time environment bindings supplied by the Vite plugin. * Only available when `buildEnv` is configured in rango() options. */ env: DefaultEnv; /** Set a variable that will be available to each handler invocation via ctx.get(). */ set: { (contextVar: ContextVar, value: T): void; (key: string, value: any): void; }; /** URL generation by route name. */ reverse: BuildReverseFunction; } export interface PrerenderHandlerDefinition< TParams extends Record = any, > { readonly __brand: "prerenderHandler"; /** Auto-generated unique ID (injected by Vite plugin). */ $$id: string; /** In dev mode, the actual handler function that path() can call. */ handler: Handler; /** Returns the list of param objects to pre-render (dynamic routes). */ getParams?: (ctx: GetParamsContext) => Promise | TParams[]; /** Pre-render options. */ options?: PrerenderOptions; /** Composable default DSL items merged when the handler is mounted. */ use?: () => UseItems; } // -- Overloads -------------------------------------------------------------- // // T accepts: named route string (global or .local) OR explicit param object. // Named routes resolve params from GeneratedRouteMap, e.g.: // Prerender<"locale.detail"> → params = { locale: string; slug: string } // Explicit params work as before: // Prerender<{ slug: string }> → params = { slug: string } // Overload 1: Static handler (build-time only) export function Prerender< T extends | keyof DefaultPrerenderRouteMap | `.${keyof TRouteMap & string}` | Record = {}, TRouteMap extends {} = DefaultPrerenderRouteMap, >( handler: ( ctx: BuildContext>, ) => | ReactNode | PrerenderPassthroughResult | Promise, options?: PrerenderOptions, __injectedId?: string, ): PrerenderHandlerDefinition>; // Overload 2: Dynamic handler (build-time only) export function Prerender< T extends | keyof DefaultPrerenderRouteMap | `.${keyof TRouteMap & string}` | Record, TRouteMap extends {} = DefaultPrerenderRouteMap, >( getParams: ( ctx: GetParamsContext, ) => | Promise[]> | ResolvePrerenderParams[], handler: ( ctx: BuildContext>, ) => | ReactNode | PrerenderPassthroughResult | Promise, options?: PrerenderOptions, __injectedId?: string, ): PrerenderHandlerDefinition>; // -- Implementation --------------------------------------------------------- export function Prerender>( handlerOrGetParams: Function, handlerOrOptions?: Function | PrerenderOptions, optionsOrId?: PrerenderOptions | string, maybeId?: string, ): PrerenderHandlerDefinition { // Resolve overloads: // 1 fn arg: Prerender(handler, options?, __injectedId?) // 2 fn args: Prerender(getParams, handler, options?, __injectedId?) let handler: Handler; let getParams: (() => Promise | TParams[]) | undefined; let options: PrerenderOptions | undefined; let id: string; if (typeof handlerOrOptions === "function") { // Two function args: getParams + handler getParams = handlerOrGetParams as () => Promise | TParams[]; handler = handlerOrOptions as Handler; if (typeof optionsOrId === "string") { id = optionsOrId; } else { options = optionsOrId as PrerenderOptions | undefined; id = maybeId ?? ""; } } else { // Single function arg: handler only handler = handlerOrGetParams as Handler; if (typeof handlerOrOptions === "object" && handlerOrOptions !== null) { options = handlerOrOptions as PrerenderOptions; } if (typeof optionsOrId === "string") { id = optionsOrId; } else { id = maybeId ?? ""; } } if (isCachedFunction(handler)) { throw new Error( 'A "use cache" function cannot be used as a Prerender() handler. ' + "Prerender handlers are rendered at build time. Remove the " + '"use cache" directive — Prerender already provides caching.', ); } if (getParams && isCachedFunction(getParams)) { throw new Error( 'A "use cache" function cannot be used as Prerender() getParams. ' + "getParams runs at build time to enumerate params. Remove the " + '"use cache" directive.', ); } if (!id) { throw new Error( "[rango] Prerender: missing $$id. " + "Ensure the exposeInternalIds Vite plugin is configured.", ); } return { __brand: "prerenderHandler" as const, $$id: id, handler, ...(getParams ? { getParams } : {}), ...(options ? { options } : {}), }; } // -- Passthrough sentinel --------------------------------------------------- /** * Sentinel returned by `ctx.passthrough()` to signal that a specific param set * should not produce a local prerender artifact. The build skips writing the * entry; at runtime the Passthrough live handler runs instead. */ export const PRERENDER_PASSTHROUGH: Readonly<{ __brand: "prerenderPassthrough"; }> = Object.freeze({ __brand: "prerenderPassthrough" as const, }); export type PrerenderPassthroughResult = typeof PRERENDER_PASSTHROUGH; /** * Type guard to check if a value is the passthrough sentinel. */ export function isPrerenderPassthrough( value: unknown, ): value is PrerenderPassthroughResult { return ( typeof value === "object" && value !== null && "__brand" in value && (value as { __brand: unknown }).__brand === "prerenderPassthrough" ); } // -- Type guards ------------------------------------------------------------ /** * Type guard to check if a value is a PrerenderHandlerDefinition. */ export function isPrerenderHandler( value: unknown, ): value is PrerenderHandlerDefinition { return ( typeof value === "object" && value !== null && "__brand" in value && (value as { __brand: unknown }).__brand === "prerenderHandler" ); } // -- Passthrough wrapper ---------------------------------------------------- /** * A prerender route with a live fallback handler for unknown params at runtime. * * Wraps a `Prerender(...)` definition with a separate handler that runs at * request time for params not covered by `getParams()`. * * - Build time: `prerenderDef` provides getParams + build handler. * - Runtime: `liveHandler` runs for unknown params with full HandlerContext. * * @example * ```ts * const BlogPrerender = Prerender( * async () => [{ slug: "getting-started" }, { slug: "api-reference" }], * async (ctx) => , * ); * * // In route definition: * path("/blog/:slug", Passthrough(BlogPrerender, async (ctx) => { * const post = await ctx.env.DB.get(ctx.params.slug); * return ; * })) * ``` */ export interface PassthroughHandlerDefinition< TParams extends Record = any, TEnv = DefaultEnv, > { readonly __brand: "passthroughHandler"; /** The underlying prerender definition (build-time rendering). */ prerenderDef: PrerenderHandlerDefinition; /** Live handler for runtime fallback on unknown params. */ liveHandler: ( ctx: HandlerContext, ) => ReactNode | Promise | Response | Promise; /** Composable default DSL items merged when the handler is mounted. */ use?: () => UseItems; } export function Passthrough< TParams extends Record, TEnv = DefaultEnv, >( prerenderDef: PrerenderHandlerDefinition, liveHandler: ( ctx: HandlerContext, ) => ReactNode | Promise | Response | Promise, ): PassthroughHandlerDefinition; // Implementation export function Passthrough< TParams extends Record, TEnv = DefaultEnv, >( prerenderDef: PrerenderHandlerDefinition, liveHandler: ( ctx: HandlerContext, ) => ReactNode | Promise | Response | Promise, ): PassthroughHandlerDefinition { if (!isPrerenderHandler(prerenderDef)) { throw new Error( "[rango] Passthrough: first argument must be a Prerender() definition.", ); } return { __brand: "passthroughHandler" as const, prerenderDef, liveHandler, }; } /** * Type guard to check if a value is a PassthroughHandlerDefinition. */ export function isPassthroughHandler( value: unknown, ): value is PassthroughHandlerDefinition { return ( typeof value === "object" && value !== null && "__brand" in value && (value as { __brand: unknown }).__brand === "passthroughHandler" ); }