Skip to content

Commit

Permalink
improve all kaioken errors, expose Signal.make(Readonly|Writable)
Browse files Browse the repository at this point in the history
  • Loading branch information
LankyMoose committed Sep 27, 2024
1 parent ef063a9 commit 5d63345
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 223 deletions.
2 changes: 1 addition & 1 deletion packages/lib/src/appContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class AppContext<T extends Record<string, unknown> = {}> {
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<AppContext<T>>((resolve) => {
scheduler.clear()
Expand Down
73 changes: 30 additions & 43 deletions packages/lib/src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -140,12 +141,16 @@ function subTextNode(node: Kaioken.VNode, dom: Text, sig: Signal<string>) {
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)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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[]) {
Expand Down
72 changes: 72 additions & 0 deletions packages/lib/src/error.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,82 @@
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 &&
(error as KaiokenError)[kaiokenErrorSymbol] === true
)
}
}

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
}
9 changes: 6 additions & 3 deletions packages/lib/src/hooks/useSyncExternalStore.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -9,9 +10,11 @@ export function useSyncExternalStore<T>(
): 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()
}
Expand Down
16 changes: 9 additions & 7 deletions packages/lib/src/hooks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,21 @@ function useHook<T, U extends HookCallback<T>>(
hookDataOrInitializer: (() => Hook<T>) | Hook<T>,
callback: U
): ReturnType<U> {
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)
Expand Down Expand Up @@ -141,7 +143,7 @@ function useHook<T, U extends HookCallback<T>>(

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.`
)
}

Expand Down
7 changes: 4 additions & 3 deletions packages/lib/src/portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
7 changes: 4 additions & 3 deletions packages/lib/src/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}
}

Expand Down
Loading

0 comments on commit 5d63345

Please sign in to comment.