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 22, 2021
1 parent a155860 commit 40f32c7
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/react-devtools-core/src/standalone.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function reload() {
canViewElementSourceFunction,
showTabBar: true,
store: ((store: any): Store),
warnIfUnsupportedBridgeProtocolDetected: true,
warnIfLegacyBackendDetected: true,
viewElementSourceFunction,
}),
Expand Down
6 changes: 6 additions & 0 deletions packages/react-devtools-shared/src/backend/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 37 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,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<BridgeProtocol> = [
{
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 = {|
Expand Down Expand Up @@ -128,6 +163,7 @@ export type BackendEvents = {|
overrideComponentFilters: [Array<ComponentFilter>],
profilingData: [ProfilingDataBackend],
profilingStatus: [boolean],
bridgeProtocol: [BridgeProtocol],
reloadAppForProfiling: [],
selectFiber: [number],
shutdown: [],
Expand All @@ -153,6 +189,7 @@ type FrontendEvents = {|
getOwnersList: [ElementAndRendererID],
getProfilingData: [{|rendererID: RendererID|}],
getProfilingStatus: [],
getBridgeProtocol: [],
highlightNativeElement: [HighlightElementInDOM],
inspectElement: [InspectElementParams],
logElementToConsole: [ElementAndRendererID],
Expand Down
26 changes: 25 additions & 1 deletion packages/react-devtools-shared/src/devtools/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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__) {
Expand Down Expand Up @@ -76,6 +80,8 @@ export default class Store extends EventEmitter<{|
supportsNativeStyleEditor: [],
supportsProfiling: [],
supportsReloadAndProfile: [],
unsupportedBridgeProtocolDetected: [],
unsupportedRendererVersionDetected: [],
unsupportedRendererVersionDetected: [],
|}> {
_bridge: FrontendBridge;
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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');
}
};
}
6 changes: 6 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 UnsupportedProtocolDialog from './UnsupportedProtocolDialog';
import UnsupportedVersionDialog from './UnsupportedVersionDialog';
import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
import {useLocalStorage} from './hooks';
Expand Down Expand Up @@ -59,6 +60,7 @@ export type Props = {|
showTabBar?: boolean,
store: Store,
warnIfLegacyBackendDetected?: boolean,
warnIfUnsupportedBridgeProtocolDetected?: boolean,
warnIfUnsupportedVersionDetected?: boolean,
viewAttributeSourceFunction?: ?ViewAttributeSource,
viewElementSourceFunction?: ?ViewElementSource,
Expand Down Expand Up @@ -102,6 +104,7 @@ export default function DevTools({
profilerPortalContainer,
showTabBar = false,
store,
warnIfUnsupportedBridgeProtocolDetected = false,
warnIfLegacyBackendDetected = false,
warnIfUnsupportedVersionDetected = false,
viewAttributeSourceFunction,
Expand Down Expand Up @@ -226,6 +229,9 @@ export default function DevTools({
</TreeContextController>
</ViewElementSourceContext.Provider>
</SettingsContextController>
{warnIfUnsupportedBridgeProtocolDetected && (
<UnsupportedProtocolDialog />
)}
{warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
{warnIfUnsupportedVersionDetected && <UnsupportedVersionDialog />}
</ModalDialogContextController>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<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, maxNpmVersion} = unsupportedBridgeProtocol;

let instructions;
if (maxNpmVersion === null) {
const upgradeInstructions = `npm i -g react-devtools@^${minNpmVersion}`;
instructions = (
<p className={styles.Instructions}>
To fix this, upgrade the DevTools NPM package:
<pre className={styles.NpmCommand}>
{upgradeInstructions}
<Button
onClick={() => copy(upgradeInstructions)}
title="Copy upgrade command to clipboard">
<ButtonIcon type="copy" />
</Button>
</pre>
</p>
);
} else {
const downgradeInstructions = `npm i -g react-devtools@${maxNpmVersion}`;
instructions = (
<p className={styles.Instructions}>
To fix this, downgrade the DevTools NPM package:
<pre className={styles.NpmCommand}>
{downgradeInstructions}
<Button
onClick={() => copy(downgradeInstructions)}
title="Copy downgrade command to clipboard">
<ButtonIcon type="copy" />
</Button>
</pre>
</p>
);
}

return (
<Fragment>
<div className={styles.Row}>
<div>
<div className={styles.Title}>
Unsupported DevTools backend version
</div>
<p>
You are running <code>react-devtools</code> version{' '}
<span className={styles.Version}>{DEVTOOLS_VERSION}</span>.
</p>
<p>
This requires bridge protocol{' '}
<span className={styles.Version}>
version {currentBridgeProtocol.version}
</span>
. However the current backend version uses bridge protocol{' '}
<span className={styles.Version}>version {version}</span>.
</p>
{instructions}
</div>
</div>
</Fragment>
);
}
Loading

0 comments on commit 40f32c7

Please sign in to comment.