Skip to content

Commit 911f92a

Browse files
author
Brian Vaughn
authored
DevTools: Support mulitple DevTools instances per page (#22949)
This is being done so that we can embed DevTools within the new React (beta) docs. The primary changes here are to `react-devtools-inline/backend`: * Add a new `createBridge` API * Add an option to the `activate` method to support passing in the custom bridge object. The `react-devtools-inline` README has been updated to include these new methods. To verify these changes, this commit also updates the test shell to add a new entry-point for multiple DevTools. This commit also replaces two direct calls to `window.postMessage()` with `bridge.send()` (and adds the related Flow types).
1 parent 5757919 commit 911f92a

File tree

16 files changed

+340
-147
lines changed

16 files changed

+340
-147
lines changed

packages/react-devtools-inline/README.md

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const iframe = document.getElementById(frameID);
5656
const contentWindow = iframe.contentWindow;
5757

5858
// This returns a React component that can be rendered into your app.
59-
// <DevTools {...props} />
59+
// e.g. render(<DevTools {...props} />);
6060
const DevTools = initialize(contentWindow);
6161
```
6262

@@ -177,32 +177,47 @@ Below is an example of an advanced integration with a website like [Replay.io](h
177177

178178
```js
179179
import {
180-
createBridge,
180+
activate as activateBackend,
181+
createBridge as createBackendBridge,
182+
initialize as initializeBackend,
183+
} from 'react-devtools-inline/backend';
184+
import {
185+
createBridge as createFrontendBridge,
181186
createStore,
182187
initialize as createDevTools,
183-
} from "react-devtools-inline/frontend";
188+
} from 'react-devtools-inline/frontend';
184189

