From ba5b62a8c4c72935f9251359865c8b26139df822 Mon Sep 17 00:00:00 2001 From: Damian Tarnawski Date: Wed, 1 Jan 2025 18:40:02 +0100 Subject: [PATCH] feat: Inspecting values in the console (#328) * wip: Add inspect action for values * fix: Fix value rows highlighting styles * feat: inspecting props * refactor: update getValueActionInspect to accept Inspector.ValueItem and streamline action handling * chore: Cleanup * feat: Support value inspecting for overlay * Create lucky-rats-care.md --- .changeset/lucky-rats-care.md | 7 + extension/src/panel.tsx | 35 +++- packages/debugger/src/inspector/index.ts | 22 ++- packages/debugger/src/inspector/types.ts | 11 ++ packages/debugger/src/main/index.ts | 6 +- packages/frontend/src/App.tsx | 10 +- packages/frontend/src/SidePanel.tsx | 4 +- packages/frontend/src/index.tsx | 2 +- packages/frontend/src/inspector.tsx | 210 ++++++++++++--------- packages/frontend/src/structure.tsx | 10 +- packages/frontend/src/ui/error-overlay.tsx | 10 +- packages/frontend/src/ui/icons.tsx | 26 ++- packages/frontend/src/ui/index.ts | 2 +- packages/frontend/src/ui/owner-name.tsx | 19 +- packages/frontend/src/ui/toggle-button.tsx | 4 +- 15 files changed, 245 insertions(+), 133 deletions(-) create mode 100644 .changeset/lucky-rats-care.md diff --git a/.changeset/lucky-rats-care.md b/.changeset/lucky-rats-care.md new file mode 100644 index 00000000..a6c6a038 --- /dev/null +++ b/.changeset/lucky-rats-care.md @@ -0,0 +1,7 @@ +--- +"@solid-devtools/extension": minor +"@solid-devtools/debugger": minor +"@solid-devtools/frontend": minor +--- + +New feature: inspecting values in the console. (Closes #166) diff --git a/extension/src/panel.tsx b/extension/src/panel.tsx index 15064db0..3068816e 100644 --- a/extension/src/panel.tsx +++ b/extension/src/panel.tsx @@ -4,8 +4,9 @@ import * as s from 'solid-js' import * as web from 'solid-js/web' -import {log} from '@solid-devtools/shared/utils' +import {error, log} from '@solid-devtools/shared/utils' import * as frontend from '@solid-devtools/frontend' +import * as debug from '@solid-devtools/debugger/types' import { ConnectionName, Place_Name, port_on_message, port_post_message_obj, @@ -62,8 +63,36 @@ function App() { } }) - /* Devtools -> Client */ - devtools.output.listen(e => port_post_message_obj(port, e)) + devtools.output.listen(e => { + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check + switch (e.name) { + case 'ConsoleInspectValue': { + /* + `chrome.devtools.inspectedWindow.eval` runs in a devtools console + so the value can be additionally inspected with `inspect()` + */ + let get_value = `window[${JSON.stringify(debug.GLOBAL_GET_VALUE)}]` + let value_id = JSON.stringify(e.details) + + chrome.devtools.inspectedWindow.eval( + /*js*/`typeof ${get_value} === 'function' && (() => { + let v = ${get_value}(${value_id}) + inspect(v) + console.log(v) + })()`, + (_, err?: chrome.devtools.inspectedWindow.EvaluationExceptionInfo) => { + if (err && (err.isError || err.isException)) { + error(err.description) + } + }) + break + } + default: + /* Devtools -> Client */ + port_post_message_obj(port, e) + break + } + }) return (
InspectorUpdateMap['propKeys'] | null) | null + /* + For the extension for inspecting values through `inspect()` + */ + function getValue(id: ValueItemID): unknown { + return valueMap.get(id)?.getValue?.() + } + window[GLOBAL_GET_VALUE] = getValue + // Batch and dedupe inspector updates // these will include updates to signals, stores, props, and node value const {pushPropState, pushValueUpdate, pushInspectToggle, triggerPropsCheck, clearUpdates} = @@ -156,11 +165,12 @@ export function createInspector(props: { }) props.emit('InspectedNodeDetails', result.details) - valueMap = result.valueMap - lastDetails = result.details + + valueMap = result.valueMap + lastDetails = result.details checkProxyProps = result.checkProxyProps || null } else { - lastDetails = undefined + lastDetails = undefined checkProxyProps = null } @@ -186,5 +196,9 @@ export function createInspector(props: { node.setSelected(selected) pushInspectToggle(id, selected) }, + consoleLogValue(value_id: ValueItemID): void { + // eslint-disable-next-line no-console + console.log(getValue(value_id)) + } } } diff --git a/packages/debugger/src/inspector/types.ts b/packages/debugger/src/inspector/types.ts index 30526ee7..90dffe5f 100644 --- a/packages/debugger/src/inspector/types.ts +++ b/packages/debugger/src/inspector/types.ts @@ -68,3 +68,14 @@ export type InspectorUpdateMap = { export type InspectorUpdate = { [T in keyof InspectorUpdateMap]: [type: T, data: InspectorUpdateMap[T]] }[keyof InspectorUpdateMap] + +/* + For the extension for inspecting values through `inspect()` +*/ +export const GLOBAL_GET_VALUE = '$SdtGetValue' + +declare global { + interface Window { + [GLOBAL_GET_VALUE]?: (id: ValueItemID) => unknown + } +} diff --git a/packages/debugger/src/main/index.ts b/packages/debugger/src/main/index.ts index bb5270d3..6860cdfd 100644 --- a/packages/debugger/src/main/index.ts +++ b/packages/debugger/src/main/index.ts @@ -10,7 +10,7 @@ import {createStructure, type StructureUpdates} from '../structure/index.ts' import {DebuggerModule, DEFAULT_MAIN_VIEW, DevtoolsMainView, TreeWalkerMode} from './constants.ts' import {getObjectById, getSdtId, ObjectType} from './id.ts' import setup from './setup.ts' -import {type Mapped, type NodeID} from './types.ts' +import {type Mapped, type NodeID, type ValueItemID} from './types.ts' export type InspectedState = { readonly ownerId: NodeID | null @@ -37,6 +37,7 @@ export type InputChannels = { ResetState: void InspectNode: {ownerId: NodeID | null; signalId: NodeID | null} | null InspectValue: ToggleInspectedValueData + ConsoleInspectValue: ValueItemID HighlightElementChange: HighlightElementPayload OpenLocation: void TreeViewModeChange: TreeWalkerMode @@ -280,6 +281,9 @@ function createDebugger() { case 'InspectValue': inspector.toggleValueNode(e.details) break + case 'ConsoleInspectValue': + inspector.consoleLogValue(e.details) + break case 'OpenLocation': openInspectedNodeLocation() break diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index b9ba5eab..d6ea7723 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -16,7 +16,7 @@ export const App: s.Component<{headerSubtitle?: s.JSX.Element}> = props => { >
- +

