diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js index ecd066af86..1461545d29 100644 --- a/packages/react-devtools-core/webpack.backend.js +++ b/packages/react-devtools-core/webpack.backend.js @@ -43,6 +43,8 @@ module.exports = { plugins: [ new DefinePlugin({ __DEV__: true, + __PROFILE__: false, + __EXPERIMENTAL__: true, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, }), diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js index a1882f20cf..61c1d96237 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -48,6 +48,8 @@ module.exports = { plugins: [ new DefinePlugin({ __DEV__: false, + __PROFILE__: false, + __EXPERIMENTAL__: true, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.NODE_ENV': `"${NODE_ENV}"`, diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js index d7de389f8c..1f649dbb6d 100644 --- a/packages/react-devtools-extensions/webpack.backend.js +++ b/packages/react-devtools-extensions/webpack.backend.js @@ -38,6 +38,8 @@ module.exports = { plugins: [ new DefinePlugin({ __DEV__: true, + __PROFILE__: false, + __EXPERIMENTAL__: true, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, }), diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index e6aa905e6c..b22ea60569 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -43,6 +43,8 @@ module.exports = { plugins: [ new DefinePlugin({ __DEV__: false, + __PROFILE__: false, + __EXPERIMENTAL__: true, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.NODE_ENV': `"${NODE_ENV}"`, diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 7e24e54a05..3f0677f80b 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -39,6 +39,8 @@ module.exports = { plugins: [ new DefinePlugin({ __DEV__, + __PROFILE__: false, + __EXPERIMENTAL__: true, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.NODE_ENV': `"${NODE_ENV}"`, diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index 6d400164a8..74e9fed9ff 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -114,7 +114,7 @@ describe('console', () => { }); it('should not append multiple stacks', () => { - const Child = () => { + const Child = ({children}) => { fakeConsole.warn('warn\n in Child (at fake.js:123)'); fakeConsole.error('error', '\n in Child (at fake.js:123)'); return null; @@ -135,12 +135,12 @@ describe('console', () => { it('should append component stacks to errors and warnings logged during render', () => { const Intermediate = ({children}) => children; - const Parent = () => ( + const Parent = ({children}) => ( ); - const Child = () => { + const Child = ({children}) => { fakeConsole.error('error'); fakeConsole.log('log'); fakeConsole.warn('warn'); @@ -156,24 +156,24 @@ describe('console', () => { expect(mockWarn.mock.calls[0]).toHaveLength(2); expect(mockWarn.mock.calls[0][0]).toBe('warn'); expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); expect(mockError).toHaveBeenCalledTimes(1); expect(mockError.mock.calls[0]).toHaveLength(2); expect(mockError.mock.calls[0][0]).toBe('error'); expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); }); it('should append component stacks to errors and warnings logged from effects', () => { const Intermediate = ({children}) => children; - const Parent = () => ( + const Parent = ({children}) => ( ); - const Child = () => { + const Child = ({children}) => { React.useLayoutEffect(() => { fakeConsole.error('active error'); fakeConsole.log('active log'); @@ -198,29 +198,29 @@ describe('console', () => { expect(mockWarn.mock.calls[0]).toHaveLength(2); expect(mockWarn.mock.calls[0][0]).toBe('active warn'); expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); expect(mockWarn.mock.calls[1]).toHaveLength(2); expect(mockWarn.mock.calls[1][0]).toBe('passive warn'); expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); expect(mockError).toHaveBeenCalledTimes(2); expect(mockError.mock.calls[0]).toHaveLength(2); expect(mockError.mock.calls[0][0]).toBe('active error'); expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); expect(mockError.mock.calls[1]).toHaveLength(2); expect(mockError.mock.calls[1][0]).toBe('passive error'); expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); }); it('should append component stacks to errors and warnings logged from commit hooks', () => { const Intermediate = ({children}) => children; - const Parent = () => ( + const Parent = ({children}) => ( @@ -254,29 +254,29 @@ describe('console', () => { expect(mockWarn.mock.calls[0]).toHaveLength(2); expect(mockWarn.mock.calls[0][0]).toBe('didMount warn'); expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); expect(mockWarn.mock.calls[1]).toHaveLength(2); expect(mockWarn.mock.calls[1][0]).toBe('didUpdate warn'); expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); expect(mockError).toHaveBeenCalledTimes(2); expect(mockError.mock.calls[0]).toHaveLength(2); expect(mockError.mock.calls[0][0]).toBe('didMount error'); expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); expect(mockError.mock.calls[1]).toHaveLength(2); expect(mockError.mock.calls[1][0]).toBe('didUpdate error'); expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); }); it('should append component stacks to errors and warnings logged from gDSFP', () => { const Intermediate = ({children}) => children; - const Parent = () => ( + const Parent = ({children}) => ( @@ -303,18 +303,18 @@ describe('console', () => { expect(mockWarn.mock.calls[0]).toHaveLength(2); expect(mockWarn.mock.calls[0][0]).toBe('warn'); expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); expect(mockError).toHaveBeenCalledTimes(1); expect(mockError.mock.calls[0]).toHaveLength(2); expect(mockError.mock.calls[0][0]).toBe('error'); expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( - '\n in Child (at **)\n in Parent (at **)', + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); }); it('should append stacks after being uninstalled and reinstalled', () => { - const Child = () => { + const Child = ({children}) => { fakeConsole.warn('warn'); fakeConsole.error('error'); return null; diff --git a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js new file mode 100644 index 0000000000..f4aab13ece --- /dev/null +++ b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js @@ -0,0 +1,298 @@ +/** + * 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 + */ + +// This is a DevTools fork of ReactComponentStackFrame. +// This fork enables DevTools to use the same "native" component stack format, +// while still maintaining support for multiple renderer versions +// (which use different values for ReactTypeOfWork). + +import type {Source} from 'shared/ReactElementType'; +import type {LazyComponent} from 'react/src/ReactLazy'; +import type {CurrentDispatcherRef} from './types'; + +import { + BLOCK_NUMBER, + BLOCK_SYMBOL_STRING, + FORWARD_REF_NUMBER, + FORWARD_REF_SYMBOL_STRING, + LAZY_NUMBER, + LAZY_SYMBOL_STRING, + MEMO_NUMBER, + MEMO_SYMBOL_STRING, + SUSPENSE_NUMBER, + SUSPENSE_SYMBOL_STRING, + SUSPENSE_LIST_NUMBER, + SUSPENSE_LIST_SYMBOL_STRING, +} from './ReactSymbols'; + +// These methods are safe to import from shared; +// there is no React-specific logic here. +import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; + +let prefix; +export function describeBuiltInComponentFrame( + name: string, + source: void | null | Source, + ownerFn: void | null | Function, +): string { + if (prefix === undefined) { + // Extract the VM specific prefix used by each line. + try { + throw Error(); + } catch (x) { + const match = x.stack.trim().match(/\n( *(at )?)/); + prefix = (match && match[1]) || ''; + } + } + // We use the prefix to ensure our stacks line up with native stack frames. + return '\n' + prefix + name; +} + +let reentry = false; +let componentFrameCache; +if (__DEV__) { + const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; + componentFrameCache = new PossiblyWeakMap(); +} + +export function describeNativeComponentFrame( + fn: Function, + construct: boolean, + currentDispatcherRef: CurrentDispatcherRef, +): string { + // If something asked for a stack inside a fake render, it should get ignored. + if (!fn || reentry) { + return ''; + } + + if (__DEV__) { + const frame = componentFrameCache.get(fn); + if (frame !== undefined) { + return frame; + } + } + + let control; + + reentry = true; + let previousDispatcher; + if (__DEV__) { + previousDispatcher = currentDispatcherRef.current; + // Set the dispatcher in DEV because this might be call in the render function + // for warnings. + currentDispatcherRef.current = null; + disableLogs(); + } + try { + // This should throw. + if (construct) { + // Something should be setting the props in the constructor. + const Fake = function() { + throw Error(); + }; + // $FlowFixMe + Object.defineProperty(Fake.prototype, 'props', { + set: function() { + // We use a throwing setter instead of frozen or non-writable props + // because that won't throw in a non-strict mode function. + throw Error(); + }, + }); + if (typeof Reflect === 'object' && Reflect.construct) { + // We construct a different control for this case to include any extra + // frames added by the construct call. + try { + Reflect.construct(Fake, []); + } catch (x) { + control = x; + } + Reflect.construct(fn, [], Fake); + } else { + try { + Fake.call(); + } catch (x) { + control = x; + } + fn.call(Fake.prototype); + } + } else { + try { + throw Error(); + } catch (x) { + control = x; + } + fn(); + } + } catch (sample) { + // This is inlined manually because closure doesn't do it for us. + if (sample && control && typeof sample.stack === 'string') { + // This extracts the first frame from the sample that isn't also in the control. + // Skipping one frame that we assume is the frame that calls the two. + const sampleLines = sample.stack.split('\n'); + const controlLines = control.stack.split('\n'); + let s = sampleLines.length - 1; + let c = controlLines.length - 1; + while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) { + // We expect at least one stack frame to be shared. + // Typically this will be the root most one. However, stack frames may be + // cut off due to maximum stack limits. In this case, one maybe cut off + // earlier than the other. We assume that the sample is longer or the same + // and there for cut off earlier. So we should find the root most frame in + // the sample somewhere in the control. + c--; + } + for (; s >= 1 && c >= 0; s--, c--) { + // Next we find the first one that isn't the same which should be the + // frame that called our sample function and the control. + if (sampleLines[s] !== controlLines[c]) { + // In V8, the first line is describing the message but other VMs don't. + // If we're about to return the first line, and the control is also on the same + // line, that's a pretty good indicator that our sample threw at same line as + // the control. I.e. before we entered the sample frame. So we ignore this result. + // This can happen if you passed a class to function component, or non-function. + if (s !== 1 || c !== 1) { + do { + s--; + c--; + // We may still have similar intermediate frames from the construct call. + // The next one that isn't the same should be our match though. + if (c < 0 || sampleLines[s] !== controlLines[c]) { + // V8 adds a "new" prefix for native classes. Let's remove it to make it prettier. + const frame = '\n' + sampleLines[s].replace(' at new ', ' at '); + if (__DEV__) { + if (typeof fn === 'function') { + componentFrameCache.set(fn, frame); + } + } + // Return the line we found. + return frame; + } + } while (s >= 1 && c >= 0); + } + break; + } + } + } + } finally { + reentry = false; + if (__DEV__) { + currentDispatcherRef.current = previousDispatcher; + reenableLogs(); + } + } + // Fallback to just using the name if we couldn't make it throw. + const name = fn ? fn.displayName || fn.name : ''; + const syntheticFrame = name ? describeBuiltInComponentFrame(name) : ''; + if (__DEV__) { + if (typeof fn === 'function') { + componentFrameCache.set(fn, syntheticFrame); + } + } + return syntheticFrame; +} + +export function describeClassComponentFrame( + ctor: Function, + source: void | null | Source, + ownerFn: void | null | Function, + currentDispatcherRef: CurrentDispatcherRef, +): string { + return describeNativeComponentFrame(ctor, true, currentDispatcherRef); +} + +export function describeFunctionComponentFrame( + fn: Function, + source: void | null | Source, + ownerFn: void | null | Function, + currentDispatcherRef: CurrentDispatcherRef, +): string { + return describeNativeComponentFrame(fn, false, currentDispatcherRef); +} + +function shouldConstruct(Component: Function) { + const prototype = Component.prototype; + return !!(prototype && prototype.isReactComponent); +} + +export function describeUnknownElementTypeFrameInDEV( + type: any, + source: void | null | Source, + ownerFn: void | null | Function, + currentDispatcherRef: CurrentDispatcherRef, +): string { + if (!__DEV__) { + return ''; + } + if (type == null) { + return ''; + } + if (typeof type === 'function') { + return describeNativeComponentFrame( + type, + shouldConstruct(type), + currentDispatcherRef, + ); + } + if (typeof type === 'string') { + return describeBuiltInComponentFrame(type, source, ownerFn); + } + switch (type) { + case SUSPENSE_NUMBER: + case SUSPENSE_SYMBOL_STRING: + return describeBuiltInComponentFrame('Suspense', source, ownerFn); + case SUSPENSE_LIST_NUMBER: + case SUSPENSE_LIST_SYMBOL_STRING: + return describeBuiltInComponentFrame('SuspenseList', source, ownerFn); + } + if (typeof type === 'object') { + switch (type.$$typeof) { + case FORWARD_REF_NUMBER: + case FORWARD_REF_SYMBOL_STRING: + return describeFunctionComponentFrame( + type.render, + source, + ownerFn, + currentDispatcherRef, + ); + case MEMO_NUMBER: + case MEMO_SYMBOL_STRING: + // Memo may contain any component type so we recursively resolve it. + return describeUnknownElementTypeFrameInDEV( + type.type, + source, + ownerFn, + currentDispatcherRef, + ); + case BLOCK_NUMBER: + case BLOCK_SYMBOL_STRING: + return describeFunctionComponentFrame( + type._render, + source, + ownerFn, + currentDispatcherRef, + ); + case LAZY_NUMBER: + case LAZY_SYMBOL_STRING: { + const lazyComponent: LazyComponent = (type: any); + const payload = lazyComponent._payload; + const init = lazyComponent._init; + try { + // Lazy may contain any component type so we recursively resolve it. + return describeUnknownElementTypeFrameInDEV( + init(payload), + source, + ownerFn, + currentDispatcherRef, + ); + } catch (x) {} + } + } + } + return ''; +} diff --git a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js new file mode 100644 index 0000000000..ecb2ac6d3b --- /dev/null +++ b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js @@ -0,0 +1,108 @@ +/** + * 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 + */ + +// This is a DevTools fork of ReactFiberComponentStack. +// This fork enables DevTools to use the same "native" component stack format, +// while still maintaining support for multiple renderer versions +// (which use different values for ReactTypeOfWork). + +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import type {CurrentDispatcherRef, WorkTagMap} from './types'; + +import { + describeBuiltInComponentFrame, + describeFunctionComponentFrame, + describeClassComponentFrame, +} from './DevToolsComponentStackFrame'; + +function describeFiber( + workTagMap: WorkTagMap, + workInProgress: Fiber, + currentDispatcherRef: CurrentDispatcherRef, +): string { + const { + HostComponent, + LazyComponent, + SuspenseComponent, + SuspenseListComponent, + FunctionComponent, + IndeterminateComponent, + SimpleMemoComponent, + ForwardRef, + Block, + ClassComponent, + } = workTagMap; + + const owner: null | Function = __DEV__ + ? workInProgress._debugOwner + ? workInProgress._debugOwner.type + : null + : null; + const source = __DEV__ ? workInProgress._debugSource : null; + switch (workInProgress.tag) { + case HostComponent: + return describeBuiltInComponentFrame(workInProgress.type, source, owner); + case LazyComponent: + return describeBuiltInComponentFrame('Lazy', source, owner); + case SuspenseComponent: + return describeBuiltInComponentFrame('Suspense', source, owner); + case SuspenseListComponent: + return describeBuiltInComponentFrame('SuspenseList', source, owner); + case FunctionComponent: + case IndeterminateComponent: + case SimpleMemoComponent: + return describeFunctionComponentFrame( + workInProgress.type, + source, + owner, + currentDispatcherRef, + ); + case ForwardRef: + return describeFunctionComponentFrame( + workInProgress.type.render, + source, + owner, + currentDispatcherRef, + ); + case Block: + return describeFunctionComponentFrame( + workInProgress.type._render, + source, + owner, + currentDispatcherRef, + ); + case ClassComponent: + return describeClassComponentFrame( + workInProgress.type, + source, + owner, + currentDispatcherRef, + ); + default: + return ''; + } +} + +export function getStackByFiberInDevAndProd( + workTagMap: WorkTagMap, + workInProgress: Fiber, + currentDispatcherRef: CurrentDispatcherRef, +): string { + try { + let info = ''; + let node = workInProgress; + do { + info += describeFiber(workTagMap, node, currentDispatcherRef); + node = node.return; + } while (node); + return info; + } catch (x) { + return '\nError generating stack: ' + x.message + '\n' + x.stack; + } +} diff --git a/packages/react-devtools-shared/src/backend/ReactSymbols.js b/packages/react-devtools-shared/src/backend/ReactSymbols.js new file mode 100644 index 0000000000..677c0a9d66 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/ReactSymbols.js @@ -0,0 +1,77 @@ +/** + * 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 + */ + +// This list should be kept updated to reflect additions to 'shared/ReactSymbols'. +// DevTools can't import symbols from 'shared/ReactSymbols' directly for two reasons: +// 1. DevTools requires symbols which may have been deleted in more recent versions (e.g. concurrent mode) +// 2. DevTools must support both Symbol and numeric forms of each symbol; +// Since e.g. standalone DevTools runs in a separate process, it can't rely on its own ES capabilities. + +export const BLOCK_NUMBER = 0xead9; +export const BLOCK_SYMBOL_STRING = 'Symbol(react.block)'; + +export const CONCURRENT_MODE_NUMBER = 0xeacf; +export const CONCURRENT_MODE_SYMBOL_STRING = 'Symbol(react.concurrent_mode)'; + +export const CONTEXT_NUMBER = 0xeace; +export const CONTEXT_SYMBOL_STRING = 'Symbol(react.context)'; + +export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = 'Symbol(react.async_mode)'; + +export const ELEMENT_NUMBER = 0xeac7; +export const ELEMENT_SYMBOL_STRING = 'Symbol(react.element)'; + +export const DEBUG_TRACING_MODE_NUMBER = 0xeae1; +export const DEBUG_TRACING_MODE_SYMBOL_STRING = + 'Symbol(react.debug_trace_mode)'; + +export const FORWARD_REF_NUMBER = 0xead0; +export const FORWARD_REF_SYMBOL_STRING = 'Symbol(react.forward_ref)'; + +export const FRAGMENT_NUMBER = 0xeacb; +export const FRAGMENT_SYMBOL_STRING = 'Symbol(react.fragment)'; + +export const FUNDAMENTAL_NUMBER = 0xead5; +export const FUNDAMENTAL_SYMBOL_STRING = 'Symbol(react.fundamental)'; + +export const LAZY_NUMBER = 0xead4; +export const LAZY_SYMBOL_STRING = 'Symbol(react.lazy)'; + +export const MEMO_NUMBER = 0xead3; +export const MEMO_SYMBOL_STRING = 'Symbol(react.memo)'; + +export const OPAQUE_ID_NUMBER = 0xeae0; +export const OPAQUE_ID_SYMBOL_STRING = 'Symbol(react.opaque.id)'; + +export const PORTAL_NUMBER = 0xeaca; +export const PORTAL_SYMBOL_STRING = 'Symbol(react.portal)'; + +export const PROFILER_NUMBER = 0xead2; +export const PROFILER_SYMBOL_STRING = 'Symbol(react.profiler)'; + +export const PROVIDER_NUMBER = 0xeacd; +export const PROVIDER_SYMBOL_STRING = 'Symbol(react.provider)'; + +export const RESPONDER_NUMBER = 0xead6; +export const RESPONDER_SYMBOL_STRING = 'Symbol(react.responder)'; + +export const SCOPE_NUMBER = 0xead7; +export const SCOPE_SYMBOL_STRING = 'Symbol(react.scope)'; + +export const SERVER_BLOCK_NUMBER = 0xeada; +export const SERVER_BLOCK_SYMBOL_STRING = 'Symbol(react.server.block)'; + +export const STRICT_MODE_NUMBER = 0xeacc; +export const STRICT_MODE_SYMBOL_STRING = 'Symbol(react.strict_mode)'; + +export const SUSPENSE_NUMBER = 0xead1; +export const SUSPENSE_SYMBOL_STRING = 'Symbol(react.suspense)'; + +export const SUSPENSE_LIST_NUMBER = 0xead8; +export const SUSPENSE_LIST_SYMBOL_STRING = 'Symbol(react.suspense_list)'; diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index b42cf2f9d3..80a6970115 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -7,21 +7,27 @@ * @flow */ -import {getInternalReactConstants} from './renderer'; -import describeComponentFrame from './describeComponentFrame'; - import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; -import type {ReactRenderer} from './types'; +import type {CurrentDispatcherRef, ReactRenderer, WorkTagMap} from './types'; + +import {getInternalReactConstants} from './renderer'; +import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack'; const APPEND_STACK_TO_METHODS = ['error', 'trace', 'warn']; -const FRAME_REGEX = /\n {4}in /; +// React's custom built component stack strings match "\s{4}in" +// Chrome's prefix matches "\s{4}at" +const PREFIX_REGEX = /\s{4}(in|at)\s{1}/; +// Firefox and Safari have no prefix ("") +// but we can fallback to looking for location info (e.g. "foo.js:12:345") +const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/; const injectedRenderers: Map< ReactRenderer, {| + currentDispatcherRef: CurrentDispatcherRef, getCurrentFiber: () => Fiber | null, - getDisplayNameForFiber: (fiber: Fiber) => string | null, + workTagMap: WorkTagMap, |}, > = new Map(); @@ -49,19 +55,27 @@ export function dangerous_setTargetConsoleForTesting( // These internals will be used if the console is patched. // Injecting them separately allows the console to easily be patched or un-patched later (at runtime). export function registerRenderer(renderer: ReactRenderer): void { - const {getCurrentFiber, findFiberByHostInstance, version} = renderer; + const { + currentDispatcherRef, + getCurrentFiber, + findFiberByHostInstance, + version, + } = renderer; // Ignore React v15 and older because they don't expose a component stack anyway. if (typeof findFiberByHostInstance !== 'function') { return; } - if (typeof getCurrentFiber === 'function') { - const {getDisplayNameForFiber} = getInternalReactConstants(version); + // currentDispatcherRef gets injected for v16.8+ to support hooks inspection. + // getCurrentFiber gets injected for v16.9+. + if (currentDispatcherRef != null && typeof getCurrentFiber === 'function') { + const {ReactTypeOfWork} = getInternalReactConstants(version); injectedRenderers.set(renderer, { + currentDispatcherRef, getCurrentFiber, - getDisplayNameForFiber, + workTagMap: ReactTypeOfWork, }); } } @@ -94,36 +108,31 @@ export function patch(): void { try { // If we are ever called with a string that already has a component stack, e.g. a React error/warning, // don't append a second stack. + const lastArg = args.length > 0 ? args[args.length - 1] : null; const alreadyHasComponentStack = - args.length > 0 && FRAME_REGEX.exec(args[args.length - 1]); + lastArg !== null && + (PREFIX_REGEX.test(lastArg) || + ROW_COLUMN_NUMBER_REGEX.test(lastArg)); if (!alreadyHasComponentStack) { // If there's a component stack for at least one of the injected renderers, append it. // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?) // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const { + currentDispatcherRef, getCurrentFiber, - getDisplayNameForFiber, + workTagMap, } of injectedRenderers.values()) { - let current: ?Fiber = getCurrentFiber(); - let ownerStack: string = ''; - while (current != null) { - const name = getDisplayNameForFiber(current); - const owner = current._debugOwner; - const ownerName = - owner != null ? getDisplayNameForFiber(owner) : null; - - ownerStack += describeComponentFrame( - name, - current._debugSource, - ownerName, + const current: ?Fiber = getCurrentFiber(); + if (current != null) { + const componentStack = getStackByFiberInDevAndProd( + workTagMap, + current, + currentDispatcherRef, ); - - current = owner; - } - - if (ownerStack !== '') { - args.push(ownerStack); + if (componentStack !== '') { + args.push(componentStack); + } break; } } diff --git a/packages/react-devtools-shared/src/backend/describeComponentFrame.js b/packages/react-devtools-shared/src/backend/describeComponentFrame.js deleted file mode 100644 index 1be9d86494..0000000000 --- a/packages/react-devtools-shared/src/backend/describeComponentFrame.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 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 - */ - -// This file was forked from the React GitHub repo: -// https://raw.githubusercontent.com/facebook/react/master/packages/shared/describeComponentFrame.js -// -// It has been modified slightly to add a zero width space as commented below. - -const BEFORE_SLASH_RE = /^(.*)[\\/]/; - -export default function describeComponentFrame( - name: null | string, - source: any, - ownerName: null | string, -) { - let sourceInfo = ''; - if (source) { - const path = source.fileName; - let fileName = path.replace(BEFORE_SLASH_RE, ''); - if (__DEV__) { - // In DEV, include code for a common special case: - // prefer "folder/index.js" instead of just "index.js". - if (/^index\./.test(fileName)) { - const match = path.match(BEFORE_SLASH_RE); - if (match) { - const pathBeforeSlash = match[1]; - if (pathBeforeSlash) { - const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, ''); - // Note the below string contains a zero width space after the "/" character. - // This is to prevent browsers like Chrome from formatting the file name as a link. - // (Since this is a source link, it would not work to open the source file anyway.) - fileName = folderName + '/​' + fileName; - } - } - } - } - sourceInfo = ' (at ' + fileName + ':' + source.lineNumber + ')'; - } else if (ownerName) { - sourceInfo = ' (created by ' + ownerName + ')'; - } - return '\n in ' + (name || 'Unknown') + sourceInfo; -} diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index dbe7270527..70bbac1949 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -49,6 +49,25 @@ import { patch as patchConsole, registerRenderer as registerRendererWithConsole, } from './console'; +import { + CONCURRENT_MODE_NUMBER, + CONCURRENT_MODE_SYMBOL_STRING, + DEPRECATED_ASYNC_MODE_SYMBOL_STRING, + PROVIDER_NUMBER, + PROVIDER_SYMBOL_STRING, + CONTEXT_NUMBER, + CONTEXT_SYMBOL_STRING, + STRICT_MODE_NUMBER, + STRICT_MODE_SYMBOL_STRING, + PROFILER_NUMBER, + PROFILER_SYMBOL_STRING, + SCOPE_NUMBER, + SCOPE_SYMBOL_STRING, + FORWARD_REF_NUMBER, + FORWARD_REF_SYMBOL_STRING, + MEMO_NUMBER, + MEMO_SYMBOL_STRING, +} from './ReactSymbols'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type { @@ -66,6 +85,7 @@ import type { ProfilingDataForRootBackend, ReactRenderer, RendererInterface, + WorkTagMap, } from './types'; import type {Interaction} from 'react-devtools-shared/src/devtools/views/Profiler/types'; import type { @@ -76,26 +96,6 @@ import type { type getDisplayNameForFiberType = (fiber: Fiber) => string | null; type getTypeSymbolType = (type: any) => Symbol | number; -type ReactSymbolsType = {| - CONCURRENT_MODE_NUMBER: number, - CONCURRENT_MODE_SYMBOL_STRING: string, - DEPRECATED_ASYNC_MODE_SYMBOL_STRING: string, - CONTEXT_CONSUMER_NUMBER: number, - CONTEXT_CONSUMER_SYMBOL_STRING: string, - CONTEXT_PROVIDER_NUMBER: number, - CONTEXT_PROVIDER_SYMBOL_STRING: string, - FORWARD_REF_NUMBER: number, - FORWARD_REF_SYMBOL_STRING: string, - MEMO_NUMBER: number, - MEMO_SYMBOL_STRING: string, - PROFILER_NUMBER: number, - PROFILER_SYMBOL_STRING: string, - STRICT_MODE_NUMBER: number, - STRICT_MODE_SYMBOL_STRING: string, - SCOPE_NUMBER: number, - SCOPE_SYMBOL_STRING: string, -|}; - type ReactPriorityLevelsType = {| ImmediatePriority: number, UserBlockingPriority: number, @@ -105,32 +105,6 @@ type ReactPriorityLevelsType = {| NoPriority: number, |}; -type ReactTypeOfWorkType = {| - ClassComponent: number, - ContextConsumer: number, - ContextProvider: number, - CoroutineComponent: number, - CoroutineHandlerPhase: number, - DehydratedSuspenseComponent: number, - ForwardRef: number, - Fragment: number, - FunctionComponent: number, - HostComponent: number, - HostPortal: number, - HostRoot: number, - HostText: number, - IncompleteClassComponent: number, - IndeterminateComponent: number, - LazyComponent: number, - MemoComponent: number, - Mode: number, - Profiler: number, - SimpleMemoComponent: number, - SuspenseComponent: number, - SuspenseListComponent: number, - YieldComponent: number, -|}; - type ReactTypeOfSideEffectType = {| NoEffect: number, PerformedWork: number, @@ -149,30 +123,9 @@ export function getInternalReactConstants( getDisplayNameForFiber: getDisplayNameForFiberType, getTypeSymbol: getTypeSymbolType, ReactPriorityLevels: ReactPriorityLevelsType, - ReactSymbols: ReactSymbolsType, ReactTypeOfSideEffect: ReactTypeOfSideEffectType, - ReactTypeOfWork: ReactTypeOfWorkType, + ReactTypeOfWork: WorkTagMap, |} { - const ReactSymbols: ReactSymbolsType = { - CONCURRENT_MODE_NUMBER: 0xeacf, - CONCURRENT_MODE_SYMBOL_STRING: 'Symbol(react.concurrent_mode)', - DEPRECATED_ASYNC_MODE_SYMBOL_STRING: 'Symbol(react.async_mode)', - CONTEXT_CONSUMER_NUMBER: 0xeace, - CONTEXT_CONSUMER_SYMBOL_STRING: 'Symbol(react.context)', - CONTEXT_PROVIDER_NUMBER: 0xeacd, - CONTEXT_PROVIDER_SYMBOL_STRING: 'Symbol(react.provider)', - FORWARD_REF_NUMBER: 0xead0, - FORWARD_REF_SYMBOL_STRING: 'Symbol(react.forward_ref)', - MEMO_NUMBER: 0xead3, - MEMO_SYMBOL_STRING: 'Symbol(react.memo)', - PROFILER_NUMBER: 0xead2, - PROFILER_SYMBOL_STRING: 'Symbol(react.profiler)', - STRICT_MODE_NUMBER: 0xeacc, - STRICT_MODE_SYMBOL_STRING: 'Symbol(react.strict_mode)', - SCOPE_NUMBER: 0xead7, - SCOPE_SYMBOL_STRING: 'Symbol(react.scope)', - }; - const ReactTypeOfSideEffect: ReactTypeOfSideEffectType = { NoEffect: 0b00, PerformedWork: 0b01, @@ -195,13 +148,14 @@ export function getInternalReactConstants( NoPriority: 90, }; - let ReactTypeOfWork: ReactTypeOfWorkType = ((null: any): ReactTypeOfWorkType); + let ReactTypeOfWork: WorkTagMap = ((null: any): WorkTagMap); // ********************************************************** // The section below is copied from files in React repo. // Keep it in sync, and add version guards if it changes. if (gte(version, '16.6.0-beta.0')) { ReactTypeOfWork = { + Block: 22, ClassComponent: 1, ContextConsumer: 9, ContextProvider: 10, @@ -228,6 +182,7 @@ export function getInternalReactConstants( }; } else if (gte(version, '16.4.3-alpha')) { ReactTypeOfWork = { + Block: -1, // Doesn't exist yet ClassComponent: 2, ContextConsumer: 11, ContextProvider: 12, @@ -254,6 +209,7 @@ export function getInternalReactConstants( }; } else { ReactTypeOfWork = { + Block: -1, // Doesn't exist yet ClassComponent: 2, ContextConsumer: 12, ContextProvider: 13, @@ -310,26 +266,6 @@ export function getInternalReactConstants( SuspenseListComponent, } = ReactTypeOfWork; - const { - CONCURRENT_MODE_NUMBER, - CONCURRENT_MODE_SYMBOL_STRING, - DEPRECATED_ASYNC_MODE_SYMBOL_STRING, - CONTEXT_PROVIDER_NUMBER, - CONTEXT_PROVIDER_SYMBOL_STRING, - CONTEXT_CONSUMER_NUMBER, - CONTEXT_CONSUMER_SYMBOL_STRING, - STRICT_MODE_NUMBER, - STRICT_MODE_SYMBOL_STRING, - PROFILER_NUMBER, - PROFILER_SYMBOL_STRING, - SCOPE_NUMBER, - SCOPE_SYMBOL_STRING, - FORWARD_REF_NUMBER, - FORWARD_REF_SYMBOL_STRING, - MEMO_NUMBER, - MEMO_SYMBOL_STRING, - } = ReactSymbols; - function resolveFiberType(type: any) { const typeSymbol = getTypeSymbol(type); switch (typeSymbol) { @@ -392,15 +328,15 @@ export function getInternalReactConstants( case CONCURRENT_MODE_SYMBOL_STRING: case DEPRECATED_ASYNC_MODE_SYMBOL_STRING: return null; - case CONTEXT_PROVIDER_NUMBER: - case CONTEXT_PROVIDER_SYMBOL_STRING: + case PROVIDER_NUMBER: + case PROVIDER_SYMBOL_STRING: // 16.3.0 exposed the context object as "context" // PR #12501 changed it to "_context" for 16.3.1+ // NOTE Keep in sync with inspectElementRaw() resolvedContext = fiber.type._context || fiber.type.context; return `${resolvedContext.displayName || 'Context'}.Provider`; - case CONTEXT_CONSUMER_NUMBER: - case CONTEXT_CONSUMER_SYMBOL_STRING: + case CONTEXT_NUMBER: + case CONTEXT_SYMBOL_STRING: // 16.3-16.5 read from "type" because the Consumer is the actual context object. // 16.6+ should read from "type._context" because Consumer can be different (in DEV). // NOTE Keep in sync with inspectElementRaw() @@ -431,7 +367,6 @@ export function getInternalReactConstants( getTypeSymbol, ReactPriorityLevels, ReactTypeOfWork, - ReactSymbols, ReactTypeOfSideEffect, }; } @@ -447,7 +382,6 @@ export function attach( getTypeSymbol, ReactPriorityLevels, ReactTypeOfWork, - ReactSymbols, ReactTypeOfSideEffect, } = getInternalReactConstants(renderer.version); const {NoEffect, PerformedWork, Placement} = ReactTypeOfSideEffect; @@ -477,19 +411,6 @@ export function attach( IdlePriority, NoPriority, } = ReactPriorityLevels; - const { - CONCURRENT_MODE_NUMBER, - CONCURRENT_MODE_SYMBOL_STRING, - DEPRECATED_ASYNC_MODE_SYMBOL_STRING, - CONTEXT_CONSUMER_NUMBER, - CONTEXT_CONSUMER_SYMBOL_STRING, - CONTEXT_PROVIDER_NUMBER, - CONTEXT_PROVIDER_SYMBOL_STRING, - PROFILER_NUMBER, - PROFILER_SYMBOL_STRING, - STRICT_MODE_NUMBER, - STRICT_MODE_SYMBOL_STRING, - } = ReactSymbols; const { overrideHookState, @@ -731,11 +652,11 @@ export function attach( case CONCURRENT_MODE_SYMBOL_STRING: case DEPRECATED_ASYNC_MODE_SYMBOL_STRING: return ElementTypeOtherOrUnknown; - case CONTEXT_PROVIDER_NUMBER: - case CONTEXT_PROVIDER_SYMBOL_STRING: + case PROVIDER_NUMBER: + case PROVIDER_SYMBOL_STRING: return ElementTypeContext; - case CONTEXT_CONSUMER_NUMBER: - case CONTEXT_CONSUMER_SYMBOL_STRING: + case CONTEXT_NUMBER: + case CONTEXT_SYMBOL_STRING: return ElementTypeContext; case STRICT_MODE_NUMBER: case STRICT_MODE_SYMBOL_STRING: @@ -2262,8 +2183,8 @@ export function attach( } } } else if ( - typeSymbol === CONTEXT_CONSUMER_NUMBER || - typeSymbol === CONTEXT_CONSUMER_SYMBOL_STRING + typeSymbol === CONTEXT_NUMBER || + typeSymbol === CONTEXT_SYMBOL_STRING ) { // 16.3-16.5 read from "type" because the Consumer is the actual context object. // 16.6+ should read from "type._context" because Consumer can be different (in DEV). @@ -2279,8 +2200,8 @@ export function attach( const currentType = current.type; const currentTypeSymbol = getTypeSymbol(currentType); if ( - currentTypeSymbol === CONTEXT_PROVIDER_NUMBER || - currentTypeSymbol === CONTEXT_PROVIDER_SYMBOL_STRING + currentTypeSymbol === PROVIDER_NUMBER || + currentTypeSymbol === PROVIDER_SYMBOL_STRING ) { // 16.3.0 exposed the context object as "context" // PR #12501 changed it to "_context" for 16.3.1+ diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 9b3c0ea335..81521bd577 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -25,6 +25,33 @@ export type WorkTag = number; export type SideEffectTag = number; export type ExpirationTime = number; +export type WorkTagMap = {| + Block: WorkTag, + ClassComponent: WorkTag, + ContextConsumer: WorkTag, + ContextProvider: WorkTag, + CoroutineComponent: WorkTag, + CoroutineHandlerPhase: WorkTag, + DehydratedSuspenseComponent: WorkTag, + ForwardRef: WorkTag, + Fragment: WorkTag, + FunctionComponent: WorkTag, + HostComponent: WorkTag, + HostPortal: WorkTag, + HostRoot: WorkTag, + HostText: WorkTag, + IncompleteClassComponent: WorkTag, + IndeterminateComponent: WorkTag, + LazyComponent: WorkTag, + MemoComponent: WorkTag, + Mode: WorkTag, + Profiler: WorkTag, + SimpleMemoComponent: WorkTag, + SuspenseComponent: WorkTag, + SuspenseListComponent: WorkTag, + YieldComponent: WorkTag, +|}; + // TODO: If it's useful for the frontend to know which types of data an Element has // (e.g. props, state, context, hooks) then we could add a bitmask field for this // to keep the number of attributes small. @@ -38,6 +65,7 @@ export type NativeType = Object; export type RendererID = number; type Dispatcher = any; +export type CurrentDispatcherRef = {|current: null | Dispatcher|}; export type GetDisplayNameForFiberID = ( id: number, @@ -77,7 +105,7 @@ export type ReactRenderer = { scheduleUpdate?: ?(fiber: Object) => void, setSuspenseHandler?: ?(shouldSuspend: (fiber: Object) => boolean) => void, // Only injected by React v16.8+ in order to support hooks inspection. - currentDispatcherRef?: {|current: null | Dispatcher|}, + currentDispatcherRef?: CurrentDispatcherRef, // Only injected by React v16.9+ in DEV mode. // Enables DevTools to append owners-only component stack to error messages. getCurrentFiber?: () => Fiber | null, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js index 5424d35039..ae0ef9493a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js @@ -463,7 +463,7 @@ function InspectedElementView({ ); } -// This function is based on packages/shared/describeComponentFrame.js +// This function is based on describeComponentFrame() in packages/shared/ReactComponentStackFrame function formatSourceForDisplay(fileName: string, lineNumber: string) { const BEFORE_SLASH_RE = /^(.*)[\\\/]/; diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index 824cd55b52..095c12cbd4 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.js @@ -42,6 +42,8 @@ const config = { plugins: [ new DefinePlugin({ __DEV__, + __PROFILE__: false, + __EXPERIMENTAL__: true, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, }), diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index a4ecac2fa6..672ed8d8aa 100644 --- a/packages/shared/ReactSymbols.js +++ b/packages/shared/ReactSymbols.js @@ -7,6 +7,10 @@ * @flow */ +// ATTENTION +// When adding new symbols to this file, +// Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols' + // The Symbol used to tag the ReactElement-like types. If there is no native Symbol // nor polyfill, then a plain number is used for performance. export let REACT_ELEMENT_TYPE = 0xeac7; diff --git a/scripts/jest/config.build-devtools.js b/scripts/jest/config.build-devtools.js index 9c8501486a..494cc4a8e7 100644 --- a/scripts/jest/config.build-devtools.js +++ b/scripts/jest/config.build-devtools.js @@ -26,13 +26,6 @@ const packages = readdirSync(packagesRoot).filter(dir => { // Create a module map to point React packages to the build output const moduleNameMapper = {}; -// Allow bundle tests to read (but not write!) default feature flags. -// This lets us determine whether we're running in different modes -// without making relevant tests internal-only. -moduleNameMapper[ - '^shared/ReactFeatureFlags' -] = `/packages/shared/forks/ReactFeatureFlags.readonly`; - // Map packages to bundles packages.forEach(name => { // Root entry point @@ -43,6 +36,11 @@ packages.forEach(name => { ] = `/build/node_modules/${name}/$1`; }); +// Allow tests to import shared code (e.g. feature flags, getStackByFiberInDevAndProd) +moduleNameMapper['^shared/([^/]+)$'] = '/packages/shared/$1'; +moduleNameMapper['^react-reconciler/([^/]+)$'] = + '/packages/react-reconciler/$1'; + module.exports = Object.assign({}, baseConfig, { // Redirect imports to the compiled bundles moduleNameMapper,