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.
139 lines
3.8 KiB
TypeScript
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";
|