From 47ebddcc69749ed33a225cc510f0240cb095bf92 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 17 Apr 2020 10:40:40 -0700 Subject: [PATCH 1/8] DevTools console override handles new component stack format DevTools does not attempt to mimic the default browser console format for its component stacks but it does properly detect the new format for Chrome, Firefox, and Safari. --- .../react-devtools-shared/src/backend/console.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index b42cf2f9d3839..3635fc5937349 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -15,7 +15,12 @@ import type {ReactRenderer} from './types'; 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+$/; const injectedRenderers: Map< ReactRenderer, @@ -94,8 +99,11 @@ 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.exec(lastArg) || + ROW_COLUMN_NUMBER_REGEX.exec(lastArg)); if (!alreadyHasComponentStack) { // If there's a component stack for at least one of the injected renderers, append it. From 2b62646376529817920c6ebc48e89d2b705b740e Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 17 Apr 2020 11:35:16 -0700 Subject: [PATCH 2/8] Inject getStackByFiberInDevAndProd() to DevTools dquote> dquote> This enables "native" component stacks (clickable) for third party warnings as well. --- .../src/__tests__/console-test.js | 74 +++++++++++-------- .../src/backend/console.js | 42 +++++++---- .../src/backend/types.js | 2 + .../src/ReactFiberReconciler.new.js | 1 + .../src/ReactFiberReconciler.old.js | 1 + 5 files changed, 76 insertions(+), 44 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index 6d400164a8356..979d0fff530e4 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -140,7 +140,7 @@ describe('console', () => { ); - const Child = () => { + const Child = props => { fakeConsole.error('error'); fakeConsole.log('log'); fakeConsole.warn('warn'); @@ -149,20 +149,31 @@ describe('console', () => { act(() => ReactDOM.render(, document.createElement('div'))); - expect(mockLog).toHaveBeenCalledTimes(1); + // TRICKY DevTools console override re-renders the component to intentionally trigger an error, + // in which case all of the mock functions will be called an additional time. + + expect(mockError).toHaveBeenCalledTimes(2); + expect(mockError.mock.calls[0]).toHaveLength(1); + expect(mockError.mock.calls[0][0]).toBe('error'); + expect(mockError.mock.calls[1]).toHaveLength(2); + expect(mockError.mock.calls[1][0]).toBe('error'); + expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', + ); + + expect(mockLog).toHaveBeenCalledTimes(2); expect(mockLog.mock.calls[0]).toHaveLength(1); expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(2); + expect(mockLog.mock.calls[1]).toHaveLength(1); + expect(mockLog.mock.calls[1][0]).toBe('log'); + + expect(mockWarn).toHaveBeenCalledTimes(2); + expect(mockWarn.mock.calls[0]).toHaveLength(1); expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( - '\n in Child (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 **)', + expect(mockWarn.mock.calls[1]).toHaveLength(2); + expect(mockWarn.mock.calls[1][0]).toBe('warn'); + expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', ); }); @@ -198,23 +209,23 @@ 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 **)', ); }); @@ -254,23 +265,23 @@ 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 **)', ); }); @@ -303,13 +314,13 @@ 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 **)', ); }); @@ -333,16 +344,19 @@ describe('console', () => { patchConsole(); act(() => ReactDOM.render(, document.createElement('div'))); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( + // TRICKY DevTools console override re-renders the component to intentionally trigger an error, + // in which case all of the mock functions will be called an additional time. + + expect(mockWarn).toHaveBeenCalledTimes(3); + expect(mockWarn.mock.calls[2]).toHaveLength(2); + expect(mockWarn.mock.calls[2][0]).toBe('warn'); + expect(normalizeCodeLocInfo(mockWarn.mock.calls[2][1])).toEqual( '\n in Child (at **)', ); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( + expect(mockError).toHaveBeenCalledTimes(3); + expect(mockError.mock.calls[2]).toHaveLength(2); + expect(mockError.mock.calls[2][0]).toBe('error'); + expect(normalizeCodeLocInfo(mockError.mock.calls[2][1])).toBe( '\n in Child (at **)', ); }); diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 3635fc5937349..7f096ee80b746 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -27,6 +27,7 @@ const injectedRenderers: Map< {| getCurrentFiber: () => Fiber | null, getDisplayNameForFiber: (fiber: Fiber) => string | null, + getStackByFiberInDevAndProd?: (fiber: Object) => string, |}, > = new Map(); @@ -54,7 +55,12 @@ 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 { + getCurrentFiber, + getStackByFiberInDevAndProd, + findFiberByHostInstance, + version, + } = renderer; // Ignore React v15 and older because they don't expose a component stack anyway. if (typeof findFiberByHostInstance !== 'function') { @@ -67,6 +73,7 @@ export function registerRenderer(renderer: ReactRenderer): void { injectedRenderers.set(renderer, { getCurrentFiber, getDisplayNameForFiber, + getStackByFiberInDevAndProd, }); } } @@ -112,22 +119,29 @@ export function patch(): void { for (const { getCurrentFiber, getDisplayNameForFiber, + getStackByFiberInDevAndProd, } 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, - ); - - current = owner; + if (current !== null) { + if (typeof getStackByFiberInDevAndProd === 'function') { + ownerStack = getStackByFiberInDevAndProd(current); + } else { + while (current != null) { + const name = getDisplayNameForFiber(current); + const owner = current._debugOwner; + const ownerName = + owner != null ? getDisplayNameForFiber(owner) : null; + + ownerStack += describeComponentFrame( + name, + current._debugSource, + ownerName, + ); + + current = owner; + } + } } if (ownerStack !== '') { diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 9b3c0ea335ae3..42b674f0e707a 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -81,6 +81,8 @@ export type ReactRenderer = { // Only injected by React v16.9+ in DEV mode. // Enables DevTools to append owners-only component stack to error messages. getCurrentFiber?: () => Fiber | null, + // Only injected by v16.14+ to support "native" component stack format. + getStackByFiberInDevAndProd?: (fiber: Object) => string, // Uniquely identifies React DOM v15. ComponentTree?: any, // Present for React DOM v12 (possibly earlier) through v15. diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index cc2d64442039a..e176ccda34963 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -553,5 +553,6 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { setRefreshHandler: __DEV__ ? setRefreshHandler : null, // Enables DevTools to append owner stacks to error messages in DEV mode. getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null, + getStackByFiberInDevAndProd: getStackByFiberInDevAndProd, }); } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 38059c6820805..1546b8f7c340e 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -553,5 +553,6 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { setRefreshHandler: __DEV__ ? setRefreshHandler : null, // Enables DevTools to append owner stacks to error messages in DEV mode. getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null, + getStackByFiberInDevAndProd: getStackByFiberInDevAndProd, }); } From c6f7852804865e1006a451c26b76d8caa2d762a9 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 17 Apr 2020 13:57:34 -0700 Subject: [PATCH 3/8] DevTools imports getStackByFiberInDevAndProd() directly Added DevTools fork of ReactFeatureFlags to make this possible. --- .../react-devtools-core/webpack.backend.js | 1 + .../react-devtools-core/webpack.standalone.js | 1 + .../webpack.backend.js | 1 + .../webpack.config.js | 1 + .../react-devtools-inline/webpack.config.js | 1 + .../src/__tests__/console-test.js | 2 +- .../src/backend/console.js | 52 +++--------------- .../src/backend/types.js | 2 - .../react-devtools-shell/webpack.config.js | 1 + .../src/ReactFiberReconciler.new.js | 1 - .../src/ReactFiberReconciler.old.js | 1 - .../forks/ReactFeatureFlags.devtools.js | 54 +++++++++++++++++++ 12 files changed, 68 insertions(+), 50 deletions(-) create mode 100644 packages/shared/forks/ReactFeatureFlags.devtools.js diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js index ecd066af867a6..c92d7a3230f39 100644 --- a/packages/react-devtools-core/webpack.backend.js +++ b/packages/react-devtools-core/webpack.backend.js @@ -38,6 +38,7 @@ module.exports = { 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), + 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, plugins: [ diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js index a1882f20cf8ac..8f7465b26cd1a 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -37,6 +37,7 @@ module.exports = { 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), + 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, node: { diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js index d7de389f8ce08..e18468be94c73 100644 --- a/packages/react-devtools-extensions/webpack.backend.js +++ b/packages/react-devtools-extensions/webpack.backend.js @@ -33,6 +33,7 @@ module.exports = { 'react-dom': resolve(builtModulesDir, 'react-dom'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), + 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, plugins: [ diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index e6aa905e6c0fa..3e0a736e5481e 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -38,6 +38,7 @@ module.exports = { 'react-dom': resolve(builtModulesDir, 'react-dom'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), + 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, plugins: [ diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 7e24e54a0558f..bfd32965c0d2b 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -35,6 +35,7 @@ module.exports = { 'react-dom': 'react-dom', 'react-is': 'react-is', scheduler: 'scheduler', + 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, plugins: [ new DefinePlugin({ diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index 979d0fff530e4..a893ae2b3c8f9 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -140,7 +140,7 @@ describe('console', () => { ); - const Child = props => { + const Child = () => { fakeConsole.error('error'); fakeConsole.log('log'); fakeConsole.warn('warn'); diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 7f096ee80b746..481256149f31c 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -7,12 +7,11 @@ * @flow */ -import {getInternalReactConstants} from './renderer'; -import describeComponentFrame from './describeComponentFrame'; - import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {ReactRenderer} from './types'; +import {getStackByFiberInDevAndProd} from 'react-reconciler/src/ReactFiberComponentStack'; + const APPEND_STACK_TO_METHODS = ['error', 'trace', 'warn']; // React's custom built component stack strings match "\s{4}in" @@ -26,8 +25,6 @@ const injectedRenderers: Map< ReactRenderer, {| getCurrentFiber: () => Fiber | null, - getDisplayNameForFiber: (fiber: Fiber) => string | null, - getStackByFiberInDevAndProd?: (fiber: Object) => string, |}, > = new Map(); @@ -55,12 +52,7 @@ 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, - getStackByFiberInDevAndProd, - findFiberByHostInstance, - version, - } = renderer; + const {getCurrentFiber, findFiberByHostInstance} = renderer; // Ignore React v15 and older because they don't expose a component stack anyway. if (typeof findFiberByHostInstance !== 'function') { @@ -68,12 +60,8 @@ export function registerRenderer(renderer: ReactRenderer): void { } if (typeof getCurrentFiber === 'function') { - const {getDisplayNameForFiber} = getInternalReactConstants(version); - injectedRenderers.set(renderer, { getCurrentFiber, - getDisplayNameForFiber, - getStackByFiberInDevAndProd, }); } } @@ -116,36 +104,10 @@ export function patch(): void { // 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 { - getCurrentFiber, - getDisplayNameForFiber, - getStackByFiberInDevAndProd, - } of injectedRenderers.values()) { - let current: ?Fiber = getCurrentFiber(); - let ownerStack: string = ''; - if (current !== null) { - if (typeof getStackByFiberInDevAndProd === 'function') { - ownerStack = getStackByFiberInDevAndProd(current); - } else { - while (current != null) { - const name = getDisplayNameForFiber(current); - const owner = current._debugOwner; - const ownerName = - owner != null ? getDisplayNameForFiber(owner) : null; - - ownerStack += describeComponentFrame( - name, - current._debugSource, - ownerName, - ); - - current = owner; - } - } - } - - if (ownerStack !== '') { - args.push(ownerStack); + for (const {getCurrentFiber} of injectedRenderers.values()) { + const current: ?Fiber = getCurrentFiber(); + if (current != null) { + args.push(getStackByFiberInDevAndProd(current)); break; } } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 42b674f0e707a..9b3c0ea335ae3 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -81,8 +81,6 @@ export type ReactRenderer = { // Only injected by React v16.9+ in DEV mode. // Enables DevTools to append owners-only component stack to error messages. getCurrentFiber?: () => Fiber | null, - // Only injected by v16.14+ to support "native" component stack format. - getStackByFiberInDevAndProd?: (fiber: Object) => string, // Uniquely identifies React DOM v15. ComponentTree?: any, // Present for React DOM v12 (possibly earlier) through v15. diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index 824cd55b522cb..6962b004c7bdf 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.js @@ -37,6 +37,7 @@ const config = { 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), + 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, plugins: [ diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index e176ccda34963..cc2d64442039a 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -553,6 +553,5 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { setRefreshHandler: __DEV__ ? setRefreshHandler : null, // Enables DevTools to append owner stacks to error messages in DEV mode. getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null, - getStackByFiberInDevAndProd: getStackByFiberInDevAndProd, }); } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 1546b8f7c340e..38059c6820805 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -553,6 +553,5 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean { setRefreshHandler: __DEV__ ? setRefreshHandler : null, // Enables DevTools to append owner stacks to error messages in DEV mode. getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null, - getStackByFiberInDevAndProd: getStackByFiberInDevAndProd, }); } diff --git a/packages/shared/forks/ReactFeatureFlags.devtools.js b/packages/shared/forks/ReactFeatureFlags.devtools.js new file mode 100644 index 0000000000000..3fef083458316 --- /dev/null +++ b/packages/shared/forks/ReactFeatureFlags.devtools.js @@ -0,0 +1,54 @@ +/** + * 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 typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags'; +import typeof * as ExportsType from './ReactFeatureFlags.test-renderer'; + +export const enableFilterEmptyStringAttributesDOM = false; +export const enableDebugTracing = false; +export const debugRenderPhaseSideEffectsForStrictMode = false; +export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false; +export const warnAboutDeprecatedLifecycles = true; +export const enableProfilerTimer = false; +export const enableProfilerCommitHooks = false; +export const enableSchedulerTracing = false; +export const enableSuspenseServerRenderer = false; +export const enableSelectiveHydration = false; +export const enableBlocksAPI = false; +export const enableSchedulerDebugging = false; +export const disableJavaScriptURLs = false; +export const enableDeprecatedFlareAPI = false; +export const enableFundamentalAPI = false; +export const enableScopeAPI = false; +export const enableUseEventAPI = false; +export const warnAboutUnmockedScheduler = false; +export const enableSuspenseCallback = false; +export const warnAboutDefaultPropsOnFunctionComponents = false; +export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; +export const enableTrustedTypesIntegration = false; +export const runAllPassiveEffectDestroysBeforeCreates = false; +export const deferPassiveEffectCleanupDuringUnmount = false; +export const warnAboutSpreadingKeyToJSX = false; +export const enableComponentStackLocations = true; +export const throwEarlyForMysteriousError = false; +export const enableNewReconciler = false; +export const disableInputAttributeSyncing = false; +export const warnAboutStringRefs = false; +export const disableLegacyContext = false; +export const disableTextareaChildren = false; +export const disableModulePatternComponents = false; +export const warnUnstableRenderSubtreeIntoContainer = false; +export const enableModernEventSystem = false; +export const enableLegacyFBSupport = false; + +// Flow magic to verify the exports of this file match the original version. +// eslint-disable-next-line no-unused-vars +type Check<_X, Y: _X, X: Y = _X> = null; +// eslint-disable-next-line no-unused-expressions +(null: Check); From a775fa4b52e6ed3f8de268e8193d6afc15bba35a Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 17 Apr 2020 14:24:12 -0700 Subject: [PATCH 4/8] Backed out DevTools feature flag fork in favor of global env vars --- .../react-devtools-core/webpack.backend.js | 3 +- .../react-devtools-core/webpack.standalone.js | 3 +- .../webpack.backend.js | 3 +- .../webpack.config.js | 3 +- .../react-devtools-inline/webpack.config.js | 3 +- .../src/__tests__/console-test.js | 66 ++++++++----------- .../src/backend/console.js | 5 +- .../react-devtools-shell/webpack.config.js | 3 +- .../forks/ReactFeatureFlags.devtools.js | 54 --------------- scripts/jest/config.build-devtools.js | 12 ++-- 10 files changed, 47 insertions(+), 108 deletions(-) delete mode 100644 packages/shared/forks/ReactFeatureFlags.devtools.js diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js index c92d7a3230f39..1461545d29d57 100644 --- a/packages/react-devtools-core/webpack.backend.js +++ b/packages/react-devtools-core/webpack.backend.js @@ -38,12 +38,13 @@ module.exports = { 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), - 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, 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 8f7465b26cd1a..61c1d962372b5 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -37,7 +37,6 @@ module.exports = { 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), - 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, node: { @@ -49,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 e18468be94c73..1f649dbb6db58 100644 --- a/packages/react-devtools-extensions/webpack.backend.js +++ b/packages/react-devtools-extensions/webpack.backend.js @@ -33,12 +33,13 @@ module.exports = { 'react-dom': resolve(builtModulesDir, 'react-dom'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), - 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, 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 3e0a736e5481e..b22ea605699e7 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -38,12 +38,13 @@ module.exports = { 'react-dom': resolve(builtModulesDir, 'react-dom'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), - 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, 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 bfd32965c0d2b..3f0677f80b793 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -35,11 +35,12 @@ module.exports = { 'react-dom': 'react-dom', 'react-is': 'react-is', scheduler: 'scheduler', - 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, 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 a893ae2b3c8f9..74e9fed9ff8ef 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'); @@ -149,42 +149,31 @@ describe('console', () => { act(() => ReactDOM.render(, document.createElement('div'))); - // TRICKY DevTools console override re-renders the component to intentionally trigger an error, - // in which case all of the mock functions will be called an additional time. - - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( - '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - - expect(mockLog).toHaveBeenCalledTimes(2); + expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog.mock.calls[0]).toHaveLength(1); expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockLog.mock.calls[1]).toHaveLength(1); - expect(mockLog.mock.calls[1][0]).toBe('log'); - - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(1); + expect(mockWarn).toHaveBeenCalledTimes(1); + expect(mockWarn.mock.calls[0]).toHaveLength(2); expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( + expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + '\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 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'); @@ -231,7 +220,7 @@ describe('console', () => { it('should append component stacks to errors and warnings logged from commit hooks', () => { const Intermediate = ({children}) => children; - const Parent = () => ( + const Parent = ({children}) => ( @@ -287,7 +276,7 @@ describe('console', () => { it('should append component stacks to errors and warnings logged from gDSFP', () => { const Intermediate = ({children}) => children; - const Parent = () => ( + const Parent = ({children}) => ( @@ -325,7 +314,7 @@ describe('console', () => { }); it('should append stacks after being uninstalled and reinstalled', () => { - const Child = () => { + const Child = ({children}) => { fakeConsole.warn('warn'); fakeConsole.error('error'); return null; @@ -344,19 +333,16 @@ describe('console', () => { patchConsole(); act(() => ReactDOM.render(, document.createElement('div'))); - // TRICKY DevTools console override re-renders the component to intentionally trigger an error, - // in which case all of the mock functions will be called an additional time. - - expect(mockWarn).toHaveBeenCalledTimes(3); - expect(mockWarn.mock.calls[2]).toHaveLength(2); - expect(mockWarn.mock.calls[2][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[2][1])).toEqual( + expect(mockWarn).toHaveBeenCalledTimes(2); + expect(mockWarn.mock.calls[1]).toHaveLength(2); + expect(mockWarn.mock.calls[1][0]).toBe('warn'); + expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( '\n in Child (at **)', ); - expect(mockError).toHaveBeenCalledTimes(3); - expect(mockError.mock.calls[2]).toHaveLength(2); - expect(mockError.mock.calls[2][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[2][1])).toBe( + expect(mockError).toHaveBeenCalledTimes(2); + expect(mockError.mock.calls[1]).toHaveLength(2); + expect(mockError.mock.calls[1][0]).toBe('error'); + expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( '\n in Child (at **)', ); }); diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 481256149f31c..806ff21982f5e 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -107,7 +107,10 @@ export function patch(): void { for (const {getCurrentFiber} of injectedRenderers.values()) { const current: ?Fiber = getCurrentFiber(); if (current != null) { - args.push(getStackByFiberInDevAndProd(current)); + const componentStack = getStackByFiberInDevAndProd(current); + if (componentStack !== '') { + args.push(componentStack); + } break; } } diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index 6962b004c7bdf..095c12cbd4709 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.js @@ -37,12 +37,13 @@ const config = { 'react-debug-tools': resolve(builtModulesDir, 'react-debug-tools'), 'react-is': resolve(builtModulesDir, 'react-is'), scheduler: resolve(builtModulesDir, 'scheduler'), - 'shared/ReactFeatureFlags': 'shared/forks/ReactFeatureFlags.devtools', }, }, 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/forks/ReactFeatureFlags.devtools.js b/packages/shared/forks/ReactFeatureFlags.devtools.js deleted file mode 100644 index 3fef083458316..0000000000000 --- a/packages/shared/forks/ReactFeatureFlags.devtools.js +++ /dev/null @@ -1,54 +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 - */ - -import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags'; -import typeof * as ExportsType from './ReactFeatureFlags.test-renderer'; - -export const enableFilterEmptyStringAttributesDOM = false; -export const enableDebugTracing = false; -export const debugRenderPhaseSideEffectsForStrictMode = false; -export const replayFailedUnitOfWorkWithInvokeGuardedCallback = false; -export const warnAboutDeprecatedLifecycles = true; -export const enableProfilerTimer = false; -export const enableProfilerCommitHooks = false; -export const enableSchedulerTracing = false; -export const enableSuspenseServerRenderer = false; -export const enableSelectiveHydration = false; -export const enableBlocksAPI = false; -export const enableSchedulerDebugging = false; -export const disableJavaScriptURLs = false; -export const enableDeprecatedFlareAPI = false; -export const enableFundamentalAPI = false; -export const enableScopeAPI = false; -export const enableUseEventAPI = false; -export const warnAboutUnmockedScheduler = false; -export const enableSuspenseCallback = false; -export const warnAboutDefaultPropsOnFunctionComponents = false; -export const disableSchedulerTimeoutBasedOnReactExpirationTime = false; -export const enableTrustedTypesIntegration = false; -export const runAllPassiveEffectDestroysBeforeCreates = false; -export const deferPassiveEffectCleanupDuringUnmount = false; -export const warnAboutSpreadingKeyToJSX = false; -export const enableComponentStackLocations = true; -export const throwEarlyForMysteriousError = false; -export const enableNewReconciler = false; -export const disableInputAttributeSyncing = false; -export const warnAboutStringRefs = false; -export const disableLegacyContext = false; -export const disableTextareaChildren = false; -export const disableModulePatternComponents = false; -export const warnUnstableRenderSubtreeIntoContainer = false; -export const enableModernEventSystem = false; -export const enableLegacyFBSupport = false; - -// Flow magic to verify the exports of this file match the original version. -// eslint-disable-next-line no-unused-vars -type Check<_X, Y: _X, X: Y = _X> = null; -// eslint-disable-next-line no-unused-expressions -(null: Check); diff --git a/scripts/jest/config.build-devtools.js b/scripts/jest/config.build-devtools.js index 9c8501486ab49..494cc4a8e721a 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, From cba7e949c513da877dbf1963abcb3eebc4142ee0 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 20 Apr 2020 15:28:35 -0700 Subject: [PATCH 5/8] Forked ComponentStackFrame and FiberComponentStack into DevTools --- .../backend/DevToolsComponentStackFrame.js | 298 ++++++++++++++++++ .../backend/DevToolsFiberComponentStack.js | 108 +++++++ .../src/backend/ReactSymbols.js | 82 +++++ .../src/backend/console.js | 34 +- .../src/backend/describeComponentFrame.js | 48 --- .../src/backend/renderer.js | 129 ++------ .../src/backend/types.js | 30 +- .../views/Components/SelectedElement.js | 2 +- packages/shared/ReactSymbols.js | 4 + 9 files changed, 575 insertions(+), 160 deletions(-) create mode 100644 packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js create mode 100644 packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js create mode 100644 packages/react-devtools-shared/src/backend/ReactSymbols.js delete mode 100644 packages/react-devtools-shared/src/backend/describeComponentFrame.js 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 0000000000000..f4aab13ece0c5 --- /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 0000000000000..ecb2ac6d3be58 --- /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 0000000000000..99dd55e8037d2 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/ReactSymbols.js @@ -0,0 +1,82 @@ +/** + * 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 CONTEXT_CONSUMER_NUMBER = 0xeace; +export const CONTEXT_CONSUMER_SYMBOL_STRING = 'Symbol(react.context)'; + +export const CONTEXT_PROVIDER_NUMBER = 0xeacd; +export const CONTEXT_PROVIDER_SYMBOL_STRING = 'Symbol(react.provider)'; + +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 806ff21982f5e..df65f0b8ab090 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -8,9 +8,10 @@ */ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; -import type {ReactRenderer} from './types'; +import type {CurrentDispatcherRef, ReactRenderer, WorkTagMap} from './types'; -import {getStackByFiberInDevAndProd} from 'react-reconciler/src/ReactFiberComponentStack'; +import {getInternalReactConstants} from './renderer'; +import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack'; const APPEND_STACK_TO_METHODS = ['error', 'trace', 'warn']; @@ -24,7 +25,9 @@ const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+$/; const injectedRenderers: Map< ReactRenderer, {| + currentDispatcherRef: CurrentDispatcherRef, getCurrentFiber: () => Fiber | null, + workTagMap: WorkTagMap, |}, > = new Map(); @@ -52,16 +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} = 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') { + // 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, + workTagMap: ReactTypeOfWork, }); } } @@ -104,10 +118,18 @@ export function patch(): void { // 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 {getCurrentFiber} of injectedRenderers.values()) { + for (const { + currentDispatcherRef, + getCurrentFiber, + workTagMap, + } of injectedRenderers.values()) { const current: ?Fiber = getCurrentFiber(); if (current != null) { - const componentStack = getStackByFiberInDevAndProd(current); + const componentStack = getStackByFiberInDevAndProd( + workTagMap, + current, + currentDispatcherRef, + ); if (componentStack !== '') { args.push(componentStack); } 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 1be9d86494211..0000000000000 --- 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 dbe7270527386..209d306f5ef84 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, + 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, +} 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) { @@ -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, diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 9b3c0ea335ae3..81521bd577c4a 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 5424d35039cb3..ae0ef9493a6c9 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/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js index a4ecac2fa6608..672ed8d8aa27d 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; From 6e98033928f2fbe0247b0e3e6f6da6442ec31dc4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Mon, 20 Apr 2020 16:38:27 -0700 Subject: [PATCH 6/8] Typo fix --- .../src/backend/ReactSymbols.js | 45 +++++++++---------- .../src/backend/renderer.js | 32 ++++++------- 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/ReactSymbols.js b/packages/react-devtools-shared/src/backend/ReactSymbols.js index 99dd55e8037d2..677c0a9d66582 100644 --- a/packages/react-devtools-shared/src/backend/ReactSymbols.js +++ b/packages/react-devtools-shared/src/backend/ReactSymbols.js @@ -14,69 +14,64 @@ // 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 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 CONTEXT_CONSUMER_NUMBER = 0xeace; -export const CONTEXT_CONSUMER_SYMBOL_STRING = 'Symbol(react.context)'; - -export const CONTEXT_PROVIDER_NUMBER = 0xeacd; -export const CONTEXT_PROVIDER_SYMBOL_STRING = 'Symbol(react.provider)'; +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 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 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 FORWARD_REF_SYMBOL_STRING = 'Symbol(react.forward_ref)'; export const FRAGMENT_NUMBER = 0xeacb; -export const FRAGMENT_SYMBOL_STRING = 'Symbol(react.fragment'; +export const FRAGMENT_SYMBOL_STRING = 'Symbol(react.fragment)'; export const FUNDAMENTAL_NUMBER = 0xead5; -export const FUNDAMENTAL_SYMBOL_STRING = 'Symbol(react.fundamental'; +export const FUNDAMENTAL_SYMBOL_STRING = 'Symbol(react.fundamental)'; export const LAZY_NUMBER = 0xead4; -export const LAZY_SYMBOL_STRING = 'Symbol(react.lazy'; +export const LAZY_SYMBOL_STRING = 'Symbol(react.lazy)'; export const MEMO_NUMBER = 0xead3; -export const MEMO_SYMBOL_STRING = 'Symbol(react.memo'; +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 OPAQUE_ID_SYMBOL_STRING = 'Symbol(react.opaque.id)'; export const PORTAL_NUMBER = 0xeaca; -export const PORTAL_SYMBOL_STRING = 'Symbol(react.portal'; +export const PORTAL_SYMBOL_STRING = 'Symbol(react.portal)'; export const PROFILER_NUMBER = 0xead2; -export const PROFILER_SYMBOL_STRING = 'Symbol(react.profiler'; +export const PROFILER_SYMBOL_STRING = 'Symbol(react.profiler)'; export const PROVIDER_NUMBER = 0xeacd; -export const PROVIDER_SYMBOL_STRING = 'Symbol(react.provider'; +export const PROVIDER_SYMBOL_STRING = 'Symbol(react.provider)'; export const RESPONDER_NUMBER = 0xead6; -export const RESPONDER_SYMBOL_STRING = 'Symbol(react.responder'; +export const RESPONDER_SYMBOL_STRING = 'Symbol(react.responder)'; export const SCOPE_NUMBER = 0xead7; -export const SCOPE_SYMBOL_STRING = 'Symbol(react.scope'; +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 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 STRICT_MODE_SYMBOL_STRING = 'Symbol(react.strict_mode)'; export const SUSPENSE_NUMBER = 0xead1; -export const SUSPENSE_SYMBOL_STRING = 'Symbol(react.suspense'; +export const SUSPENSE_SYMBOL_STRING = 'Symbol(react.suspense)'; export const SUSPENSE_LIST_NUMBER = 0xead8; -export const SUSPENSE_LIST_SYMBOL_STRING = 'Symbol(react.suspense_list'; +export const SUSPENSE_LIST_SYMBOL_STRING = 'Symbol(react.suspense_list)'; diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 209d306f5ef84..70bbac1949f73 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -53,10 +53,10 @@ import { 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, + PROVIDER_NUMBER, + PROVIDER_SYMBOL_STRING, + CONTEXT_NUMBER, + CONTEXT_SYMBOL_STRING, STRICT_MODE_NUMBER, STRICT_MODE_SYMBOL_STRING, PROFILER_NUMBER, @@ -328,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() @@ -652,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: @@ -2183,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). @@ -2200,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+ From 6474d2fea79c0002465a3e92ab72ea7c0cd3da0f Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 21 Apr 2020 11:29:26 -0700 Subject: [PATCH 7/8] Replaced regex .exec() with .test() --- packages/react-devtools-shared/src/backend/console.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index df65f0b8ab090..25ef0fb0ded86 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -111,8 +111,8 @@ export function patch(): void { const lastArg = args.length > 0 ? args[args.length - 1] : null; const alreadyHasComponentStack = lastArg !== null && - (PREFIX_REGEX.exec(lastArg) || - ROW_COLUMN_NUMBER_REGEX.exec(lastArg)); + (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. From 2033ced1a4800102da25e667d8bc8c6982cc9527 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 21 Apr 2020 11:31:58 -0700 Subject: [PATCH 8/8] Made fallback regex more multiline friendly --- packages/react-devtools-shared/src/backend/console.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 25ef0fb0ded86..80a69701158ca 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -20,7 +20,7 @@ const APPEND_STACK_TO_METHODS = ['error', 'trace', 'warn']; 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+$/; +const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/; const injectedRenderers: Map< ReactRenderer,