Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
ecdf9a77f1 |
3 changed files with 161 additions and 6 deletions
152
framework/engine/prerender.tsx
Normal file
152
framework/engine/prerender.tsx
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
// Partial pre-rendering allows a component with dynamic properties to
|
||||||
|
// have its static components rendered statically in a two pass render.
|
||||||
|
// The pre-rendering pass takes in `placeholder` objects (eg, request
|
||||||
|
// context or view data) and captures which components access those
|
||||||
|
// states, serializing the inputs alongside. This process tracks what
|
||||||
|
// file and export name the component came from so that rendering may
|
||||||
|
// resume from another process.
|
||||||
|
|
||||||
|
/** Placeholders can be passed around in component props,
|
||||||
|
* but throw a `PlaceholderError` when trying to read it. */
|
||||||
|
export function placeholder<T extends object>(id: string): T {
|
||||||
|
return new Proxy({ [kPlaceholder]: id }, throwOnInteractHandlers) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaceholderError extends Error {
|
||||||
|
/* @ts-expect-error */
|
||||||
|
declare stack: NodeJS.CallSite[];
|
||||||
|
|
||||||
|
constructor(public id: string) {
|
||||||
|
super("This action must be delayed to runtime.");
|
||||||
|
|
||||||
|
const _prepareStackTrace = Error.prepareStackTrace;
|
||||||
|
Error.prepareStackTrace = (_, stack) => stack;
|
||||||
|
void (/* volatile */ this.stack);
|
||||||
|
Error.prepareStackTrace = _prepareStackTrace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kPlaceholder = Symbol("Placeholder");
|
||||||
|
function throwSelf(target: { [kPlaceholder]: string }): never {
|
||||||
|
throw new PlaceholderError(target[kPlaceholder]);
|
||||||
|
}
|
||||||
|
const throwOnInteractHandlers: ProxyHandler<{ [kPlaceholder]: string }> = {
|
||||||
|
get: (t, k) => (k === kPlaceholder ? t[kPlaceholder] : throwSelf(t)),
|
||||||
|
apply: throwSelf,
|
||||||
|
construct: throwSelf,
|
||||||
|
defineProperty: throwSelf,
|
||||||
|
deleteProperty: throwSelf,
|
||||||
|
getOwnPropertyDescriptor: throwSelf,
|
||||||
|
getPrototypeOf: throwSelf,
|
||||||
|
has: throwSelf,
|
||||||
|
isExtensible: throwSelf,
|
||||||
|
ownKeys: throwSelf,
|
||||||
|
preventExtensions: throwSelf,
|
||||||
|
set: throwSelf,
|
||||||
|
setPrototypeOf: throwSelf,
|
||||||
|
};
|
||||||
|
function isToken(token: unknown): string | null {
|
||||||
|
if (!!token && typeof token === "object") {
|
||||||
|
const value = (token as { [kPlaceholder]: unknown })[kPlaceholder];
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State extends render.State {
|
||||||
|
prerenderStack: Frame[];
|
||||||
|
}
|
||||||
|
interface Frame {}
|
||||||
|
|
||||||
|
export function Component({ meow }: { meow: number }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>app shell</h1>
|
||||||
|
<Thing bwaa={meow} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Thing({ bwaa }: { bwaa: number }) {
|
||||||
|
return <div>Thing is {bwaa * 2}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function main() {
|
||||||
|
prepare(<Component meow={placeholder<number>("meow")} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Slot {
|
||||||
|
file: string;
|
||||||
|
key: string;
|
||||||
|
props: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepare(node: render.Node) {
|
||||||
|
const token = "\0" + crypto.randomUUID().slice(0, 8);
|
||||||
|
const slots: Slot[] = [];
|
||||||
|
const state = render.init(true, {});
|
||||||
|
state.resolveNode = (s, node) => {
|
||||||
|
try {
|
||||||
|
return render.resolveNode(s, node);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof PlaceholderError) {
|
||||||
|
// If not a component element, forward the error higher.
|
||||||
|
if (
|
||||||
|
!Array.isArray(node) ||
|
||||||
|
node[0] != render.kElement ||
|
||||||
|
typeof node[1] !== "function"
|
||||||
|
)
|
||||||
|
throw err;
|
||||||
|
const fn = node[1];
|
||||||
|
// Locate the file in the stack trace. Since it was
|
||||||
|
// called directly by `resolveNode`, it should be here.
|
||||||
|
const frame = UNWRAP(err.stack.find((f) => f.getFunction() === fn));
|
||||||
|
// Use reflection to get the module's export name.
|
||||||
|
const fileName = UNWRAP(frame.getFileName());
|
||||||
|
const module = UNWRAP(require.cache[fileName]);
|
||||||
|
const key = Object.entries(module.exports)
|
||||||
|
.find(([, v]) => v === fn)?.[0]
|
||||||
|
?.toString();
|
||||||
|
if (!key) {
|
||||||
|
throw new Error(
|
||||||
|
`To enable partial pre-rendering of <${fn.name} />, ` +
|
||||||
|
`export it in ${incr.toRel(fileName)} so resuming will be ` +
|
||||||
|
`able to import and reference it directly.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
slots.push({ file: incr.toRel(fileName), key, props: node[2] });
|
||||||
|
// Return a 'render token' in place. The output stream is split on these.
|
||||||
|
return token + (slots.length - 1).toString().padStart(4, "0");
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statics = [];
|
||||||
|
const resumes = [];
|
||||||
|
{
|
||||||
|
const treeWithPpr = state.resolveNode(state, node);
|
||||||
|
const baseShell = render.stringifyNode(treeWithPpr);
|
||||||
|
const regexp = new RegExp(`${token}(\\d{4})`, "g");
|
||||||
|
let last = 0;
|
||||||
|
for (const match of baseShell.matchAll(regexp)) {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
[0]: { length },
|
||||||
|
[1]: slotIndexString,
|
||||||
|
} = match;
|
||||||
|
const slotIndex = parseInt(UNWRAP(slotIndexString), 10);
|
||||||
|
const slot = UNWRAP(slots[slotIndex], `Slot ${slotIndex}`);
|
||||||
|
resumes.push(slot);
|
||||||
|
statics.push(baseShell.slice(last, index));
|
||||||
|
last = index + length;
|
||||||
|
}
|
||||||
|
const end = baseShell.slice(last);
|
||||||
|
if (end) statics.push(end);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({ parts: statics, slots });
|
||||||
|
}
|
||||||
|
|
||||||
|
import * as render from "#engine/render";
|
||||||
|
import * as incr from "../incremental.ts";
|
|
@ -76,6 +76,8 @@ export interface State {
|
||||||
rejections: unknown[] | null;
|
rejections: unknown[] | null;
|
||||||
/** Add-ons to the rendering engine store state here */
|
/** Add-ons to the rendering engine store state here */
|
||||||
addon: Addons;
|
addon: Addons;
|
||||||
|
/** Can be overridden to inject logic. */
|
||||||
|
resolveNode: typeof resolveNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const kElement = Symbol("Element");
|
export const kElement = Symbol("Element");
|
||||||
|
@ -101,7 +103,7 @@ export type Element = [
|
||||||
export type Raw = [tag: typeof kRaw, html: ResolvedNode];
|
export type Raw = [tag: typeof kRaw, html: ResolvedNode];
|
||||||
/** Components must return a value; 'undefined' is prohibited here
|
/** Components must return a value; 'undefined' is prohibited here
|
||||||
* to avoid functions that are missing a return statement. */
|
* to avoid functions that are missing a return statement. */
|
||||||
export type Component = (props: Record<any, any>) => Exclude<Node, undefined>;
|
export type Component = (props: any) => Exclude<Node, undefined>;
|
||||||
/** Emitted by JSX runtime */
|
/** Emitted by JSX runtime */
|
||||||
export interface SrcLoc {
|
export interface SrcLoc {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
@ -132,7 +134,7 @@ export function resolveNode(r: State, node: unknown): ResolvedNode {
|
||||||
const placeholder: InsertionPoint = [null];
|
const placeholder: InsertionPoint = [null];
|
||||||
r.async += 1;
|
r.async += 1;
|
||||||
node
|
node
|
||||||
.then((result) => void (placeholder[0] = resolveNode(r, result)))
|
.then((result) => void (placeholder[0] = r.resolveNode(r, result)))
|
||||||
// Intentionally catching errors in `resolveNode`
|
// Intentionally catching errors in `resolveNode`
|
||||||
.catch((e) => (r.rejections ??= []).push(e))
|
.catch((e) => (r.rejections ??= []).push(e))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
@ -154,7 +156,7 @@ export function resolveNode(r: State, node: unknown): ResolvedNode {
|
||||||
if (typeof tag === "function") {
|
if (typeof tag === "function") {
|
||||||
current = r;
|
current = r;
|
||||||
try {
|
try {
|
||||||
return resolveNode(r, tag(props));
|
return r.resolveNode(r, tag(props));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { 4: src } = node;
|
const { 4: src } = node;
|
||||||
if (e && typeof e === "object") (e as { src?: string }).src = src;
|
if (e && typeof e === "object") (e as { src?: string }).src = src;
|
||||||
|
@ -165,11 +167,11 @@ export function resolveNode(r: State, node: unknown): ResolvedNode {
|
||||||
}
|
}
|
||||||
if (typeof tag !== "string") throw new Error("Unexpected " + inspect(type));
|
if (typeof tag !== "string") throw new Error("Unexpected " + inspect(type));
|
||||||
const children = props?.children;
|
const children = props?.children;
|
||||||
if (children) return [kElement, tag, props, resolveNode(r, children)];
|
if (children) return [kElement, tag, props, r.resolveNode(r, children)];
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
if (type === kRaw) return node[1];
|
if (type === kRaw) return node[1];
|
||||||
return node.map((elem) => resolveNode(r, elem));
|
return node.map((elem) => r.resolveNode(r, elem));
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResolvedNode =
|
export type ResolvedNode =
|
||||||
|
@ -280,6 +282,7 @@ export function init(allowAsync: boolean, addon: Addons): State {
|
||||||
rejections: null,
|
rejections: null,
|
||||||
asyncDone: null,
|
asyncDone: null,
|
||||||
addon,
|
addon,
|
||||||
|
resolveNode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,7 @@ export const fonts: sg.Font[] = [
|
||||||
name: "AT Name Sans Display Hairline",
|
name: "AT Name Sans Display Hairline",
|
||||||
sources: [
|
sources: [
|
||||||
"ATNameSansDisplay-Hairline.woff2",
|
"ATNameSansDisplay-Hairline.woff2",
|
||||||
path.join(fontRoot, "/ArrowType/AT Name Sans Display/ATNameSansDisplay-Hairline.woff2"),
|
path.join(fontRoot, "/ArrowType/Recursive/Recursive_VF_1.085.ttf"),
|
||||||
],
|
],
|
||||||
subsets: [
|
subsets: [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue