/** * Shared React Router entry-server handler for agent-native templates. * * Templates can keep the shared behavior while importing `ServerRouter` from * their own app-local `react-router` dependency: * * import { ServerRouter } from "react-router"; * import { createDocumentRequestHandler, streamTimeout } from "@agent-native/core/server/entry-server"; * * const handleDocumentRequest = createDocumentRequestHandler(ServerRouter); * export { streamTimeout }; * export default handleDocumentRequest; * * Keeping `ServerRouter` app-local matters in pnpm/published-package installs: * React Router's ``, ``, and `` read framework * context from the same package singleton that rendered ``. * * The superset behavior covers all variants observed across the template fleet: * - HEAD requests: return early with status/headers, no stream body * - .well-known rejection: 404 before rendering (avoids Chrome DevTools probe noise) * - streamTimeout + AbortController: abort render after 5 s, preserving * partial output already streamed to the client * - bot/allReady detection: wait for full render for bots and SPA mode so * crawlers receive complete HTML * - wrapWithAnalytics: applied unconditionally — it is a plain import (never * conditionally undefined), so the `typeof === "function"` guards that * appeared in older template copies were dead code and are removed here */ import type { ReactElement } from "react"; import ReactDOMServer from "react-dom/server.browser"; import type { EntryContext, RouterContextProvider } from "react-router"; const { renderToReadableStream } = ReactDOMServer; import { isbot } from "isbot"; import { wrapWithAnalytics } from "./analytics.js"; export const streamTimeout = 5_000; type ServerRouterComponent = (props: { context: EntryContext; url: string; }) => ReactElement; export type DocumentRequestHandler = ( request: Request, responseStatusCode: number, responseHeaders: Headers, routerContext: EntryContext, loadContext: RouterContextProvider, ) => Promise; export function createDocumentRequestHandler( ServerRouter: ServerRouterComponent, ): DocumentRequestHandler { return async function handleDocumentRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, routerContext: EntryContext, _loadContext: RouterContextProvider, ): Promise { // HEAD requests need no body — return immediately. if (request.method.toUpperCase() === "HEAD") { return new Response(null, { status: responseStatusCode, headers: responseHeaders, }); } // Reject Chrome DevTools well-known probes that have no matching route. // Content template introduced this improvement; it becomes the default here. const url = new URL(request.url); if (url.pathname.startsWith("/.well-known/")) { return new Response(null, { status: 404 }); } const userAgent = request.headers.get("user-agent"); // Wait for full render for bots (so crawlers see complete HTML) and in SPA // mode (where the stream must be fully hydrated before sending). const waitForAll = (userAgent && isbot(userAgent)) || routerContext.isSpaMode; const abortController = new AbortController(); const timeoutId = setTimeout(() => abortController.abort(), streamTimeout); try { const body = await renderToReadableStream( , { signal: abortController.signal, onError(error: unknown) { // Only record a 500 when the stream hasn't already been deliberately // aborted by the timeout above. if (!abortController.signal.aborted) { responseStatusCode = 500; console.error(error); } }, }, ); if (waitForAll) { await body.allReady; } responseHeaders.set("Content-Type", "text/html"); return new Response(wrapWithAnalytics(body), { headers: responseHeaders, status: responseStatusCode, }); } finally { clearTimeout(timeoutId); } }; } let defaultDocumentRequestHandler: DocumentRequestHandler | null = null; async function getDefaultDocumentRequestHandler(): Promise { if (!defaultDocumentRequestHandler) { const { ServerRouter } = await import("react-router"); defaultDocumentRequestHandler = createDocumentRequestHandler(ServerRouter); } return defaultDocumentRequestHandler; } /** * Backwards-compatible default for older generated apps. * * New templates should call `createDocumentRequestHandler(ServerRouter)` from * their own `entry.server.tsx` so React Router framework context always comes * from the app-local singleton. */ export async function handleDocumentRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, routerContext: EntryContext, loadContext: RouterContextProvider, ): Promise { const handler = await getDefaultDocumentRequestHandler(); return handler( request, responseStatusCode, responseHeaders, routerContext, loadContext, ); } export default handleDocumentRequest;