From 76ab4096a2ad531ae015e35c2475b78f08ac45a7 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 01/15] wip: Work on rewriting the walker in dom-mode --- packages/debugger/src/main/types.ts | 4 +- packages/debugger/src/structure/walker.ts | 246 +++++++++++++++------- 2 files changed, 171 insertions(+), 79 deletions(-) diff --git a/packages/debugger/src/main/types.ts b/packages/debugger/src/main/types.ts index e1dab1d9..0cc77097 100644 --- a/packages/debugger/src/main/types.ts +++ b/packages/debugger/src/main/types.ts @@ -143,6 +143,8 @@ export type Rect = { height: number } +export type ElementChildren = Iterable & ArrayLike + /** * When using a custom solid renderer, you should provide a custom element interface. * By default the debugger assumes that rendered elements are DOM elements. @@ -151,7 +153,7 @@ export type ElementInterface = { isElement: (obj: object | T) => obj is T, getElementAt: (e: MouseEvent) => T | null, getName: (el: T) => string | null, - getChildren: (el: T) => Iterable, + getChildren: (el: T) => ElementChildren, getParent: (el: T) => T | null, getLocation: (el: T) => SourceLocation | null, getRect: (el: T) => Rect | null, diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index a241adfb..4b0b0cf7 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -2,6 +2,7 @@ import {untrackedCallback} from '@solid-devtools/shared/primitives' import {ObjectType, getSdtId} from '../main/id.ts' import {observeComputationUpdate} from '../main/observe.ts' import { + type ElementChildren, type ElementInterface, type Mapped, type NodeID, @@ -214,8 +215,6 @@ export type TreeWalkerConfig = { eli: ElementInterface } -const ElementsMap = new Map() - const $WALKER = Symbol('tree-walker') function observeComputation( @@ -273,68 +272,33 @@ function pushResolvedElements(list: TEl[], value: unknown, e } } -let MappedOwnerNode: Mapped.Owner -let AddedToParentElements = false - /** - * @param els elements to map - * @param parent_children parent owner children. - * Will be checked for existing elements, and if found, `MappedOwnerNode` will be injected in the place of the element. - * Passing `undefined` will skip this check. + * Updates a map of Element to Component_Owner by traversing the owner tree + * + * @param owner owner to start traversal from + * @param eli Element interface + * @param map Optional existing map to update + * + * The elements are resolved shallowly, + * so only top-level elements will be mapped to their components. */ -function mapElements( - els: Iterable, - parent_children: Mapped.Owner[] | undefined, - eli: ElementInterface, - out: Mapped.Owner[] = [], -): Mapped.Owner[] { - - els: for (let el of els) { - if (!eli.isElement(el)) continue - - if (parent_children) { - - // find el in parent els and remove it - let to_check = [parent_children] - let index = 0 - let el_nodes = to_check[index++] - - while (el_nodes) { - for (let i = 0; i < el_nodes.length; i++) { - let el_node = el_nodes[i]! - let el_node_data = ElementsMap.get(el_node) - if (el_node_data && el_node_data.el === el) { - if (AddedToParentElements) { - // if the element is already added to the parent, just remove the element - el_nodes.splice(i, 1) - } else { - // otherwise, we can just replace it with the component - el_nodes[i] = MappedOwnerNode - AddedToParentElements = true - } - out.push(el_node) - el_node_data.component = MappedOwnerNode - continue els - } - if (el_node.children.length) to_check.push(el_node.children) - } - el_nodes = to_check[index++] - } +export function gatherElementMap( + owner: Solid.Owner, + map: Map = new Map(), + eli: ElementInterface, +): Map { + + if (markOwnerType(owner) === NodeType.Component) { + for (let el of resolveElements(owner.value, eli)) { + map.set(el, owner as Solid.Component) } - - let el_json: Mapped.Owner = { - id: getSdtId(el, ObjectType.Element), - type: NodeType.Element, - name: eli.getName(el) ?? UNKNOWN, - children: [], - } - out.push(el_json) - ElementsMap.set(el_json, {el, component: MappedOwnerNode}) - - mapElements(eli.getChildren(el), parent_children, eli, el_json.children) } - - return out + + for (let child of owner_each_child(owner)) { + gatherElementMap(child, map, eli) + } + + return map } function mapChildren( @@ -360,6 +324,8 @@ function mapChildren( return children } +let element_set = new Set() + function mapOwner( owner: Solid.Owner, parent: Mapped.Owner | null, @@ -390,7 +356,7 @@ function mapOwner( The provider component will be omitted */ if (name === 'provider' && - owner.owned && + owner.owned != null && owner.owned.length === 1 && markOwnerType(first_owned = owner.owned[0]!) === NodeType.Context ) { @@ -404,7 +370,7 @@ function mapOwner( // Refresh // omitting refresh memo — map it's children instead let refresh = getComponentRefreshNode(owner as Solid.Component) - if (refresh) { + if (refresh != null) { mapped.hmr = true owner = refresh } @@ -417,24 +383,153 @@ function mapOwner( } } - AddedToParentElements = false as boolean - MappedOwnerNode = mapped - + mapChildren(owner, mapped, config, mapped.children) + // Map html elements in DOM mode if (config.mode === TreeWalkerMode.DOM) { // elements might already be resolved when mapping components resolved_els ??= resolveElements(owner.value, config.eli) - mapElements(resolved_els, parent?.children, config.eli, mapped.children) - } - // global `AddedToParentElements` will be changed in mapChildren - let addedToParent = AddedToParentElements + let elements_stack_arr = [resolved_els] as (ElementChildren)[] + let elements_stack_idx = [0] + let elements_stack_owner = [mapped] + let elements_stack_len = 1 - mapChildren(owner, mapped, config, mapped.children) + let children_stack_arr = [mapped.children] + let children_stack_idx = [0] + let children_stack_len = 1 - return addedToParent ? undefined : mapped -} + while (children_stack_len > 0) { + + let children = children_stack_arr[children_stack_len-1]! + let child_idx = children_stack_idx[children_stack_len-1]! + + if (child_idx >= children.length) { + children_stack_len -= 1 + continue + } + + children_stack_idx[children_stack_len-1]! += 1 + + let child = children[child_idx]! + + if (child.type === NodeType.Element) { + + // Don't go over added element children + // TODO: add children cap stack + if (children_stack_len-1 === 0) { + continue + } + + while (elements_stack_len > 0) { + + let elements = elements_stack_arr [elements_stack_len-1]! + let el_idx = elements_stack_idx [elements_stack_len-1]! + let el_owner = elements_stack_owner[elements_stack_len-1]! + + if (el_idx >= elements.length) { + elements_stack_len -= 1 + continue + } + + elements_stack_idx[elements_stack_len-1]! += 1 + + let el = elements[el_idx]! + let el_id = getSdtId(el, ObjectType.Element) + + // Child has this element + if (el_id === child.id) { + if (elements_stack_len > 1) { + el_owner.children.push(children_stack_arr[0]![children_stack_idx[0]!-1]!) + children_stack_arr[0]!.splice(children_stack_idx[0]!-1, 1) + children_stack_idx[0]! -= 1 + } + children_stack_len = 0 + // Skip remaining elements from the child + for (let skip_child_idx = child_idx + 1;;) { + let el_idx = elements_stack_idx[elements_stack_len-1]! + + if (el_idx >= elements.length || + skip_child_idx >= children.length || + children[skip_child_idx]!.id !== getSdtId(elements[el_idx]!, ObjectType.Element) + ) { + break + } + + elements_stack_idx[elements_stack_len-1]! += 1 + skip_child_idx += 1 + } + + break + } + + if (element_set.has(el)) { + continue + } + + else { + let el_json: Mapped.Owner = { + id: el_id, + type: NodeType.Element, + name: config.eli.getName(el) ?? UNKNOWN, + children: [], + } + el_owner.children.push(el_json) + element_set.add(el) + + elements_stack_arr [elements_stack_len] = config.eli.getChildren(el) + elements_stack_idx [elements_stack_len] = 0 + elements_stack_owner[elements_stack_len] = el_json + elements_stack_len += 1 + } + } + + } else { + children_stack_arr[children_stack_len] = child.children + children_stack_idx[children_stack_len] = 0 + children_stack_len += 1 + continue + } + } + + // append remaining elements to children + while (elements_stack_len > 0) { + let elements = elements_stack_arr [elements_stack_len-1]! + let idx = elements_stack_idx [elements_stack_len-1]! + let el_owner = elements_stack_owner[elements_stack_len-1]! + + if (idx >= elements.length) { + elements_stack_len -= 1 + continue + } + + elements_stack_idx[elements_stack_len-1]! += 1 + + let el = elements[idx]! + + if (element_set.has(el)) { + continue + } + + let el_json: Mapped.Owner = { + id: getSdtId(el, ObjectType.Element), + type: NodeType.Element, + name: config.eli.getName(el) ?? UNKNOWN, + children: [], + } + el_owner.children.push(el_json) + element_set.add(el) + + elements_stack_arr [elements_stack_len] = config.eli.getChildren(el) + elements_stack_idx [elements_stack_len] = 0 + elements_stack_owner[elements_stack_len] = el_json + elements_stack_len += 1 + } + } + + return mapped +} export const walkSolidTree = /*#__PURE__*/ untrackedCallback(function ( owner: Solid.Owner | Solid.Root, @@ -444,12 +539,7 @@ export const walkSolidTree = /*#__PURE__*/ untrackedCallback(function Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 02/15] Fix annoying mistake --- examples/sandbox/src/App.tsx | 124 +++++++++++----------- packages/debugger/src/structure/walker.ts | 2 +- 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/examples/sandbox/src/App.tsx b/examples/sandbox/src/App.tsx index 9ee40c77..c8cf5373 100644 --- a/examples/sandbox/src/App.tsx +++ b/examples/sandbox/src/App.tsx @@ -163,69 +163,67 @@ const App: s.Component = () => { const [showBroken, setShowBroken] = s.createSignal(false) - return ( - <> - {header()} - {objmemo().subheader} -
-
-
- - - {s.createComponent(() => <> - - , {})} - - - {/* -
*/} - - <> - {err.toString()} - - } - > - - - - -
-
-
- - - -
- - {s.untrack(() => { - const MARGIN = '24px' - return <> -
- -
-
- -
- - })} - - - ) + return <> + {header()} + {objmemo().subheader} +
+
+
+ + + {s.createComponent(() => <> + + , {})} + + + {/* +
*/} + + <> + {err.toString()} + + } + > + + + + +
+
+
+ + + +
+ + {s.untrack(() => { + const MARGIN = '24px' + return <> +
+ +
+
+ +
+ + })} + + } const CountingComponent = () => { diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index 4b0b0cf7..498c732d 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -444,7 +444,7 @@ function mapOwner( children_stack_arr[0]!.splice(children_stack_idx[0]!-1, 1) children_stack_idx[0]! -= 1 } - children_stack_len = 0 + children_stack_len = 1 // Skip remaining elements from the child for (let skip_child_idx = child_idx + 1;;) { From cfea7b12496af86e2e8741a2317f1e88214417d7 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 03/15] Cleanup --- packages/debugger/src/main/types.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/debugger/src/main/types.ts b/packages/debugger/src/main/types.ts index 0cc77097..d71b5217 100644 --- a/packages/debugger/src/main/types.ts +++ b/packages/debugger/src/main/types.ts @@ -198,17 +198,14 @@ export const getValueItemId = ( export type ValueUpdateListener = (newValue: unknown, oldValue: unknown) => void export namespace Solid { - export type OwnerBase = import('solid-js').Owner - export type SourceMapValue = import('solid-js/types/reactive/signal.d.ts').SourceMapValue - export type Signal = import('solid-js/types/reactive/signal.d.ts').SignalState - export type Computation = import('solid-js/types/reactive/signal.d.ts').Computation - export type Memo = import('solid-js/types/reactive/signal.d.ts').Memo + export type OwnerBase = import('solid-js').Owner + export type SourceMapValue = import('solid-js/types/reactive/signal.d.ts').SourceMapValue + export type Signal = import('solid-js/types/reactive/signal.d.ts').SignalState + export type Computation = import('solid-js/types/reactive/signal.d.ts').Computation + export type Memo = import('solid-js/types/reactive/signal.d.ts').Memo export type RootFunction = import('solid-js/types/reactive/signal.d.ts').RootFunction - export type EffectFunction = - import('solid-js/types/reactive/signal.d.ts').EffectFunction - export type Component = import('solid-js/types/reactive/signal.d.ts').DevComponent<{ - [key: string]: unknown - }> + export type EffectFunction = import('solid-js/types/reactive/signal.d.ts').EffectFunction + export type Component = import('solid-js/types/reactive/signal.d.ts').DevComponent<{[key: string]: unknown}> export type CatchError = Omit & {fn: undefined} @@ -232,10 +229,10 @@ export namespace Solid { // STORE // - export type StoreNode = import('solid-js/store').StoreNode - export type NotWrappable = import('solid-js/store').NotWrappable + export type StoreNode = import('solid-js/store').StoreNode + export type NotWrappable = import('solid-js/store').NotWrappable export type OnStoreNodeUpdate = import('solid-js/store/types/store.d.ts').OnStoreNodeUpdate - export type Store = SourceMapValue & {value: StoreNode} + export type Store = SourceMapValue & {value: StoreNode} } declare module 'solid-js/types/reactive/signal.d.ts' { From d101bce479bd7dbab3cc3ed79c4c0a9a906a6394 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 04/15] Improve element mapping algorithm --- packages/debugger/src/main/id.ts | 7 ++ packages/debugger/src/structure/walker.ts | 146 +++++++++++----------- 2 files changed, 77 insertions(+), 76 deletions(-) diff --git a/packages/debugger/src/main/id.ts b/packages/debugger/src/main/id.ts index b0d4ca1f..77437f7b 100644 --- a/packages/debugger/src/main/id.ts +++ b/packages/debugger/src/main/id.ts @@ -49,6 +49,13 @@ export function getSdtId(obj: ValueMap[T], objType: T): No return id } +export const get_id_owner = (o: Solid.Owner): NodeID => getSdtId(o, ObjectType.Owner) +export const get_id_el = (o: object): NodeID => getSdtId(o, ObjectType.Element) +export const get_id_signal = (o: Solid.Signal): NodeID => getSdtId(o, ObjectType.Signal) +export const get_id_store = (o: Solid.Store): NodeID => getSdtId(o, ObjectType.Store) +export const get_id_store_node = (o: Solid.StoreNode): NodeID => getSdtId(o, ObjectType.StoreNode) +export const get_id_custom_value = (o: Solid.SourceMapValue): NodeID => getSdtId(o, ObjectType.CustomValue) + export function getObjectById(id: NodeID, objType: T): ValueMap[T] | null { const ref = RefMapMap[objType].get(id) return ref?.deref() ?? null diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index 498c732d..993a32d2 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -1,5 +1,5 @@ import {untrackedCallback} from '@solid-devtools/shared/primitives' -import {ObjectType, getSdtId} from '../main/id.ts' +import {ObjectType, getSdtId, get_id_el} from '../main/id.ts' import {observeComputationUpdate} from '../main/observe.ts' import { type ElementChildren, @@ -326,6 +326,13 @@ function mapChildren( let element_set = new Set() +const make_el_json = (el: TEl, eli: ElementInterface): Mapped.Owner => ({ + id: get_id_el(el), + type: NodeType.Element, + name: eli.getName(el) ?? UNKNOWN, + children: [], +}) + function mapOwner( owner: Solid.Owner, parent: Mapped.Owner | null, @@ -387,6 +394,7 @@ function mapOwner( // Map html elements in DOM mode if (config.mode === TreeWalkerMode.DOM) { + // elements might already be resolved when mapping components resolved_els ??= resolveElements(owner.value, config.eli) @@ -412,84 +420,77 @@ function mapOwner( children_stack_idx[children_stack_len-1]! += 1 let child = children[child_idx]! - - if (child.type === NodeType.Element) { - // Don't go over added element children - // TODO: add children cap stack - if (children_stack_len-1 === 0) { + /* Other children are already in mapped children so will be skipped */ + if (child.type !== NodeType.Element) { + children_stack_arr[children_stack_len] = child.children + children_stack_idx[children_stack_len] = 0 + children_stack_len += 1 + continue + } + + // Don't go over added element children + // TODO: add children cap stack + if (children_stack_len-1 === 0) { + continue + } + + while (elements_stack_len > 0) { + + let elements = elements_stack_arr [elements_stack_len-1]! + let el_idx = elements_stack_idx [elements_stack_len-1]! + let el_owner = elements_stack_owner[elements_stack_len-1]! + + if (el_idx >= elements.length) { + elements_stack_len -= 1 continue } - - while (elements_stack_len > 0) { - - let elements = elements_stack_arr [elements_stack_len-1]! - let el_idx = elements_stack_idx [elements_stack_len-1]! - let el_owner = elements_stack_owner[elements_stack_len-1]! - - if (el_idx >= elements.length) { - elements_stack_len -= 1 - continue - } - elements_stack_idx[elements_stack_len-1]! += 1 - - let el = elements[el_idx]! - let el_id = getSdtId(el, ObjectType.Element) - - // Child has this element - if (el_id === child.id) { - if (elements_stack_len > 1) { - el_owner.children.push(children_stack_arr[0]![children_stack_idx[0]!-1]!) - children_stack_arr[0]!.splice(children_stack_idx[0]!-1, 1) - children_stack_idx[0]! -= 1 - } - children_stack_len = 1 - - // Skip remaining elements from the child - for (let skip_child_idx = child_idx + 1;;) { - let el_idx = elements_stack_idx[elements_stack_len-1]! - - if (el_idx >= elements.length || - skip_child_idx >= children.length || - children[skip_child_idx]!.id !== getSdtId(elements[el_idx]!, ObjectType.Element) - ) { - break - } + elements_stack_idx[elements_stack_len-1]! += 1 - elements_stack_idx[elements_stack_len-1]! += 1 - skip_child_idx += 1 + let el = elements[el_idx]! + let el_id = get_id_el(el) + + // Child has this element + if (el_id === child.id) { + /* + Push child to the owner + and previous ones that were not pushed yet + */ + for (let i = 0; i < children_stack_idx[0]!; i++) { + el_owner.children.push(children_stack_arr[0]![i]!) + } + children_stack_arr[0]!.splice(0, children_stack_idx[0]) + children_stack_idx[0] = 0 + children_stack_len = 1 + + // Skip remaining elements from the child + for (let ci = child_idx + 1; ci < children.length; ci++) { + let ei = elements_stack_idx[elements_stack_len-1]! + + if (ei >= elements.length || + children[ci]!.id !== get_id_el(elements[ei]!) + ) { + break } - break + elements_stack_idx[elements_stack_len-1]! += 1 } - if (element_set.has(el)) { - continue - } + break + } - else { - let el_json: Mapped.Owner = { - id: el_id, - type: NodeType.Element, - name: config.eli.getName(el) ?? UNKNOWN, - children: [], - } - el_owner.children.push(el_json) - element_set.add(el) + /* Not seen yet */ + if (!element_set.has(el)) { + let el_json = make_el_json(el, config.eli) + el_owner.children.push(el_json) + element_set.add(el) - elements_stack_arr [elements_stack_len] = config.eli.getChildren(el) - elements_stack_idx [elements_stack_len] = 0 - elements_stack_owner[elements_stack_len] = el_json - elements_stack_len += 1 - } + elements_stack_arr [elements_stack_len] = config.eli.getChildren(el) + elements_stack_idx [elements_stack_len] = 0 + elements_stack_owner[elements_stack_len] = el_json + elements_stack_len += 1 } - - } else { - children_stack_arr[children_stack_len] = child.children - children_stack_idx[children_stack_len] = 0 - children_stack_len += 1 - continue } } @@ -508,16 +509,9 @@ function mapOwner( let el = elements[idx]! - if (element_set.has(el)) { - continue - } + if (element_set.has(el)) continue - let el_json: Mapped.Owner = { - id: getSdtId(el, ObjectType.Element), - type: NodeType.Element, - name: config.eli.getName(el) ?? UNKNOWN, - children: [], - } + let el_json = make_el_json(el, config.eli) el_owner.children.push(el_json) element_set.add(el) From 00e9a889723ab081100bff2298ddce44d4928048 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 05/15] Cleanup --- packages/debugger/src/structure/walker.ts | 123 ++++++++++------------ 1 file changed, 58 insertions(+), 65 deletions(-) diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index 993a32d2..e9370dff 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -324,7 +324,7 @@ function mapChildren( return children } -let element_set = new Set() +let els_seen = new Set() const make_el_json = (el: TEl, eli: ElementInterface): Mapped.Owner => ({ id: get_id_el(el), @@ -398,127 +398,120 @@ function mapOwner( // elements might already be resolved when mapping components resolved_els ??= resolveElements(owner.value, config.eli) - let elements_stack_arr = [resolved_els] as (ElementChildren)[] - let elements_stack_idx = [0] - let elements_stack_owner = [mapped] - let elements_stack_len = 1 + let stack_els_arr: ElementChildren[] = [resolved_els] + let stack_els_idx = [0] + let stack_els_own = [mapped] + let stack_els_len = 1 - let children_stack_arr = [mapped.children] - let children_stack_idx = [0] - let children_stack_len = 1 + let stack_child_arr = [mapped.children] + let stack_child_idx = [0] + let stack_child_len = 1 - while (children_stack_len > 0) { + while (stack_child_len > 0) { - let children = children_stack_arr[children_stack_len-1]! - let child_idx = children_stack_idx[children_stack_len-1]! + let child_arr = stack_child_arr[stack_child_len-1]! + let child_idx = stack_child_idx[stack_child_len-1]! - if (child_idx >= children.length) { - children_stack_len -= 1 + if (child_idx >= child_arr.length) { + stack_child_len -= 1 continue } - children_stack_idx[children_stack_len-1]! += 1 + stack_child_idx[stack_child_len-1]! += 1 - let child = children[child_idx]! + let child = child_arr[child_idx]! /* Other children are already in mapped children so will be skipped */ if (child.type !== NodeType.Element) { - children_stack_arr[children_stack_len] = child.children - children_stack_idx[children_stack_len] = 0 - children_stack_len += 1 + stack_child_arr[stack_child_len] = child.children + stack_child_idx[stack_child_len] = 0 + stack_child_len += 1 continue } // Don't go over added element children // TODO: add children cap stack - if (children_stack_len-1 === 0) { + if (stack_child_len-1 === 0) { continue } - while (elements_stack_len > 0) { + while (stack_els_len > 0) { - let elements = elements_stack_arr [elements_stack_len-1]! - let el_idx = elements_stack_idx [elements_stack_len-1]! - let el_owner = elements_stack_owner[elements_stack_len-1]! + let el_arr = stack_els_arr[stack_els_len-1]! + let el_idx = stack_els_idx[stack_els_len-1]! + let el_own = stack_els_own[stack_els_len-1]! - if (el_idx >= elements.length) { - elements_stack_len -= 1 + if (el_idx >= el_arr.length) { + stack_els_len -= 1 continue } - elements_stack_idx[elements_stack_len-1]! += 1 + stack_els_idx[stack_els_len-1]! += 1 - let el = elements[el_idx]! - let el_id = get_id_el(el) + let el = el_arr[el_idx]! // Child has this element - if (el_id === child.id) { + if (get_id_el(el) === child.id) { /* Push child to the owner and previous ones that were not pushed yet */ - for (let i = 0; i < children_stack_idx[0]!; i++) { - el_owner.children.push(children_stack_arr[0]![i]!) - } - children_stack_arr[0]!.splice(0, children_stack_idx[0]) - children_stack_idx[0] = 0 - children_stack_len = 1 + el_own.children.push(...mapped.children.splice(0, stack_child_idx[0])) + stack_child_idx[0] = 0 + stack_child_len = 1 // Skip remaining elements from the child - for (let ci = child_idx + 1; ci < children.length; ci++) { - let ei = elements_stack_idx[elements_stack_len-1]! + for (let ci = child_idx + 1; ci < child_arr.length; ci++) { + let ei = stack_els_idx[stack_els_len-1]! - if (ei >= elements.length || - children[ci]!.id !== get_id_el(elements[ei]!) - ) { + if (ei >= el_arr.length || child_arr[ci]!.id !== get_id_el(el_arr[ei]!)) break - } - elements_stack_idx[elements_stack_len-1]! += 1 + stack_els_idx[stack_els_len-1]! += 1 } break } /* Not seen yet */ - if (!element_set.has(el)) { + if (!els_seen.has(el)) { let el_json = make_el_json(el, config.eli) - el_owner.children.push(el_json) - element_set.add(el) + el_own.children.push(el_json) + els_seen.add(el) - elements_stack_arr [elements_stack_len] = config.eli.getChildren(el) - elements_stack_idx [elements_stack_len] = 0 - elements_stack_owner[elements_stack_len] = el_json - elements_stack_len += 1 + stack_els_arr[stack_els_len] = config.eli.getChildren(el) + stack_els_idx[stack_els_len] = 0 + stack_els_own[stack_els_len] = el_json + stack_els_len += 1 } } } // append remaining elements to children - while (elements_stack_len > 0) { - let elements = elements_stack_arr [elements_stack_len-1]! - let idx = elements_stack_idx [elements_stack_len-1]! - let el_owner = elements_stack_owner[elements_stack_len-1]! + while (stack_els_len > 0) { + let el_arr = stack_els_arr[stack_els_len-1]! + let el_idx = stack_els_idx[stack_els_len-1]! + let el_own = stack_els_own[stack_els_len-1]! - if (idx >= elements.length) { - elements_stack_len -= 1 + if (el_idx >= el_arr.length) { + stack_els_len -= 1 continue } - elements_stack_idx[elements_stack_len-1]! += 1 + stack_els_idx[stack_els_len-1]! += 1 - let el = elements[idx]! + let el = el_arr[el_idx]! - if (element_set.has(el)) continue + if (els_seen.has(el)) continue let el_json = make_el_json(el, config.eli) - el_owner.children.push(el_json) - element_set.add(el) + el_own.children.push(el_json) + els_seen.add(el) - elements_stack_arr [elements_stack_len] = config.eli.getChildren(el) - elements_stack_idx [elements_stack_len] = 0 - elements_stack_owner[elements_stack_len] = el_json - elements_stack_len += 1 + stack_els_arr[stack_els_len] = config.eli.getChildren(el) + stack_els_idx[stack_els_len] = 0 + stack_els_own[stack_els_len] = el_json + stack_els_len += 1 } } @@ -533,7 +526,7 @@ export const walkSolidTree = /*#__PURE__*/ untrackedCallback(function Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 06/15] Cleanup --- packages/debugger/src/structure/walker.ts | 49 ++++++++++++----------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index e9370dff..1651876b 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -324,7 +324,7 @@ function mapChildren( return children } -let els_seen = new Set() +let els_seen = new Set() const make_el_json = (el: TEl, eli: ElementInterface): Mapped.Owner => ({ id: get_id_el(el), @@ -332,6 +332,16 @@ const make_el_json = (el: TEl, eli: ElementInterface): name: eli.getName(el) ?? UNKNOWN, children: [], }) +const push_make_el_json = ( + arr: Mapped.Owner[], + el: TEl, + eli: ElementInterface, +): Mapped.Owner => { + let el_json = make_el_json(el, eli) + arr.push(el_json) + els_seen.add(el) + return el_json +} function mapOwner( owner: Solid.Owner, @@ -408,7 +418,6 @@ function mapOwner( let stack_child_len = 1 while (stack_child_len > 0) { - let child_arr = stack_child_arr[stack_child_len-1]! let child_idx = stack_child_idx[stack_child_len-1]! @@ -430,13 +439,15 @@ function mapOwner( } // Don't go over added element children - // TODO: add children cap stack - if (stack_child_len-1 === 0) { + if (stack_child_len === 1) continue - } - + + /* Check each element and its children + - not seen -> add it to the owner + - a child of the current child -> add it to the owner + - already seen -> skip it + */ while (stack_els_len > 0) { - let el_arr = stack_els_arr[stack_els_len-1]! let el_idx = stack_els_idx[stack_els_len-1]! let el_own = stack_els_own[stack_els_len-1]! @@ -450,7 +461,7 @@ function mapOwner( let el = el_arr[el_idx]! - // Child has this element + /* Child has this element */ if (get_id_el(el) === child.id) { /* Push child to the owner @@ -475,13 +486,9 @@ function mapOwner( /* Not seen yet */ if (!els_seen.has(el)) { - let el_json = make_el_json(el, config.eli) - el_own.children.push(el_json) - els_seen.add(el) - stack_els_arr[stack_els_len] = config.eli.getChildren(el) stack_els_idx[stack_els_len] = 0 - stack_els_own[stack_els_len] = el_json + stack_els_own[stack_els_len] = push_make_el_json(el_own.children, el, config.eli) stack_els_len += 1 } } @@ -502,16 +509,12 @@ function mapOwner( let el = el_arr[el_idx]! - if (els_seen.has(el)) continue - - let el_json = make_el_json(el, config.eli) - el_own.children.push(el_json) - els_seen.add(el) - - stack_els_arr[stack_els_len] = config.eli.getChildren(el) - stack_els_idx[stack_els_len] = 0 - stack_els_own[stack_els_len] = el_json - stack_els_len += 1 + if (!els_seen.has(el)) { + stack_els_arr[stack_els_len] = config.eli.getChildren(el) + stack_els_idx[stack_els_len] = 0 + stack_els_own[stack_els_len] = push_make_el_json(el_own.children, el, config.eli) + stack_els_len += 1 + } } } From 66eee74766413634e808fd54e6173a63f48b4b60 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 07/15] Register mapped elements --- packages/debugger/src/structure/walker.ts | 92 ++++++++++++----------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index 1651876b..98be471c 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -113,37 +113,41 @@ const registerComponent = ( export const registerElement = ( - r: ComponentRegistry, - componentId: NodeID, - elementId: NodeID, - element: TEl, + r: ComponentRegistry, + component_id: NodeID, + element_id: NodeID, + element: TEl, ): void => { - let component = r.components.get(componentId) - if (!component) return + let component = r.components.get(component_id) + if (component == null) return - component.element_nodes.add(elementId) - r.element_nodes.set(elementId, {el: element as any as TEl, component}) + component.element_nodes.add(element_id) + r.element_nodes.set(element_id, {el: element, component}) } export const getComponent = ( - r: ComponentRegistry, + r: ComponentRegistry, id: NodeID, ): {name: string | undefined; id: NodeID; elements: TEl[]} | null => { // provided if might be of an element node (in DOM mode) or component node // both need to be checked let component = r.components.get(id) - if (component) return { - name: component.name, - elements: [...component.elements].map(el => el as any as TEl), - id + if (component != null) return { + name: component.name, + elements: [...component.elements], + id: id, } - let elData = r.element_nodes.get(id) - return elData - ? {name: elData.component.name, id: elData.component.id, elements: [elData.el]} - : null + let el_data = r.element_nodes.get(id) + if (el_data == null) return null + + return { + name: el_data.component.name, + id: el_data.component.id, + elements: [el_data.el], + } } /** @@ -156,11 +160,13 @@ const getComponent = ( export const getComponentElement = ( r: ComponentRegistry, - elementId: NodeID, + element_id: NodeID, ): {name: string | undefined; id: NodeID; element: TEl} | undefined => { - let el_data = r.element_nodes.get(elementId) - if (el_data != null) { - return {name: el_data.component.name, id: el_data.component.id, element: el_data.el} + let el_data = r.element_nodes.get(element_id) + if (el_data != null) return { + name: el_data.component.name, + id: el_data.component.id, + element: el_data.el, } } @@ -201,13 +207,15 @@ const findComponent = ( } -export type ComputationUpdateHandler = ( +export +type ComputationUpdateHandler = ( rootId: NodeID, owner: Solid.Owner, changedStructure: boolean, ) => void -export type TreeWalkerConfig = { +export +type TreeWalkerConfig = { mode: TreeWalkerMode rootId: NodeID onUpdate: ComputationUpdateHandler @@ -326,20 +334,21 @@ function mapChildren( let els_seen = new Set() -const make_el_json = (el: TEl, eli: ElementInterface): Mapped.Owner => ({ - id: get_id_el(el), - type: NodeType.Element, - name: eli.getName(el) ?? UNKNOWN, - children: [], -}) -const push_make_el_json = ( - arr: Mapped.Owner[], - el: TEl, - eli: ElementInterface, +const add_new_el_json = ( + comp_id: NodeID, + child_arr: Mapped.Owner[], + el: TEl, + config: TreeWalkerConfig, ): Mapped.Owner => { - let el_json = make_el_json(el, eli) - arr.push(el_json) + let el_json: Mapped.Owner = { + id: get_id_el(el), + type: NodeType.Element, + name: config.eli.getName(el) ?? UNKNOWN, + children: [], + } + child_arr.push(el_json) els_seen.add(el) + registerElement(config.registry, comp_id, el_json.id, el) return el_json } @@ -488,7 +497,7 @@ function mapOwner( if (!els_seen.has(el)) { stack_els_arr[stack_els_len] = config.eli.getChildren(el) stack_els_idx[stack_els_len] = 0 - stack_els_own[stack_els_len] = push_make_el_json(el_own.children, el, config.eli) + stack_els_own[stack_els_len] = add_new_el_json(id, el_own.children, el, config) stack_els_len += 1 } } @@ -512,7 +521,7 @@ function mapOwner( if (!els_seen.has(el)) { stack_els_arr[stack_els_len] = config.eli.getChildren(el) stack_els_idx[stack_els_len] = 0 - stack_els_own[stack_els_len] = push_make_el_json(el_own.children, el, config.eli) + stack_els_own[stack_els_len] = add_new_el_json(id, el_own.children, el, config) stack_els_len += 1 } } @@ -521,16 +530,15 @@ function mapOwner( return mapped } -export const walkSolidTree = /*#__PURE__*/ untrackedCallback(function ( +export +const walkSolidTree = /*#__PURE__*/ untrackedCallback(function ( owner: Solid.Owner | Solid.Root, config: TreeWalkerConfig, ): Mapped.Owner { - const r = mapOwner(owner, null, config)! + let r = mapOwner(owner, null, config)! - if (config.mode === TreeWalkerMode.DOM) { - els_seen.clear() - } + els_seen.clear() return r }) From 4d37415af1e936705b4e24910e7f2b40ca3b13a7 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 08/15] Register elements to the correct component --- packages/debugger/src/structure/walker.ts | 43 ++++++++++++++--------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index 98be471c..9af30754 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -114,10 +114,15 @@ const registerComponent = ( export const registerElement = ( r: ComponentRegistry, - component_id: NodeID, + component_id: NodeID | null, element_id: NodeID, element: TEl, ): void => { + // TODO: also allow registering elements without a component + if (component_id == null) { + return + } + let component = r.components.get(component_id) if (component == null) return @@ -310,22 +315,22 @@ export function gatherElementMap( } function mapChildren( - owner: Solid.Owner, - owner_map: Mapped.Owner | null, - config: TreeWalkerConfig, - children: Mapped.Owner[] = [], + owner: Solid.Owner, + component_id: NodeID | null, + config: TreeWalkerConfig, + children: Mapped.Owner[] = [], ): Mapped.Owner[] { for (let child of owner_each_child(owner)) { if (config.mode === TreeWalkerMode.Owners || markOwnerType(child) === NodeType.Component ) { - unwrap_append(children, mapOwner(child, owner_map, config)) + unwrap_append(children, mapOwner(child, component_id, config)) } else { if (isSolidComputation(child)) { observeComputation(child, owner, config) } - mapChildren(child, owner_map, config, children) + mapChildren(child, component_id, config, children) } } @@ -334,12 +339,12 @@ function mapChildren( let els_seen = new Set() -const add_new_el_json = ( - comp_id: NodeID, +function add_new_el_json( child_arr: Mapped.Owner[], el: TEl, + comp_id: NodeID | null, config: TreeWalkerConfig, -): Mapped.Owner => { +): Mapped.Owner { let el_json: Mapped.Owner = { id: get_id_el(el), type: NodeType.Element, @@ -353,9 +358,10 @@ const add_new_el_json = ( } function mapOwner( - owner: Solid.Owner, - parent: Mapped.Owner | null, - config: TreeWalkerConfig, + owner: Solid.Owner, + /** last seen component id - for registering elements to components */ + last_comp_id: NodeID | null, + config: TreeWalkerConfig, ): Mapped.Owner | undefined { let id = getSdtId(owner, ObjectType.Owner) @@ -386,7 +392,7 @@ function mapOwner( owner.owned.length === 1 && markOwnerType(first_owned = owner.owned[0]!) === NodeType.Context ) { - return mapOwner(first_owned, parent, config) + return mapOwner(first_owned, last_comp_id, config) } // Register component to global map @@ -400,6 +406,9 @@ function mapOwner( mapped.hmr = true owner = refresh } + + /* This owner is now last seen component */ + last_comp_id = id } // Computation else if (isSolidComputation(owner)) { @@ -409,7 +418,7 @@ function mapOwner( } } - mapChildren(owner, mapped, config, mapped.children) + mapChildren(owner, last_comp_id, config, mapped.children) // Map html elements in DOM mode if (config.mode === TreeWalkerMode.DOM) { @@ -497,7 +506,7 @@ function mapOwner( if (!els_seen.has(el)) { stack_els_arr[stack_els_len] = config.eli.getChildren(el) stack_els_idx[stack_els_len] = 0 - stack_els_own[stack_els_len] = add_new_el_json(id, el_own.children, el, config) + stack_els_own[stack_els_len] = add_new_el_json(el_own.children, el, last_comp_id, config) stack_els_len += 1 } } @@ -521,7 +530,7 @@ function mapOwner( if (!els_seen.has(el)) { stack_els_arr[stack_els_len] = config.eli.getChildren(el) stack_els_idx[stack_els_len] = 0 - stack_els_own[stack_els_len] = add_new_el_json(id, el_own.children, el, config) + stack_els_own[stack_els_len] = add_new_el_json(el_own.children, el, last_comp_id, config) stack_els_len += 1 } } From a4801b2f9d2e71d41aa6862ab17d0e02cb0adfee Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 09/15] Remove unused gatherElementMap --- packages/debugger/src/structure/walker.ts | 29 ----------------------- 1 file changed, 29 deletions(-) diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index 9af30754..7e380acd 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -285,35 +285,6 @@ function pushResolvedElements(list: TEl[], value: unknown, e } } -/** - * Updates a map of Element to Component_Owner by traversing the owner tree - * - * @param owner owner to start traversal from - * @param eli Element interface - * @param map Optional existing map to update - * - * The elements are resolved shallowly, - * so only top-level elements will be mapped to their components. - */ -export function gatherElementMap( - owner: Solid.Owner, - map: Map = new Map(), - eli: ElementInterface, -): Map { - - if (markOwnerType(owner) === NodeType.Component) { - for (let el of resolveElements(owner.value, eli)) { - map.set(el, owner as Solid.Component) - } - } - - for (let child of owner_each_child(owner)) { - gatherElementMap(child, map, eli) - } - - return map -} - function mapChildren( owner: Solid.Owner, component_id: NodeID | null, From f45da51d556187b1230bad5e38c51868ffcaaa76 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 10/15] Add a nice comment --- packages/debugger/src/structure/walker.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index 7e380acd..8c5c972e 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -391,7 +391,22 @@ function mapOwner( mapChildren(owner, last_comp_id, config, mapped.children) - // Map html elements in DOM mode + /* + | Merge element tree and owner tree depth-first, using elements as positional hints + | + | Component A +
-> Component A + | └─ Component B ├─ └─
+ | └─

├─ Component B + | │ └─ + | └─

+ | 1. Walk owner children first (depth-first) + | - child elements are already part of children tree + | 2. For each element in resolved tree: + | - If element matches current child owner → attach preceding children to element + | - If element not seen → add as new element node + | - Skip already processed elements + | 3. Use dual stacks to track position in both trees simultaneously + */ if (config.mode === TreeWalkerMode.DOM) { // elements might already be resolved when mapping components @@ -432,8 +447,8 @@ function mapOwner( continue /* Check each element and its children - - not seen -> add it to the owner - - a child of the current child -> add it to the owner + - not seen -> add element to the owner + - a child of the current child -> add child to the owner - already seen -> skip it */ while (stack_els_len > 0) { From 9316f3b56941bdf1c913e16b5c08439adb1ff22c Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:49:05 +0200 Subject: [PATCH 11/15] Update closest tree component on owner update to see if the structure changed --- packages/debugger/src/structure/index.ts | 7 ++++--- packages/debugger/src/structure/walker.ts | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/debugger/src/structure/index.ts b/packages/debugger/src/structure/index.ts index 7b5b4767..0a3be2d7 100644 --- a/packages/debugger/src/structure/index.ts +++ b/packages/debugger/src/structure/index.ts @@ -58,13 +58,14 @@ export function createStructure(props: { let shouldUpdateAllRoots = true const onComputationUpdate: walker.ComputationUpdateHandler = ( - rootId, owner, changedStructure, + root_id, owner, changed_structure, ) => { // separate the callback from the computation queueMicrotask(() => { if (!props.enabled()) return - if (changedStructure) { - updateOwner(owner, rootId) + if (changed_structure) { + let owner_to_update = getClosestIncludedOwner(owner, treeWalkerMode) ?? owner + updateOwner(owner_to_update, root_id) } let id = getSdtId(owner, ObjectType.Owner) props.onNodeUpdate(id) diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts index 8c5c972e..3588d293 100644 --- a/packages/debugger/src/structure/walker.ts +++ b/packages/debugger/src/structure/walker.ts @@ -214,9 +214,9 @@ const findComponent = ( export type ComputationUpdateHandler = ( - rootId: NodeID, - owner: Solid.Owner, - changedStructure: boolean, + rootId: NodeID, + owner: Solid.Owner, + changed_structure: boolean, ) => void export @@ -244,7 +244,7 @@ function observeComputation( // copy values in case config gets mutated let {rootId, onUpdate: onComputationUpdate, mode} = config - const handler = () => { + let handler = () => { let is_leaf = !comp.owned || comp.owned.length === 0 let changed_structure = was_leaf !== is_leaf || !is_leaf || mode === TreeWalkerMode.DOM was_leaf = is_leaf From 0caca2cbeed7435dcca483bb7647abeb9314e1a0 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:50:10 +0200 Subject: [PATCH 12/15] Cleanup --- packages/debugger/src/structure/walker.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/debugger/src/structure/walker.test.tsx b/packages/debugger/src/structure/walker.test.tsx index 98155d50..3c898b7c 100644 --- a/packages/debugger/src/structure/walker.test.tsx +++ b/packages/debugger/src/structure/walker.test.tsx @@ -362,10 +362,10 @@ test.describe('TreeWalkerMode.DOM', () => { let to_trigger: (() => void)[] = [] let test_components: Solid.Component[] = [] - + let el_header!: HTMLElement let el_h1!: HTMLHeadingElement - + let el_footer!: HTMLElement let el_main!: HTMLElement let el_h2!: HTMLHeadingElement @@ -392,7 +392,7 @@ test.describe('TreeWalkerMode.DOM', () => { Click me } - + const App = () => { return ( <> From ea3b35729599a05763db7f454cd897f3c20f0b71 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 19:58:06 +0200 Subject: [PATCH 13/15] Add fallback to Show to test #342 --- examples/sandbox/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sandbox/src/App.tsx b/examples/sandbox/src/App.tsx index c8cf5373..c39a9631 100644 --- a/examples/sandbox/src/App.tsx +++ b/examples/sandbox/src/App.tsx @@ -175,7 +175,7 @@ const App: s.Component = () => { component='div' style={{height: '1rem', 'margin-top': '1rem'}} > - + Count is very odd}> {s.createComponent(() => <> , {})} From fa0a2c8a9a8404314c1d1e70921c04f530ab8b98 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 20:09:22 +0200 Subject: [PATCH 14/15] Add changeset --- .changeset/orange-islands-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/orange-islands-drop.md diff --git a/.changeset/orange-islands-drop.md b/.changeset/orange-islands-drop.md new file mode 100644 index 00000000..1af11ae1 --- /dev/null +++ b/.changeset/orange-islands-drop.md @@ -0,0 +1,5 @@ +--- +"@solid-devtools/debugger": patch +--- + +Improve tree walking algorithm for mapping elements. (#348) From ea6c1c21d5c59df54272b1cf1cb2add25f39a3fa Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Tue, 17 Jun 2025 20:11:02 +0200 Subject: [PATCH 15/15] Add another changeset --- .changeset/bright-rabbits-nail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/bright-rabbits-nail.md diff --git a/.changeset/bright-rabbits-nail.md b/.changeset/bright-rabbits-nail.md new file mode 100644 index 00000000..44604dc4 --- /dev/null +++ b/.changeset/bright-rabbits-nail.md @@ -0,0 +1,5 @@ +--- +"@solid-devtools/debugger": minor +--- + +Require `ElementInterface.getChildren` to return `ArrayLike`. (76ab4096a2ad531ae015e35c2475b78f08ac45a7)