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 { disposeFiber } from "./fiber.js"; import { reconcileProps, reconcileChildren, commitMutations } from "./reconcile.js"; import type { CommitContext } 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; finalizeInstance?(instance: Instance, ctx: RootCtx): 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, cachedNode: node, disposed: false, hash: 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, cachedNode: node, disposed: false, hash: 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; } function createFiberForInsert(node: UNode, parentFiber: Fiber): Fiber { if (isUPrimitive(node)) { const text = node === null ? "" : String(node); const t = host.createTextInstance(text, ctx, parentFiber.instance); host.emit?.("instance.create", `text_${Date.now()}`, { kind: "text", value: text }); return { instance: t, tag: "#text", props: { text }, key: undefined, children: [], parent: parentFiber, effect: null, signalDisposers: [], prevProps: null, cachedNode: node, disposed: false, hash: null, }; } 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 createFiberForInsert(out, parentFiber); } const tag = el.type as TTag; const inst = host.createInstance(tag, el.props as Record, ctx, parentFiber.instance); 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, cachedNode: node, disposed: false, hash: null, }; for (const child of el.children) { const childFiber = createFiberForInsert(child, fiber); if (childFiber) { host.appendChild(inst, childFiber.instance, ctx); fiber.children.push(childFiber); } } return fiber; } function resolveUNode(node: UNode): UNode[] { if (node == null || node === false) return []; if (isUPrimitive(node)) return [node]; if (isURoot(node)) { const result: UNode[] = []; for (const child of (node as URoot).children) { result.push(...resolveUNode(child)); } return result; } const el = node as UElement; if (typeof el.type === "function") { const component = el.type as ComponentFn; const out = component({ ...el.props, children: el.children }); return resolveUNode(out); } return [node]; } function reconcileNode(fiber: Fiber, nextNode: UNode): void { if (isUPrimitive(nextNode)) { if (fiber.tag === "#text") { reconcileProps(fiber, nextNode, host as HostConfig, ctx as unknown); } return; } if (isURoot(nextNode)) { const rootChildren = (nextNode as URoot).children; const classification = reconcileChildren( fiber.children, rootChildren, ); for (const m of classification.matched) { reconcileProps(m.oldFiber, m.newChild, host as HostConfig, ctx as unknown); } const commitCtx: CommitContext = { host: host as HostConfig, ctx: ctx as unknown, createFiber: (node, parent) => createFiberForInsert(node, parent), }; commitMutations(fiber, classification, commitCtx); return; } const el = nextNode as UElement; if (typeof el.type === "function") { const component = el.type as ComponentFn; const out = component({ ...el.props, children: el.children }); reconcileNode(fiber, out); return; } if (fiber.tag !== el.type) return; reconcileProps(fiber, el, host as HostConfig, ctx as unknown); const classification = reconcileChildren(fiber.children, el.children); for (const m of classification.matched) { reconcileProps(m.oldFiber, m.newChild, host as HostConfig, ctx as unknown); } const commitCtx: CommitContext = { host: host as HostConfig, ctx: ctx as unknown, createFiber: (node, parent) => createFiberForInsert(node, parent), }; commitMutations(fiber, classification, commitCtx); } return { host, ctx, container, context: rootContext, rootFiber: null, render(node: UNode) { if (this.rootFiber) { const payloadChildren = isURoot(node) ? (node as URoot).children : [node]; const resolvedChildren: UNode[] = []; for (const child of payloadChildren) { resolvedChildren.push(...resolveUNode(child)); } const classification = reconcileChildren( this.rootFiber.children, resolvedChildren, ); for (const m of classification.matched) { reconcileNode(m.oldFiber, m.newChild); } const commitCtx: CommitContext = { host: host as HostConfig, ctx: ctx as unknown, createFiber: (node, parent) => createFiberForInsert(node, parent), }; commitMutations(this.rootFiber, classification, commitCtx); 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, cachedNode: null, disposed: false, hash: 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() { const rootFiber = this.rootFiber; if (!rootFiber) return; disposeFiber(rootFiber, host as import("./fiber.js").HostLike, ctx); for (const child of rootFiber.children) { host.removeChild?.(rootFiber.instance as never, child.instance as never, ctx as never); } host.finalizeRoot?.(ctx); host.emit?.("root.unmount", `root_${Date.now()}`, {}); this.rootFiber = null; }, }; }