Skip to content

Commit c8fa51a

Browse files
author
Brian Vaughn
committed
DevTools: Support mulitple DevTools instances per page
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. To verify these changes, this commit also updates the test shell to add a new entry-point for multiple DevTools.
1 parent 3b3daf5 commit c8fa51a

File tree

13 files changed

+268
-54
lines changed

13 files changed

+268
-54
lines changed

packages/react-devtools-inline/README.md

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import {
1010
MESSAGE_TYPE_SAVED_PREFERENCES,
1111
} from './constants';
1212

13-
function startActivation(contentWindow: window) {
13+
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
14+
import type {Wall} from 'react-devtools-shared/src/types';
15+
16+
function startActivation(contentWindow: window, bridge: BackendBridge) {
1417
const {parent} = contentWindow;
1518

1619
const onMessage = ({data}) => {
@@ -48,7 +51,7 @@ function startActivation(contentWindow: window) {
4851
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
4952
}
5053

51-
finishActivation(contentWindow);
54+
finishActivation(contentWindow, bridge);
5255
break;
5356
default:
5457
break;
@@ -61,27 +64,11 @@ function startActivation(contentWindow: window) {
6164
// because they are stored in localStorage within the context of the extension (on the frontend).
6265
// Instead it relies on the extension to pass preferences through.
6366
// Because we might be in a sandboxed iframe, we have to ask for them by way of postMessage().
67+
// TODO WHAT HUH
6468
parent.postMessage({type: MESSAGE_TYPE_GET_SAVED_PREFERENCES}, '*');
6569
}
6670

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-
71+
function finishActivation(contentWindow: window, bridge: BackendBridge) {
8572
const agent = new Agent(bridge);
8673

8774
const hook = contentWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -100,8 +87,45 @@ function finishActivation(contentWindow: window) {
10087
}
10188
}
10289

103-
export function activate(contentWindow: window): void {
104-
startActivation(contentWindow);
90+
export function activate(
91+
contentWindow: window,
92+
{
93+
bridge,
94+
}: {|
95+
bridge?: BackendBridge,
96+
|} = {},
97+
): void {
98+
if (bridge == null) {
99+
bridge = createBridge(contentWindow);
100+
}
101+
102+
startActivation(contentWindow, bridge);
103+
}
104+
105+
export function createBridge(
106+
contentWindow: window,
107+
wall?: Wall,
108+
): BackendBridge {
109+
const {parent} = contentWindow;
110+
111+
if (wall == null) {
112+
wall = {
113+
listen(fn) {
114+
const onMessage = ({data}) => {
115+
fn(data);
116+
};
117+
window.addEventListener('message', onMessage);
118+
return () => {
119+
window.removeEventListener('message', onMessage);
120+
};
121+
},
122+
send(event: string, payload: any, transferable?: Array<any>) {
123+
parent.postMessage({event, payload}, '*', transferable);
124+
},
125+
};
126+
}
127+
128+
return (new Bridge(wall): BackendBridge);
105129
}
106130

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

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

Lines changed: 4 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;
@@ -320,6 +323,7 @@ export default class Agent extends EventEmitter<{|
320323
}
321324

322325
getBridgeProtocol = () => {
326+
console.log('[agent] getBridgeProtocol -> bridge.send("bridgeProtocol")');
323327
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
324328
};
325329

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>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf8">
5+
<title>React DevTools</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<style>
8+
* {
9+
box-sizing: border-box;
10+
}
11+
body {
12+
display: flex;
13+
flex-direction: row;
14+
position: absolute;
15+
top: 0;
16+
left: 0;
17+
right: 0;
18+
bottom: 0;
19+
margin: 0;
20+
padding: 0;
21+
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
22+
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
23+
font-size: 12px;
24+
line-height: 1.5;
25+
}
26+
.column {
27+
display: flex;
28+
flex-direction: column;
29+
flex: 1 1 50%;
30+
}
31+
.column:first-of-type {
32+
border-right: 1px solid #3d424a;
33+
}
34+
.iframe {
35+
height: 50%;
36+
flex: 0 0 50%;
37+
border: none;
38+
}
39+
.devtools {
40+
height: 50%;
41+
flex: 0 0 50%;
42+
}
43+
</style>
44+
</head>
45+
<body>
46+
<div class="column left-column">
47+
<iframe id="iframe-left" class="iframe"></iframe>
48+
<div id="devtools-left" class="devtools"></div>
49+
</div>
50+
<div class="column">
51+
<iframe id="iframe-right" class="iframe"></iframe>
52+
<div id="devtools-right" class="devtools"></div>
53+
</div>
54+
55+
<script src="dist/multi-devtools.js"></script>
56+
</body>
57+
</html>

packages/react-devtools-shell/now.json

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

packages/react-devtools-shell/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"name": "react-devtools-shell",
44
"version": "0.0.0",
55
"scripts": {
6-
"build": "cross-env NODE_ENV=development cross-env TARGET=remote webpack --config webpack.config.js",
7-
"deploy": "yarn run build && now deploy && now alias react-devtools-experimental",
8-
"start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open"
6+
"start": "yarn start:app",
7+
"start:app": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open-page app.html",
8+
"start:multi": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open-page multi.html"
99
},
1010
"dependencies": {
1111
"immutable": "^4.0.0-rc.12",

packages/react-devtools-shell/src/devtools.js renamed to packages/react-devtools-shell/src/app/devtools.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function hookNamesModuleLoaderFunction() {
6060
return import('react-devtools-inline/hookNames');
6161
}
6262

63-
inject('dist/app.js', () => {
63+
inject('dist/app-index.js', () => {
6464
initDevTools({
6565
connect(cb) {
6666
const root = createRoot(container);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as React from 'react';
2+
import {createRoot} from 'react-dom';
3+
import {
4+
activate as activateBackend,
5+
createBridge as createBackendBridge,
6+
initialize as initializeBackend,
7+
} from 'react-devtools-inline/backend';
8+
import {
9+
createBridge as createFrontendBridge,
10+
createStore,
11+
initialize as createDevTools,
12+
} from 'react-devtools-inline/frontend';
13+
import {__DEBUG__} from 'react-devtools-shared/src/constants';
14+
15+
function inject(contentDocument, sourcePath, callback) {
16+
const script = contentDocument.createElement('script');
17+
script.onload = callback;
18+
script.src = sourcePath;
19+
20+
((contentDocument.body: any): HTMLBodyElement).appendChild(script);
21+
}
22+
23+
function init(appIframe, devtoolsContainer, appSource) {
24+
const {contentDocument, contentWindow} = appIframe;
25+
26+
// Wire each DevTools instance directly to its app.
27+
// By default, DevTools dispatches "message" events on the window,
28+
// but this means that only one instance of DevTools can live on a page.
29+
const wall = {
30+
_listeners: [],
31+
listen(listener) {
32+
if (__DEBUG__) {
33+
console.log('[Shell] Wall.listen()');
34+
}
35+
36+
wall._listeners.push(listener);
37+
},
38+
send(event, payload) {
39+
if (__DEBUG__) {
40+
console.log('[Shell] Wall.send()', {event, payload});
41+
}
42+
43+
wall._listeners.forEach(listener => listener({event, payload}));
44+
},
45+
};
46+
47+
const backendBridge = createBackendBridge(contentWindow, wall);
48+
49+
initializeBackend(contentWindow);
50+
51+
const frontendBridge = createFrontendBridge(contentWindow, wall);
52+
const store = createStore(frontendBridge);
53+
const DevTools = createDevTools(contentWindow, {
54+
bridge: frontendBridge,
55+
store,
56+
});
57+
58+
inject(contentDocument, appSource, () => {
59+
createRoot(devtoolsContainer).render(<DevTools />);
60+
});
61+
62+
activateBackend(contentWindow, {bridge: backendBridge});
63+
}
64+
65+
const appIframeLeft = document.getElementById('iframe-left');
66+
const appIframeRight = document.getElementById('iframe-right');
67+
const devtoolsContainerLeft = document.getElementById('devtools-left');
68+
const devtoolsContainerRight = document.getElementById('devtools-right');
69+
70+
init(appIframeLeft, devtoolsContainerLeft, 'dist/multi-left.js');
71+
init(appIframeRight, devtoolsContainerRight, 'dist/multi-right.js');
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as React from 'react';
2+
import {useState} from 'react';
3+
import {createRoot} from 'react-dom';
4+
5+
function createContainer() {
6+
const container = document.createElement('div');
7+
8+
((document.body: any): HTMLBodyElement).appendChild(container);
9+
10+
return container;
11+
}
12+
13+
function StatefulCounter() {
14+
const [count, setCount] = useState(0);
15+
const handleClick = () => setCount(count + 1);
16+
return <button onClick={handleClick}>Count {count}</button>;
17+
}
18+
19+
createRoot(createContainer()).render(<StatefulCounter />);

0 commit comments

Comments
 (0)