Agent
Agent
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 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>,
};
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);
}
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);
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);
}
if (value != null) {
this._bridge.send('saveToClipboard', value);
} else {
console.warn(`Unable to obtain serialized value for element "${id}"`);
}
}
};
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);
}
this._bridge.send('profilingData', renderer.getProfilingData());
};
// 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');
};
setRendererInterface(
rendererID: RendererID,
rendererInterface: RendererInterface,
) {
this._rendererInterfaces[rendererID] = rendererInterface;
if (this._isProfiling) {
rendererInterface.startProfiling(this._recordChangeDescriptions);
}
rendererInterface.setTraceUpdatesEnabled(this._traceUpdatesEnabled);
setTraceUpdatesEnabled(traceUpdatesEnabled);
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,
});
};
this._bridge.send('fastRefreshScheduled');
};
// 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);
}