diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index 19090533447cb..e93733fa8e72a 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "4.10.1", + "version": "4.10.4", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index 817b34386c205..63064e2360310 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -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(); diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 8e631995c6b4b..d02c0cd79d867 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Chrome Developer Tools.", - "version": "4.10.1", - "version_name": "4.10.1", + "version": "4.10.4", + "version_name": "4.10.4", "minimum_chrome_version": "49", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 06b0d892391cc..08ec92993aa61 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", - "version": "4.10.1", - "version_name": "4.10.1", + "version": "4.10.4", + "version_name": "4.10.4", "minimum_chrome_version": "49", diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 0a4631baf5799..b5fe7d1f8cdf9 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "4.10.1", + "version": "4.10.4", "applications": { "gecko": { diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index e1234341353c5..5e97e95f78892 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "4.10.1", + "version": "4.10.4", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js index 9a5c145471139..4ed8f510ba465 100644 --- a/packages/react-devtools-inline/src/frontend.js +++ b/packages/react-devtools-inline/src/frontend.js @@ -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, ref) => ( diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 2bde2aed8b071..c964bc581ab42 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -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 = {| @@ -117,6 +130,7 @@ type UpdateConsolePatchSettingsParams = {| |}; type BackendEvents = {| + bridgeProtocol: [BridgeProtocol], extensionBackendInitialized: [], inspectedElement: [InspectedElementPayload], isBackendStorageAPISupported: [boolean], @@ -144,6 +158,7 @@ type FrontendEvents = {| clearNativeElementHighlight: [], copyElementPath: [CopyElementPathParams], deletePath: [DeletePath], + getBridgeProtocol: [], getOwnersList: [ElementAndRendererID], getProfilingData: [{|rendererID: RendererID|}], getProfilingStatus: [], diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index cd829bb3bf3ab..c166e5ee4090d 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -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__) { @@ -49,6 +52,7 @@ const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY = 'React::DevTools::recordChangeDescriptions'; type Config = {| + checkBridgeProtocolCompatibility?: boolean, isProfiling?: boolean, supportsNativeInspection?: boolean, supportsReloadAndProfile?: boolean, @@ -74,6 +78,7 @@ export default class Store extends EventEmitter<{| supportsNativeStyleEditor: [], supportsProfiling: [], supportsReloadAndProfile: [], + unsupportedBridgeProtocolDetected: [], unsupportedRendererVersionDetected: [], |}> { _bridge: FrontendBridge; @@ -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). @@ -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. @@ -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; } @@ -1020,6 +1037,7 @@ export default class Store extends EventEmitter<{| 'isBackendStorageAPISupported', this.onBridgeStorageSupported, ); + this._bridge.removeListener('bridgeProtocol', this.onBridgeProtocol); }; onBridgeStorageSupported = (isBackendStorageAPISupported: boolean) => { @@ -1033,4 +1051,9 @@ export default class Store extends EventEmitter<{| this.emit('unsupportedRendererVersionDetected'); }; + + onBridgeProtocol = (bridgeProtocol: BridgeProtocol) => { + this._unsupportedBridgeProtocol = bridgeProtocol; + this.emit('unsupportedBridgeProtocolDetected'); + }; } diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index cb5c36cf680bc..a7ac1c4397990 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -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'; @@ -226,6 +227,7 @@ export default function DevTools({ + {warnIfLegacyBackendDetected && } {warnIfUnsupportedVersionDetected && } diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index 2a89a9fe679b7..4e81d55618962 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -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); diff --git a/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.css b/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.css new file mode 100644 index 0000000000000..ae4d73fd78142 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.css @@ -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); +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js b/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js new file mode 100644 index 0000000000000..c3987f2afec42 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js @@ -0,0 +1,110 @@ +/** + * 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://fb.me/devtools-unsupported-bridge-protocol'; + +export default function UnsupportedBridgeProtocolDialog(_: {||}) { + const {dispatch} = useContext(ModalDialogContext); + const store = useContext(StoreContext); + const [state, setState] = useState('dialog-not-shown'); + + useEffect(() => { + if (state === 'dialog-not-shown') { + const showDialog = () => { + batchedUpdates(() => { + setState('show-dialog'); + dispatch({ + canBeDismissed: false, + type: 'SHOW', + content: ( + + ), + }); + }); + }; + + 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 ( + +
+
Unsupported DevTools backend version
+

+ You are running react-devtools version{' '} + {DEVTOOLS_VERSION}. +

+

+ This requires bridge protocol{' '} + version 0. However the current + backend version uses bridge protocol{' '} + version {version}. +

+

+ To fix this, upgrade the DevTools NPM package: +

+
+          {upgradeInstructions}
+          
+        
+

+ Or{' '} + + click here + {' '} + for more information. +

+
+
+ ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/hooks.js b/packages/react-devtools-shared/src/devtools/views/hooks.js index 3af0ceaeb29ad..54b1d7d20ce8b 100644 --- a/packages/react-devtools-shared/src/devtools/views/hooks.js +++ b/packages/react-devtools-shared/src/devtools/views/hooks.js @@ -207,14 +207,13 @@ export function useModalDismissSignal( return () => {}; } - const handleDocumentKeyDown = ({key}: any) => { - if (key === 'Escape') { + const handleDocumentKeyDown = (event: any) => { + if (event.key === 'Escape') { dismissCallback(); } }; const handleDocumentClick = (event: any) => { - // $FlowFixMe if ( modalRef.current !== null && !modalRef.current.contains(event.target) @@ -226,18 +225,33 @@ export function useModalDismissSignal( } }; - // It's important to listen to the ownerDocument to support the browser extension. - // Here we use portals to render individual tabs (e.g. Profiler), - // and the root document might belong to a different window. - const ownerDocument = modalRef.current.ownerDocument; - ownerDocument.addEventListener('keydown', handleDocumentKeyDown); - if (dismissOnClickOutside) { - ownerDocument.addEventListener('click', handleDocumentClick); - } + let ownerDocument = null; + + // Delay until after the current call stack is empty, + // in case this effect is being run while an event is currently bubbling. + // In that case, we don't want to listen to the pre-existing event. + let timeoutID = setTimeout(() => { + timeoutID = null; + + // It's important to listen to the ownerDocument to support the browser extension. + // Here we use portals to render individual tabs (e.g. Profiler), + // and the root document might belong to a different window. + ownerDocument = ((modalRef.current: any): HTMLDivElement).ownerDocument; + ownerDocument.addEventListener('keydown', handleDocumentKeyDown); + if (dismissOnClickOutside) { + ownerDocument.addEventListener('click', handleDocumentClick); + } + }, 0); return () => { - ownerDocument.removeEventListener('keydown', handleDocumentKeyDown); - ownerDocument.removeEventListener('click', handleDocumentClick); + if (timeoutID !== null) { + clearTimeout(timeoutID); + } + + if (ownerDocument !== null) { + ownerDocument.removeEventListener('keydown', handleDocumentKeyDown); + ownerDocument.removeEventListener('click', handleDocumentClick); + } }; }, [modalRef, dismissCallback, dismissOnClickOutside]); } diff --git a/packages/react-devtools-shared/src/devtools/views/root.css b/packages/react-devtools-shared/src/devtools/views/root.css index b49213ccdffee..d0994df648e83 100644 --- a/packages/react-devtools-shared/src/devtools/views/root.css +++ b/packages/react-devtools-shared/src/devtools/views/root.css @@ -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: #eff0f1; + --light-color-bridge-version-npm-text: #000000; + --light-color-bridge-version-number: #0088fa; --light-color-record-active: #fc3a4b; --light-color-record-hover: #3578e5; --light-color-record-inactive: #0088fa; @@ -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; diff --git a/packages/react-devtools/app.js b/packages/react-devtools/app.js index 38304b4ffadda..ef3c43470fb08 100644 --- a/packages/react-devtools/app.js +++ b/packages/react-devtools/app.js @@ -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( diff --git a/packages/react-devtools/package.json b/packages/react-devtools/package.json index 3a22f5c622f20..e1464ad695332 100644 --- a/packages/react-devtools/package.json +++ b/packages/react-devtools/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools", - "version": "4.10.1", + "version": "4.10.4", "description": "Use react-devtools outside of the browser", "license": "MIT", "repository": { @@ -27,7 +27,7 @@ "electron": "^9.1.0", "ip": "^1.1.4", "minimist": "^1.2.3", - "react-devtools-core": "4.10.1", + "react-devtools-core": "4.10.4", "update-notifier": "^2.1.0" } }