Skip to content

Commit

Permalink
DevTools: Add Bridge protocol version backend/frontend
Browse files Browse the repository at this point in the history
Frontend shows upgrade or downgrade instructions if the version does not match.
  • Loading branch information
Brian Vaughn committed Apr 24, 2021
1 parent 3a8c04e commit e8d6f06
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 3 deletions.
5 changes: 4 additions & 1 deletion packages/react-devtools-core/src/standalone.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ function initialize(socket: WebSocket) {
socket.close();
});

store = new Store(bridge, {supportsNativeInspection: false});
store = new Store(bridge, {
checkBridgeProtocolCompatibility: true,
supportsNativeInspection: false,
});

log('Connected');
reload();
Expand Down
5 changes: 4 additions & 1 deletion packages/react-devtools-inline/src/frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ export function initialize(
},
});

const store: Store = new Store(bridge, {supportsTraceUpdates: true});
const store: Store = new Store(bridge, {
checkBridgeProtocolCompatibility: true,
supportsTraceUpdates: true,
});

const ForwardRef = forwardRef<Props, mixed>((props, ref) => (
<DevTools ref={ref} bridge={bridge} store={store} {...props} />
Expand Down
15 changes: 15 additions & 0 deletions packages/react-devtools-shared/src/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ 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,
|};

type ElementAndRendererID = {|id: number, rendererID: RendererID|};

type Message = {|
Expand Down Expand Up @@ -117,6 +130,7 @@ type UpdateConsolePatchSettingsParams = {|
|};

type BackendEvents = {|
bridgeProtocol: [BridgeProtocol],
extensionBackendInitialized: [],
inspectedElement: [InspectedElementPayload],
isBackendStorageAPISupported: [boolean],
Expand Down Expand Up @@ -144,6 +158,7 @@ type FrontendEvents = {|
clearNativeElementHighlight: [],
copyElementPath: [CopyElementPathParams],
deletePath: [DeletePath],
getBridgeProtocol: [],
getOwnersList: [ElementAndRendererID],
getProfilingData: [{|rendererID: RendererID|}],
getProfilingStatus: [],
Expand Down
25 changes: 24 additions & 1 deletion packages/react-devtools-shared/src/devtools/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import ProfilerStore from './ProfilerStore';

import type {Element} from './views/Components/types';
import type {ComponentFilter, ElementType} from '../types';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {
BridgeProtocol,
FrontendBridge,
} from 'react-devtools-shared/src/bridge';

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

type Config = {|
checkBridgeProtocolCompatibility?: boolean,
isProfiling?: boolean,
supportsNativeInspection?: boolean,
supportsReloadAndProfile?: boolean,
Expand All @@ -74,6 +78,7 @@ export default class Store extends EventEmitter<{|
supportsNativeStyleEditor: [],
supportsProfiling: [],
supportsReloadAndProfile: [],
unsupportedBridgeProtocolDetected: [],
unsupportedRendererVersionDetected: [],
|}> {
_bridge: FrontendBridge;
Expand Down Expand Up @@ -128,6 +133,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).
Expand Down Expand Up @@ -194,6 +200,13 @@ 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) {
bridge.addListener('bridgeProtocol', this.onBridgeProtocol);
bridge.send('getBridgeProtocol');
}
}

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

get unsupportedBridgeProtocol(): BridgeProtocol | null {
return this._unsupportedBridgeProtocol;
}

get unsupportedRendererVersionDetected(): boolean {
return this._unsupportedRendererVersionDetected;
}
Expand Down Expand Up @@ -1020,6 +1037,7 @@ export default class Store extends EventEmitter<{|
'isBackendStorageAPISupported',
this.onBridgeStorageSupported,
);
this._bridge.removeListener('bridgeProtocol', this.onBridgeProtocol);
};

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

this.emit('unsupportedRendererVersionDetected');
};

onBridgeProtocol = (bridgeProtocol: BridgeProtocol) => {
this._unsupportedBridgeProtocol = bridgeProtocol;
this.emit('unsupportedBridgeProtocolDetected');
};
}
2 changes: 2 additions & 0 deletions packages/react-devtools-shared/src/devtools/views/DevTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -226,6 +227,7 @@ export default function DevTools({
</TreeContextController>
</ViewElementSourceContext.Provider>
</SettingsContextController>
<UnsupportedBridgeProtocolDialog />
{warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
{warnIfUnsupportedVersionDetected && <UnsupportedVersionDialog />}
</ModalDialogContextController>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,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-record-active', documentElements);
updateStyleHelper(theme, 'color-record-hover', documentElements);
updateStyleHelper(theme, 'color-record-inactive', documentElements);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* 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 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';

type DAILOG_STATE = 'dialog-not-shown' | 'show-dialog' | 'dialog-shown';

const DEVTOOLS_VERSION = process.env.DEVTOOLS_VERSION;
const INSTRUCTIONS_FB_URL = 'https://fburl.com/devtools-bridge-protocol';

export default function UnsupportedBridgeProtocolDialog(_: {||}) {
const {dispatch} = useContext(ModalDialogContext);
const store = useContext(StoreContext);
const [state, setState] = useState<DAILOG_STATE>('dialog-not-shown');

useEffect(() => {
if (state === 'dialog-not-shown') {
const showDialog = () => {
batchedUpdates(() => {
setState('show-dialog');
dispatch({
canBeDismissed: false,
type: 'SHOW',
content: (
<DialogContent
unsupportedBridgeProtocol={store.unsupportedBridgeProtocol}
/>
),
});
});
};

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} = unsupportedBridgeProtocol;
const upgradeInstructions = `npm i -g react-devtools@^${minNpmVersion}`;
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 0</span>. However the current
backend version uses bridge protocol{' '}
<span className={styles.Version}>version {version}</span>.
</p>
<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>
<p className={styles.Paragraph}>
Or{' '}
<a
data-electron-external-link="true"
className={styles.Link}
href={INSTRUCTIONS_FB_URL}
target="_blank">
click here
</a>{' '}
for more information.
</p>
</div>
</Fragment>
);
}
6 changes: 6 additions & 0 deletions packages/react-devtools-shared/src/devtools/views/root.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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: rgba(255, 255, 255, 0.25);
--light-color-bridge-version-npm-text: #ffffff;
--light-color-bridge-version-number: yellow;
--light-color-record-active: #fc3a4b;
--light-color-record-hover: #3578e5;
--light-color-record-inactive: #0088fa;
Expand Down Expand Up @@ -136,6 +139,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-record-active: #fc3a4b;
--dark-color-record-hover: #a2e9fc;
--dark-color-record-inactive: #61dafb;
Expand Down
6 changes: 6 additions & 0 deletions packages/react-devtools/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit e8d6f06

Please sign in to comment.