Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
2f68ed528b |
3 changed files with 6 additions and 161 deletions
|
@ -1,152 +0,0 @@
|
||||||
// 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,8 +76,6 @@ 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");
|
||||||
|
@ -103,7 +101,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: any) => Exclude<Node, undefined>;
|
export type Component = (props: Record<any, any>) => Exclude<Node, undefined>;
|
||||||
/** Emitted by JSX runtime */
|
/** Emitted by JSX runtime */
|
||||||
export interface SrcLoc {
|
export interface SrcLoc {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
@ -134,7 +132,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] = r.resolveNode(r, result)))
|
.then((result) => void (placeholder[0] = 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(() => {
|
||||||
|
@ -156,7 +154,7 @@ export function resolveNode(r: State, node: unknown): ResolvedNode {
|
||||||
if (typeof tag === "function") {
|
if (typeof tag === "function") {
|
||||||
current = r;
|
current = r;
|
||||||
try {
|
try {
|
||||||
return r.resolveNode(r, tag(props));
|
return 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;
|
||||||
|
@ -167,11 +165,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, r.resolveNode(r, children)];
|
if (children) return [kElement, tag, props, resolveNode(r, children)];
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
if (type === kRaw) return node[1];
|
if (type === kRaw) return node[1];
|
||||||
return node.map((elem) => r.resolveNode(r, elem));
|
return node.map((elem) => resolveNode(r, elem));
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResolvedNode =
|
export type ResolvedNode =
|
||||||
|
@ -282,7 +280,6 @@ 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/Recursive/Recursive_VF_1.085.ttf"),
|
path.join(fontRoot, "/ArrowType/AT Name Sans Display/ATNameSansDisplay-Hairline.woff2"),
|
||||||
],
|
],
|
||||||
subsets: [
|
subsets: [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue