feat[devtools]: add method for connecting backend with custom messaging protocol (#28552)
## 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.
Ruslan Lesiutin committed
Apr 12, 2024 at 15:17 UTC
d012a32f840cf575b9d07bf1ed96af34af99e060
2 files changed
+105
-1
packages/react-devtools-core/README.md
+10
@@ -36,6 +36,16 @@ if (process.env.NODE_ENV !== 'production') {
36
| `useHttps` | `false` | Socket connection to frontend should use secure protocol (wss). |
37
| `websocket` | | Custom `WebSocket` connection to frontend; overrides `host` and `port` settings. |
38
39
+
40
+### `connectWithCustomMessagingProtocol` options
41
+| Prop | Description |
42
+|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
43
+| `onSubscribe` | Function, which receives listener (function, with a single argument) as an argument. Called when backend subscribes to messages from the other end (frontend). |
44
+| `onUnsubscribe` | Function, which receives listener (function) as an argument. Called when backend unsubscribes to messages from the other end (frontend). |
45
+| `onMessage` | Function, which receives 2 arguments: event (string) and payload (any). Called when backend emits a message, which should be sent to the frontend. |
46
+
47
+Unlike `connectToDevTools`, `connectWithCustomMessagingProtocol` returns a callback, which can be used for unsubscribing the backend from the global DevTools hook.
48
+
49
# Frontend API
50
51
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.
packages/react-devtools-core/src/backend.js
+95
-1
@@ -22,7 +22,10 @@ import {
22
} from './cachedSettings';
23
24
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
25
-import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
25
+import type {
26
+ ComponentFilter,
27
+ Wall,
28
+} from 'react-devtools-shared/src/frontend/types';
29
import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
30
import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
31
@@ -310,3 +313,94 @@ export function connectToDevTools(options: ?ConnectOptions) {
313
});
314
}
315
}
316
+
317
+type ConnectWithCustomMessagingOptions = {
318
+ onSubscribe: (cb: Function) => void,
319
+ onUnsubscribe: (cb: Function) => void,
320
+ onMessage: (event: string, payload: any) => void,
321
+ settingsManager: ?DevToolsSettingsManager,
322
+ nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
323
+ resolveRNStyle?: ResolveNativeStyle,
324
+};
325
+
326
+export function connectWithCustomMessagingProtocol({
327
+ onSubscribe,
328
+ onUnsubscribe,
329
+ onMessage,
330
+ settingsManager,
331
+ nativeStyleEditorValidAttributes,
332
+ resolveRNStyle,
333
+}: ConnectWithCustomMessagingOptions): Function {
334
+ if (hook == null) {
335
+ // DevTools didn't get injected into this page (maybe b'c of the contentType).
336
+ return;
337
+ }
338
+
339
+ if (settingsManager != null) {
340
+ try {
341
+ initializeUsingCachedSettings(settingsManager);
342
+ } catch (e) {
343
+ // If we call a method on devToolsSettingsManager that throws, or if
344
+ // is invalid data read out, don't throw and don't interrupt initialization
345
+ console.error(e);
346
+ }
347
+ }
348
+
349
+ const wall: Wall = {
350
+ listen(fn: Function) {
351
+ onSubscribe(fn);
352
+
353
+ return () => {
354
+ onUnsubscribe(fn);
355
+ };
356
+ },
357
+ send(event: string, payload: any) {
358
+ onMessage(event, payload);
359
+ },
360
+ };
361
+
362
+ const bridge: BackendBridge = new Bridge(wall);
363
+
364
+ bridge.addListener(
365
+ 'updateComponentFilters',
366
+ (componentFilters: Array<ComponentFilter>) => {
367
+ // Save filter changes in memory, in case DevTools is reloaded.
368
+ // In that case, the renderer will already be using the updated values.
369
+ // We'll lose these in between backend reloads but that can't be helped.
370
+ savedComponentFilters = componentFilters;
371
+ },
372
+ );
373
+
374
+ if (settingsManager != null) {
375
+ bridge.addListener('updateConsolePatchSettings', consolePatchSettings =>
376
+ cacheConsolePatchSettings(settingsManager, consolePatchSettings),
377
+ );
378
+ }
379
+
380
+ if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) {
381
+ bridge.send('overrideComponentFilters', savedComponentFilters);
382
+ }
383
+
384
+ const agent = new Agent(bridge);
385
+ agent.addListener('shutdown', () => {
386
+ // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
387
+ // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
388
+ hook.emit('shutdown');
389
+ });
390
+
391
+ const unsubscribeBackend = initBackend(hook, agent, window);
392
+
393
+ const nativeStyleResolver: ResolveNativeStyle | void =
394
+ resolveRNStyle || hook.resolveRNStyle;
395
+
396
+ if (nativeStyleResolver != null) {
397
+ const validAttributes =
398
+ nativeStyleEditorValidAttributes ||
399
+ hook.nativeStyleEditorValidAttributes ||
400
+ null;
401
+
402
+ setupNativeStyleEditor(bridge, agent, nativeStyleResolver, validAttributes);
403
+ }
404
+
405
+ return unsubscribeBackend;
406
+}