Repository: facebook/react
Pull request: feat[devtools]: add method for connecting backend with custom messaging protocol

## Summary

RDT backend will now expose method `connectWithCustomMessagingProtocol`, which will be similar to the classic `connectToDevTools` one, but with few differences:
1. It delegates the communication management between frontend and backend to the owner (whos injecting RDT backend). Unlike the `connectToDevTools`, which is relying on websocket connection and receives host and port as an arguments.
2. It returns a callback, which can be used for unsubscribing the current backend instance from the global DevTools hook.


This is a prerequisite for any non-browser RDT integration, which is not designed to be based on websocket.


Changed files:
filename: packages/react-devtools-core/README.md
patch: @@ -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                                                                                                                                                    |
+|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `onSubscribe`   | Function, which receives listener (function, with a single argument) as an argument. Called when backend subscribes to messages from the other end (frontend). |
+| `onUnsubscribe` | 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.

filename: packages/react-devtools-core/src/backend.js
patch: @@ -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 = {
+  onSubscribe: (cb: Function) => void,
+  onUnsubscribe: (cb: Function) => void,
+  onMessage: (event: string, payload: any) => void,
+  settingsManager: ?DevToolsSettingsManager,
+  nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
+  resolveRNStyle?: ResolveNativeStyle,
+};
+
+export function connectWithCustomMessagingProtocol({
+  onSubscribe,
+  onUnsubscribe,
+  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) {
+      onSubscribe(fn);
+
+      return () => {
+        onUnsubscribe(fn);
+      };
+    },
+    send(event: string, payload: any) {
+      onMessage(event, payload);
+    },
+  };
+
+  const bridge: BackendBridge = new Bridge(wall);
+
+  bridge.addListener(
+    'updateComponentFilters',
+    (componentFilters: Array<ComponentFilter>) => {
+      // 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;
+}