sitegen/framework/engine/marko-runtime.ts
chloe caruso c5ac450f21 feat: dynamic page regeneration (#24)
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.
2025-08-11 22:43:27 -07:00

139 lines
3.8 KiB
TypeScript

// This file is used to integrate Marko into the Clover Engine and Sitegen
// To use, replace the "marko/html" import with this file.
export * from "#marko/html";
interface BodyContentObject {
[x: PropertyKey]: unknown;
content: ServerRenderer;
}
export const createTemplate = (
templateId: string,
renderer: ServerRenderer,
) => {
const { render: renderFn } = marko.createTemplate(templateId, renderer);
function wrap(props: Record<string, unknown>, n: number) {
// Marko Custom Tags
const cloverAsyncMarker = { isAsync: false };
const r = render.current;
// Support using Marko outside of Clover SSR
if (!r) return renderer(props, n);
render.setCurrent(null);
const markoResult = renderFn.call(renderer, {
...props,
$global: { clover: r, cloverAsyncMarker },
});
if (cloverAsyncMarker.isAsync) {
return markoResult.then(render.raw);
}
const rr = markoResult.toString();
return render.raw(rr);
}
wrap.render = render;
wrap.unwrapped = renderer;
return wrap;
};
export const dynamicTag = (
scopeId: number,
accessor: Accessor,
tag: unknown | string | ServerRenderer | BodyContentObject,
inputOrArgs: unknown,
content?: (() => void) | 0,
inputIsArgs?: 1,
serializeReason?: 1 | 0,
) => {
if (typeof tag === "function") {
clover: {
const unwrapped = (tag as any).unwrapped;
if (unwrapped) {
tag = unwrapped;
break clover;
}
const r = render.current ?? (marko.$global().clover as render.State);
if (!r) throw new Error("No Clover Render Active");
const subRender = render.init(r.async !== -1, r.addon);
const resolved = render.resolveNode(
subRender,
render.element(
tag as render.Component,
inputOrArgs as Record<any, any>,
),
);
if (subRender.async > 0) {
const marker = marko.$global().cloverAsyncMarker as Async;
marker.isAsync = true;
// Wait for async work to finish
const { resolve, reject, promise } = Promise.withResolvers<string>();
subRender.asyncDone = () => {
const rejections = subRender.rejections;
if (!rejections) return resolve(render.stringifyNode(resolved));
(r.rejections ??= []).push(...rejections);
return reject(new Error("Render had errors"));
};
marko.fork(
scopeId,
accessor,
promise,
(string: string) => marko.write(string),
0,
);
} else {
marko.write(render.stringifyNode(resolved));
}
return;
}
}
return marko.dynamicTag(
scopeId,
accessor,
tag,
inputOrArgs,
content,
inputIsArgs,
serializeReason,
);
};
export function fork(
scopeId: number,
accessor: Accessor,
promise: Promise<unknown>,
callback: (data: unknown) => void,
serializeMarker?: 0 | 1,
) {
const marker = marko.$global().cloverAsyncMarker as Async;
marker.isAsync = true;
marko.fork(scopeId, accessor, promise, callback, serializeMarker);
}
export function escapeXML(input: unknown) {
// The rationale of this check is that the default toString method
// creating `[object Object]` is universally useless to any end user.
if (
input == null ||
(typeof input === "object" &&
input &&
// only block this if it's the default `toString`
input.toString === Object.prototype.toString)
) {
throw new Error(
`Unexpected value in template placeholder: '` +
render.inspect(input) +
"'. " +
`To emit a literal '${input}', use \${String(value)}`,
);
}
return marko.escapeXML(input);
}
interface Async {
isAsync: boolean;
}
import * as render from "#engine/render";
import type { ServerRenderer } from "marko/html/template";
import { type Accessor } from "marko/common/types";
import * as marko from "#marko/html";