prerenderPage
uses node:url
A page's shell (url): what renders the same for every request, rendered once, as partial prerendering does. Its
server components that render the same for the same props (isCacheable) render in it; the rest, live (they read
tables, cookies, the clock), are holes: they suspend, so their nearest <Suspense> boundary is left for each
request to render (renderPage, given the shell). Undefined when the shell itself waits on a live one (one
outside any boundary): the page is rendered whole, each time.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { decode } from "#codebase/tcmts/cli/codec"; import { dataDir, errorReply, installSsr, isHttpError, renderComponent, serverApiHost, ssrRequests, } from "#codebase/tcmts/cli/web"; import type { ComponentPlan, PartialRender, RpcOptions, Shell, SsrRequest } from "#codebase/tcmts/cli/web"; import { exportId } from "#codebase/tcmts/core/hash"; import { pathToFileURL } from "node:url"; export async function prerenderPage( cb: Codebase, url: URL, components: ComponentPlan, opts: RpcOptions, ssr: PartialRender, ): Promise<Shell | undefined> { installSsr(); const host = await serverApiHost(cb)(); const trees: SsrRequest["trees"] = {}; const abort = new AbortController(); // done when nothing that renders the same is still rendering: what's left is live let pending = 0; const settle = () => { if (pending) return; setImmediate(() => setImmediate(() => { if (!pending && !abort.signal.aborted) abort.abort(new Error("the rest is live")); }), ); }; // (what renders the same reads nothing of the request: one for all of them, so React's `cache` is shared) const call = { method: "GET", url: url.pathname, headers: {}, ip: undefined, signal: abort.signal, }; const page: SsrRequest = { url, cache: new Map(), trees, async render(hash, body, write) { // a live one: a hole, rendered for each request if (!components.isCacheable?.(hash)) return new Promise<void>(() => {}); pending++; try { const rec = cb.store.getDef(hash)!; const fn = ( (await import(pathToFileURL(cb.modulePath(rec.componentHash)).href)) as Record< string, (...a: unknown[]) => unknown > )[exportId(hash)]!; const key = hash + body; trees[key] = ""; const name = cb.displayRef(`#${hash}`); const describe = (e: unknown) => errorReply(e as Error, isHttpError(e), name, opts.production).body; const args = decode(body) as unknown[]; const r = await host.withCall( call, { secure: opts.secure, dataDir: dataDir(cb), cookiePath: opts.base }, () => renderComponent( cb, fn, args[0], describe, (text) => { trees[key] += text; write(text); }, abort.signal, ), ); if (!r.ok) throw r.error; } finally { pending--; settle(); } }, }; settle(); try { const shell = await ssrRequests.run(page, () => ssr.prerenderShell(abort.signal, () => {})); return shell.html ? { html: shell.html, postponed: shell.postponed === null ? null : JSON.stringify(shell.postponed), trees, } : undefined; } catch { return undefined; } }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.codec.decode
- tcmts.cli.web.ComponentPlan
- tcmts.cli.web.PartialRender
- tcmts.cli.web.RpcOptions
- tcmts.cli.web.Shell
- tcmts.cli.web.SsrRequest
- tcmts.cli.web.dataDir
- tcmts.cli.web.errorReply
- tcmts.cli.web.installSsr
- tcmts.cli.web.isHttpError
- tcmts.cli.web.renderComponent
- tcmts.cli.web.serverApiHost
- tcmts.cli.web.ssrRequests
- tcmts.core.hash.exportId