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 { name: string; createRootContext(container: unknown, options?: Record, context?: Context): RootCtx; finalizeRoot?(ctx: RootCtx): void; createInstance(tag: TTag, props: Record, 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, nextProps: Record, ctx: RootCtx, ): unknown | null; commitUpdate?( instance: Instance, payload: unknown, tag: TTag, prevProps: Record, nextProps: Record, ctx: RootCtx, ): void; emit?(type: string, id: string, payload: unknown): void; } export interface Root { host: HostConfig; ctx: RootCtx; container: unknown; context: Context; rootFiber: Fiber | null; render(node: UNode): void; unmount(): void; } export function createRoot( host: HostConfig, container: unknown, options?: Record, context?: Context, ): Root { const ctx = host.createRootContext(container, options, context); const rootContext = context ?? new Context(); function mountNode(node: UNode, parentFiber: Fiber | null): Fiber | 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: 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, ctx, parentInst); host.emit?.("instance.create", `${tag}_${Date.now()}`, { kind: "element", tag, props: el.props }); const fiber: Fiber = { instance: inst, tag, props: el.props as Record, 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, ctx as unknown); } } commitEffects(this.rootFiber, host as HostConfig, ctx as unknown); host.emit?.("root.render", `root_${Date.now()}`, { childCount: payloadChildren.length }); return; } const root: Fiber = { 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()}`, {}); }, }; }