From edc46d7be7ce8fff2b5c21a00eb6741efbb9ef42 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 13 Aug 2019 17:53:28 -0700 Subject: [PATCH] Misc Flow and import fixes 1. Fixed all reported Flow errors 2. Added a few missing package declarations 3. Deleted ReactDebugHooks fork in favor of react-debug-tools --- .../react-debug-tools/src/ReactDebugHooks.js | 4 +- .../react-devtools-core/src/standalone.js | 4 +- packages/react-devtools-extensions/flow.js | 2 +- packages/react-devtools-shared/package.json | 1 + .../src/__tests__/storeOwners-test.js | 2 +- .../src/__tests__/storeSerializer.js | 72 +- .../setupNativeStyleEditor.js | 7 +- .../src/backend/ReactDebugHooks.js | 636 ------------------ .../src/backend/agent.js | 2 +- .../src/backend/legacy/renderer.js | 66 +- .../src/backend/renderer.js | 7 +- .../src/backend/types.js | 9 - .../src/backend/views/Highlighter/index.js | 1 + packages/react-devtools-shared/src/bridge.js | 2 +- .../src/devtools/ProfilerStore.js | 2 +- .../src/devtools/store.js | 4 +- .../src/devtools/utils.js | 74 ++ .../devtools/views/Components/HooksTree.js | 2 +- .../Components/InspectedElementContext.js | 2 +- .../src/devtools/views/Components/types.js | 3 +- .../src/devtools/views/utils.js | 2 +- .../react-devtools-shared/src/hydration.js | 4 +- yarn.lock | 5 + 23 files changed, 146 insertions(+), 767 deletions(-) delete mode 100644 packages/react-devtools-shared/src/backend/ReactDebugHooks.js create mode 100644 packages/react-devtools-shared/src/devtools/utils.js diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index c4a65ac5ba37d..994745b8a75ae 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -253,14 +253,14 @@ const Dispatcher: DispatcherType = { // Inspect -type HooksNode = { +export type HooksNode = { id: number | null, isStateEditable: boolean, name: string, value: mixed, subHooks: Array, }; -type HooksTree = Array; +export type HooksTree = Array; // Don't assume // diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index 23d5424fec53c..67d1dae775cc5 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -300,7 +300,9 @@ function startServer(port?: number = 8097) { close: function() { connected = null; onDisconnected(); - clearTimeout(startServerTimeoutID); + if (startServerTimeoutID !== null) { + clearTimeout(startServerTimeoutID); + } server.close(); httpServer.close(); }, diff --git a/packages/react-devtools-extensions/flow.js b/packages/react-devtools-extensions/flow.js index dc0c5759006b4..9ffd20656336b 100644 --- a/packages/react-devtools-extensions/flow.js +++ b/packages/react-devtools-extensions/flow.js @@ -1,6 +1,6 @@ // @flow -declare module 'events' { +declare module 'node-events' { declare class EventEmitter { addListener>( event: Event, diff --git a/packages/react-devtools-shared/package.json b/packages/react-devtools-shared/package.json index 2ba9cf60125b4..7515050e950ab 100644 --- a/packages/react-devtools-shared/package.json +++ b/packages/react-devtools-shared/package.json @@ -8,6 +8,7 @@ "clipboard-js": "^0.3.6", "lodash.throttle": "^4.1.1", "memoize-one": "^3.1.1", + "node-events": "npm:events@^3.0.0", "react-virtualized-auto-sizer": "^1.0.2" } } diff --git a/packages/react-devtools-shared/src/__tests__/storeOwners-test.js b/packages/react-devtools-shared/src/__tests__/storeOwners-test.js index 9ed0e53fdcc04..51788f9462dc6 100644 --- a/packages/react-devtools-shared/src/__tests__/storeOwners-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeOwners-test.js @@ -1,6 +1,6 @@ // @flow -const { printOwnersList } = require('./storeSerializer'); +const { printOwnersList } = require('../devtools/utils'); describe('Store owners list', () => { let React; diff --git a/packages/react-devtools-shared/src/__tests__/storeSerializer.js b/packages/react-devtools-shared/src/__tests__/storeSerializer.js index 1dde4bc7a0809..e59d5b057abb8 100644 --- a/packages/react-devtools-shared/src/__tests__/storeSerializer.js +++ b/packages/react-devtools-shared/src/__tests__/storeSerializer.js @@ -1,3 +1,5 @@ +import { printStore } from 'react-devtools-shared/src/devtools/utils' + // test() is part of Jest's serializer API export function test(maybeStore) { // It's important to lazy-require the Store rather than imported at the head of the module. @@ -11,74 +13,6 @@ export function print(store, serialize, indent) { return printStore(store); } -export function printElement(element, includeWeight = false) { - let prefix = ' '; - if (element.children.length > 0) { - prefix = element.isCollapsed ? '▸' : '▾'; - } - - let key = ''; - if (element.key !== null) { - key = ` key="${element.key}"`; - } - - let hocs = ''; - if (element.hocDisplayNames !== null) { - hocs = ` [${element.hocDisplayNames.join('][')}]`; - } - - let suffix = ''; - if (includeWeight) { - suffix = ` (${element.isCollapsed ? 1 : element.weight})`; - } - - return `${' '.repeat(element.depth + 1)}${prefix} <${element.displayName || - 'null'}${key}>${hocs}${suffix}`; -} - -export function printOwnersList(elements, includeWeight = false) { - return elements - .map(element => printElement(element, includeWeight)) - .join('\n'); -} - // Used for Jest snapshot testing. // May also be useful for visually debugging the tree, so it lives on the Store. -export function printStore(store, includeWeight = false) { - const snapshotLines = []; - - let rootWeight = 0; - - store.roots.forEach(rootID => { - const { weight } = store.getElementByID(rootID); - - snapshotLines.push('[root]' + (includeWeight ? ` (${weight})` : '')); - - for (let i = rootWeight; i < rootWeight + weight; i++) { - const element = store.getElementAtIndex(i); - - if (element == null) { - throw Error(`Could not find element at index ${i}`); - } - - snapshotLines.push(printElement(element, includeWeight)); - } - - rootWeight += weight; - }); - - // Make sure the pretty-printed test align with the Store's reported number of total rows. - if (rootWeight !== store.numElements) { - throw Error( - `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${ - store.numElements - })` - ); - } - - // If roots have been unmounted, verify that they've been removed from maps. - // This helps ensure the Store doesn't leak memory. - store.assertExpectedRootMapSizes(); - - return snapshotLines.join('\n'); -} +export { printStore }; diff --git a/packages/react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor.js b/packages/react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor.js index 81b7f9d71cdea..7568b6cc45459 100644 --- a/packages/react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor.js +++ b/packages/react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor.js @@ -7,7 +7,7 @@ import type { BackendBridge } from 'react-devtools-shared/src/bridge'; import type { RendererID } from '../types'; import type { StyleAndLayout } from './types'; -export type ResolveNativeStyle = (stylesheetID: number) => ?Object; +export type ResolveNativeStyle = (stylesheetID: any) => ?Object; export default function setupNativeStyleEditor( bridge: BackendBridge, @@ -136,9 +136,8 @@ function measureStyle( ); return; } - const margin = resolveBoxStyle('margin', resolvedStyle) || EMPTY_BOX_STYLE; - const padding = - resolveBoxStyle('padding', resolvedStyle) || EMPTY_BOX_STYLE; + const margin = resolvedStyle != null && resolveBoxStyle('margin', resolvedStyle) || EMPTY_BOX_STYLE; + const padding = resolvedStyle != null && resolveBoxStyle('padding', resolvedStyle) || EMPTY_BOX_STYLE; bridge.send( 'NativeStyleEditor_styleAndLayout', ({ diff --git a/packages/react-devtools-shared/src/backend/ReactDebugHooks.js b/packages/react-devtools-shared/src/backend/ReactDebugHooks.js deleted file mode 100644 index 6baaa2530031b..0000000000000 --- a/packages/react-devtools-shared/src/backend/ReactDebugHooks.js +++ /dev/null @@ -1,636 +0,0 @@ -// @flow - -// This file was forked from the React GitHub repo: -// https://github.com/facebook/react/blob/master/packages/react-debug-tools/src/ReactDebugHooks.js -// It has been modified slightly though to account for "shared" imports and different lint configs. -// I've also removed some of the Flow types that don't exist in DevTools. -// TODO Remove this fork and use the NPM version of this package once it's released. - -import ErrorStackParser from 'error-stack-parser'; - -import type { - ReactContext, - ReactEventResponder, - ReactEventResponderListener, -} from './types'; - -type Fiber = any; -type Hook = any; - -// HACK: These values are copied from attachRendererFiber -// In the future, the react-debug-hooks package will be published to NPM, -// and be locked to a specific range of react versions -// For now we are just hard-coding the current/latest versions. -const ContextProvider = 10; -const ForwardRef = 11; -const FunctionComponent = 0; -const SimpleMemoComponent = 15; - -// Used to track hooks called during a render - -type HookLogEntry = { - primitive: string, - stackError: Error, - value: mixed, -}; - -let hookLog: Array = []; - -// Primitives - -type BasicStateAction = (S => S) | S; - -type Dispatch = A => void; - -let primitiveStackCache: null | Map> = null; - -function getPrimitiveStackCache(): Map> { - // This initializes a cache of all primitive hooks so that the top - // most stack frames added by calling the primitive hook can be removed. - if (primitiveStackCache === null) { - const cache = new Map(); - let readHookLog; - try { - // Use all hooks here to add them to the hook log. - Dispatcher.useContext(({ _currentValue: null }: any)); - Dispatcher.useState(null); - Dispatcher.useReducer((s, a) => s, null); - Dispatcher.useRef(null); - Dispatcher.useLayoutEffect(() => {}); - Dispatcher.useEffect(() => {}); - Dispatcher.useImperativeHandle(undefined, () => null); - Dispatcher.useDebugValue(null); - Dispatcher.useCallback(() => {}); - Dispatcher.useMemo(() => null); - } finally { - readHookLog = hookLog; - hookLog = []; - } - for (let i = 0; i < readHookLog.length; i++) { - const hook = readHookLog[i]; - cache.set(hook.primitive, ErrorStackParser.parse(hook.stackError)); - } - primitiveStackCache = cache; - } - return primitiveStackCache; -} - -let currentHook: null | Hook = null; -function nextHook(): null | Hook { - const hook = currentHook; - if (hook !== null) { - currentHook = hook.next; - } - return hook; -} - -function readContext( - context: ReactContext, - observedBits: void | number | boolean -): T { - // For now we don't expose readContext usage in the hooks debugging info. - return context._currentValue; -} - -function useContext( - context: ReactContext, - observedBits: void | number | boolean -): T { - hookLog.push({ - primitive: 'Context', - stackError: new Error(), - value: context._currentValue, - }); - return context._currentValue; -} - -function useState( - initialState: (() => S) | S -): [S, Dispatch>] { - const hook = nextHook(); - const state: S = - hook !== null - ? hook.memoizedState - : typeof initialState === 'function' - ? (initialState: any)() - : initialState; - hookLog.push({ primitive: 'State', stackError: new Error(), value: state }); - return [state, (action: BasicStateAction) => {}]; -} - -function useReducer( - reducer: (S, A) => S, - initialArg: I, - init?: I => S -): [S, Dispatch] { - const hook = nextHook(); - let state; - if (hook !== null) { - state = hook.memoizedState; - } else { - state = init !== undefined ? init(initialArg) : ((initialArg: any): S); - } - hookLog.push({ - primitive: 'Reducer', - stackError: new Error(), - value: state, - }); - return [state, (action: A) => {}]; -} - -function useRef(initialValue: T): { current: T } { - const hook = nextHook(); - const ref = hook !== null ? hook.memoizedState : { current: initialValue }; - hookLog.push({ - primitive: 'Ref', - stackError: new Error(), - value: ref.current, - }); - return ref; -} - -function useLayoutEffect( - create: () => (() => void) | void, - inputs: Array | void | null -): void { - nextHook(); - hookLog.push({ - primitive: 'LayoutEffect', - stackError: new Error(), - value: create, - }); -} - -function useEffect( - create: () => (() => void) | void, - inputs: Array | void | null -): void { - nextHook(); - hookLog.push({ primitive: 'Effect', stackError: new Error(), value: create }); -} - -function useImperativeHandle( - ref: { current: T | null } | ((inst: T | null) => mixed) | null | void, - create: () => T, - inputs: Array | void | null -): void { - nextHook(); - // We don't actually store the instance anywhere if there is no ref callback - // and if there is a ref callback it might not store it but if it does we - // have no way of knowing where. So let's only enable introspection of the - // ref itself if it is using the object form. - let instance = undefined; - if (ref !== null && typeof ref === 'object') { - instance = ref.current; - } - hookLog.push({ - primitive: 'ImperativeHandle', - stackError: new Error(), - value: instance, - }); -} - -function useDebugValue(value: any, formatterFn: ?(value: any) => any) { - hookLog.push({ - primitive: 'DebugValue', - stackError: new Error(), - value: typeof formatterFn === 'function' ? formatterFn(value) : value, - }); -} - -function useCallback(callback: T, inputs: Array | void | null): T { - const hook = nextHook(); - hookLog.push({ - primitive: 'Callback', - stackError: new Error(), - value: hook !== null ? hook.memoizedState[0] : callback, - }); - return callback; -} - -function useMemo( - nextCreate: () => T, - inputs: Array | void | null -): T { - const hook = nextHook(); - const value = hook !== null ? hook.memoizedState[0] : nextCreate(); - hookLog.push({ primitive: 'Memo', stackError: new Error(), value }); - return value; -} - -function useResponder( - responder: ReactEventResponder, - listenerProps: Object -): ReactEventResponderListener { - // Don't put the actual event responder object in, just its displayName - const value = { - responder: responder.displayName || 'EventResponder', - props: listenerProps, - }; - hookLog.push({ primitive: 'Responder', stackError: new Error(), value }); - return { - responder, - props: listenerProps, - }; -} - -const Dispatcher = { - readContext, - useCallback, - useContext, - useEffect, - useImperativeHandle, - useDebugValue, - useLayoutEffect, - useMemo, - useReducer, - useRef, - useState, - useResponder, -}; - -// Inspect - -type ReactCurrentDispatcher = { - current: null | typeof Dispatcher, -}; - -type HooksNode = { - id: number | null, - isStateEditable: boolean, - name: string, - value: mixed, - subHooks: Array, -}; -type HooksTree = Array; - -// Don't assume -// -// We can't assume that stack frames are nth steps away from anything. -// E.g. we can't assume that the root call shares all frames with the stack -// of a hook call. A simple way to demonstrate this is wrapping `new Error()` -// in a wrapper constructor like a polyfill. That'll add an extra frame. -// Similar things can happen with the call to the dispatcher. The top frame -// may not be the primitive. Likewise the primitive can have fewer stack frames -// such as when a call to useState got inlined to use dispatcher.useState. -// -// We also can't assume that the last frame of the root call is the same -// frame as the last frame of the hook call because long stack traces can be -// truncated to a stack trace limit. - -let mostLikelyAncestorIndex = 0; - -function findSharedIndex(hookStack, rootStack, rootIndex) { - const source = rootStack[rootIndex].source; - hookSearch: for (let i = 0; i < hookStack.length; i++) { - if (hookStack[i].source === source) { - // This looks like a match. Validate that the rest of both stack match up. - for ( - let a = rootIndex + 1, b = i + 1; - a < rootStack.length && b < hookStack.length; - a++, b++ - ) { - if (hookStack[b].source !== rootStack[a].source) { - // If not, give up and try a different match. - continue hookSearch; - } - } - return i; - } - } - return -1; -} - -function findCommonAncestorIndex(rootStack, hookStack) { - let rootIndex = findSharedIndex( - hookStack, - rootStack, - mostLikelyAncestorIndex - ); - if (rootIndex !== -1) { - return rootIndex; - } - // If the most likely one wasn't a hit, try any other frame to see if it is shared. - // If that takes more than 5 frames, something probably went wrong. - for (let i = 0; i < rootStack.length && i < 5; i++) { - rootIndex = findSharedIndex(hookStack, rootStack, i); - if (rootIndex !== -1) { - mostLikelyAncestorIndex = i; - return rootIndex; - } - } - return -1; -} - -function isReactWrapper(functionName, primitiveName) { - if (!functionName) { - return false; - } - const expectedPrimitiveName = 'use' + primitiveName; - if (functionName.length < expectedPrimitiveName.length) { - return false; - } - return ( - functionName.lastIndexOf(expectedPrimitiveName) === - functionName.length - expectedPrimitiveName.length - ); -} - -function findPrimitiveIndex(hookStack, hook) { - const stackCache = getPrimitiveStackCache(); - const primitiveStack = stackCache.get(hook.primitive); - if (primitiveStack === undefined) { - return -1; - } - for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) { - if (primitiveStack[i].source !== hookStack[i].source) { - // If the next two frames are functions called `useX` then we assume that they're part of the - // wrappers that the React packager or other packages adds around the dispatcher. - if ( - i < hookStack.length - 1 && - isReactWrapper(hookStack[i].functionName, hook.primitive) - ) { - i++; - } - if ( - i < hookStack.length - 1 && - isReactWrapper(hookStack[i].functionName, hook.primitive) - ) { - i++; - } - return i; - } - } - return -1; -} - -function parseTrimmedStack(rootStack, hook) { - // Get the stack trace between the primitive hook function and - // the root function call. I.e. the stack frames of custom hooks. - const hookStack = ErrorStackParser.parse(hook.stackError); - const rootIndex = findCommonAncestorIndex(rootStack, hookStack); - const primitiveIndex = findPrimitiveIndex(hookStack, hook); - if ( - rootIndex === -1 || - primitiveIndex === -1 || - rootIndex - primitiveIndex < 2 - ) { - // Something went wrong. Give up. - return null; - } - return hookStack.slice(primitiveIndex, rootIndex - 1); -} - -function parseCustomHookName(functionName: void | string): string { - if (!functionName) { - return ''; - } - let startIndex = functionName.lastIndexOf('.'); - if (startIndex === -1) { - startIndex = 0; - } - if (functionName.substr(startIndex, 3) === 'use') { - startIndex += 3; - } - return functionName.substr(startIndex); -} - -function buildTree(rootStack, readHookLog): HooksTree { - const rootChildren = []; - let prevStack = null; - let levelChildren = rootChildren; - let nativeHookID = 0; - const stackOfChildren = []; - for (let i = 0; i < readHookLog.length; i++) { - const hook = readHookLog[i]; - const stack = parseTrimmedStack(rootStack, hook); - if (stack !== null) { - // Note: The indices 0 <= n < length-1 will contain the names. - // The indices 1 <= n < length will contain the source locations. - // That's why we get the name from n - 1 and don't check the source - // of index 0. - let commonSteps = 0; - if (prevStack !== null) { - // Compare the current level's stack to the new stack. - while (commonSteps < stack.length && commonSteps < prevStack.length) { - const stackSource = stack[stack.length - commonSteps - 1].source; - const prevSource = - prevStack[prevStack.length - commonSteps - 1].source; - if (stackSource !== prevSource) { - break; - } - commonSteps++; - } - // Pop back the stack as many steps as were not common. - for (let j = prevStack.length - 1; j > commonSteps; j--) { - levelChildren = stackOfChildren.pop(); - } - } - // The remaining part of the new stack are custom hooks. Push them - // to the tree. - for (let j = stack.length - commonSteps - 1; j >= 1; j--) { - const children = []; - levelChildren.push({ - id: null, - isStateEditable: false, - name: parseCustomHookName(stack[j - 1].functionName), - value: undefined, - subHooks: children, - }); - stackOfChildren.push(levelChildren); - levelChildren = children; - } - prevStack = stack; - } - const { primitive } = hook; - - // For now, the "id" of stateful hooks is just the stateful hook index. - // Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue). - const id = - primitive === 'Context' || primitive === 'DebugValue' - ? null - : nativeHookID++; - - // For the time being, only State and Reducer hooks support runtime overrides. - const isStateEditable = primitive === 'Reducer' || primitive === 'State'; - - levelChildren.push({ - id, - isStateEditable, - name: primitive, - value: hook.value, - subHooks: [], - }); - } - - // Associate custom hook values (useDebugValue() hook entries) with the correct hooks. - processDebugValues(rootChildren, null); - - return rootChildren; -} - -// Custom hooks support user-configurable labels (via the special useDebugValue() hook). -// That hook adds user-provided values to the hooks tree, -// but these values aren't intended to appear alongside of the other hooks. -// Instead they should be attributed to their parent custom hook. -// This method walks the tree and assigns debug values to their custom hook owners. -function processDebugValues( - hooksTree: HooksTree, - parentHooksNode: HooksNode | null -): void { - const debugValueHooksNodes: Array = []; - - for (let i = 0; i < hooksTree.length; i++) { - const hooksNode = hooksTree[i]; - if (hooksNode.name === 'DebugValue' && hooksNode.subHooks.length === 0) { - hooksTree.splice(i, 1); - i--; - debugValueHooksNodes.push(hooksNode); - } else { - processDebugValues(hooksNode.subHooks, hooksNode); - } - } - - // Bubble debug value labels to their custom hook owner. - // If there is no parent hook, just ignore them for now. - // (We may warn about this in the future.) - if (parentHooksNode !== null) { - if (debugValueHooksNodes.length === 1) { - parentHooksNode.value = debugValueHooksNodes[0].value; - } else if (debugValueHooksNodes.length > 1) { - parentHooksNode.value = debugValueHooksNodes.map(({ value }) => value); - } - } -} - -export function inspectHooks( - renderFunction: Props => React$Node, - props: Props, - currentDispatcher: ReactCurrentDispatcher -): HooksTree { - // DevTools will pass the current renderer's injected dispatcher. - // Other apps might compile debug hooks as part of their app though. - //if (currentDispatcher == null) { - //currentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; - //} - - const previousDispatcher = currentDispatcher.current; - let readHookLog; - currentDispatcher.current = Dispatcher; - let ancestorStackError; - try { - ancestorStackError = new Error(); - renderFunction(props); - } finally { - readHookLog = hookLog; - hookLog = []; - currentDispatcher.current = previousDispatcher; - } - const rootStack = ErrorStackParser.parse(ancestorStackError); - return buildTree(rootStack, readHookLog); -} - -function setupContexts(contextMap: Map, fiber: Fiber) { - let current = fiber; - while (current) { - if (current.tag === ContextProvider) { - const providerType: any = current.type; - const context: any = providerType._context; - if (!contextMap.has(context)) { - // Store the current value that we're going to restore later. - contextMap.set(context, context._currentValue); - // Set the inner most provider value on the context. - context._currentValue = current.memoizedProps.value; - } - } - current = current.return; - } -} - -function restoreContexts(contextMap: Map) { - contextMap.forEach((value, context) => (context._currentValue = value)); -} - -function inspectHooksOfForwardRef( - renderFunction: (Props, Ref) => React$Node, - props: Props, - ref: Ref, - currentDispatcher: ReactCurrentDispatcher -): HooksTree { - const previousDispatcher = currentDispatcher.current; - let readHookLog; - currentDispatcher.current = Dispatcher; - let ancestorStackError; - try { - ancestorStackError = new Error(); - renderFunction(props, ref); - } finally { - readHookLog = hookLog; - hookLog = []; - currentDispatcher.current = previousDispatcher; - } - const rootStack = ErrorStackParser.parse(ancestorStackError); - return buildTree(rootStack, readHookLog); -} - -function resolveDefaultProps(Component, baseProps) { - if (Component && Component.defaultProps) { - // Resolve default props. Taken from ReactElement - const props = Object.assign({}, baseProps); - const defaultProps = Component.defaultProps; - for (const propName in defaultProps) { - if (props[propName] === undefined) { - props[propName] = defaultProps[propName]; - } - } - return props; - } - return baseProps; -} - -export function inspectHooksOfFiber( - fiber: Fiber, - currentDispatcher: ReactCurrentDispatcher -) { - // DevTools will pass the current renderer's injected dispatcher. - // Other apps might compile debug hooks as part of their app though. - //if (currentDispatcher == null) { - //currentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; - //} - - if ( - fiber.tag !== FunctionComponent && - fiber.tag !== SimpleMemoComponent && - fiber.tag !== ForwardRef - ) { - throw new Error( - 'Unknown Fiber. Needs to be a function component to inspect hooks.' - ); - } - // Warm up the cache so that it doesn't consume the currentHook. - getPrimitiveStackCache(); - const type = fiber.type; - let props = fiber.memoizedProps; - if (type !== fiber.elementType) { - props = resolveDefaultProps(type, props); - } - // Set up the current hook so that we can step through and read the - // current state from them. - currentHook = (fiber.memoizedState: Hook); - const contextMap = new Map(); - try { - setupContexts(contextMap, fiber); - if (fiber.tag === ForwardRef) { - return inspectHooksOfForwardRef( - type.render, - props, - fiber.ref, - currentDispatcher - ); - } - return inspectHooks(type, props, currentDispatcher); - } finally { - currentHook = null; - restoreContexts(contextMap); - } -} diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 362518b8c7b5c..eb72e0bfd3e01 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -1,6 +1,6 @@ // @flow -import EventEmitter from 'events'; +import EventEmitter from 'node-events'; import throttle from 'lodash.throttle'; import { SESSION_STORAGE_LAST_SELECTION_KEY, diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 18d3513b121ba..4fff97a5fbf0a 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -407,17 +407,18 @@ export function attach( parentID: number, rootID: number ) { - const internalInstance = idToInternalInstanceMap.get(id); - if (__DEBUG__) { console.group('crawlAndRecordInitialMounts() id:', id); } - internalInstanceToRootIDMap.set(internalInstance, rootID); - recordMount(internalInstance, id, parentID); - getChildren(internalInstance).forEach(child => - crawlAndRecordInitialMounts(getID(child), id, rootID) - ); + const internalInstance = idToInternalInstanceMap.get(id); + if (internalInstance != null) { + internalInstanceToRootIDMap.set(internalInstance, rootID); + recordMount(internalInstance, id, parentID); + getChildren(internalInstance).forEach(child => + crawlAndRecordInitialMounts(getID(child), id, rootID) + ); + } if (__DEBUG__) { console.groupEnd(); @@ -686,7 +687,12 @@ export function attach( function inspectElementRaw(id: number): InspectedElement | null { const internalInstance = idToInternalInstanceMap.get(id); - const displayName = getData(internalInstance).displayName; + + if (internalInstance == null) { + return null; + } + + const { displayName } = getData(internalInstance); const type = getElementType(internalInstance); let context = null; @@ -695,33 +701,31 @@ export function attach( let state = null; let source = null; - if (internalInstance != null) { - const element = internalInstance._currentElement; - if (element !== null) { - props = element.props; - source = element._source != null ? element._source : null; - - let owner = element._owner; - if (owner) { - owners = []; - while (owner != null) { - owners.push({ - displayName: getData(owner).displayName || 'Unknown', - id: getID(owner), - type: getElementType(owner), - }); - if (owner._currentElement) { - owner = owner._currentElement._owner; - } + const element = internalInstance._currentElement; + if (element !== null) { + props = element.props; + source = element._source != null ? element._source : null; + + let owner = element._owner; + if (owner) { + owners = []; + while (owner != null) { + owners.push({ + displayName: getData(owner).displayName || 'Unknown', + id: getID(owner), + type: getElementType(owner), + }); + if (owner._currentElement) { + owner = owner._currentElement._owner; } } } + } - const publicInstance = internalInstance._instance; - if (publicInstance != null) { - context = publicInstance.context || null; - state = publicInstance.state || null; - } + const publicInstance = internalInstance._instance; + if (publicInstance != null) { + context = publicInstance.context || null; + state = publicInstance.state || null; } return { diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 44f00a9f02ba4..2effb3344562a 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -36,7 +36,7 @@ import { TREE_OPERATION_REORDER_CHILDREN, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, } from '../constants'; -import { inspectHooksOfFiber } from './ReactDebugHooks'; +import { inspectHooksOfFiber } from 'react-debug-tools'; import { patch as patchConsole, registerRenderer as registerRendererWithConsole, @@ -285,6 +285,7 @@ export function getInternalReactConstants( const symbolOrNumber = typeof type === 'object' && type !== null ? type.$$typeof : type; + // $FlowFixMe Flow doesn't know about typeof "symbol" return typeof symbolOrNumber === 'symbol' ? symbolOrNumber.toString() : symbolOrNumber; @@ -2746,7 +2747,9 @@ export function attach( } } const fiber = idToFiberMap.get(id); - scheduleUpdate(fiber); + if (fiber != null) { + scheduleUpdate(fiber); + } } // Remember if we're trying to restore the selection after reload. diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 498d1ef8fc159..2a08164decc21 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -343,15 +343,6 @@ export type DevToolsHook = { ) => void, }; -export type HooksNode = { - id: number, - isStateEditable: boolean, - name: string, - value: mixed, - subHooks: Array, -}; -export type HooksTree = Array; - export type ReactEventResponder = { $$typeof: Symbol | number, displayName: string, diff --git a/packages/react-devtools-shared/src/backend/views/Highlighter/index.js b/packages/react-devtools-shared/src/backend/views/Highlighter/index.js index ca91a95858592..b8a561fcdeac2 100644 --- a/packages/react-devtools-shared/src/backend/views/Highlighter/index.js +++ b/packages/react-devtools-shared/src/backend/views/Highlighter/index.js @@ -106,6 +106,7 @@ export default function setupHighlighter( if (scrollIntoView && typeof node.scrollIntoView === 'function') { // If the node isn't visible show it before highlighting it. // We may want to reconsider this; it might be a little disruptive. + // $FlowFixMe Flow only knows about 'start' | 'end' node.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index a4ac0ecea7567..c0396cb1fb138 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -1,6 +1,6 @@ // @flow -import EventEmitter from 'events'; +import EventEmitter from 'node-events'; import type { ComponentFilter, Wall } from './types'; import type { diff --git a/packages/react-devtools-shared/src/devtools/ProfilerStore.js b/packages/react-devtools-shared/src/devtools/ProfilerStore.js index 98a78470bd225..e60233d8a17bb 100644 --- a/packages/react-devtools-shared/src/devtools/ProfilerStore.js +++ b/packages/react-devtools-shared/src/devtools/ProfilerStore.js @@ -1,6 +1,6 @@ // @flow -import EventEmitter from 'events'; +import EventEmitter from 'node-events'; import { prepareProfilingDataFrontendFromBackendAndStore } from './views/Profiler/utils'; import ProfilingCache from './ProfilingCache'; import Store from './store'; diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 9492a4dc7c7b6..6dc4fd2334997 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -1,6 +1,6 @@ // @flow -import EventEmitter from 'events'; +import EventEmitter from 'node-events'; import { inspect } from 'util'; import { TREE_OPERATION_ADD, @@ -18,7 +18,7 @@ import { } from '../utils'; import { localStorageGetItem, localStorageSetItem } from '../storage'; import { __DEBUG__ } from '../constants'; -import { printStore } from '../__tests__/storeSerializer'; +import { printStore } from './utils'; import ProfilerStore from './ProfilerStore'; import type { Element } from './views/Components/types'; diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js new file mode 100644 index 0000000000000..2f70961ef789a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/utils.js @@ -0,0 +1,74 @@ +// @flow + +import type { Element } from './views/Components/types'; +import type Store from './store'; + +export function printElement(element: Element, includeWeight: boolean = false) { + let prefix = ' '; + if (element.children.length > 0) { + prefix = element.isCollapsed ? '▸' : '▾'; + } + + let key = ''; + if (element.key !== null) { + key = ` key="${element.key}"`; + } + + let hocs = ''; + if (element.hocDisplayNames !== null) { + hocs = ` [${element.hocDisplayNames.join('][')}]`; + } + + let suffix = ''; + if (includeWeight) { + suffix = ` (${element.isCollapsed ? 1 : element.weight})`; + } + + return `${' '.repeat(element.depth + 1)}${prefix} <${element.displayName || + 'null'}${key}>${hocs}${suffix}`; +} + +export function printOwnersList(elements: Array, includeWeight: boolean = false) { + return elements + .map(element => printElement(element, includeWeight)) + .join('\n'); +} + +export function printStore(store: Store, includeWeight: boolean = false) { + const snapshotLines = []; + + let rootWeight = 0; + + store.roots.forEach(rootID => { + const { weight } = ((store.getElementByID(rootID): any): Element); + + snapshotLines.push('[root]' + (includeWeight ? ` (${weight})` : '')); + + for (let i = rootWeight; i < rootWeight + weight; i++) { + const element = store.getElementAtIndex(i); + + if (element == null) { + throw Error(`Could not find element at index ${i}`); + } + + snapshotLines.push(printElement(element, includeWeight)); + } + + rootWeight += weight; + }); + + // Make sure the pretty-printed test align with the Store's reported number of total rows. + if (rootWeight !== store.numElements) { + throw Error( + `Inconsistent Store state. Individual root weights (${rootWeight}) do not match total weight (${ + store.numElements + })` + ); + } + + // If roots have been unmounted, verify that they've been removed from maps. + // This helps ensure the Store doesn't leak memory. + store.assertExpectedRootMapSizes(); + + return snapshotLines.join('\n'); +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js index 84c9a1929e6f9..c35d0a743e3d7 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js @@ -14,7 +14,7 @@ import styles from './HooksTree.css'; import { meta } from '../../../hydration'; import type { InspectPath } from './SelectedElement'; -import type { HooksNode, HooksTree } from 'react-devtools-shared/src/backend/types'; +import type { HooksNode, HooksTree } from 'react-debug-tools/src/ReactDebugHooks'; type HooksTreeViewProps = {| canEditHooks: boolean, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js index df92a6ffd5a5c..951a34b34f2fc 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js @@ -285,7 +285,7 @@ function InspectedElementContextController({ children }: Props) { } function hydrateHelper( - dehydratedData: DehydratedData | null, + dehydratedData: any | null, path?: Array ): Object | null { if (dehydratedData !== null) { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/types.js b/packages/react-devtools-shared/src/devtools/views/Components/types.js index a641e8d40df7f..57cc31041629e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/types.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/types.js @@ -1,5 +1,6 @@ // @flow +import type { Dehydrated } from 'react-devtools-shared/src/hydration'; import type { ElementType } from 'react-devtools-shared/src/types'; // Each element on the frontend corresponds to a Fiber on the backend. @@ -83,5 +84,5 @@ export type InspectedElement = {| export type DehydratedData = {| cleaned: Array>, - data: Object, + data: string | Dehydrated | Array | { [key: string]: string | Dehydrated }, |}; diff --git a/packages/react-devtools-shared/src/devtools/views/utils.js b/packages/react-devtools-shared/src/devtools/views/utils.js index 734ac5effbcef..db41f09ca9adc 100644 --- a/packages/react-devtools-shared/src/devtools/views/utils.js +++ b/packages/react-devtools-shared/src/devtools/views/utils.js @@ -3,7 +3,7 @@ import escapeStringRegExp from 'escape-string-regexp'; import { meta } from '../../hydration'; -import type { HooksTree } from 'react-devtools-shared/src/backend/types'; +import type { HooksTree } from 'react-debug-tools/src/ReactDebugHooks'; export function createRegExp(string: string): RegExp { // Allow /regex/ syntax with optional last / diff --git a/packages/react-devtools-shared/src/hydration.js b/packages/react-devtools-shared/src/hydration.js index ac94f73d1c93f..4fd5bcfd32cab 100644 --- a/packages/react-devtools-shared/src/hydration.js +++ b/packages/react-devtools-shared/src/hydration.js @@ -27,7 +27,7 @@ export const meta = { type: Symbol('type'), }; -type Dehydrated = {| +export type Dehydrated = {| inspectable: boolean, name: string | null, readonly?: boolean, @@ -176,7 +176,7 @@ export function dehydrate( path: Array, isPathWhitelisted: (path: Array) => boolean, level?: number = 0 -): string | Dehydrated | { [key: string]: string | Dehydrated } { +): string | Dehydrated | Array | { [key: string]: string | Dehydrated } { const type = getDataType(data); switch (type) { diff --git a/yarn.lock b/yarn.lock index d6fc6a832c148..5dcaac0211133 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4643,6 +4643,11 @@ node-cleanup@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c" +"node-events@npm:events@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" + integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== + node-fetch@^1.0.1, node-fetch@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"