the asset system is reworked to support "dynamic" entries, where each entry is a separate file on disk containing the latest generation's headers+raw+gzip+zstd. when calling view.regenerate, it will look for pages that had "export const regenerate" during generation, and render those pages using the view system, but then store the results as assets instead of sending as a response. pages configured as regenerable are also bundled as views, using the non-aliasing key "page:${page.id}". this cannot alias because file paths may not contain a colon.
121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
// 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<PropsFromModule<ViewMap[K]>> };
|
|
scripts: Record<string, string>;
|
|
regenTtls: Ttl[];
|
|
regenTags: Record<RegenKey, Key[]>;
|
|
}
|
|
// 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<Props extends Record<string, unknown>> {
|
|
component: render.Component;
|
|
meta: meta.Meta | ((props: Props) => Promise<meta.Meta> | meta.Meta);
|
|
layout?: render.Component;
|
|
inlineCss: string;
|
|
scripts: Record<string, string>;
|
|
}
|
|
export interface Ttl {
|
|
seconds: number;
|
|
key: Key;
|
|
}
|
|
export type Key = keyof ViewMap;
|
|
|
|
export async function serve<K extends Key>(
|
|
context: hono.Context,
|
|
id: K,
|
|
props: PropsFromModule<ViewMap[K]>,
|
|
) {
|
|
return context.html(await renderToString(id, { context, ...props }));
|
|
}
|
|
|
|
type PropsFromModule<M extends any> = M extends {
|
|
default: (props: infer T) => render.Node;
|
|
} ? T
|
|
: never;
|
|
|
|
export async function renderToString<K extends Key>(
|
|
id: K,
|
|
props: PropsFromModule<ViewMap[K]>,
|
|
) {
|
|
const {
|
|
component,
|
|
inlineCss,
|
|
layout,
|
|
meta: metadata,
|
|
}: View<PropsFromModule<ViewMap[K]>> = 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 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";
|