sitegen/framework/lib/view.ts
clover caruso 30ad9c27ff chore: rework Clover Engine API, remove "SSR" term
"server side rendering" is a misleading term since it implies there is a
server. that isn't neccecarily the case here, since it supports running
in the browser. I think "clover engine" is cute, short for "clover html
rendering engine". Instead of "server side rendering", it's just rendering.

This commit makes things a lot more concise, such as `ssr.ssrAsync`
being renamed to `render.async` to play nicely with namespaced imports.
`getCurrentRender` and `setCurrentRender` are just `current` and
`setCurrent`, and the addon interface has been redesigned to force
symbols with a wrapping helper.
2025-08-02 22:22:07 -04:00

95 lines
2.5 KiB
TypeScript

export interface View {
component: render.Component;
meta:
| meta.Meta
| ((props: { context?: hono.Context }) => Promise<meta.Meta> | meta.Meta);
layout?: render.Component;
inlineCss: string;
scripts: Record<string, string>;
}
let views: Record<string, View> = null!;
let scripts: Record<string, string> = null!;
export async function renderView(
context: hono.Context,
id: string,
props: Record<string, unknown>,
) {
return context.html(await renderViewToString(id, { context, ...props }));
}
export async function renderViewToString(
id: string,
props: Record<string, unknown>,
) {
views ?? ({ views, scripts } = require("$views"));
// 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(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 wrapDocument({
body,
head: await renderedMetaPromise,
inlineCss,
scripts: joinScripts(
Array.from(sitegen.scripts, (id) =>
UNWRAP(scripts[id], `Missing script ${id}`),
),
),
});
}
export function provideViewData(v: typeof views, s: typeof scripts) {
(views = v), (scripts = s);
}
export function joinScripts(scriptSources: string[]) {
const { length } = scriptSources;
if (length === 0) return "";
if (length === 1) return scriptSources[0];
return scriptSources.map((source) => `{${source}}`).join(";");
}
export function wrapDocument({
body,
head,
inlineCss,
scripts,
}: {
head: string;
body: string;
inlineCss: string;
scripts: string;
}) {
return `<!doctype html><html lang=en><head>${head}${
inlineCss ? `<style>${inlineCss}</style>` : ""
}</head><body>${body}${
scripts ? `<script>${scripts}</script>` : ""
}</body></html>`;
}
import * as meta from "./meta.ts";
import type * as hono from "#hono";
import * as render from "#engine/render";
import * as sg from "./sitegen.ts";