Skip to content

Commit 40f32c7

Browse files
author
Brian Vaughn
committed
DevTools: Add Bridge protocol version backend/frontend
Frontend shows upgrade or downgrade instructions if the version does not match.
1 parent a155860 commit 40f32c7

File tree

10 files changed

+259
-1
lines changed

10 files changed

+259
-1
lines changed

Diff for: packages/react-devtools-core/src/standalone.js

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ function reload() {
101101
canViewElementSourceFunction,
102102
showTabBar: true,
103103
store: ((store: any): Store),
104+
warnIfUnsupportedBridgeProtocolDetected: true,
104105
warnIfLegacyBackendDetected: true,
105106
viewElementSourceFunction,
106107
}),

Diff for: packages/react-devtools-shared/src/backend/agent.js

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
toggleEnabled as setTraceUpdatesEnabled,
2727
} from './views/TraceUpdates';
2828
import {patch as patchConsole, unpatch as unpatchConsole} from './console';
29+
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
2930

3031
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
3132
import type {
@@ -178,6 +179,7 @@ export default class Agent extends EventEmitter<{|
178179
bridge.addListener('deletePath', this.deletePath);
179180
bridge.addListener('getProfilingData', this.getProfilingData);
180181
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
182+
bridge.addListener('getBridgeProtocol', this.getBridgeProtocol);
181183
bridge.addListener('getOwnersList', this.getOwnersList);
182184
bridge.addListener('inspectElement', this.inspectElement);
183185
bridge.addListener('logElementToConsole', this.logElementToConsole);
@@ -321,6 +323,10 @@ export default class Agent extends EventEmitter<{|
321323
this._bridge.send('profilingStatus', this._isProfiling);
322324
};
323325

326+
getBridgeProtocol = () => {
327+
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
328+
};
329+
324330
getOwnersList = ({id, rendererID}: ElementAndRendererID) => {
325331
const renderer = this._rendererInterfaces[rendererID];
326332
if (renderer == null) {

Diff for: packages/react-devtools-shared/src/bridge.js

+37
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,41 @@ import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-share
2020

2121
const BATCH_DURATION = 100;
2222

23+
// This message specifies the version of the DevTools protocol currently supported by the backend,
24+
// as well as the earliest NPM version (e.g. "4.13.0") that protocol is supported by on the frontend.
25+
// This enables an older frontend to display an upgrade message to users for a newer, unsupported backend.
26+
export type BridgeProtocol = {|
27+
// Version supported by the current frontend/backend.
28+
version: number,
29+
30+
// NPM version range that also supports this version.
31+
// Note that 'maxNpmVersion' is only set when the version is bumped.
32+
minNpmVersion: string,
33+
maxNpmVersion: string | null,
34+
|};
35+
36+
// Bump protocol version whenever a backwards breaking change is made
37+
// in the messages sent between BackendBridge and FrontendBridge.
38+
// This mapping is embedded in both frontend and backend builds.
39+
//
40+
// The backend protocol will always be the latest entry in the BRIDGE_PROTOCOL array.
41+
//
42+
// When an older frontend connects to a newer backend,
43+
// the backend can send the minNpmVersion and the frontend can display an NPM upgrade prompt.
44+
//
45+
// When a newer frontend connects with an older protocol version,
46+
// the frontend can use the embedded minNpmVersion/maxNpmVersion values to display a downgrade prompt.
47+
export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
48+
{
49+
version: 1,
50+
minNpmVersion: '4.11.0',
51+
maxNpmVersion: null,
52+
},
53+
];
54+
55+
export const currentBridgeProtocol: BridgeProtocol =
56+
BRIDGE_PROTOCOL[BRIDGE_PROTOCOL.length - 1];
57+
2358
type ElementAndRendererID = {|id: number, rendererID: RendererID|};
2459

2560
type Message = {|
@@ -128,6 +163,7 @@ export type BackendEvents = {|
128163
overrideComponentFilters: [Array<ComponentFilter>],
129164
profilingData: [ProfilingDataBackend],
130165
profilingStatus: [boolean],
166+
bridgeProtocol: [BridgeProtocol],
131167
reloadAppForProfiling: [],
132168
selectFiber: [number],
133169
shutdown: [],
@@ -153,6 +189,7 @@ type FrontendEvents = {|
153189
getOwnersList: [ElementAndRendererID],
154190
getProfilingData: [{|rendererID: RendererID|}],
155191
getProfilingStatus: [],
192+
getBridgeProtocol: [],
156193
highlightNativeElement: [HighlightElementInDOM],
157194
inspectElement: [InspectElementParams],
158195
logElementToConsole: [ElementAndRendererID],

Diff for: packages/react-devtools-shared/src/devtools/store.js

+25-1
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@ import {localStorageGetItem, localStorageSetItem} from '../storage';
2929
import {__DEBUG__} from '../constants';
3030
import {printStore} from './utils';
3131
import ProfilerStore from './ProfilerStore';
32+
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
3233

3334
import type {Element} from './views/Components/types';
3435
import type {ComponentFilter, ElementType} from '../types';
35-
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
36+
import type {
37+
FrontendBridge,
38+
BridgeProtocol,
39+
} from 'react-devtools-shared/src/bridge';
3640

3741
const debug = (methodName, ...args) => {
3842
if (__DEBUG__) {
@@ -76,6 +80,8 @@ export default class Store extends EventEmitter<{|
7680
supportsNativeStyleEditor: [],
7781
supportsProfiling: [],
7882
supportsReloadAndProfile: [],
83+
unsupportedBridgeProtocolDetected: [],
84+
unsupportedRendererVersionDetected: [],
7985
unsupportedRendererVersionDetected: [],
8086
|}> {
8187
_bridge: FrontendBridge;
@@ -147,6 +153,7 @@ export default class Store extends EventEmitter<{|
147153
_supportsReloadAndProfile: boolean = false;
148154
_supportsTraceUpdates: boolean = false;
149155

156+
_unsupportedBridgeProtocol: BridgeProtocol | null = null;
150157
_unsupportedRendererVersionDetected: boolean = false;
151158

152159
// Total number of visible elements (within all roots).
@@ -215,8 +222,13 @@ export default class Store extends EventEmitter<{|
215222
'unsupportedRendererVersion',
216223
this.onBridgeUnsupportedRendererVersion,
217224
);
225+
bridge.addListener('bridgeProtocol', this.onBridgeProtocol);
218226

219227
this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
228+
229+
// Verify that the frontend version is compatible with the connected backend.
230+
// See github.com/facebook/react/issues/21326
231+
bridge.send('getBridgeProtocol');
220232
}
221233

222234
// This is only used in tests to avoid memory leaks.
@@ -385,6 +397,10 @@ export default class Store extends EventEmitter<{|
385397
return this._supportsTraceUpdates;
386398
}
387399

400+
get unsupportedBridgeProtocol(): BridgeProtocol | null {
401+
return this._unsupportedBridgeProtocol;
402+
}
403+
388404
get unsupportedRendererVersionDetected(): boolean {
389405
return this._unsupportedRendererVersionDetected;
390406
}
@@ -1187,4 +1203,12 @@ export default class Store extends EventEmitter<{|
11871203

11881204
this.emit('unsupportedRendererVersionDetected');
11891205
};
1206+
1207+
onBridgeProtocol = (bridgeProtocol: BridgeProtocol) => {
1208+
if (bridgeProtocol.version !== currentBridgeProtocol.version) {
1209+
this._unsupportedBridgeProtocol = bridgeProtocol;
1210+
1211+
this.emit('unsupportedBridgeProtocolDetected');
1212+
}
1213+
};
11901214
}

Diff for: packages/react-devtools-shared/src/devtools/views/DevTools.js

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import ViewElementSourceContext from './Components/ViewElementSourceContext';
2525
import {ProfilerContextController} from './Profiler/ProfilerContext';
2626
import {ModalDialogContextController} from './ModalDialog';
2727
import ReactLogo from './ReactLogo';
28+
import UnsupportedProtocolDialog from './UnsupportedProtocolDialog';
2829
import UnsupportedVersionDialog from './UnsupportedVersionDialog';
2930
import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
3031
import {useLocalStorage} from './hooks';
@@ -59,6 +60,7 @@ export type Props = {|
5960
showTabBar?: boolean,
6061
store: Store,
6162
warnIfLegacyBackendDetected?: boolean,
63+
warnIfUnsupportedBridgeProtocolDetected?: boolean,
6264
warnIfUnsupportedVersionDetected?: boolean,
6365
viewAttributeSourceFunction?: ?ViewAttributeSource,
6466
viewElementSourceFunction?: ?ViewElementSource,
@@ -102,6 +104,7 @@ export default function DevTools({
102104
profilerPortalContainer,
103105
showTabBar = false,
104106
store,
107+
warnIfUnsupportedBridgeProtocolDetected = false,
105108
warnIfLegacyBackendDetected = false,
106109
warnIfUnsupportedVersionDetected = false,
107110
viewAttributeSourceFunction,
@@ -226,6 +229,9 @@ export default function DevTools({
226229
</TreeContextController>
227230
</ViewElementSourceContext.Provider>
228231
</SettingsContextController>
232+
{warnIfUnsupportedBridgeProtocolDetected && (
233+
<UnsupportedProtocolDialog />
234+
)}
229235
{warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
230236
{warnIfUnsupportedVersionDetected && <UnsupportedVersionDialog />}
231237
</ModalDialogContextController>

Diff for: packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js

+7
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,13 @@ export function updateThemeVariables(
383383
updateStyleHelper(theme, 'color-expand-collapse-toggle', documentElements);
384384
updateStyleHelper(theme, 'color-link', documentElements);
385385
updateStyleHelper(theme, 'color-modal-background', documentElements);
386+
updateStyleHelper(
387+
theme,
388+
'color-bridge-version-npm-background',
389+
documentElements,
390+
);
391+
updateStyleHelper(theme, 'color-bridge-version-npm-text', documentElements);
392+
updateStyleHelper(theme, 'color-bridge-version-number', documentElements);
386393
updateStyleHelper(
387394
theme,
388395
'color-primitive-hook-badge-background',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.Row {
2+
display: flex;
3+
flex-direction: row;
4+
align-items: center;
5+
}
6+
7+
.Column {
8+
display: flex;
9+
flex-direction: column;
10+
align-items: center;
11+
}
12+
13+
.Title {
14+
font-size: var(--font-size-sans-large);
15+
margin-bottom: 0.5rem;
16+
}
17+
18+
.ReleaseNotesLink {
19+
color: var(--color-button-active);
20+
}
21+
22+
.Version {
23+
color: var(--color-bridge-version-number);
24+
font-weight: bold;
25+
}
26+
27+
.NpmCommand {
28+
display: flex;
29+
justify-content: space-between;
30+
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
31+
background-color: var(--color-bridge-version-npm-background);
32+
color: var(--color-bridge-version-npm-text);
33+
margin-bottom: 0;
34+
font-family: var(--font-family-monospace);
35+
font-size: var(--font-size-monospace-large);
36+
}
37+
38+
.Instructions {
39+
margin-bottom: 0;
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import * as React from 'react';
11+
import {Fragment, useContext, useEffect, useState} from 'react';
12+
import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
13+
import {ModalDialogContext} from './ModalDialog';
14+
import {StoreContext} from './context';
15+
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
16+
import Button from './Button';
17+
import ButtonIcon from './ButtonIcon';
18+
import {copy} from 'clipboard-js';
19+
import styles from './UnsupportedProtocolDialog.css';
20+
21+
import type {BridgeProtocol} from 'react-devtools-shared/src/bridge';
22+
23+
type DAILOG_STATE = 'dialog-not-shown' | 'show-dialog' | 'dialog-shown';
24+
25+
const DEVTOOLS_VERSION = process.env.DEVTOOLS_VERSION;
26+
27+
export default function UnsupportedProtocolDialog(_: {||}) {
28+
const {dispatch} = useContext(ModalDialogContext);
29+
const store = useContext(StoreContext);
30+
const [state, setState] = useState<DAILOG_STATE>('dialog-not-shown');
31+
32+
useEffect(() => {
33+
if (state === 'dialog-not-shown') {
34+
const showDialog = () => {
35+
batchedUpdates(() => {
36+
setState('show-dialog');
37+
dispatch({
38+
canBeDismissed: false,
39+
type: 'SHOW',
40+
content: (
41+
<DialogContent
42+
unsupportedBridgeProtocol={store.unsupportedBridgeProtocol}
43+
/>
44+
),
45+
});
46+
});
47+
};
48+
49+
if (store.unsupportedBridgeProtocol !== null) {
50+
showDialog();
51+
} else {
52+
store.addListener('unsupportedBridgeProtocolDetected', showDialog);
53+
return () => {
54+
store.removeListener('unsupportedBridgeProtocolDetected', showDialog);
55+
};
56+
}
57+
}
58+
}, [state, store]);
59+
60+
return null;
61+
}
62+
63+
function DialogContent({
64+
unsupportedBridgeProtocol,
65+
}: {|
66+
unsupportedBridgeProtocol: BridgeProtocol,
67+
|}) {
68+
const {version, minNpmVersion, maxNpmVersion} = unsupportedBridgeProtocol;
69+
70+
let instructions;
71+
if (maxNpmVersion === null) {
72+
const upgradeInstructions = `npm i -g react-devtools@^${minNpmVersion}`;
73+
instructions = (
74+
<p className={styles.Instructions}>
75+
To fix this, upgrade the DevTools NPM package:
76+
<pre className={styles.NpmCommand}>
77+
{upgradeInstructions}
78+
<Button
79+
onClick={() => copy(upgradeInstructions)}
80+
title="Copy upgrade command to clipboard">
81+
<ButtonIcon type="copy" />
82+
</Button>
83+
</pre>
84+
</p>
85+
);
86+
} else {
87+
const downgradeInstructions = `npm i -g react-devtools@${maxNpmVersion}`;
88+
instructions = (
89+
<p className={styles.Instructions}>
90+
To fix this, downgrade the DevTools NPM package:
91+
<pre className={styles.NpmCommand}>
92+
{downgradeInstructions}
93+
<Button
94+
onClick={() => copy(downgradeInstructions)}
95+
title="Copy downgrade command to clipboard">
96+
<ButtonIcon type="copy" />
97+
</Button>
98+
</pre>
99+
</p>
100+
);
101+
}
102+
103+
return (
104+
<Fragment>
105+
<div className={styles.Row}>
106+
<div>
107+
<div className={styles.Title}>
108+
Unsupported DevTools backend version
109+
</div>
110+
<p>
111+
You are running <code>react-devtools</code> version{' '}
112+
<span className={styles.Version}>{DEVTOOLS_VERSION}</span>.
113+
</p>
114+
<p>
115+
This requires bridge protocol{' '}
116+
<span className={styles.Version}>
117+
version {currentBridgeProtocol.version}
118+
</span>
119+
. However the current backend version uses bridge protocol{' '}
120+
<span className={styles.Version}>version {version}</span>.
121+
</p>
122+
{instructions}
123+
</div>
124+
</div>
125+
</Fragment>
126+
);
127+
}

0 commit comments

Comments
 (0)