Skip to content

Commit

Permalink
feat: expose installHook with settings argument from react-devtools-c…
Browse files Browse the repository at this point in the history
…ore/backend (#30987)

Stacked on #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.
  • Loading branch information
hoxyq committed Sep 19, 2024
1 parent e72127a commit a86afe8
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 23 deletions.
47 changes: 29 additions & 18 deletions packages/react-devtools-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
38 changes: 33 additions & 5 deletions packages/react-devtools-core/src/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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();

Expand All @@ -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,
Expand All @@ -66,6 +76,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
resolveRNStyle = (null: $FlowFixMe),
retryConnectionDelay = 2000,
isAppActive = () => true,
onSettingsUpdated,
} = options || {};

const protocol = useHttps ? 'wss' : 'ws';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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({
Expand All @@ -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;
Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit a86afe8

Please sign in to comment.