import { existsSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";
import { AsyncLocalStorage } from "node:async_hooks";
import { fileURLToPath } from "node:url";
import {
init$ as cache_init$,
dispose$ as cache_dispose$,
useCache,
StorageCache,
} from "../../cache/index.mjs";
import { context$, ContextStorage, getContext } from "../../server/context.mjs";
import { createWorker } from "../../server/create-worker.mjs";
import { useErrorComponent } from "../../server/error-handler.mjs";
import { style as errorStyle } from "../../server/error-styles.mjs";
import { getPrerender } from "../../server/prerender-storage.mjs";
import { createRenderContext } from "../../server/render-context.mjs";
import { getRuntime, runtime$ } from "../../server/runtime.mjs";
import {
ABORT_SIGNAL,
CLIENT_MODULES_CONTEXT,
COLLECT_CLIENT_MODULES,
COLLECT_STYLESHEETS,
CONFIG_CONTEXT,
CONFIG_ROOT,
ERROR_BOUNDARY,
ERROR_CONTEXT,
HTTP_CONTEXT,
HTTP_HEADERS,
HTTP_STATUS,
IMPORT_MAP,
LINK_QUEUE,
LOGGER_CONTEXT,
MAIN_MODULE,
MANIFEST,
MEMORY_CACHE_CONTEXT,
MODULE_LOADER,
MODULE_CACHE,
OTEL_SPAN,
OTEL_CONTEXT,
POSTPONE_CONTEXT,
POSTPONE_STATE,
PRELUDE_HTML,
PRERENDER_CACHE,
PRERENDER_CACHE_DATA,
REDIRECT_CONTEXT,
RENDER,
RENDER_CONTEXT,
RENDER_STREAM,
SCROLL_RESTORATION_MODULE,
REQUEST_CACHE_CONTEXT,
REQUEST_CACHE_SHARED,
SERVER_CONTEXT,
SOURCEMAP_SUPPORT,
STYLES_CONTEXT,
WORKER_THREAD,
} from "../../server/symbols.mjs";
import { mergeContextHeaders } from "../http/middleware-response.mjs";
import * as sys from "../sys.mjs";
globalThis.AsyncLocalStorage = AsyncLocalStorage;
const cwd = sys.cwd();
const REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference");
export default async function ssrHandler(root, options = {}) {
const outDir = options.outDir ?? ".react-server";
const defaultRoot = sys.normalizePath(join(outDir, "server/root.mjs"));
const logger = getRuntime(LOGGER_CONTEXT);
const config = getRuntime(CONFIG_CONTEXT);
const configRoot = config?.[CONFIG_ROOT] ?? {};
await import("./manifest.mjs").then(({ init$ }) => init$(options));
// Install source map support for production stack trace rewriting (Node.js only)
// Skip if Node.js native source maps are already enabled via NODE_OPTIONS
const sourcemapSupport = getRuntime(SOURCEMAP_SUPPORT);
const nativeSourceMaps =
process.execArgv?.includes("--enable-source-maps") ||
process.env.NODE_OPTIONS?.includes("--enable-source-maps");
if (sourcemapSupport && !sys.isEdgeRuntime && !nativeSourceMaps) {
const { default: sourceMapSupport } = await import("source-map-support");
sourceMapSupport.install({
environment: "node",
hookRequire: sourcemapSupport === "inline",
handleUncaughtExceptions: false,
retrieveSourceMap:
sourcemapSupport === "hidden"
? (source) => {
if (source.startsWith("file:")) {
const mapFilePath = fileURLToPath(source) + ".map";
if (existsSync(mapFilePath)) {
return {
url: source,
map: readFileSync(mapFilePath, "utf8"),
};
}
}
return null;
}
: undefined,
});
logger.info(
`Source map (${sourcemapSupport === true ? "file" : sourcemapSupport}) stack trace support enabled`
);
}
const rootModule = sys.normalizePath(
join(cwd, root ?? configRoot.entry ?? defaultRoot)
);
const globalErrorModule = sys.normalizePath(
join(cwd, outDir, "server/error.mjs")
);
const [
{ render },
// Optional secondary render entry. The build emits `server/render-action.mjs`
// as a parallel input only for client-root builds (lib/build/server.mjs
// sets `renderActionModulePath` when isClientRootBuild). For non-client-root
// builds the import resolves to nothing and the per-request switch
// below collapses to "always use `render`".
//
// Why a second entry instead of dispatching inside render-ssr.jsx: the
// SSR shortcut does not have the action-decode/encode pipeline (~200
// lines in render-rsc.jsx — decryptActionId, decodeReply, decodeAction,
// decodeFormState, the action span, the formState branch). Bundling the
// RSC entry as an alternate `renderAction` keeps that logic in one
// place and dispatches at request time.
{ render: renderAction },
{ default: Component, init$: root_init$, warmup$: root_warmup$ },
{ default: GlobalErrorComponent },
{ default: ErrorBoundary },
{ clientReferenceMap },
rscSerializer,
] = await Promise.all([
import("@lazarv/react-server/dist/server/render"),
(async () => {
try {
return await import("@lazarv/react-server/dist/server/render-action");
} catch {
return { render: null };
}
})(),
import("@lazarv/react-server/dist/server/root"),
import("@lazarv/react-server/dist/server/error"),
(async () => {
try {
return await import("@lazarv/react-server/dist/server/error-boundary");
} catch {
return { default: null };
}
})(),
import("@lazarv/react-server/dist/server/client-reference-map"),
import("../../cache/rsc.mjs"),
]);
// Warm up all route module imports in the background.
// This populates the ESM module cache and the loader resolve cache
// so the first request doesn't pay the cold-import penalty.
// Intentionally not awaited — runs in parallel with remaining server setup.
root_warmup$?.()?.catch?.(() => {});
const collectClientModules = getRuntime(COLLECT_CLIENT_MODULES);
const collectStylesheets = getRuntime(COLLECT_STYLESHEETS);
const manifest = getRuntime(MANIFEST);
// Detect client-root in production: the build emits dist/server/root.mjs
// as a registerClientReference proxy when the user's root is a "use client"
// module (lib/build/server.mjs also swaps the bundled `render` entry to
// render-ssr.jsx in that case — see isClientRootBuild). Same detection as
// the dev path: a single property read on the loaded export.
//
// For CSS / client-modules collection we have to bridge from the bundled
// server path (which has no original imports) back to the user's source
// path (which the browser/client manifest indexes). The server manifest
// entry's `src` field carries that original path.
const isClientRoot = Component?.$$typeof === REACT_CLIENT_REFERENCE;
let rootSrc = null;
if (isClientRoot && manifest?.server) {
const serverEntry = Object.values(manifest.server).find((entry) =>
rootModule.endsWith(entry.file)
);
rootSrc = serverEntry?.src ?? null;
}
const clientModules = collectClientModules?.(rootModule) ?? [];
// Server-side traversal for client-root yields nothing useful (the
// registerClientReference stub has no transitive client refs to discover);
// render-ssr.jsx unshifts the root's browser id at request time, so the
// empty list here is fine.
const styles = isClientRoot
? (collectStylesheets?.(rootSrc ?? rootModule, manifest?.client) ?? [])
: (collectStylesheets?.(rootModule) ?? []);
const hasClientComponents = Object.keys(clientReferenceMap()).length > 0;
const mainModule = hasClientComponents
? getRuntime(MAIN_MODULE)?.map((mod) =>
`${configRoot.base || "/"}/${mod}`.replace(/\/+/g, "/")
)
: [];
const scrollRestorationModule = getRuntime(SCROLL_RESTORATION_MODULE)
? `${configRoot.base || "/"}/${getRuntime(SCROLL_RESTORATION_MODULE)}`.replace(
/\/+/g,
"/" )
: null;
const moduleLoader = getRuntime(MODULE_LOADER);
const memoryCache = getRuntime(MEMORY_CACHE_CONTEXT);
const moduleCacheStorage = new AsyncLocalStorage();
const linkQueueStorage = new AsyncLocalStorage();
runtime$({
[MODULE_CACHE]: moduleCacheStorage,
[LINK_QUEUE]: linkQueueStorage,
});
const importMap =
configRoot.importMap || configRoot.resolve?.shared
? {
...configRoot.importMap,
imports: await new Promise(async (resolve, reject) => {
try {
if (!configRoot.importMap?.imports) {
return resolve({});
}
const entries = Object.entries(configRoot.importMap.imports);
for await (const [key, value] of entries) {
const entry = Object.values(manifest.browser).find(
(entry) => entry.name === key
);
if (entry) {
delete configRoot.importMap.imports[key];
configRoot.importMap.imports[
`/${sys.normalizePath(relative(cwd, entry.file))}`
] = value;
}
}
resolve(configRoot.importMap.imports);
} catch (e) {
reject(e);
}
}),
}
: null;
for (const mod of configRoot.resolve?.shared ?? []) {
if (!importMap.imports[mod]) {
const entry = Object.values(manifest.browser).find(
(entry) => entry.name === mod
)?.file;
if (entry) {
importMap.imports[mod] = `/${entry}`;
}
}
}
runtime$(IMPORT_MAP, importMap);
const renderStream = createWorker();
const hasWorkerThread = !!getRuntime(WORKER_THREAD);
const errorHandler = async (e) => {
const httpStatus = getContext(HTTP_STATUS) ?? {
status: 500,
statusText: "Internal Server Error",
};
const headers = getContext(HTTP_HEADERS) ?? new Headers();
if (getContext(RENDER_CONTEXT)?.flags?.isHTML) {
const html = `<html lang="en">
<head>
<title>Server Error</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
${errorStyle}
</style>
</head>
<body class="react-server-global-error">
<h1>${e?.digest || e?.message}</h1>
<pre>${(e?.digest ? e?.message : e?.stack) || "An unexpected error occurred while rendering the page. The specific message is omitted in production builds to avoid leaking sensitive details."}</pre>
<a href="${getContext(HTTP_CONTEXT)?.url.pathname}">
<button>Retry</button>
</a>
<script type="module">
window.addEventListener("popstate", () => {
location.reload();
});
</body>
</html>`;
headers.set("Content-Type", "text/html; charset=utf-8");
return new Response(html, {
status: httpStatus.status,
headers,
});
}
headers.set("Content-Type", "text/plain; charset=utf-8");
return new Response(e?.digest || e?.message, {
...httpStatus,
headers,
});
};
// Edge mode uses a same-thread channel pair instead of a real worker thread.
// Detect by checking for `threadId` (present only on real Worker instances).
const workerThread = getRuntime(WORKER_THREAD);
const hasRealWorkerThread =
hasWorkerThread && typeof workerThread?.threadId === "number";
// Load shared cache and memory driver modules in parallel
const [
{ createSharedRequestCache, createInProcessRequestCache },
{ default: memoryDriver },
] = await Promise.all([
import("../../cache/request-cache-shared.mjs"),
import("unstorage/drivers/memory"),
]);
return async function ssr(httpContext) {
const noCache =
httpContext.request.headers.get("cache-control") === "no-cache";
// Create per-request cache for "use cache: request"
const requestCache = new StorageCache(memoryDriver, { type: "raw" });
const sharedRequestCache = hasRealWorkerThread
? createSharedRequestCache()
: createInProcessRequestCache();
return new Promise((resolve, reject) => {
try {
moduleCacheStorage.run(new Map(), () => {
ContextStorage.run(
{
[SERVER_CONTEXT]: getRuntime(SERVER_CONTEXT),
[CONFIG_CONTEXT]: config,
[HTTP_CONTEXT]: httpContext,
[ABORT_SIGNAL]: httpContext.signal,
[ERROR_CONTEXT]: errorHandler,
[LOGGER_CONTEXT]: logger,
[MAIN_MODULE]: mainModule,
[SCROLL_RESTORATION_MODULE]: scrollRestorationModule,
[MODULE_LOADER]: moduleLoader,
[IMPORT_MAP]: importMap,
[MEMORY_CACHE_CONTEXT]: memoryCache,
[MANIFEST]: manifest,
[REQUEST_CACHE_CONTEXT]: requestCache,
[REQUEST_CACHE_SHARED]: sharedRequestCache,
[REDIRECT_CONTEXT]: {},
[COLLECT_CLIENT_MODULES]: collectClientModules,
[CLIENT_MODULES_CONTEXT]: clientModules,
[COLLECT_STYLESHEETS]: collectStylesheets,
[STYLES_CONTEXT]: styles,
[RENDER_STREAM]: renderStream,
[PRELUDE_HTML]: getPrerender(PRELUDE_HTML),
[POSTPONE_STATE]: getPrerender(POSTPONE_STATE),
[PRERENDER_CACHE]: httpContext.prerenderCache ?? null,
[ERROR_BOUNDARY]: ErrorBoundary,
[MODULE_CACHE]: moduleCacheStorage,
[LINK_QUEUE]: linkQueueStorage,
// Propagate OTel span from the HTTP layer into per‑request context
[OTEL_SPAN]: httpContext._otelSpan ?? null,
[OTEL_CONTEXT]: httpContext._otelCtx ?? null,
},
async () => {
if (!noCache) {
await cache_init$?.();
}
cache_dispose$("request");
let expiredPrerenderCache = false;
const prerenderCacheData = getPrerender(PRERENDER_CACHE_DATA);
if (prerenderCacheData?.length > 0) {
await Promise.all(
prerenderCacheData.map(async (entry) => {
const [kBuffer, vBuffer, timestamp, ttl, provider] = entry;
if (Date.now() < timestamp + (ttl ?? Infinity)) {
const [keys, result, { default: driver }] =
await Promise.all([
rscSerializer.fromBuffer(
Buffer.from(kBuffer, "base64")
),
rscSerializer.fromBuffer(
Buffer.from(vBuffer, "base64")
),
typeof provider.driverPath === "string"
? import(
sys.toFileUrl(
provider.driverPath || provider.driver
)
)
: Promise.resolve({ default: null }),
]);
return useCache(keys, result, ttl ?? Infinity, false, {
...provider,
driver,
serializer:
provider?.serializer === "rsc"
? rscSerializer
: undefined,
prerenderCache: true,
});
}
expiredPrerenderCache = true;
return null; })
);
}
if (noCache || expiredPrerenderCache) {
context$(PRELUDE_HTML, null);
context$(POSTPONE_STATE, null);
}
const renderContext = createRenderContext(httpContext);
context$(RENDER_CONTEXT, renderContext);
context$(RENDER, render);
if (GlobalErrorComponent) {
useErrorComponent(GlobalErrorComponent, globalErrorModule);
}
let middlewareError = null;
try {
const middlewareHandler = await root_init$?.();
if (middlewareHandler) {
const middlewares = Array.isArray(middlewareHandler)
? middlewareHandler
: [middlewareHandler];
for (const middleware of middlewares) {
const response = await middleware(httpContext);
if (response) {
const final =
typeof response === "function"
? await response(httpContext)
: response;
// Merge headers set by earlier middlewares (via
// setHeader / appendHeader / headers()) into the
// short-circuit Response. Without this, common
// discovery headers like `Link` set by an earlier
// middleware would be silently dropped when a later
// middleware returned a Response directly.
return resolve(mergeContextHeaders(final));
}
}
}
} catch (e) {
const redirect = getContext(REDIRECT_CONTEXT);
if (redirect?.response) {
return resolve(redirect.response);
} else {
if (e instanceof Error) {
middlewareError = e;
} else {
middlewareError = new Error(
e?.message ?? "Internal Server Error",
{
cause: e,
}
);
}
}
}
if (renderContext.flags.isUnknown) {
return resolve();
}
if (getContext(POSTPONE_CONTEXT) === null) {
context$(POSTPONE_CONTEXT, true);
}
// Per-request entry selection (parity with the dev handler).
//
// Build-time vs runtime contract:
// - Build emits `render.mjs` from render-ssr.jsx ONLY when it
// decided the root is a "use client" module. When that
// happens it also emits `render-action.mjs` from render-rsc.jsx
// as a parallel entry. So `renderAction != null` is the
// signal that `render` is the SSR shortcut.
// - The shortcut's invariant requires `Component.$$typeof ===
// REACT_CLIENT_REFERENCE`. Build-time detection is a literal
// substring scan and can disagree with the runtime check
// (directive text inside a string literal, comment, etc.).
//
// Rule: the build-time decision is a *hint* about what was
// emitted; the runtime decides what to *dispatch*. Use the
// shortcut only when runtime confirms a client-reference root
// AND the request is not an action POST. Otherwise fall back
// to the full RSC entry — either via `renderAction` (when the
// build emitted the shortcut) or directly via `render` (when
// the build didn't emit the shortcut, so `render` IS the RSC
// entry).
const method = httpContext.request.method;
const isMutating = "POST,PUT,PATCH,DELETE".includes(method);
const hasActionHeader = !!httpContext.request.headers.get(
"react-server-action"
);
const isMultipart = !!httpContext.request.headers
.get("content-type")
?.includes("multipart/form-data");
const isActionRequest =
isMutating && (hasActionHeader || isMultipart);
const useShortcut = isClientRoot && !isActionRequest;
const dispatchRender = useShortcut
? render
: (renderAction ?? render);
dispatchRender(Component, {}, { middlewareError }).then(
(result) => {
resolve(result);
},
(err) => {
reject(err);
}
);
}
);
});
} catch (e) {
logger.error(e);
errorHandler(e)?.then(resolve);
}
});
};
}