import type { FrameContent, FrameHandle } from './component.ts' import { createFrameHandle } from './component.ts' import { invariant } from './invariant.ts' import type { RemixNode } from './jsx.ts' import { createComponentErrorEvent, getComponentError, type ComponentErrorEvent, } from './error-event.ts' import { createScheduler, type Scheduler } from './scheduler.ts' import { diffVNodes, remove as removeVNode } from './reconcile.ts' import { toVNode } from './to-vnode.ts' import { TypedEventTarget } from './typed-event-target.ts' import { ROOT_VNODE, type VNode } from './vnode.ts' import { resetStyleState, defaultStyleManager } from './diff-props.ts' import type { StyleManager } from './style/index.ts' /** * Events emitted by virtual roots. */ export type VirtualRootEventMap = { error: ComponentErrorEvent } /** * Root controller returned by {@link createRoot} and {@link createRangeRoot}. */ export type VirtualRoot = TypedEventTarget & { render: (element: RemixNode) => void dispose: () => void flush: () => void } /** * Options for creating a virtual DOM root with {@link createRoot} or {@link createRangeRoot}. */ export type VirtualRootOptions = { frame?: FrameHandle scheduler?: Scheduler styleManager?: StyleManager frameInit?: { src?: string resolveFrame: ( src: string, signal?: AbortSignal, target?: string, ) => Promise | FrameContent loadModule?: (moduleUrl: string, exportName: string) => Promise | Function } } export { createScheduler, type Scheduler } export { diffVNodes, toVNode } export { resetStyleState } function getHydrationComponentIdFromRangeStart(start: Node): string | undefined { if (!(start instanceof Comment)) return undefined let marker = start.data.trim() if (!marker.startsWith('rmx:h:')) return undefined let id = marker.slice('rmx:h:'.length) return id.length > 0 ? id : undefined } /** * Creates a virtual root bounded by two DOM nodes. * * @param boundaries Start and end marker nodes that define the render region. * @param options Root configuration. * @returns A virtual root controller. */ export function createRangeRoot( boundaries: [Node, Node], options: VirtualRootOptions = {}, ): VirtualRoot { let [start, end] = boundaries let vroot: VNode | null = null let styles = options.styleManager ?? defaultStyleManager let container = end.parentNode invariant(container, 'Expected parent node') invariant(start.parentNode === container, 'Boundaries must share parent') let parent = container let hydrationCursor = start.nextSibling let eventTarget = new TypedEventTarget() let scheduler = options.scheduler ?? createScheduler(parent.ownerDocument ?? document, eventTarget, styles) let frameStub = options.frame ?? createRootFrameHandle({ src: options.frameInit?.src, resolveFrame: options.frameInit?.resolveFrame, loadModule: options.frameInit?.loadModule, errorTarget: eventTarget, scheduler, styleManager: styles, }) let isErrorForwardingAttached = false function forwardDomError(event: Event) { eventTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event))) } function attachDomErrorForwarding() { if (isErrorForwardingAttached) return parent.addEventListener('error', forwardDomError) isErrorForwardingAttached = true } function detachDomErrorForwarding() { if (!isErrorForwardingAttached) return parent.removeEventListener('error', forwardDomError) isErrorForwardingAttached = false } attachDomErrorForwarding() return Object.assign(eventTarget, { render(element: RemixNode) { attachDomErrorForwarding() let vnode = toVNode(element) let vParent: VNode = { type: ROOT_VNODE, _svg: false, _rangeStart: start, _rangeEnd: end, _pendingHydrationComponentId: getHydrationComponentIdFromRangeStart(start), } scheduler.enqueueWork([ () => { diffVNodes( vroot, vnode, parent, frameStub, scheduler, styles, vParent, eventTarget, end, hydrationCursor, ) vroot = vnode hydrationCursor = null }, ]) scheduler.dequeue() }, dispose() { detachDomErrorForwarding() if (!vroot) return let current = vroot vroot = null scheduler.enqueueWork([() => removeVNode(current, parent, scheduler, styles)]) scheduler.dequeue() }, flush() { scheduler.dequeue() }, }) } /** * Creates a virtual root for a host container element. * * @param container Host element to render into. * @param options Root configuration. * @returns A virtual root controller. */ export function createRoot(container: HTMLElement, options: VirtualRootOptions = {}): VirtualRoot { let vroot: VNode | null = null let styles = options.styleManager ?? defaultStyleManager let hydrationCursor = container.innerHTML.trim() !== '' ? container.firstChild : undefined let eventTarget = new TypedEventTarget() let scheduler = options.scheduler ?? createScheduler(container.ownerDocument ?? document, eventTarget, styles) let frameStub = options.frame ?? createRootFrameHandle({ src: options.frameInit?.src, resolveFrame: options.frameInit?.resolveFrame, loadModule: options.frameInit?.loadModule, errorTarget: eventTarget, scheduler, styleManager: styles, }) let isErrorForwardingAttached = false function forwardDomError(event: Event) { eventTarget.dispatchEvent(createComponentErrorEvent(getComponentError(event))) } function attachDomErrorForwarding() { if (isErrorForwardingAttached) return container.addEventListener('error', forwardDomError) isErrorForwardingAttached = true } function detachDomErrorForwarding() { if (!isErrorForwardingAttached) return container.removeEventListener('error', forwardDomError) isErrorForwardingAttached = false } attachDomErrorForwarding() return Object.assign(eventTarget, { render(element: RemixNode) { attachDomErrorForwarding() let vnode = toVNode(element) let vParent: VNode = { type: ROOT_VNODE, _svg: false } scheduler.enqueueWork([ () => { diffVNodes( vroot, vnode, container, frameStub, scheduler, styles, vParent, eventTarget, undefined, hydrationCursor, ) vroot = vnode hydrationCursor = undefined }, ]) scheduler.dequeue() }, dispose() { detachDomErrorForwarding() if (!vroot) return let current = vroot vroot = null scheduler.enqueueWork([() => removeVNode(current, container, scheduler, styles)]) scheduler.dequeue() }, flush() { scheduler.dequeue() }, }) } function createRootFrameHandle(init: { src?: string resolveFrame?: ( src: string, signal?: AbortSignal, target?: string, ) => Promise | FrameContent loadModule?: (moduleUrl: string, exportName: string) => Promise | Function errorTarget: EventTarget scheduler: Scheduler styleManager: StyleManager }): FrameHandle { let resolveFrame = init.resolveFrame ?? (() => { throw new Error( 'Cannot render without frame runtime. Use run() or pass frameInit to createRoot/createRangeRoot.', ) }) let frame = createFrameHandle({ src: init.src ?? '/', $runtime: { canResolveFrames: !!init.resolveFrame, topFrame: undefined, loadModule: init.loadModule ?? (() => { throw new Error('loadModule is required to hydrate client entries inside ') }), resolveFrame, errorTarget: init.errorTarget, pendingClientEntries: new Map(), scheduler: init.scheduler, styleManager: init.styleManager, data: {}, moduleCache: new Map(), moduleLoads: new Map(), frameInstances: new WeakMap(), namedFrames: new Map(), }, }) let runtime = frame.$runtime as { topFrame?: FrameHandle } | undefined if (runtime) runtime.topFrame = frame return frame }