sitegen/framework/lib/view.ts

100 lines
2.8 KiB
TypeScript
Raw Normal View History

2025-08-02 21:31:56 -07:00
// 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<ViewKey, View>;
scripts: Record<string, string>;
regenTtls: Ttl[];
regenTags: Record<string, ViewKey[]>;
}
2025-07-07 20:58:02 -07:00
export interface View {
component: render.Component;
2025-07-07 20:58:02 -07:00
meta:
| meta.Meta
| ((props: { context?: hono.Context }) => Promise<meta.Meta> | meta.Meta);
layout?: render.Component;
2025-07-07 20:58:02 -07:00
inlineCss: string;
scripts: Record<string, string>;
}
2025-08-02 21:31:56 -07:00
export interface Ttl {
seconds: number;
key: ViewKey;
}
type ViewKey = keyof ViewMap;
2025-07-07 20:58:02 -07:00
2025-08-02 21:31:56 -07:00
export async function renderView<K extends ViewKey>(
2025-07-07 20:58:02 -07:00
context: hono.Context,
2025-08-02 21:31:56 -07:00
id: K,
props: PropsFromModule<ViewMap[K]>,
2025-07-07 20:58:02 -07:00
) {
return context.html(await renderViewToString(id, { context, ...props }));
}
2025-08-02 21:31:56 -07:00
type PropsFromModule<M extends any> = M extends {
default: (props: infer T) => render.Node;
}
? T
: never;
export async function renderViewToString<K extends ViewKey>(
id: K,
props: PropsFromModule<ViewMap[K]>,
2025-07-07 20:58:02 -07:00
) {
// 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,
2025-08-02 21:31:56 -07:00
}: View = UNWRAP(codegen.views[id], `Missing view ${id}`);
2025-07-07 20:58:02 -07:00
// -- 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() });
2025-07-07 20:58:02 -07:00
// -- join document and send --
2025-08-02 21:31:56 -07:00
return sg.wrapDocument({
2025-07-07 20:58:02 -07:00
body,
head: await renderedMetaPromise,
inlineCss,
scripts: joinScripts(
Array.from(sitegen.scripts, (id) =>
2025-08-02 21:31:56 -07:00
UNWRAP(codegen.scripts[id], `Missing script ${id}`),
2025-07-07 20:58:02 -07:00
),
),
});
}
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";
2025-07-07 20:58:02 -07:00
import * as sg from "./sitegen.ts";
2025-08-02 21:31:56 -07:00
import type { RegisteredViews as ViewMap } from "../../.clover/ts/view.d.ts";