Repository: facebook/react
Pull request: feat: expose installHook with settings argument from react-devtools-core/backend

Stacked on https://github.com/facebook/react/pull/30986. 

Previously, we would call `installHook` at a top level of the JavaScript module. Because of this, having `require` statement for `react-devtools-core` package was enough to initialize the React DevTools global hook on the `window`.

Now, the Hook can actually receive an argument - initial user settings for console patching. We expose this as a function `initialize`, which can be used by third parties (including React Native) to provide the persisted settings.

The README was also updated to reflect the changes.

Changed files:
filename: packages/react-devtools-core/README.md
patch: @@ -14,35 +14,46 @@ If you are building a non-browser-based React renderer, you can use the backend
 
 ```js
 if (process.env.NODE_ENV !== 'production') {
-  const { connectToDevTools } = require("react-devtools-core");
+  const { initialize, connectToDevTools } = require("react-devtools-core");
 
+  initialize(settings);
   // Must be called before packages like react or react-native are imported
-  connectToDevTools({
-    ...config
-  });
+  connectToDevTools({...config});
 }
 ```
 
 > **NOTE** that this API (`connectToDevTools`) must be (1) run in the same context as React and (2) must be called before React packages are imported (e.g. `react`, `react-dom`, `react-native`).
 
+### `initialize` arguments
+| Argument   | Description                                                                                                                                                                                                                                                                                   |
+|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `settings` | Optional. If not specified, or received as null, then default settings are used. Can be plain object or a Promise that resolves with the [plain settings object](#Settings). If Promise rejects, the console will not be patched and some console features from React DevTools will not work. |
+
+#### `Settings`
+| Spec                                                                                                                                                                           | Default value                                                                                                                                                        |
+|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| <pre>{<br>  appendComponentStack: boolean,<br>  breakOnConsoleErrors: boolean,<br>  showInlineWarningsAndErrors: boolean,<br>  hideConsoleLogsInStrictMode: boolean<br>}</pre> | <pre>{<br>  appendComponentStack: true,<br>  breakOnConsoleErrors: false,<br>  showInlineWarningsAndErrors: true,<br>  hideConsoleLogsInStrictMode: false<br>}</pre> |
+
 ### `connectToDevTools` options
-| Prop | Default | Description |
-|---|---|---|
-| `host` | `"localhost"` | Socket connection to frontend should use this host. |
-| `isAppActive` |  | (Optional) function that returns true/false, telling DevTools when it's ready to connect to React. |
-| `port` | `8097` | Socket connection to frontend should use this port. |
-| `resolveRNStyle` |  | (Optional) function that accepts a key (number) and returns a style (object); used by React Native. |
-| `retryConnectionDelay` | `200` | Delay (ms) to wait between retrying a failed Websocket connection |
-| `useHttps` | `false` | Socket connection to frontend should use secure protocol (wss). |
-| `websocket` |  | Custom `WebSocket` connection to frontend; overrides `host` and `port` settings. |
+| Prop                   | Default       | Description                                                                                                               |
+|------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------|
+| `host`                 | `"localhost"` | Socket connection to frontend should use this host.                                                                       |
+| `isAppActive`          |               | (Optional) function that returns true/false, telling DevTools when it's ready to connect to React.                        |
+| `port`                 | `8097`        | Socket connection to frontend should use this port.                                                                       |
+| `resolveRNStyle`       |               | (Optional) function that accepts a key (number) and returns a style (object); used by React Native.                       |
+| `retryConnectionDelay` | `200`         | Delay (ms) to wait between retrying a failed Websocket connection                                                         |
+| `useHttps`             | `false`       | Socket connection to frontend should use secure protocol (wss).                                                           |
+| `websocket`            |               | Custom `WebSocket` connection to frontend; overrides `host` and `port` settings.                                          |
+| `onSettingsUpdated`    |               | A callback that will be called when the user updates the settings in the UI. You can use it for persisting user 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.             |
+| 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.             |
+| `onSettingsUpdated` | A callback that will be called when the user updates the settings in the UI. You can use it for persisting user settings.                                      |       
 
 Unlike `connectToDevTools`, `connectWithCustomMessagingProtocol` returns a callback, which can be used for unsubscribing the backend from the global DevTools hook.
 

filename: packages/react-devtools-core/src/backend.js
patch: @@ -20,7 +20,10 @@ import type {
   ComponentFilter,
   Wall,
 } from 'react-devtools-shared/src/frontend/types';
-import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
+import type {
+  DevToolsHook,
+  DevToolsHookSettings,
+} from 'react-devtools-shared/src/backend/types';
 import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
 
 type ConnectOptions = {
@@ -32,12 +35,9 @@ type ConnectOptions = {
   retryConnectionDelay?: number,
   isAppActive?: () => boolean,
   websocket?: ?WebSocket,
+  onSettingsUpdated?: (settings: $ReadOnly<DevToolsHookSettings>) => void,
 };
 
-installHook(window);
-
-const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
-
 let savedComponentFilters: Array<ComponentFilter> =
   getDefaultComponentFilters();
 
@@ -52,11 +52,21 @@ function debug(methodName: string, ...args: Array<mixed>) {
   }
 }
 
+export function initialize(
+  maybeSettingsOrSettingsPromise?:
+    | DevToolsHookSettings
+    | Promise<DevToolsHookSettings>,
+) {
+  installHook(window, maybeSettingsOrSettingsPromise);
+}
+
 export function connectToDevTools(options: ?ConnectOptions) {
+  const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
   if (hook == null) {
     // DevTools didn't get injected into this page (maybe b'c of the contentType).
     return;
   }
+
   const {
     host = 'localhost',
     nativeStyleEditorValidAttributes,
@@ -66,6 +76,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
     resolveRNStyle = (null: $FlowFixMe),
     retryConnectionDelay = 2000,
     isAppActive = () => true,
+    onSettingsUpdated,
   } = options || {};
 
   const protocol = useHttps ? 'wss' : 'ws';
@@ -160,7 +171,14 @@ export function connectToDevTools(options: ?ConnectOptions) {
     // TODO (npm-packages) Warn if "isBackendStorageAPISupported"
     // $FlowFixMe[incompatible-call] found when upgrading Flow
     const agent = new Agent(bridge);
+    if (onSettingsUpdated != null) {
+      agent.addListener('updateHookSettings', onSettingsUpdated);
+    }
     agent.addListener('shutdown', () => {
+      if (onSettingsUpdated != null) {
+        agent.removeListener('updateHookSettings', onSettingsUpdated);
+      }
+
       // 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');
@@ -290,6 +308,7 @@ type ConnectWithCustomMessagingOptions = {
   onMessage: (event: string, payload: any) => void,
   nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
   resolveRNStyle?: ResolveNativeStyle,
+  onSettingsUpdated?: (settings: $ReadOnly<DevToolsHookSettings>) => void,
 };
 
 export function connectWithCustomMessagingProtocol({
@@ -298,7 +317,9 @@ export function connectWithCustomMessagingProtocol({
   onMessage,
   nativeStyleEditorValidAttributes,
   resolveRNStyle,
+  onSettingsUpdated,
 }: ConnectWithCustomMessagingOptions): Function {
+  const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
   if (hook == null) {
     // DevTools didn't get injected into this page (maybe b'c of the contentType).
     return;
@@ -334,7 +355,14 @@ export function connectWithCustomMessagingProtocol({
   }
 
   const agent = new Agent(bridge);
+  if (onSettingsUpdated != null) {
+    agent.addListener('updateHookSettings', onSettingsUpdated);
+  }
   agent.addListener('shutdown', () => {
+    if (onSettingsUpdated != null) {
+      agent.removeListener('updateHookSettings', onSettingsUpdated);
+    }
+
     // 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');