diff --git a/.changeset/light-friends-film.md b/.changeset/light-friends-film.md new file mode 100644 index 00000000..7e26ebec --- /dev/null +++ b/.changeset/light-friends-film.md @@ -0,0 +1,5 @@ +--- +"@solid-devtools/debugger": minor +--- + +Remove element field from SourceCodeData diff --git a/.changeset/proud-dogs-end.md b/.changeset/proud-dogs-end.md new file mode 100644 index 00000000..d040fd48 --- /dev/null +++ b/.changeset/proud-dogs-end.md @@ -0,0 +1,6 @@ +--- +"@solid-devtools/debugger": minor +"solid-devtools": minor +--- + +Add setElementInterface funtion to setup to support custom renderers (closes #343) diff --git a/.changeset/silent-onions-invite.md b/.changeset/silent-onions-invite.md new file mode 100644 index 00000000..6dbe1349 --- /dev/null +++ b/.changeset/silent-onions-invite.md @@ -0,0 +1,6 @@ +--- +"@solid-devtools/shared": patch +"@solid-devtools/extension": patch +--- + +Move assert to shared/utils diff --git a/extension/src/background.ts b/extension/src/background.ts index a8bab565..81e622e9 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -6,7 +6,7 @@ It has to coordinate the communication between the different scripts based on th */ -import {error, log} from '@solid-devtools/shared/utils' +import {assert, error, log} from '@solid-devtools/shared/utils' import { Place_Name, ConnectionName, type Port, @@ -37,12 +37,6 @@ chrome.tabs } }) -function assert(condition: any, message?: string, cause?: any): asserts condition { - if (!condition) { - throw Error(message ?? 'Assertion failed', {cause}) - } -} - function get_assert_tab_id(port: Port, place: Place_Name): Tab_Id { let tab_id = port.sender?.tab?.id assert(tab_id, `${place} has no port sender tab id.`, port) diff --git a/package.json b/package.json index 7faeb2fa..1abe82b3 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "vite-plugin-solid": "^2.11.6", "vitest": "^2.1.9" }, - "packageManager": "pnpm@10.6.0", + "packageManager": "pnpm@10.7.0", "engines": { "node": ">=22", "pnpm": ">=10.6.0" diff --git a/packages/debugger/README.md b/packages/debugger/README.md index 3177ebb4..b7202319 100644 --- a/packages/debugger/README.md +++ b/packages/debugger/README.md @@ -27,7 +27,7 @@ pnpm add @solid-devtools/debugger > **Warning** > This package changes extremely often, and is not meant to be used directly. Unless you know what you're doing, use the main package instead. -### Module overview +## Module overview The debugger is split into four submodules: @@ -48,19 +48,19 @@ The debugger needs to be setup before it can be used. To do that, import the `./ ```ts import '@solid-devtools/debugger/setup' -import { useDebugger } from '@solid-devtools/debugger/bundled' // or from '@solid-devtools/debugger' +import {useDebugger} from '@solid-devtools/debugger/bundled' // or from '@solid-devtools/debugger' const debug = useDebugger() ``` -### Using component locator +## Using component locator _Debugger feature inspired by [LocatorJS](https://www.locatorjs.com)_ Locator let's you locate components on the page, and go to their source code in your IDE. All you need to do is configure it by calling `setLocatorOptions` with some options. ```ts -import { useDebugger } from '@solid-devtools/debugger' // or 'solid-devtools/setup' +import {useDebugger} from '@solid-devtools/debugger' // or 'solid-devtools/setup' const debug = useDebugger() debug.setLocatorOptions() @@ -68,13 +68,13 @@ debug.setLocatorOptions() It will not allow you to highlight hovered components on the page and reveal them in the IDE or the Chrome Extension. _(depending of if the extension panel is open or not)_ -#### Locator Options +### Locator Options Not passing any options will enable the locator with Alt as the trigger key and no `targetIDE` selected. Currently Locator allows for specifying these props: -##### `targetIDE` +#### `targetIDE` Choose in which IDE the component source code should be revealed. @@ -111,7 +111,7 @@ setLocatorOptions({ }) ``` -##### `key` +#### `key` Holding which key should enable the locator overlay? It's `"Alt"` by default — Alt on Windows, and Option or on macOS. @@ -123,7 +123,7 @@ setLocatorOptions({ }) ``` -#### Using the Locator on the page +### Using the Locator on the page To activate the Locator module — you have to hold down the Alt/Option key and move your mouse around the page to highlight components and their different HTML Elements. @@ -131,6 +131,33 @@ Clicking the component should take you to the component source code, given that https://user-images.githubusercontent.com/24491503/174093606-a0d80331-021f-4d43-b0bb-e9a4041e1a26.mp4 +## Supporting custom renderers + +By default the debugger assumes you are using `"solid-js/web"` as jsx renderer and that the rendered elements are `HTMLElement`s. + +If you are using a custom renderer—such as Three.js, Pixi.js, or Lightning.js—you need to provide the debugger with an `ElementInterface` implementation. + +```ts +import * as debug from '@solid-devtools/debugger/types' +import {setElementInterface} from '@solid-devtools/debugger/setup' // or 'solid-devtools/setup' + +/** ElementInterface implementation for DOM Element */ +let element_interface: debug.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 = debug.getLocationAttr(el) + return attr && debug.parseLocationString(attr) || null + }, +} + +setElementInterface(element_interface) +``` + ## Changelog See [CHANGELOG.md](./CHANGELOG.md). diff --git a/packages/debugger/package.json b/packages/debugger/package.json index 6859f1df..d72af6fd 100644 --- a/packages/debugger/package.json +++ b/packages/debugger/package.json @@ -52,7 +52,6 @@ "@nothing-but/utils": "~0.17.0", "@solid-devtools/shared": "workspace:^", "@solid-primitives/bounds": "^0.1.0", - "@solid-primitives/cursor": "^0.1.0", "@solid-primitives/event-listener": "^2.4.0", "@solid-primitives/keyboard": "^1.3.0", "@solid-primitives/platform": "^0.2.0", diff --git a/packages/debugger/src/inspector/index.ts b/packages/debugger/src/inspector/index.ts index e1c6ed6a..16d93774 100644 --- a/packages/debugger/src/inspector/index.ts +++ b/packages/debugger/src/inspector/index.ts @@ -62,6 +62,7 @@ export function createInspector(props: { const encoded = encodeValue( node.getValue(), selected, + setup.eli, selected && (storeNode => node.addStoreObserver(observeStoreNode(storeNode))), ) @@ -79,7 +80,7 @@ export function createInspector(props: { [ storeProperty, typeof data === 'object' - ? encodeValue(data.value, true, undefined, true) + ? encodeValue(data.value, true, setup.eli, undefined, true) : data ?? null, ], ]) @@ -163,6 +164,7 @@ export function createInspector(props: { onValueUpdate: pushValueUpdate, onPropStateChange: pushPropState, observedPropsMap: propsMap, + eli: setup.eli, }) props.emit(msg('InspectedNodeDetails', result.details)) diff --git a/packages/debugger/src/inspector/index.test.tsx b/packages/debugger/src/inspector/inspector.test.tsx similarity index 88% rename from packages/debugger/src/inspector/index.test.tsx rename to packages/debugger/src/inspector/inspector.test.tsx index 3b73b53c..e5ac37ad 100644 --- a/packages/debugger/src/inspector/index.test.tsx +++ b/packages/debugger/src/inspector/inspector.test.tsx @@ -4,9 +4,11 @@ import * as s from 'solid-js' import * as test from 'vitest' import {getObjectById, getSdtId, ObjectType} from '../main/id.ts' import setup from '../main/setup.ts' -import {type Mapped, NodeType, PropGetterState, type Solid, ValueType} from '../types.ts' +import {dom_element_interface, type Mapped, NodeType, PropGetterState, type Solid, ValueType} from '../types.ts' import {collectOwnerDetails} from './inspector.ts' +const eli = dom_element_interface + test.describe('collectOwnerDetails', () => { test.it('collects focused owner details', () => { s.createRoot(dispose => { @@ -42,13 +44,10 @@ test.describe('collectOwnerDetails', () => { const [innerMemo] = memo.owned as [Solid.Memo, Solid.Computation] const {details, valueMap} = collectOwnerDetails(memo, { - observedPropsMap: new WeakMap(), - onPropStateChange: () => { - /**/ - }, - onValueUpdate: () => { - /**/ - }, + observedPropsMap: new WeakMap(), + onPropStateChange: () => {/**/}, + onValueUpdate: () => {/**/}, + eli: eli, }) test.expect(details).toEqual({ @@ -109,13 +108,10 @@ test.describe('collectOwnerDetails', () => { )) const {details} = collectOwnerDetails(owner, { - observedPropsMap: new WeakMap(), - onPropStateChange: () => { - /**/ - }, - onValueUpdate: () => { - /**/ - }, + observedPropsMap: new WeakMap(), + onPropStateChange: () => {/**/}, + onValueUpdate: () => {/**/}, + eli: eli, }) dispose() @@ -169,13 +165,10 @@ test.describe('collectOwnerDetails', () => { }) const {details} = collectOwnerDetails(owner, { - observedPropsMap: new WeakMap(), - onPropStateChange: () => { - /**/ - }, - onValueUpdate: () => { - /**/ - }, + observedPropsMap: new WeakMap(), + onPropStateChange: () => {/**/}, + onValueUpdate: () => {/**/}, + eli: eli, }) test.expect(details).toEqual({ @@ -215,11 +208,10 @@ test.describe('collectOwnerDetails', () => { const onValueUpdate = test.vi.fn() collectOwnerDetails(owner, { - observedPropsMap: new WeakMap(), - onPropStateChange: () => { - /**/ - }, - onValueUpdate: onValueUpdate, + observedPropsMap: new WeakMap(), + onPropStateChange: () => {/**/}, + onValueUpdate: onValueUpdate, + eli: eli, }) test.expect(onValueUpdate).not.toBeCalled() @@ -249,11 +241,10 @@ test.describe('collectOwnerDetails', () => { const onValueUpdate = test.vi.fn() collectOwnerDetails(owner, { - observedPropsMap: new WeakMap(), - onPropStateChange: () => { - /**/ - }, - onValueUpdate: onValueUpdate, + observedPropsMap: new WeakMap(), + onPropStateChange: () => {/**/}, + onValueUpdate: onValueUpdate, + eli: eli, }) test.expect(onValueUpdate).not.toBeCalled() diff --git a/packages/debugger/src/inspector/inspector.ts b/packages/debugger/src/inspector/inspector.ts index df7e52e5..fa14d3a1 100644 --- a/packages/debugger/src/inspector/inspector.ts +++ b/packages/debugger/src/inspector/inspector.ts @@ -1,9 +1,9 @@ import {misc} from '@nothing-but/utils' -import {parseLocationString, type SourceLocation} from '../locator/index.ts' +import {parseLocationString} from '../locator/index.ts' import {ObjectType, getSdtId} from '../main/id.ts' import {observeValueUpdate, removeValueUpdateObserver} from '../main/observe.ts' import setup from '../main/setup.ts' -import {type Mapped, type NodeID, type Solid, type ValueItemID, NodeType, ValueItemType} from '../main/types.ts' +import {type ElementInterface, type Mapped, type NodeID, type Solid, type SourceLocation, type ValueItemID, NodeType, ValueItemType} from '../main/types.ts' import * as utils from '../main/utils.ts' import {UNOWNED_ROOT} from '../main/roots.ts' import {encodeValue} from './serialize.ts' @@ -163,9 +163,10 @@ let PropsMap: ObservedPropsMap const $INSPECTOR = Symbol('inspector') -function mapSourceValue( +function mapSourceValue( node_raw: Solid.SourceMapValue | Solid.Computation, handler: (nodeId: NodeID, value: unknown) => void, + eli: ElementInterface, ): Mapped.SourceValue | null { let node = utils.getNode(node_raw) @@ -194,11 +195,14 @@ function mapSourceValue( type: node.kind, name: utils.getNodeName(node.data), id: id, - value: encodeValue(value, false), + value: encodeValue(value, false, eli), } } -function mapProps(props: Solid.Component['props']) { +function mapProps( + props: Solid.Component['props'], + eli: ElementInterface, +) { // proxy props need to be checked for changes in keys const isProxy = !!(props as any)[setup.solid.$PROXY] const record: Mapped.Props['record'] = {} @@ -232,14 +236,14 @@ function mapProps(props: Solid.Component['props']) { const lastValue = getValue() record[key] = { getter: isStale ? PropGetterState.Stale : PropGetterState.Live, - value: lastValue !== $NOT_SET ? encodeValue(getValue(), false) : null, + value: lastValue !== $NOT_SET ? encodeValue(getValue(), false, eli) : null, } } // VALUE else { record[key] = { getter: false, - value: encodeValue(desc.value, false), + value: encodeValue(desc.value, false, eli), } // non-object props cannot be inspected (won't ever change and aren't deep) if (Array.isArray(desc.value) || misc.is_plain_object(desc.value)) @@ -251,18 +255,19 @@ function mapProps(props: Solid.Component['props']) { return {props: {proxy: isProxy, record}, checkProxyProps} } -export type CollectDetailsConfig = { +export type CollectDetailsConfig = { onPropStateChange: Inspector.OnPropStateChange onValueUpdate: Inspector.OnValueUpdate observedPropsMap: ObservedPropsMap + eli: ElementInterface, } -export function collectOwnerDetails( +export function collectOwnerDetails( owner: Solid.Owner, - config: CollectDetailsConfig, + config: CollectDetailsConfig, ) { - const {onValueUpdate} = config + const {onValueUpdate, eli} = config // Set globals ValueMap = new ValueNodeMap() @@ -308,7 +313,7 @@ export function collectOwnerDetails( details.hmr = true } - ;({checkProxyProps, props: details.props} = mapProps(owner.props)) + ;({checkProxyProps, props: details.props} = mapProps(owner.props, eli)) let location: string | SourceLocation | undefined if (( @@ -326,7 +331,7 @@ export function collectOwnerDetails( observeValueUpdate(owner, () => onValueUpdate(ValueItemType.Value), $INSPECTOR) } - details.value = encodeValue(getValue(), false) + details.value = encodeValue(getValue(), false, eli) } const onSignalUpdate = (signalId: NodeID) => @@ -334,13 +339,13 @@ export function collectOwnerDetails( // map signals if (sourceMap) for (let signal of sourceMap) { - let mapped = mapSourceValue(signal, onSignalUpdate) + let mapped = mapSourceValue(signal, onSignalUpdate, eli) if (mapped) details.signals.push(mapped) } // map memos if (owned) for (let node of owned) { - let mapped = mapSourceValue(node, onSignalUpdate) + let mapped = mapSourceValue(node, onSignalUpdate, eli) if (mapped) details.signals.push(mapped) } @@ -351,7 +356,7 @@ export function collectOwnerDetails( let signal = signal_ref.deref() if (signal == null) continue - let mapped = mapSourceValue(signal, onSignalUpdate) + let mapped = mapSourceValue(signal, onSignalUpdate, eli) if (mapped == null) continue details.signals.push(mapped) diff --git a/packages/debugger/src/inspector/serialize.test.ts b/packages/debugger/src/inspector/serialize.test.ts index e76dd9d0..cb307f1f 100644 --- a/packages/debugger/src/inspector/serialize.test.ts +++ b/packages/debugger/src/inspector/serialize.test.ts @@ -1,25 +1,25 @@ import '../setup.ts' -import {type Truthy} from '@solid-primitives/utils' import {createMutable, createStore} from 'solid-js/store' import {describe, expect, test, vi} from 'vitest' import {ObjectType, getObjectById} from '../main/id.ts' -import {encodeValue} from './serialize.ts' +import {encodeValue, type HandleStoreCallback} from './serialize.ts' import {type EncodedValue, INFINITY, NAN, NEGATIVE_INFINITY, UNDEFINED, ValueType} from './types.ts' +import {dom_element_interface} from '../types.ts' type Expectations = [name: string, data: unknown, encoded: EncodedValue[]][] const div1 = document.createElement('div') -const a1 = document.createElement('a') +const a1 = document.createElement('a') const div2 = document.createElement('div') -const _testFunction = () => { - /**/ -} +const _testFunction = () => {/**/} const [state] = createStore({a: 1, b: 2, c: 3}) const mutable = createMutable({a: 1, b: 2, c: 3}) +const eli = dom_element_interface + describe('encodeValue Preview', () => { const encodePreviewExpectations: Expectations = [ @@ -72,9 +72,9 @@ describe('encodeValue Preview', () => { ], ] - for (const [testName, value, expectation] of encodePreviewExpectations) { + for (let [testName, value, expectation] of encodePreviewExpectations) { test(testName, () => { - const s = encodeValue(value, false) + let s = encodeValue(value, false, eli) expect(s).toEqual(expectation) expect(JSON.parse(JSON.stringify(s))).toEqual(s) }) @@ -150,7 +150,7 @@ describe('encodeValue Deep', () => { for (const [testName, value, expectation] of encodeDeepExpectations) { test(testName, () => { - const s = encodeValue(value, true) + const s = encodeValue(value, true, eli) expect(s).toEqual(expectation) expect(JSON.parse(JSON.stringify(s))).toEqual(s) }) @@ -174,7 +174,7 @@ describe('save elements to a map', () => { for (const [testName, value, expectation] of elMapExpectations) { test(testName, () => { - const s = encodeValue(value, true) + const s = encodeValue(value, true, eli) expect(s).toEqual(expectation) expect(JSON.parse(JSON.stringify(s))).toEqual(s) }) @@ -225,7 +225,7 @@ describe('encodeValue with repeated references', () => { for (const [testName, value, expectation] of circularExpectations) { test(testName, () => { - const s = encodeValue(value, true) + const s = encodeValue(value, true, eli) expect(s).toEqual(expectation) expect(JSON.parse(JSON.stringify(s))).toEqual(s) }) @@ -247,7 +247,7 @@ describe('finding stores in values', () => { name: string, data: unknown, encoded: EncodedValue[], - calledWith: Parameters[2]>>[], + calledWith: Parameters[], ][] = [ [ 'Store', @@ -311,7 +311,7 @@ describe('finding stores in values', () => { for (const [testName, value, expectation, calledWith] of storeExpectations) { test(testName, () => { const onStore = vi.fn() - const s = encodeValue(value, true, onStore) + const s = encodeValue(value, true, eli, onStore) expect(s).toEqual(expectation) expect(JSON.parse(JSON.stringify(s))).toEqual(s) expect(onStore).toBeCalledTimes(calledWith.length) diff --git a/packages/debugger/src/inspector/serialize.ts b/packages/debugger/src/inspector/serialize.ts index cbdc7df0..af70c112 100644 --- a/packages/debugger/src/inspector/serialize.ts +++ b/packages/debugger/src/inspector/serialize.ts @@ -3,6 +3,7 @@ import {getSdtId, ObjectType} from '../main/id.ts' import setup from '../main/setup.ts' import * as utils from '../main/utils.ts' import { + type ElementInterface, type EncodedValue, INFINITY, NAN, @@ -13,12 +14,15 @@ import { ValueType, } from '../types.ts' +export type HandleStoreCallback = (node: Solid.StoreNode, nodeId: NodeID) => void +export type HandleStore = HandleStoreCallback | FalsyValue + // globals let Deep: boolean let List: EncodedValue[] let Seen: Map let InStore: boolean -let HandleStore: ((node: Solid.StoreNode, nodeId: NodeID) => void) | FalsyValue +let HandleStore: HandleStore let IgnoreNextSeen: boolean const encodeNonObject = (value: unknown): EncodedValue => { @@ -38,7 +42,10 @@ const encodeNonObject = (value: unknown): EncodedValue => { } } -function encode(value: unknown): number { +function encode( + value: unknown, + eli: ElementInterface, +): number { const ignoreNextStore = IgnoreNextSeen if (ignoreNextStore) IgnoreNextSeen = false else { @@ -58,12 +65,11 @@ function encode(value: unknown): number { ignoreNextStore || Seen.set(value, index) // HTML Elements - if (value instanceof Element) { + if (eli.isElement(value)) { ;(encoded as EncodedValue)[0] = ValueType.Element - ;(encoded as EncodedValue)[1] = `${getSdtId( - value, - ObjectType.Element, - )}:${value.localName}` + let id = getSdtId(value, ObjectType.Element) + let name = eli.getName(value) + ;(encoded as EncodedValue)[1] = `${id}:${name}` } // Store Nodes else if (!ignoreNextStore && utils.isStoreNode(value)) { @@ -76,14 +82,24 @@ function encode(value: unknown): number { const wasInStore = InStore InStore = IgnoreNextSeen = true ;(encoded as EncodedValue)[0] = ValueType.Store - ;(encoded as EncodedValue)[1] = `${id}:${encode(node)}` + ;(encoded as EncodedValue)[1] = `${id}:${encode(node, eli)}` InStore = wasInStore } // Arrays else if (Array.isArray(value)) { ;(encoded as EncodedValue)[0] = ValueType.Array - ;(encoded as EncodedValue)[1] = Deep ? value.map(encode) : value.length - } else { + if (Deep) { + let data: number[] = Array(value.length) + for (let i = 0; i < value.length; i++) { + data[i] = encode(value[i], eli) + } + ;(encoded as EncodedValue)[1] = data + } else { + ;(encoded as EncodedValue)[1] = value.length + } + } + // Objects + else { const name = Object.prototype.toString.call(value).slice(8, -1) // normal objects (records) if (name === 'Object') { @@ -95,7 +111,7 @@ function encode(value: unknown): number { for (const [key, descriptor] of Object.entries( Object.getOwnPropertyDescriptors(value), )) { - data[key] = descriptor.get ? -1 : encode(descriptor.value) + data[key] = descriptor.get ? -1 : encode(descriptor.value, eli) } } else { ;(encoded as EncodedValue)[1] = Object.keys(value).length @@ -119,11 +135,12 @@ function encode(value: unknown): number { * @param handleStore handle encountered store nodes * @returns encoded value */ -export function encodeValue( - value: unknown, - deep: boolean, - handleStore?: typeof HandleStore, - inStore = false, +export function encodeValue( + value: unknown, + deep: boolean, + eli: ElementInterface, + handleStore?: HandleStore, + inStore: boolean = false, ): EncodedValue[] { Deep = deep List = [] @@ -131,7 +148,7 @@ export function encodeValue( InStore = inStore HandleStore = handleStore - encode(value) + encode(value, eli) const result = List Deep = List = Seen = HandleStore = InStore = undefined! diff --git a/packages/debugger/src/locator/element-overlay.tsx b/packages/debugger/src/locator/element-overlay.tsx index 3fdd6864..dd8798d6 100644 --- a/packages/debugger/src/locator/element-overlay.tsx +++ b/packages/debugger/src/locator/element-overlay.tsx @@ -1,69 +1,80 @@ -import {createElementBounds} from '@solid-primitives/bounds' -import {createElementCursor} from '@solid-primitives/cursor' +import * as s from 'solid-js' +import * as sweb from 'solid-js/web' import {createRootPool} from '@solid-primitives/rootless' -import {type Accessor, type Component, createMemo, getOwner, runWithOwner, Show} from 'solid-js' -import {Portal} from 'solid-js/web' -import {type LocatorComponent} from './find-components.ts' +import type {LocatorComponent} from './index.ts' +import {UNKNOWN, type ElementInterface, type Rect} from '../types.ts' -export function createElementsOverlay(selected: Accessor) { - const useElementOverlay = createRootPool((component: Accessor, active) => ( - - )) +export function createElementsOverlay( + selected: s.Accessor[]>, + eli: ElementInterface, +) { - // wait a second to let the framework mess with the document before attaching the overlay - const owner = getOwner()! - setTimeout(() => { - runWithOwner(owner, () => ( - -
{selected().map(useElementOverlay)}
-
- )) - }, 1000) -} + const useElementOverlay = createRootPool((componentRaw: s.Accessor>, active) => { -const ElementOverlay: Component<{component: LocatorComponent | null}> = props => { - const element = () => props.component?.element - // set pointer cursor to selected component - createElementCursor(element, 'pointer') - const tag = () => element()?.localName - const name = () => props.component?.name + const component = () => active() ? componentRaw() : null - const bounds = createElementBounds(element) - const left = createMemo(prev => (bounds.left === null ? prev : bounds.left), 0) - const top = createMemo(prev => (bounds.top === null ? prev : bounds.top), 0) - const width = createMemo(prev => (bounds.width === null ? prev : bounds.width), 0) - const height = createMemo(prev => (bounds.height === null ? prev : bounds.height), 0) - const transform = createMemo(() => `translate(${Math.round(left())}px, ${Math.round(top())}px)`) - const placeOnTop = createMemo(() => top() > window.innerHeight / 2) + const name = () => component()?.name - return ( - <> - -
-
- -
-
-
-
- {name()}: {tag()} -
-
- {name()}: {tag()} + const rect = s.createMemo((prev: Rect) => { + let comp = component() + if (comp === null) return prev + + let rect = eli.getRect(comp.element) + if (rect === null) return prev + + return rect + }, {x: 0, y: 0, width: 0, height: 0}) + + const transform = () => `translate(${Math.round(rect().x)}px, ${Math.round(rect().y)}px)` + const placeOnTop = () => rect().y > window.innerHeight / 2 + + const tag = () => { + let comp = component() + if (comp === null) return UNKNOWN + + return eli.getName(comp.element) ?? UNKNOWN + } + + return ( + <> + +
+
+ +
+
+
+
+ {name()}: {tag()} +
+
+ {name()}: {tag()} +
-
- -
- - ) + +
+ + ) + + }) + + // 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 ( - <> - -