prerender
uses node:url
Render a component app's pages, as tcm start renders a page's first paint, for a static site: from /, every
page of the app it links to (linkedPaths), each its HTML and the trees its server components rendered (by
treeName, Flight's text), which the app reads when it goes to another page, and which trees each page uses.
page is the page the build has, under the site's base.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { decode } from "#codebase/tcmts/cli/codec"; import { useFlight } from "#codebase/tcmts/cli/flight"; import { treeName } from "#codebase/tcmts/cli/rsc"; import { appsDir, linkedPaths, pageTreesOf, renderedPage } from "#codebase/tcmts/cli/web"; import type { BuildManifest, ComponentPlan, RenderApp } from "#codebase/tcmts/cli/web"; import path from "node:path"; import { pathToFileURL } from "node:url"; export async function prerender( cb: Codebase, build: BuildManifest, page: string, base: string, ): Promise<{ pages: Map<string, string>; trees: Map<string, string>; uses: Map<string, string[]>; warnings: string[]; }> { const { render } = (await import( pathToFileURL(path.join(appsDir(cb), build.hash, build.ssr!)).href )) as { render: RenderApp; }; const components = new Set(build.components ?? []); const refs = new Set(build.clientRefs ?? []); // (as `tcm start` renders them: production's Flight, whose trees carry no debugging information) process.env.NODE_ENV ??= "production"; const given = new Set(build.serverRefs ?? []); useFlight(cb, { client: (h) => refs.has(h), server: (h) => given.has(h), chunks: (c) => build.chunks?.[c] ?? [], packageImports: (c) => build.packageImports?.[c] ?? [], }); const plan: ComponentPlan = { isComponent: (h) => components.has(h), isClientRef: (h) => refs.has(h), }; const pages = new Map<string, string>(); const trees = new Map<string, string>(); const uses = new Map<string, string[]>(); const warnings: string[] = []; const g = globalThis as { __TCM_BASE__?: string }; const was = g.__TCM_BASE__; // (server rendering reads the base from here, as under `tcm start --base`) if (base) g.__TCM_BASE__ = base; else delete g.__TCM_BASE__; try { const queue = ["/"]; const seen = new Set(queue); while (queue.length) { const at = queue.shift()!; let html = (await renderedPage(cb, at, { host: "tcm.static" }, page, render, plan, { production: true, secure: false, ...(base ? { base } : {}), })) ?? ""; if (!html) { warnings.push( `${at} can't render before its code runs (see above), so it renders in the browser`, ); html = page; } // cookies set after the page went out can't be collected: there's no server if (/var __TCM_COOKIES__/.test(html)) { warnings.push(`${at} sets cookies as it renders, which a static site can't`); html = html.replace(/ var __TCM_COOKIES__ = "[^"]*";/, ""); } const recorded = pageTreesOf(html); if (recorded) { const byKey = decode(recorded) as Record<string, string>; for (const [key, tree] of Object.entries(byKey)) trees.set(treeName(key), tree); uses.set(at, Object.keys(byKey).map(treeName)); } pages.set(at, html); for (const p of linkedPaths(html, `${base}${at}`, base)) { if (seen.has(p)) continue; seen.add(p); if (seen.size > 10000) { warnings.push("more than 10000 pages: the rest aren't rendered"); break; } queue.push(p); } } } finally { if (was === undefined) delete g.__TCM_BASE__; else g.__TCM_BASE__ = was; } return { pages, trees, uses, warnings }; }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.codec.decode
- tcmts.cli.flight.useFlight
- tcmts.cli.rsc.treeName
- tcmts.cli.web.BuildManifest
- tcmts.cli.web.ComponentPlan
- tcmts.cli.web.RenderApp
- tcmts.cli.web.appsDir
- tcmts.cli.web.linkedPaths
- tcmts.cli.web.pageTreesOf
- tcmts.cli.web.renderedPage