Skip to content

Commit 648546a

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 3a8c04e commit 648546a

File tree

10 files changed

+214
-3
lines changed

10 files changed

+214
-3
lines changed

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,10 @@ function initialize(socket: WebSocket) {
216216
socket.close();
217217
});
218218

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

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

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ export function initialize(
6767
},
6868
});
6969

70-
const store: Store = new Store(bridge, {supportsTraceUpdates: true});
70+
const store: Store = new Store(bridge, {
71+
checkBridgeProtocolCompatibility: true,
72+
supportsTraceUpdates: true,
73+
});
7174

7275
const ForwardRef = forwardRef<Props, mixed>((props, ref) => (
7376
<DevTools ref={ref} bridge={bridge} store={store} {...props} />

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

+15
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ 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+
2336
type ElementAndRendererID = {|id: number, rendererID: RendererID|};
2437

2538
type Message = {|
@@ -117,6 +130,7 @@ type UpdateConsolePatchSettingsParams = {|
117130
|};
118131

119132
type BackendEvents = {|
133+
bridgeProtocol: [BridgeProtocol],
120134
extensionBackendInitialized: [],
121135
inspectedElement: [InspectedElementPayload],
122136
isBackendStorageAPISupported: [boolean],
@@ -144,6 +158,7 @@ type FrontendEvents = {|
144158
clearNativeElementHighlight: [],
145159
copyElementPath: [CopyElementPathParams],
146160
deletePath: [DeletePath],
161+
getBridgeProtocol: [],
147162
getOwnersList: [ElementAndRendererID],
148163
getProfilingData: [{|rendererID: RendererID|}],
149164
getProfilingStatus: [],

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

+24-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import ProfilerStore from './ProfilerStore';
3030

3131
import type {Element} from './views/Components/types';
3232
import type {ComponentFilter, ElementType} from '../types';
33-
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
33+
import type {
34+
BridgeProtocol,
35+
FrontendBridge,
36+
} from 'react-devtools-shared/src/bridge';
3437

3538
const debug = (methodName, ...args) => {
3639
if (__DEBUG__) {
@@ -49,6 +52,7 @@ const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
4952
'React::DevTools::recordChangeDescriptions';
5053

5154
type Config = {|
55+
checkBridgeProtocolCompatibility?: boolean,
5256
isProfiling?: boolean,
5357
supportsNativeInspection?: boolean,
5458
supportsReloadAndProfile?: boolean,
@@ -74,6 +78,7 @@ export default class Store extends EventEmitter<{|
7478
supportsNativeStyleEditor: [],
7579
supportsProfiling: [],
7680
supportsReloadAndProfile: [],
81+
unsupportedBridgeProtocolDetected: [],
7782
unsupportedRendererVersionDetected: [],
7883
|}> {
7984
_bridge: FrontendBridge;
@@ -128,6 +133,7 @@ export default class Store extends EventEmitter<{|
128133
_supportsReloadAndProfile: boolean = false;
129134
_supportsTraceUpdates: boolean = false;
130135

136+
_unsupportedBridgeProtocol: BridgeProtocol | null = null;
131137
_unsupportedRendererVersionDetected: boolean = false;
132138

133139
// Total number of visible elements (within all roots).
@@ -194,6 +200,13 @@ export default class Store extends EventEmitter<{|
194200
);
195201

196202
this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
203+
204+
// Verify that the frontend version is compatible with the connected backend.
205+
// See github.com/facebook/react/issues/21326
206+
if (config != null && config.checkBridgeProtocolCompatibility) {
207+
bridge.addListener('bridgeProtocol', this.onBridgeProtocol);
208+
bridge.send('getBridgeProtocol');
209+
}
197210
}
198211

199212
// This is only used in tests to avoid memory leaks.
@@ -353,6 +366,10 @@ export default class Store extends EventEmitter<{|
353366
return this._supportsTraceUpdates;
354367
}
355368

369+
get unsupportedBridgeProtocol(): BridgeProtocol | null {
370+
return this._unsupportedBridgeProtocol;
371+
}
372+
356373
get unsupportedRendererVersionDetected(): boolean {
357374
return this._unsupportedRendererVersionDetected;
358375
}
@@ -1020,6 +1037,7 @@ export default class Store extends EventEmitter<{|
10201037
'isBackendStorageAPISupported',
10211038
this.onBridgeStorageSupported,
10221039
);
1040+
this._bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
10231041
};
10241042

10251043
onBridgeStorageSupported = (isBackendStorageAPISupported: boolean) => {
@@ -1033,4 +1051,9 @@ export default class Store extends EventEmitter<{|
10331051

10341052
this.emit('unsupportedRendererVersionDetected');
10351053
};
1054+
1055+
onBridgeProtocol = (bridgeProtocol: BridgeProtocol) => {
1056+
this._unsupportedBridgeProtocol = bridgeProtocol;
1057+
this.emit('unsupportedBridgeProtocolDetected');
1058+
};
10361059
}

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

+2
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

+7
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,13 @@ export function updateThemeVariables(
343343
updateStyleHelper(theme, 'color-expand-collapse-toggle', documentElements);
344344
updateStyleHelper(theme, 'color-link', documentElements);
345345
updateStyleHelper(theme, 'color-modal-background', documentElements);
346+
updateStyleHelper(
347+
theme,
348+
'color-bridge-version-npm-background',
349+
documentElements,
350+
);
351+
updateStyleHelper(theme, 'color-bridge-version-npm-text', documentElements);
352+
updateStyleHelper(theme, 'color-bridge-version-number', documentElements);
346353
updateStyleHelper(theme, 'color-record-active', documentElements);
347354
updateStyleHelper(theme, 'color-record-hover', documentElements);
348355
updateStyleHelper(theme, 'color-record-inactive', documentElements);
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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 Button from './Button';
16+
import ButtonIcon from './ButtonIcon';
17+
import {copy} from 'clipboard-js';
18+
import styles from './UnsupportedBridgeProtocolDialog.css';
19+
20+
import type {BridgeProtocol} from 'react-devtools-shared/src/bridge';
21+
22+
type DAILOG_STATE = 'dialog-not-shown' | 'show-dialog' | 'dialog-shown';
23+
24+
const DEVTOOLS_VERSION = process.env.DEVTOOLS_VERSION;
25+
const INSTRUCTIONS_FB_URL = 'https://fburl.com/devtools-bridge-protocol';
26+
27+
export default function UnsupportedBridgeProtocolDialog(_: {||}) {
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} = unsupportedBridgeProtocol;
69+
const upgradeInstructions = `npm i -g react-devtools@^${minNpmVersion}`;
70+
return (
71+
<Fragment>
72+
<div className={styles.Column}>
73+
<div className={styles.Title}>Unsupported DevTools backend version</div>
74+
<p className={styles.Paragraph}>
75+
You are running <code>react-devtools</code> version{' '}
76+
<span className={styles.Version}>{DEVTOOLS_VERSION}</span>.
77+
</p>
78+
<p className={styles.Paragraph}>
79+
This requires bridge protocol{' '}
80+
<span className={styles.Version}>version 0</span>. However the current
81+
backend version uses bridge protocol{' '}
82+
<span className={styles.Version}>version {version}</span>.
83+
</p>
84+
<p className={styles.Paragraph}>
85+
To fix this, upgrade the DevTools NPM package:
86+
</p>
87+
<pre className={styles.NpmCommand}>
88+
{upgradeInstructions}
89+
<Button
90+
onClick={() => copy(upgradeInstructions)}
91+
title="Copy upgrade command to clipboard">
92+
<ButtonIcon type="copy" />
93+
</Button>
94+
</pre>
95+
<p className={styles.Paragraph}>
96+
Or{' '}
97+
<a
98+
data-electron-external-link="true"
99+
className={styles.Link}
100+
href={INSTRUCTIONS_FB_URL}
101+
target="_blank">
102+
click here
103+
</a>{' '}
104+
for more information.
105+
</p>
106+
</div>
107+
</Fragment>
108+
);
109+
}

packages/react-devtools-shared/src/devtools/views/root.css

+6
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959
--light-color-expand-collapse-toggle: #777d88;
6060
--light-color-link: #0000ff;
6161
--light-color-modal-background: rgba(255, 255, 255, 0.75);
62+
--light-color-bridge-version-npm-background: #eff0f1;
63+
--light-color-bridge-version-npm-text: #000000;
64+
--light-color-bridge-version-number: #0088fa;
6265
--light-color-record-active: #fc3a4b;
6366
--light-color-record-hover: #3578e5;
6467
--light-color-record-inactive: #0088fa;
@@ -136,6 +139,9 @@
136139
--dark-color-expand-collapse-toggle: #8f949d;
137140
--dark-color-link: #61dafb;
138141
--dark-color-modal-background: rgba(0, 0, 0, 0.75);
142+
--dark-color-bridge-version-npm-background: rgba(0, 0, 0, 0.25);
143+
--dark-color-bridge-version-npm-text: #ffffff;
144+
--dark-color-bridge-version-number: yellow;
139145
--dark-color-record-active: #fc3a4b;
140146
--dark-color-record-hover: #a2e9fc;
141147
--dark-color-record-inactive: #61dafb;

packages/react-devtools/app.js

+6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ app.on('ready', function() {
3232
},
3333
});
3434

35+
// https://stackoverflow.com/questions/32402327/
36+
mainWindow.webContents.on('new-window', function(event, url) {
37+
event.preventDefault();
38+
require('electron').shell.openExternal(url);
39+
});
40+
3541
// and load the index.html of the app.
3642
mainWindow.loadURL('file://' + __dirname + '/app.html'); // eslint-disable-line no-path-concat
3743
mainWindow.webContents.executeJavaScript(

0 commit comments

Comments
 (0)