From 9ee44cad291de8471384427eec12d6ba8f0bee86 Mon Sep 17 00:00:00 2001
From: Brian Vaughn
Date: Wed, 21 Apr 2021 23:18:53 -0400
Subject: [PATCH] DevTools: Add Bridge protocol version backend/frontend
Frontend shows upgrade or downgrade instructions if the version does not match.
---
.../react-devtools-core/src/standalone.js | 1 +
.../src/backend/agent.js | 6 +
packages/react-devtools-shared/src/bridge.js | 37 +++++
.../src/devtools/store.js | 26 +++-
.../src/devtools/views/DevTools.js | 6 +
.../views/Settings/SettingsContext.js | 7 +
.../views/UnsupportedProtocolDialog.css | 40 ++++++
.../views/UnsupportedProtocolDialog.js | 127 ++++++++++++++++++
.../src/devtools/views/root.css | 9 ++
packages/react-devtools-shell/src/devtools.js | 1 +
10 files changed, 259 insertions(+), 1 deletion(-)
create mode 100644 packages/react-devtools-shared/src/devtools/views/UnsupportedProtocolDialog.css
create mode 100644 packages/react-devtools-shared/src/devtools/views/UnsupportedProtocolDialog.js
diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js
index e31b43d724d1b..929b32f3760bc 100644
--- a/packages/react-devtools-core/src/standalone.js
+++ b/packages/react-devtools-core/src/standalone.js
@@ -101,6 +101,7 @@ function reload() {
canViewElementSourceFunction,
showTabBar: true,
store: ((store: any): Store),
+ warnIfUnsupportedBridgeProtocolDetected: true,
warnIfLegacyBackendDetected: true,
viewElementSourceFunction,
}),
diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js
index 86c35ae1b49bd..3515416aecdd7 100644
--- a/packages/react-devtools-shared/src/backend/agent.js
+++ b/packages/react-devtools-shared/src/backend/agent.js
@@ -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 {
@@ -178,6 +179,7 @@ export default class Agent extends EventEmitter<{|
bridge.addListener('deletePath', this.deletePath);
bridge.addListener('getProfilingData', this.getProfilingData);
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
+ bridge.addListener('getBridgeProtocol', this.getBridgeProtocol);
bridge.addListener('getOwnersList', this.getOwnersList);
bridge.addListener('inspectElement', this.inspectElement);
bridge.addListener('logElementToConsole', this.logElementToConsole);
@@ -321,6 +323,10 @@ export default class Agent extends EventEmitter<{|
this._bridge.send('profilingStatus', this._isProfiling);
};
+ getBridgeProtocol = () => {
+ this._bridge.send('bridgeProtocol', currentBridgeProtocol);
+ };
+
getOwnersList = ({id, rendererID}: ElementAndRendererID) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js
index d3b669bd42871..6929187a0e733 100644
--- a/packages/react-devtools-shared/src/bridge.js
+++ b/packages/react-devtools-shared/src/bridge.js
@@ -20,6 +20,41 @@ 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 = [
+ {
+ version: 1,
+ minNpmVersion: '4.11.0',
+ maxNpmVersion: null,
+ },
+];
+
+export const currentBridgeProtocol: BridgeProtocol =
+ BRIDGE_PROTOCOL[BRIDGE_PROTOCOL.length - 1];
+
type ElementAndRendererID = {|id: number, rendererID: RendererID|};
type Message = {|
@@ -128,6 +163,7 @@ export type BackendEvents = {|
overrideComponentFilters: [Array],
profilingData: [ProfilingDataBackend],
profilingStatus: [boolean],
+ bridgeProtocol: [BridgeProtocol],
reloadAppForProfiling: [],
selectFiber: [number],
shutdown: [],
@@ -153,6 +189,7 @@ type FrontendEvents = {|
getOwnersList: [ElementAndRendererID],
getProfilingData: [{|rendererID: RendererID|}],
getProfilingStatus: [],
+ getBridgeProtocol: [],
highlightNativeElement: [HighlightElementInDOM],
inspectElement: [InspectElementParams],
logElementToConsole: [ElementAndRendererID],
diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js
index 5fa4bff344da9..8ae8460d5bfc7 100644
--- a/packages/react-devtools-shared/src/devtools/store.js
+++ b/packages/react-devtools-shared/src/devtools/store.js
@@ -29,10 +29,14 @@ import {localStorageGetItem, localStorageSetItem} from '../storage';
import {__DEBUG__} from '../constants';
import {printStore} from './utils';
import ProfilerStore from './ProfilerStore';
+import {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__) {
@@ -76,6 +80,8 @@ export default class Store extends EventEmitter<{|
supportsNativeStyleEditor: [],
supportsProfiling: [],
supportsReloadAndProfile: [],
+ unsupportedBridgeProtocolDetected: [],
+ unsupportedRendererVersionDetected: [],
unsupportedRendererVersionDetected: [],
|}> {
_bridge: FrontendBridge;
@@ -147,6 +153,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).
@@ -215,8 +222,13 @@ export default class Store extends EventEmitter<{|
'unsupportedRendererVersion',
this.onBridgeUnsupportedRendererVersion,
);
+ bridge.addListener('bridgeProtocol', this.onBridgeProtocol);
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
+ bridge.send('getBridgeProtocol');
}
// This is only used in tests to avoid memory leaks.
@@ -385,6 +397,10 @@ export default class Store extends EventEmitter<{|
return this._supportsTraceUpdates;
}
+ get unsupportedBridgeProtocol(): BridgeProtocol | null {
+ return this._unsupportedBridgeProtocol;
+ }
+
get unsupportedRendererVersionDetected(): boolean {
return this._unsupportedRendererVersionDetected;
}
@@ -1187,4 +1203,12 @@ export default class Store extends EventEmitter<{|
this.emit('unsupportedRendererVersionDetected');
};
+
+ onBridgeProtocol = (bridgeProtocol: BridgeProtocol) => {
+ if (bridgeProtocol.version !== currentBridgeProtocol.version) {
+ this._unsupportedBridgeProtocol = bridgeProtocol;
+
+ this.emit('unsupportedBridgeProtocolDetected');
+ }
+ };
}
diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js
index cb5c36cf680bc..ac98d51166728 100644
--- a/packages/react-devtools-shared/src/devtools/views/DevTools.js
+++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js
@@ -25,6 +25,7 @@ import ViewElementSourceContext from './Components/ViewElementSourceContext';
import {ProfilerContextController} from './Profiler/ProfilerContext';
import {ModalDialogContextController} from './ModalDialog';
import ReactLogo from './ReactLogo';
+import UnsupportedProtocolDialog from './UnsupportedProtocolDialog';
import UnsupportedVersionDialog from './UnsupportedVersionDialog';
import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
import {useLocalStorage} from './hooks';
@@ -59,6 +60,7 @@ export type Props = {|
showTabBar?: boolean,
store: Store,
warnIfLegacyBackendDetected?: boolean,
+ warnIfUnsupportedBridgeProtocolDetected?: boolean,
warnIfUnsupportedVersionDetected?: boolean,
viewAttributeSourceFunction?: ?ViewAttributeSource,
viewElementSourceFunction?: ?ViewElementSource,
@@ -102,6 +104,7 @@ export default function DevTools({
profilerPortalContainer,
showTabBar = false,
store,
+ warnIfUnsupportedBridgeProtocolDetected = false,
warnIfLegacyBackendDetected = false,
warnIfUnsupportedVersionDetected = false,
viewAttributeSourceFunction,
@@ -226,6 +229,9 @@ export default function DevTools({
+ {warnIfUnsupportedBridgeProtocolDetected && (
+
+ )}
{warnIfLegacyBackendDetected && }
{warnIfUnsupportedVersionDetected && }
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
index b6cd278df59dc..93d7bfd9d500d 100644
--- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
@@ -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',
diff --git a/packages/react-devtools-shared/src/devtools/views/UnsupportedProtocolDialog.css b/packages/react-devtools-shared/src/devtools/views/UnsupportedProtocolDialog.css
new file mode 100644
index 0000000000000..c2b9cc5fe4d29
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/UnsupportedProtocolDialog.css
@@ -0,0 +1,40 @@
+.Row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.Column {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.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-bottom: 0;
+ font-family: var(--font-family-monospace);
+ font-size: var(--font-size-monospace-large);
+}
+
+.Instructions {
+ margin-bottom: 0;
+}
\ No newline at end of file
diff --git a/packages/react-devtools-shared/src/devtools/views/UnsupportedProtocolDialog.js b/packages/react-devtools-shared/src/devtools/views/UnsupportedProtocolDialog.js
new file mode 100644
index 0000000000000..afcc336a8bcb8
--- /dev/null
+++ b/packages/react-devtools-shared/src/devtools/views/UnsupportedProtocolDialog.js
@@ -0,0 +1,127 @@
+/**
+ * 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, useState} from 'react';
+import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
+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 './UnsupportedProtocolDialog.css';
+
+import type {BridgeProtocol} from 'react-devtools-shared/src/bridge';
+
+type DAILOG_STATE = 'dialog-not-shown' | 'show-dialog' | 'dialog-shown';
+
+const DEVTOOLS_VERSION = process.env.DEVTOOLS_VERSION;
+
+export default function UnsupportedProtocolDialog(_: {||}) {
+ const {dispatch} = useContext(ModalDialogContext);
+ const store = useContext(StoreContext);
+ const [state, setState] = useState('dialog-not-shown');
+
+ useEffect(() => {
+ if (state === 'dialog-not-shown') {
+ const showDialog = () => {
+ batchedUpdates(() => {
+ setState('show-dialog');
+ dispatch({
+ canBeDismissed: false,
+ type: 'SHOW',
+ content: (
+
+ ),
+ });
+ });
+ };
+
+ if (store.unsupportedBridgeProtocol !== null) {
+ showDialog();
+ } else {
+ store.addListener('unsupportedBridgeProtocolDetected', showDialog);
+ return () => {
+ store.removeListener('unsupportedBridgeProtocolDetected', showDialog);
+ };
+ }
+ }
+ }, [state, 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 = (
+
+ To fix this, upgrade the DevTools NPM package:
+
+ {upgradeInstructions}
+ copy(upgradeInstructions)}
+ title="Copy upgrade command to clipboard">
+
+
+
+
+ );
+ } else {
+ const downgradeInstructions = `npm i -g react-devtools@${maxNpmVersion}`;
+ instructions = (
+
+ To fix this, downgrade the DevTools NPM package:
+
+ {downgradeInstructions}
+ copy(downgradeInstructions)}
+ title="Copy downgrade command to clipboard">
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Unsupported DevTools backend version
+
+
+ You are running react-devtools
version{' '}
+ {DEVTOOLS_VERSION} .
+
+
+ This requires bridge protocol{' '}
+
+ version {currentBridgeProtocol.version}
+
+ . However the current backend version uses bridge protocol{' '}
+ version {version} .
+
+ {instructions}
+
+
+
+ );
+}
diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css
index e703e011843e5..e3b9661c149ef 100644
--- a/packages/react-devtools-shared/src/devtools/views/root.css
+++ b/packages/react-devtools-shared/src/devtools/views/root.css
@@ -69,6 +69,12 @@
--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: rgba(255, 255, 255, 0.25);
+ --light-color-bridge-version-npm-text: #ffffff;
+ --light-color-bridge-version-number: yellow;
+ --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;
--light-color-primitive-hook-badge-background: #e5e5e5;
--light-color-primitive-hook-badge-text: #5f6673;
--light-color-record-active: #fc3a4b;
@@ -158,6 +164,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;
diff --git a/packages/react-devtools-shell/src/devtools.js b/packages/react-devtools-shell/src/devtools.js
index d3fe565be28a3..4f0ea24424591 100644
--- a/packages/react-devtools-shell/src/devtools.js
+++ b/packages/react-devtools-shell/src/devtools.js
@@ -57,6 +57,7 @@ inject('dist/app.js', () => {
browserTheme: 'light',
enabledInspectedElementContextMenu: true,
showTabBar: true,
+ warnIfUnsupportedBridgeProtocolDetected: true,
warnIfLegacyBackendDetected: true,
warnIfUnsupportedVersionDetected: true,
}),