Solid Developer Tools

{props.headerSubtitle && ( @@ -80,7 +80,7 @@ const Options: s.Component = () => { }}> - @@ -97,7 +97,7 @@ const Options: s.Component = () => { class=' flex items-center gap-1 p-1 rounded-md outline-none text-text transition-colors hover:bg-orange-500/10 focus:bg-orange-500/10'> - + Report a bug { class=' flex items-center gap-1 p-1 rounded-md outline-none text-text transition-colors hover:bg-pink-500/10 focus:bg-pink-500/10'> - + Support the project
@@ -141,4 +141,4 @@ const Options: s.Component = () => { // View: {view.get().toUpperCase()} // // ) -// } \ No newline at end of file +// } diff --git a/packages/frontend/src/SidePanel.tsx b/packages/frontend/src/SidePanel.tsx index d540a5da..133f23c4 100644 --- a/packages/frontend/src/SidePanel.tsx +++ b/packages/frontend/src/SidePanel.tsx @@ -77,7 +77,7 @@ export function createSidePanel() { class={action_button} onClick={openComponentLocation} > - + )}
(props: {when: T; title: s.JSX.Element; children: s.JSX.E export function InspectorView(): s.JSX.Element { - const {inspector, hovered} = useAppCtx() - const {state} = inspector + const ctx = useAppCtx() const {setOpenPanel} = s.useContext(SidePanelCtx)! + function getValueActionInspect(item: Inspector.ValueItem): ValueNodeAction | undefined { + + if (item.value.type !== debug.ValueType.Unknown) { + return { + icon: 'Eye', + title: 'Inspect', + onClick() { + ctx.output.emit({ + name: 'ConsoleInspectValue', + details: item.itemId, + }) + }, + } + } + } + function getValueActionGraph(signal: Inspector.Signal): ValueNodeAction { + return { + icon: 'Graph', + title: 'Open in Graph panel', + onClick() { + s.batch(() => { + ctx.inspector.setInspectedSignal(signal.id) + setOpenPanel('dgraph') + }) + }, + } + } + const valueItems = s.createMemo(() => { - const list = Object.values(state.signals) const memos: Inspector.Signal[] = [] const signals: Inspector.Signal[] = [] const stores: Inspector.Signal[] = [] - for (const signal of list) { + for (const signal of Object.values(ctx.inspector.state.signals)) { switch (signal.type) { case debug.NodeType.Memo: memos.push(signal) ;break case debug.NodeType.Signal: signals.push(signal) ;break @@ -423,19 +449,25 @@ export function InspectorView(): s.JSX.Element {
Props {state.props!.proxy && PROXY}} + when={ + ctx.inspector.state.props != null && + Object.keys(ctx.inspector.state.props.record).length + } + title={<>Props {ctx.inspector.state.props!.proxy && PROXY}} > - + {(name, value) => ( inspector.inspectValueItem(value())} - onElementHover={hovered.toggleHoveredElement} + onClick={() => ctx.inspector.inspectValueItem(value())} + onElementHover={ctx.hovered.toggleHoveredElement} isSignal={value().getter !== false} isStale={value().getter === debug.PropGetterState.Stale} + actions={[ + getValueActionInspect(value()), + ]} /> )} @@ -447,8 +479,11 @@ export function InspectorView(): s.JSX.Element { name={store.name} value={store.value} isExtended={store.extended} - onClick={() => inspector.inspectValueItem(store)} - onElementHover={hovered.toggleHoveredElement} + onClick={() => ctx.inspector.inspectValueItem(store)} + onElementHover={ctx.hovered.toggleHoveredElement} + actions={[ + getValueActionInspect(store), + ]} /> )} @@ -459,21 +494,17 @@ export function InspectorView(): s.JSX.Element { inspector.inspectValueItem(signal)} - onElementHover={hovered.toggleHoveredElement} + onClick={() => { + ctx.inspector.inspectValueItem(signal) + }} + onElementHover={ctx.hovered.toggleHoveredElement} isExtended={signal.extended} - isInspected={inspector.isInspected(signal.id)} + isInspected={ctx.inspector.isInspected(signal.id)} isSignal - actions={[{ - icon: 'Graph', - title: 'Open in Graph panel', - onClick() { - s.batch(() => { - inspector.setInspectedSignal(signal.id) - setOpenPanel('dgraph') - }) - }, - }]} + actions={[ + getValueActionInspect(signal), + getValueActionGraph(signal), + ]} /> )} @@ -485,60 +516,64 @@ export function InspectorView(): s.JSX.Element { name={memo.name} value={memo.value} isExtended={memo.extended} - onClick={() => inspector.inspectValueItem(memo)} - onElementHover={hovered.toggleHoveredElement} + onClick={() => ctx.inspector.inspectValueItem(memo)} + onElementHover={ctx.hovered.toggleHoveredElement} isSignal - actions={[{ - icon: 'Graph', - title: 'Open in Graph panel', - onClick() { - s.batch(() => { - inspector.setInspectedOwner(memo.id) - setOpenPanel('dgraph') - }) - }, - }]} + actions={[ + getValueActionInspect(memo), + getValueActionGraph(memo), + ]} /> )} - +
- {state.type ? debug.NODE_TYPE_NAMES[state.type] : 'Owner'} + {ctx.inspector.state.type + ? debug.NODE_TYPE_NAMES[ctx.inspector.state.type] + : 'Owner'} - {state.value && ( + {ctx.inspector.state.value && ( inspector.inspectValueItem(state.value!)} - onElementHover = {hovered.toggleHoveredElement} + value = {ctx.inspector.state.value.value} + isExtended = {ctx.inspector.state.value.extended} + onClick = {() => { + ctx.inspector.inspectValueItem(ctx.inspector.state.value!) + }} + onElementHover = {ctx.hovered.toggleHoveredElement} isSignal = { - state.type === debug.NodeType.Computation || - state.type === debug.NodeType.CatchError || - state.type === debug.NodeType.Effect || - state.type === debug.NodeType.Memo || - state.type === debug.NodeType.Refresh || - state.type === debug.NodeType.Render || - state.type === debug.NodeType.Signal || - state.type === debug.NodeType.Store || - state.hmr + ctx.inspector.state.type === debug.NodeType.Computation || + ctx.inspector.state.type === debug.NodeType.CatchError || + ctx.inspector.state.type === debug.NodeType.Effect || + ctx.inspector.state.type === debug.NodeType.Memo || + ctx.inspector.state.type === debug.NodeType.Refresh || + ctx.inspector.state.type === debug.NodeType.Render || + ctx.inspector.state.type === debug.NodeType.Signal || + ctx.inspector.state.type === debug.NodeType.Store || + ctx.inspector.state.hmr } + actions={[ + getValueActionInspect(ctx.inspector.state.value), + ]} /> )} - {state.location && ( + {ctx.inspector.state.location && ( @@ -568,7 +603,7 @@ const CollapsableObjectPreview: s.Component<{ return ( { + children={_ => { const [extended, setExtended] = s.createSignal(false) return ( ) - })} + }} fallback={} /> ) @@ -598,8 +633,14 @@ const ObjectValuePreview: s.Component<{ }> = props => { return ( } + when={ + props.data.value && + props.data.length && + props.extended + } + children={ + + } fallback={ '; color: ${theme.vars.disabled}; } - .${value_element_container_class}:hover { - ${ui.highlight_opacity_var}: 0.6; - } /**/ ` @@ -735,7 +772,6 @@ const ValuePreview: s.Component<{ return ( -
{value.name} ) @@ -785,6 +821,12 @@ function createNestedHover(): NestedHover { } } +export type ValueNodeAction = { + icon: keyof typeof ui.icon; + title?: string; + onClick: () => void +} + export const ValueNode: s.Component<{ value: decode.DecodedValue name: string | undefined @@ -798,7 +840,7 @@ export const ValueNode: s.Component<{ isStale?: boolean onClick?: VoidFunction onElementHover?: ToggleElementHover - actions?: {icon: keyof typeof ui.Icon; title?: string; onClick: VoidFunction}[] + actions?: (ValueNodeAction | FalsyValue)[] class?: string }> = props => { const ctx = s.useContext(ValueContext) @@ -827,6 +869,9 @@ export const ValueNode: s.Component<{ 'font-mono leading-inspector_row', props.isStale && 'opacity-60', )} + style={{ + [ui.highlight_opacity_var]: isHovered() ? '0.3' : '0', + }} {...(props.name && {'aria-label': `${props.name} signal`})} {...hoverProps} > @@ -841,7 +886,6 @@ export const ValueNode: s.Component<{ class={clsx( ui.highlight_element, 'b b-solid b-highlight-border', - isHovered() ? 'opacity-30' : 'opacity-0', )} /> @@ -866,26 +910,24 @@ export const ValueNode: s.Component<{
- - {action => { - const IconComponent = ui.Icon[action.icon] - return ( - - ) - }} + + {action => <> + + }
)} diff --git a/packages/frontend/src/structure.tsx b/packages/frontend/src/structure.tsx index 267fdfd8..23e7cd85 100644 --- a/packages/frontend/src/structure.tsx +++ b/packages/frontend/src/structure.tsx @@ -360,7 +360,7 @@ const LocatorButton: s.Component = () => { selected={locator.locatorEnabled()} title="Select an element in the page to inspect it" > - + ) } @@ -417,14 +417,14 @@ const Search: s.Component = () => {
- +
{value() && ( )} @@ -857,7 +857,7 @@ export const OwnerPath: s.Component = () => { 'background-image': `linear-gradient(to right, ${theme.vars.panel.bg} ${theme.spacing[8]}, transparent ${theme.spacing[32]})`, }} > - +
)}
@@ -869,7 +869,7 @@ export const OwnerPath: s.Component = () => { return <>
- +
{props.currentCount} @@ -167,7 +167,7 @@ const RenderErrorOverlay: s.Component
diff --git a/packages/frontend/src/ui/icons.tsx b/packages/frontend/src/ui/icons.tsx index 12b424cd..3ba21f1a 100644 --- a/packages/frontend/src/ui/icons.tsx +++ b/packages/frontend/src/ui/icons.tsx @@ -2,11 +2,12 @@ * Some icons taken from https://phosphoricons.com */ -import {type Component} from 'solid-js' +import * as s from 'solid-js' +import * as sweb from 'solid-js/web' -export type ProxyIconComponent = Component<{id: ID}> +export type ProxyIconComponent = s.Component<{id: ID}> -export type IconComponent = Component<{class?: string}> +export type IconComponent = s.Component<{class?: string}> const ArrowRight: ProxyIconComponent<'ArrowRight'> = ({id}) => ( @@ -817,12 +818,12 @@ const iconComponents = { SolidWhite, } as const -export const Icon: { - [key in keyof typeof embedIconComponents | keyof typeof iconComponents]: IconComponent -} = {} as any +export type IconName = keyof typeof embedIconComponents | keyof typeof iconComponents + +export const icon: {[key in IconName]: IconComponent} = {} as any for (const name in embedIconComponents) { - ;(Icon as any)[name] = (props: {class?: string}) => ( + ;(icon as any)[name] = (props: {class?: string}) => ( @@ -830,12 +831,17 @@ for (const name in embedIconComponents) { } for (const name in iconComponents) { - ;(Icon as any)[name] = iconComponents[name as keyof typeof iconComponents] + ;(icon as any)[name] = iconComponents[name as keyof typeof iconComponents] } -export default Icon +export function Icon(props: { + icon: IconName + class?: string, +}) { + return +} -export const MountIcons: Component = () => { +export const MountIcons: s.Component = () => { return (
{(Object.keys(embedIconComponents) as (keyof typeof embedIconComponents)[]).map( diff --git a/packages/frontend/src/ui/index.ts b/packages/frontend/src/ui/index.ts index 00de5099..74c4643b 100644 --- a/packages/frontend/src/ui/index.ts +++ b/packages/frontend/src/ui/index.ts @@ -3,7 +3,7 @@ export * from './styles.tsx' export * from './badge.tsx' export * from './error-overlay.tsx' export {Highlight} from './highlight.tsx' -export {Icon, MountIcons} from './icons.tsx' +export * from './icons.tsx' export type {IconComponent} from './icons.tsx' export * from './owner-name.tsx' export {Scrollable} from './scrollable.tsx' diff --git a/packages/frontend/src/ui/owner-name.tsx b/packages/frontend/src/ui/owner-name.tsx index 356b25ed..a75150c2 100644 --- a/packages/frontend/src/ui/owner-name.tsx +++ b/packages/frontend/src/ui/owner-name.tsx @@ -3,28 +3,27 @@ import clsx from 'clsx' import {NodeType, UNKNOWN} from '@solid-devtools/debugger/types' import {createPingedSignal} from '@solid-devtools/shared/primitives' import {Highlight} from './highlight.tsx' -import Icon, {type IconComponent} from './icons.tsx' import * as ui from '../ui/index.ts' export function Node_Type_Icon(props: { type: NodeType | undefined | null class?: string }): s.JSX.Element { - let prev_icon: IconComponent | undefined + let prev_icon: ui.IconComponent | undefined let prev_rendered: s.JSX.Element | undefined const fn = (): s.JSX.Element => { - let IconComp: IconComponent | undefined + let IconComp: ui.IconComponent | undefined // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (props.type) { - case NodeType.Memo: IconComp = Icon.Memo ;break - case NodeType.Effect: IconComp = Icon.Effect ;break - case NodeType.Root: IconComp = Icon.Root ;break - case NodeType.Render: IconComp = Icon.RenderEffect ;break - case NodeType.Computation: IconComp = Icon.Computation ;break - case NodeType.Context: IconComp = Icon.Context ;break - case NodeType.Signal: IconComp = Icon.Signal ;break + case NodeType.Memo: IconComp = ui.icon.Memo ;break + case NodeType.Effect: IconComp = ui.icon.Effect ;break + case NodeType.Root: IconComp = ui.icon.Root ;break + case NodeType.Render: IconComp = ui.icon.RenderEffect ;break + case NodeType.Computation: IconComp = ui.icon.Computation ;break + case NodeType.Context: IconComp = ui.icon.Context ;break + case NodeType.Signal: IconComp = ui.icon.Signal ;break } if (IconComp === prev_icon) { diff --git a/packages/frontend/src/ui/toggle-button.tsx b/packages/frontend/src/ui/toggle-button.tsx index 87ef726f..5cd75b23 100644 --- a/packages/frontend/src/ui/toggle-button.tsx +++ b/packages/frontend/src/ui/toggle-button.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx' import * as s from 'solid-js' import {combineProps} from '@solid-primitives/props' import * as theme from '@solid-devtools/shared/theme' -import Icon from './icons.tsx' +import {icon} from './icons.tsx' import * as color from './color.ts' export const toggle_button = 'toggle-button' @@ -88,7 +88,7 @@ export const CollapseToggle: s.Component<{ } {...(props.name && {'aria-label': `Expand or collapse ${props.name}`})} > -