mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
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).
This commit is contained in:
@@ -56,7 +56,7 @@ const iframe = document.getElementById(frameID);
|
||||
const contentWindow = iframe.contentWindow;
|
||||
|
||||
// This returns a React component that can be rendered into your app.
|
||||
// <DevTools {...props} />
|
||||
// e.g. render(<DevTools {...props} />);
|
||||
const DevTools = initialize(contentWindow);
|
||||
```
|
||||
|
||||
@@ -177,32 +177,47 @@ Below is an example of an advanced integration with a website like [Replay.io](h
|
||||
|
||||
```js
|
||||
import {
|
||||
createBridge,
|
||||
activate as activateBackend,
|
||||
createBridge as createBackendBridge,
|
||||
initialize as initializeBackend,
|
||||
} from 'react-devtools-inline/backend';
|
||||
import {
|
||||
createBridge as createFrontendBridge,
|
||||
createStore,
|
||||
initialize as createDevTools,
|
||||
} from "react-devtools-inline/frontend";
|
||||
} from 'react-devtools-inline/frontend';
|
||||
|
||||
// Custom Wall implementation enables serializing data
|
||||
// using an API other than window.postMessage()
|
||||
// DevTools uses "message" events and window.postMessage() by default,
|
||||
// but we can override this behavior by creating a custom "Wall" object.
|
||||
// For example...
|
||||
const wall = {
|
||||
emit() {},
|
||||
_listeners: [],
|
||||
listen(listener) {
|
||||
wall._listener = listener;
|
||||
wall._listeners.push(listener);
|
||||
},
|
||||
async send(event, payload) {
|
||||
const response = await fetch(...).json();
|
||||
wall._listener(response);
|
||||
send(event, payload) {
|
||||
wall._listeners.forEach(listener => listener({event, payload}));
|
||||
},
|
||||
};
|
||||
|
||||
// Create a Bridge and Store that use the custom Wall.
|
||||
// Initialize the DevTools backend before importing React (or any other packages that might import React).
|
||||
initializeBackend(contentWindow);
|
||||
|
||||
// Prepare DevTools for rendering.
|
||||
// To use the custom Wall we've created, we need to also create our own "Bridge" and "Store" objects.
|
||||
const bridge = createBridge(target, wall);
|
||||
const store = createStore(bridge);
|
||||
const DevTools = createDevTools(target, { bridge, store });
|
||||
|
||||
// Render DevTools with it.
|
||||
<DevTools {...otherProps} />;
|
||||
// You can render DevTools now:
|
||||
const root = createRoot(container);
|
||||
root.render(<DevTools {...otherProps} />);
|
||||
|
||||
// Lastly, let the DevTools backend know that the frontend is ready.
|
||||
// To use the custom Wall we've created, we need to also pass in the "Bridge".
|
||||
activateBackend(contentWindow, {
|
||||
bridge: createBackendBridge(contentWindow, wall),
|
||||
});
|
||||
```
|
||||
|
||||
## Local development
|
||||
|
||||
139
packages/react-devtools-inline/src/backend.js
vendored
139
packages/react-devtools-inline/src/backend.js
vendored
@@ -5,83 +5,57 @@ import Bridge from 'react-devtools-shared/src/bridge';
|
||||
import {initBackend} from 'react-devtools-shared/src/backend';
|
||||
import {installHook} from 'react-devtools-shared/src/hook';
|
||||
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
||||
import {
|
||||
MESSAGE_TYPE_GET_SAVED_PREFERENCES,
|
||||
MESSAGE_TYPE_SAVED_PREFERENCES,
|
||||
} from './constants';
|
||||
|
||||
function startActivation(contentWindow: window) {
|
||||
const {parent} = contentWindow;
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {Wall} from 'react-devtools-shared/src/types';
|
||||
|
||||
const onMessage = ({data}) => {
|
||||
switch (data.type) {
|
||||
case MESSAGE_TYPE_SAVED_PREFERENCES:
|
||||
// This is the only message we're listening for,
|
||||
// so it's safe to cleanup after we've received it.
|
||||
contentWindow.removeEventListener('message', onMessage);
|
||||
function startActivation(contentWindow: window, bridge: BackendBridge) {
|
||||
const onSavedPreferences = data => {
|
||||
// This is the only message we're listening for,
|
||||
// so it's safe to cleanup after we've received it.
|
||||
bridge.removeListener('savedPreferences', onSavedPreferences);
|
||||
|
||||
const {
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
componentFilters,
|
||||
showInlineWarningsAndErrors,
|
||||
hideConsoleLogsInStrictMode,
|
||||
} = data;
|
||||
const {
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
componentFilters,
|
||||
showInlineWarningsAndErrors,
|
||||
hideConsoleLogsInStrictMode,
|
||||
} = data;
|
||||
|
||||
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
|
||||
contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
|
||||
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
|
||||
contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
|
||||
contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
|
||||
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
|
||||
contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
|
||||
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
|
||||
contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
|
||||
contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
|
||||
|
||||
// TRICKY
|
||||
// The backend entry point may be required in the context of an iframe or the parent window.
|
||||
// If it's required within the parent window, store the saved values on it as well,
|
||||
// since the injected renderer interface will read from window.
|
||||
// Technically we don't need to store them on the contentWindow in this case,
|
||||
// but it doesn't really hurt anything to store them there too.
|
||||
if (contentWindow !== window) {
|
||||
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
|
||||
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
|
||||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
|
||||
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
|
||||
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
|
||||
}
|
||||
|
||||
finishActivation(contentWindow);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
// TRICKY
|
||||
// The backend entry point may be required in the context of an iframe or the parent window.
|
||||
// If it's required within the parent window, store the saved values on it as well,
|
||||
// since the injected renderer interface will read from window.
|
||||
// Technically we don't need to store them on the contentWindow in this case,
|
||||
// but it doesn't really hurt anything to store them there too.
|
||||
if (contentWindow !== window) {
|
||||
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
|
||||
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
|
||||
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
|
||||
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
|
||||
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
|
||||
}
|
||||
|
||||
finishActivation(contentWindow, bridge);
|
||||
};
|
||||
|
||||
contentWindow.addEventListener('message', onMessage);
|
||||
bridge.addListener('savedPreferences', onSavedPreferences);
|
||||
|
||||
// The backend may be unable to read saved preferences directly,
|
||||
// because they are stored in localStorage within the context of the extension (on the frontend).
|
||||
// Instead it relies on the extension to pass preferences through.
|
||||
// Because we might be in a sandboxed iframe, we have to ask for them by way of postMessage().
|
||||
parent.postMessage({type: MESSAGE_TYPE_GET_SAVED_PREFERENCES}, '*');
|
||||
bridge.send('getSavedPreferences');
|
||||
}
|
||||
|
||||
function finishActivation(contentWindow: window) {
|
||||
const {parent} = contentWindow;
|
||||
|
||||
const bridge = new Bridge({
|
||||
listen(fn) {
|
||||
const onMessage = event => {
|
||||
fn(event.data);
|
||||
};
|
||||
contentWindow.addEventListener('message', onMessage);
|
||||
return () => {
|
||||
contentWindow.removeEventListener('message', onMessage);
|
||||
};
|
||||
},
|
||||
send(event: string, payload: any, transferable?: Array<any>) {
|
||||
parent.postMessage({event, payload}, '*', transferable);
|
||||
},
|
||||
});
|
||||
|
||||
function finishActivation(contentWindow: window, bridge: BackendBridge) {
|
||||
const agent = new Agent(bridge);
|
||||
|
||||
const hook = contentWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
@@ -100,8 +74,45 @@ function finishActivation(contentWindow: window) {
|
||||
}
|
||||
}
|
||||
|
||||
export function activate(contentWindow: window): void {
|
||||
startActivation(contentWindow);
|
||||
export function activate(
|
||||
contentWindow: window,
|
||||
{
|
||||
bridge,
|
||||
}: {|
|
||||
bridge?: BackendBridge,
|
||||
|} = {},
|
||||
): void {
|
||||
if (bridge == null) {
|
||||
bridge = createBridge(contentWindow);
|
||||
}
|
||||
|
||||
startActivation(contentWindow, bridge);
|
||||
}
|
||||
|
||||
export function createBridge(
|
||||
contentWindow: window,
|
||||
wall?: Wall,
|
||||
): BackendBridge {
|
||||
const {parent} = contentWindow;
|
||||
|
||||
if (wall == null) {
|
||||
wall = {
|
||||
listen(fn) {
|
||||
const onMessage = ({data}) => {
|
||||
fn(data);
|
||||
};
|
||||
contentWindow.addEventListener('message', onMessage);
|
||||
return () => {
|
||||
contentWindow.removeEventListener('message', onMessage);
|
||||
};
|
||||
},
|
||||
send(event: string, payload: any, transferable?: Array<any>) {
|
||||
parent.postMessage({event, payload}, '*', transferable);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return (new Bridge(wall): BackendBridge);
|
||||
}
|
||||
|
||||
export function initialize(contentWindow: window): void {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/** @flow */
|
||||
|
||||
export const MESSAGE_TYPE_GET_SAVED_PREFERENCES =
|
||||
'React::DevTools::getSavedPreferences';
|
||||
export const MESSAGE_TYPE_SAVED_PREFERENCES =
|
||||
'React::DevTools::savedPreferences';
|
||||
65
packages/react-devtools-inline/src/frontend.js
vendored
65
packages/react-devtools-inline/src/frontend.js
vendored
@@ -12,10 +12,6 @@ import {
|
||||
getShowInlineWarningsAndErrors,
|
||||
getHideConsoleLogsInStrictMode,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {
|
||||
MESSAGE_TYPE_GET_SAVED_PREFERENCES,
|
||||
MESSAGE_TYPE_SAVED_PREFERENCES,
|
||||
} from './constants';
|
||||
|
||||
import type {Wall} from 'react-devtools-shared/src/types';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
@@ -68,49 +64,40 @@ export function initialize(
|
||||
store?: Store,
|
||||
|} = {},
|
||||
): React.AbstractComponent<Props, mixed> {
|
||||
const onGetSavedPreferencesMessage = ({data, source}) => {
|
||||
if (source === 'react-devtools-content-script') {
|
||||
// Ignore messages from the DevTools browser extension.
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case MESSAGE_TYPE_GET_SAVED_PREFERENCES:
|
||||
// This is the only message we're listening for,
|
||||
// so it's safe to cleanup after we've received it.
|
||||
window.removeEventListener('message', onGetSavedPreferencesMessage);
|
||||
|
||||
// The renderer interface can't read saved preferences directly,
|
||||
// because they are stored in localStorage within the context of the extension.
|
||||
// Instead it relies on the extension to pass them through.
|
||||
contentWindow.postMessage(
|
||||
{
|
||||
type: MESSAGE_TYPE_SAVED_PREFERENCES,
|
||||
appendComponentStack: getAppendComponentStack(),
|
||||
breakOnConsoleErrors: getBreakOnConsoleErrors(),
|
||||
componentFilters: getSavedComponentFilters(),
|
||||
showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
|
||||
hideConsoleLogsInStrictMode: getHideConsoleLogsInStrictMode(),
|
||||
},
|
||||
'*',
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', onGetSavedPreferencesMessage);
|
||||
|
||||
if (bridge == null) {
|
||||
bridge = createBridge(contentWindow);
|
||||
}
|
||||
|
||||
// Type refinement.
|
||||
const frontendBridge = ((bridge: any): FrontendBridge);
|
||||
|
||||
if (store == null) {
|
||||
store = createStore(bridge);
|
||||
store = createStore(frontendBridge);
|
||||
}
|
||||
|
||||
const onGetSavedPreferences = () => {
|
||||
// This is the only message we're listening for,
|
||||
// so it's safe to cleanup after we've received it.
|
||||
frontendBridge.removeListener('getSavedPreferences', onGetSavedPreferences);
|
||||
|
||||
const data = {
|
||||
appendComponentStack: getAppendComponentStack(),
|
||||
breakOnConsoleErrors: getBreakOnConsoleErrors(),
|
||||
componentFilters: getSavedComponentFilters(),
|
||||
showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
|
||||
hideConsoleLogsInStrictMode: getHideConsoleLogsInStrictMode(),
|
||||
};
|
||||
|
||||
// The renderer interface can't read saved preferences directly,
|
||||
// because they are stored in localStorage within the context of the extension.
|
||||
// Instead it relies on the extension to pass them through.
|
||||
frontendBridge.send('savedPreferences', data);
|
||||
};
|
||||
|
||||
frontendBridge.addListener('getSavedPreferences', onGetSavedPreferences);
|
||||
|
||||
const ForwardRef = forwardRef<Props, mixed>((props, ref) => (
|
||||
<DevTools ref={ref} bridge={bridge} store={store} {...props} />
|
||||
<DevTools ref={ref} bridge={frontendBridge} store={store} {...props} />
|
||||
));
|
||||
ForwardRef.displayName = 'DevTools';
|
||||
|
||||
|
||||
@@ -225,6 +225,9 @@ export default class Agent extends EventEmitter<{|
|
||||
bridge.send('profilingStatus', true);
|
||||
}
|
||||
|
||||
// Send the Bridge protocol after initialization in case the frontend has already requested it.
|
||||
this._bridge.send('bridgeProtocol', currentBridgeProtocol);
|
||||
|
||||
// Notify the frontend if the backend supports the Storage API (e.g. localStorage).
|
||||
// If not, features like reload-and-profile will not work correctly and must be disabled.
|
||||
let isBackendStorageAPISupported = false;
|
||||
|
||||
14
packages/react-devtools-shared/src/bridge.js
vendored
14
packages/react-devtools-shared/src/bridge.js
vendored
@@ -176,10 +176,19 @@ type UpdateConsolePatchSettingsParams = {|
|
||||
browserTheme: BrowserTheme,
|
||||
|};
|
||||
|
||||
type SavedPreferencesParams = {|
|
||||
appendComponentStack: boolean,
|
||||
breakOnConsoleErrors: boolean,
|
||||
componentFilters: Array<ComponentFilter>,
|
||||
showInlineWarningsAndErrors: boolean,
|
||||
hideConsoleLogsInStrictMode: boolean,
|
||||
|};
|
||||
|
||||
export type BackendEvents = {|
|
||||
bridgeProtocol: [BridgeProtocol],
|
||||
extensionBackendInitialized: [],
|
||||
fastRefreshScheduled: [],
|
||||
getSavedPreferences: [],
|
||||
inspectedElement: [InspectedElementPayload],
|
||||
isBackendStorageAPISupported: [boolean],
|
||||
isSynchronousXHRSupported: [boolean],
|
||||
@@ -223,6 +232,7 @@ type FrontendEvents = {|
|
||||
profilingData: [ProfilingDataBackend],
|
||||
reloadAndProfile: [boolean],
|
||||
renamePath: [RenamePath],
|
||||
savedPreferences: [SavedPreferencesParams],
|
||||
selectFiber: [number],
|
||||
setTraceUpdatesEnabled: [boolean],
|
||||
shutdown: [],
|
||||
@@ -277,7 +287,9 @@ class Bridge<
|
||||
|
||||
this._wallUnlisten =
|
||||
wall.listen((message: Message) => {
|
||||
(this: any).emit(message.event, message.payload);
|
||||
if (message && message.event) {
|
||||
(this: any).emit(message.event, message.payload);
|
||||
}
|
||||
}) || null;
|
||||
|
||||
// Temporarily support older standalone front-ends sending commands to newer embedded backends.
|
||||
|
||||
@@ -64,6 +64,6 @@
|
||||
<!-- This script installs the hook, injects the backend, and renders the DevTools UI -->
|
||||
<!-- In DEV mode, this file is served by the Webpack dev server -->
|
||||
<!-- For production builds, it's built by Webpack and uploaded from the local file system -->
|
||||
<script src="dist/devtools.js"></script>
|
||||
<script src="dist/app-devtools.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
57
packages/react-devtools-shell/multi.html
Normal file
57
packages/react-devtools-shell/multi.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>React DevTools</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
|
||||
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 50%;
|
||||
}
|
||||
.column:first-of-type {
|
||||
border-right: 1px solid #3d424a;
|
||||
}
|
||||
.iframe {
|
||||
height: 50%;
|
||||
flex: 0 0 50%;
|
||||
border: none;
|
||||
}
|
||||
.devtools {
|
||||
height: 50%;
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="column left-column">
|
||||
<iframe id="iframe-left" class="iframe"></iframe>
|
||||
<div id="devtools-left" class="devtools"></div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<iframe id="iframe-right" class="iframe"></iframe>
|
||||
<div id="devtools-right" class="devtools"></div>
|
||||
</div>
|
||||
|
||||
<script src="dist/multi-devtools.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "react-devtools-experimental",
|
||||
"alias": ["react-devtools-experimental"],
|
||||
"files": ["index.html", "dist"]
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
"name": "react-devtools-shell",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=development cross-env TARGET=remote webpack --config webpack.config.js",
|
||||
"deploy": "yarn run build && now deploy && now alias react-devtools-experimental",
|
||||
"start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open"
|
||||
"start": "yarn start:app",
|
||||
"start:app": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open-page app.html",
|
||||
"start:multi": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open-page multi.html"
|
||||
},
|
||||
"dependencies": {
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
|
||||
@@ -60,7 +60,7 @@ function hookNamesModuleLoaderFunction() {
|
||||
return import('react-devtools-inline/hookNames');
|
||||
}
|
||||
|
||||
inject('dist/app.js', () => {
|
||||
inject('dist/app-index.js', () => {
|
||||
initDevTools({
|
||||
connect(cb) {
|
||||
const root = createRoot(container);
|
||||
71
packages/react-devtools-shell/src/multi/devtools.js
vendored
Normal file
71
packages/react-devtools-shell/src/multi/devtools.js
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import {createRoot} from 'react-dom';
|
||||
import {
|
||||
activate as activateBackend,
|
||||
createBridge as createBackendBridge,
|
||||
initialize as initializeBackend,
|
||||
} from 'react-devtools-inline/backend';
|
||||
import {
|
||||
createBridge as createFrontendBridge,
|
||||
createStore,
|
||||
initialize as createDevTools,
|
||||
} from 'react-devtools-inline/frontend';
|
||||
import {__DEBUG__} from 'react-devtools-shared/src/constants';
|
||||
|
||||
function inject(contentDocument, sourcePath, callback) {
|
||||
const script = contentDocument.createElement('script');
|
||||
script.onload = callback;
|
||||
script.src = sourcePath;
|
||||
|
||||
((contentDocument.body: any): HTMLBodyElement).appendChild(script);
|
||||
}
|
||||
|
||||
function init(appIframe, devtoolsContainer, appSource) {
|
||||
const {contentDocument, contentWindow} = appIframe;
|
||||
|
||||
// Wire each DevTools instance directly to its app.
|
||||
// By default, DevTools dispatches "message" events on the window,
|
||||
// but this means that only one instance of DevTools can live on a page.
|
||||
const wall = {
|
||||
_listeners: [],
|
||||
listen(listener) {
|
||||
if (__DEBUG__) {
|
||||
console.log('[Shell] Wall.listen()');
|
||||
}
|
||||
|
||||
wall._listeners.push(listener);
|
||||
},
|
||||
send(event, payload) {
|
||||
if (__DEBUG__) {
|
||||
console.log('[Shell] Wall.send()', {event, payload});
|
||||
}
|
||||
|
||||
wall._listeners.forEach(listener => listener({event, payload}));
|
||||
},
|
||||
};
|
||||
|
||||
const backendBridge = createBackendBridge(contentWindow, wall);
|
||||
|
||||
initializeBackend(contentWindow);
|
||||
|
||||
const frontendBridge = createFrontendBridge(contentWindow, wall);
|
||||
const store = createStore(frontendBridge);
|
||||
const DevTools = createDevTools(contentWindow, {
|
||||
bridge: frontendBridge,
|
||||
store,
|
||||
});
|
||||
|
||||
inject(contentDocument, appSource, () => {
|
||||
createRoot(devtoolsContainer).render(<DevTools />);
|
||||
});
|
||||
|
||||
activateBackend(contentWindow, {bridge: backendBridge});
|
||||
}
|
||||
|
||||
const appIframeLeft = document.getElementById('iframe-left');
|
||||
const appIframeRight = document.getElementById('iframe-right');
|
||||
const devtoolsContainerLeft = document.getElementById('devtools-left');
|
||||
const devtoolsContainerRight = document.getElementById('devtools-right');
|
||||
|
||||
init(appIframeLeft, devtoolsContainerLeft, 'dist/multi-left.js');
|
||||
init(appIframeRight, devtoolsContainerRight, 'dist/multi-right.js');
|
||||
19
packages/react-devtools-shell/src/multi/left.js
vendored
Normal file
19
packages/react-devtools-shell/src/multi/left.js
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import {useState} from 'react';
|
||||
import {createRoot} from 'react-dom';
|
||||
|
||||
function createContainer() {
|
||||
const container = document.createElement('div');
|
||||
|
||||
((document.body: any): HTMLBodyElement).appendChild(container);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function StatefulCounter() {
|
||||
const [count, setCount] = useState(0);
|
||||
const handleClick = () => setCount(count + 1);
|
||||
return <button onClick={handleClick}>Count {count}</button>;
|
||||
}
|
||||
|
||||
createRoot(createContainer()).render(<StatefulCounter />);
|
||||
33
packages/react-devtools-shell/src/multi/right.js
vendored
Normal file
33
packages/react-devtools-shell/src/multi/right.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import {useLayoutEffect, useRef, useState} from 'react';
|
||||
import {render} from 'react-dom';
|
||||
|
||||
function createContainer() {
|
||||
const container = document.createElement('div');
|
||||
|
||||
((document.body: any): HTMLBodyElement).appendChild(container);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function EffectWithState() {
|
||||
const [didMount, setDidMount] = useState(0);
|
||||
|
||||
const renderCountRef = useRef(0);
|
||||
renderCountRef.current++;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!didMount) {
|
||||
setDidMount(true);
|
||||
}
|
||||
}, [didMount]);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
<li>Rendered {renderCountRef.current} times</li>
|
||||
{didMount && <li>Mounted!</li>}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
render(<EffectWithState />, createContainer());
|
||||
@@ -42,8 +42,11 @@ const config = {
|
||||
mode: __DEV__ ? 'development' : 'production',
|
||||
devtool: __DEV__ ? 'cheap-source-map' : 'source-map',
|
||||
entry: {
|
||||
app: './src/app/index.js',
|
||||
devtools: './src/devtools.js',
|
||||
'app-index': './src/app/index.js',
|
||||
'app-devtools': './src/app/devtools.js',
|
||||
'multi-left': './src/multi/left.js',
|
||||
'multi-devtools': './src/multi/devtools.js',
|
||||
'multi-right': './src/multi/right.js',
|
||||
},
|
||||
node: {
|
||||
// source-maps package has a dependency on 'fs'
|
||||
|
||||
@@ -11,14 +11,7 @@
|
||||
"bin": {
|
||||
"react-devtools": "./bin.js"
|
||||
},
|
||||
"files": [
|
||||
"bin.js",
|
||||
"build-info.json",
|
||||
"app.html",
|
||||
"app.js",
|
||||
"index.js",
|
||||
"icons"
|
||||
],
|
||||
"files": [],
|
||||
"scripts": {
|
||||
"start": "node bin.js"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user