Files
ujsx/src/host/config.ts

163 lines
5.5 KiB
TypeScript

import type { UNode, UElement, URoot, ComponentFn, UComponent } from "../core/schema.js";
import { isURoot, isUPrimitive } from "../core/schema.js";
import { Context } from "../core/context.js";
import type { Fiber } from "./fiber.js";
import { reconcileProps, commitEffects } from "./reconcile.js";
export interface HostConfig<TTag extends string, Instance, RootCtx> {
name: string;
createRootContext(container: unknown, options?: Record<string, unknown>, context?: Context): RootCtx;
finalizeRoot?(ctx: RootCtx): void;
createInstance(tag: TTag, props: Record<string, unknown>, ctx: RootCtx, parent?: Instance): Instance;
createTextInstance(text: string, ctx: RootCtx, parent?: Instance): Instance;
appendChild(parent: Instance, child: Instance, ctx: RootCtx): void;
insertBefore?(parent: Instance, child: Instance, before: Instance, ctx: RootCtx): void;
removeChild?(parent: Instance, child: Instance, ctx: RootCtx): void;
prepareUpdate?(
instance: Instance,
tag: TTag,
prevProps: Record<string, unknown>,
nextProps: Record<string, unknown>,
ctx: RootCtx,
): unknown | null;
commitUpdate?(
instance: Instance,
payload: unknown,
tag: TTag,
prevProps: Record<string, unknown>,
nextProps: Record<string, unknown>,
ctx: RootCtx,
): void;
emit?(type: string, id: string, payload: unknown): void;
}
export interface Root<TTag extends string, Instance, RootCtx> {
host: HostConfig<TTag, Instance, RootCtx>;
ctx: RootCtx;
container: unknown;
context: Context;
rootFiber: Fiber<Instance> | null;
render(node: UNode): void;
unmount(): void;
}
export function createRoot<TTag extends string, Instance, RootCtx>(
host: HostConfig<TTag, Instance, RootCtx>,
container: unknown,
options?: Record<string, unknown>,
context?: Context,
): Root<TTag, Instance, RootCtx> {
const ctx = host.createRootContext(container, options, context);
const rootContext = context ?? new Context();
function mountNode(node: UNode, parentFiber: Fiber<Instance> | null): Fiber<Instance> | undefined {
if (node == null || node === false) return undefined;
if (isUPrimitive(node)) {
const text = node === null ? "" : String(node);
const parentInst = parentFiber?.instance;
const t = host.createTextInstance(text, ctx, parentInst);
if (parentInst) host.appendChild(parentInst, t, ctx);
host.emit?.("instance.create", `text_${Date.now()}`, { kind: "text", value: text });
const fiber: Fiber<Instance> = {
instance: t,
tag: "#text",
props: { text },
key: undefined,
children: [],
parent: parentFiber,
effect: null,
signalDisposers: [],
prevProps: null,
};
if (parentFiber) parentFiber.children.push(fiber);
return fiber;
}
if (isURoot(node)) {
for (const child of node.children) {
mountNode(child, parentFiber);
}
return parentFiber ?? undefined;
}
const el = node as UElement;
if (typeof el.type === "function") {
const component = el.type as ComponentFn;
const out = component({ ...el.props, children: el.children });
host.emit?.("component.invoke", `comp_${Date.now()}`, { type: (component as unknown as UComponent).displayName ?? "anonymous" });
return mountNode(out, parentFiber);
}
const tag = el.type as TTag;
const parentInst = parentFiber?.instance;
const inst = host.createInstance(tag, el.props as Record<string, unknown>, ctx, parentInst);
host.emit?.("instance.create", `${tag}_${Date.now()}`, { kind: "element", tag, props: el.props });
const fiber: Fiber<Instance> = {
instance: inst,
tag,
props: el.props as Record<string, unknown>,
key: el.key,
children: [],
parent: parentFiber,
effect: null,
signalDisposers: [],
prevProps: null,
};
for (const child of el.children) {
mountNode(child, fiber);
}
if (parentInst) host.appendChild(parentInst, inst, ctx);
if (parentFiber) parentFiber.children.push(fiber);
return fiber;
}
return {
host,
ctx,
container,
context: rootContext,
rootFiber: null,
render(node: UNode) {
if (this.rootFiber) {
const payloadChildren = isURoot(node) ? (node as URoot).children : [node];
for (let i = 0; i < payloadChildren.length; i++) {
const childFiber = this.rootFiber.children[i];
if (childFiber) {
reconcileProps(childFiber, payloadChildren[i]!, host as HostConfig<string, Instance, unknown>, ctx as unknown);
}
}
commitEffects(this.rootFiber, host as HostConfig<string, Instance, unknown>, ctx as unknown);
host.emit?.("root.render", `root_${Date.now()}`, { childCount: payloadChildren.length });
return;
}
const root: Fiber<Instance> = {
instance: undefined as unknown as Instance,
tag: "#root",
props: {},
key: undefined,
children: [],
parent: null,
effect: null,
signalDisposers: [],
prevProps: null,
};
const payloadChildren = isURoot(node) ? (node as URoot).children : [node];
for (const child of payloadChildren) {
mountNode(child, root);
}
this.rootFiber = root;
host.finalizeRoot?.(ctx);
host.emit?.("root.render", `root_${Date.now()}`, { childCount: payloadChildren.length });
},
unmount() {
host.finalizeRoot?.(ctx);
host.emit?.("root.unmount", `root_${Date.now()}`, {});
},
};
}