From f016bda4f8d3084aa8f86509b088f583c3caaa4c Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Fri, 28 Mar 2025 14:23:48 +0100 Subject: [PATCH 01/19] Refactor the walker to take an element interface object --- packages/debugger/src/locator/index.ts | 19 +- .../debugger/src/main/component-registry.ts | 147 ------ packages/debugger/src/main/id.ts | 2 +- packages/debugger/src/main/index.ts | 7 + packages/debugger/src/main/types.ts | 21 + packages/debugger/src/main/utils.ts | 25 + packages/debugger/src/structure/index.ts | 33 +- .../debugger/src/structure/walker.test.tsx | 135 +++--- packages/debugger/src/structure/walker.ts | 445 +++++++++++++----- 9 files changed, 476 insertions(+), 358 deletions(-) delete mode 100644 packages/debugger/src/main/component-registry.ts diff --git a/packages/debugger/src/locator/index.ts b/packages/debugger/src/locator/index.ts index 265c718a..800c45d1 100644 --- a/packages/debugger/src/locator/index.ts +++ b/packages/debugger/src/locator/index.ts @@ -5,7 +5,7 @@ import {createKeyHold} from '@solid-primitives/keyboard' import {scheduleIdle} from '@solid-primitives/scheduled' import {makeHoverElementListener} from '@solid-devtools/shared/primitives' import {msg, warn} from '@solid-devtools/shared/utils' -import * as registry from '../main/component-registry.ts' +import * as walker from '../structure/walker.ts' import {ObjectType, getObjectById} from '../main/id.ts' import SolidAPI from '../main/setup.ts' import {type NodeID, type OutputEmit} from '../main/types.ts' @@ -27,11 +27,12 @@ export {parseLocationString} from './find-components.ts' export * from './types.ts' -export function createLocator(props: { - locatorEnabled: s.Accessor - setLocatorEnabledSignal(signal: s.Accessor): void - onComponentClick(componentId: NodeID, next: VoidFunction): void - emit: OutputEmit +export function createLocator(props: { + locatorEnabled: s.Accessor, + setLocatorEnabledSignal(signal: s.Accessor): void, + onComponentClick(componentId: NodeID, next: VoidFunction): void, + emit: OutputEmit, + component_registry: walker.ComponentRegistry, }) { const [enabledByPressingSignal, setEnabledByPressingSignal] = s.createSignal((): boolean => false) props.setLocatorEnabledSignal(s.createMemo(() => enabledByPressingSignal()())) @@ -55,7 +56,7 @@ export function createLocator(props: { // target is an element if (target instanceof HTMLElement) { - const comp = registry.findComponent(target) + const comp = walker.findComponent(props.component_registry, target) if (!comp) return [] return [ { @@ -68,7 +69,7 @@ export function createLocator(props: { } // target is a component or an element of a component (in DOM walker mode) - const comp = registry.getComponent(target.id) + const comp = walker.getComponent(props.component_registry, target.id) if (!comp) return [] return comp.elements.map(element => ({ element, @@ -91,7 +92,7 @@ export function createLocator(props: { // notify of component hovered by using the debugger s.createEffect((prev: NodeID | undefined) => { const target = hoverTarget() - const comp = target && registry.findComponent(target) + const comp = target && walker.findComponent(props.component_registry, target) if (prev) { props.emit(msg('HoveredComponent', {nodeId: prev, state: false})) } diff --git a/packages/debugger/src/main/component-registry.ts b/packages/debugger/src/main/component-registry.ts deleted file mode 100644 index f7963c1a..00000000 --- a/packages/debugger/src/main/component-registry.ts +++ /dev/null @@ -1,147 +0,0 @@ -import {type NodeID, type Solid} from './types.ts' -import {onOwnerCleanup} from './utils.ts' - -const $CLEANUP = Symbol('component-registry-cleanup') - -type ComponentData = { - id: NodeID - owner: Solid.Component - name: string | undefined - elements: Set - elementNodes: Set - cleanup: VoidFunction -} - -// Map of component nodes -const ComponentMap = new Map() - -// Map of element nodes to component nodes -const ElementNodeMap = new Map() - -function cleanupComponent(nodeID: NodeID) { - const component = ComponentMap.get(nodeID) - if (!component) return - component.cleanup() - ComponentMap.delete(nodeID) - for (const element of component.elementNodes) ElementNodeMap.delete(element) -} - -export type ComponentRegisterHandler = typeof registerComponent - -// used in walker to register component nodes -export function registerComponent( - data: - | { - owner: Solid.Component - id: NodeID - name: string | undefined - elements: HTMLElement[] | null - } - | { - componentId: NodeID - elementId: NodeID - element: HTMLElement - }, -): void { - // Add new element node to existing component node - if ('elementId' in data) { - const {componentId, elementId, element} = data - const component = ComponentMap.get(componentId) - if (!component) return - - component.elementNodes.add(elementId) - ElementNodeMap.set(elementId, {el: element, component}) - } - // Add new component node - else { - const {owner, id, name, elements: elementsList} = data - if (!elementsList) return cleanupComponent(id) - - const set = new Set(elementsList) - - const existing = ComponentMap.get(id) - if (existing) { - existing.elements = set - return - } - - const cleanup = onOwnerCleanup(owner, () => cleanupComponent(id), false, $CLEANUP) - - ComponentMap.set(id, { - id, - owner, - name, - elements: set, - cleanup, - elementNodes: new Set(), - }) - } -} - -export function clearComponentRegistry() { - for (const component of ComponentMap.values()) component.cleanup() - ComponentMap.clear() - ElementNodeMap.clear() -} - -export function getComponent( - id: NodeID, -): {name: string | undefined; id: NodeID; elements: HTMLElement[]} | null { - // provided if might be of an element node (in DOM mode) or component node - // both need to be checked - - const component = ComponentMap.get(id) - if (component) return {name: component.name, elements: [...component.elements], id} - - const elData = ElementNodeMap.get(id) - return elData - ? {name: elData.component.name, id: elData.component.id, elements: [elData.el]} - : null -} - -/** - * Searches for an HTML element with the given id in the component with the given id. - * - * It is assumed that the element is a child of the component. - * - * Used only in the DOM walker mode. - */ -export function getComponentElement( - elementId: NodeID, -): {name: string | undefined; id: NodeID; element: HTMLElement} | undefined { - const elData = ElementNodeMap.get(elementId) - return elData && {name: elData.component.name, id: elData.component.id, element: elData.el} -} - -// TODO could use some optimization (caching) -export function findComponent(el: HTMLElement): {name: string; id: NodeID} | null { - const including = new Map() - - let currEl: HTMLElement | null = el - while (currEl) { - for (const component of ComponentMap.values()) { - if (component.elements.has(currEl)) including.set(component.owner, component) - } - currEl = including.size === 0 ? currEl.parentElement : null - } - - if (including.size > 1) { - // find the closest component - for (const owner of including.keys()) { - if (!including.has(owner)) continue - let currOwner = owner.owner - while (currOwner) { - const deleted = including.delete(currOwner) - if (deleted) break - currOwner = currOwner.owner - } - } - } - - if (including.size === 0) return null - const value = including.values().next().value - if (value && value.name) { - return {name: value.name, id: value.id} - } - return null -} diff --git a/packages/debugger/src/main/id.ts b/packages/debugger/src/main/id.ts index 31106613..b0d4ca1f 100644 --- a/packages/debugger/src/main/id.ts +++ b/packages/debugger/src/main/id.ts @@ -11,7 +11,7 @@ export const enum ObjectType { type ValueMap = { [ObjectType.Owner]: Solid.Owner - [ObjectType.Element]: Element + [ObjectType.Element]: object [ObjectType.Signal]: Solid.Signal [ObjectType.Store]: Solid.Store [ObjectType.StoreNode]: Solid.StoreNode diff --git a/packages/debugger/src/main/index.ts b/packages/debugger/src/main/index.ts index 26ec9e4b..36f8ec3d 100644 --- a/packages/debugger/src/main/index.ts +++ b/packages/debugger/src/main/index.ts @@ -6,6 +6,7 @@ import {createDependencyGraph} from '../dependency/index.ts' import {createInspector} from '../inspector/index.ts' import {createLocator} from '../locator/index.ts' import {createStructure} from '../structure/index.ts' +import * as walker from '../structure/walker.ts' import {getObjectById, getSdtId, ObjectType} from './id.ts' import {initRoots} from './roots.ts' import setup from './setup.ts' @@ -18,6 +19,7 @@ import { type NodeID, type OutputListener, type OutputMessage, + dom_element_interface, } from './types.ts' function createDebugger() { @@ -137,6 +139,9 @@ function createDebugger() { }) } } + + let eli = dom_element_interface + let component_registry = walker.makeComponentRegistry(eli) // // Structure: @@ -152,6 +157,7 @@ function createDebugger() { }, onNodeUpdate: pushNodeUpdate, enabled: debuggerEnabled, + component_registry: component_registry, }) // @@ -190,6 +196,7 @@ function createDebugger() { } }, emit: emitOutput, + component_registry: component_registry, }) // Opens the source code of the inspected component diff --git a/packages/debugger/src/main/types.ts b/packages/debugger/src/main/types.ts index a10950bf..f365b24d 100644 --- a/packages/debugger/src/main/types.ts +++ b/packages/debugger/src/main/types.ts @@ -130,6 +130,27 @@ export type OutputListener = (e: OutputMessage) => void export type OutputEmit = (e: OutputMessage) => void +/** + * When using a custom solid renderer, you should provide a custom element interface. + * By default the debugger assumes that rendered elements are DOM elements. + */ +export type ElementInterface = { + isElement: (obj: object | T) => obj is T, + getElementName: (el: T) => string, + getElementChildren: (el: T) => Iterable, + getElementAt: (e: MouseEvent) => T | null, +} + +/** + * Implementation of {@link ElementInterface} for {@link Element} + */ +export const dom_element_interface: ElementInterface = { + isElement: obj => obj instanceof Element, + getElementName: el => el.localName, + getElementChildren: el => el.children, + getElementAt: e => e.target as Element | null, +} + // // EXPOSED SOLID API // diff --git a/packages/debugger/src/main/utils.ts b/packages/debugger/src/main/utils.ts index f9a0785c..282f0d3d 100644 --- a/packages/debugger/src/main/utils.ts +++ b/packages/debugger/src/main/utils.ts @@ -2,6 +2,31 @@ import {trimString} from '@solid-devtools/shared/utils' import {type Node, type Solid, NodeType} from './types.ts' import setup from './setup.ts' +export function* unwrap_each(arr: readonly T[] | null | undefined): ArrayIterator { + if (arr != null) { + yield* arr.values() + } +} + +export function append_array(arr: T[], items: readonly T[]): void { + arr.push.apply(arr, items as any) +} +export function unwrap_append(arr: T[], item: T | null | undefined): void { + if (item != null) { + arr.push(item) + } +} +export function unwrap_append_array(arr: T[], items: readonly T[] | null | undefined): void { + if (items != null) { + arr.push.apply(arr, items as any) + } +} + +export function* owner_each_child(o: Solid.Owner): ArrayIterator { + yield* unwrap_each(o.owned) + yield* unwrap_each(o.sdtSubRoots) +} + export const isSolidOwner = (o: Solid.SourceMapValue | Solid.Owner | Solid.Store | Solid.Signal): o is Solid.Owner => 'owned' in o diff --git a/packages/debugger/src/structure/index.ts b/packages/debugger/src/structure/index.ts index c6287c17..7b5b4767 100644 --- a/packages/debugger/src/structure/index.ts +++ b/packages/debugger/src/structure/index.ts @@ -1,10 +1,9 @@ import {throttle} from '@solid-primitives/scheduled' -import * as registry from '../main/component-registry.ts' import {ObjectType, getSdtId} from '../main/id.ts' import * as roots from '../main/roots.ts' import {type Mapped, type NodeID, type Solid, DEFAULT_WALKER_MODE, DevtoolsMainView, NodeType, TreeWalkerMode} from '../main/types.ts' import {isDisposed, markOwnerType} from '../main/utils.ts' -import {type ComputationUpdateHandler, walkSolidTree} from './walker.ts' +import * as walker from './walker.ts' export type StructureUpdates = { /** Partial means that the updates are based on the previous structure state */ @@ -43,10 +42,11 @@ function getClosestIncludedOwner(owner: Solid.Owner, mode: TreeWalkerMode): Soli return root } -export function createStructure(props: { +export function createStructure(props: { onStructureUpdate: (updates: StructureUpdates) => void onNodeUpdate: (nodeId: NodeID) => void enabled: () => boolean + component_registry: walker.ComponentRegistry, }) { let treeWalkerMode: TreeWalkerMode = DEFAULT_WALKER_MODE @@ -57,7 +57,7 @@ export function createStructure(props: { const removedRoots = new Set() let shouldUpdateAllRoots = true - const onComputationUpdate: ComputationUpdateHandler = ( + const onComputationUpdate: walker.ComputationUpdateHandler = ( rootId, owner, changedStructure, ) => { // separate the callback from the computation @@ -72,9 +72,9 @@ export function createStructure(props: { } function forceFlushRootUpdateQueue(): void { - + if (props.enabled()) { - + let partial = !shouldUpdateAllRoots shouldUpdateAllRoots = false @@ -92,16 +92,17 @@ export function createStructure(props: { } for (let owner of owners) { - let rootId = getRootId(owner) - let tree = walkSolidTree(owner, { - rootId, - mode: treeWalkerMode, - onComputationUpdate, - registerComponent: registry.registerComponent, + let root_id = getRootId(owner) + let tree = walker.walkSolidTree(owner, { + rootId: root_id, + mode: treeWalkerMode, + onUpdate: onComputationUpdate, + eli: props.component_registry.eli, + registry: props.component_registry, }) - let map = updated[rootId] - if (map) map[tree.id] = tree - else updated[rootId] = {[tree.id]: tree} + let map = updated[root_id] + if (map != null) map[tree.id] = tree + else updated[root_id] = {[tree.id]: tree} } props.onStructureUpdate({partial, updated, removed: [...removedRoots]}) @@ -143,7 +144,7 @@ export function createStructure(props: { function setTreeWalkerMode(mode: TreeWalkerMode): void { treeWalkerMode = mode updateAllRoots() - registry.clearComponentRegistry() + walker.clearComponentRegistry(props.component_registry) } return { diff --git a/packages/debugger/src/structure/walker.test.tsx b/packages/debugger/src/structure/walker.test.tsx index af162fd1..d838a73e 100644 --- a/packages/debugger/src/structure/walker.test.tsx +++ b/packages/debugger/src/structure/walker.test.tsx @@ -3,9 +3,8 @@ import '../setup.ts' import * as s from 'solid-js' import * as test from 'vitest' import {$setSdtId} from '../main/id.ts' -import {NodeType, TreeWalkerMode, type Mapped, type Solid} from '../main/types.ts' -import {getNodeName} from '../main/utils.ts' -import {type ComputationUpdateHandler, walkSolidTree} from './walker.ts' +import {dom_element_interface, NodeType, TreeWalkerMode, type Mapped, type Solid} from '../main/types.ts' +import * as walker from './walker.ts' import setup from '../main/setup.ts' import {initRoots} from '../main/roots.ts' @@ -13,11 +12,15 @@ test.beforeAll(() => { initRoots() }) +let eli = dom_element_interface + test.describe('TreeWalkerMode.Owners', () => { test.it('default options', () => { { + let registry = walker.makeComponentRegistry(eli) + const [dispose, owner] = s.createRoot(_dispose => { - + const [source] = s.createSignal('foo', {name: 's0'}) s.createSignal('hello', {name: 's1'}) @@ -28,19 +31,18 @@ test.describe('TreeWalkerMode.Owners', () => { s.createSignal(0, {name: 's3'}) }, undefined, {name: 'c1'}) }, undefined, {name: 'e0'}) - + return [_dispose, setup.solid.getOwner()! as Solid.Root] }) - const tree = walkSolidTree(owner, { - onComputationUpdate: () => { + const tree = walker.walkSolidTree(owner, { + onUpdate: () => { /**/ }, rootId: $setSdtId(owner, '#ff'), - registerComponent: () => { - /**/ - }, mode: TreeWalkerMode.Owners, + eli: eli, + registry: registry, }) dispose() @@ -76,6 +78,8 @@ test.describe('TreeWalkerMode.Owners', () => { } { + let registry = walker.makeComponentRegistry(eli) + s.createRoot(dispose => { const [source] = s.createSignal(0, {name: 'source'}) @@ -101,15 +105,14 @@ test.describe('TreeWalkerMode.Owners', () => { ) const rootOwner = setup.solid.getOwner()! as Solid.Root - const tree = walkSolidTree(rootOwner, { + const tree = walker.walkSolidTree(rootOwner, { rootId: $setSdtId(rootOwner, '#0'), - onComputationUpdate: () => { - /**/ - }, - registerComponent: () => { + onUpdate: () => { /**/ }, mode: TreeWalkerMode.Owners, + eli: eli, + registry: registry, }) test.expect(tree).toEqual({ @@ -153,8 +156,11 @@ test.describe('TreeWalkerMode.Owners', () => { }) test.it('listen to computation updates', () => { + + let registry = walker.makeComponentRegistry(eli) + s.createRoot(dispose => { - const capturedComputationUpdates: Parameters[] = [] + const capturedComputationUpdates: Parameters[] = [] let computedOwner!: Solid.Owner const [a, setA] = s.createSignal(0) @@ -164,13 +170,12 @@ test.describe('TreeWalkerMode.Owners', () => { }) const owner = setup.solid.getOwner()! as Solid.Root - walkSolidTree(owner, { - onComputationUpdate: (...args) => capturedComputationUpdates.push(args), + walker.walkSolidTree(owner, { + onUpdate: (...args) => capturedComputationUpdates.push(args), rootId: $setSdtId(owner, '#ff'), mode: TreeWalkerMode.Owners, - registerComponent: () => { - /**/ - }, + eli: eli, + registry: registry, }) test.expect(capturedComputationUpdates.length).toBe(0) @@ -189,52 +194,54 @@ test.describe('TreeWalkerMode.Owners', () => { }) test.it('gathers components', () => { + + let registry = walker.makeComponentRegistry(eli) + s.createRoot(dispose => { const TestComponent = (props: {n: number}) => { const [a] = s.createSignal(0) s.createComputed(a) return
{props.n === 0 ? 'end' : }
} - const Button = () => { - return - } + const Button = () => - s.createRenderEffect(() => { - return ( - <> - -