mirror of
https://github.com/facebook/react.git
synced 2026-02-26 18:58:05 +00:00
Stacked on #30625 and #30657. This ensures that we only create instances during the commit reconciliation and that we don't create unnecessary instances for things that are filtered or not mounted. This ensures that we also can rely on the reconciliation to do all the clean up. Now everything is created and deleted as a pair in the same pass. Previously we were including unfiltered components in the owner stack which probably doesn't make sense since you're intending to filter them everywhere presumably. However, it also means that those links were broken since you can't link into owners that don't exist in the parent tree. The main complication is the component filters. It relied on not unmounting the old instances. I had to update some tests that asserted on ids that are now shifted. For warnings/errors tracking I now restore them back into the pending set when they unmount. Basically it puts them back into their "pre-commit" state. That way when they remount they’re still there. For restoring the current selection I use the tracked path mechanism instead of relying on the id being unchanged. This is better anyway because if you filter out the currently selected item it's better to select the nearest match instead of just losing the selection.
936 lines
29 KiB
JavaScript
936 lines
29 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {
|
|
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,
|
|
HostInstance,
|
|
OwnersList,
|
|
PathFrame,
|
|
PathMatch,
|
|
RendererID,
|
|
RendererInterface,
|
|
ConsolePatchSettings,
|
|
} from './types';
|
|
import type {
|
|
ComponentFilter,
|
|
BrowserTheme,
|
|
} from 'react-devtools-shared/src/frontend/types';
|
|
import {isSynchronousXHRSupported, isReactNativeEnvironment} from './utils';
|
|
|
|
const debug = (methodName: string, ...args: Array<string>) => {
|
|
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: [HostInstance],
|
|
startInspectingNative: [],
|
|
stopInspectingNative: [],
|
|
shutdown: [],
|
|
traceUpdates: [Set<HostInstance>],
|
|
drawTraceUpdates: [Array<HostInstance>],
|
|
disableTraceUpdates: [],
|
|
}> {
|
|
_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('clearErrorsForElementID', this.clearErrorsForElementID);
|
|
bridge.addListener(
|
|
'clearWarningsForElementID',
|
|
this.clearWarningsForElementID,
|
|
);
|
|
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(
|
|
'syncSelectionFromBuiltinElementsPanel',
|
|
this.syncSelectionFromBuiltinElementsPanel,
|
|
);
|
|
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}) => void = ({
|
|
rendererID,
|
|
}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
} else {
|
|
renderer.clearErrorsAndWarnings();
|
|
}
|
|
};
|
|
|
|
clearErrorsForElementID: ElementAndRendererID => void = ({
|
|
id,
|
|
rendererID,
|
|
}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
} else {
|
|
renderer.clearErrorsForElementID(id);
|
|
}
|
|
};
|
|
|
|
clearWarningsForElementID: ElementAndRendererID => void = ({
|
|
id,
|
|
rendererID,
|
|
}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
} else {
|
|
renderer.clearWarningsForElementID(id);
|
|
}
|
|
};
|
|
|
|
copyElementPath: CopyElementParams => void = ({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
}: CopyElementParams) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
const value = renderer.getSerializedElementValueByPath(id, path);
|
|
|
|
if (value != null) {
|
|
this._bridge.send('saveToClipboard', value);
|
|
} else {
|
|
console.warn(`Unable to obtain serialized value for element "${id}"`);
|
|
}
|
|
}
|
|
};
|
|
|
|
deletePath: DeletePathParams => void = ({
|
|
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);
|
|
}
|
|
|
|
getIDForHostInstance(target: HostInstance): number | null {
|
|
let bestMatch: null | HostInstance = null;
|
|
let bestRenderer: null | RendererInterface = null;
|
|
// Find the nearest ancestor which is mounted by a React.
|
|
for (const rendererID in this._rendererInterfaces) {
|
|
const renderer = ((this._rendererInterfaces[
|
|
(rendererID: any)
|
|
]: any): RendererInterface);
|
|
const nearestNode: null = renderer.getNearestMountedHostInstance(target);
|
|
if (nearestNode !== null) {
|
|
if (nearestNode === target) {
|
|
// Exact match we can exit early.
|
|
bestMatch = nearestNode;
|
|
bestRenderer = renderer;
|
|
break;
|
|
}
|
|
if (
|
|
bestMatch === null ||
|
|
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
|
|
) {
|
|
// If this is the first match or the previous match contains the new match,
|
|
// so the new match is a deeper and therefore better match.
|
|
bestMatch = nearestNode;
|
|
bestRenderer = renderer;
|
|
}
|
|
}
|
|
}
|
|
if (bestRenderer != null && bestMatch != null) {
|
|
try {
|
|
return bestRenderer.getElementIDForHostInstance(bestMatch, true);
|
|
} catch (error) {
|
|
// Some old React versions might throw if they can't find a match.
|
|
// If so we should ignore it...
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getComponentNameForHostInstance(target: HostInstance): string | null {
|
|
// We duplicate this code from getIDForHostInstance to avoid an object allocation.
|
|
let bestMatch: null | HostInstance = null;
|
|
let bestRenderer: null | RendererInterface = null;
|
|
// Find the nearest ancestor which is mounted by a React.
|
|
for (const rendererID in this._rendererInterfaces) {
|
|
const renderer = ((this._rendererInterfaces[
|
|
(rendererID: any)
|
|
]: any): RendererInterface);
|
|
const nearestNode = renderer.getNearestMountedHostInstance(target);
|
|
if (nearestNode !== null) {
|
|
if (nearestNode === target) {
|
|
// Exact match we can exit early.
|
|
bestMatch = nearestNode;
|
|
bestRenderer = renderer;
|
|
break;
|
|
}
|
|
if (
|
|
bestMatch === null ||
|
|
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
|
|
) {
|
|
// If this is the first match or the previous match contains the new match,
|
|
// so the new match is a deeper and therefore better match.
|
|
bestMatch = nearestNode;
|
|
bestRenderer = renderer;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestRenderer != null && bestMatch != null) {
|
|
try {
|
|
const id = bestRenderer.getElementIDForHostInstance(bestMatch, true);
|
|
if (id) {
|
|
return bestRenderer.getDisplayNameForElementID(id);
|
|
}
|
|
} catch (error) {
|
|
// Some old React versions might throw if they can't find a match.
|
|
// If so we should ignore it...
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getBackendVersion: () => void = () => {
|
|
const version = process.env.DEVTOOLS_VERSION;
|
|
if (version) {
|
|
this._bridge.send('backendVersion', version);
|
|
}
|
|
};
|
|
|
|
getBridgeProtocol: () => void = () => {
|
|
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
|
|
};
|
|
|
|
getProfilingData: ({rendererID: RendererID}) => void = ({rendererID}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}"`);
|
|
}
|
|
|
|
this._bridge.send('profilingData', renderer.getProfilingData());
|
|
};
|
|
|
|
getProfilingStatus: () => void = () => {
|
|
this._bridge.send('profilingStatus', this._isProfiling);
|
|
};
|
|
|
|
getOwnersList: ElementAndRendererID => void = ({id, rendererID}) => {
|
|
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: InspectElementParams => void = ({
|
|
forceFullData,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
requestID,
|
|
}) => {
|
|
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);
|
|
// Throttle persisting the selection.
|
|
this._lastSelectedElementID = id;
|
|
this._lastSelectedRendererID = rendererID;
|
|
if (!this._persistSelectionTimerScheduled) {
|
|
this._persistSelectionTimerScheduled = true;
|
|
setTimeout(this._persistSelection, 1000);
|
|
}
|
|
}
|
|
|
|
// TODO: If there was a way to change the selected DOM element
|
|
// in built-in 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: ElementAndRendererID => void = ({id, rendererID}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.logElementToConsole(id);
|
|
}
|
|
};
|
|
|
|
overrideError: OverrideErrorParams => void = ({
|
|
id,
|
|
rendererID,
|
|
forceError,
|
|
}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.overrideError(id, forceError);
|
|
}
|
|
};
|
|
|
|
overrideSuspense: OverrideSuspenseParams => void = ({
|
|
id,
|
|
rendererID,
|
|
forceFallback,
|
|
}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.overrideSuspense(id, forceFallback);
|
|
}
|
|
};
|
|
|
|
overrideValueAtPath: OverrideValueAtPathParams => void = ({
|
|
hookID,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
type,
|
|
value,
|
|
}) => {
|
|
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: SetInParams => void = ({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
wasForwarded,
|
|
value,
|
|
}) => {
|
|
// 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: OverrideHookParams => void = ({
|
|
id,
|
|
hookID,
|
|
path,
|
|
rendererID,
|
|
wasForwarded,
|
|
value,
|
|
}) => {
|
|
// 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: SetInParams => void = ({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
wasForwarded,
|
|
value,
|
|
}) => {
|
|
// 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: SetInParams => void = ({
|
|
id,
|
|
path,
|
|
rendererID,
|
|
wasForwarded,
|
|
value,
|
|
}) => {
|
|
// 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) => void =
|
|
recordChangeDescriptions => {
|
|
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: RenamePathParams => void = ({
|
|
hookID,
|
|
id,
|
|
newPath,
|
|
oldPath,
|
|
rendererID,
|
|
type,
|
|
}) => {
|
|
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: HostInstance): void {
|
|
const id = this.getIDForHostInstance(target);
|
|
if (id !== null) {
|
|
this._bridge.send('selectElement', 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) => void =
|
|
traceUpdatesEnabled => {
|
|
this._traceUpdatesEnabled = traceUpdatesEnabled;
|
|
|
|
setTraceUpdatesEnabled(traceUpdatesEnabled);
|
|
|
|
for (const rendererID in this._rendererInterfaces) {
|
|
const renderer = ((this._rendererInterfaces[
|
|
(rendererID: any)
|
|
]: any): RendererInterface);
|
|
renderer.setTraceUpdatesEnabled(traceUpdatesEnabled);
|
|
}
|
|
};
|
|
|
|
syncSelectionFromBuiltinElementsPanel: () => void = () => {
|
|
const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
|
|
if (target == null) {
|
|
return;
|
|
}
|
|
this.selectNode(target);
|
|
};
|
|
|
|
shutdown: () => void = () => {
|
|
// Clean up the overlay if visible, and associated events.
|
|
this.emit('shutdown');
|
|
};
|
|
|
|
startProfiling: (recordChangeDescriptions: boolean) => void =
|
|
recordChangeDescriptions => {
|
|
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: () => void = () => {
|
|
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) => void = selected => {
|
|
this._bridge.send('stopInspectingHost', selected);
|
|
};
|
|
|
|
storeAsGlobal: StoreAsGlobalParams => void = ({
|
|
count,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
}) => {
|
|
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: boolean,
|
|
breakOnConsoleErrors: boolean,
|
|
browserTheme: BrowserTheme,
|
|
hideConsoleLogsInStrictMode: boolean,
|
|
showInlineWarningsAndErrors: boolean,
|
|
}) => void = ({
|
|
appendComponentStack,
|
|
breakOnConsoleErrors,
|
|
showInlineWarningsAndErrors,
|
|
hideConsoleLogsInStrictMode,
|
|
browserTheme,
|
|
}: ConsolePatchSettings) => {
|
|
// If the frontend preferences have changed,
|
|
// or in the case of React Native- if the backend is just finding out the preferences-
|
|
// then reinstall the console overrides.
|
|
// It's safe to call `patchConsole` multiple times.
|
|
patchConsole({
|
|
appendComponentStack,
|
|
breakOnConsoleErrors,
|
|
showInlineWarningsAndErrors,
|
|
hideConsoleLogsInStrictMode,
|
|
browserTheme,
|
|
});
|
|
};
|
|
|
|
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void =
|
|
componentFilters => {
|
|
for (const rendererIDString in this._rendererInterfaces) {
|
|
const rendererID = +rendererIDString;
|
|
const renderer = ((this._rendererInterfaces[
|
|
(rendererID: any)
|
|
]: any): RendererInterface);
|
|
if (this._lastSelectedRendererID === rendererID) {
|
|
// Changing component filters will unmount and remount the DevTools tree.
|
|
// Track the last selection's path so we can restore the selection.
|
|
const path = renderer.getPathForElement(this._lastSelectedElementID);
|
|
if (path !== null) {
|
|
renderer.setTrackedPath(path);
|
|
this._persistedSelection = {
|
|
rendererID,
|
|
path,
|
|
};
|
|
}
|
|
}
|
|
renderer.updateComponentFilters(componentFilters);
|
|
}
|
|
};
|
|
|
|
viewAttributeSource: CopyElementParams => void = ({id, path, rendererID}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.prepareViewAttributeSource(id, path);
|
|
}
|
|
};
|
|
|
|
viewElementSource: ElementAndRendererID => void = ({id, rendererID}) => {
|
|
const renderer = this._rendererInterfaces[rendererID];
|
|
if (renderer == null) {
|
|
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
|
} else {
|
|
renderer.prepareViewElementSource(id);
|
|
}
|
|
};
|
|
|
|
onTraceUpdates: (nodes: Set<HostInstance>) => void = nodes => {
|
|
this.emit('traceUpdates', nodes);
|
|
};
|
|
|
|
onFastRefreshScheduled: () => void = () => {
|
|
if (__DEBUG__) {
|
|
debug('onFastRefreshScheduled');
|
|
}
|
|
|
|
this._bridge.send('fastRefreshScheduled');
|
|
};
|
|
|
|
onHookOperations: (operations: Array<number>) => void = operations => {
|
|
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('selectElement', 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);
|
|
}
|
|
|
|
_persistSelectionTimerScheduled: boolean = false;
|
|
_lastSelectedRendererID: number = -1;
|
|
_lastSelectedElementID: number = -1;
|
|
_persistSelection: any = () => {
|
|
this._persistSelectionTimerScheduled = false;
|
|
const rendererID = this._lastSelectedRendererID;
|
|
const id = this._lastSelectedElementID;
|
|
// 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);
|
|
}
|
|
};
|
|
}
|