DevTools: Improved "native" component stacks (#18656)

* 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.
This commit is contained in:
Brian Vaughn 2020-04-21 11:46:11 -07:00 committed by GitHub
parent 940f48b999
commit 36cab2720a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 630 additions and 223 deletions

View File

@ -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}"`,
}),

View File

@ -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}"`,

View File

@ -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}"`,
}),

View File

@ -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}"`,

View File

@ -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}"`,

View File

@ -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}) => (
<Intermediate>
<Child />
</Intermediate>
);
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}) => (
<Intermediate>
<Child />
</Intermediate>
);
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}) => (
<Intermediate>
<Child />
</Intermediate>
@ -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}) => (
<Intermediate>
<Child />
</Intermediate>
@ -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;

View File

@ -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<any, any> = (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 '';
}

View File

@ -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;
}
}

View File

@ -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)';

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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+

View File

@ -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,

View File

@ -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 = /^(.*)[\\\/]/;

View File

@ -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}"`,
}),

View File

@ -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;

View File

@ -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'
] = `<rootDir>/packages/shared/forks/ReactFeatureFlags.readonly`;
// Map packages to bundles
packages.forEach(name => {
// Root entry point
@ -43,6 +36,11 @@ packages.forEach(name => {
] = `<rootDir>/build/node_modules/${name}/$1`;
});
// Allow tests to import shared code (e.g. feature flags, getStackByFiberInDevAndProd)
moduleNameMapper['^shared/([^/]+)$'] = '<rootDir>/packages/shared/$1';
moduleNameMapper['^react-reconciler/([^/]+)$'] =
'<rootDir>/packages/react-reconciler/$1';
module.exports = Object.assign({}, baseConfig, {
// Redirect imports to the compiled bundles
moduleNameMapper,