/**
* Static handler definition for build-time rendering of individual segments.
*
* Static wraps a handler so that in production the segment is
* rendered once at build time. The handler is then replaced with a static
* asset import -- no runtime store lookup needed.
*
* In dev mode, Static behaves as a normal handler: the wrapped
* function runs on every request, identical to a regular layout/path handler.
*
* The $$id is auto-generated by the Vite exposeInternalIds plugin based on
* file path and export name. No manual naming required.
*
* Key difference from Prerender:
* - Prerender: route-scoped, produces URLs via getParams, renders subtree per-params
* - Static: segment-scoped, renders once, no URLs, no params
*
* Works on: layout(), parallel(), and path().
*
* @example
* ```ts
* export const DocsNav = Static((ctx) => );
* export const DocShell = Static((ctx) => );
*
* urls(({ path, layout }) => [
* layout(DocsNav, () => [
* path("/getting-started", DocShell, { name: "doc.gs" }),
* path("/:slug", Prerender(getParams, DocPageHandler)),
* ]),
* ]);
* ```
*/
import type { ReactNode } from "react";
import type { Handler } from "./types.js";
import type { StaticBuildContext } from "./prerender.js";
import type { UseItems, HandlerUseItem } from "./route-types.js";
import { isCachedFunction } from "./cache/taint.js";
// -- Types ------------------------------------------------------------------
export interface StaticHandlerOptions {
/**
* Keep handler in server bundle for live fallback (default: false).
* false: handler replaced with stub, source-only APIs excluded from bundle.
* true: handler stays in bundle, renders live at request time.
*/
passthrough?: boolean;
}
export interface StaticHandlerDefinition<
TParams extends Record = any,
> {
readonly __brand: "staticHandler";
/** Auto-generated unique ID (injected by Vite plugin). */
$$id: string;
/** In dev mode, the actual handler function that layout/path/parallel can call. */
handler: Handler;
/** Static handler options (passthrough support). */
options?: StaticHandlerOptions;
/** Composable default DSL items merged when the handler is mounted. */
use?: () => UseItems;
}
// -- Function ---------------------------------------------------------------
export function Static = {}>(
handler: (ctx: StaticBuildContext) => ReactNode | Promise,
options?: StaticHandlerOptions,
__injectedId?: string,
): StaticHandlerDefinition;
// -- Implementation ---------------------------------------------------------
export function Static>(
handler: Function,
optionsOrId?: StaticHandlerOptions | string,
maybeId?: string,
): StaticHandlerDefinition {
if (isCachedFunction(handler)) {
throw new Error(
'A "use cache" function cannot be used as a Static() handler. ' +
"Static handlers are rendered once at build time. Remove the " +
'"use cache" directive — Static already provides caching.',
);
}
let options: StaticHandlerOptions | undefined;
let id: string;
if (typeof optionsOrId === "string") {
id = optionsOrId;
} else {
options = optionsOrId as StaticHandlerOptions | undefined;
id = maybeId ?? "";
}
if (!id) {
throw new Error(
"[rango] Static: missing $$id. " +
"Ensure the exposeInternalIds Vite plugin is configured.",
);
}
return {
__brand: "staticHandler" as const,
$$id: id,
handler: handler as Handler,
...(options ? { options } : {}),
};
}
// -- Type guard -------------------------------------------------------------
/**
* Type guard to check if a value is a StaticHandlerDefinition.
*/
export function isStaticHandler(
value: unknown,
): value is StaticHandlerDefinition {
return (
typeof value === "object" &&
value !== null &&
"__brand" in value &&
(value as { __brand: unknown }).__brand === "staticHandler"
);
}