// The "view" system allows rendering dynamic pages within backends. // This is done by scanning all `views` dirs, bundling their client // resources, and then providing `renderView` which renders a page. // // This system also implements page regeneration. let codegen: Codegen; try { codegen = require("$views"); } catch { throw new Error("Can only import '#sitegen/view' in backends."); } // Generated in `bundle.ts` export interface Codegen { views: Record; scripts: Record; regenTtls: Ttl[]; regenTags: Record; } export interface View { component: render.Component; meta: | meta.Meta | ((props: { context?: hono.Context }) => Promise | meta.Meta); layout?: render.Component; inlineCss: string; scripts: Record; } export interface Ttl { seconds: number; key: ViewKey; } type ViewKey = keyof ViewMap; export async function renderView( context: hono.Context, id: K, props: PropsFromModule, ) { return context.html(await renderViewToString(id, { context, ...props })); } type PropsFromModule = M extends { default: (props: infer T) => render.Node; } ? T : never; export async function renderViewToString( id: K, props: PropsFromModule, ) { // The view contains pre-bundled CSS and scripts, but keeps the scripts // separate for run-time dynamic scripts. For example, the file viewer // includes the canvas for the current page, but only the current page. const { component, inlineCss, layout, meta: metadata, }: View = UNWRAP(codegen.views[id], `Missing view ${id}`); // -- metadata -- const renderedMetaPromise = Promise.resolve( typeof metadata === "function" ? metadata(props) : metadata, ).then((m) => meta.renderMeta(m)); // -- html -- let page: render.Element = render.element(component, props); if (layout) page = render.element(layout, { children: page }); const { text: body, addon: { [sg.userData.key]: sitegen }, } = await render.async(page, { [sg.userData.key]: sg.initRender() }); // -- join document and send -- return sg.wrapDocument({ body, head: await renderedMetaPromise, inlineCss, scripts: joinScripts( Array.from(sitegen.scripts, (id) => UNWRAP(codegen.scripts[id], `Missing script ${id}`), ), ), }); } export function joinScripts(scriptSources: string[]) { const { length } = scriptSources; if (length === 0) return ""; if (length === 1) return scriptSources[0]; return scriptSources.map((source) => `{${source}}`).join(";"); } import * as meta from "./meta.ts"; import type * as hono from "#hono"; import * as render from "#engine/render"; import * as sg from "./sitegen.ts"; import type { RegisteredViews as ViewMap } from "../../.clover/ts/view.d.ts";