From dcc35ff0e4a8a6098d373b1ef34bd54ec961591b Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 13 Mar 2024 14:44:07 +0000 Subject: [PATCH] feat[devtools]: add method for connecting backend with custom messaging protocol --- packages/react-devtools-core/README.md | 10 +++ packages/react-devtools-core/src/backend.js | 96 ++++++++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-core/README.md b/packages/react-devtools-core/README.md index af8f6f5bcc12c..6b6083e61a595 100644 --- a/packages/react-devtools-core/README.md +++ b/packages/react-devtools-core/README.md @@ -36,6 +36,16 @@ if (process.env.NODE_ENV !== 'production') { | `useHttps` | `false` | Socket connection to frontend should use secure protocol (wss). | | `websocket` | | Custom `WebSocket` connection to frontend; overrides `host` and `port` settings. | + +### `connectWithCustomMessagingProtocol` options +| Prop | Description | +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `onMessageSubscription` | Function, which receives listener (function, with a single argument) as an argument. Called when backend subscribes to messages from the other end (frontend). | +| `onMessageSubscriptionRemoval` | Function, which receives listener (function) as an argument. Called when backend unsubscribes to messages from the other end (frontend). | +| `onMessage` | Function, which receives 2 arguments: event (string) and payload (any). Called when backend emits a message, which should be sent to the frontend. | + +Unlike `connectToDevTools`, `connectWithCustomMessagingProtocol` returns a callback, which can be used for unsubscribing the backend from the global DevTools hook. + # Frontend API Frontend APIs can be used to render the DevTools UI into a DOM node. One example of this is [`react-devtools`](https://github.com/facebook/react/tree/main/packages/react-devtools) which wraps DevTools in an Electron app. diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index ee26ff35a4090..21c6eb830a286 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -22,7 +22,10 @@ import { } from './cachedSettings'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; -import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; +import type { + ComponentFilter, + Wall, +} from 'react-devtools-shared/src/frontend/types'; import type {DevToolsHook} from 'react-devtools-shared/src/backend/types'; import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; @@ -310,3 +313,94 @@ export function connectToDevTools(options: ?ConnectOptions) { }); } } + +type ConnectWithCustomMessagingOptions = { + onMessageSubscription: (cb: Function) => void, + onMessageSubscriptionRemoval: (cb: Function) => void, + onMessage: (event: string, payload: any) => void, + settingsManager: ?DevToolsSettingsManager, + nativeStyleEditorValidAttributes?: $ReadOnlyArray, + resolveRNStyle?: ResolveNativeStyle, +}; + +export function connectWithCustomMessagingProtocol({ + onMessageSubscription, + onMessageSubscriptionRemoval, + onMessage, + settingsManager, + nativeStyleEditorValidAttributes, + resolveRNStyle, +}: ConnectWithCustomMessagingOptions): Function { + if (hook == null) { + // DevTools didn't get injected into this page (maybe b'c of the contentType). + return; + } + + if (settingsManager != null) { + try { + initializeUsingCachedSettings(settingsManager); + } catch (e) { + // If we call a method on devToolsSettingsManager that throws, or if + // is invalid data read out, don't throw and don't interrupt initialization + console.error(e); + } + } + + const wall: Wall = { + listen(fn: Function) { + onMessageSubscription(fn); + + return () => { + onMessageSubscriptionRemoval(fn); + }; + }, + send(event: string, payload: any) { + onMessage(event, payload); + }, + }; + + const bridge: BackendBridge = new Bridge(wall); + + bridge.addListener( + 'updateComponentFilters', + (componentFilters: Array) => { + // Save filter changes in memory, in case DevTools is reloaded. + // In that case, the renderer will already be using the updated values. + // We'll lose these in between backend reloads but that can't be helped. + savedComponentFilters = componentFilters; + }, + ); + + if (settingsManager != null) { + bridge.addListener('updateConsolePatchSettings', consolePatchSettings => + cacheConsolePatchSettings(settingsManager, consolePatchSettings), + ); + } + + if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) { + bridge.send('overrideComponentFilters', savedComponentFilters); + } + + const agent = new Agent(bridge); + agent.addListener('shutdown', () => { + // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down, + // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here. + hook.emit('shutdown'); + }); + + const unsubscribeBackend = initBackend(hook, agent, window); + + const nativeStyleResolver: ResolveNativeStyle | void = + resolveRNStyle || hook.resolveRNStyle; + + if (nativeStyleResolver != null) { + const validAttributes = + nativeStyleEditorValidAttributes || + hook.nativeStyleEditorValidAttributes || + null; + + setupNativeStyleEditor(bridge, agent, nativeStyleResolver, validAttributes); + } + + return unsubscribeBackend; +}