// The "view" system allows rendering dynamic pages within backends. // This is done by scanning all `views` dirs, bundling their client // resources, and then providing `serve` 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: { [K in Key]: View> }; scripts: Record; metaTemplate: meta.Template; regenTtls: Ttl[]; regenTags: Record; } // 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. export interface View> { component: render.Component; meta: meta.Meta | ((props: Props) => Promise | meta.Meta); layout?: render.Component; inlineCss: string; scripts: Record; } export interface Ttl { seconds: number; key: Key; } export type Key = keyof ViewMap; export async function serve( context: hono.Context, id: K, props: PropsFromModule, ) { return context.html(await renderToString(id, { context, ...props }), { headers: { "Content-Type": "text/html;charset=utf8", }, }); } type PropsFromModule = M extends { default: (props: infer T) => render.Node; } ? T : never; export async function renderToString( id: K, props: PropsFromModule, ) { 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.render(m, codegen.metaTemplate)); // -- 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 regenerate(tag: RegenKey) { for (const view of codegen.regenTags[tag]) { const key = view.slice("page:".length); renderToString(view, {}) .then((result) => { console.info(`regenerate ${key}`); asset.overwriteDynamic(key as asset.Key, result, { "content-type": "text/html", }); }) .catch((e) => { console.error(`Failed regenerating ${view} from tag ${tag}`, e); }); } } function joinScripts(scriptSources: string[]) { const { length } = scriptSources; if (length === 0) return ""; if (0 in scriptSources) 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 * as asset from "./assets.ts"; import type { RegenKey, RegisteredViews as ViewMap, } from "../../.clover/ts/view.d.ts";