diff --git a/packages/react-devtools-inline/README.md b/packages/react-devtools-inline/README.md
index b7b3f8373adc9..dff2ce2a06261 100644
--- a/packages/react-devtools-inline/README.md
+++ b/packages/react-devtools-inline/README.md
@@ -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.
-//
+// e.g. render();
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.
-;
+// You can render DevTools now:
+const root = createRoot(container);
+root.render();
+
+// 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
diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js
index a6466d82709fc..c0c87871df117 100644
--- a/packages/react-devtools-inline/src/backend.js
+++ b/packages/react-devtools-inline/src/backend.js
@@ -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;
-
- 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);
-
- 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;
-
- // 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;
+import type {BackendBridge} from 'react-devtools-shared/src/bridge';
+import type {Wall} from 'react-devtools-shared/src/types';
+
+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;
+
+ 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, 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) {
- 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) {
+ parent.postMessage({event, payload}, '*', transferable);
+ },
+ };
+ }
+
+ return (new Bridge(wall): BackendBridge);
}
export function initialize(contentWindow: window): void {
diff --git a/packages/react-devtools-inline/src/constants.js b/packages/react-devtools-inline/src/constants.js
deleted file mode 100644
index cfa443cc04806..0000000000000
--- a/packages/react-devtools-inline/src/constants.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/** @flow */
-
-export const MESSAGE_TYPE_GET_SAVED_PREFERENCES =
- 'React::DevTools::getSavedPreferences';
-export const MESSAGE_TYPE_SAVED_PREFERENCES =
- 'React::DevTools::savedPreferences';
diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js
index 065bb1238715e..0e90c884667a6 100644
--- a/packages/react-devtools-inline/src/frontend.js
+++ b/packages/react-devtools-inline/src/frontend.js
@@ -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 {
- 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, ref) => (
-
+
));
ForwardRef.displayName = 'DevTools';
diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js
index fdd46bf9c88f0..f31e3a6c38a7b 100644
--- a/packages/react-devtools-shared/src/backend/agent.js
+++ b/packages/react-devtools-shared/src/backend/agent.js
@@ -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;
diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js
index 68891e8c18d4b..cdeec7aebfc4b 100644
--- a/packages/react-devtools-shared/src/bridge.js
+++ b/packages/react-devtools-shared/src/bridge.js
@@ -176,10 +176,19 @@ type UpdateConsolePatchSettingsParams = {|
browserTheme: BrowserTheme,
|};
+type SavedPreferencesParams = {|
+ appendComponentStack: boolean,
+ breakOnConsoleErrors: boolean,
+ componentFilters: Array,
+ 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.
diff --git a/packages/react-devtools-shell/index.html b/packages/react-devtools-shell/app.html
similarity index 97%
rename from packages/react-devtools-shell/index.html
rename to packages/react-devtools-shell/app.html
index 410dc5bdc2abd..7bd183891a99b 100644
--- a/packages/react-devtools-shell/index.html
+++ b/packages/react-devtools-shell/app.html
@@ -64,6 +64,6 @@
-
+