74 lines
2.7 KiB
TypeScript
74 lines
2.7 KiB
TypeScript
import { partialPrerender } from "@wrnexus/ssr";
|
|
import {
|
|
fillSlots,
|
|
normalizeComponentName,
|
|
parseComponentProps,
|
|
readElementBody,
|
|
} from "./runtime.ts";
|
|
|
|
export interface PartialBuildModule {
|
|
default?: unknown;
|
|
render?: (props?: Record<string, unknown>) => string | Promise<string>;
|
|
layout?: string | { name?: string; render?: (props?: Record<string, unknown>) => string };
|
|
__wrnexusBuildStaticShell?: (ctx?: Record<string, unknown>) => string | Promise<string>;
|
|
}
|
|
|
|
export interface PartialBuildEntry {
|
|
name: string;
|
|
mod: PartialBuildModule;
|
|
}
|
|
|
|
const MOUNT_OPEN_RE =
|
|
/<([A-Za-z][A-Za-z0-9-]*)\b([^>]*?\bdata-component="([A-Za-z0-9_-]+)"[^>]*?)(\/?)>/;
|
|
|
|
/** Expand compiler component mounts at build time using only their pure render exports. */
|
|
export async function expandStaticComponents(
|
|
html: string,
|
|
components: readonly PartialBuildEntry[],
|
|
depth = 0,
|
|
): Promise<string> {
|
|
if (depth > 15) throw new Error("WRN-PARTIAL-STATIC-DEPTH: component nesting exceeds 15");
|
|
if (!html.includes("data-component=")) return html;
|
|
let output = "";
|
|
let cursor = 0;
|
|
for (;;) {
|
|
const match = MOUNT_OPEN_RE.exec(html.slice(cursor));
|
|
if (!match) return output + html.slice(cursor);
|
|
const start = cursor + match.index;
|
|
output += html.slice(cursor, start);
|
|
const [open, tag, attributes, name, selfClosing] = match;
|
|
const openEnd = start + open.length;
|
|
const body =
|
|
selfClosing === "/" ? { inner: "", end: openEnd } : readElementBody(html, tag!, openEnd);
|
|
const component = components.find(
|
|
(entry) => normalizeComponentName(entry.name) === normalizeComponentName(name!),
|
|
);
|
|
if (!component || typeof component.mod.render !== "function") {
|
|
throw new Error(`WRN-PARTIAL-STATIC-COMPONENT: '${name}' has no build-time renderer`);
|
|
}
|
|
const rendered = await component.mod.render(parseComponentProps(attributes!));
|
|
output += await expandStaticComponents(
|
|
fillSlots(String(rendered), body.inner),
|
|
components,
|
|
depth + 1,
|
|
);
|
|
cursor = body.end;
|
|
}
|
|
}
|
|
|
|
/** Produce the body shell stored in dist; dynamic region bodies are never evaluated here. */
|
|
export async function precomputePartialStaticShell(
|
|
page: PartialBuildModule,
|
|
components: readonly PartialBuildEntry[],
|
|
): Promise<{ shell: string; regions: number }> {
|
|
if (typeof page.__wrnexusBuildStaticShell !== "function") {
|
|
throw new Error("WRN-PARTIAL-STATIC-EXPORT: compiler did not emit a static-shell renderer");
|
|
}
|
|
const body = await expandStaticComponents(
|
|
String(await page.__wrnexusBuildStaticShell({})),
|
|
components,
|
|
);
|
|
const result = partialPrerender(body);
|
|
return { shell: result.shell, regions: result.regions.length };
|
|
}
|