Disable unsupported Bridge protocol version dialog and add workaround for old protocol operations format (#24093)
Rationale: The only case where the unsupported dialog really matters is React Naive. That's the case where the frontend and backend versions are most likely to mismatch. In React Native, the backend is likely to send the bridge protocol version before sending operations– since the agent does this proactively during initialization. I've tested the React Native starter app– after forcefully downgrading the backend version to 4.19.1 (see #23307 (comment)) and verified that this change "fixes" things. Not only does DevTools no longer throw an error that causes the UI to be hidden– it works (meaning that the Components tree can be inspected and interacted with).
Brian Vaughn committed
Mar 15, 2022 at 10:48 UTC
63b86e19955a3f68c3e6e4928f4e5b24fd8a8342
5 files changed
+43
-25
packages/react-devtools-core/src/standalone.js
+2
-2
@@ -11,7 +11,7 @@ import {createElement} from 'react';
11
import {
12
// $FlowFixMe Flow does not yet know about flushSync()
13
flushSync,
14
-} from 'react-dom/client';
14
+} from 'react-dom';
15
import {createRoot} from 'react-dom/client';
16
import Bridge from 'react-devtools-shared/src/bridge';
17
import Store from 'react-devtools-shared/src/devtools/store';
@@ -106,9 +106,9 @@ function safeUnmount() {
106
flushSync(() => {
107
if (root !== null) {
108
root.unmount();
109
+ root = null;
110
}
111
});
111
- root = null;
112
}
113
114
function reload() {
packages/react-devtools-shared/src/bridge.js
+3
@@ -54,6 +54,9 @@ export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
54
minNpmVersion: '"<4.11.0"',
55
maxNpmVersion: '"<4.11.0"',
56
},
57
+ // Versions 4.11.x – 4.12.x contained the backwards breaking change,
58
+ // but we didn't add the "fix" of checking the protocol version until 4.13,
59
+ // so we don't recommend downgrading to 4.11 or 4.12.
60
{
61
version: 1,
62
minNpmVersion: '4.13.0',
packages/react-devtools-shared/src/devtools/store.js
+28
-14
@@ -180,7 +180,8 @@ export default class Store extends EventEmitter<{|
180
_rootSupportsBasicProfiling: boolean = false;
181
_rootSupportsTimelineProfiling: boolean = false;
182
183
- _unsupportedBridgeProtocol: BridgeProtocol | null = null;
183
+ _bridgeProtocol: BridgeProtocol | null = null;
184
+ _unsupportedBridgeProtocolDetected: boolean = false;
185
_unsupportedRendererVersionDetected: boolean = false;
186
187
// Total number of visible elements (within all roots).
@@ -375,6 +376,10 @@ export default class Store extends EventEmitter<{|
376
this.emit('componentFilters');
377
}
378
379
+ get bridgeProtocol(): BridgeProtocol | null {
380
+ return this._bridgeProtocol;
381
+ }
382
+
383
get errorCount(): number {
384
return this._cachedErrorCount;
385
}
@@ -466,8 +471,8 @@ export default class Store extends EventEmitter<{|
471
return this._supportsTraceUpdates;
472
}
473
469
- get unsupportedBridgeProtocol(): BridgeProtocol | null {
470
- return this._unsupportedBridgeProtocol;
474
+ get unsupportedBridgeProtocolDetected(): boolean {
475
+ return this._unsupportedBridgeProtocolDetected;
476
}
477
478
get unsupportedRendererVersionDetected(): boolean {
@@ -942,11 +947,21 @@ export default class Store extends EventEmitter<{|
947
(operations[i] & PROFILING_FLAG_TIMELINE_SUPPORT) !== 0;
948
i++;
949
945
- const supportsStrictMode = operations[i] > 0;
946
- i++;
950
+ let supportsStrictMode = false;
951
+ let hasOwnerMetadata = false;
952
948
- const hasOwnerMetadata = operations[i] > 0;
949
- i++;
953
+ // If we don't know the bridge protocol, guess that we're dealing with the latest.
954
+ // If we do know it, we can take it into consideration when parsing operations.
955
+ if (
956
+ this._bridgeProtocol === null ||
957
+ this._bridgeProtocol.version >= 2
958
+ ) {
959
+ supportsStrictMode = operations[i] > 0;
960
+ i++;
961
+
962
+ hasOwnerMetadata = operations[i] > 0;
963
+ i++;
964
+ }
965
966
this._roots = this._roots.concat(id);
967
this._rootIDToRendererID.set(id, rendererID);
@@ -1383,14 +1398,13 @@ export default class Store extends EventEmitter<{|
1398
this._onBridgeProtocolTimeoutID = null;
1399
}
1400
1401
+ this._bridgeProtocol = bridgeProtocol;
1402
+
1403
if (bridgeProtocol.version !== currentBridgeProtocol.version) {
1387
- this._unsupportedBridgeProtocol = bridgeProtocol;
1388
- } else {
1389
- // If we should happen to get a response after timing out...
1390
- this._unsupportedBridgeProtocol = null;
1404
+ // Technically newer versions of the frontend can, at least for now,
1405
+ // gracefully handle older versions of the backend protocol.
1406
+ // So for now we don't need to display the unsupported dialog.
1407
}
1392
-
1393
- this.emit('unsupportedBridgeProtocolDetected');
1408
};
1409
1410
onBridgeProtocolTimeout = () => {
@@ -1398,7 +1412,7 @@ export default class Store extends EventEmitter<{|
1412
1413
// If we timed out, that indicates the backend predates the bridge protocol,
1414
// so we can set a fake version (0) to trigger the downgrade message.
1401
- this._unsupportedBridgeProtocol = BRIDGE_PROTOCOL[0];
1415
+ this._bridgeProtocol = BRIDGE_PROTOCOL[0];
1416
1417
this.emit('unsupportedBridgeProtocolDetected');
1418
};
packages/react-devtools-shared/src/devtools/views/UnsupportedBridgeProtocolDialog.js
+3
-5
@@ -33,20 +33,18 @@ export default function UnsupportedBridgeProtocolDialog(_: {||}) {
33
useEffect(() => {
34
const updateDialog = () => {
35
if (!isVisible) {
36
- if (store.unsupportedBridgeProtocol !== null) {
36
+ if (store.unsupportedBridgeProtocolDetected) {
37
dispatch({
38
canBeDismissed: false,
39
id: MODAL_DIALOG_ID,
40
type: 'SHOW',
41
content: (
42
- <DialogContent
43
- unsupportedBridgeProtocol={store.unsupportedBridgeProtocol}
44
- />
42
+ <DialogContent unsupportedBridgeProtocol={store.bridgeProtocol} />
43
),
44
});
45
}
46
} else {
49
- if (store.unsupportedBridgeProtocol === null) {
47
+ if (!store.unsupportedBridgeProtocolDetected) {
48
dispatch({
49
type: 'HIDE',
50
id: MODAL_DIALOG_ID,
packages/react-devtools-shared/src/devtools/views/hooks.js
+7
-4
@@ -239,10 +239,13 @@ export function useModalDismissSignal(
239
// It's important to listen to the ownerDocument to support the browser extension.
240
// Here we use portals to render individual tabs (e.g. Profiler),
241
// and the root document might belong to a different window.
242
- ownerDocument = ((modalRef.current: any): HTMLDivElement).ownerDocument;
243
- ownerDocument.addEventListener('keydown', handleDocumentKeyDown);
244
- if (dismissOnClickOutside) {
245
- ownerDocument.addEventListener('click', handleDocumentClick, true);
242
+ const div = modalRef.current;
243
+ if (div != null) {
244
+ ownerDocument = div.ownerDocument;
245
+ ownerDocument.addEventListener('keydown', handleDocumentKeyDown);
246
+ if (dismissOnClickOutside) {
247
+ ownerDocument.addEventListener('click', handleDocumentClick, true);
248
+ }
249
}
250
}, 0);
251