mirror of
https://github.com/facebook/react.git
synced 2026-02-26 05:05:06 +00:00
* Add inspectingNative events for RN * Enable inspection toggle UI for RN * Add a stopInspectingNative method for agent
819 lines
26 KiB
JavaScript
819 lines
26 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import EventEmitter from '../events';
|
|
import throttle from 'lodash.throttle';
|
|
import {
|
|
SESSION_STORAGE_LAST_SELECTION_KEY,
|
|
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
|
|
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
|
__DEBUG__,
|
|
} from '../constants';
|
|
import {
|
|
sessionStorageGetItem,
|
|
sessionStorageRemoveItem,
|
|
sessionStorageSetItem,
|
|
} from 'react-devtools-shared/src/storage';
|
|
import setupHighlighter from './views/Highlighter';
|
|
import {
|
|
initialize as setupTraceUpdates,
|
|
toggleEnabled as setTraceUpdatesEnabled,
|
|
} from './views/TraceUpdates';
|
|
import {patch as patchConsole} from './console';
|
|
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
|
|
|
|
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
|
import type {
|
|
InstanceAndStyle,
|
|
NativeType,
|
|
OwnersList,
|
|
PathFrame,
|
|
PathMatch,
|
|
RendererID,
|
|
RendererInterface,
|
|
} from './types';
|
|
import type {ComponentFilter} from '../types';
|
|
import {isSynchronousXHRSupported} from './utils';
|
|
import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
|
|
|
|
const debug = (methodName, ...args) => {
|
|
if (__DEBUG__) {
|
|
console.log(
|
|
`%cAgent %c${methodName}`,
|
|
'color: purple; font-weight: bold;',
|
|
'font-weight: bold;',
|
|
...args,
|
|
);
|
|
}
|
|
};
|
|
|
|
type ElementAndRendererID = {|
|
|
id: number,
|
|
rendererID: number,
|
|
|};
|
|
|
|
type StoreAsGlobalParams = {|
|
|
count: number,
|
|
id: number,
|
|
path: Array<string | number>,
|
|
rendererID: number,
|
|
|};
|
|
|
|
type CopyElementParams = {|
|
|
id: number,
|
|
path: Array<string | number>,
|
|
rendererID: number,
|
|
|};
|
|
|
|
type InspectElementParams = {|
|
|
forceFullData: boolean,
|
|
id: number,
|
|
path: Array<string | number> | null,
|
|
rendererID: number,
|
|
requestID: number,
|
|
|};
|
|
|
|
type OverrideHookParams = {|
|
|
id: number,
|
|
hookID: number,
|
|
path: Array<string | number>,
|
|
rendererID: number,
|
|
wasForwarded?: boolean,
|
|
value: any,
|
|
|};
|
|
|
|
type SetInParams = {|
|
|
id: number,
|
|
path: Array<string | number>,
|
|
rendererID: number,
|
|
wasForwarded?: boolean,
|
|
value: any,
|
|
|};
|
|
|
|
type PathType = 'props' | 'hooks' | 'state' | 'context';
|
|
|
|
type DeletePathParams = {|
|
|
type: PathType,
|
|
hookID?: ?number,
|
|
id: number,
|
|
path: Array<string | number>,
|
|
rendererID: number,
|
|
|};
|
|
|
|
type RenamePathParams = {|
|
|
type: PathType,
|
|
hookID?: ?number,
|
|
id: number,
|
|
oldPath: Array<string | number>,
|
|
newPath: Array<string | number>,
|
|
rendererID: number,
|
|
|};
|
|
|
|
type OverrideValueAtPathParams = {|
|
|
type: PathType,
|
|
hookID?: ?number,
|
|
id: number,
|
|
path: Array<string | number>,
|
|
rendererID: number,
|
|
value: any,
|
|
|};
|
|
|
|
type OverrideErrorParams = {|
|
|
id: number,
|
|
rendererID: number,
|
|
forceError: boolean,
|
|
|};
|
|
|
|
type OverrideSuspenseParams = {|
|
|
id: number,
|
|
rendererID: number,
|
|
forceFallback: boolean,
|
|
|};
|
|
|
|
type PersistedSelection = {|
|
|
rendererID: number,
|
|
path: Array<PathFrame>,
|
|
|};
|
|
|
|
export default class Agent extends EventEmitter<{|
|
|
hideNativeHighlight: [],
|
|
showNativeHighlight: [NativeType],
|
|
startInspectingNative: [],
|
|
stopInspectingNative: [],
|
|
shutdown: [],
|
|
traceUpdates: [Set<NativeType>],
|
|
|}> {
|
|
_bridge: BackendBridge;
|
|
_isProfiling: boolean = false;
|
|
_recordChangeDescriptions: boolean = false;
|
|
_rendererInterfaces: {[key: RendererID]: RendererInterface, ...} = {};
|
|
_persistedSelection: PersistedSelection | null = null;
|
|
_persistedSelectionMatch: PathMatch | null = null;
|
|
_traceUpdatesEnabled: boolean = false;
|
|
|
|
constructor(bridge: BackendBridge) {
|
|
super();
|
|
|
|
if (
|
|
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
|
|
) {
|
|
this._recordChangeDescriptions =
|
|
sessionStorageGetItem(
|
|
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
|
) === 'true';
|
|
this._isProfiling = true;
|
|
|
|
sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
|
|
sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY);
|
|
}
|
|
|
|
const persistedSelectionString = sessionStorageGetItem(
|
|
SESSION_STORAGE_LAST_SELECTION_KEY,
|
|
);
|
|
if (persistedSelectionString != null) {
|
|
this._persistedSelection = JSON.parse(persistedSelectionString);
|
|
}
|
|
|
|
this._bridge = bridge;
|
|
|
|
bridge.addListener('clearErrorsAndWarnings', this.clearErrorsAndWarnings);
|
|
bridge.addListener('clearErrorsForFiberID', this.clearErrorsForFiberID);
|
|
bridge.addListener('clearWarningsForFiberID', this.clearWarningsForFiberID);
|
|
bridge.addListener('copyElementPath', this.copyElementPath);
|
|
bridge.addListener('deletePath', this.deletePath);
|
|
bridge.addListener('getBackendVersion', this.getBackendVersion);
|
|
bridge.addListener('getBridgeProtocol', this.getBridgeProtocol);
|
|
bridge.addListener('getProfilingData', this.getProfilingData);
|
|
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
|
|
bridge.addListener('getOwnersList', this.getOwnersList);
|
|
bridge.addListener('inspectElement', this.inspectElement);
|
|
bridge.addListener('logElementToConsole', this.logElementToConsole);
|
|
bridge.addListener('overrideError', this.overrideError);
|
|
bridge.addListener('overrideSuspense', this.overrideSuspense);
|
|
bridge.addListener('overrideValueAtPath', this.overrideValueAtPath);
|
|
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
|
|
bridge.addListener('renamePath', this.renamePath);
|
|
bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled);
|
|
bridge.addListener('startProfiling', this.startProfiling);
|
|
bridge.addListener('stopProfiling', this.stopProfiling);
|
|
bridge.addListener('storeAsGlobal', this.storeAsGlobal);
|
|
bridge.addListener(
|
|
'syncSelectionFromNativeElementsPanel',
|
|
this.syncSelectionFromNativeElementsPanel,
|
|
);
|
|
bridge.addListener('shutdown', this.shutdown);
|
|
bridge.addListener(
|
|
'updateConsolePatchSettings',
|
|
this.updateConsolePatchSettings,
|
|
);
|
|
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
|
|
bridge.addListener('viewAttributeSource', this.viewAttributeSource);
|
|
bridge.addListener('viewElementSource', this.viewElementSource);
|
|
|
|
// Temporarily support older standalone front-ends sending commands to newer embedded backends.
|
|
// We do this because React Native embeds the React DevTools backend,
|
|
// but cannot control which version of the frontend users use.
|
|
bridge.addListener('overrideContext', this.overrideContext);
|
|
bridge.addListener('overrideHookState', this.overrideHookState);
|
|
bridge.addListener('overrideProps', this.overrideProps);
|
|
bridge.addListener('overrideState', this.overrideState);
|
|
|
|
if (this._isProfiling) {
|
|
bridge.send('profilingStatus', true);
|
|
}
|
|
|
|
// Send the Bridge protocol and backend versions, after initialization, in case the frontend has already requested it.
|
|
// The Store may be instantiated beore the agent.
|
|
const version = process.env.DEVTOOLS_VERSION;
|
|
if (version) {
|
|
this._bridge.send('backendVersion', version);
|
|
}
|
|
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
|
|
|
|
// Notify the frontend if the backend supports the Storage API (e.g. localStorage).
|
|
// If not, features like reload-and-profile will not work correctly and must be disabled.
|
|
let isBackendStorageAPISupported = false;
|
|
try {
|
|
localStorage.getItem('test');
|
|
isBackendStorageAPISupported = true;
|
|
} catch (error) {}
|
|
bridge.send('isBackendStorageAPISupported', isBackendStorageAPISupported);
|
|
bridge.send('isSynchronousXHRSupported', isSynchronousXHRSupported());
|
|
|
|
setupHighlighter(bridge, this);
|
|
setupTraceUpdates(this);
|
|
}
|
|
|
|
get rendererInterfaces(): {[key: RendererID]: RendererInterface, ...} {
|
|
return this._rendererInterfaces;
|
|
}
|
|
|
|
clearErrorsAndWarnings = ({rendererID}: {|rendererID: RendererID|}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
} else {
|
|
renderer.clearErrorsAndWarnings();
|
|
}
|
|
};
|
|
|
|
clearErrorsForFiberID = ({id, rendererID}: ElementAndRendererID) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
} else {
|
|
renderer.clearErrorsForFiberID(id);
|
|
}
|
|
};
|
|
|
|
clearWarningsForFiberID = ({id, rendererID}: ElementAndRendererID) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
} else {
|
|
renderer.clearWarningsForFiberID(id);
|
|
}
|
|
};
|
|
|
|
copyElementPath = ({id, path, rendererID}: CopyElementParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.copyElementPath(id, path);
|
|
}
|
|
};
|
|
|
|
deletePath = ({hookID, id, path, rendererID, type}: DeletePathParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.deletePath(type, id, hookID, path);
|
|
}
|
|
};
|
|
|
|
getInstanceAndStyle({
|
|
id,
|
|
rendererID,
|
|
}: ElementAndRendererID): InstanceAndStyle | null {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
return null;
|
|
}
|
|
return renderer.getInstanceAndStyle(id);
|
|
}
|
|
|
|
getBestMatchingRendererInterface(node: Object): RendererInterface | null {
|
|
let bestMatch = null;
|
|
for (const rendererID in this._rendererInterfaces) {
|
|
const renderer = ((this._rendererInterfaces[
|
|
(rendererID: any)
|
|
]: any): RendererInterface);
|
|
const fiber = renderer.getFiberForNative(node);
|
|
if (fiber !== null) {
|
|
// check if fiber.stateNode is matching the original hostInstance
|
|
if (fiber.stateNode === node) {
|
|
return renderer;
|
|
} else if (bestMatch === null) {
|
|
bestMatch = renderer;
|
|
}
|
|
}
|
|
}
|
|
// if an exact match is not found, return the first valid renderer as fallback
|
|
return bestMatch;
|
|
}
|
|
|
|
getIDForNode(node: Object): number | null {
|
|
const rendererInterface = this.getBestMatchingRendererInterface(node);
|
|
if (rendererInterface != null) {
|
|
try {
|
|
return rendererInterface.getFiberIDForNative(node, true);
|
|
} catch (error) {
|
|
// Some old React versions might throw if they can't find a match.
|
|
// If so we should ignore it...
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getBackendVersion = () => {
|
|
const version = process.env.DEVTOOLS_VERSION;
|
|
if (version) {
|
|
this._bridge.send('backendVersion', version);
|
|
}
|
|
};
|
|
|
|
getBridgeProtocol = () => {
|
|
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
|
|
};
|
|
|
|
getProfilingData = ({rendererID}: {|rendererID: RendererID|}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
}
|
|
|
|
this._bridge.send('profilingData', renderer.getProfilingData());
|
|
};
|
|
|
|
getProfilingStatus = () => {
|
|
this._bridge.send('profilingStatus', this._isProfiling);
|
|
};
|
|
|
|
getOwnersList = ({id, rendererID}: ElementAndRendererID) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
const owners = renderer.getOwnersList(id);
|
|
this._bridge.send('ownersList', ({id, owners}: OwnersList));
|
|
}
|
|
};
|
|
|
|
inspectElement = ({
|
|
forceFullData,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
requestID,
|
|
}: InspectElementParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
this._bridge.send(
|
|
'inspectedElement',
|
|
renderer.inspectElement(requestID, id, path, forceFullData),
|
|
);
|
|
|
|
// When user selects an element, stop trying to restore the selection,
|
|
// and instead remember the current selection for the next reload.
|
|
if (
|
|
this._persistedSelectionMatch === null ||
|
|
this._persistedSelectionMatch.id !== id
|
|
) {
|
|
this._persistedSelection = null;
|
|
this._persistedSelectionMatch = null;
|
|
renderer.setTrackedPath(null);
|
|
this._throttledPersistSelection(rendererID, id);
|
|
}
|
|
|
|
// TODO: If there was a way to change the selected DOM element
|
|
// in native Elements tab without forcing a switch to it, we'd do it here.
|
|
// For now, it doesn't seem like there is a way to do that:
|
|
// https://github.com/bvaughn/react-devtools-experimental/issues/102
|
|
// (Setting $0 doesn't work, and calling inspect() switches the tab.)
|
|
}
|
|
};
|
|
|
|
logElementToConsole = ({id, rendererID}: ElementAndRendererID) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.logElementToConsole(id);
|
|
}
|
|
};
|
|
|
|
overrideError = ({id, rendererID, forceError}: OverrideErrorParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.overrideError(id, forceError);
|
|
}
|
|
};
|
|
|
|
overrideSuspense = ({
|
|
id,
|
|
rendererID,
|
|
forceFallback,
|
|
}: OverrideSuspenseParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.overrideSuspense(id, forceFallback);
|
|
}
|
|
};
|
|
|
|
overrideValueAtPath = ({
|
|
hookID,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
type,
|
|
value,
|
|
}: OverrideValueAtPathParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.overrideValueAtPath(type, id, hookID, path, value);
|
|
}
|
|
};
|
|
|
|
// Temporarily support older standalone front-ends by forwarding the older message types
|
|
// to the new "overrideValueAtPath" command the backend is now listening to.
|
|
overrideContext = ({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
wasForwarded,
|
|
value,
|
|
}: SetInParams) => {
|
|
// Don't forward a message that's already been forwarded by the front-end Bridge.
|
|
// We only need to process the override command once!
|
|
if (!wasForwarded) {
|
|
this.overrideValueAtPath({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
type: 'context',
|
|
value,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Temporarily support older standalone front-ends by forwarding the older message types
|
|
// to the new "overrideValueAtPath" command the backend is now listening to.
|
|
overrideHookState = ({
|
|
id,
|
|
hookID,
|
|
path,
|
|
rendererID,
|
|
wasForwarded,
|
|
value,
|
|
}: OverrideHookParams) => {
|
|
// Don't forward a message that's already been forwarded by the front-end Bridge.
|
|
// We only need to process the override command once!
|
|
if (!wasForwarded) {
|
|
this.overrideValueAtPath({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
type: 'hooks',
|
|
value,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Temporarily support older standalone front-ends by forwarding the older message types
|
|
// to the new "overrideValueAtPath" command the backend is now listening to.
|
|
overrideProps = ({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
wasForwarded,
|
|
value,
|
|
}: SetInParams) => {
|
|
// Don't forward a message that's already been forwarded by the front-end Bridge.
|
|
// We only need to process the override command once!
|
|
if (!wasForwarded) {
|
|
this.overrideValueAtPath({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
type: 'props',
|
|
value,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Temporarily support older standalone front-ends by forwarding the older message types
|
|
// to the new "overrideValueAtPath" command the backend is now listening to.
|
|
overrideState = ({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
wasForwarded,
|
|
value,
|
|
}: SetInParams) => {
|
|
// Don't forward a message that's already been forwarded by the front-end Bridge.
|
|
// We only need to process the override command once!
|
|
if (!wasForwarded) {
|
|
this.overrideValueAtPath({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
type: 'state',
|
|
value,
|
|
});
|
|
}
|
|
};
|
|
|
|
reloadAndProfile = (recordChangeDescriptions: boolean) => {
|
|
sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
|
|
sessionStorageSetItem(
|
|
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
|
recordChangeDescriptions ? 'true' : 'false',
|
|
);
|
|
|
|
// This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
|
|
// In that case, the shell must also listen for this specific message to know when it needs to reload the app.
|
|
// The agent can't do this in a way that is renderer agnostic.
|
|
this._bridge.send('reloadAppForProfiling');
|
|
};
|
|
|
|
renamePath = ({
|
|
hookID,
|
|
id,
|
|
newPath,
|
|
oldPath,
|
|
rendererID,
|
|
type,
|
|
}: RenamePathParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.renamePath(type, id, hookID, oldPath, newPath);
|
|
}
|
|
};
|
|
|
|
selectNode(target: Object): void {
|
|
const id = this.getIDForNode(target);
|
|
if (id !== null) {
|
|
this._bridge.send('selectFiber', id);
|
|
}
|
|
}
|
|
|
|
setRendererInterface(
|
|
rendererID: RendererID,
|
|
rendererInterface: RendererInterface,
|
|
) {
|
|
this._rendererInterfaces[rendererID] = rendererInterface;
|
|
|
|
if (this._isProfiling) {
|
|
rendererInterface.startProfiling(this._recordChangeDescriptions);
|
|
}
|
|
|
|
rendererInterface.setTraceUpdatesEnabled(this._traceUpdatesEnabled);
|
|
|
|
// When the renderer is attached, we need to tell it whether
|
|
// we remember the previous selection that we'd like to restore.
|
|
// It'll start tracking mounts for matches to the last selection path.
|
|
const selection = this._persistedSelection;
|
|
if (selection !== null && selection.rendererID === rendererID) {
|
|
rendererInterface.setTrackedPath(selection.path);
|
|
}
|
|
}
|
|
|
|
setTraceUpdatesEnabled = (traceUpdatesEnabled: boolean) => {
|
|
this._traceUpdatesEnabled = traceUpdatesEnabled;
|
|
|
|
setTraceUpdatesEnabled(traceUpdatesEnabled);
|
|
|
|
for (const rendererID in this._rendererInterfaces) {
|
|
const renderer = ((this._rendererInterfaces[
|
|
(rendererID: any)
|
|
]: any): RendererInterface);
|
|
renderer.setTraceUpdatesEnabled(traceUpdatesEnabled);
|
|
}
|
|
};
|
|
|
|
syncSelectionFromNativeElementsPanel = () => {
|
|
const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
|
|
if (target == null) {
|
|
return;
|
|
}
|
|
this.selectNode(target);
|
|
};
|
|
|
|
shutdown = () => {
|
|
// Clean up the overlay if visible, and associated events.
|
|
this.emit('shutdown');
|
|
};
|
|
|
|
startProfiling = (recordChangeDescriptions: boolean) => {
|
|
this._recordChangeDescriptions = recordChangeDescriptions;
|
|
this._isProfiling = true;
|
|
for (const rendererID in this._rendererInterfaces) {
|
|
const renderer = ((this._rendererInterfaces[
|
|
(rendererID: any)
|
|
]: any): RendererInterface);
|
|
renderer.startProfiling(recordChangeDescriptions);
|
|
}
|
|
this._bridge.send('profilingStatus', this._isProfiling);
|
|
};
|
|
|
|
stopProfiling = () => {
|
|
this._isProfiling = false;
|
|
this._recordChangeDescriptions = false;
|
|
for (const rendererID in this._rendererInterfaces) {
|
|
const renderer = ((this._rendererInterfaces[
|
|
(rendererID: any)
|
|
]: any): RendererInterface);
|
|
renderer.stopProfiling();
|
|
}
|
|
this._bridge.send('profilingStatus', this._isProfiling);
|
|
};
|
|
|
|
stopInspectingNative = (selected: boolean) => {
|
|
this._bridge.send('stopInspectingNative', selected);
|
|
};
|
|
|
|
storeAsGlobal = ({count, id, path, rendererID}: StoreAsGlobalParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.storeAsGlobal(id, path, count);
|
|
}
|
|
};
|
|
|
|
updateConsolePatchSettings = ({
|
|
appendComponentStack,
|
|
breakOnConsoleErrors,
|
|
showInlineWarningsAndErrors,
|
|
hideConsoleLogsInStrictMode,
|
|
browserTheme,
|
|
}: {|
|
|
appendComponentStack: boolean,
|
|
breakOnConsoleErrors: boolean,
|
|
showInlineWarningsAndErrors: boolean,
|
|
hideConsoleLogsInStrictMode: boolean,
|
|
browserTheme: BrowserTheme,
|
|
|}) => {
|
|
// If the frontend preference has change,
|
|
// or in the case of React Native- if the backend is just finding out the preference-
|
|
// then reinstall the console overrides.
|
|
// It's safe to call these methods multiple times, so we don't need to worry about that.
|
|
patchConsole({
|
|
appendComponentStack,
|
|
breakOnConsoleErrors,
|
|
showInlineWarningsAndErrors,
|
|
hideConsoleLogsInStrictMode,
|
|
browserTheme,
|
|
});
|
|
};
|
|
|
|
updateComponentFilters = (componentFilters: Array<ComponentFilter>) => {
|
|
for (const rendererID in this._rendererInterfaces) {
|
|
const renderer = ((this._rendererInterfaces[
|
|
(rendererID: any)
|
|
]: any): RendererInterface);
|
|
renderer.updateComponentFilters(componentFilters);
|
|
}
|
|
};
|
|
|
|
viewAttributeSource = ({id, path, rendererID}: CopyElementParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.prepareViewAttributeSource(id, path);
|
|
}
|
|
};
|
|
|
|
viewElementSource = ({id, rendererID}: ElementAndRendererID) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.prepareViewElementSource(id);
|
|
}
|
|
};
|
|
|
|
onTraceUpdates = (nodes: Set<NativeType>) => {
|
|
this.emit('traceUpdates', nodes);
|
|
};
|
|
|
|
onFastRefreshScheduled = () => {
|
|
if (__DEBUG__) {
|
|
debug('onFastRefreshScheduled');
|
|
}
|
|
|
|
this._bridge.send('fastRefreshScheduled');
|
|
};
|
|
|
|
onHookOperations = (operations: Array<number>) => {
|
|
if (__DEBUG__) {
|
|
debug(
|
|
'onHookOperations',
|
|
`(${operations.length}) [${operations.join(', ')}]`,
|
|
);
|
|
}
|
|
|
|
// TODO:
|
|
// The chrome.runtime does not currently support transferables; it forces JSON serialization.
|
|
// See bug https://bugs.chromium.org/p/chromium/issues/detail?id=927134
|
|
//
|
|
// Regarding transferables, the postMessage doc states:
|
|
// If the ownership of an object is transferred, it becomes unusable (neutered)
|
|
// in the context it was sent from and becomes available only to the worker it was sent to.
|
|
//
|
|
// Even though Chrome is eventually JSON serializing the array buffer,
|
|
// using the transferable approach also sometimes causes it to throw:
|
|
// DOMException: Failed to execute 'postMessage' on 'Window': ArrayBuffer at index 0 is already neutered.
|
|
//
|
|
// See bug https://github.com/bvaughn/react-devtools-experimental/issues/25
|
|
//
|
|
// The Store has a fallback in place that parses the message as JSON if the type isn't an array.
|
|
// For now the simplest fix seems to be to not transfer the array.
|
|
// This will negatively impact performance on Firefox so it's unfortunate,
|
|
// but until we're able to fix the Chrome error mentioned above, it seems necessary.
|
|
//
|
|
// this._bridge.send('operations', operations, [operations.buffer]);
|
|
this._bridge.send('operations', operations);
|
|
|
|
if (this._persistedSelection !== null) {
|
|
const rendererID = operations[0];
|
|
if (this._persistedSelection.rendererID === rendererID) {
|
|
// Check if we can select a deeper match for the persisted selection.
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
} else {
|
|
const prevMatch = this._persistedSelectionMatch;
|
|
const nextMatch = renderer.getBestMatchForTrackedPath();
|
|
this._persistedSelectionMatch = nextMatch;
|
|
const prevMatchID = prevMatch !== null ? prevMatch.id : null;
|
|
const nextMatchID = nextMatch !== null ? nextMatch.id : null;
|
|
if (prevMatchID !== nextMatchID) {
|
|
if (nextMatchID !== null) {
|
|
// We moved forward, unlocking a deeper node.
|
|
this._bridge.send('selectFiber', nextMatchID);
|
|
}
|
|
}
|
|
if (nextMatch !== null && nextMatch.isFullMatch) {
|
|
// We've just unlocked the innermost selected node.
|
|
// There's no point tracking it further.
|
|
this._persistedSelection = null;
|
|
this._persistedSelectionMatch = null;
|
|
renderer.setTrackedPath(null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
onUnsupportedRenderer(rendererID: number) {
|
|
this._bridge.send('unsupportedRendererVersion', rendererID);
|
|
}
|
|
|
|
_throttledPersistSelection = throttle((rendererID: number, id: number) => {
|
|
// This is throttled, so both renderer and selected ID
|
|
// might not be available by the time we read them.
|
|
// This is why we need the defensive checks here.
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
const path = renderer != null ? renderer.getPathForElement(id) : null;
|
|
if (path !== null) {
|
|
sessionStorageSetItem(
|
|
SESSION_STORAGE_LAST_SELECTION_KEY,
|
|
JSON.stringify(({rendererID, path}: PersistedSelection)),
|
|
);
|
|
} else {
|
|
sessionStorageRemoveItem(SESSION_STORAGE_LAST_SELECTION_KEY);
|
|
}
|
|
}, 1000);
|
|
}
|