mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
Patch console to append component stacks (#348)
* Patch console.warn and console.error to auto-append owners-only component stacks. This setting is enabled by default and will work for React Native even if no front-end DevTools shell is being used. The setting can be disabled via a new, persisted user preference though.
This commit is contained in:
@@ -66,7 +66,7 @@ declare module 'react-test-renderer' {
|
||||
options?: TestRendererOptions
|
||||
): ReactTestRenderer;
|
||||
|
||||
declare function act(callback: () => void): Thenable;
|
||||
declare function act(callback: () => ?Thenable): Thenable;
|
||||
}
|
||||
|
||||
declare module 'react-test-renderer/shallow' {
|
||||
|
||||
10
package.json
10
package.json
@@ -138,18 +138,18 @@
|
||||
"opener": "^1.5.1",
|
||||
"prettier": "^1.16.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^0.0.0-50b50c26f",
|
||||
"react": "^0.0.0-424099da6",
|
||||
"react-15": "npm:react@^15",
|
||||
"react-color": "^2.11.7",
|
||||
"react-dom": "^0.0.0-50b50c26f",
|
||||
"react-dom": "^0.0.0-424099da6",
|
||||
"react-dom-15": "npm:react-dom@^15",
|
||||
"react-is": "^0.0.0-50b50c26f",
|
||||
"react-test-renderer": "^0.0.0-50b50c26f",
|
||||
"react-is": "0.0.0-424099da6",
|
||||
"react-test-renderer": "^0.0.0-424099da6",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "./vendor/react-window",
|
||||
"request-promise": "^4.2.4",
|
||||
"rimraf": "^2.6.3",
|
||||
"scheduler": "^0.0.0-50b50c26f",
|
||||
"scheduler": "^0.0.0-424099da6",
|
||||
"semver": "^5.5.1",
|
||||
"serve-static": "^1.14.1",
|
||||
"style-loader": "^0.23.1",
|
||||
|
||||
14
packages/react-devtools-core/src/standalone.js
vendored
14
packages/react-devtools-core/src/standalone.js
vendored
@@ -9,7 +9,7 @@ import {
|
||||
} from 'react-dom';
|
||||
import Bridge from 'src/bridge';
|
||||
import Store from 'src/devtools/store';
|
||||
import { getSavedComponentFilters } from 'src/utils';
|
||||
import { getSavedComponentFilters, getAppendComponentStack } from 'src/utils';
|
||||
import { Server } from 'ws';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { installHook } from 'src/hook';
|
||||
@@ -241,12 +241,16 @@ function startServer(port?: number = 8097) {
|
||||
// because they are generally stored in localStorage within the context of the extension.
|
||||
// Because of this it relies on the extension to pass filters, so include them wth the response here.
|
||||
// This will ensure that saved filters are shared across different web pages.
|
||||
const savedFiltersString = `window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
|
||||
getSavedComponentFilters()
|
||||
)};`;
|
||||
const savedPreferencesString = `
|
||||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
|
||||
getSavedComponentFilters()
|
||||
)};
|
||||
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
|
||||
getAppendComponentStack()
|
||||
)};`;
|
||||
|
||||
response.end(
|
||||
savedFiltersString +
|
||||
savedPreferencesString +
|
||||
'\n;' +
|
||||
backendFile.toString() +
|
||||
'\n;' +
|
||||
|
||||
@@ -6,7 +6,7 @@ import Bridge from 'src/bridge';
|
||||
import Store from 'src/devtools/store';
|
||||
import inject from './inject';
|
||||
import { createViewElementSource, getBrowserTheme } from './utils';
|
||||
import { getSavedComponentFilters } from 'src/utils';
|
||||
import { getSavedComponentFilters, getAppendComponentStack } from 'src/utils';
|
||||
import {
|
||||
localStorageGetItem,
|
||||
localStorageRemoveItem,
|
||||
@@ -22,16 +22,23 @@ let panelCreated = false;
|
||||
// The renderer interface can't read saved component filters directly,
|
||||
// because they are stored in localStorage within the context of the extension.
|
||||
// Instead it relies on the extension to pass filters through.
|
||||
function initializeSavedComponentFilters() {
|
||||
function syncSavedPreferences() {
|
||||
const componentFilters = getSavedComponentFilters();
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
|
||||
componentFilters
|
||||
)};`
|
||||
);
|
||||
|
||||
const appendComponentStack = getAppendComponentStack();
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
|
||||
appendComponentStack
|
||||
)};`
|
||||
);
|
||||
}
|
||||
|
||||
initializeSavedComponentFilters();
|
||||
syncSavedPreferences();
|
||||
|
||||
function createPanelIfReactLoaded() {
|
||||
if (panelCreated) {
|
||||
@@ -286,7 +293,7 @@ function createPanelIfReactLoaded() {
|
||||
chrome.devtools.network.onNavigated.addListener(function onNavigated() {
|
||||
// Re-initialize saved filters on navigation,
|
||||
// since global values stored on window get reset in this case.
|
||||
initializeSavedComponentFilters();
|
||||
syncSavedPreferences();
|
||||
|
||||
// It's easiest to recreate the DevTools panel (to clean up potential stale state).
|
||||
// We can revisit this in the future as a small optimization.
|
||||
@@ -302,7 +309,7 @@ function createPanelIfReactLoaded() {
|
||||
|
||||
// Load (or reload) the DevTools extension when the user navigates to a new page.
|
||||
function checkPageForReact() {
|
||||
initializeSavedComponentFilters();
|
||||
syncSavedPreferences();
|
||||
createPanelIfReactLoaded();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { installHook } from 'src/hook';
|
||||
import { initDevTools } from 'src/devtools';
|
||||
import Store from 'src/devtools/store';
|
||||
import DevTools from 'src/devtools/views/DevTools';
|
||||
import { getSavedComponentFilters } from 'src/utils';
|
||||
import { getSavedComponentFilters, getAppendComponentStack } from 'src/utils';
|
||||
|
||||
const iframe = ((document.getElementById('target'): any): HTMLIFrameElement);
|
||||
|
||||
@@ -18,6 +18,7 @@ const { contentDocument, contentWindow } = iframe;
|
||||
// because they are stored in localStorage within the context of the extension.
|
||||
// Instead it relies on the extension to pass filters through.
|
||||
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getSavedComponentFilters();
|
||||
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = getAppendComponentStack();
|
||||
|
||||
installHook(contentWindow);
|
||||
|
||||
|
||||
349
src/__tests__/console-test.js
Normal file
349
src/__tests__/console-test.js
Normal file
@@ -0,0 +1,349 @@
|
||||
// @flow
|
||||
|
||||
describe('console', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let act;
|
||||
let enableConsole;
|
||||
let disableConsole;
|
||||
let fakeConsole;
|
||||
let mockError;
|
||||
let mockLog;
|
||||
let mockWarn;
|
||||
let patchConsole;
|
||||
let unpatchConsole;
|
||||
|
||||
beforeEach(() => {
|
||||
const Console = require('../backend/console');
|
||||
enableConsole = Console.enable;
|
||||
disableConsole = Console.disable;
|
||||
patchConsole = Console.patch;
|
||||
unpatchConsole = Console.unpatch;
|
||||
|
||||
const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
|
||||
global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
|
||||
inject(internals);
|
||||
|
||||
Console.registerRenderer(internals);
|
||||
};
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
|
||||
const utils = require('./utils');
|
||||
act = utils.act;
|
||||
|
||||
// Patch a fake console so we can verify with tests below.
|
||||
// Patching the real console is too complicated,
|
||||
// because Jest itself has hooks into it as does our test env setup.
|
||||
mockError = jest.fn();
|
||||
mockLog = jest.fn();
|
||||
mockWarn = jest.fn();
|
||||
fakeConsole = {
|
||||
error: mockError,
|
||||
log: mockLog,
|
||||
warn: mockWarn,
|
||||
};
|
||||
|
||||
patchConsole(fakeConsole);
|
||||
});
|
||||
|
||||
function normalizeCodeLocInfo(str) {
|
||||
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
|
||||
}
|
||||
|
||||
it('should only patch the console once', () => {
|
||||
const { error, warn } = fakeConsole;
|
||||
|
||||
patchConsole(fakeConsole);
|
||||
|
||||
expect(fakeConsole.error).toBe(error);
|
||||
expect(fakeConsole.warn).toBe(warn);
|
||||
});
|
||||
|
||||
it('should un-patch when requested', () => {
|
||||
expect(fakeConsole.error).not.toBe(mockError);
|
||||
expect(fakeConsole.warn).not.toBe(mockWarn);
|
||||
|
||||
unpatchConsole();
|
||||
|
||||
expect(fakeConsole.error).toBe(mockError);
|
||||
expect(fakeConsole.warn).toBe(mockWarn);
|
||||
});
|
||||
|
||||
it('should pass through logs when there is no current fiber', () => {
|
||||
expect(mockLog).toHaveBeenCalledTimes(0);
|
||||
expect(mockWarn).toHaveBeenCalledTimes(0);
|
||||
expect(mockError).toHaveBeenCalledTimes(0);
|
||||
fakeConsole.log('log');
|
||||
fakeConsole.warn('warn');
|
||||
fakeConsole.error('error');
|
||||
expect(mockLog).toHaveBeenCalledTimes(1);
|
||||
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(1);
|
||||
expect(mockWarn.mock.calls[0][0]).toBe('warn');
|
||||
expect(mockError).toHaveBeenCalledTimes(1);
|
||||
expect(mockError.mock.calls[0]).toHaveLength(1);
|
||||
expect(mockError.mock.calls[0][0]).toBe('error');
|
||||
});
|
||||
|
||||
it('should suppress console logging when disabled', () => {
|
||||
disableConsole();
|
||||
fakeConsole.log('log');
|
||||
fakeConsole.warn('warn');
|
||||
fakeConsole.error('error');
|
||||
expect(mockLog).toHaveBeenCalledTimes(0);
|
||||
expect(mockWarn).toHaveBeenCalledTimes(0);
|
||||
expect(mockError).toHaveBeenCalledTimes(0);
|
||||
|
||||
enableConsole();
|
||||
fakeConsole.log('log');
|
||||
fakeConsole.warn('warn');
|
||||
fakeConsole.error('error');
|
||||
expect(mockLog).toHaveBeenCalledTimes(1);
|
||||
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(1);
|
||||
expect(mockWarn.mock.calls[0][0]).toBe('warn');
|
||||
expect(mockError).toHaveBeenCalledTimes(1);
|
||||
expect(mockError.mock.calls[0]).toHaveLength(1);
|
||||
expect(mockError.mock.calls[0][0]).toBe('error');
|
||||
});
|
||||
|
||||
it('should not append multiple stacks', () => {
|
||||
const Child = () => {
|
||||
fakeConsole.warn('warn\n in Child (at fake.js:123)');
|
||||
fakeConsole.error('error', '\n in Child (at fake.js:123)');
|
||||
return null;
|
||||
};
|
||||
|
||||
act(() => ReactDOM.render(<Child />, document.createElement('div')));
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledTimes(1);
|
||||
expect(mockWarn.mock.calls[0]).toHaveLength(1);
|
||||
expect(mockWarn.mock.calls[0][0]).toBe(
|
||||
'warn\n in Child (at fake.js:123)'
|
||||
);
|
||||
expect(mockError).toHaveBeenCalledTimes(1);
|
||||
expect(mockError.mock.calls[0]).toHaveLength(2);
|
||||
expect(mockError.mock.calls[0][0]).toBe('error');
|
||||
expect(mockError.mock.calls[0][1]).toBe('\n in Child (at fake.js:123)');
|
||||
});
|
||||
|
||||
it('should append component stacks to errors and warnings logged during render', () => {
|
||||
const Intermediate = ({ children }) => children;
|
||||
const Parent = () => (
|
||||
<Intermediate>
|
||||
<Child />
|
||||
</Intermediate>
|
||||
);
|
||||
const Child = () => {
|
||||
fakeConsole.error('error');
|
||||
fakeConsole.log('log');
|
||||
fakeConsole.warn('warn');
|
||||
return null;
|
||||
};
|
||||
|
||||
act(() => ReactDOM.render(<Parent />, document.createElement('div')));
|
||||
|
||||
expect(mockLog).toHaveBeenCalledTimes(1);
|
||||
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(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 **)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should append component stacks to errors and warnings logged from effects', () => {
|
||||
const Intermediate = ({ children }) => children;
|
||||
const Parent = () => (
|
||||
<Intermediate>
|
||||
<Child />
|
||||
</Intermediate>
|
||||
);
|
||||
const Child = () => {
|
||||
React.useLayoutEffect(() => {
|
||||
fakeConsole.error('active error');
|
||||
fakeConsole.log('active log');
|
||||
fakeConsole.warn('active warn');
|
||||
});
|
||||
React.useEffect(() => {
|
||||
fakeConsole.error('passive error');
|
||||
fakeConsole.log('passive log');
|
||||
fakeConsole.warn('passive warn');
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
act(() => ReactDOM.render(<Parent />, document.createElement('div')));
|
||||
|
||||
expect(mockLog).toHaveBeenCalledTimes(2);
|
||||
expect(mockLog.mock.calls[0]).toHaveLength(1);
|
||||
expect(mockLog.mock.calls[0][0]).toBe('active log');
|
||||
expect(mockLog.mock.calls[1]).toHaveLength(1);
|
||||
expect(mockLog.mock.calls[1][0]).toBe('passive log');
|
||||
expect(mockWarn).toHaveBeenCalledTimes(2);
|
||||
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 **)'
|
||||
);
|
||||
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 **)'
|
||||
);
|
||||
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 **)'
|
||||
);
|
||||
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 **)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should append component stacks to errors and warnings logged from commit hooks', () => {
|
||||
const Intermediate = ({ children }) => children;
|
||||
const Parent = () => (
|
||||
<Intermediate>
|
||||
<Child />
|
||||
</Intermediate>
|
||||
);
|
||||
class Child extends React.Component<any> {
|
||||
componentDidMount() {
|
||||
fakeConsole.error('didMount error');
|
||||
fakeConsole.log('didMount log');
|
||||
fakeConsole.warn('didMount warn');
|
||||
}
|
||||
componentDidUpdate() {
|
||||
fakeConsole.error('didUpdate error');
|
||||
fakeConsole.log('didUpdate log');
|
||||
fakeConsole.warn('didUpdate warn');
|
||||
}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
act(() => ReactDOM.render(<Parent />, container));
|
||||
act(() => ReactDOM.render(<Parent />, container));
|
||||
|
||||
expect(mockLog).toHaveBeenCalledTimes(2);
|
||||
expect(mockLog.mock.calls[0]).toHaveLength(1);
|
||||
expect(mockLog.mock.calls[0][0]).toBe('didMount log');
|
||||
expect(mockLog.mock.calls[1]).toHaveLength(1);
|
||||
expect(mockLog.mock.calls[1][0]).toBe('didUpdate log');
|
||||
expect(mockWarn).toHaveBeenCalledTimes(2);
|
||||
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 **)'
|
||||
);
|
||||
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 **)'
|
||||
);
|
||||
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 **)'
|
||||
);
|
||||
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 **)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should append component stacks to errors and warnings logged from gDSFP', () => {
|
||||
const Intermediate = ({ children }) => children;
|
||||
const Parent = () => (
|
||||
<Intermediate>
|
||||
<Child />
|
||||
</Intermediate>
|
||||
);
|
||||
class Child extends React.Component<any, any> {
|
||||
state = {};
|
||||
static getDerivedStateFromProps() {
|
||||
fakeConsole.error('error');
|
||||
fakeConsole.log('log');
|
||||
fakeConsole.warn('warn');
|
||||
return null;
|
||||
}
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
act(() => ReactDOM.render(<Parent />, document.createElement('div')));
|
||||
|
||||
expect(mockLog).toHaveBeenCalledTimes(1);
|
||||
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(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 **)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should append stacks after being uninstalled and reinstalled', () => {
|
||||
const Child = () => {
|
||||
fakeConsole.warn('warn');
|
||||
fakeConsole.error('error');
|
||||
return null;
|
||||
};
|
||||
|
||||
unpatchConsole();
|
||||
act(() => ReactDOM.render(<Child />, document.createElement('div')));
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledTimes(1);
|
||||
expect(mockWarn.mock.calls[0]).toHaveLength(1);
|
||||
expect(mockWarn.mock.calls[0][0]).toBe('warn');
|
||||
expect(mockError).toHaveBeenCalledTimes(1);
|
||||
expect(mockError.mock.calls[0]).toHaveLength(1);
|
||||
expect(mockError.mock.calls[0][0]).toBe('error');
|
||||
|
||||
patchConsole(fakeConsole);
|
||||
act(() => ReactDOM.render(<Child />, 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(
|
||||
'\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(
|
||||
'\n in Child (at **)'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -530,16 +530,20 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
|
||||
jest.runOnlyPendingTimers();
|
||||
TestRenderer.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
|
||||
jest.runOnlyPendingTimers();
|
||||
TestRenderer.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
@@ -548,16 +552,18 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, [
|
||||
'props',
|
||||
'nestedObject',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
0,
|
||||
'd',
|
||||
]);
|
||||
jest.runOnlyPendingTimers();
|
||||
TestRenderer.act(() => {
|
||||
getInspectedElementPath(id, [
|
||||
'props',
|
||||
'nestedObject',
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
0,
|
||||
'd',
|
||||
]);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
@@ -566,16 +572,20 @@ describe('InspectedElementContext', () => {
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['hooks', 0, 'value']);
|
||||
jest.runOnlyPendingTimers();
|
||||
TestRenderer.act(() => {
|
||||
getInspectedElementPath(id, ['hooks', 0, 'value']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('5: Inspect hooks.0.value');
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
|
||||
jest.runOnlyPendingTimers();
|
||||
TestRenderer.act(() => {
|
||||
getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
@@ -645,7 +655,7 @@ describe('InspectedElementContext', () => {
|
||||
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
TestRenderer.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
@@ -653,44 +663,46 @@ describe('InspectedElementContext', () => {
|
||||
expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
TestRenderer.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'c']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('3: Inspect props.nestedObject.c');
|
||||
|
||||
TestUtils.act(() => {
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
nestedObject={{
|
||||
a: {
|
||||
value: 2,
|
||||
b: {
|
||||
TestRenderer.act(() => {
|
||||
TestUtils.act(() => {
|
||||
ReactDOM.render(
|
||||
<Example
|
||||
nestedObject={{
|
||||
a: {
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
c: {
|
||||
value: 2,
|
||||
d: {
|
||||
value: 2,
|
||||
e: {
|
||||
b: {
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
container
|
||||
);
|
||||
c: {
|
||||
value: 2,
|
||||
d: {
|
||||
value: 2,
|
||||
e: {
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
container
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
TestUtils.act(() => {
|
||||
TestRenderer.act(() => {
|
||||
inspectedElement = null;
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('4: update inspected element');
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('4: update inspected element');
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -764,9 +776,12 @@ describe('InspectedElementContext', () => {
|
||||
});
|
||||
|
||||
inspectedElement = null;
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
TestRenderer.act(() => {
|
||||
TestUtils.act(() => {
|
||||
getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
});
|
||||
expect(inspectedElement).not.toBeNull();
|
||||
expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('ProfilingCache', () => {
|
||||
|
||||
it('should collect data for each root (including ones added or mounted after profiling started)', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
@@ -46,7 +46,7 @@ describe('ProfilingCache', () => {
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
Scheduler.unstable_advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
@@ -118,7 +118,7 @@ describe('ProfilingCache', () => {
|
||||
|
||||
it('should collect data for each commit', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
@@ -130,7 +130,7 @@ describe('ProfilingCache', () => {
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
Scheduler.unstable_advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
@@ -305,7 +305,7 @@ describe('ProfilingCache', () => {
|
||||
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
|
||||
|
||||
const Grandparent = () => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Parent key="one" />
|
||||
@@ -314,11 +314,11 @@ describe('ProfilingCache', () => {
|
||||
);
|
||||
};
|
||||
const Parent = () => {
|
||||
Scheduler.advanceTime(2);
|
||||
Scheduler.unstable_advanceTime(2);
|
||||
return <Child />;
|
||||
};
|
||||
const Child = () => {
|
||||
Scheduler.advanceTime(1);
|
||||
Scheduler.unstable_advanceTime(1);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -361,7 +361,7 @@ describe('ProfilingCache', () => {
|
||||
};
|
||||
|
||||
const Parent = () => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
return (
|
||||
<React.Suspense fallback={<Fallback />}>
|
||||
<Async />
|
||||
@@ -369,11 +369,11 @@ describe('ProfilingCache', () => {
|
||||
);
|
||||
};
|
||||
const Fallback = () => {
|
||||
Scheduler.advanceTime(2);
|
||||
Scheduler.unstable_advanceTime(2);
|
||||
return 'Fallback...';
|
||||
};
|
||||
const Async = () => {
|
||||
Scheduler.advanceTime(3);
|
||||
Scheduler.unstable_advanceTime(3);
|
||||
const data = getData();
|
||||
return data;
|
||||
};
|
||||
@@ -412,7 +412,7 @@ describe('ProfilingCache', () => {
|
||||
|
||||
it('should collect data for each rendered fiber', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
@@ -424,7 +424,7 @@ describe('ProfilingCache', () => {
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
Scheduler.unstable_advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
@@ -496,7 +496,7 @@ describe('ProfilingCache', () => {
|
||||
|
||||
it('should report every traced interaction', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
@@ -508,7 +508,7 @@ describe('ProfilingCache', () => {
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
Scheduler.unstable_advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('profiling charts', () => {
|
||||
describe('flamegraph chart', () => {
|
||||
it('should contain valid data', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Child key="first" duration={3} />
|
||||
@@ -42,7 +42,7 @@ describe('profiling charts', () => {
|
||||
|
||||
// Memoize children to verify that chart doesn't include in the update.
|
||||
const Child = React.memo(function Child({ duration }) {
|
||||
Scheduler.advanceTime(duration);
|
||||
Scheduler.unstable_advanceTime(duration);
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('profiling charts', () => {
|
||||
describe('ranked chart', () => {
|
||||
it('should contain valid data', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Child key="first" duration={3} />
|
||||
@@ -118,7 +118,7 @@ describe('profiling charts', () => {
|
||||
|
||||
// Memoize children to verify that chart doesn't include in the update.
|
||||
const Child = React.memo(function Child({ duration }) {
|
||||
Scheduler.advanceTime(duration);
|
||||
Scheduler.unstable_advanceTime(duration);
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -178,7 +178,7 @@ describe('profiling charts', () => {
|
||||
describe('interactions', () => {
|
||||
it('should contain valid data', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Child key="first" duration={3} />
|
||||
@@ -190,7 +190,7 @@ describe('profiling charts', () => {
|
||||
|
||||
// Memoize children to verify that chart doesn't include in the update.
|
||||
const Child = React.memo(function Child({ duration }) {
|
||||
Scheduler.advanceTime(duration);
|
||||
Scheduler.unstable_advanceTime(duration);
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@ describe('commit tree', () => {
|
||||
|
||||
it('should be able to rebuild the store tree for each commit', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
Scheduler.unstable_advanceTime(10);
|
||||
return new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} />);
|
||||
};
|
||||
const Child = React.memo(function Child() {
|
||||
Scheduler.advanceTime(2);
|
||||
Scheduler.unstable_advanceTime(2);
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
@@ -8,14 +8,20 @@ import type { ProfilingDataFrontend } from 'src/devtools/views/Profiler/types';
|
||||
import type { ElementType } from 'src/types';
|
||||
|
||||
export function act(callback: Function): void {
|
||||
const TestUtils = require('react-dom/test-utils');
|
||||
TestUtils.act(() => {
|
||||
callback();
|
||||
const { act: actTestRenderer } = require('react-test-renderer');
|
||||
const { act: actDOM } = require('react-dom/test-utils');
|
||||
|
||||
actDOM(() => {
|
||||
actTestRenderer(() => {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
||||
// Flush Bridge operations
|
||||
TestUtils.act(() => {
|
||||
jest.runAllTimers();
|
||||
actDOM(() => {
|
||||
actTestRenderer(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,24 +29,31 @@ export async function actAsync(
|
||||
cb: () => *,
|
||||
recursivelyFlush: boolean = true
|
||||
): Promise<void> {
|
||||
const TestUtils = require('react-dom/test-utils');
|
||||
const { act: actTestRenderer } = require('react-test-renderer');
|
||||
const { act: actDOM } = require('react-dom/test-utils');
|
||||
|
||||
// $FlowFixMe Flow doens't know about "await act()" yet
|
||||
await TestUtils.act(async () => {
|
||||
await cb();
|
||||
await actDOM(async () => {
|
||||
await actTestRenderer(async () => {
|
||||
await cb();
|
||||
});
|
||||
});
|
||||
|
||||
if (recursivelyFlush) {
|
||||
while (jest.getTimerCount() > 0) {
|
||||
// $FlowFixMe Flow doens't know about "await act()" yet
|
||||
await TestUtils.act(async () => {
|
||||
jest.runAllTimers();
|
||||
await actDOM(async () => {
|
||||
await actTestRenderer(async () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// $FlowFixMe Flow doesn't know about "await act()" yet
|
||||
await TestUtils.act(async () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
await actDOM(async () => {
|
||||
await actTestRenderer(async () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -122,10 +135,17 @@ export function getRendererID(): number {
|
||||
throw Error('Agent unavailable.');
|
||||
}
|
||||
const ids = Object.keys(global.agent._rendererInterfaces);
|
||||
if (ids.length !== 1) {
|
||||
throw Error('Multiple renderers attached.');
|
||||
|
||||
const id = ids.find(id => {
|
||||
const rendererInterface = global.agent._rendererInterfaces[id];
|
||||
return rendererInterface.renderer.rendererPackageName === 'react-dom';
|
||||
});
|
||||
|
||||
if (ids == null) {
|
||||
throw Error('Could not find renderer.');
|
||||
}
|
||||
return parseInt(ids[0], 10);
|
||||
|
||||
return parseInt(id, 10);
|
||||
}
|
||||
|
||||
export function requireTestRenderer(): ReactTestRenderer {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
sessionStorageSetItem,
|
||||
} from 'src/storage';
|
||||
import setupHighlighter from './views/Highlighter';
|
||||
import { patch as patchConsole, unpatch as unpatchConsole } from './console';
|
||||
|
||||
import type {
|
||||
InstanceAndStyle,
|
||||
@@ -133,6 +134,10 @@ export default class Agent extends EventEmitter<{|
|
||||
this.syncSelectionFromNativeElementsPanel
|
||||
);
|
||||
bridge.addListener('shutdown', this.shutdown);
|
||||
bridge.addListener(
|
||||
'updateAppendComponentStack',
|
||||
this.updateAppendComponentStack
|
||||
);
|
||||
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
|
||||
bridge.addListener('viewElementSource', this.viewElementSource);
|
||||
|
||||
@@ -402,6 +407,18 @@ export default class Agent extends EventEmitter<{|
|
||||
this._bridge.send('profilingStatus', this._isProfiling);
|
||||
};
|
||||
|
||||
updateAppendComponentStack = (appendComponentStack: boolean) => {
|
||||
// If the frontend preference has change,
|
||||
// or in the case of React Native- if the backend is just finding out the preference-
|
||||
// then install or uninstall the console overrides.
|
||||
// It's safe to call these methods multiple times, so we don't need to worry about that.
|
||||
if (appendComponentStack) {
|
||||
patchConsole();
|
||||
} else {
|
||||
unpatchConsole();
|
||||
}
|
||||
};
|
||||
|
||||
updateComponentFilters = (componentFilters: Array<ComponentFilter>) => {
|
||||
for (let rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
|
||||
125
src/backend/console.js
Normal file
125
src/backend/console.js
Normal file
@@ -0,0 +1,125 @@
|
||||
// @flow
|
||||
|
||||
import { getInternalReactConstants } from './renderer';
|
||||
import describeComponentFrame from './describeComponentFrame';
|
||||
|
||||
import type { Fiber, ReactRenderer } from './types';
|
||||
|
||||
const FRAME_REGEX = /\n {4}in /;
|
||||
|
||||
const injectedRenderers: Map<
|
||||
ReactRenderer,
|
||||
{|
|
||||
getCurrentFiber: () => Fiber | null,
|
||||
getDisplayNameForFiber: (fiber: Fiber) => string | null,
|
||||
|}
|
||||
> = new Map();
|
||||
|
||||
let isDisabled: boolean = false;
|
||||
let unpatchFn: null | (() => void) = null;
|
||||
|
||||
export function disable(): void {
|
||||
isDisabled = true;
|
||||
}
|
||||
|
||||
export function enable(): void {
|
||||
isDisabled = false;
|
||||
}
|
||||
|
||||
export function registerRenderer(renderer: ReactRenderer): void {
|
||||
const { 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);
|
||||
|
||||
injectedRenderers.set(renderer, {
|
||||
getCurrentFiber,
|
||||
getDisplayNameForFiber,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function patch(targetConsole?: Object = console): void {
|
||||
if (unpatchFn !== null) {
|
||||
// Don't patch twice.
|
||||
return;
|
||||
}
|
||||
|
||||
const originalConsoleMethods = { ...targetConsole };
|
||||
|
||||
unpatchFn = () => {
|
||||
for (let method in targetConsole) {
|
||||
try {
|
||||
// $FlowFixMe property error|warn is not writable.
|
||||
targetConsole[method] = originalConsoleMethods[method];
|
||||
} catch (error) {}
|
||||
}
|
||||
};
|
||||
|
||||
for (let method in targetConsole) {
|
||||
const appendComponentStack =
|
||||
method === 'error' || method === 'warn' || method === 'trace';
|
||||
|
||||
const originalMethod = targetConsole[method];
|
||||
const overrideMethod = (...args) => {
|
||||
if (isDisabled) return;
|
||||
|
||||
if (appendComponentStack) {
|
||||
// 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 alreadyHasComponentStack =
|
||||
args.length > 0 && FRAME_REGEX.exec(args[args.length - 1]);
|
||||
|
||||
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?)
|
||||
for (let {
|
||||
getCurrentFiber,
|
||||
getDisplayNameForFiber,
|
||||
} 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 (ownerStack !== '') {
|
||||
args.push(ownerStack);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
originalMethod(...args);
|
||||
};
|
||||
|
||||
try {
|
||||
// $FlowFixMe property error|warn is not writable.
|
||||
targetConsole[method] = overrideMethod;
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
|
||||
export function unpatch(): void {
|
||||
if (unpatchFn !== null) {
|
||||
unpatchFn();
|
||||
unpatchFn = null;
|
||||
}
|
||||
}
|
||||
41
src/backend/describeComponentFrame.js
Normal file
41
src/backend/describeComponentFrame.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// @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 sligthly 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) {
|
||||
let 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;
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import type { DevToolsHook, ReactRenderer, RendererInterface } from './types';
|
||||
import Agent from './agent';
|
||||
|
||||
import { attach } from './renderer';
|
||||
import { attach as attachLegacy } from './legacy/renderer';
|
||||
|
||||
import type { DevToolsHook, ReactRenderer, RendererInterface } from './types';
|
||||
|
||||
export function initBackend(
|
||||
hook: DevToolsHook,
|
||||
agent: Agent,
|
||||
|
||||
@@ -39,6 +39,12 @@ import {
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
} from '../constants';
|
||||
import { inspectHooksOfFiber } from './ReactDebugHooks';
|
||||
import {
|
||||
disable as disableConsole,
|
||||
enable as enableConsole,
|
||||
patch as patchConsole,
|
||||
registerRenderer as registerRendererWithConsole,
|
||||
} from './console';
|
||||
|
||||
import type {
|
||||
ChangeDescription,
|
||||
@@ -59,8 +65,89 @@ import type {
|
||||
import type { Interaction } from 'src/devtools/views/Profiler/types';
|
||||
import type { ComponentFilter, ElementType } from 'src/types';
|
||||
|
||||
function getInternalReactConstants(version) {
|
||||
const ReactSymbols = {
|
||||
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,
|
||||
EVENT_COMPONENT_NUMBER: number,
|
||||
EVENT_COMPONENT_STRING: string,
|
||||
EVENT_TARGET_NUMBER: number,
|
||||
EVENT_TARGET_STRING: string,
|
||||
EVENT_TARGET_TOUCH_HIT_NUMBER: number,
|
||||
EVENT_TARGET_TOUCH_HIT_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,
|
||||
SUSPENSE_NUMBER: number,
|
||||
SUSPENSE_SYMBOL_STRING: string,
|
||||
DEPRECATED_PLACEHOLDER_SYMBOL_STRING: string,
|
||||
};
|
||||
|
||||
type ReactPriorityLevelsType = {|
|
||||
ImmediatePriority: number,
|
||||
UserBlockingPriority: number,
|
||||
NormalPriority: number,
|
||||
LowPriority: number,
|
||||
IdlePriority: number,
|
||||
NoPriority: number,
|
||||
|};
|
||||
|
||||
type ReactTypeOfWorkType = {|
|
||||
ClassComponent: number,
|
||||
ContextConsumer: number,
|
||||
ContextProvider: number,
|
||||
CoroutineComponent: number,
|
||||
CoroutineHandlerPhase: number,
|
||||
DehydratedSuspenseComponent: number,
|
||||
EventComponent: number,
|
||||
EventTarget: 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,
|
||||
YieldComponent: number,
|
||||
|};
|
||||
|
||||
type ReactTypeOfSideEffectType = {|
|
||||
NoEffect: number,
|
||||
PerformedWork: number,
|
||||
Placement: number,
|
||||
|};
|
||||
|
||||
export function getInternalReactConstants(
|
||||
version: string
|
||||
): {|
|
||||
getDisplayNameForFiber: getDisplayNameForFiberType,
|
||||
getTypeSymbol: getTypeSymbolType,
|
||||
ReactPriorityLevels: ReactPriorityLevelsType,
|
||||
ReactSymbols: ReactSymbolsType,
|
||||
ReactTypeOfSideEffect: ReactTypeOfSideEffectType,
|
||||
ReactTypeOfWork: ReactTypeOfWorkType,
|
||||
|} {
|
||||
const ReactSymbols: ReactSymbolsType = {
|
||||
CONCURRENT_MODE_NUMBER: 0xeacf,
|
||||
CONCURRENT_MODE_SYMBOL_STRING: 'Symbol(react.concurrent_mode)',
|
||||
DEPRECATED_ASYNC_MODE_SYMBOL_STRING: 'Symbol(react.async_mode)',
|
||||
@@ -87,7 +174,7 @@ function getInternalReactConstants(version) {
|
||||
DEPRECATED_PLACEHOLDER_SYMBOL_STRING: 'Symbol(react.placeholder)',
|
||||
};
|
||||
|
||||
const ReactTypeOfSideEffect = {
|
||||
const ReactTypeOfSideEffect: ReactTypeOfSideEffectType = {
|
||||
NoEffect: 0b00,
|
||||
PerformedWork: 0b01,
|
||||
Placement: 0b10,
|
||||
@@ -100,7 +187,7 @@ function getInternalReactConstants(version) {
|
||||
// Technically these priority levels are invalid for versions before 16.9,
|
||||
// but 16.9 is the first version to report priority level to DevTools,
|
||||
// so we can avoid checking for earlier versions and support pre-16.9 canary releases in the process.
|
||||
const ReactPriorityLevels = {
|
||||
const ReactPriorityLevels: ReactPriorityLevelsType = {
|
||||
ImmediatePriority: 99,
|
||||
UserBlockingPriority: 98,
|
||||
NormalPriority: 97,
|
||||
@@ -109,7 +196,7 @@ function getInternalReactConstants(version) {
|
||||
NoPriority: 90,
|
||||
};
|
||||
|
||||
let ReactTypeOfWork;
|
||||
let ReactTypeOfWork: ReactTypeOfWorkType = ((null: any): ReactTypeOfWorkType);
|
||||
|
||||
// **********************************************************
|
||||
// The section below is copied from files in React repo.
|
||||
@@ -200,7 +287,149 @@ function getInternalReactConstants(version) {
|
||||
// End of copied code.
|
||||
// **********************************************************
|
||||
|
||||
function getTypeSymbol(type: any): Symbol | number {
|
||||
const symbolOrNumber =
|
||||
typeof type === 'object' && type !== null ? type.$$typeof : type;
|
||||
|
||||
return typeof symbolOrNumber === 'symbol'
|
||||
? symbolOrNumber.toString()
|
||||
: symbolOrNumber;
|
||||
}
|
||||
|
||||
const {
|
||||
ClassComponent,
|
||||
IncompleteClassComponent,
|
||||
FunctionComponent,
|
||||
IndeterminateComponent,
|
||||
EventComponent,
|
||||
EventTarget,
|
||||
ForwardRef,
|
||||
HostRoot,
|
||||
HostComponent,
|
||||
HostPortal,
|
||||
HostText,
|
||||
Fragment,
|
||||
MemoComponent,
|
||||
SimpleMemoComponent,
|
||||
} = ReactTypeOfWork;
|
||||
|
||||
const {
|
||||
EVENT_TARGET_TOUCH_HIT_NUMBER,
|
||||
EVENT_TARGET_TOUCH_HIT_STRING,
|
||||
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,
|
||||
SUSPENSE_NUMBER,
|
||||
SUSPENSE_SYMBOL_STRING,
|
||||
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
|
||||
PROFILER_NUMBER,
|
||||
PROFILER_SYMBOL_STRING,
|
||||
} = ReactSymbols;
|
||||
|
||||
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
|
||||
function getDisplayNameForFiber(fiber: Fiber): string | null {
|
||||
const { elementType, type, tag } = fiber;
|
||||
|
||||
// This is to support lazy components with a Promise as the type.
|
||||
// see https://github.com/facebook/react/pull/13397
|
||||
let resolvedType = type;
|
||||
if (typeof type === 'object' && type !== null) {
|
||||
if (typeof type.then === 'function') {
|
||||
resolvedType = type._reactResult;
|
||||
}
|
||||
}
|
||||
|
||||
let resolvedContext: any = null;
|
||||
|
||||
switch (tag) {
|
||||
case ClassComponent:
|
||||
case IncompleteClassComponent:
|
||||
return getDisplayName(resolvedType);
|
||||
case FunctionComponent:
|
||||
case IndeterminateComponent:
|
||||
return getDisplayName(resolvedType);
|
||||
case EventComponent:
|
||||
return type.responder.displayName || 'EventComponent';
|
||||
case EventTarget:
|
||||
switch (getTypeSymbol(elementType.type)) {
|
||||
case EVENT_TARGET_TOUCH_HIT_NUMBER:
|
||||
case EVENT_TARGET_TOUCH_HIT_STRING:
|
||||
return 'TouchHitTarget';
|
||||
default:
|
||||
return elementType.displayName || 'EventTarget';
|
||||
}
|
||||
case ForwardRef:
|
||||
return (
|
||||
resolvedType.displayName ||
|
||||
getDisplayName(resolvedType.render, 'Anonymous')
|
||||
);
|
||||
case HostRoot:
|
||||
return null;
|
||||
case HostComponent:
|
||||
return type;
|
||||
case HostPortal:
|
||||
case HostText:
|
||||
case Fragment:
|
||||
return null;
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
if (elementType.displayName) {
|
||||
return elementType.displayName;
|
||||
} else {
|
||||
return getDisplayName(type, 'Anonymous');
|
||||
}
|
||||
default:
|
||||
const typeSymbol = getTypeSymbol(type);
|
||||
|
||||
switch (typeSymbol) {
|
||||
case CONCURRENT_MODE_NUMBER:
|
||||
case CONCURRENT_MODE_SYMBOL_STRING:
|
||||
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
||||
return null;
|
||||
case CONTEXT_PROVIDER_NUMBER:
|
||||
case CONTEXT_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:
|
||||
// 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()
|
||||
resolvedContext = fiber.type._context || fiber.type;
|
||||
|
||||
// NOTE: TraceUpdatesBackendManager depends on the name ending in '.Consumer'
|
||||
// If you change the name, figure out a more resilient way to detect it.
|
||||
return `${resolvedContext.displayName || 'Context'}.Consumer`;
|
||||
case STRICT_MODE_NUMBER:
|
||||
case STRICT_MODE_SYMBOL_STRING:
|
||||
return null;
|
||||
case SUSPENSE_NUMBER:
|
||||
case SUSPENSE_SYMBOL_STRING:
|
||||
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
|
||||
return 'Suspense';
|
||||
case PROFILER_NUMBER:
|
||||
case PROFILER_SYMBOL_STRING:
|
||||
return `Profiler(${fiber.memoizedProps.id})`;
|
||||
default:
|
||||
// Unknown element type.
|
||||
// This may mean a new element type that has not yet been added to DevTools.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getDisplayNameForFiber,
|
||||
getTypeSymbol,
|
||||
ReactPriorityLevels,
|
||||
ReactTypeOfWork,
|
||||
ReactSymbols,
|
||||
@@ -215,6 +444,8 @@ export function attach(
|
||||
global: Object
|
||||
): RendererInterface {
|
||||
const {
|
||||
getDisplayNameForFiber,
|
||||
getTypeSymbol,
|
||||
ReactPriorityLevels,
|
||||
ReactTypeOfWork,
|
||||
ReactSymbols,
|
||||
@@ -256,8 +487,6 @@ export function attach(
|
||||
CONTEXT_CONSUMER_SYMBOL_STRING,
|
||||
CONTEXT_PROVIDER_NUMBER,
|
||||
CONTEXT_PROVIDER_SYMBOL_STRING,
|
||||
EVENT_TARGET_TOUCH_HIT_NUMBER,
|
||||
EVENT_TARGET_TOUCH_HIT_STRING,
|
||||
PROFILER_NUMBER,
|
||||
PROFILER_SYMBOL_STRING,
|
||||
STRICT_MODE_NUMBER,
|
||||
@@ -277,6 +506,22 @@ export function attach(
|
||||
typeof setSuspenseHandler === 'function' &&
|
||||
typeof scheduleUpdate === 'function';
|
||||
|
||||
// Patching the console enables DevTools to do a few useful things:
|
||||
// * Append component stacks to warnings and error messages
|
||||
// * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
|
||||
//
|
||||
// Don't patch in test environments because we don't want to interfere with Jest's own console overrides.
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
registerRendererWithConsole(renderer);
|
||||
|
||||
// The renderer interface can't read this preference directly,
|
||||
// because it is stored in localStorage within the context of the extension.
|
||||
// It relies on the extension to pass the preference through via the global.
|
||||
if (window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false) {
|
||||
patchConsole();
|
||||
}
|
||||
}
|
||||
|
||||
const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
|
||||
if (__DEBUG__) {
|
||||
const displayName = getDisplayNameForFiber(fiber) || 'null';
|
||||
@@ -345,7 +590,10 @@ export function attach(
|
||||
if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ != null) {
|
||||
applyComponentFilters(window.__REACT_DEVTOOLS_COMPONENT_FILTERS__);
|
||||
} else {
|
||||
console.warn('⚛️ DevTools: Could not locate saved component filters');
|
||||
// Unfortunately this feature is not expected to work for React Native for now.
|
||||
// It would be annoying for us to spam YellowBox warnings with unactionable stuff,
|
||||
// so for now just skip this message...
|
||||
//console.warn('⚛️ DevTools: Could not locate saved component filters');
|
||||
|
||||
// Fallback to assuming the default filters in this case.
|
||||
applyComponentFilters(getDefaultComponentFilters());
|
||||
@@ -447,111 +695,6 @@ export function attach(
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTypeSymbol(type: any): Symbol | number {
|
||||
const symbolOrNumber =
|
||||
typeof type === 'object' && type !== null ? type.$$typeof : type;
|
||||
|
||||
return typeof symbolOrNumber === 'symbol'
|
||||
? symbolOrNumber.toString()
|
||||
: symbolOrNumber;
|
||||
}
|
||||
|
||||
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
|
||||
function getDisplayNameForFiber(fiber: Fiber): string | null {
|
||||
const { elementType, type, tag } = fiber;
|
||||
|
||||
// This is to support lazy components with a Promise as the type.
|
||||
// see https://github.com/facebook/react/pull/13397
|
||||
let resolvedType = type;
|
||||
if (typeof type === 'object' && type !== null) {
|
||||
if (typeof type.then === 'function') {
|
||||
resolvedType = type._reactResult;
|
||||
}
|
||||
}
|
||||
|
||||
let resolvedContext: any = null;
|
||||
|
||||
switch (tag) {
|
||||
case ClassComponent:
|
||||
case IncompleteClassComponent:
|
||||
return getDisplayName(resolvedType);
|
||||
case FunctionComponent:
|
||||
case IndeterminateComponent:
|
||||
return getDisplayName(resolvedType);
|
||||
case EventComponent:
|
||||
return type.responder.displayName || 'EventComponent';
|
||||
case EventTarget:
|
||||
switch (getTypeSymbol(elementType.type)) {
|
||||
case EVENT_TARGET_TOUCH_HIT_NUMBER:
|
||||
case EVENT_TARGET_TOUCH_HIT_STRING:
|
||||
return 'TouchHitTarget';
|
||||
default:
|
||||
return elementType.displayName || 'EventTarget';
|
||||
}
|
||||
case ForwardRef:
|
||||
return (
|
||||
resolvedType.displayName ||
|
||||
getDisplayName(resolvedType.render, 'Anonymous')
|
||||
);
|
||||
case HostRoot:
|
||||
return null;
|
||||
case HostComponent:
|
||||
return type;
|
||||
case HostPortal:
|
||||
case HostText:
|
||||
case Fragment:
|
||||
return null;
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
if (elementType.displayName) {
|
||||
return elementType.displayName;
|
||||
} else {
|
||||
return getDisplayName(type, 'Anonymous');
|
||||
}
|
||||
default:
|
||||
const typeSymbol = getTypeSymbol(type);
|
||||
|
||||
switch (typeSymbol) {
|
||||
case CONCURRENT_MODE_NUMBER:
|
||||
case CONCURRENT_MODE_SYMBOL_STRING:
|
||||
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
||||
return null;
|
||||
case CONTEXT_PROVIDER_NUMBER:
|
||||
case CONTEXT_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:
|
||||
// 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()
|
||||
resolvedContext = fiber.type._context || fiber.type;
|
||||
|
||||
// NOTE: TraceUpdatesBackendManager depends on the name ending in '.Consumer'
|
||||
// If you change the name, figure out a more resilient way to detect it.
|
||||
return `${resolvedContext.displayName || 'Context'}.Consumer`;
|
||||
case STRICT_MODE_NUMBER:
|
||||
case STRICT_MODE_SYMBOL_STRING:
|
||||
return null;
|
||||
case SUSPENSE_NUMBER:
|
||||
case SUSPENSE_SYMBOL_STRING:
|
||||
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
|
||||
return 'Suspense';
|
||||
case PROFILER_NUMBER:
|
||||
case PROFILER_SYMBOL_STRING:
|
||||
return `Profiler(${fiber.memoizedProps.id})`;
|
||||
default:
|
||||
// Unknown element type.
|
||||
// This may mean a new element type that has not yet been added to DevTools.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
|
||||
function getElementTypeForFiber(fiber: Fiber): ElementType {
|
||||
const { type, tag } = fiber;
|
||||
@@ -2109,6 +2252,20 @@ export function attach(
|
||||
node = node.return;
|
||||
}
|
||||
|
||||
let hooks = null;
|
||||
if (usesHooks) {
|
||||
// Suppress console logging while re-rendering
|
||||
try {
|
||||
disableConsole();
|
||||
hooks = inspectHooksOfFiber(
|
||||
fiber,
|
||||
(renderer.currentDispatcherRef: any)
|
||||
);
|
||||
} finally {
|
||||
enableConsole();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -2136,9 +2293,7 @@ export function attach(
|
||||
// TODO Review sanitization approach for the below inspectable values.
|
||||
context,
|
||||
events,
|
||||
hooks: usesHooks
|
||||
? inspectHooksOfFiber(fiber, (renderer.currentDispatcherRef: any))
|
||||
: null,
|
||||
hooks,
|
||||
props: memoizedProps,
|
||||
state: usesHooks ? null : memoizedState,
|
||||
|
||||
|
||||
@@ -118,6 +118,10 @@ export type ReactRenderer = {
|
||||
// Only injected by React v16.8+ in order to support hooks inspection.
|
||||
currentDispatcherRef?: {| current: null | Dispatcher |},
|
||||
|
||||
// Only injected by React v16.9+ in DEV mode.
|
||||
// Enables DevTools to append owners-only component stack to error messages.
|
||||
getCurrentFiber?: () => Fiber | null,
|
||||
|
||||
// <= 15
|
||||
Mount?: any,
|
||||
};
|
||||
|
||||
@@ -98,6 +98,7 @@ export default class Bridge extends EventEmitter<{|
|
||||
stopProfiling: [],
|
||||
syncSelectionFromNativeElementsPanel: [],
|
||||
syncSelectionToNativeElementsPanel: [],
|
||||
updateAppendComponentStack: [boolean],
|
||||
updateComponentFilters: [Array<ComponentFilter>],
|
||||
viewElementSource: [ElementAndRendererID],
|
||||
|
||||
|
||||
@@ -20,4 +20,7 @@ export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
|
||||
export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY =
|
||||
'React::DevTools::reloadAndProfile';
|
||||
|
||||
export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
|
||||
'React::DevTools::appendComponentStack';
|
||||
|
||||
export const PROFILER_EXPORT_VERSION = 4;
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { SettingsContext } from './SettingsContext';
|
||||
|
||||
import styles from './SettingsShared.css';
|
||||
|
||||
export default function GeneralSettings(_: {||}) {
|
||||
const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
|
||||
SettingsContext
|
||||
);
|
||||
const {
|
||||
displayDensity,
|
||||
setDisplayDensity,
|
||||
theme,
|
||||
setTheme,
|
||||
appendComponentStack,
|
||||
setAppendComponentStack,
|
||||
} = useContext(SettingsContext);
|
||||
|
||||
const updateDisplayDensity = useCallback(
|
||||
({ currentTarget }) => {
|
||||
setDisplayDensity(currentTarget.value);
|
||||
},
|
||||
[setDisplayDensity]
|
||||
);
|
||||
const updateTheme = useCallback(
|
||||
({ currentTarget }) => {
|
||||
setTheme(currentTarget.value);
|
||||
},
|
||||
[setTheme]
|
||||
);
|
||||
const updateDisplayDensity = ({ currentTarget }) =>
|
||||
setDisplayDensity(currentTarget.value);
|
||||
const updateTheme = ({ currentTarget }) => setTheme(currentTarget.value);
|
||||
const updateappendComponentStack = ({ currentTarget }) =>
|
||||
setAppendComponentStack(currentTarget.checked);
|
||||
|
||||
return (
|
||||
<div className={styles.Settings}>
|
||||
@@ -45,6 +43,17 @@ export default function GeneralSettings(_: {||}) {
|
||||
<option value="comfortable">Comfortable</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={styles.Setting}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={appendComponentStack}
|
||||
onChange={updateappendComponentStack}
|
||||
/>{' '}
|
||||
Append component stacks to console warnings and errors.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import React, { createContext, useLayoutEffect, useMemo } from 'react';
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY } from 'src/constants';
|
||||
import { useLocalStorage } from '../hooks';
|
||||
import { BridgeContext } from '../context';
|
||||
|
||||
import type { BrowserTheme } from '../DevTools';
|
||||
|
||||
@@ -16,6 +24,9 @@ type Context = {|
|
||||
// Specified as a separate prop so it can trigger a re-render of FixedSizeList.
|
||||
lineHeight: number,
|
||||
|
||||
appendComponentStack: boolean,
|
||||
setAppendComponentStack: (value: boolean) => void,
|
||||
|
||||
theme: Theme,
|
||||
setTheme(value: Theme): void,
|
||||
|};
|
||||
@@ -40,6 +51,8 @@ function SettingsContextController({
|
||||
profilerPortalContainer,
|
||||
settingsPortalContainer,
|
||||
}: Props) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
|
||||
const [displayDensity, setDisplayDensity] = useLocalStorage<DisplayDensity>(
|
||||
'React::DevTools::displayDensity',
|
||||
'compact'
|
||||
@@ -48,6 +61,10 @@ function SettingsContextController({
|
||||
'React::DevTools::theme',
|
||||
'auto'
|
||||
);
|
||||
const [
|
||||
appendComponentStack,
|
||||
setAppendComponentStack,
|
||||
] = useLocalStorage<boolean>(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, true);
|
||||
|
||||
const documentElements = useMemo<DocumentElements>(() => {
|
||||
const array: Array<HTMLElement> = [
|
||||
@@ -117,12 +134,18 @@ function SettingsContextController({
|
||||
}
|
||||
}, [browserTheme, theme, documentElements]);
|
||||
|
||||
useEffect(() => {
|
||||
bridge.send('updateAppendComponentStack', appendComponentStack);
|
||||
}, [bridge, appendComponentStack]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
displayDensity,
|
||||
setDisplayDensity,
|
||||
theme,
|
||||
setTheme,
|
||||
appendComponentStack,
|
||||
setAppendComponentStack,
|
||||
lineHeight:
|
||||
displayDensity === 'compact'
|
||||
? compactLineHeight
|
||||
@@ -134,6 +157,8 @@ function SettingsContextController({
|
||||
displayDensity,
|
||||
setDisplayDensity,
|
||||
setTheme,
|
||||
appendComponentStack,
|
||||
setAppendComponentStack,
|
||||
theme,
|
||||
]
|
||||
);
|
||||
|
||||
31
src/hook.js
31
src/hook.js
@@ -7,6 +7,11 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {
|
||||
patch as patchConsole,
|
||||
registerRenderer as registerRendererWithConsole,
|
||||
} from './backend/console';
|
||||
|
||||
import type { DevToolsHook } from 'src/backend/types';
|
||||
|
||||
declare var window: any;
|
||||
@@ -155,6 +160,32 @@ export function installHook(target: any): DevToolsHook | null {
|
||||
? 'deadcode'
|
||||
: detectReactBuildType(renderer);
|
||||
|
||||
// Patching the console enables DevTools to do a few useful things:
|
||||
// * Append component stacks to warnings and error messages
|
||||
// * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
|
||||
//
|
||||
// For React Native, we intentionally patch early (during injection).
|
||||
// This provides React Native developers with components stacks even if they don't run DevTools.
|
||||
// This won't work for DOM though, since this entire file is eval'ed and inserted as a script tag.
|
||||
// In that case, we'll patch later (when the frontend attaches).
|
||||
//
|
||||
// Don't patch in test environments because we don't want to interfere with Jest's own console overrides.
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
try {
|
||||
// The installHook() function is injected by being stringified in the browser,
|
||||
// so imports outside of this function do not get included.
|
||||
//
|
||||
// Normally we could check "typeof patchConsole === 'function'",
|
||||
// but Webpack wraps imports with an object (e.g. _backend_console__WEBPACK_IMPORTED_MODULE_0__)
|
||||
// and the object itself will be undefined as well for the reasons mentioned above,
|
||||
// so we use try/catch instead.
|
||||
if (window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false) {
|
||||
registerRendererWithConsole(renderer);
|
||||
patchConsole();
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
// If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
|
||||
// Otherwise the renderer won't yet exist and we can skip this step.
|
||||
const attach = target.__REACT_DEVTOOLS_ATTACH__;
|
||||
|
||||
22
src/utils.js
22
src/utils.js
@@ -8,7 +8,10 @@ import {
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
} from './constants';
|
||||
import { ElementTypeRoot } from 'src/types';
|
||||
import { LOCAL_STORAGE_FILTER_PREFERENCES_KEY } from './constants';
|
||||
import {
|
||||
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
|
||||
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
|
||||
} from './constants';
|
||||
import { ComponentFilterElementType, ElementTypeHostComponent } from './types';
|
||||
import {
|
||||
ElementTypeClass,
|
||||
@@ -198,6 +201,23 @@ export function saveComponentFilters(
|
||||
);
|
||||
}
|
||||
|
||||
export function getAppendComponentStack(): boolean {
|
||||
try {
|
||||
const raw = localStorageGetItem(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY);
|
||||
if (raw != null) {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
} catch (error) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setAppendComponentStack(value: boolean): void {
|
||||
localStorageSetItem(
|
||||
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
|
||||
JSON.stringify(value)
|
||||
);
|
||||
}
|
||||
|
||||
export function separateDisplayNameAndHOCs(
|
||||
displayName: string | null,
|
||||
type: ElementType
|
||||
|
||||
46
yarn.lock
46
yarn.lock
@@ -9944,20 +9944,20 @@ react-color@^2.11.7:
|
||||
object-assign "^4.1.0"
|
||||
prop-types "^15.5.10"
|
||||
|
||||
react-dom@^0.0.0-50b50c26f:
|
||||
version "0.0.0-50b50c26f"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-50b50c26f.tgz#3cd8da0f2276ed4b7a926e1807d2675b2eb40227"
|
||||
integrity sha512-da9qleWDdBdAguEIDvvpFE0iuS8hfcCSGgZTYKRQMlSh5A94Ktr1otL4rgDTFH+bNsOwz3XrvEBYRA6WaE9xzQ==
|
||||
react-dom@^0.0.0-424099da6:
|
||||
version "0.0.0-424099da6"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-424099da6.tgz#2a6392d5730fd7688d7ec5a56b2f9f8e3627d271"
|
||||
integrity sha512-B6x2YWaw06xJ0br9wsny6a9frqmaNAw+vGrm08W7JZp7WfiBYKhM6Nt9seijaAVzajp49fe18orNMgL12Lafsg==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.2"
|
||||
scheduler "0.0.0-50b50c26f"
|
||||
scheduler "0.0.0-424099da6"
|
||||
|
||||
react-is@0.0.0-50b50c26f, react-is@^0.0.0-50b50c26f:
|
||||
version "0.0.0-50b50c26f"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-0.0.0-50b50c26f.tgz#c4003ffffef9bd2b287979f9041a23d12a607bf2"
|
||||
integrity sha512-9Y6ZvdOVmOxXs9mGuFy6eXHBww8RJCtJAh94b1hkbjhnW8Mb5ADScDoxJBVxcNuX9hvDkhENspC96ZQK1NIv3g==
|
||||
react-is@0.0.0-424099da6:
|
||||
version "0.0.0-424099da6"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-0.0.0-424099da6.tgz#a8b1322bbb1ef014b33ee3bff30d3be9a4729baa"
|
||||
integrity sha512-VMFvIdqNV0eB8YxmE9katf3XM4qbdKGhLYANfohwktTryrWWOUOoVRX6IHm4iN06LgHwWLBOBP/YARc1qzuF2w==
|
||||
|
||||
react-is@^16.8.1:
|
||||
version "16.8.3"
|
||||
@@ -9969,15 +9969,15 @@ react-is@^16.8.4:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2"
|
||||
integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==
|
||||
|
||||
react-test-renderer@^0.0.0-50b50c26f:
|
||||
version "0.0.0-50b50c26f"
|
||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-0.0.0-50b50c26f.tgz#1a85cf9073ef5a932d03bee36fcfd9bf15aeae2c"
|
||||
integrity sha512-gWc4L+mFIUCjvBpafR88n4/i/oaKHD6rzVyZY+XBou9MNtr2rRkjePOhBVsiYlCwkj+zZi6klV9b05TMzftosA==
|
||||
react-test-renderer@^0.0.0-424099da6:
|
||||
version "0.0.0-424099da6"
|
||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-0.0.0-424099da6.tgz#75272c39e0b45e99dbd674977a4afc32a2d37f81"
|
||||
integrity sha512-tF9NutO52Js52L390poDUvnN43j77SekXyIt0KzQa+HdgY7uvvzKH9ouqCz3M5f52eDZDff4OG4cZuZ+lNLIfQ==
|
||||
dependencies:
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.2"
|
||||
react-is "0.0.0-50b50c26f"
|
||||
scheduler "0.0.0-50b50c26f"
|
||||
react-is "0.0.0-424099da6"
|
||||
scheduler "0.0.0-424099da6"
|
||||
|
||||
react-virtualized-auto-sizer@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -9990,10 +9990,10 @@ react-window@./vendor/react-window:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
memoize-one ">=3.1.1 <6"
|
||||
|
||||
react@^0.0.0-50b50c26f:
|
||||
version "0.0.0-50b50c26f"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-50b50c26f.tgz#b782b579ce1f5d8bd696c5e45c744714ebecb111"
|
||||
integrity sha512-jUAzS4DeWTdUZ/3kqm2T6C9OIpiAf2qdwVamCts0qzwYVni1/gUTOWK1ui0J+eaRzKxrIEzVvmCMxFd35lP/pA==
|
||||
react@^0.0.0-424099da6:
|
||||
version "0.0.0-424099da6"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-424099da6.tgz#bf9155a10bb09783cfdc9e79438062af4b249861"
|
||||
integrity sha512-z/brDYS4RaX3+zknH8nIV7i9B7We3hFBdD0QWhDKKgEHInFLF1Y/+2GsdedDI69GWw8Hv6mw1iilycHjHRaCZA==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
@@ -10658,10 +10658,10 @@ sax@>=0.6.0, sax@^1.2.4:
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
||||
scheduler@0.0.0-50b50c26f, scheduler@^0.0.0-50b50c26f:
|
||||
version "0.0.0-50b50c26f"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-50b50c26f.tgz#09bedde1c64d7a042b557bee2dbf5faf5fd58a50"
|
||||
integrity sha512-LBN3zrP8iBdILOoYxybFtkU7j+ldZTHORKyYyVLwXuIwGQ8/Xhs5VZjNQ5R2Xru2zv3GGVpJSbd47EpDuD2EHw==
|
||||
scheduler@0.0.0-424099da6, scheduler@^0.0.0-424099da6:
|
||||
version "0.0.0-424099da6"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-424099da6.tgz#5311edfc2716479475517fdcbc24909948026bdb"
|
||||
integrity sha512-eDsz8sdikcel1lKRDJhUZ17K22rOdmJAV07coMgIvdX0MoHh9cVNQ+iryU7O9Gi/c7ySE3DaX1M9xBqAqyXA3g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
Reference in New Issue
Block a user