mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
DevTools: Add Bridge protocol version backend/frontend (#21331)
Add an explicit Bridge protocol version to the frontend and backend components as well as a check during initialization to ensure that both are compatible. If not, the frontend will display either upgrade or downgrade instructions. Note that only the `react-devtools-core` (React Native) and `react-devtools-inline` (Code Sandbox) packages implement this check. Browser extensions inject their own backend and so the check is unnecessary. (Arguably the `react-devtools-inline` check is also unlikely to be necessary _but_ has been added as an extra guard for use cases such as Replay.io.)
This commit is contained in:
+4
-1
@@ -217,7 +217,10 @@ function initialize(socket: WebSocket) {
|
||||
socket.close();
|
||||
});
|
||||
|
||||
store = new Store(bridge, {supportsNativeInspection: false});
|
||||
store = new Store(bridge, {
|
||||
checkBridgeProtocolCompatibility: true,
|
||||
supportsNativeInspection: false,
|
||||
});
|
||||
|
||||
log('Connected');
|
||||
reload();
|
||||
|
||||
+4
-1
@@ -21,7 +21,10 @@ import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {Props} from 'react-devtools-shared/src/devtools/views/DevTools';
|
||||
|
||||
export function createStore(bridge: FrontendBridge): Store {
|
||||
return new Store(bridge, {supportsTraceUpdates: true});
|
||||
return new Store(bridge, {
|
||||
checkBridgeProtocolCompatibility: true,
|
||||
supportsTraceUpdates: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function createBridge(
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
toggleEnabled as setTraceUpdatesEnabled,
|
||||
} from './views/TraceUpdates';
|
||||
import {patch as patchConsole, unpatch as unpatchConsole} from './console';
|
||||
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
|
||||
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {
|
||||
@@ -176,6 +177,7 @@ export default class Agent extends EventEmitter<{|
|
||||
bridge.addListener('clearWarningsForFiberID', this.clearWarningsForFiberID);
|
||||
bridge.addListener('copyElementPath', this.copyElementPath);
|
||||
bridge.addListener('deletePath', this.deletePath);
|
||||
bridge.addListener('getBridgeProtocol', this.getBridgeProtocol);
|
||||
bridge.addListener('getProfilingData', this.getProfilingData);
|
||||
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
|
||||
bridge.addListener('getOwnersList', this.getOwnersList);
|
||||
@@ -308,6 +310,10 @@ export default class Agent extends EventEmitter<{|
|
||||
return null;
|
||||
}
|
||||
|
||||
getBridgeProtocol = () => {
|
||||
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
|
||||
};
|
||||
|
||||
getProfilingData = ({rendererID}: {|rendererID: RendererID|}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
|
||||
+45
@@ -20,6 +20,49 @@ import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-share
|
||||
|
||||
const BATCH_DURATION = 100;
|
||||
|
||||
// This message specifies the version of the DevTools protocol currently supported by the backend,
|
||||
// as well as the earliest NPM version (e.g. "4.13.0") that protocol is supported by on the frontend.
|
||||
// This enables an older frontend to display an upgrade message to users for a newer, unsupported backend.
|
||||
export type BridgeProtocol = {|
|
||||
// Version supported by the current frontend/backend.
|
||||
version: number,
|
||||
|
||||
// NPM version range that also supports this version.
|
||||
// Note that 'maxNpmVersion' is only set when the version is bumped.
|
||||
minNpmVersion: string,
|
||||
maxNpmVersion: string | null,
|
||||
|};
|
||||
|
||||
// Bump protocol version whenever a backwards breaking change is made
|
||||
// in the messages sent between BackendBridge and FrontendBridge.
|
||||
// This mapping is embedded in both frontend and backend builds.
|
||||
//
|
||||
// The backend protocol will always be the latest entry in the BRIDGE_PROTOCOL array.
|
||||
//
|
||||
// When an older frontend connects to a newer backend,
|
||||
// the backend can send the minNpmVersion and the frontend can display an NPM upgrade prompt.
|
||||
//
|
||||
// When a newer frontend connects with an older protocol version,
|
||||
// the frontend can use the embedded minNpmVersion/maxNpmVersion values to display a downgrade prompt.
|
||||
export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
|
||||
// This version technically never existed,
|
||||
// but a backwards breaking change was added in 4.11,
|
||||
// so the safest guess to downgrade the frontend would be to version 4.10.
|
||||
{
|
||||
version: 0,
|
||||
minNpmVersion: '<4.11.0',
|
||||
maxNpmVersion: '<4.11.0',
|
||||
},
|
||||
{
|
||||
version: 1,
|
||||
minNpmVersion: '4.13.0',
|
||||
maxNpmVersion: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const currentBridgeProtocol: BridgeProtocol =
|
||||
BRIDGE_PROTOCOL[BRIDGE_PROTOCOL.length - 1];
|
||||
|
||||
type ElementAndRendererID = {|id: number, rendererID: RendererID|};
|
||||
|
||||
type Message = {|
|
||||
@@ -119,6 +162,7 @@ type UpdateConsolePatchSettingsParams = {|
|
||||
|};
|
||||
|
||||
export type BackendEvents = {|
|
||||
bridgeProtocol: [BridgeProtocol],
|
||||
extensionBackendInitialized: [],
|
||||
inspectedElement: [InspectedElementPayload],
|
||||
isBackendStorageAPISupported: [boolean],
|
||||
@@ -150,6 +194,7 @@ type FrontendEvents = {|
|
||||
clearWarningsForFiberID: [ElementAndRendererID],
|
||||
copyElementPath: [CopyElementPathParams],
|
||||
deletePath: [DeletePath],
|
||||
getBridgeProtocol: [],
|
||||
getOwnersList: [ElementAndRendererID],
|
||||
getProfilingData: [{|rendererID: RendererID|}],
|
||||
getProfilingStatus: [],
|
||||
|
||||
+65
-1
@@ -29,10 +29,17 @@ import {localStorageGetItem, localStorageSetItem} from '../storage';
|
||||
import {__DEBUG__} from '../constants';
|
||||
import {printStore} from './utils';
|
||||
import ProfilerStore from './ProfilerStore';
|
||||
import {
|
||||
BRIDGE_PROTOCOL,
|
||||
currentBridgeProtocol,
|
||||
} from 'react-devtools-shared/src/bridge';
|
||||
|
||||
import type {Element} from './views/Components/types';
|
||||
import type {ComponentFilter, ElementType} from '../types';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {
|
||||
FrontendBridge,
|
||||
BridgeProtocol,
|
||||
} from 'react-devtools-shared/src/bridge';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
if (__DEBUG__) {
|
||||
@@ -51,6 +58,7 @@ const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
|
||||
'React::DevTools::recordChangeDescriptions';
|
||||
|
||||
type Config = {|
|
||||
checkBridgeProtocolCompatibility?: boolean,
|
||||
isProfiling?: boolean,
|
||||
supportsNativeInspection?: boolean,
|
||||
supportsReloadAndProfile?: boolean,
|
||||
@@ -76,6 +84,7 @@ export default class Store extends EventEmitter<{|
|
||||
supportsNativeStyleEditor: [],
|
||||
supportsProfiling: [],
|
||||
supportsReloadAndProfile: [],
|
||||
unsupportedBridgeProtocolDetected: [],
|
||||
unsupportedRendererVersionDetected: [],
|
||||
|}> {
|
||||
_bridge: FrontendBridge;
|
||||
@@ -119,6 +128,10 @@ export default class Store extends EventEmitter<{|
|
||||
|
||||
_nativeStyleEditorValidAttributes: $ReadOnlyArray<string> | null = null;
|
||||
|
||||
// Older backends don't support an explicit bridge protocol,
|
||||
// so we should timeout eventually and show a downgrade message.
|
||||
_onBridgeProtocolTimeoutID: TimeoutID | null = null;
|
||||
|
||||
// Map of element (id) to the set of elements (ids) it owns.
|
||||
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
|
||||
_ownersMap: Map<number, Set<number>> = new Map();
|
||||
@@ -147,6 +160,7 @@ export default class Store extends EventEmitter<{|
|
||||
_supportsReloadAndProfile: boolean = false;
|
||||
_supportsTraceUpdates: boolean = false;
|
||||
|
||||
_unsupportedBridgeProtocol: BridgeProtocol | null = null;
|
||||
_unsupportedRendererVersionDetected: boolean = false;
|
||||
|
||||
// Total number of visible elements (within all roots).
|
||||
@@ -217,6 +231,20 @@ export default class Store extends EventEmitter<{|
|
||||
);
|
||||
|
||||
this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
|
||||
|
||||
// Verify that the frontend version is compatible with the connected backend.
|
||||
// See github.com/facebook/react/issues/21326
|
||||
if (config != null && config.checkBridgeProtocolCompatibility) {
|
||||
// Older backends don't support an explicit bridge protocol,
|
||||
// so we should timeout eventually and show a downgrade message.
|
||||
this._onBridgeProtocolTimeoutID = setTimeout(
|
||||
this.onBridgeProtocolTimeout,
|
||||
10000,
|
||||
);
|
||||
|
||||
bridge.addListener('bridgeProtocol', this.onBridgeProtocol);
|
||||
bridge.send('getBridgeProtocol');
|
||||
}
|
||||
}
|
||||
|
||||
// This is only used in tests to avoid memory leaks.
|
||||
@@ -385,6 +413,10 @@ export default class Store extends EventEmitter<{|
|
||||
return this._supportsTraceUpdates;
|
||||
}
|
||||
|
||||
get unsupportedBridgeProtocol(): BridgeProtocol | null {
|
||||
return this._unsupportedBridgeProtocol;
|
||||
}
|
||||
|
||||
get unsupportedRendererVersionDetected(): boolean {
|
||||
return this._unsupportedRendererVersionDetected;
|
||||
}
|
||||
@@ -1168,6 +1200,12 @@ export default class Store extends EventEmitter<{|
|
||||
'unsupportedRendererVersion',
|
||||
this.onBridgeUnsupportedRendererVersion,
|
||||
);
|
||||
bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
|
||||
|
||||
if (this._onBridgeProtocolTimeoutID !== null) {
|
||||
clearTimeout(this._onBridgeProtocolTimeoutID);
|
||||
this._onBridgeProtocolTimeoutID = null;
|
||||
}
|
||||
};
|
||||
|
||||
onBackendStorageAPISupported = (isBackendStorageAPISupported: boolean) => {
|
||||
@@ -1187,4 +1225,30 @@ export default class Store extends EventEmitter<{|
|
||||
|
||||
this.emit('unsupportedRendererVersionDetected');
|
||||
};
|
||||
|
||||
onBridgeProtocol = (bridgeProtocol: BridgeProtocol) => {
|
||||
if (this._onBridgeProtocolTimeoutID !== null) {
|
||||
clearTimeout(this._onBridgeProtocolTimeoutID);
|
||||
this._onBridgeProtocolTimeoutID = null;
|
||||
}
|
||||
|
||||
if (bridgeProtocol.version !== currentBridgeProtocol.version) {
|
||||
this._unsupportedBridgeProtocol = bridgeProtocol;
|
||||
} else {
|
||||
// If we should happen to get a response after timing out...
|
||||
this._unsupportedBridgeProtocol = null;
|
||||
}
|
||||
|
||||
this.emit('unsupportedBridgeProtocolDetected');
|
||||
};
|
||||
|
||||
onBridgeProtocolTimeout = () => {
|
||||
this._onBridgeProtocolTimeoutID = null;
|
||||
|
||||
// If we timed out, that indicates the backend predates the bridge protocol,
|
||||
// so we can set a fake version (0) to trigger the downgrade message.
|
||||
this._unsupportedBridgeProtocol = BRIDGE_PROTOCOL[0];
|
||||
|
||||
this.emit('unsupportedBridgeProtocolDetected');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import ViewElementSourceContext from './Components/ViewElementSourceContext';
|
||||
import {ProfilerContextController} from './Profiler/ProfilerContext';
|
||||
import {ModalDialogContextController} from './ModalDialog';
|
||||
import ReactLogo from './ReactLogo';
|
||||
import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog';
|
||||
import UnsupportedVersionDialog from './UnsupportedVersionDialog';
|
||||
import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
|
||||
import {useLocalStorage} from './hooks';
|
||||
@@ -226,6 +227,7 @@ export default function DevTools({
|
||||
</TreeContextController>
|
||||
</ViewElementSourceContext.Provider>
|
||||
</SettingsContextController>
|
||||
<UnsupportedBridgeProtocolDialog />
|
||||
{warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
|
||||
{warnIfUnsupportedVersionDetected && <UnsupportedVersionDialog />}
|
||||
</ModalDialogContextController>
|
||||
|
||||
@@ -383,6 +383,13 @@ export function updateThemeVariables(
|
||||
updateStyleHelper(theme, 'color-expand-collapse-toggle', documentElements);
|
||||
updateStyleHelper(theme, 'color-link', documentElements);
|
||||
updateStyleHelper(theme, 'color-modal-background', documentElements);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-bridge-version-npm-background',
|
||||
documentElements,
|
||||
);
|
||||
updateStyleHelper(theme, 'color-bridge-version-npm-text', documentElements);
|
||||
updateStyleHelper(theme, 'color-bridge-version-number', documentElements);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-primitive-hook-badge-background',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
.Column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.Title {
|
||||
font-size: var(--font-size-sans-large);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.ReleaseNotesLink {
|
||||
color: var(--color-button-active);
|
||||
}
|
||||
|
||||
.Version {
|
||||
color: var(--color-bridge-version-number);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.NpmCommand {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
|
||||
background-color: var(--color-bridge-version-npm-background);
|
||||
color: var(--color-bridge-version-npm-text);
|
||||
margin: 0;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-large);
|
||||
}
|
||||
|
||||
.Paragraph {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.Link {
|
||||
color: var(--color-link);
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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 * as React from 'react';
|
||||
import {Fragment, useContext, useEffect} from 'react';
|
||||
import {ModalDialogContext} from './ModalDialog';
|
||||
import {StoreContext} from './context';
|
||||
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
|
||||
import Button from './Button';
|
||||
import ButtonIcon from './ButtonIcon';
|
||||
import {copy} from 'clipboard-js';
|
||||
import styles from './UnsupportedBridgeProtocolDialog.css';
|
||||
|
||||
import type {BridgeProtocol} from 'react-devtools-shared/src/bridge';
|
||||
|
||||
const DEVTOOLS_VERSION = process.env.DEVTOOLS_VERSION;
|
||||
const INSTRUCTIONS_FB_URL = 'https://fburl.com/devtools-bridge-protocol';
|
||||
|
||||
export default function UnsupportedBridgeProtocolDialog(_: {||}) {
|
||||
const {dispatch, isVisible} = useContext(ModalDialogContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
useEffect(() => {
|
||||
const updateDialog = () => {
|
||||
if (!isVisible) {
|
||||
if (store.unsupportedBridgeProtocol !== null) {
|
||||
dispatch({
|
||||
canBeDismissed: false,
|
||||
type: 'SHOW',
|
||||
content: (
|
||||
<DialogContent
|
||||
unsupportedBridgeProtocol={store.unsupportedBridgeProtocol}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (store.unsupportedBridgeProtocol === null) {
|
||||
dispatch({type: 'HIDE'});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateDialog();
|
||||
|
||||
store.addListener('unsupportedBridgeProtocolDetected', updateDialog);
|
||||
return () => {
|
||||
store.removeListener('unsupportedBridgeProtocolDetected', updateDialog);
|
||||
};
|
||||
}, [isVisible, store]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
unsupportedBridgeProtocol,
|
||||
}: {|
|
||||
unsupportedBridgeProtocol: BridgeProtocol,
|
||||
|}) {
|
||||
const {version, minNpmVersion, maxNpmVersion} = unsupportedBridgeProtocol;
|
||||
|
||||
let instructions;
|
||||
if (maxNpmVersion === null) {
|
||||
const upgradeInstructions = `npm i -g react-devtools@^${minNpmVersion}`;
|
||||
instructions = (
|
||||
<>
|
||||
<p className={styles.Paragraph}>
|
||||
To fix this, upgrade the DevTools NPM package:
|
||||
</p>
|
||||
<pre className={styles.NpmCommand}>
|
||||
{upgradeInstructions}
|
||||
<Button
|
||||
onClick={() => copy(upgradeInstructions)}
|
||||
title="Copy upgrade command to clipboard">
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const downgradeInstructions = `npm i -g react-devtools@${maxNpmVersion}`;
|
||||
instructions = (
|
||||
<>
|
||||
<p className={styles.Paragraph}>
|
||||
To fix this, downgrade the DevTools NPM package:
|
||||
</p>
|
||||
<pre className={styles.NpmCommand}>
|
||||
{downgradeInstructions}
|
||||
<Button
|
||||
onClick={() => copy(downgradeInstructions)}
|
||||
title="Copy downgrade command to clipboard">
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles.Column}>
|
||||
<div className={styles.Title}>Unsupported DevTools backend version</div>
|
||||
<p className={styles.Paragraph}>
|
||||
You are running <code>react-devtools</code> version{' '}
|
||||
<span className={styles.Version}>{DEVTOOLS_VERSION}</span>.
|
||||
</p>
|
||||
<p className={styles.Paragraph}>
|
||||
This requires bridge protocol{' '}
|
||||
<span className={styles.Version}>
|
||||
version {currentBridgeProtocol.version}
|
||||
</span>
|
||||
. However the current backend version uses bridge protocol{' '}
|
||||
<span className={styles.Version}>version {version}</span>.
|
||||
</p>
|
||||
{instructions}
|
||||
<p className={styles.Paragraph}>
|
||||
Or{' '}
|
||||
<a className={styles.Link} href={INSTRUCTIONS_FB_URL} target="_blank">
|
||||
click here
|
||||
</a>{' '}
|
||||
for more information.
|
||||
</p>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -69,6 +69,9 @@
|
||||
--light-color-expand-collapse-toggle: #777d88;
|
||||
--light-color-link: #0000ff;
|
||||
--light-color-modal-background: rgba(255, 255, 255, 0.75);
|
||||
--light-color-bridge-version-npm-background: #eff0f1;
|
||||
--light-color-bridge-version-npm-text: #000000;
|
||||
--light-color-bridge-version-number: #0088fa;
|
||||
--light-color-primitive-hook-badge-background: #e5e5e5;
|
||||
--light-color-primitive-hook-badge-text: #5f6673;
|
||||
--light-color-record-active: #fc3a4b;
|
||||
@@ -158,6 +161,9 @@
|
||||
--dark-color-expand-collapse-toggle: #8f949d;
|
||||
--dark-color-link: #61dafb;
|
||||
--dark-color-modal-background: rgba(0, 0, 0, 0.75);
|
||||
--dark-color-bridge-version-npm-background: rgba(0, 0, 0, 0.25);
|
||||
--dark-color-bridge-version-npm-text: #ffffff;
|
||||
--dark-color-bridge-version-number: yellow;
|
||||
--dark-color-primitive-hook-badge-background: rgba(0, 0, 0, 0.25);
|
||||
--dark-color-primitive-hook-badge-text: rgba(255, 255, 255, 0.7);
|
||||
--dark-color-record-active: #fc3a4b;
|
||||
|
||||
Vendored
+6
@@ -32,6 +32,12 @@ app.on('ready', function() {
|
||||
},
|
||||
});
|
||||
|
||||
// https://stackoverflow.com/questions/32402327/
|
||||
mainWindow.webContents.on('new-window', function(event, url) {
|
||||
event.preventDefault();
|
||||
require('electron').shell.openExternal(url);
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
mainWindow.loadURL('file://' + __dirname + '/app.html'); // eslint-disable-line no-path-concat
|
||||
mainWindow.webContents.executeJavaScript(
|
||||
|
||||
Reference in New Issue
Block a user