diff --git a/shells/browser/shared/src/panels/utils.js b/shells/browser/shared/src/panels/utils.js index bcc579b1..a58c58bc 100644 --- a/shells/browser/shared/src/panels/utils.js +++ b/shells/browser/shared/src/panels/utils.js @@ -1,5 +1,5 @@ import { createElement } from 'react'; -import { createRoot, flushSync } from 'react-dom'; +import { unstable_createRoot as createRoot, flushSync } from 'react-dom'; import DevTools from 'src/devtools/views/DevTools'; import { getBrowserName, getBrowserTheme } from '../utils'; diff --git a/shells/dev/src/devtools.js b/shells/dev/src/devtools.js index accc6af8..a4d3fca3 100644 --- a/shells/dev/src/devtools.js +++ b/shells/dev/src/devtools.js @@ -2,7 +2,7 @@ import { createElement } from 'react'; // $FlowFixMe Flow does not yet know about createRoot() -import { createRoot } from 'react-dom'; +import { unstable_createRoot as createRoot } from 'react-dom'; import Bridge from 'src/bridge'; import { installHook } from 'src/hook'; import { initDevTools } from 'src/devtools'; diff --git a/src/backend/ReactDebugHooks.js b/src/backend/ReactDebugHooks.js index 1c62f740..86bfac4f 100644 --- a/src/backend/ReactDebugHooks.js +++ b/src/backend/ReactDebugHooks.js @@ -225,6 +225,7 @@ type ReactCurrentDispatcher = { }; type HooksNode = { + nativeHookIndex: number, name: string, value: mixed, subHooks: Array, @@ -366,6 +367,7 @@ function buildTree(rootStack, readHookLog): HooksTree { const rootChildren = []; let prevStack = null; let levelChildren = rootChildren; + let nativeHookIndex = 0; const stackOfChildren = []; for (let i = 0; i < readHookLog.length; i++) { const hook = readHookLog[i]; @@ -399,6 +401,7 @@ function buildTree(rootStack, readHookLog): HooksTree { levelChildren.push({ name: parseCustomHookName(stack[j - 1].functionName), value: undefined, + nativeHookIndex: -1, subHooks: children, }); stackOfChildren.push(levelChildren); @@ -409,12 +412,13 @@ function buildTree(rootStack, readHookLog): HooksTree { levelChildren.push({ name: hook.primitive, value: hook.value, + nativeHookIndex: hook.primitive === 'DebugValue' ? -1 : nativeHookIndex++, subHooks: [], }); } // Associate custom hook values (useDebugValue() hook entries) with the correct hooks. - rollupDebugValues(rootChildren, null); + processDebugValues(rootChildren, null); return rootChildren; } @@ -423,7 +427,7 @@ function buildTree(rootStack, readHookLog): HooksTree { // That hook adds the user-provided values to the hooks tree. // This method removes those values (so they don't appear in DevTools), // and bubbles them up to the "value" attribute of their parent custom hook. -function rollupDebugValues( +function processDebugValues( hooksTree: HooksTree, parentHooksNode: HooksNode | null ): void { @@ -436,7 +440,7 @@ function rollupDebugValues( i--; debugValueHooksNodes.push(hooksNode); } else { - rollupDebugValues(hooksNode.subHooks, hooksNode); + processDebugValues(hooksNode.subHooks, hooksNode); } } diff --git a/src/backend/agent.js b/src/backend/agent.js index 3848f7b8..0319026b 100644 --- a/src/backend/agent.js +++ b/src/backend/agent.js @@ -23,6 +23,14 @@ type InspectSelectParams = {| rendererID: number, |}; +type OverrideHookParams = {| + id: number, + nativeHookIndex: number, + path: Array, + rendererID: number, + value: any, +|}; + type SetInParams = {| id: number, path: Array, @@ -40,6 +48,7 @@ export default class Agent extends EventEmitter { bridge.addListener('highlightElementInDOM', this.highlightElementInDOM); bridge.addListener('inspectElement', this.inspectElement); bridge.addListener('overrideContext', this.overrideContext); + bridge.addListener('overrideHook', this.overrideHook); bridge.addListener('overrideProps', this.overrideProps); bridge.addListener('overrideState', this.overrideState); bridge.addListener('selectElement', this.selectElement); @@ -122,6 +131,21 @@ export default class Agent extends EventEmitter { } }; + overrideHook = ({ + id, + nativeHookIndex, + path, + rendererID, + value, + }: OverrideHookParams) => { + const renderer = this._rendererInterfaces[rendererID]; + if (renderer == null) { + console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`); + } else { + renderer.setInHook(id, nativeHookIndex, path, value); + } + }; + overrideProps = ({ id, path, rendererID, value }: SetInParams) => { const renderer = this._rendererInterfaces[rendererID]; if (renderer == null) { diff --git a/src/backend/index.js b/src/backend/index.js index 895096ba..4b084740 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -1,11 +1,15 @@ // @flow -import type { Hook, ReactRenderer, RendererInterface } from './types'; +import type { DevToolsHook, ReactRenderer, RendererInterface } from './types'; import Agent from './agent'; import { attach } from './renderer'; -export function initBackend(hook: Hook, agent: Agent, global: Object): void { +export function initBackend( + hook: DevToolsHook, + agent: Agent, + global: Object +): void { const subs = [ hook.sub( 'renderer-attached', diff --git a/src/backend/renderer.js b/src/backend/renderer.js index fb841bfd..42ebfb65 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -24,8 +24,8 @@ import { getUID } from '../utils'; import { inspectHooksOfFiber } from './ReactDebugHooks'; import type { + DevToolsHook, Fiber, - Hook, ReactRenderer, FiberData, RendererInterface, @@ -151,7 +151,7 @@ function getInternalReactConstants(version) { } export function attach( - hook: Hook, + hook: DevToolsHook, rendererID: number, renderer: ReactRenderer, global: Object @@ -194,7 +194,7 @@ export function attach( DEPRECATED_PLACEHOLDER_SYMBOL_STRING, } = ReactSymbols; - const { overrideProps } = renderer; + const { overrideHook, overrideProps } = renderer; const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => { if (__DEBUG__) { @@ -1141,7 +1141,10 @@ export function attach( return { id, - // Does the current renderer support editable props/state/hooks? + // Does the current renderer support editable hooks? + canEditHooks: typeof overrideHook === 'function', + + // Does the current renderer support editable function props? canEditFunctionProps: typeof overrideProps === 'function', // Inspectable properties. @@ -1163,6 +1166,20 @@ export function attach( }; } + function setInHook( + id: number, + nativeHookIndex: number, + path: Array, + value: any + ) { + const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); + if (fiber !== null) { + if (typeof overrideHook === 'function') { + overrideHook(fiber, nativeHookIndex, path, value); + } + } + } + function setInProps(id: number, path: Array, value: any) { const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id)); if (fiber !== null) { @@ -1216,6 +1233,7 @@ export function attach( cleanup, renderer, setInContext, + setInHook, setInProps, setInState, walkTree, diff --git a/src/backend/types.js b/src/backend/types.js index 85a890b5..8ff4a670 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -33,6 +33,16 @@ export type ReactRenderer = { findFiberByHostInstance: (hostInstance: NativeType) => ?Fiber, version: string, bundleType: BundleType, + + // 16.9+ + overrideHook?: ?( + fiber: Object, + nativeHookIndex: number, + path: Array, + value: any + ) => void, + + // 16.7+ overrideProps?: ?( fiber: Object, path: Array, @@ -55,15 +65,21 @@ export type RendererInterface = { inspectElement: (id: number) => InspectedElement | null, renderer: ReactRenderer | null, selectElement: (id: number) => void, + setInContext: (id: number, path: Array, value: any) => void, + setInHook: ( + id: number, + nativeHookIndex: number, + path: Array, + value: any + ) => void, setInProps: (id: number, path: Array, value: any) => void, setInState: (id: number, path: Array, value: any) => void, - setInContext: (id: number, path: Array, value: any) => void, walkTree: () => void, }; export type Handler = (data: any) => void; -export type Hook = { +export type DevToolsHook = { listeners: { [key: string]: Array }, rendererInterfaces: Map, renderers: Map, @@ -83,6 +99,7 @@ export type Hook = { }; export type HooksNode = { + nativeHookIndex: number, name: string, value: mixed, subHooks: Array, diff --git a/src/devtools/types.js b/src/devtools/types.js index 9b08e5f3..a6678d39 100644 --- a/src/devtools/types.js +++ b/src/devtools/types.js @@ -48,6 +48,9 @@ export type Owner = {| export type InspectedElement = {| id: number, + // Does the current renderer support editable hooks? + canEditHooks: boolean, + // Does the current renderer support editable function props? canEditFunctionProps: boolean, diff --git a/src/devtools/views/HooksTree.css b/src/devtools/views/HooksTree.css index 28e9b73e..0d2ded18 100644 --- a/src/devtools/views/HooksTree.css +++ b/src/devtools/views/HooksTree.css @@ -3,15 +3,22 @@ border-top: 1px solid var(--color-border); } -.HooksNode { +.Hook { padding-left: 0.75rem; } .NameValueRow { + display: flex; } .Name { color: var(--color-attribute-name); + flex: 0 0 auto; +} +.Name:after { + content: ': '; + color: var(--color-text-color); + margin-right: 0.5rem; } .Value { diff --git a/src/devtools/views/HooksTree.js b/src/devtools/views/HooksTree.js index 303130a2..6bf51ef3 100644 --- a/src/devtools/views/HooksTree.js +++ b/src/devtools/views/HooksTree.js @@ -1,36 +1,67 @@ // @flow -import React from 'react'; -import { KeyValue } from './InspectedElementTree'; +import React, { useContext } from 'react'; +import { BridgeContext, StoreContext } from './context'; +import { EditableValue, KeyValue } from './InspectedElementTree'; import styles from './HooksTree.css'; import type { HooksNode, HooksTree } from 'src/backend/types'; -export function HooksTreeView({ hooksTree }: { hooksTree: HooksTree | null }) { - if (hooksTree === null) { +type HooksTreeViewProps = {| + canEditHooks: boolean, + hooks: HooksTree | null, + id: number, +|}; + +export function HooksTreeView({ canEditHooks, hooks, id }: HooksTreeViewProps) { + if (hooks === null) { return null; } else { return (
hooks
- +
); } } -export function InnerHooksTreeView({ hooksTree }: { hooksTree: HooksTree }) { +type InnerHooksTreeViewProps = {| + canEditHooks: boolean, + hooks: HooksTree, + id: number, +|}; + +export function InnerHooksTreeView({ + canEditHooks, + hooks, + id, +}: InnerHooksTreeViewProps) { // $FlowFixMe "Missing type annotation for U" whatever that means - return hooksTree.map((hooksNode, index) => ( - + return hooks.map((hook, index) => ( + )); } -function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) { - const { name, subHooks, value } = hooksNode; +type HookViewProps = {| + canEditHooks: boolean, + hook: HooksNode, + id: number, + path?: Array, +|}; + +function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) { + const { name, nativeHookIndex, subHooks, value } = hook; + + const bridge = useContext(BridgeContext); + const store = useContext(StoreContext); // TODO Add click and key handlers for toggling element open/close state. - // TODO Support editable props const isCustomHook = subHooks.length > 0; @@ -40,7 +71,11 @@ function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) { let isComplexDisplayValue = false; // Format data for display to mimic the props/state/context for now. - if (type === 'number' || type === 'string' || type === 'boolean') { + if (type === 'string') { + displayValue = `"${((value: any): string)}"`; + } else if (type === 'boolean') { + displayValue = value ? 'true' : 'false'; + } else if (type === 'number') { displayValue = value; } else if (value === null) { displayValue = 'null'; @@ -57,39 +92,75 @@ function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) { if (isCustomHook) { if (isComplexDisplayValue) { return ( -
+
- {name}: + {name}
- +
); } else { return ( -
+
- {name}: {/* $FlowFixMe */} + {name} {/* $FlowFixMe */} {displayValue}
- +
); } } else { + let overrideValueFn = null; + if (canEditHooks && name === 'State') { + overrideValueFn = (path: Array, value: any) => { + const rendererID = store.getRendererIDForElement(id); + bridge.send('overrideHook', { + id, + nativeHookIndex, + path, + rendererID, + value, + }); + }; + } + if (isComplexDisplayValue) { return ( -
- +
+
); } else { return ( -
+
- {name}: - {/* $FlowFixMe */} - {displayValue} + {name} + {typeof overrideValueFn === 'function' ? ( + + ) : ( + // $FlowFixMe Cannot create span element because in property children + {displayValue} + )}
); diff --git a/src/devtools/views/InspectedElementTree.js b/src/devtools/views/InspectedElementTree.js index fd62500d..077334c2 100644 --- a/src/devtools/views/InspectedElementTree.js +++ b/src/devtools/views/InspectedElementTree.js @@ -26,7 +26,6 @@ export default function InspectedElementTree({ return null; } else { // TODO Add click and key handlers for toggling element open/close state. - // TODO Support editable props return (
{label}
@@ -164,7 +163,7 @@ type EditableValueProps = {| value: any, |}; -function EditableValue({ +export function EditableValue({ dataType, overrideValueFn, path, @@ -214,7 +213,13 @@ function EditableValue({ onKeyDown={handleKeyDown} onKeyPress={handleKeyPress} type={type} - value={dataType === 'boolean' ? undefined : editableValue || ''} + value={ + dataType === 'boolean' + ? undefined + : editableValue != null + ? editableValue + : '' + } /> ); diff --git a/src/devtools/views/SelectedElement.js b/src/devtools/views/SelectedElement.js index 81a725ce..f163d70e 100644 --- a/src/devtools/views/SelectedElement.js +++ b/src/devtools/views/SelectedElement.js @@ -107,7 +107,15 @@ function InspectedElementView({ inspectedElement, }: InspectedElementViewProps) { const { id, type } = element; - const { context, hooks, owners, props, state } = inspectedElement; + const { + canEditFunctionProps, + canEditHooks, + context, + hooks, + owners, + props, + state, + } = inspectedElement; const { ownerStack } = useContext(TreeContext); const bridge = useContext(BridgeContext); @@ -129,8 +137,7 @@ function InspectedElementView({ const rendererID = store.getRendererIDForElement(id); bridge.send('overrideState', { id, path, rendererID, value }); }; - } else if (type === ElementTypeFunction) { - // TODO Only enable this if renderer.canEditFunctionProps is true! + } else if (type === ElementTypeFunction && canEditFunctionProps) { overridePropsFn = (path: Array, value: any) => { const rendererID = store.getRendererIDForElement(id); bridge.send('overrideProps', { id, path, rendererID, value }); @@ -150,7 +157,7 @@ function InspectedElementView({ data={state} overrideValueFn={overrideStateFn} /> - +