// Partial pre-rendering allows a component with dynamic properties to // have its static components rendered statically in a two pass render. // The pre-rendering pass takes in `placeholder` objects (eg, request // context or view data) and captures which components access those // states, serializing the inputs alongside. This process tracks what // file and export name the component came from so that rendering may // resume from another process. /** Placeholders can be passed around in component props, * but throw a `PlaceholderError` when trying to read it. */ export function placeholder(id: string): T { return new Proxy({ [kPlaceholder]: id }, throwOnInteractHandlers) as T; } class PlaceholderError extends Error { /* @ts-expect-error */ declare stack: NodeJS.CallSite[]; constructor(public id: string) { super("This action must be delayed to runtime."); const _prepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (_, stack) => stack; void (/* volatile */ this.stack); Error.prepareStackTrace = _prepareStackTrace; } } const kPlaceholder = Symbol("Placeholder"); function throwSelf(target: { [kPlaceholder]: string }): never { throw new PlaceholderError(target[kPlaceholder]); } const throwOnInteractHandlers: ProxyHandler<{ [kPlaceholder]: string }> = { get: (t, k) => (k === kPlaceholder ? t[kPlaceholder] : throwSelf(t)), apply: throwSelf, construct: throwSelf, defineProperty: throwSelf, deleteProperty: throwSelf, getOwnPropertyDescriptor: throwSelf, getPrototypeOf: throwSelf, has: throwSelf, isExtensible: throwSelf, ownKeys: throwSelf, preventExtensions: throwSelf, set: throwSelf, setPrototypeOf: throwSelf, }; function isToken(token: unknown): string | null { if (!!token && typeof token === "object") { const value = (token as { [kPlaceholder]: unknown })[kPlaceholder]; if (typeof value === "string") return value; } return null; } interface State extends render.State { prerenderStack: Frame[]; } interface Frame {} export function Component({ meow }: { meow: number }) { return (

app shell

); } export function Thing({ bwaa }: { bwaa: number }) { return
Thing is {bwaa * 2}
; } export async function main() { prepare(("meow")} />); } interface Slot { file: string; key: string; props: unknown; } function prepare(node: render.Node) { const token = "\0" + crypto.randomUUID().slice(0, 8); const slots: Slot[] = []; const state = render.init(true, {}); state.resolveNode = (s, node) => { try { return render.resolveNode(s, node); } catch (err) { if (err instanceof PlaceholderError) { // If not a component element, forward the error higher. if ( !Array.isArray(node) || node[0] != render.kElement || typeof node[1] !== "function" ) throw err; const fn = node[1]; // Locate the file in the stack trace. Since it was // called directly by `resolveNode`, it should be here. const frame = UNWRAP(err.stack.find((f) => f.getFunction() === fn)); // Use reflection to get the module's export name. const fileName = UNWRAP(frame.getFileName()); const module = UNWRAP(require.cache[fileName]); const key = Object.entries(module.exports) .find(([, v]) => v === fn)?.[0] ?.toString(); if (!key) { throw new Error( `To enable partial pre-rendering of <${fn.name} />, ` + `export it in ${incr.toRel(fileName)} so resuming will be ` + `able to import and reference it directly.`, ); } slots.push({ file: incr.toRel(fileName), key, props: node[2] }); // Return a 'render token' in place. The output stream is split on these. return token + (slots.length - 1).toString().padStart(4, "0"); } throw err; } }; const statics = []; const resumes = []; { const treeWithPpr = state.resolveNode(state, node); const baseShell = render.stringifyNode(treeWithPpr); const regexp = new RegExp(`${token}(\\d{4})`, "g"); let last = 0; for (const match of baseShell.matchAll(regexp)) { const { index, [0]: { length }, [1]: slotIndexString, } = match; const slotIndex = parseInt(UNWRAP(slotIndexString), 10); const slot = UNWRAP(slots[slotIndex], `Slot ${slotIndex}`); resumes.push(slot); statics.push(baseShell.slice(last, index)); last = index + length; } const end = baseShell.slice(last); if (end) statics.push(end); } console.log({ parts: statics, slots }); } import * as render from "#engine/render"; import * as incr from "../incremental.ts";