+ >
+ )
+
+ })
+
+ // wait a second to let the framework mess with the document before attaching the overlay
+ const owner = s.getOwner()!
+ setTimeout(() => {
+ s.runWithOwner(owner, () => (
+
+
{selected().map(useElementOverlay)}
+
+ ))
+ }, 1000)
}
const styles = /*css*/ `
diff --git a/packages/debugger/src/locator/index.test.ts b/packages/debugger/src/locator/index.test.ts
index cc0d2f28..7f5677df 100644
--- a/packages/debugger/src/locator/index.test.ts
+++ b/packages/debugger/src/locator/index.test.ts
@@ -8,7 +8,7 @@ vi.mock('@solid-primitives/platform', () => ({
},
}))
-const fetchFunction = async () => (await import('./find-components.ts')).parseLocationString
+const fetchFunction = async () => (await import('./locator.ts')).parseLocationString
describe('locator attribute pasting', () => {
beforeEach(() => {
diff --git a/packages/debugger/src/locator/index.ts b/packages/debugger/src/locator/index.ts
index 265c718a..0a5909ca 100644
--- a/packages/debugger/src/locator/index.ts
+++ b/packages/debugger/src/locator/index.ts
@@ -3,95 +3,133 @@ import {defer} from '@solid-primitives/utils'
import {makeEventListener} from '@solid-primitives/event-listener'
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'
+import {type ElementInterface, type NodeID, type OutputEmit, type SourceLocation} from '../main/types.ts'
import {createElementsOverlay} from './element-overlay.tsx'
-import {
- type LocatorComponent,
- type SourceCodeData,
- type SourceLocation,
- type TargetIDE,
- type TargetURLFunction,
- getLocationAttr,
- getProjectPath,
- getSourceCodeData,
- openSourceCode,
-} from './find-components.ts'
-import {type HighlightElementPayload, type LocatorOptions} from './types.ts'
-
-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
-}) {
- const [enabledByPressingSignal, setEnabledByPressingSignal] = s.createSignal((): boolean => false)
- props.setLocatorEnabledSignal(s.createMemo(() => enabledByPressingSignal()()))
+import * as locator from './locator.ts'
+import {unwrap_append} from '../main/utils.ts'
- const [hoverTarget, setHoverTarget] = s.createSignal(null)
- const [devtoolsTarget, setDevtoolsTarget] = s.createSignal(null)
+export * from './locator.ts'
- const [highlightedComponents, setHighlightedComponents] = s.createSignal([])
-
- const calcHighlightedComponents = (
- target: HTMLElement | HighlightElementPayload,
- ): LocatorComponent[] => {
- if (!target) return []
+export type LocatorComponent = {
+ id: NodeID
+ name: string | undefined
+ element: TEl
+ location?: SourceLocation | null
+}
- // target is an elementId
- if ('type' in target && target.type === 'element') {
- const element = getObjectById(target.id, ObjectType.Element)
- if (!(element instanceof HTMLElement)) return []
- target = element
+function makeHoverElementListener(
+ eli: ElementInterface,
+ onHover: (el: TEl | null) => void,
+): void {
+ let last: TEl | null = null
+ makeEventListener(window, 'mouseover', e => {
+ let el = eli.getElementAt(e)
+ if (el !== last) {
+ onHover(last = el)
}
-
- // target is an element
- if (target instanceof HTMLElement) {
- const comp = registry.findComponent(target)
- if (!comp) return []
- return [
- {
- location: getLocationAttr(target),
- element: target,
- id: comp.id,
- name: comp.name,
- },
- ]
+ })
+ makeEventListener(document, 'mouseleave', () => {
+ if (null !== last) {
+ onHover(last = null)
}
+ })
+}
- // target is a component or an element of a component (in DOM walker mode)
- const comp = registry.getComponent(target.id)
- if (!comp) return []
- return comp.elements.map(element => ({
- element,
+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()()))
+
+ const [hoverTarget, setHoverTarget] = s.createSignal(null)
+ const [devtoolsTarget, setDevtoolsTarget] = s.createSignal(null)
+
+ const [highlightedComponents, setHighlightedComponents] = s.createSignal[]>([])
+
+ function getLocatorComponentFromElement(
+ el: TEl,
+ ): LocatorComponent | null {
+ let comp = walker.findComponent(props.component_registry, el)
+ return comp && {
+ location: props.component_registry.eli.getLocation(el),
+ element: el,
id: comp.id,
name: comp.name,
- }))
+ }
}
- s.createEffect(
- defer(
- () => hoverTarget() ?? devtoolsTarget(),
- scheduleIdle(target =>
- setHighlightedComponents(() => calcHighlightedComponents(target)),
- ),
- ),
- )
+ const target = s.createMemo(() => {
+ let hover = hoverTarget()
+ return hover != null
+ ? {
+ type: 'hover' as const,
+ element: hover,
+ }
+ : devtoolsTarget()
+ }, undefined, {
+ equals: (a, b) => {
+ if (a === b) return true
+ if (a == null && b == null) return true
+ if (a == null || b == null) return false
+ if (a.type !== b.type) return false
+ switch (a.type) {
+ case 'hover': return a.element === (b as any).element
+ case 'node': return a.id === (b as any).id
+ case 'element': return a.id === (b as any).id
+ }
+ },
+ })
+
+ s.createEffect(defer(target, scheduleIdle(target => {
+
+ let locator_components: LocatorComponent[] = []
+
+ if (target != null) {
+ switch (target.type) {
+ case 'hover': {
+ unwrap_append(locator_components, getLocatorComponentFromElement(target.element))
+ break
+ }
+ case 'element': {
+ let element = getObjectById(target.id, ObjectType.Element) as TEl | null
+ if (element != null) {
+ unwrap_append(locator_components, getLocatorComponentFromElement(element))
+ }
+ break
+ }
+ case 'node': {
+ // target is a component or an element of a component (in DOM walker mode)
+ let comp = walker.getComponent(props.component_registry, target.id)
+ if (comp != null) {
+ for (let el of comp.elements) {
+ locator_components.push({
+ element: el,
+ id: comp.id,
+ name: comp.name,
+ })
+ }
+ }
+ }
+ }
+ }
+
+ setHighlightedComponents(locator_components)
+ })))
- createElementsOverlay(highlightedComponents)
+ createElementsOverlay(highlightedComponents, props.component_registry.eli)
// 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}))
}
@@ -102,39 +140,42 @@ export function createLocator(props: {
}
})
- let targetIDE: TargetIDE | TargetURLFunction | undefined
+ let target_ide: locator.TargetIDE | locator.TargetURLFunction | undefined
s.createEffect(() => {
if (!props.locatorEnabled()) return
// set hovered element as target
- makeHoverElementListener(el => setHoverTarget(el))
+ makeHoverElementListener(props.component_registry.eli, el => setHoverTarget(() => el))
s.onCleanup(() => setHoverTarget(null))
// go to selected component source code on click
- makeEventListener(
- window,
- 'click',
- e => {
- const {target} = e
- if (!(target instanceof HTMLElement)) return
- const highlighted = highlightedComponents()
- const comp =
- highlighted.find(({element}) => target.contains(element)) ?? highlighted[0]
- if (!comp) return
- const sourceCodeData =
- comp.location && getSourceCodeData(comp.location, comp.element)
-
- // intercept on-page components clicks and send them to the devtools overlay
- props.onComponentClick(comp.id, () => {
- if (!targetIDE || !sourceCodeData) return
- e.preventDefault()
- e.stopPropagation()
- openSourceCode(targetIDE, sourceCodeData)
- })
- },
- true,
- )
+ makeEventListener(window, 'click', e => {
+
+ let el = props.component_registry.eli.getElementAt(e)
+ if (el == null) {
+ DEV: {warn("Locator: can't find element at click target (target=%o)", e.target)}
+ return
+ }
+
+ let comp = getLocatorComponentFromElement(el)
+ if (comp == null) {
+ DEV: {warn("Locator: can't find component at click target (target=%o, el=%o)", e.target, el)}
+ return
+ }
+
+ let source_code_data = comp.location
+ ? locator.getSourceCodeData(comp.location)
+ : null
+
+ // intercept on-page components clicks and send them to the devtools overlay
+ props.onComponentClick(comp.id, () => {
+ if (target_ide == null || source_code_data == null) return
+ e.preventDefault()
+ e.stopPropagation()
+ locator.openSourceCode(target_ide, source_code_data)
+ })
+ }, true)
})
let locatorUsed = false
@@ -146,11 +187,11 @@ export function createLocator(props: {
*
* @param options {@link LocatorOptions} for the locator.
*/
- function useLocator(options: LocatorOptions): void {
+ function useLocator(options: locator.LocatorOptions): void {
s.runWithOwner(owner, () => {
if (locatorUsed) return warn('useLocator can be called only once.')
locatorUsed = true
- if (options.targetIDE) targetIDE = options.targetIDE
+ if (options.targetIDE) target_ide = options.targetIDE
if (options.key !== false) {
const isHoldingKey = createKeyHold(options.key ?? 'Alt', {preventDefault: true})
setEnabledByPressingSignal(() => isHoldingKey)
@@ -166,17 +207,16 @@ export function createLocator(props: {
return {
useLocator,
- setDevtoolsHighlightTarget(target: HighlightElementPayload) {
+ setDevtoolsHighlightTarget(target: locator.HighlightElementPayload) {
setDevtoolsTarget(target)
},
- openElementSourceCode(location: SourceLocation, element: SourceCodeData['element']) {
- if (!targetIDE) return warn('Please set `targetIDE` it in useLocator options.')
- const projectPath = getProjectPath()
+ openElementSourceCode(location: SourceLocation) {
+ if (!target_ide) return warn('Please set `targetIDE` it in useLocator options.')
+ const projectPath = locator.getProjectPath()
if (!projectPath) return warn('projectPath is not set.')
- openSourceCode(targetIDE, {
+ locator.openSourceCode(target_ide, {
...location,
projectPath,
- element,
})
},
}
diff --git a/packages/debugger/src/locator/find-components.ts b/packages/debugger/src/locator/locator.ts
similarity index 70%
rename from packages/debugger/src/locator/find-components.ts
rename to packages/debugger/src/locator/locator.ts
index 8284522d..0886a33f 100644
--- a/packages/debugger/src/locator/find-components.ts
+++ b/packages/debugger/src/locator/locator.ts
@@ -1,26 +1,33 @@
+import type {KbdKey} from '@solid-primitives/keyboard'
import {isWindows} from '@solid-primitives/platform'
-import {LOCATION_ATTRIBUTE_NAME, type NodeID, WINDOW_PROJECTPATH_PROPERTY} from '../types.ts'
+import type {ToDyscriminatedUnion} from '@solid-devtools/shared/utils'
+import {type NodeID, type SourceLocation} from '../main/types.ts'
-export type LocationAttr = `${string}:${number}:${number}`
-
-export type LocatorComponent = {
- id: NodeID
- name: string | undefined
- element: HTMLElement
- location?: LocationAttr | undefined
+export type LocatorOptions = {
+ /** Choose in which IDE the component source code should be revealed. */
+ targetIDE?: false | TargetIDE | TargetURLFunction
+ /**
+ * Holding which key should enable the locator overlay?
+ * @default 'Alt'
+ */
+ key?: false | KbdKey
}
-export type TargetIDE = 'vscode' | 'webstorm' | 'atom' | 'vscode-insiders'
+export type HighlightElementPayload = ToDyscriminatedUnion<{
+ node: {id: NodeID}
+ element: {id: NodeID}
+}> | null
-export type SourceLocation = {
- file: string
- line: number
- column: number
-}
+// used by the transform
+export const WINDOW_PROJECTPATH_PROPERTY = '$sdt_projectPath'
+export const LOCATION_ATTRIBUTE_NAME = 'data-source-loc'
+
+export type LocationAttr = `${string}:${number}:${number}`
+
+export type TargetIDE = 'vscode' | 'webstorm' | 'atom' | 'vscode-insiders'
export type SourceCodeData = SourceLocation & {
projectPath: string
- element: HTMLElement | string | undefined
}
export type TargetURLFunction = (data: SourceCodeData) => string | void
@@ -31,9 +38,9 @@ const LOC_ATTR_REGEX_UNIX =
export const LOC_ATTR_REGEX = isWindows ? LOC_ATTR_REGEX_WIN : LOC_ATTR_REGEX_UNIX
-export function getLocationAttr(element: Element): LocationAttr | undefined {
- const attr = element.getAttribute(LOCATION_ATTRIBUTE_NAME)
- if (!attr || !LOC_ATTR_REGEX.test(attr)) return
+export function getLocationAttr(element: Element): LocationAttr | null {
+ let attr = element.getAttribute(LOCATION_ATTRIBUTE_NAME)
+ if (!attr || !LOC_ATTR_REGEX.test(attr)) return null
return attr as LocationAttr
}
@@ -56,14 +63,13 @@ function getTargetURL(target: TargetIDE | TargetURLFunction, data: SourceCodeDat
export const getProjectPath = (): string | undefined => (window as any)[WINDOW_PROJECTPATH_PROPERTY]
export function getSourceCodeData(
- location: LocationAttr,
- element: SourceCodeData['element'],
+ location: SourceLocation,
): SourceCodeData | undefined {
- const projectPath: string | undefined = getProjectPath()
- if (!projectPath) return
- const parsed = parseLocationString(location)
- if (!parsed) return
- return {...parsed, projectPath, element}
+
+ let projectPath: string | undefined = getProjectPath()
+ if (projectPath == null) return
+
+ return {...location, projectPath}
}
/**
diff --git a/packages/debugger/src/locator/types.ts b/packages/debugger/src/locator/types.ts
deleted file mode 100644
index 17c3cbc4..00000000
--- a/packages/debugger/src/locator/types.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import type {ToDyscriminatedUnion} from '@solid-devtools/shared/utils'
-import type {KbdKey} from '@solid-primitives/keyboard'
-import type {NodeID} from '../main/types.ts'
-import type {TargetIDE, TargetURLFunction} from './find-components.ts'
-
-export type {
- LocationAttr,
- LocatorComponent,
- SourceLocation,
- TargetIDE,
- TargetURLFunction,
-} from './find-components.ts'
-
-export type LocatorOptions = {
- /** Choose in which IDE the component source code should be revealed. */
- targetIDE?: false | TargetIDE | TargetURLFunction
- /**
- * Holding which key should enable the locator overlay?
- * @default 'Alt'
- */
- key?: false | KbdKey
-}
-
-export type HighlightElementPayload = ToDyscriminatedUnion<{
- node: {id: NodeID}
- element: {id: NodeID}
-}> | null
-
-// used by the transform
-export const WINDOW_PROJECTPATH_PROPERTY = '$sdt_projectPath'
-export const LOCATION_ATTRIBUTE_NAME = 'data-source-loc'
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..40281db6 100644
--- a/packages/debugger/src/main/index.ts
+++ b/packages/debugger/src/main/index.ts
@@ -1,11 +1,12 @@
import * as s from 'solid-js'
import {createStaticStore} from '@solid-primitives/static-store'
import {defer} from '@solid-primitives/utils'
-import {log_message, msg, mutate_remove, type Timeout} from '@solid-devtools/shared/utils'
+import {assert, log_message, msg, mutate_remove, type Timeout} from '@solid-devtools/shared/utils'
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'
@@ -21,6 +22,7 @@ import {
} from './types.ts'
function createDebugger() {
+ assert(globalThis.SolidDevtools$$, 'solid-devtools is not setup')
initRoots()
@@ -137,6 +139,8 @@ function createDebugger() {
})
}
}
+
+ let component_registry = walker.makeComponentRegistry(setup.eli)
//
// Structure:
@@ -152,6 +156,7 @@ function createDebugger() {
},
onNodeUpdate: pushNodeUpdate,
enabled: debuggerEnabled,
+ component_registry: component_registry,
})
//
@@ -190,12 +195,13 @@ function createDebugger() {
}
},
emit: emitOutput,
+ component_registry: component_registry,
})
// Opens the source code of the inspected component
function openInspectedNodeLocation() {
const details = inspector.getLastDetails()
- details?.location && locator.openElementSourceCode(details.location, details.name)
+ details?.location && locator.openElementSourceCode(details.location)
}
// send the state of the client locator mode
@@ -259,12 +265,13 @@ function createDebugger() {
}
}
-let _debugger_instance: ReturnType | undefined
+export type Debugger = ReturnType
+let _debugger_instance: Debugger | undefined
/**
* Used for connecting debugger to devtools
*/
-export function useDebugger() {
+export function useDebugger(): Debugger {
_debugger_instance ??= createDebugger()
return _debugger_instance
}
diff --git a/packages/debugger/src/main/types.ts b/packages/debugger/src/main/types.ts
index a10950bf..e1dab1d9 100644
--- a/packages/debugger/src/main/types.ts
+++ b/packages/debugger/src/main/types.ts
@@ -1,6 +1,6 @@
import type {Union} from '@solid-devtools/shared/utils'
import type {EncodedValue, InspectorUpdate, PropGetterState, ToggleInspectedValueData} from '../inspector/types.ts'
-import type {HighlightElementPayload, SourceLocation} from '../locator/types.ts'
+import * as locator from '../locator/locator.ts'
import type {StructureUpdates, DGraphUpdate} from '../types.ts'
/**
@@ -115,7 +115,7 @@ export type InputChannels = {
InspectNode: {ownerId: NodeID | null; signalId: NodeID | null} | null
InspectValue: ToggleInspectedValueData
ConsoleInspectValue: ValueItemID
- HighlightElementChange: HighlightElementPayload
+ HighlightElementChange: locator.HighlightElementPayload
OpenLocation: void
TreeViewModeChange: TreeWalkerMode
ViewChange: DevtoolsMainView
@@ -130,6 +130,49 @@ export type OutputListener = (e: OutputMessage) => void
export type OutputEmit = (e: OutputMessage) => void
+export type SourceLocation = {
+ file: string
+ line: number
+ column: number
+}
+
+export type Rect = {
+ x: number
+ y: number
+ width: number
+ height: number
+}
+
+/**
+ * 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,
+ getElementAt: (e: MouseEvent) => T | null,
+ getName: (el: T) => string | null,
+ getChildren: (el: T) => Iterable,
+ getParent: (el: T) => T | null,
+ getLocation: (el: T) => SourceLocation | null,
+ getRect: (el: T) => Rect | null,
+}
+
+/**
+ * Implementation of {@link ElementInterface} for {@link Element}
+ */
+export const dom_element_interface: ElementInterface = {
+ isElement: obj => obj instanceof Element,
+ getElementAt: e => e.target as Element | null,
+ getName: el => el.localName,
+ getChildren: el => el.children,
+ getParent: el => el.parentElement,
+ getRect: el => el.getBoundingClientRect(),
+ getLocation: el => {
+ let attr = locator.getLocationAttr(el)
+ return attr && locator.parseLocationString(attr) || 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/setup.ts b/packages/debugger/src/setup.ts
index 58b11518..590a9044 100644
--- a/packages/debugger/src/setup.ts
+++ b/packages/debugger/src/setup.ts
@@ -8,31 +8,32 @@ It also starts listening to Solid DEV events and stores them to be sent to the d
import * as s from 'solid-js'
import * as store from 'solid-js/store'
-import {error} from '@solid-devtools/shared/utils'
-import type {LocatorOptions} from './locator/types.ts'
-import type {Solid} from './main/types.ts'
+import {assert, error} from '@solid-devtools/shared/utils'
+import * as debug from './types.ts'
-
-let PassedLocatorOptions: LocatorOptions | null = null
/** @deprecated use `setLocatorOptions` */
-export function useLocator(options: LocatorOptions) {
- PassedLocatorOptions = options
+export function useLocator(options: debug.LocatorOptions) {
+ setLocatorOptions(options)
}
-export function setLocatorOptions(options: LocatorOptions) {
- PassedLocatorOptions = options
+export function setLocatorOptions(options: debug.LocatorOptions) {
+ assert(globalThis.SolidDevtools$$, 'solid-devtools is not setup')
+ globalThis.SolidDevtools$$.locator_options = options
}
-let ClientVersion: string | null = null
-let SolidVersion: string | null = null
-let ExpectedSolidVersion: string | null = null
+export function setElementInterface(eli: debug.ElementInterface) {
+ assert(globalThis.SolidDevtools$$, 'solid-devtools is not setup')
+ globalThis.SolidDevtools$$.eli = eli
+}
export function setClientVersion(version: string) {
- ClientVersion = version
+ assert(globalThis.SolidDevtools$$, 'solid-devtools is not setup')
+ globalThis.SolidDevtools$$.versions.client = version
}
export function setSolidVersion(version: string, expected: string) {
- SolidVersion = version
- ExpectedSolidVersion = expected
+ assert(globalThis.SolidDevtools$$, 'solid-devtools is not setup')
+ globalThis.SolidDevtools$$.versions.solid = version
+ globalThis.SolidDevtools$$.versions.expected_solid = expected
}
export type SetupApi = {
@@ -50,17 +51,22 @@ export type SetupApi = {
$RAW: typeof store.$RAW
}
// custom
- get_created_owners(): Solid.Owner[]
- get_locator_options(): LocatorOptions | null
+ eli: debug.ElementInterface
+ locator_options: debug.LocatorOptions | null
+ get_created_owners: () => debug.Solid.Owner[]
+ get_locator_options: () => debug.LocatorOptions | null
versions: {
- get_client(): string | null
- get_solid(): string | null
- get_expected_solid(): string | null
+ client: string | null
+ solid: string | null
+ expected_solid: string | null
+ get_client: () => string | null
+ get_solid: () => string | null
+ get_expected_solid: () => string | null
}
unowned: {
- signals: WeakRef[]
- onSignalAdded: ((ref: WeakRef, idx: number) => void) | null
- onSignalRemoved: ((ref: WeakRef, idx: number) => void) | null
+ signals: WeakRef[]
+ onSignalAdded: ((ref: WeakRef, idx: number) => void) | null
+ onSignalRemoved: ((ref: WeakRef, idx: number) => void) | null
}
}
@@ -77,7 +83,7 @@ if (!s.DEV || !store.DEV) {
error('SolidJS in not in development mode!')
} else {
- let created_owners: Solid.Owner[] | null = []
+ let created_owners: debug.Solid.Owner[] | null = []
let setup: SetupApi = {
solid: {
@@ -100,13 +106,16 @@ if (!s.DEV || !store.DEV) {
created_owners = null
return events
},
- get_locator_options() {
- return PassedLocatorOptions
- },
+ eli: debug.dom_element_interface,
+ locator_options: null,
+ get_locator_options() {return this.locator_options},
versions: {
- get_client() {return ClientVersion},
- get_solid() {return SolidVersion},
- get_expected_solid() {return ExpectedSolidVersion},
+ client: null,
+ solid: null,
+ expected_solid: null,
+ get_client() {return this.client},
+ get_solid() {return this.solid},
+ get_expected_solid() {return this.expected_solid},
},
unowned: {
signals: [],
@@ -120,7 +129,7 @@ if (!s.DEV || !store.DEV) {
created_owners?.push(owner)
}
- let signals_registry = new FinalizationRegistry>(ref => {
+ let signals_registry = new FinalizationRegistry>(ref => {
let idx = setup.unowned.signals.indexOf(ref)
setup.unowned.signals.splice(idx, 1)
setup.unowned.onSignalRemoved?.(ref, idx)
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 (
- <>
-
-
- >
- )
- })
+ s.createRenderEffect(() => <>
+
+
+ >)
const owner = setup.solid.getOwner()! as Solid.Root
- const components: string[] = []
-
- walkSolidTree(owner, {
- onComputationUpdate: () => {
+ walker.walkSolidTree(owner, {
+ onUpdate: () => {
/**/
},
rootId: $setSdtId(owner, '#ff'),
mode: TreeWalkerMode.Owners,
- registerComponent: c => {
- if (!('owner' in c)) return
- const name = getNodeName(c.owner)
- name && components.push(name)
- },
+ eli: eli,
+ registry: registry,
})
- test.expect(components.length).toBe(7)
+ let n_text_comps = 0
+ let n_buttons = 0
+
+ for (let comp of registry.components.values()) {
+ // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
+ switch (comp.name) {
+ case 'TestComponent':
+ n_text_comps++
+ break
+ case 'Button':
+ n_buttons++
+ break
+ default:
+ test.assert(false, `Unexpected component name: ${comp.name}`)
+ }
+ }
- let testCompsLength = 0
- let btn!: string
- components.forEach(c => {
- if (c === 'TestComponent') testCompsLength++
- else if (c === 'Button') btn = c
- })
- test.expect(testCompsLength).toBe(6)
- test.expect(btn).toBeTruthy()
+ test.expect(registry.components.size).toBe(7)
+ test.expect(n_text_comps).toBe(6)
+ test.expect(n_buttons).toBe(1)
dispose()
})
@@ -243,6 +250,9 @@ test.describe('TreeWalkerMode.Owners', () => {
test.describe('TreeWalkerMode.Components', () => {
test.it('map component tree', () => {
+
+ let registry = walker.makeComponentRegistry(eli)
+
const toTrigger: VoidFunction[] = []
const testComponents: Solid.Component[] = []
@@ -276,15 +286,14 @@ test.describe('TreeWalkerMode.Components', () => {
const owner = setup.solid.getOwner()! as Solid.Root
- const computationUpdates: Parameters[] = []
+ const computationUpdates: Parameters[] = []
- const tree = walkSolidTree(owner, {
- onComputationUpdate: (...a) => computationUpdates.push(a),
+ const tree = walker.walkSolidTree(owner, {
+ onUpdate: (...a) => computationUpdates.push(a),
rootId: $setSdtId(owner, '#ff'),
mode: TreeWalkerMode.Components,
- registerComponent: () => {
- /**/
- },
+ eli: eli,
+ registry: registry,
})
test.expect(tree).toMatchObject({
@@ -348,6 +357,9 @@ test.describe('TreeWalkerMode.Components', () => {
test.describe('TreeWalkerMode.DOM', () => {
test.it('map dom tree', () => {
+
+ let registry = walker.makeComponentRegistry(eli)
+
const toTrigger: VoidFunction[] = []
const testComponents: Solid.Component[] = []
@@ -384,15 +396,14 @@ test.describe('TreeWalkerMode.DOM', () => {
const owner = setup.solid.getOwner()! as Solid.Root
- const computationUpdates: Parameters[] = []
+ const computationUpdates: Parameters[] = []
- const tree = walkSolidTree(owner, {
- onComputationUpdate: (...a) => computationUpdates.push(a),
+ const tree = walker.walkSolidTree(owner, {
+ onUpdate: (...a) => computationUpdates.push(a),
rootId: $setSdtId(owner, '#ff'),
mode: TreeWalkerMode.DOM,
- registerComponent: () => {
- /**/
- },
+ eli: eli,
+ registry: registry,
})
test.expect(tree).toMatchObject({
diff --git a/packages/debugger/src/structure/walker.ts b/packages/debugger/src/structure/walker.ts
index 40bc8b7c..a241adfb 100644
--- a/packages/debugger/src/structure/walker.ts
+++ b/packages/debugger/src/structure/walker.ts
@@ -1,80 +1,276 @@
import {untrackedCallback} from '@solid-devtools/shared/primitives'
-import {asArray} from '@solid-devtools/shared/utils'
-import type {ComponentRegisterHandler} from '../main/component-registry.ts'
import {ObjectType, getSdtId} from '../main/id.ts'
import {observeComputationUpdate} from '../main/observe.ts'
-import {type Mapped, type NodeID, type Solid, NodeType, TreeWalkerMode} from '../main/types.ts'
+import {
+ type ElementInterface,
+ type Mapped,
+ type NodeID,
+ type Solid,
+ NodeType,
+ TreeWalkerMode,
+ UNKNOWN,
+} from '../main/types.ts'
import {
getComponentRefreshNode,
getNodeName,
isSolidComputation,
markOwnerType,
- resolveElements,
+ onOwnerCleanup,
+ owner_each_child,
+ unwrap_append,
} from '../main/utils.ts'
+export
+type ComponentData = {
+ id: NodeID,
+ owner: Solid.Component,
+ name: string | undefined,
+ elements: Set,
+ element_nodes: Set,
+ cleanup: () => void,
+}
+
+export
+type ComponentRegistry = {
+ eli: ElementInterface,
+ /** Map of component nodes */
+ components: Map>,
+ /** Map of element nodes to component nodes */
+ element_nodes: Map}>,
+}
+
+export
+const makeComponentRegistry = (
+ eli: ElementInterface,
+): ComponentRegistry => {
+ return {
+ eli: eli,
+ components: new Map,
+ element_nodes: new Map,
+ }
+}
+
+export
+const clearComponentRegistry = (
+ r: ComponentRegistry,
+) => {
+ for (let component of r.components.values()) component.cleanup()
+ r.components.clear()
+ r.element_nodes.clear()
+}
+
+export
+const cleanupComponent = (
+ r: ComponentRegistry,
+ nodeID: NodeID,
+) => {
+ let component = r.components.get(nodeID)
+ if (component != null) {
+ component.cleanup()
+ r.components.delete(nodeID)
+ for (let element of component.element_nodes) {
+ r.element_nodes.delete(element)
+ }
+ }
+}
+
+const $CLEANUP = Symbol('component-registry-cleanup')
+
+export
+const registerComponent = (
+ r: ComponentRegistry,
+ owner: Solid.Component,
+ id: NodeID,
+ name: string | undefined,
+ elements: TEl[] | null,
+): void => {
+ // Handle cleanup if elements is null
+ if (elements == null) {
+ cleanupComponent(r, id)
+ return
+ }
+
+ let set = new Set(elements)
+
+ let existing = r.components.get(id)
+ if (existing != null) {
+ existing.elements = set
+ return
+ }
+
+ let cleanup = onOwnerCleanup(owner, () => cleanupComponent(r, id), false, $CLEANUP)
+
+ r.components.set(id, {
+ id: id,
+ owner: owner,
+ name: name,
+ elements: set,
+ element_nodes: new Set(),
+ cleanup: cleanup,
+ })
+}
+
+export
+const registerElement = (
+ r: ComponentRegistry,
+ componentId: NodeID,
+ elementId: NodeID,
+ element: TEl,
+): void => {
+ let component = r.components.get(componentId)
+ if (!component) return
+
+ component.element_nodes.add(elementId)
+ r.element_nodes.set(elementId, {el: element as any as TEl, component})
+}
+
+export
+const getComponent = (
+ 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
+ }
+
+ let elData = r.element_nodes.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
+const getComponentElement = (
+ r: ComponentRegistry,
+ elementId: 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}
+ }
+}
+
+export
+const findComponent = (
+ r: ComponentRegistry,
+ el: TEl,
+): ComponentData | null => {
+
+ let including = new Map>()
+
+ for (let curr: TEl | null = el;
+ curr != null && including.size === 0;
+ ) {
+ for (let comp of r.components.values()) {
+ for (let comp_el of comp.elements) {
+ if (comp_el === curr) {
+ including.set(comp.owner, comp)
+ }
+ }
+ }
+ curr = r.eli.getParent(curr) // go up
+ }
+
+ if (including.size > 1) {
+ // find the closest component
+ for (let owner of including.keys()) {
+ if (including.has(owner)) {
+ for (let curr = owner.owner; curr != null;) {
+ if (including.delete(curr)) break
+ curr = curr.owner // go up
+ }
+ }
+ }
+ }
+
+ return including.values().next().value ?? null
+}
+
+
export type ComputationUpdateHandler = (
- rootId: NodeID,
- owner: Solid.Owner,
+ rootId: NodeID,
+ owner: Solid.Owner,
changedStructure: boolean,
) => void
-// Globals set before each walker cycle
-let Mode: TreeWalkerMode
-let RootId: NodeID
-let OnComputationUpdate: ComputationUpdateHandler
-let RegisterComponent: ComponentRegisterHandler
+export type TreeWalkerConfig = {
+ mode: TreeWalkerMode
+ rootId: NodeID
+ onUpdate: ComputationUpdateHandler
+ registry: ComponentRegistry
+ eli: ElementInterface
+}
-const ElementsMap = new Map()
+const ElementsMap = new Map()
const $WALKER = Symbol('tree-walker')
-function observeComputation(comp: Solid.Computation, owner_to_update: Solid.Owner): void {
+function observeComputation(
+ comp: Solid.Computation,
+ owner_to_update: Solid.Owner,
+ config: TreeWalkerConfig,
+): void {
// leaf nodes (ones that don't have children) don't have to cause a structure update
// Unless the walker is in DOM mode, then we need to observe all computations
// This is because DOM can change without the owner structure changing
let was_leaf = !comp.owned || comp.owned.length === 0
- // copy globals
- let root_id = RootId
- let on_computation_update = OnComputationUpdate
- let mode = Mode
+ // copy values in case config gets mutated
+ let {rootId, onUpdate: onComputationUpdate, mode} = config
const 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
- on_computation_update(root_id, owner_to_update, changed_structure)
+ onComputationUpdate(rootId, owner_to_update, changed_structure)
}
observeComputationUpdate(comp, handler, $WALKER)
}
-function mapChildren(owner: Solid.Owner, mappedOwner: Mapped.Owner | null): Mapped.Owner[] {
- const children: Mapped.Owner[] = []
-
- const rawChildren: Solid.Owner[] = owner.owned ? owner.owned.slice() : []
- if (owner.sdtSubRoots) rawChildren.push.apply(rawChildren, owner.sdtSubRoots)
+function resolveElements(
+ value: unknown, eli: ElementInterface, list: TEl[] = []
+): TEl[] {
+ pushResolvedElements(list, value, eli)
+ return list
+}
- if (Mode === TreeWalkerMode.Owners) {
- for (const child of rawChildren) {
- const mappedChild = mapOwner(child, mappedOwner)
- if (mappedChild) children.push(mappedChild)
- }
- } else {
- for (const child of rawChildren) {
- const type = markOwnerType(child)
- if (type === NodeType.Component) {
- const mappedChild = mapOwner(child, mappedOwner)
- if (mappedChild) children.push(mappedChild)
- } else {
- if (isSolidComputation(child)) observeComputation(child, owner)
- children.push.apply(children, mapChildren(child, mappedOwner))
+function pushResolvedElements(list: TEl[], value: unknown, eli: ElementInterface): void {
+ if (value != null) {
+ // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
+ switch (typeof value) {
+ case 'function':
+ // do not call a function, unless it's a signal (to prevent creating new nodes)
+ if (value.length === 0 && value.name === 'bound readSignal') {
+ pushResolvedElements(list, value(), eli)
}
+ break
+ case 'object':
+ if (Array.isArray(value)) {
+ for (let item of value) {
+ pushResolvedElements(list, item, eli)
+ }
+ } else if (eli.isElement(value)) {
+ list.push(value)
+ }
+
+ break
}
}
-
- return children
}
let MappedOwnerNode: Mapped.Owner
@@ -82,109 +278,132 @@ let AddedToParentElements = false
/**
* @param els elements to map
- * @param parentChildren parent owner children.
+ * @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.
*/
-function mapElements(
- els: Iterable,
- parentChildren: Mapped.Owner[] | undefined,
+function mapElements(
+ els: Iterable,
+ parent_children: Mapped.Owner[] | undefined,
+ eli: ElementInterface,
+ out: Mapped.Owner[] = [],
): Mapped.Owner[] {
- const r = [] as Mapped.Owner[]
- els: for (const el of els) {
- if (!(el instanceof HTMLElement)) continue
+ els: for (let el of els) {
+ if (!eli.isElement(el)) continue
+
+ if (parent_children) {
- if (parentChildren) {
// find el in parent els and remove it
- const toCheck = [parentChildren]
+ let to_check = [parent_children]
let index = 0
- let elNodes = toCheck[index++]
- while (elNodes) {
- for (let i = 0; i < elNodes.length; i++) {
- const elNode = elNodes[i]!
- const elNodeData = ElementsMap.get(elNode)
- if (elNodeData && elNodeData.el === el) {
+ 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
- elNodes.splice(i, 1)
+ el_nodes.splice(i, 1)
} else {
// otherwise, we can just replace it with the component
- elNodes[i] = MappedOwnerNode
+ el_nodes[i] = MappedOwnerNode
AddedToParentElements = true
}
- r.push(elNode)
- elNodeData.component = MappedOwnerNode
+ out.push(el_node)
+ el_node_data.component = MappedOwnerNode
continue els
}
- if (elNode.children.length) toCheck.push(elNode.children)
+ if (el_node.children.length) to_check.push(el_node.children)
}
- elNodes = toCheck[index++]
+ el_nodes = to_check[index++]
}
}
- const mappedEl: Mapped.Owner = {
- id: getSdtId(el, ObjectType.Element),
- type: NodeType.Element,
- name: el.localName,
+ let el_json: Mapped.Owner = {
+ id: getSdtId(el, ObjectType.Element),
+ type: NodeType.Element,
+ name: eli.getName(el) ?? UNKNOWN,
children: [],
}
- r.push(mappedEl)
- ElementsMap.set(mappedEl, {el, component: MappedOwnerNode})
+ out.push(el_json)
+ ElementsMap.set(el_json, {el, component: MappedOwnerNode})
- if (el.children.length) mappedEl.children = mapElements(el.children, parentChildren)
+ mapElements(eli.getChildren(el), parent_children, eli, el_json.children)
}
- return r
+ return out
}
-function mapOwner(
+function mapChildren(
+ owner: Solid.Owner,
+ owner_map: Mapped.Owner | 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))
+ } else {
+ if (isSolidComputation(child)) {
+ observeComputation(child, owner, config)
+ }
+ mapChildren(child, owner_map, config, children)
+ }
+ }
+
+ return children
+}
+
+function mapOwner(
owner: Solid.Owner,
parent: Mapped.Owner | null,
+ config: TreeWalkerConfig,
): Mapped.Owner | undefined {
- const id = getSdtId(owner, ObjectType.Owner)
- const type = markOwnerType(owner)
- const name = getNodeName(owner)
+ let id = getSdtId(owner, ObjectType.Owner)
+ let type = markOwnerType(owner)
+ let name = getNodeName(owner)
- const mapped = {id, type, name} as Mapped.Owner
+ let mapped = {id, type, name, children: []} as Mapped.Owner
- let resolvedElements: ReturnType | undefined
+ let resolved_els: TEl[] | undefined
// Component
if (type === NodeType.Component) {
let first_owned: Solid.Owner | undefined
- /*
+ /*
Context
-
+
- Component
↳ RenderEffect - node with context key (first_owned)
↳ children memo - memoizing children fn param
↳ children memo - resolving nested children
-
+
The provider component will be omitted
*/
if (name === 'provider' &&
owner.owned &&
owner.owned.length === 1 &&
- markOwnerType((first_owned = owner.owned[0]!)) === NodeType.Context
+ markOwnerType(first_owned = owner.owned[0]!) === NodeType.Context
) {
- return mapOwner(first_owned, parent)
+ return mapOwner(first_owned, parent, config)
}
// Register component to global map
- RegisterComponent({
- owner: owner as Solid.Component,
- id,
- name,
- elements: (resolvedElements = resolveElements(owner.value)),
- })
+ resolved_els = resolveElements(owner.value, config.eli)
+ registerComponent(config.registry, owner as Solid.Component, id, name, resolved_els)
// Refresh
// omitting refresh memo — map it's children instead
- const refresh = getComponentRefreshNode(owner as Solid.Component)
+ let refresh = getComponentRefreshNode(owner as Solid.Component)
if (refresh) {
mapped.hmr = true
owner = refresh
@@ -192,68 +411,46 @@ function mapOwner(
}
// Computation
else if (isSolidComputation(owner)) {
- observeComputation(owner, owner)
+ observeComputation(owner, owner, config)
if (type != NodeType.Context && (!owner.sources || owner.sources.length === 0)) {
mapped.frozen = true
}
}
- const children: Mapped.Owner[] = []
- mapped.children = children
-
AddedToParentElements = false as boolean
MappedOwnerNode = mapped
// Map html elements in DOM mode
- // elements might already be resolved when mapping components
- if (Mode === TreeWalkerMode.DOM &&
- (resolvedElements = resolvedElements === undefined
- ? resolveElements(owner.value)
- : resolvedElements)
- ) {
- children.push.apply(children, mapElements(asArray(resolvedElements), parent?.children))
+ 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
- const addedToParent = AddedToParentElements
+ let addedToParent = AddedToParentElements
- children.push.apply(children, mapChildren(owner, mapped))
+ mapChildren(owner, mapped, config, mapped.children)
return addedToParent ? undefined : mapped
}
-export const walkSolidTree = /*#__PURE__*/ untrackedCallback(function (
- owner: Solid.Owner | Solid.Root,
- config: {
- mode: TreeWalkerMode
- rootId: NodeID
- onComputationUpdate: ComputationUpdateHandler
- registerComponent: ComponentRegisterHandler
- },
+
+export const walkSolidTree = /*#__PURE__*/ untrackedCallback(function (
+ owner: Solid.Owner | Solid.Root,
+ config: TreeWalkerConfig,
): Mapped.Owner {
- // set the globals to be available for this walk cycle
- Mode = config.mode
- RootId = config.rootId
- OnComputationUpdate = config.onComputationUpdate
- RegisterComponent = config.registerComponent
- const r = mapOwner(owner, null)!
+ const r = mapOwner(owner, null, config)!
- if (Mode === TreeWalkerMode.DOM) {
+ if (config.mode === TreeWalkerMode.DOM) {
// Register all mapped element nodes to their components
- for (const [elNode, {el, component}] of ElementsMap) {
- RegisterComponent({
- element: el,
- componentId: component.id,
- elementId: elNode.id,
- })
+ for (let [elNode, {el, component}] of ElementsMap) {
+ registerElement(config.registry, component.id, elNode.id, el as TEl)
}
ElementsMap.clear()
}
- // clear the globals
- Mode = RootId = OnComputationUpdate = RegisterComponent = undefined!
-
return r
})
diff --git a/packages/debugger/src/types.ts b/packages/debugger/src/types.ts
index a4edf1ef..27d138ab 100644
--- a/packages/debugger/src/types.ts
+++ b/packages/debugger/src/types.ts
@@ -1,5 +1,5 @@
export type {DGraphUpdate, SerializedDGraph} from './dependency/index.ts'
export * from './inspector/types.ts'
-export * from './locator/types.ts'
+export * from './locator/locator.ts'
export * from './main/types.ts'
export type {StructureUpdates} from './structure/index.ts'
diff --git a/packages/main/src/setup.ts b/packages/main/src/setup.ts
index 63a7c4f8..ad4a0691 100644
--- a/packages/main/src/setup.ts
+++ b/packages/main/src/setup.ts
@@ -3,10 +3,12 @@ import '@solid-devtools/debugger/setup'
import {
setClientVersion,
setSolidVersion,
- setLocatorOptions,
} from '@solid-devtools/debugger/setup'
setClientVersion(process.env.CLIENT_VERSION)
setSolidVersion(process.env.SOLID_VERSION, process.env.EXPECTED_SOLID_VERSION)
-export {setLocatorOptions}
+export {
+ setLocatorOptions,
+ setElementInterface,
+} from '@solid-devtools/debugger/setup'
diff --git a/packages/main/src/setup_noop.ts b/packages/main/src/setup_noop.ts
index 71ab069f..60ef6323 100644
--- a/packages/main/src/setup_noop.ts
+++ b/packages/main/src/setup_noop.ts
@@ -1,7 +1,9 @@
import type * as API from './setup.ts'
-export const {setLocatorOptions}: typeof API = {
- setLocatorOptions() {
- /**/
- },
+export const {
+ setLocatorOptions,
+ setElementInterface,
+}: typeof API = {
+ setLocatorOptions() {/**/},
+ setElementInterface() {/**/},
}
diff --git a/packages/overlay/src/index.tsx b/packages/overlay/src/index.tsx
index 6df6d25f..c4040189 100644
--- a/packages/overlay/src/index.tsx
+++ b/packages/overlay/src/index.tsx
@@ -39,19 +39,21 @@ export function attachDevtoolsOverlay(props?: OverlayOptions): (() => void) {
})
}
-const Overlay: s.Component = ({defaultOpen, alwaysOpen, noPadding}) => {
+const Overlay: s.Component = props => {
- const debug = useDebugger()
+ let {alwaysOpen, defaultOpen, noPadding} = props
+
+ const instance = useDebugger()
if (defaultOpen || alwaysOpen) {
- debug.toggleEnabled(true)
+ instance.toggleEnabled(true)
}
- const isOpen = atom(alwaysOpen || debug.enabled())
+ const isOpen = atom(alwaysOpen || instance.enabled())
function toggleOpen(enabled?: boolean) {
if (!alwaysOpen) {
enabled ??= !isOpen()
- debug.toggleEnabled(enabled)
+ instance.toggleEnabled(enabled)
isOpen.set(enabled)
}
}
@@ -106,19 +108,19 @@ const Overlay: s.Component = ({defaultOpen, alwaysOpen, noPaddin
{_ => {
- debug.emit(msg('ResetState', undefined))
+ instance.emit(msg('ResetState', undefined))
- s.onCleanup(() => debug.emit(msg('InspectNode', null)))
+ s.onCleanup(() => instance.emit(msg('InspectNode', null)))
const devtools = createDevtools({
headerSubtitle: () => 'overlay',
})
devtools.output.listen(e => {
- separate(e, debug.emit)
+ separate(e, instance.emit)
})
- debug.listen(e => {
+ instance.listen(e => {
separate(e, devtools.input.emit)
})
diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts
index cc85680d..2c760740 100644
--- a/packages/shared/src/utils.ts
+++ b/packages/shared/src/utils.ts
@@ -9,6 +9,12 @@ export type UnionMember = {
data: T[K],
}
+export function assert(condition: any, message?: string, cause?: any): asserts condition {
+ if (!condition) {
+ throw Error(message ?? 'Assertion failed', {cause})
+ }
+}
+
export function msg(kind: K, data: T[K]): UnionMember {
return {kind, data}
}
@@ -21,20 +27,20 @@ export function info(data: T): T {
return data
}
-export function log(...args: any[]): undefined {
+export function log(message: string, ...args: any[]): undefined {
// eslint-disable-next-line no-console
- console.log(LOG_LABEL_CYAN, ...args)
+ console.log(LOG_LABEL_CYAN+' '+message, ...args)
return
}
-export function warn(...args: any[]): undefined {
+export function warn(message: string, ...args: any[]): undefined {
// eslint-disable-next-line no-console
- console.warn(LOG_LABEL_CYAN, ...args)
+ console.warn(LOG_LABEL_CYAN+' '+message, ...args)
return
}
-export function error(...args: any[]): undefined {
+export function error(message: string, ...args: any[]): undefined {
// eslint-disable-next-line no-console
- console.error(LOG_LABEL_CYAN, ...args)
+ console.error(LOG_LABEL_CYAN+' '+message, ...args)
return
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5622ce9b..6a8acaf1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -118,9 +118,6 @@ importers:
'@solid-primitives/bounds':
specifier: ^0.1.0
version: 0.1.0(solid-js@1.9.5)
- '@solid-primitives/cursor':
- specifier: ^0.1.0
- version: 0.1.0(solid-js@1.9.5)
'@solid-primitives/event-listener':
specifier: ^2.4.0
version: 2.4.0(solid-js@1.9.5)