1// The "view" system allows rendering dynamic pages within backends.
2// This is done by scanning all `views` dirs, bundling their client
3// resources, and then providing `serve` which renders a page.
4//
5// This system also implements page regeneration.
6let codegen: Codegen;
7try {
8 codegen = require("$views");
9} catch (e) {
10 console.error(e);
11 throw new Error("Can only import '#sitegen/view' in backends.");
12}
13
14// Generated in `bundle.ts`
15export interface Codegen {
16 views: { [K in Key]: View<PropsFromModule<ViewMap[K]>> };
17 scripts: Record<string, string>;
18 metaTemplate: meta.Template;
19 regenTtls: Ttl[];
20 regenTags: Record<RegenKey, Key[]>;
21}
22// The view contains pre-bundled CSS and scripts, but keeps the scripts
23// separate for run-time dynamic scripts. For example, the file viewer
24// includes the canvas for the current page, but only the current page.
25export interface View<Props extends Record<string, unknown>> {
26 component: Marko.Component<Props>;
27 meta: meta.Meta | ((props: Props) => Promise<meta.Meta> | meta.Meta);
28 inlineCss: string;
29 scripts: Record<string, string>;
30}
31export interface Ttl {
32 seconds: number;
33 key: Key;
34}
35export type Key = keyof ViewMap;
36
37export async function serve<K extends Key>(
38 context: hono.Context,
39 id: K,
40 props: Record<string, unknown>,
41) {
42 return context.html(await renderToString(id, { context, ...props }), {
43 headers: {
44 "Content-Type": "text/html;charset=utf8",
45 },
46 });
47}
48
49export async function renderToString<K extends Key>(
50 id: K,
51 attrs: Record<string, unknown>,
52) {
53 const {
54 component: Page,
55 inlineCss,
56 meta: metadata,
57 }: View<Record<string, unknown>> = UNWRAP(
58 codegen.views[id],
59 `Missing view ${id}`,
60 );
61
62 // -- metadata --
63 const renderedMetaPromise = Promise.resolve(
64 typeof metadata === "function" ? metadata(attrs) : metadata,
65 ).then((m) => meta.toHtml(codegen.metaTemplate, m));
66
67 const cloverScripts = new Set<string>();
68 const text = await SitegenWrap.render({
69 cloverScripts,
70 page: Page,
71 attrs,
72 });
73
74 // -- join document and send --
75 return sg.wrapDocument({
76 body: text,
77 head: await renderedMetaPromise,
78 inlineCss,
79 scripts: sg.joinScripts(
80 Array.from(
81 cloverScripts,
82 (id) => UNWRAP(codegen.scripts[id], `Missing script ${id}`),
83 ),
84 ),
85 });
86}
87
88export function regenerate(tag: RegenKey) {
89 for (const view of codegen.regenTags[tag]) {
90 const key = view.slice("page:".length);
91 renderToString(view, {})
92 .then((result) => {
93 console.info(`regenerate ${key}`);
94 asset.overwriteDynamic(key as asset.Key, result, {
95 "content-type": "text/html",
96 });
97 })
98 .catch((e) => {
99 console.error(`Failed regenerating ${view} from tag ${tag}`, e);
100 });
101 }
102}
103
104import { UNWRAP } from "@clo/lib/assert";
105import * as meta from "@clo/lib/meta";
106import type * as hono from "hono";
107import type { RegenKey, RegisteredViews as ViewMap } from "../../.clover/ts/view.d.ts";
108import SitegenWrap from "../sitegen-wrap.marko";
109import * as asset from "./assets.ts";
110import * as sg from "./sitegen.ts";