diff --git a/package.json b/package.json index ebcf881ef1680..6a6ee8db29745 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "create-react-class": "^15.6.3", "cross-env": "^5.1.1", "danger": "^3.0.4", + "error-stack-parser": "^2.0.2", "eslint": "^4.1.0", "eslint-config-fbjs": "^1.1.1", "eslint-plugin-babel": "^3.3.0", diff --git a/packages/react-debug-tools/README.md b/packages/react-debug-tools/README.md new file mode 100644 index 0000000000000..5d8c0ac872943 --- /dev/null +++ b/packages/react-debug-tools/README.md @@ -0,0 +1,7 @@ +# react-debug-tools + +This is an experimental package for debugging React renderers. + +**Its API is not as stable as that of React, React Native, or React DOM, and does not follow the common versioning scheme.** + +**Use it at your own risk.** diff --git a/packages/react-debug-tools/index.js b/packages/react-debug-tools/index.js new file mode 100644 index 0000000000000..c381393770b5f --- /dev/null +++ b/packages/react-debug-tools/index.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const ReactDebugTools = require('./src/ReactDebugTools'); + +// This is hacky but makes it work with both Rollup and Jest. +module.exports = ReactDebugTools.default || ReactDebugTools; diff --git a/packages/react-debug-tools/npm/index.js b/packages/react-debug-tools/npm/index.js new file mode 100644 index 0000000000000..436e6ee2019f1 --- /dev/null +++ b/packages/react-debug-tools/npm/index.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-debug-tools.production.min.js'); +} else { + module.exports = require('./cjs/react-debug-tools.development.js'); +} diff --git a/packages/react-debug-tools/package.json b/packages/react-debug-tools/package.json new file mode 100644 index 0000000000000..d58d35b12de3f --- /dev/null +++ b/packages/react-debug-tools/package.json @@ -0,0 +1,28 @@ +{ + "name": "react-debug-tools", + "description": "React package for debugging React trees.", + "version": "0.16.0", + "keywords": [ + "react" + ], + "homepage": "https://reactjs.org/", + "bugs": "https://github.com/facebook/react/issues", + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "index.js", + "cjs/" + ], + "main": "index.js", + "repository": "facebook/react", + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^16.0.0" + }, + "dependencies": { + "error-stack-parser": "^2.0.2" + } +} diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js new file mode 100644 index 0000000000000..61d468bd0726b --- /dev/null +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -0,0 +1,530 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactContext, ReactProviderType} from 'shared/ReactTypes'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {Hook} from 'react-reconciler/src/ReactFiberHooks'; + +import ErrorStackParser from 'error-stack-parser'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import { + FunctionComponent, + SimpleMemoComponent, + ContextProvider, + ForwardRef, +} from 'shared/ReactWorkTags'; + +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; + +// 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) { + let 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.useMutationEffect(() => {}); + Dispatcher.useLayoutEffect(() => {}); + Dispatcher.useEffect(() => {}); + Dispatcher.useImperativeMethods(undefined, () => null); + Dispatcher.useCallback(() => {}); + Dispatcher.useMemo(() => null); + } finally { + readHookLog = hookLog; + hookLog = []; + } + for (let i = 0; i < readHookLog.length; i++) { + let hook = readHookLog[i]; + cache.set(hook.primitive, ErrorStackParser.parse(hook.stackError)); + } + primitiveStackCache = cache; + } + return primitiveStackCache; +} + +let currentHook: null | Hook = null; +function nextHook(): null | Hook { + let 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>] { + let hook = nextHook(); + let state: S = + hook !== null + ? hook.memoizedState + : typeof initialState === 'function' + ? initialState() + : initialState; + hookLog.push({primitive: 'State', stackError: new Error(), value: state}); + return [state, (action: BasicStateAction) => {}]; +} + +function useReducer( + reducer: (S, A) => S, + initialState: S, + initialAction: A | void | null, +): [S, Dispatch] { + let hook = nextHook(); + let state = hook !== null ? hook.memoizedState : initialState; + hookLog.push({ + primitive: 'Reducer', + stackError: new Error(), + value: state, + }); + return [state, (action: A) => {}]; +} + +function useRef(initialValue: T): {current: T} { + let hook = nextHook(); + let ref = hook !== null ? hook.memoizedState : {current: initialValue}; + hookLog.push({ + primitive: 'Ref', + stackError: new Error(), + value: ref.current, + }); + return ref; +} + +function useMutationEffect( + create: () => mixed, + inputs: Array | void | null, +): void { + nextHook(); + hookLog.push({ + primitive: 'MutationEffect', + stackError: new Error(), + value: create, + }); +} + +function useLayoutEffect( + create: () => mixed, + inputs: Array | void | null, +): void { + nextHook(); + hookLog.push({ + primitive: 'LayoutEffect', + stackError: new Error(), + value: create, + }); +} + +function useEffect( + create: () => mixed, + inputs: Array | void | null, +): void { + nextHook(); + hookLog.push({primitive: 'Effect', stackError: new Error(), value: create}); +} + +function useImperativeMethods( + 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: 'ImperativeMethods', + stackError: new Error(), + value: instance, + }); +} + +function useCallback(callback: T, inputs: Array | void | null): T { + let 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 { + let hook = nextHook(); + let value = hook !== null ? hook.memoizedState[0] : nextCreate(); + hookLog.push({primitive: 'Memo', stackError: new Error(), value}); + return value; +} + +const Dispatcher = { + readContext, + useCallback, + useContext, + useEffect, + useImperativeMethods, + useLayoutEffect, + useMemo, + useMutationEffect, + useReducer, + useRef, + useState, +}; + +// Inspect + +type HooksNode = { + 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) { + let 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; + } + let expectedPrimitiveName = 'use' + primitiveName; + if (functionName.length < expectedPrimitiveName.length) { + return false; + } + return ( + functionName.lastIndexOf(expectedPrimitiveName) === + functionName.length - expectedPrimitiveName.length + ); +} + +function findPrimitiveIndex(hookStack, hook) { + let stackCache = getPrimitiveStackCache(); + let 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. + let hookStack = ErrorStackParser.parse(hook.stackError); + let rootIndex = findCommonAncestorIndex(rootStack, hookStack); + let 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 { + let rootChildren = []; + let prevStack = null; + let levelChildren = rootChildren; + let stackOfChildren = []; + for (let i = 0; i < readHookLog.length; i++) { + let hook = readHookLog[i]; + let 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) { + let stackSource = stack[stack.length - commonSteps - 1].source; + let 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--) { + let children = []; + levelChildren.push({ + name: parseCustomHookName(stack[j - 1].functionName), + value: undefined, // TODO: Support custom inspectable values. + subHooks: children, + }); + stackOfChildren.push(levelChildren); + levelChildren = children; + } + prevStack = stack; + } + levelChildren.push({ + name: hook.primitive, + value: hook.value, + subHooks: [], + }); + } + return rootChildren; +} + +export function inspectHooks( + renderFunction: Props => React$Node, + props: Props, +): HooksTree { + let previousDispatcher = ReactCurrentOwner.currentDispatcher; + let readHookLog; + ReactCurrentOwner.currentDispatcher = Dispatcher; + let ancestorStackError; + try { + ancestorStackError = new Error(); + renderFunction(props); + } finally { + readHookLog = hookLog; + hookLog = []; + ReactCurrentOwner.currentDispatcher = previousDispatcher; + } + let rootStack = ErrorStackParser.parse(ancestorStackError); + return buildTree(rootStack, readHookLog); +} + +function setupContexts(contextMap: Map, any>, fiber: Fiber) { + let current = fiber; + while (current) { + if (current.tag === ContextProvider) { + const providerType: ReactProviderType = current.type; + const context: ReactContext = 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, any>) { + contextMap.forEach((value, context) => (context._currentValue = value)); +} + +function inspectHooksOfForwardRef( + renderFunction: (Props, Ref) => React$Node, + props: Props, + ref: Ref, +): HooksTree { + let previousDispatcher = ReactCurrentOwner.currentDispatcher; + let readHookLog; + ReactCurrentOwner.currentDispatcher = Dispatcher; + let ancestorStackError; + try { + ancestorStackError = new Error(); + renderFunction(props, ref); + } finally { + readHookLog = hookLog; + hookLog = []; + ReactCurrentOwner.currentDispatcher = previousDispatcher; + } + let 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 (let propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + return props; + } + return baseProps; +} + +export function inspectHooksOfFiber(fiber: Fiber) { + 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(); + let 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); + let contextMap = new Map(); + try { + setupContexts(contextMap, fiber); + if (fiber.tag === ForwardRef) { + return inspectHooksOfForwardRef(type.render, props, fiber.ref); + } + return inspectHooks(type, props); + } finally { + currentHook = null; + restoreContexts(contextMap); + } +} diff --git a/packages/react-debug-tools/src/ReactDebugTools.js b/packages/react-debug-tools/src/ReactDebugTools.js new file mode 100644 index 0000000000000..7f441e0d2fedc --- /dev/null +++ b/packages/react-debug-tools/src/ReactDebugTools.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {inspectHooks, inspectHooksOfFiber} from './ReactDebugHooks'; + +export {inspectHooks, inspectHooksOfFiber}; diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js new file mode 100644 index 0000000000000..ebb3481dfdce2 --- /dev/null +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js @@ -0,0 +1,219 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +'use strict'; + +let React; +let ReactDebugTools; + +describe('ReactHooksInspection', () => { + beforeEach(() => { + jest.resetModules(); + let ReactFeatureFlags = require('shared/ReactFeatureFlags'); + // TODO: Switch this test to non-internal once the flag is on by default. + ReactFeatureFlags.enableHooks = true; + React = require('react'); + ReactDebugTools = require('react-debug-tools'); + }); + + it('should inspect a simple useState hook', () => { + function Foo(props) { + let [state] = React.useState('hello world'); + return
{state}
; + } + let tree = ReactDebugTools.inspectHooks(Foo, {}); + expect(tree).toEqual([ + { + name: 'State', + value: 'hello world', + subHooks: [], + }, + ]); + }); + + it('should inspect a simple custom hook', () => { + function useCustom(value) { + let [state] = React.useState(value); + return state; + } + function Foo(props) { + let value = useCustom('hello world'); + return
{value}
; + } + let tree = ReactDebugTools.inspectHooks(Foo, {}); + expect(tree).toEqual([ + { + name: 'Custom', + value: undefined, + subHooks: [ + { + name: 'State', + value: 'hello world', + subHooks: [], + }, + ], + }, + ]); + }); + + it('should inspect a tree of multiple hooks', () => { + function effect() {} + function useCustom(value) { + let [state] = React.useState(value); + React.useEffect(effect); + return state; + } + function Foo(props) { + let value1 = useCustom('hello'); + let value2 = useCustom('world'); + return ( +
+ {value1} {value2} +
+ ); + } + let tree = ReactDebugTools.inspectHooks(Foo, {}); + expect(tree).toEqual([ + { + name: 'Custom', + value: undefined, + subHooks: [ + { + name: 'State', + subHooks: [], + value: 'hello', + }, + { + name: 'Effect', + subHooks: [], + value: effect, + }, + ], + }, + { + name: 'Custom', + value: undefined, + subHooks: [ + { + name: 'State', + value: 'world', + subHooks: [], + }, + { + name: 'Effect', + value: effect, + subHooks: [], + }, + ], + }, + ]); + }); + + it('should inspect a tree of multiple levels of hooks', () => { + function effect() {} + function useCustom(value) { + let [state] = React.useReducer((s, a) => s, value); + React.useEffect(effect); + return state; + } + function useBar(value) { + let result = useCustom(value); + React.useLayoutEffect(effect); + return result; + } + function useBaz(value) { + React.useMutationEffect(effect); + let result = useCustom(value); + return result; + } + function Foo(props) { + let value1 = useBar('hello'); + let value2 = useBaz('world'); + return ( +
+ {value1} {value2} +
+ ); + } + let tree = ReactDebugTools.inspectHooks(Foo, {}); + expect(tree).toEqual([ + { + name: 'Bar', + value: undefined, + subHooks: [ + { + name: 'Custom', + value: undefined, + subHooks: [ + { + name: 'Reducer', + value: 'hello', + subHooks: [], + }, + { + name: 'Effect', + value: effect, + subHooks: [], + }, + ], + }, + { + name: 'LayoutEffect', + value: effect, + subHooks: [], + }, + ], + }, + { + name: 'Baz', + value: undefined, + subHooks: [ + { + name: 'MutationEffect', + value: effect, + subHooks: [], + }, + { + name: 'Custom', + subHooks: [ + { + name: 'Reducer', + subHooks: [], + value: 'world', + }, + { + name: 'Effect', + subHooks: [], + value: effect, + }, + ], + value: undefined, + }, + ], + }, + ]); + }); + + it('should inspect the default value using the useContext hook', () => { + let MyContext = React.createContext('default'); + function Foo(props) { + let value = React.useContext(MyContext); + return
{value}
; + } + let tree = ReactDebugTools.inspectHooks(Foo, {}); + expect(tree).toEqual([ + { + name: 'Context', + value: 'default', + subHooks: [], + }, + ]); + }); +}); diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js new file mode 100644 index 0000000000000..bcc903457b6e1 --- /dev/null +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js @@ -0,0 +1,247 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +'use strict'; + +let React; +let ReactTestRenderer; +let ReactDebugTools; + +describe('ReactHooksInspectionIntergration', () => { + beforeEach(() => { + jest.resetModules(); + let ReactFeatureFlags = require('shared/ReactFeatureFlags'); + // TODO: Switch this test to non-internal once the flag is on by default. + ReactFeatureFlags.enableHooks = true; + React = require('react'); + ReactTestRenderer = require('react-test-renderer'); + ReactDebugTools = require('react-debug-tools'); + }); + + it('should inspect the current state of useState hooks', () => { + let useState = React.useState; + function Foo(props) { + let [state1, setState1] = useState('hello'); + let [state2, setState2] = useState('world'); + return ( +
+ {state1} {state2} +
+ ); + } + let renderer = ReactTestRenderer.create(); + + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + {name: 'State', value: 'hello', subHooks: []}, + {name: 'State', value: 'world', subHooks: []}, + ]); + + let { + onMouseDown: setStateA, + onMouseUp: setStateB, + } = renderer.root.findByType('div').props; + + setStateA('Hi'); + + childFiber = renderer.root.findByType(Foo)._currentFiber(); + tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + + expect(tree).toEqual([ + {name: 'State', value: 'Hi', subHooks: []}, + {name: 'State', value: 'world', subHooks: []}, + ]); + + setStateB('world!'); + + childFiber = renderer.root.findByType(Foo)._currentFiber(); + tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + + expect(tree).toEqual([ + {name: 'State', value: 'Hi', subHooks: []}, + {name: 'State', value: 'world!', subHooks: []}, + ]); + }); + + it('should inspect the current state of all stateful hooks', () => { + let outsideRef = React.createRef(); + function effect() {} + function Foo(props) { + let [state1, setState] = React.useState('a'); + let [state2, dispatch] = React.useReducer((s, a) => a.value, 'b'); + let ref = React.useRef('c'); + + React.useMutationEffect(effect); + React.useLayoutEffect(effect); + React.useEffect(effect); + + React.useImperativeMethods( + outsideRef, + () => { + // Return a function so that jest treats them as non-equal. + return function Instance() {}; + }, + [], + ); + + React.useMemo(() => state1 + state2, [state1]); + + function update() { + setState('A'); + dispatch({value: 'B'}); + ref.current = 'C'; + } + let memoizedUpdate = React.useCallback(update, []); + return ( +
+ {state1} {state2} +
+ ); + } + let renderer = ReactTestRenderer.create(); + + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + + let {onClick: updateStates} = renderer.root.findByType('div').props; + + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + {name: 'State', value: 'a', subHooks: []}, + {name: 'Reducer', value: 'b', subHooks: []}, + {name: 'Ref', value: 'c', subHooks: []}, + {name: 'MutationEffect', value: effect, subHooks: []}, + {name: 'LayoutEffect', value: effect, subHooks: []}, + {name: 'Effect', value: effect, subHooks: []}, + {name: 'ImperativeMethods', value: outsideRef.current, subHooks: []}, + {name: 'Memo', value: 'ab', subHooks: []}, + {name: 'Callback', value: updateStates, subHooks: []}, + ]); + + updateStates(); + + childFiber = renderer.root.findByType(Foo)._currentFiber(); + tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + + expect(tree).toEqual([ + {name: 'State', value: 'A', subHooks: []}, + {name: 'Reducer', value: 'B', subHooks: []}, + {name: 'Ref', value: 'C', subHooks: []}, + {name: 'MutationEffect', value: effect, subHooks: []}, + {name: 'LayoutEffect', value: effect, subHooks: []}, + {name: 'Effect', value: effect, subHooks: []}, + {name: 'ImperativeMethods', value: outsideRef.current, subHooks: []}, + {name: 'Memo', value: 'Ab', subHooks: []}, + {name: 'Callback', value: updateStates, subHooks: []}, + ]); + }); + + it('should inspect the value of the current provider in useContext', () => { + let MyContext = React.createContext('default'); + function Foo(props) { + let value = React.useContext(MyContext); + return
{value}
; + } + let renderer = ReactTestRenderer.create( + + + , + ); + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + name: 'Context', + value: 'contextual', + subHooks: [], + }, + ]); + }); + + it('should inspect forwardRef', () => { + let obj = function() {}; + let Foo = React.forwardRef(function(props, ref) { + React.useImperativeMethods(ref, () => obj); + return
; + }); + let ref = React.createRef(); + let renderer = ReactTestRenderer.create(); + + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + {name: 'ImperativeMethods', value: obj, subHooks: []}, + ]); + }); + + it('should inspect memo', () => { + function InnerFoo(props) { + let [value] = React.useState('hello'); + return
{value}
; + } + let Foo = React.memo(InnerFoo); + let renderer = ReactTestRenderer.create(); + // TODO: Test renderer findByType is broken for memo. Have to search for the inner. + let childFiber = renderer.root.findByType(InnerFoo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([{name: 'State', value: 'hello', subHooks: []}]); + }); + + it('should inspect custom hooks', () => { + function useCustom() { + let [value] = React.useState('hello'); + return value; + } + function Foo(props) { + let value = useCustom(); + return
{value}
; + } + let renderer = ReactTestRenderer.create(); + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + name: 'Custom', + value: undefined, + subHooks: [{name: 'State', value: 'hello', subHooks: []}], + }, + ]); + }); + + it('should support defaultProps and lazy', async () => { + let Suspense = React.Suspense; + + function Foo(props) { + let [value] = React.useState(props.defaultValue.substr(0, 3)); + return
{value}
; + } + Foo.defaultProps = { + defaultValue: 'default', + }; + + async function fakeImport(result) { + return {default: result}; + } + + let LazyFoo = React.lazy(() => fakeImport(Foo)); + + let renderer = ReactTestRenderer.create( + + + , + ); + + await LazyFoo; + + let childFiber = renderer.root._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([{name: 'State', value: 'def', subHooks: []}]); + }); +}); diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index ddd4bcac9b2fa..d1092d9d2e798 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -49,7 +49,7 @@ type UpdateQueue
= { dispatch: any, }; -type Hook = { +export type Hook = { memoizedState: any, baseState: any, diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 21f007525f4f7..9ab3d974ec9dc 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -211,6 +211,7 @@ const validWrapperTypes = new Set([ HostComponent, ForwardRef, MemoComponent, + SimpleMemoComponent, // Normally skipped, but used when there's more than one root child. HostRoot, ]); diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index b3076d8ceeccb..f5e2da875b218 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -363,6 +363,16 @@ const bundles = [ externals: [], }, + /******* React Debug Tools *******/ + { + label: 'react-debug-tools', + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'react-debug-tools', + global: 'ReactDebugTools', + externals: [], + }, + /******* React Cache (experimental) *******/ { label: 'react-cache', diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index ebdeb6fe0fcb3..d67db5e1e9623 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -4,29 +4,29 @@ "filename": "react.development.js", "bundleType": "UMD_DEV", "packageName": "react", - "size": 95936, - "gzip": 25258 + "size": 98406, + "gzip": 25783 }, { "filename": "react.production.min.js", "bundleType": "UMD_PROD", "packageName": "react", - "size": 11933, - "gzip": 4716 + "size": 11771, + "gzip": 4678 }, { "filename": "react.development.js", "bundleType": "NODE_DEV", "packageName": "react", - "size": 57749, - "gzip": 15548 + "size": 61674, + "gzip": 16606 }, { "filename": "react.production.min.js", "bundleType": "NODE_PROD", "packageName": "react", - "size": 6215, - "gzip": 2648 + "size": 6223, + "gzip": 2655 }, { "filename": "React-dev.js", @@ -46,29 +46,29 @@ "filename": "react-dom.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 697227, - "gzip": 161844 + "size": 720847, + "gzip": 166955 }, { "filename": "react-dom.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 100367, - "gzip": 32791 + "size": 105329, + "gzip": 34551 }, { "filename": "react-dom.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 692458, - "gzip": 160422 + "size": 716045, + "gzip": 165551 }, { "filename": "react-dom.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 100389, - "gzip": 32291 + "size": 105421, + "gzip": 34088 }, { "filename": "ReactDOM-dev.js", @@ -88,8 +88,8 @@ "filename": "react-dom-test-utils.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 46915, - "gzip": 12731 + "size": 46926, + "gzip": 12739 }, { "filename": "react-dom-test-utils.production.min.js", @@ -102,8 +102,8 @@ "filename": "react-dom-test-utils.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 46629, - "gzip": 12668 + "size": 46640, + "gzip": 12676 }, { "filename": "react-dom-test-utils.production.min.js", @@ -165,29 +165,29 @@ "filename": "react-dom-server.browser.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 110808, - "gzip": 29290 + "size": 120946, + "gzip": 31984 }, { "filename": "react-dom-server.browser.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 16261, - "gzip": 6183 + "size": 18147, + "gzip": 6947 }, { "filename": "react-dom-server.browser.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 106846, - "gzip": 28277 + "size": 116984, + "gzip": 31008 }, { "filename": "react-dom-server.browser.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 16156, - "gzip": 6171 + "size": 18045, + "gzip": 6934 }, { "filename": "ReactDOMServer-dev.js", @@ -207,43 +207,43 @@ "filename": "react-dom-server.node.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 108814, - "gzip": 28843 + "size": 118952, + "gzip": 31557 }, { "filename": "react-dom-server.node.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 16981, - "gzip": 6481 + "size": 18870, + "gzip": 7243 }, { "filename": "react-art.development.js", "bundleType": "UMD_DEV", "packageName": "react-art", - "size": 479568, - "gzip": 106369 + "size": 502853, + "gzip": 111403 }, { "filename": "react-art.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-art", - "size": 92149, - "gzip": 28279 + "size": 96973, + "gzip": 30018 }, { "filename": "react-art.development.js", "bundleType": "NODE_DEV", "packageName": "react-art", - "size": 409729, - "gzip": 88882 + "size": 433008, + "gzip": 93974 }, { "filename": "react-art.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-art", - "size": 56279, - "gzip": 17346 + "size": 61174, + "gzip": 19077 }, { "filename": "ReactART-dev.js", @@ -291,29 +291,29 @@ "filename": "react-test-renderer.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 422767, - "gzip": 91691 + "size": 445955, + "gzip": 96705 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 57493, - "gzip": 17706 + "size": 62392, + "gzip": 19461 }, { "filename": "react-test-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 418235, - "gzip": 90567 + "size": 441056, + "gzip": 95534 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-test-renderer", - "size": 57208, - "gzip": 17513 + "size": 62067, + "gzip": 19233 }, { "filename": "ReactTestRenderer-dev.js", @@ -326,21 +326,21 @@ "filename": "react-test-renderer-shallow.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 27270, - "gzip": 7324 + "size": 27383, + "gzip": 7340 }, { "filename": "react-test-renderer-shallow.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 7394, - "gzip": 2412 + "size": 7442, + "gzip": 2425 }, { "filename": "react-test-renderer-shallow.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 21638, + "size": 21639, "gzip": 5871 }, { @@ -361,50 +361,50 @@ "filename": "react-noop-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-noop-renderer", - "size": 28548, - "gzip": 6219 + "size": 28829, + "gzip": 6286 }, { "filename": "react-noop-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-noop-renderer", - "size": 10802, - "gzip": 3587 + "size": 10889, + "gzip": 3632 }, { "filename": "react-reconciler.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 407389, - "gzip": 87287 + "size": 430802, + "gzip": 92370 }, { "filename": "react-reconciler.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 57356, - "gzip": 17175 + "size": 62418, + "gzip": 18868 }, { "filename": "react-reconciler-persistent.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 405959, - "gzip": 86717 + "size": 429212, + "gzip": 91734 }, { "filename": "react-reconciler-persistent.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 57367, - "gzip": 17180 + "size": 62429, + "gzip": 18874 }, { "filename": "react-reconciler-reflection.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 16735, - "gzip": 5071 + "size": 16747, + "gzip": 5081 }, { "filename": "react-reconciler-reflection.production.min.js", @@ -431,29 +431,29 @@ "filename": "react-is.development.js", "bundleType": "UMD_DEV", "packageName": "react-is", - "size": 7539, - "gzip": 2364 + "size": 7691, + "gzip": 2393 }, { "filename": "react-is.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-is", - "size": 2113, - "gzip": 836 + "size": 2171, + "gzip": 854 }, { "filename": "react-is.development.js", "bundleType": "NODE_DEV", "packageName": "react-is", - "size": 7350, - "gzip": 2313 + "size": 7502, + "gzip": 2344 }, { "filename": "react-is.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-is", - "size": 2074, - "gzip": 775 + "size": 2132, + "gzip": 793 }, { "filename": "ReactIs-dev.js", @@ -501,36 +501,36 @@ "filename": "React-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react", - "size": 54009, - "gzip": 14673 + "size": 58044, + "gzip": 15664 }, { "filename": "React-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react", - "size": 14256, + "size": 14026, "gzip": 3899 }, { "filename": "ReactDOM-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 711278, - "gzip": 161142 + "size": 734797, + "gzip": 166398 }, { "filename": "ReactDOM-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 303046, - "gzip": 55626 + "size": 318299, + "gzip": 58930 }, { "filename": "ReactTestUtils-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 42309, - "gzip": 11422 + "size": 42319, + "gzip": 11429 }, { "filename": "ReactDOMUnstableNativeDependencies-dev.js", @@ -550,113 +550,113 @@ "filename": "ReactDOMServer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 106413, - "gzip": 27558 + "size": 116990, + "gzip": 30470 }, { "filename": "ReactDOMServer-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 35825, - "gzip": 8548 + "size": 41749, + "gzip": 9884 }, { "filename": "ReactART-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-art", - "size": 414596, - "gzip": 87310 + "size": 437769, + "gzip": 92475 }, { "filename": "ReactART-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-art", - "size": 173284, - "gzip": 29264 + "size": 188325, + "gzip": 32413 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 544253, - "gzip": 118996 + "size": 567081, + "gzip": 124053 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 229349, - "gzip": 39944 + "size": 244223, + "gzip": 43036 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 543957, - "gzip": 118920 + "size": 566746, + "gzip": 123945 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 229363, - "gzip": 39944 + "size": 244237, + "gzip": 43035 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 534243, - "gzip": 116534 + "size": 557032, + "gzip": 121529 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 223762, - "gzip": 38608 + "size": 238983, + "gzip": 41707 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 534278, - "gzip": 116548 + "size": 557067, + "gzip": 121541 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 223798, - "gzip": 38624 + "size": 239019, + "gzip": 41721 }, { "filename": "ReactTestRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 423481, - "gzip": 89361 + "size": 446010, + "gzip": 94386 }, { "filename": "ReactShallowRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 18563, - "gzip": 4857 + "size": 18564, + "gzip": 4859 }, { "filename": "ReactIs-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-is", - "size": 5721, - "gzip": 1588 + "size": 5873, + "gzip": 1621 }, { "filename": "ReactIs-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-is", - "size": 4227, - "gzip": 1105 + "size": 4382, + "gzip": 1135 }, { "filename": "scheduler.development.js", @@ -676,15 +676,15 @@ "filename": "scheduler.development.js", "bundleType": "NODE_DEV", "packageName": "scheduler", - "size": 22710, - "gzip": 6136 + "size": 22073, + "gzip": 5976 }, { "filename": "scheduler.production.min.js", "bundleType": "NODE_PROD", "packageName": "scheduler", - "size": 4907, - "gzip": 1905 + "size": 4755, + "gzip": 1865 }, { "filename": "SimpleCacheProvider-dev.js", @@ -704,50 +704,50 @@ "filename": "react-noop-renderer-persistent.development.js", "bundleType": "NODE_DEV", "packageName": "react-noop-renderer", - "size": 28667, - "gzip": 6232 + "size": 28948, + "gzip": 6299 }, { "filename": "react-noop-renderer-persistent.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-noop-renderer", - "size": 10824, - "gzip": 3593 + "size": 10911, + "gzip": 3639 }, { "filename": "react-dom.profiling.min.js", "bundleType": "NODE_PROFILING", "packageName": "react-dom", - "size": 102854, - "gzip": 32671 + "size": 107853, + "gzip": 34402 }, { "filename": "ReactNativeRenderer-profiling.js", "bundleType": "RN_OSS_PROFILING", "packageName": "react-native-renderer", - "size": 235178, - "gzip": 41307 + "size": 250067, + "gzip": 44259 }, { "filename": "ReactFabric-profiling.js", "bundleType": "RN_OSS_PROFILING", "packageName": "react-native-renderer", - "size": 228734, - "gzip": 39942 + "size": 243670, + "gzip": 43000 }, { "filename": "Scheduler-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "scheduler", - "size": 22987, - "gzip": 6180 + "size": 22314, + "gzip": 6027 }, { "filename": "Scheduler-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "scheduler", - "size": 13870, - "gzip": 2990 + "size": 13375, + "gzip": 2927 }, { "filename": "react.profiling.min.js", @@ -760,50 +760,50 @@ "filename": "React-profiling.js", "bundleType": "FB_WWW_PROFILING", "packageName": "react", - "size": 14256, + "size": 14026, "gzip": 3899 }, { "filename": "ReactDOM-profiling.js", "bundleType": "FB_WWW_PROFILING", "packageName": "react-dom", - "size": 307635, - "gzip": 56727 + "size": 322659, + "gzip": 59833 }, { "filename": "ReactNativeRenderer-profiling.js", "bundleType": "RN_FB_PROFILING", "packageName": "react-native-renderer", - "size": 235159, - "gzip": 41309 + "size": 250048, + "gzip": 44263 }, { "filename": "ReactFabric-profiling.js", "bundleType": "RN_FB_PROFILING", "packageName": "react-native-renderer", - "size": 228693, - "gzip": 39926 + "size": 243629, + "gzip": 42984 }, { "filename": "react.profiling.min.js", "bundleType": "UMD_PROFILING", "packageName": "react", - "size": 14140, - "gzip": 5250 + "size": 13977, + "gzip": 5211 }, { "filename": "react-dom.profiling.min.js", "bundleType": "UMD_PROFILING", "packageName": "react-dom", - "size": 102786, - "gzip": 33199 + "size": 107720, + "gzip": 34916 }, { "filename": "scheduler-tracing.development.js", "bundleType": "NODE_DEV", "packageName": "scheduler", - "size": 10319, - "gzip": 2326 + "size": 10480, + "gzip": 2403 }, { "filename": "scheduler-tracing.production.min.js", @@ -823,8 +823,8 @@ "filename": "SchedulerTracing-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "scheduler", - "size": 10105, - "gzip": 2117 + "size": 10467, + "gzip": 2295 }, { "filename": "SchedulerTracing-prod.js", @@ -909,6 +909,34 @@ "packageName": "jest-react", "size": 5201, "gzip": 1623 + }, + { + "filename": "react-debug-tools.development.js", + "bundleType": "NODE_DEV", + "packageName": "react-debug-tools", + "size": 14080, + "gzip": 4317 + }, + { + "filename": "react-debug-tools.production.min.js", + "bundleType": "NODE_PROD", + "packageName": "react-debug-tools", + "size": 4112, + "gzip": 1643 + }, + { + "filename": "eslint-plugin-react-hooks.development.js", + "bundleType": "NODE_DEV", + "packageName": "eslint-plugin-react-hooks", + "size": 25596, + "gzip": 5886 + }, + { + "filename": "eslint-plugin-react-hooks.production.min.js", + "bundleType": "NODE_PROD", + "packageName": "eslint-plugin-react-hooks", + "size": 4943, + "gzip": 1815 } ] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ef3d2d3556ad7..4f2225a379255 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1584,6 +1584,13 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" +error-stack-parser@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.2.tgz#4ae8dbaa2bf90a8b450707b9149dcabca135520d" + integrity sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw== + dependencies: + stackframe "^1.0.4" + es-abstract@^1.5.1: version "1.12.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" @@ -4638,6 +4645,11 @@ stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" +stackframe@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b" + integrity sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw== + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"