Skip to content

Commit 8ecc654

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 29faeb2 commit 8ecc654

File tree

11 files changed

+314
-3
lines changed

11 files changed

+314
-3
lines changed

packages/react-devtools-core/src/standalone.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ function initialize(socket: WebSocket) {
217217
socket.close();
218218
});
219219

220-
store = new Store(bridge, {supportsNativeInspection: false});
220+
store = new Store(bridge, {
221+
checkBridgeProtocolCompatibility: true,
222+
supportsNativeInspection: false,
223+
});
221224

222225
log('Connected');
223226
reload();

packages/react-devtools-inline/src/frontend.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
2121
import type {Props} from 'react-devtools-shared/src/devtools/views/DevTools';
2222

2323
export function createStore(bridge: FrontendBridge): Store {
24-
return new Store(bridge, {supportsTraceUpdates: true});
24+
return new Store(bridge, {
25+
checkBridgeProtocolCompatibility: true,
26+
supportsTraceUpdates: true,
27+
});
2528
}
2629

2730
export function createBridge(

packages/react-devtools-shared/src/backend/agent.js

Lines changed: 6 additions & 0 deletions
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 {
@@ -176,6 +177,7 @@ export default class Agent extends EventEmitter<{|
176177
bridge.addListener('clearWarningsForFiberID', this.clearWarningsForFiberID);
177178
bridge.addListener('copyElementPath', this.copyElementPath);
178179
bridge.addListener('deletePath', this.deletePath);
180+
bridge.addListener('getBridgeProtocol', this.getBridgeProtocol);
179181
bridge.addListener('getProfilingData', this.getProfilingData);
180182
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
181183
bridge.addListener('getOwnersList', this.getOwnersList);
@@ -308,6 +310,10 @@ export default class Agent extends EventEmitter<{|
308310
return null;
309311
}
310312

313+
getBridgeProtocol = () => {
314+
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
315+
};
316+
311317
getProfilingData = ({rendererID}: {|rendererID: RendererID|}) => {
312318
const renderer = this._rendererInterfaces[rendererID];
313319
if (renderer == null) {

packages/react-devtools-shared/src/bridge.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,49 @@ 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+
// This version technically never existed,
49+
// but a backwards breaking change was added in 4.11,
50+
// so the safest guess to downgrade the frontend would be to version 4.10.
51+
{
52+
version: 0,
53+
minNpmVersion: '<4.11.0',
54+
maxNpmVersion: '<4.11.0',
55+
},
56+
{
57+
version: 1,
58+
minNpmVersion: '4.13.0',
59+
maxNpmVersion: null,
60+
},
61+
];
62+
63+
export const currentBridgeProtocol: BridgeProtocol =
64+
BRIDGE_PROTOCOL[BRIDGE_PROTOCOL.length - 1];
65+
2366
type ElementAndRendererID = {|id: number, rendererID: RendererID|};
2467

2568
type Message = {|
@@ -119,6 +162,7 @@ type UpdateConsolePatchSettingsParams = {|
119162
|};
120163

121164
export type BackendEvents = {|
165+
bridgeProtocol: [BridgeProtocol],
122166
extensionBackendInitialized: [],
123167
inspectedElement: [InspectedElementPayload],
124168
isBackendStorageAPISupported: [boolean],
@@ -150,6 +194,7 @@ type FrontendEvents = {|
150194
clearWarningsForFiberID: [ElementAndRendererID],
151195
copyElementPath: [CopyElementPathParams],
152196
deletePath: [DeletePath],
197+
getBridgeProtocol: [],
153198
getOwnersList: [ElementAndRendererID],
154199
getProfilingData: [{|rendererID: RendererID|}],
155200
getProfilingStatus: [],

packages/react-devtools-shared/src/devtools/store.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,17 @@ import {localStorageGetItem, localStorageSetItem} from '../storage';
2929
import {__DEBUG__} from '../constants';
3030
import {printStore} from './utils';
3131
import ProfilerStore from './ProfilerStore';
32+
import {
33+
BRIDGE_PROTOCOL,
34+
currentBridgeProtocol,
35+
} from 'react-devtools-shared/src/bridge';
3236

3337
import type {Element} from './views/Components/types';
3438
import type {ComponentFilter, ElementType} from '../types';
35-
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
39+
import type {
40+
FrontendBridge,
41+
BridgeProtocol,
42+
} from 'react-devtools-shared/src/bridge';
3643

3744
const debug = (methodName, ...args) => {
3845
if (__DEBUG__) {
@@ -51,6 +58,7 @@ const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
5158
'React::DevTools::recordChangeDescriptions';
5259

5360
type Config = {|
61+
checkBridgeProtocolCompatibility?: boolean,
5462
isProfiling?: boolean,
5563
supportsNativeInspection?: boolean,
5664
supportsReloadAndProfile?: boolean,
@@ -76,6 +84,7 @@ export default class Store extends EventEmitter<{|
7684
supportsNativeStyleEditor: [],
7785
supportsProfiling: [],
7886
supportsReloadAndProfile: [],
87+
unsupportedBridgeProtocolDetected: [],
7988
unsupportedRendererVersionDetected: [],
8089
|}> {
8190
_bridge: FrontendBridge;
@@ -119,6 +128,10 @@ export default class Store extends EventEmitter<{|
119128

120129
_nativeStyleEditorValidAttributes: $ReadOnlyArray<string> | null = null;
121130

131+
// Older backends don't support an explicit bridge protocol,
132+
// so we should timeout eventually and show a downgrade message.
133+
_onBridgeProtocolTimeoutID: TimeoutID | null = null;
134+
122135
// Map of element (id) to the set of elements (ids) it owns.
123136
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
124137
_ownersMap: Map<number, Set<number>> = new Map();
@@ -147,6 +160,7 @@ export default class Store extends EventEmitter<{|
147160
_supportsReloadAndProfile: boolean = false;
148161
_supportsTraceUpdates: boolean = false;
149162

163+
_unsupportedBridgeProtocol: BridgeProtocol | null = null;
150164
_unsupportedRendererVersionDetected: boolean = false;
151165

152166
// Total number of visible elements (within all roots).
@@ -217,6 +231,20 @@ export default class Store extends EventEmitter<{|
217231
);
218232

219233
this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
234+
235+
// Verify that the frontend version is compatible with the connected backend.
236+
// See github.com/facebook/react/issues/21326
237+
if (config != null && config.checkBridgeProtocolCompatibility) {
238+
// Older backends don't support an explicit bridge protocol,
239+
// so we should timeout eventually and show a downgrade message.
240+
this._onBridgeProtocolTimeoutID = setTimeout(
241+
this.onBridgeProtocolTimeout,
242+
10000,
243+
);
244+
245+
bridge.addListener('bridgeProtocol', this.onBridgeProtocol);
246+
bridge.send('getBridgeProtocol');
247+
}
220248
}
221249

222250
// This is only used in tests to avoid memory leaks.
@@ -385,6 +413,10 @@ export default class Store extends EventEmitter<{|
385413
return this._supportsTraceUpdates;
386414
}
387415

416+
get unsupportedBridgeProtocol(): BridgeProtocol | null {
417+
return this._unsupportedBridgeProtocol;
418+
}
419+
388420
get unsupportedRendererVersionDetected(): boolean {
389421
return this._unsupportedRendererVersionDetected;
390422
}
@@ -1168,6 +1200,12 @@ export default class Store extends EventEmitter<{|
11681200
'unsupportedRendererVersion',
11691201
this.onBridgeUnsupportedRendererVersion,
11701202
);
1203+
bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
1204+
1205+
if (this._onBridgeProtocolTimeoutID !== null) {
1206+
clearTimeout(this._onBridgeProtocolTimeoutID);
1207+
this._onBridgeProtocolTimeoutID = null;
1208+
}
11711209
};
11721210

11731211
onBackendStorageAPISupported = (isBackendStorageAPISupported: boolean) => {
@@ -1187,4 +1225,30 @@ export default class Store extends EventEmitter<{|
11871225

11881226
this.emit('unsupportedRendererVersionDetected');
11891227
};
1228+
1229+
onBridgeProtocol = (bridgeProtocol: BridgeProtocol) => {
1230+
if (this._onBridgeProtocolTimeoutID !== null) {
1231+
clearTimeout(this._onBridgeProtocolTimeoutID);
1232+
this._onBridgeProtocolTimeoutID = null;
1233+
}
1234+
1235+
if (bridgeProtocol.version !== currentBridgeProtocol.version) {
1236+
this._unsupportedBridgeProtocol = bridgeProtocol;
1237+
} else {
1238+
// If we should happen to get a response after timing out...
1239+
this._unsupportedBridgeProtocol = null;
1240+
}
1241+
1242+
this.emit('unsupportedBridgeProtocolDetected');
1243+
};
1244+
1245+
onBridgeProtocolTimeout = () => {
1246+
this._onBridgeProtocolTimeoutID = null;
1247+
1248+
// If we timed out, that indicates the backend predates the bridge protocol,
1249+
// so we can set a fake version (0) to trigger the downgrade message.
1250+
this._unsupportedBridgeProtocol = BRIDGE_PROTOCOL[0];
1251+
1252+
this.emit('unsupportedBridgeProtocolDetected');
1253+
};
11901254
}

packages/react-devtools-shared/src/devtools/views/DevTools.js

Lines changed: 2 additions & 0 deletions
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 UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog';
2829
import UnsupportedVersionDialog from './UnsupportedVersionDialog';
2930
import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
3031
import {useLocalStorage} from './hooks';
@@ -226,6 +227,7 @@ export default function DevTools({
226227
</TreeContextController>
227228
</ViewElementSourceContext.Provider>
228229
</SettingsContextController>
230+
<UnsupportedBridgeProtocolDialog />
229231
{warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
230232
{warnIfUnsupportedVersionDetected && <UnsupportedVersionDialog />}
231233
</ModalDialogContextController>

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

Lines changed: 7 additions & 0 deletions
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',
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
.Column {
2+
display: flex;
3+
flex-direction: column;
4+
}
5+
6+
.Title {
7+
font-size: var(--font-size-sans-large);
8+
margin-bottom: 0.5rem;
9+
}
10+
11+
.ReleaseNotesLink {
12+
color: var(--color-button-active);
13+
}
14+
15+
.Version {
16+
color: var(--color-bridge-version-number);
17+
font-weight: bold;
18+
}
19+
20+
.NpmCommand {
21+
display: flex;
22+
justify-content: space-between;
23+
padding: 0.25rem 0.25rem 0.25rem 0.5rem;
24+
background-color: var(--color-bridge-version-npm-background);
25+
color: var(--color-bridge-version-npm-text);
26+
margin: 0;
27+
font-family: var(--font-family-monospace);
28+
font-size: var(--font-size-monospace-large);
29+
}
30+
31+
.Paragraph {
32+
margin: 0.5rem 0;
33+
}
34+
35+
.Link {
36+
color: var(--color-link);
37+
}

0 commit comments

Comments
 (0)