From 5d633451287c1817f143a4e914dbfc0a7f9a21b3 Mon Sep 17 00:00:00 2001 From: Robby6Strings Date: Fri, 27 Sep 2024 21:35:02 +1200 Subject: [PATCH] improve all kaioken errors, expose Signal.make(Readonly|Writable) --- packages/lib/src/appContext.ts | 2 +- packages/lib/src/dom.ts | 73 +++---- packages/lib/src/error.ts | 72 +++++++ .../lib/src/hooks/useSyncExternalStore.ts | 9 +- packages/lib/src/hooks/utils.ts | 16 +- packages/lib/src/portal.ts | 7 +- packages/lib/src/props.ts | 7 +- packages/lib/src/scheduler.ts | 192 +++++++----------- packages/lib/src/signal.ts | 43 ++-- packages/lib/src/utils.ts | 119 ++++++++--- 10 files changed, 317 insertions(+), 223 deletions(-) diff --git a/packages/lib/src/appContext.ts b/packages/lib/src/appContext.ts index 2402030..3586542 100644 --- a/packages/lib/src/appContext.ts +++ b/packages/lib/src/appContext.ts @@ -90,7 +90,7 @@ export class AppContext = {}> { const scheduler = this.scheduler if (!this.mounted || !rootChild || !scheduler) throw new KaiokenError( - "[kaioken]: failed to apply new props - ensure the app is mounted" + "Failed to apply new props - ensure the app is mounted" ) return new Promise>((resolve) => { scheduler.clear() diff --git a/packages/lib/src/dom.ts b/packages/lib/src/dom.ts index c12dacf..41962ab 100644 --- a/packages/lib/src/dom.ts +++ b/packages/lib/src/dom.ts @@ -5,6 +5,7 @@ import { propFilters, propToHtmlAttr, svgTags, + postOrderApply, } from "./utils.js" import { cleanupHook } from "./hooks/utils.js" import { ELEMENT_TYPE, FLAG } from "./constants.js" @@ -140,12 +141,16 @@ function subTextNode(node: Kaioken.VNode, dom: Text, sig: Signal) { function hydrateDom(vNode: VNode) { const dom = hydrationStack.nextChild() if (!dom) - throw new KaiokenError(`[kaioken]: Hydration mismatch - no node found`) + throw new KaiokenError({ + message: `Hydration mismatch - no node found`, + vNode, + }) const nodeName = dom.nodeName.toLowerCase() if ((vNode.type as string) !== nodeName) { - throw new KaiokenError( - `[kaioken]: Hydration mismatch - expected node of type ${vNode.type.toString()} but received ${nodeName}` - ) + throw new KaiokenError({ + message: `Hydration mismatch - expected node of type ${vNode.type.toString()} but received ${nodeName}`, + vNode, + }) } vNode.dom = dom setDomRef(vNode, dom) @@ -303,9 +308,10 @@ function getDomParent(node: VNode): ElementVNode { if (node.dom) return node as ElementVNode } - throw new KaiokenError( - "[kaioken]: no domParent found - seek help!\n" + String(node) - ) + throw new KaiokenError({ + message: "No DOM parent found while attempting to place node.", + vNode: node, + }) } return parentNode as ElementVNode } @@ -357,24 +363,14 @@ function commitWork(vNode: VNode) { return commitDeletion(vNode) } - // perform a depth-first crawl through the tree, starting from the root. - // we accumulate a stack of 'host node -> last child' as we go, - // so that we can reuse them in the next iteration. - const root = vNode const hostNodes: HostNode[] = [] - let branch = root.child - while (branch) { - let node = branch - // traverse the tree in a depth first manner, - // collecting host nodes as we go - while (node) { - if (node.dom && node.type !== ELEMENT_TYPE.text && node.child) { - hostNodes.push({ - node: node as ElementVNode, - }) - } - if (!node.child) break - if (!node.dom && bitmapOps.isFlagSet(node, FLAG.PLACEMENT)) { + postOrderApply(vNode, { + onDescent: (node) => { + if (!node.child) return + if (node.dom) { + // collect host nodes as we go + hostNodes.push({ node: node as ElementVNode }) + } else if (bitmapOps.isFlagSet(node, FLAG.PLACEMENT)) { // no dom node, propagate the flag down the tree. // we shouldn't need to do this if we were to instead // treat the placement flag as a modifier that affects @@ -385,31 +381,22 @@ function commitWork(vNode: VNode) { child = child.sibling } } - node = node.child - } - while (node && node !== root) { - // at this point we're operating on the deepest nodes, - // traversing back up the tree until we reach a new branch - // or the root. + }, + onAscent: (node) => { if (bitmapOps.isFlagSet(node, FLAG.DELETION)) { - commitDeletion(node) - } else { - if (node.dom) { - commitDom(node as DomVNode, hostNodes) - } - commitSnapshot(node) + return commitDeletion(node) } - if (node.sibling) { - branch = node.sibling - break + if (node.dom) { + commitDom(node as DomVNode, hostNodes) } + commitSnapshot(node) + }, + onBeforeAscent(node) { if (hostNodes[hostNodes.length - 1]?.node === node.parent) { hostNodes.pop() } - node = node.parent! - } - if (node === root) break - } + }, + }) } function commitDom(node: DomVNode, hostNodes: HostNode[]) { diff --git a/packages/lib/src/error.ts b/packages/lib/src/error.ts index a01e33d..b308700 100644 --- a/packages/lib/src/error.ts +++ b/packages/lib/src/error.ts @@ -1,6 +1,37 @@ import { kaiokenErrorSymbol } from "./constants.js" +import { __DEV__ } from "./env.js" +import { findParent, noop } from "./utils.js" + +type KaiokenErrorOptions = + | string + | { + message: string + /** Used to indicate that the error is fatal and should crash the application */ + fatal?: boolean + /** Used to generate custom node stack */ + vNode?: Kaioken.VNode + } + export class KaiokenError extends Error { [kaiokenErrorSymbol] = true + /** Indicates whether the error is fatal and should crash the application */ + fatal?: boolean + /** Present if vNode is provided */ + customNodeStack?: string + constructor(optionsOrMessage: KaiokenErrorOptions) { + const message = + typeof optionsOrMessage === "string" + ? optionsOrMessage + : optionsOrMessage.message + super(message) + if (typeof optionsOrMessage !== "string") { + if (optionsOrMessage?.vNode) { + this.customNodeStack = captureErrorStack(optionsOrMessage.vNode) + } + this.fatal = optionsOrMessage?.fatal + } + } + static isKaiokenError(error: unknown): error is KaiokenError { return ( error instanceof Error && @@ -8,3 +39,44 @@ export class KaiokenError extends Error { ) } } + +function captureErrorStack(vNode: Kaioken.VNode) { + let n = vNode + let componentFns: string[] = [] + while (n) { + if (!n.parent) break // skip root node + if (typeof n.type === "function") { + componentFns.push(getComponentErrorDisplayText(n.type)) + } else if (typeof n.type === "string") { + componentFns.push(n.type) + } + n = n.parent + } + const componentNode = ( + typeof vNode.type === "function" + ? vNode + : findParent(vNode, (n) => typeof n.type === "function") + ) as (Kaioken.VNode & { type: Function }) | undefined + return `The above error occurred in the <${getFunctionName(componentNode?.type || noop)}> component: + +${componentFns.map((x) => ` at ${x}`).join("\n")}\n` +} + +function getComponentErrorDisplayText(fn: Function) { + let str = getFunctionName(fn) + if (__DEV__) { + const fileLink = getComponentFileLink(fn) + if (fileLink) { + str = `${str} (${fileLink})` + } + } + return str +} + +function getFunctionName(fn: Function) { + return (fn as any).displayName ?? (fn.name || "Anonymous Function") +} + +function getComponentFileLink(fn: Function) { + return fn.toString().match(/\/\/ \[kaioken_devtools\]:(.*)/)?.[1] ?? null +} diff --git a/packages/lib/src/hooks/useSyncExternalStore.ts b/packages/lib/src/hooks/useSyncExternalStore.ts index 1c15cc9..10865a7 100644 --- a/packages/lib/src/hooks/useSyncExternalStore.ts +++ b/packages/lib/src/hooks/useSyncExternalStore.ts @@ -1,3 +1,4 @@ +import { node } from "../globals.js" import { KaiokenError } from "../error.js" import { noop } from "../utils.js" import { sideEffectsEnabled, useHook } from "./utils.js" @@ -9,9 +10,11 @@ export function useSyncExternalStore( ): T { if (!sideEffectsEnabled()) { if (getServerState === undefined) { - throw new KaiokenError( - "[kaioken]: useSyncExternalStore must receive a getServerSnapshot function if the component is rendered on the server." - ) + throw new KaiokenError({ + message: + "useSyncExternalStore must receive a getServerSnapshot function if the component is rendered on the server.", + vNode: node.current, + }) } return getServerState() } diff --git a/packages/lib/src/hooks/utils.ts b/packages/lib/src/hooks/utils.ts index b9f7d76..52db5f0 100644 --- a/packages/lib/src/hooks/utils.ts +++ b/packages/lib/src/hooks/utils.ts @@ -83,19 +83,21 @@ function useHook>( hookDataOrInitializer: (() => Hook) | Hook, callback: U ): ReturnType { + const vNode = node.current + if (!vNode) error_hookMustBeCalledTopLevel(hookName) + if ( currentHookName !== null && !nestedHookWarnings.has(hookName + currentHookName) ) { nestedHookWarnings.add(hookName + currentHookName) - throw new KaiokenError( - `[kaioken]: nested primitive "useHook" calls are not supported. "${hookName}" was called inside "${currentHookName}". Strange things may happen.` - ) + throw new KaiokenError({ + message: `Nested primitive "useHook" calls are not supported. "${hookName}" was called inside "${currentHookName}". Strange will most certainly happen.`, + vNode: node.current, + }) } - const vNode = node.current - if (!vNode) error_hookMustBeCalledTopLevel(hookName) - const ctx = getVNodeAppContext(vNode) + const ctx = getVNodeAppContext(vNode) const oldHook = ( vNode.prev ? vNode.prev.hooks?.at(ctx.hookIndex) @@ -141,7 +143,7 @@ function useHook>( function error_hookMustBeCalledTopLevel(hookName: string): never { throw new KaiokenError( - `[kaioken]: hook "${hookName}" must be used at the top level of a component or inside another composite hook.` + `Hook "${hookName}" must be used at the top level of a component or inside another composite hook.` ) } diff --git a/packages/lib/src/portal.ts b/packages/lib/src/portal.ts index 77e2043..bf56496 100644 --- a/packages/lib/src/portal.ts +++ b/packages/lib/src/portal.ts @@ -18,9 +18,10 @@ function Portal({ children, container }: PortalProps) { node.dom = typeof container === "function" ? container() : container if (!(node.dom instanceof HTMLElement)) { if (__DEV__) { - throw new KaiokenError( - `[kaioken]: Invalid portal container, expected HTMLElement, got ${node.dom}` - ) + throw new KaiokenError({ + message: `Invalid portal container, expected HTMLElement, got ${node.dom}`, + vNode: node, + }) } return null } diff --git a/packages/lib/src/props.ts b/packages/lib/src/props.ts index 6194d7d..cca6948 100644 --- a/packages/lib/src/props.ts +++ b/packages/lib/src/props.ts @@ -3,9 +3,10 @@ import { Signal } from "./signal.js" export function assertValidElementProps(vNode: Kaioken.VNode) { if ("children" in vNode.props && vNode.props.innerHTML) { - throw new KaiokenError( - "[kaioken]: Cannot use both children and innerHTML on an element" - ) + throw new KaiokenError({ + message: "Cannot use both children and innerHTML on an element", + vNode, + }) } } diff --git a/packages/lib/src/scheduler.ts b/packages/lib/src/scheduler.ts index bdbcd92..922bf1c 100644 --- a/packages/lib/src/scheduler.ts +++ b/packages/lib/src/scheduler.ts @@ -13,45 +13,11 @@ import { ctx, node, renderMode } from "./globals.js" import { hydrationStack } from "./hydration.js" import { assertValidElementProps } from "./props.js" import { reconcileChildren } from "./reconciler.js" -import { traverseApply, vNodeContains } from "./utils.js" +import { postOrderApply, traverseApply, vNodeContains } from "./utils.js" type VNode = Kaioken.VNode type FunctionNode = VNode & { type: (...args: any) => any } -function fireEffects(tree: VNode, immediate?: boolean) { - const root = tree - // traverse tree in a depth first manner - // fire effects from the child to the root - const rootChild = root.child - if (!rootChild) { - const arr = immediate ? tree.immediateEffects : tree.effects - while (arr?.length) arr.shift()!() - return - } - - let branch = root.child! - while (branch) { - let c = branch - while (c) { - if (!c.child) break - c = c.child - } - inner: while (c && c !== root) { - const arr = immediate ? c.immediateEffects : c.effects - while (arr?.length) arr.shift()!() - if (c.sibling) { - branch = c.sibling - break inner - } - c = c.parent! - } - if (c === root) break - } - - const arr = immediate ? root.immediateEffects : root.effects - while (arr?.length) arr.shift()!() -} - export class Scheduler { private nextUnitOfWork: VNode | undefined = undefined private treesInProgress: VNode[] = [] @@ -67,7 +33,6 @@ export class Scheduler { private immediateEffectDirtiedRender = false private isRenderDirtied = false private consecutiveDirtyCount = 0 - private fatalError = "" constructor( private appCtx: AppContext, @@ -321,25 +286,26 @@ export class Scheduler { this.updateHostComponent(vNode) } } catch (error) { - if (this.fatalError) { - setTimeout(() => { - throw new Error(this.fatalError) - }) - throw error - } window.__kaioken?.emit( "error", this.appCtx, error instanceof Error ? error : new Error(String(error)) ) if (KaiokenError.isKaiokenError(error)) { - console.error(error) - } else { - // ensure that the error is thrown in the next tick - setTimeout(() => { + if (error.customNodeStack) { + setTimeout(() => { + throw new Error(error.customNodeStack) + }) + } + if (error.fatal) { throw error - }) + } + console.error(error) + return } + setTimeout(() => { + throw error + }) } if (vNode.child) { if (renderMode.current === "hydrate" && vNode.dom) { @@ -364,96 +330,78 @@ export class Scheduler { } private updateFunctionComponent(vNode: FunctionNode) { - node.current = vNode - let newChildren - let renderTryCount = 0 - do { - this.isRenderDirtied = false - this.appCtx.hookIndex = 0 - newChildren = vNode.type(vNode.props) - if (++renderTryCount > CONSECUTIVE_DIRTY_LIMIT) { - const stackMsg = this.captureErrorStack(vNode) - this.fatalError = stackMsg - throw new KaiokenError( - "[kaioken]: Too many re-renders. Kaioken limits the number of renders to prevent an infinite loop." - ) - } - } while (this.isRenderDirtied) - vNode.child = - reconcileChildren(this.appCtx, vNode, vNode.child || null, newChildren) || - undefined - node.current = undefined + try { + node.current = vNode + let newChildren + let renderTryCount = 0 + do { + this.isRenderDirtied = false + this.appCtx.hookIndex = 0 + newChildren = vNode.type(vNode.props) + if (++renderTryCount > CONSECUTIVE_DIRTY_LIMIT) { + throw new KaiokenError({ + message: + "Too many re-renders. Kaioken limits the number of renders to prevent an infinite loop.", + fatal: true, + vNode, + }) + } + } while (this.isRenderDirtied) + vNode.child = + reconcileChildren( + this.appCtx, + vNode, + vNode.child || null, + newChildren + ) || undefined + } finally { + node.current = undefined + } } private updateHostComponent(vNode: VNode) { - node.current = vNode - assertValidElementProps(vNode) - if (!vNode.dom) { - if (renderMode.current === "hydrate") { - hydrateDom(vNode) - } else { - vNode.dom = createDom(vNode) + try { + node.current = vNode + assertValidElementProps(vNode) + if (!vNode.dom) { + if (renderMode.current === "hydrate") { + hydrateDom(vNode) + } else { + vNode.dom = createDom(vNode) + } } - } - - if (vNode.dom) { - // @ts-expect-error we apply vNode to the dom node - vNode.dom!.__kaiokenNode = vNode - } - - vNode.child = - reconcileChildren( - this.appCtx, - vNode, - vNode.child || null, - vNode.props.children - ) || undefined - - node.current = undefined - } - private captureErrorStack(vNode: FunctionNode) { - let n: VNode | undefined = vNode.parent - const srcText = getComponentErrorDisplayText(vNode.type) - let componentFns: string[] = [srcText] - while (n) { - if (n === this.appCtx.rootNode) break - if (typeof n.type === "function") { - componentFns.push(getComponentErrorDisplayText(n.type)) - } else if (typeof n.type === "string") { - componentFns.push(n.type) + if (vNode.dom) { + // @ts-expect-error we apply vNode to the dom node + vNode.dom!.__kaiokenNode = vNode } - n = n.parent - } - return `The above error occurred in the <${getFunctionName(vNode.type as any)}> component: -${componentFns.map((x) => ` at ${x}`).join("\n")}\n` + vNode.child = + reconcileChildren( + this.appCtx, + vNode, + vNode.child || null, + vNode.props.children + ) || undefined + } finally { + node.current = undefined + } } private checkForTooManyConsecutiveDirtyRenders() { if (this.consecutiveDirtyCount > CONSECUTIVE_DIRTY_LIMIT) { throw new KaiokenError( - "[kiakoken]: Maximum update depth exceeded. This can happen when a component repeatedly calls setState during render or in useLayoutEffect. Kaioken limits the number of nested updates to prevent infinite loops." + "Maximum update depth exceeded. This can happen when a component repeatedly calls setState during render or in useLayoutEffect. Kaioken limits the number of nested updates to prevent infinite loops." ) } } } -function getComponentErrorDisplayText(fn: Function) { - let str = getFunctionName(fn) - if (__DEV__) { - const fileLink = getComponentFileLink(fn) - if (fileLink) { - str = `${str} (${fileLink})` - } - } - return str -} - -function getFunctionName(fn: Function) { - return (fn as any).displayName ?? (fn.name || "Anonymous Function") -} - -function getComponentFileLink(fn: Function) { - return fn.toString().match(/\/\/ \[kaioken_devtools\]:(.*)/)?.[1] ?? null +function fireEffects(tree: VNode, immediate?: boolean) { + postOrderApply(tree, { + onAscent(node) { + const arr = immediate ? node.immediateEffects : node.effects + while (arr?.length) arr.shift()!() + }, + }) } diff --git a/packages/lib/src/signal.ts b/packages/lib/src/signal.ts index 700b202..99bc556 100644 --- a/packages/lib/src/signal.ts +++ b/packages/lib/src/signal.ts @@ -38,7 +38,7 @@ export const computed = ( displayName?: string ): ReadonlySignal => { if (!node.current) { - const computed = makeReadonly(new Signal(null as T, displayName)) + const computed = Signal.makeReadonly(new Signal(null as T, displayName)) const subs = new Map, Function>() appliedTrackedSignals(getter, computed, subs) @@ -66,7 +66,7 @@ export const computed = ( } } hook.subs = new Map() - hook.signal = makeReadonly(new Signal(null as T, displayName)) + hook.signal = Signal.makeReadonly(new Signal(null as T, displayName)) appliedTrackedSignals(getter, hook.signal, hook.subs) } @@ -120,7 +120,7 @@ export class Signal { map(fn: (value: T) => U, displayName?: string): ReadonlySignal { const initialVal = fn(this.#value) - const sig = makeReadonly(signal(initialVal, displayName)) + const sig = Signal.makeReadonly(signal(initialVal, displayName)) if (node.current && !sideEffectsEnabled()) return sig this.subscribe((value) => (sig.sneak(fn(value)), sig.notify())) @@ -159,8 +159,31 @@ export class Signal { return signal.#subscribers } - static setValueQuietly(signal: Signal, value: T) { - signal.sneak(value) + static makeReadonly(signal: Signal): ReadonlySignal { + const desc = Object.getOwnPropertyDescriptor(signal, "value") + if (desc && !desc.writable) return signal + return Object.defineProperty(signal, "value", { + get: function (this: Signal) { + onSignalValueObserved(this) + return this.#value + }, + configurable: true, + }) + } + static makeWritable(signal: Signal): Signal { + const desc = Object.getOwnPropertyDescriptor(signal, "value") + if (desc && desc.writable) return signal + return Object.defineProperty(signal, "value", { + get: function (this: Signal) { + onSignalValueObserved(this) + return this.#value + }, + set: function (this: Signal, value) { + this.#value = value + this.notify() + }, + configurable: true, + }) } } @@ -211,13 +234,3 @@ const onSignalValueObserved = (signal: Signal) => { Signal.subscribers(signal).add(node.current) } } - -const makeReadonly = (signal: Signal): ReadonlySignal => { - if (!Object.getOwnPropertyDescriptor(signal, "value")?.writable) return signal - return Object.defineProperty(signal, "value", { - get: function () { - onSignalValueObserved(signal) - return signal.peek() - }, - }) -} diff --git a/packages/lib/src/utils.ts b/packages/lib/src/utils.ts index 4f9c0b3..57dd1a9 100644 --- a/packages/lib/src/utils.ts +++ b/packages/lib/src/utils.ts @@ -1,4 +1,4 @@ -import { nodeToCtxMap, renderMode } from "./globals.js" +import { node, nodeToCtxMap, renderMode } from "./globals.js" import { contextProviderSymbol, fragmentSymbol, @@ -6,15 +6,19 @@ import { } from "./constants.js" import { unwrap } from "./signal.js" import { KaiokenError } from "./error.js" +import type { AppContext } from "./appContext.js" export { isVNode, isFragment, isContextProvider, vNodeContains, + getCurrentVNode, getVNodeAppContext, commitSnapshot, traverseApply, + postOrderApply, + findParent, propToHtmlAttr, propValueToHtmlAttrValue, propsToElementAttributes, @@ -29,44 +33,56 @@ export { booleanAttributes, } -const noop = Object.freeze(() => {}) +type VNode = Kaioken.VNode -function sideEffectsEnabled() { +const noop: () => void = Object.freeze(() => {}) + +function sideEffectsEnabled(): boolean { return renderMode.current === "dom" || renderMode.current === "hydrate" } -function isVNode(thing: unknown): thing is Kaioken.VNode { +function isVNode(thing: unknown): thing is VNode { return typeof thing === "object" && thing !== null && "type" in thing } function isFragment( thing: unknown -): thing is Kaioken.VNode & { type: typeof fragmentSymbol } { +): thing is VNode & { type: typeof fragmentSymbol } { return isVNode(thing) && thing.type === fragmentSymbol } -function isContextProvider(thing: unknown) { +function isContextProvider( + thing: unknown +): thing is VNode & { type: typeof contextProviderSymbol } { return isVNode(thing) && thing.type === contextProviderSymbol } -function getVNodeAppContext(node: Kaioken.VNode) { +function getCurrentVNode(): VNode | undefined { + return node.current +} + +function getVNodeAppContext(node: VNode): AppContext { const n = nodeToCtxMap.get(node) - if (!n) throw new KaiokenError("[kaioken]: Unable to find node's AppContext") + if (!n) + throw new KaiokenError({ + message: "Unable to find VNode's AppContext.", + vNode: node, + }) return n } -function commitSnapshot(vNode: Kaioken.VNode) { +function commitSnapshot(vNode: VNode): void { vNode.prev = { ...vNode, props: { ...vNode.props }, prev: undefined } vNode.flags = 0 } function vNodeContains( - haystack: Kaioken.VNode, - needle: Kaioken.VNode, + haystack: VNode, + needle: VNode, checkImmediateSiblings = false ): boolean { if (haystack === needle) return true - const stack: Kaioken.VNode[] = [haystack] + const stack: VNode[] = [haystack] while (stack.length) { const n = stack.pop()! if (n === needle) return true @@ -77,22 +93,73 @@ function vNodeContains( return false } -function traverseApply( - node: Kaioken.VNode, - func: (node: Kaioken.VNode) => void -) { - let commitSiblings = false - const nodes: Kaioken.VNode[] = [node] - const apply = (node: Kaioken.VNode) => { +function traverseApply(node: VNode, func: (node: VNode) => void): void { + let applyToSiblings = false + const nodes: VNode[] = [node] + const apply = (node: VNode) => { func(node) node.child && nodes.push(node.child) - commitSiblings && node.sibling && nodes.push(node.sibling) - commitSiblings = true + applyToSiblings && node.sibling && nodes.push(node.sibling) + applyToSiblings = true } while (nodes.length) apply(nodes.shift()!) } -function shallowCompare(objA: T, objB: T) { +function postOrderApply( + tree: VNode, + callbacks: { + /** called upon traversing to the next parent, and on the root */ + onAscent: (node: VNode) => void + /** called before traversing to the next parent */ + onBeforeAscent?: (node: VNode) => void + /** called before traversing to the next child */ + onDescent?: (node: VNode) => void + } +): void { + const root = tree + const rootChild = root.child + if (!rootChild) { + callbacks.onAscent(root) + return + } + + let branch = rootChild + while (branch) { + let c = branch + while (c) { + callbacks.onDescent?.(c) + if (!c.child) break + c = c.child + } + + while (c && c !== root) { + callbacks.onAscent(c) + if (c.sibling) { + branch = c.sibling + break + } + callbacks.onBeforeAscent?.(c) + c = c.parent! + } + if (c === root) break + } + + callbacks.onAscent(root) +} + +function findParent( + vNode: Kaioken.VNode, + predicate: (n: Kaioken.VNode) => boolean +) { + let n: Kaioken.VNode | undefined = vNode.parent + while (n) { + if (predicate(n)) return n + n = n.parent + } + return undefined +} + +function shallowCompare(objA: T, objB: T): boolean { if (Object.is(objA, objB)) { return true } @@ -263,7 +330,7 @@ const booleanAttributes = [ "wrap", ] -function propToHtmlAttr(key: string) { +function propToHtmlAttr(key: string): string { switch (key) { case "className": return "class" @@ -378,7 +445,7 @@ const snakeCaseAttrs = new Map([ ["xHeight", "x-height"], ]) -function styleObjectToCss(obj: Partial) { +function styleObjectToCss(obj: Partial): string { let cssString = "" for (const key in obj) { const cssKey = key.replace(REGEX_UNIT.ALPHA_UPPER_G, "-$&").toLowerCase() @@ -387,12 +454,12 @@ function styleObjectToCss(obj: Partial) { return cssString } -function propValueToHtmlAttrValue(key: string, value: unknown) { +function propValueToHtmlAttrValue(key: string, value: unknown): string { return key === "style" && typeof value === "object" && !!value ? styleObjectToCss(value) : String(value) } -function propsToElementAttributes(props: Record) { +function propsToElementAttributes(props: Record): string { const attrs: string[] = [] const keys = Object.keys(props).filter(propFilters.isProperty) for (let i = 0; i < keys.length; i++) {