prerenderPage

tcmts.cli.web.prerenderPage · #db3jbg47 · term · runs on the server

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

used by