185-
// Custom Wall implementation enables serializing data
186-
// using an API other than window.postMessage()
190+
// DevTools uses "message" events and window.postMessage() by default,
191+
// but we can override this behavior by creating a custom "Wall" object.
187192
// For example...
188193
const wall = {
189-
emit() {},
194+
_listeners: [],
190195
listen(listener) {
191-
wall._listener = listener;
196+
wall._listeners.push(listener);
192197
},
193-
async send(event, payload) {
194-
const response = await fetch(...).json();
195-
wall._listener(response);
198+
send(event, payload) {
199+
wall._listeners.forEach(listener => listener({event, payload}));
196200
},
197201
};
198202

199-
// Create a Bridge and Store that use the custom Wall.
203+
// Initialize the DevTools backend before importing React (or any other packages that might import React).
204+
initializeBackend(contentWindow);
205+
206+
// Prepare DevTools for rendering.
207+
// To use the custom Wall we've created, we need to also create our own "Bridge" and "Store" objects.
200208
const bridge = createBridge(target, wall);
201209
const store = createStore(bridge);
202210
const DevTools = createDevTools(target, { bridge, store });
203211

204-
// Render DevTools with it.
205-
<DevTools {...otherProps} />;
212+
// You can render DevTools now:
213+
const root = createRoot(container);
214+
root.render(<DevTools {...otherProps} />);
215+
216+
// Lastly, let the DevTools backend know that the frontend is ready.
217+
// To use the custom Wall we've created, we need to also pass in the "Bridge".
218+
activateBackend(contentWindow, {
219+
bridge: createBackendBridge(contentWindow, wall),
220+
});
206221
```
207222

208223
## Local development

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

Lines changed: 79 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,83 +5,57 @@ import Bridge from 'react-devtools-shared/src/bridge';
55
import {initBackend} from 'react-devtools-shared/src/backend';
66
import {installHook} from 'react-devtools-shared/src/hook';
77
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
8-
import {
9-
MESSAGE_TYPE_GET_SAVED_PREFERENCES,
10-
MESSAGE_TYPE_SAVED_PREFERENCES,
11-
} from './constants';
128

13-
function startActivation(contentWindow: window) {
14-
const {parent} = contentWindow;
15-
16-
const onMessage = ({data}) => {
17-
switch (data.type) {
18-
case MESSAGE_TYPE_SAVED_PREFERENCES:
19-
// This is the only message we're listening for,
20-
// so it's safe to cleanup after we've received it.
21-
contentWindow.removeEventListener('message', onMessage);
22-
23-
const {
24-
appendComponentStack,
25-
breakOnConsoleErrors,
26-
componentFilters,
27-
showInlineWarningsAndErrors,
28-
hideConsoleLogsInStrictMode,
29-
} = data;
30-
31-
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
32-
contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
33-
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
34-
contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
35-
contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
36-
37-
// TRICKY
38-
// The backend entry point may be required in the context of an iframe or the parent window.
39-
// If it's required within the parent window, store the saved values on it as well,
40-
// since the injected renderer interface will read from window.
41-
// Technically we don't need to store them on the contentWindow in this case,
42-
// but it doesn't really hurt anything to store them there too.
43-
if (contentWindow !== window) {
44-
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
45-
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
46-
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
47-
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
48-
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
49-
}
50-
51-
finishActivation(contentWindow);
52-
break;
53-
default:
54-
break;
9+
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
10+
import type {Wall} from 'react-devtools-shared/src/types';
11+
12+
function startActivation(contentWindow: window, bridge: BackendBridge) {
13+
const onSavedPreferences = data => {
14+
// This is the only message we're listening for,
15+
// so it's safe to cleanup after we've received it.
16+
bridge.removeListener('savedPreferences', onSavedPreferences);
17+
18+
const {
19+
appendComponentStack,
20+
breakOnConsoleErrors,
21+
componentFilters,
22+
showInlineWarningsAndErrors,
23+
hideConsoleLogsInStrictMode,
24+
} = data;
25+
26+
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
27+
contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
28+
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
29+
contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
30+
contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
31+
32+
// TRICKY
33+
// The backend entry point may be required in the context of an iframe or the parent window.
34+
// If it's required within the parent window, store the saved values on it as well,
35+
// since the injected renderer interface will read from window.
36+
// Technically we don't need to store them on the contentWindow in this case,
37+
// but it doesn't really hurt anything to store them there too.
38+
if (contentWindow !== window) {
39+
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
40+
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
41+
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
42+
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
43+
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
5544
}
45+
46+
finishActivation(contentWindow, bridge);
5647
};
5748

58-
contentWindow.addEventListener('message', onMessage);
49+
bridge.addListener('savedPreferences', onSavedPreferences);
5950

6051
// The backend may be unable to read saved preferences directly,
6152
// because they are stored in localStorage within the context of the extension (on the frontend).
6253
// Instead it relies on the extension to pass preferences through.
6354
// Because we might be in a sandboxed iframe, we have to ask for them by way of postMessage().
64-
parent.postMessage({type: MESSAGE_TYPE_GET_SAVED_PREFERENCES}, '*');
55+
bridge.send('getSavedPreferences');
6556
}
6657

67-
function finishActivation(contentWindow: window) {
68-
const {parent} = contentWindow;
69-
70-
const bridge = new Bridge({
71-
listen(fn) {
72-
const onMessage = event => {
73-
fn(event.data);
74-
};
75-
contentWindow.addEventListener('message', onMessage);
76-
return () => {
77-
contentWindow.removeEventListener('message', onMessage);
78-
};
79-
},
80-
send(event: string, payload: any, transferable?: Array<any>) {
81-
parent.postMessage({event, payload}, '*', transferable);
82-
},
83-
});
84-
58+
function finishActivation(contentWindow: window, bridge: BackendBridge) {
8559
const agent = new Agent(bridge);
8660

8761
const hook = contentWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -100,8 +74,45 @@ function finishActivation(contentWindow: window) {
10074
}
10175
}
10276

103-
export function activate(contentWindow: window): void {
104-
startActivation(contentWindow);
77+
export function activate(
78+
contentWindow: window,
79+
{
80+
bridge,
81+
}: {|
82+
bridge?: BackendBridge,
83+
|} = {},
84+
): void {
85+
if (bridge == null) {
86+
bridge = createBridge(contentWindow);
87+
}
88+
89+
startActivation(contentWindow, bridge);
90+
}
91+
92+
export function createBridge(
93+
contentWindow: window,
94+
wall?: Wall,
95+
): BackendBridge {
96+
const {parent} = contentWindow;
97+
98+
if (wall == null) {
99+
wall = {
100+
listen(fn) {
101+
const onMessage = ({data}) => {
102+
fn(data);
103+
};
104+
contentWindow.addEventListener('message', onMessage);
105+
return () => {
106+
contentWindow.removeEventListener('message', onMessage);
107+
};
108+
},
109+
send(event: string, payload: any, transferable?: Array<any>) {
110+
parent.postMessage({event, payload}, '*', transferable);
111+
},
112+
};
113+
}
114+
115+
return (new Bridge(wall): BackendBridge);
105116
}
106117

107118
export function initialize(contentWindow: window): void {

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

Lines changed: 0 additions & 6 deletions
This file was deleted.

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

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ import {
1212
getShowInlineWarningsAndErrors,
1313
getHideConsoleLogsInStrictMode,
1414
} from 'react-devtools-shared/src/utils';
15-
import {
16-
MESSAGE_TYPE_GET_SAVED_PREFERENCES,
17-
MESSAGE_TYPE_SAVED_PREFERENCES,
18-
} from './constants';
1915

2016
import type {Wall} from 'react-devtools-shared/src/types';
2117
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
@@ -68,49 +64,40 @@ export function initialize(
6864
store?: Store,
6965
|} = {},
7066
): React.AbstractComponent<Props, mixed> {
71-
const onGetSavedPreferencesMessage = ({data, source}) => {
72-
if (source === 'react-devtools-content-script') {
73-
// Ignore messages from the DevTools browser extension.
74-
}
75-
76-
switch (data.type) {
77-
case MESSAGE_TYPE_GET_SAVED_PREFERENCES:
78-
// This is the only message we're listening for,
79-
// so it's safe to cleanup after we've received it.
80-
window.removeEventListener('message', onGetSavedPreferencesMessage);
81-
82-
// The renderer interface can't read saved preferences directly,
83-
// because they are stored in localStorage within the context of the extension.
84-
// Instead it relies on the extension to pass them through.
85-
contentWindow.postMessage(
86-
{
87-
type: MESSAGE_TYPE_SAVED_PREFERENCES,
88-
appendComponentStack: getAppendComponentStack(),
89-
breakOnConsoleErrors: getBreakOnConsoleErrors(),
90-
componentFilters: getSavedComponentFilters(),
91-
showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
92-
hideConsoleLogsInStrictMode: getHideConsoleLogsInStrictMode(),
93-
},
94-
'*',
95-
);
96-
break;
97-
default:
98-
break;
99-
}
100-
};
101-
102-
window.addEventListener('message', onGetSavedPreferencesMessage);
103-
10467
if (bridge == null) {
10568
bridge = createBridge(contentWindow);
10669
}
10770

71+
// Type refinement.
72+
const frontendBridge = ((bridge: any): FrontendBridge);
73+
10874
if (store == null) {
109-
store = createStore(bridge);
75+
store = createStore(frontendBridge);
11076
}
11177

78+
const onGetSavedPreferences = () => {
79+
// This is the only message we're listening for,
80+
// so it's safe to cleanup after we've received it.
81+
frontendBridge.removeListener('getSavedPreferences', onGetSavedPreferences);
82+
83+
const data = {
84+
appendComponentStack: getAppendComponentStack(),
85+
breakOnConsoleErrors: getBreakOnConsoleErrors(),
86+
componentFilters: getSavedComponentFilters(),
87+
showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
88+
hideConsoleLogsInStrictMode: getHideConsoleLogsInStrictMode(),
89+
};
90+
91+
// The renderer interface can't read saved preferences directly,
92+
// because they are stored in localStorage within the context of the extension.
93+
// Instead it relies on the extension to pass them through.
94+
frontendBridge.send('savedPreferences', data);
95+
};
96+
97+
frontendBridge.addListener('getSavedPreferences', onGetSavedPreferences);
98+
11299
const ForwardRef = forwardRef<Props, mixed>((props, ref) => (
113-
<DevTools ref={ref} bridge={bridge} store={store} {...props} />
100+
<DevTools ref={ref} bridge={frontendBridge} store={store} {...props} />
114101
));
115102
ForwardRef.displayName = 'DevTools';
116103

packages/react-devtools-shared/src/backend/agent.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ export default class Agent extends EventEmitter<{|
225225
bridge.send('profilingStatus', true);
226226
}
227227

228+
// Send the Bridge protocol after initialization in case the frontend has already requested it.
229+
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
230+
228231
// Notify the frontend if the backend supports the Storage API (e.g. localStorage).
229232
// If not, features like reload-and-profile will not work correctly and must be disabled.
230233
let isBackendStorageAPISupported = false;

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,19 @@ type UpdateConsolePatchSettingsParams = {|
176176
browserTheme: BrowserTheme,
177177
|};
178178

179+
type SavedPreferencesParams = {|
180+
appendComponentStack: boolean,
181+
breakOnConsoleErrors: boolean,
182+
componentFilters: Array<ComponentFilter>,
183+
showInlineWarningsAndErrors: boolean,
184+
hideConsoleLogsInStrictMode: boolean,
185+
|};
186+
179187
export type BackendEvents = {|
180188
bridgeProtocol: [BridgeProtocol],
181189
extensionBackendInitialized: [],
182190
fastRefreshScheduled: [],
191+
getSavedPreferences: [],
183192
inspectedElement: [InspectedElementPayload],
184193
isBackendStorageAPISupported: [boolean],
185194
isSynchronousXHRSupported: [boolean],
@@ -223,6 +232,7 @@ type FrontendEvents = {|
223232
profilingData: [ProfilingDataBackend],
224233
reloadAndProfile: [boolean],
225234
renamePath: [RenamePath],
235+
savedPreferences: [SavedPreferencesParams],
226236
selectFiber: [number],
227237
setTraceUpdatesEnabled: [boolean],
228238
shutdown: [],
@@ -277,7 +287,9 @@ class Bridge<
277287

278288
this._wallUnlisten =
279289
wall.listen((message: Message) => {
280-
(this: any).emit(message.event, message.payload);
290+
if (message && message.event) {
291+
(this: any).emit(message.event, message.payload);
292+
}
281293
}) || null;
282294

283295
// Temporarily support older standalone front-ends sending commands to newer embedded backends.

packages/react-devtools-shell/index.html renamed to packages/react-devtools-shell/app.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,6 @@
6464
<!-- This script installs the hook, injects the backend, and renders the DevTools UI -->
6565
<!-- In DEV mode, this file is served by the Webpack dev server -->
6666
<!-- For production builds, it's built by Webpack and uploaded from the local file system -->
67-
<script src="dist/devtools.js"></script>
67+
<script src="dist/app-devtools.js"></script>
6868
</body>
6969
</html>

0 commit comments

Comments
 (0)