From 5e83d9ab3b3f88853591dff43cd70ee4e5c90c5d Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 18 Sep 2024 17:37:00 +0100 Subject: [PATCH] feat[react-devtools]: add settings to global hook object (#30564) Right now we are patching console 2 times: when hook is installed (before page is loaded) and when backend is connected. Because of this, even if user had `appendComponentStack` setting enabled, all emitted error and warning logs are not going to have component stacks appended. They also won't have component stacks appended retroactively when user opens browser DevTools (this is when frontend is initialized and connects to backend). This behavior adds potential race conditions with LogBox in React Native, and also unpredictable to the user, because in order to get component stacks logged you have to open browser DevTools, but by the time you do it, error or warning log was already emitted. To solve this, we are going to only patch console in the hook object, because it is guaranteed to load even before React. Settings are going to be synchronized with the hook via Bridge, and React DevTools Backend Host (React Native or browser extension shell) will be responsible for persisting these settings across the session, this is going to be implemented in a separate PR. --- .../src/backend/agent.js | 39 +++++++------------ .../src/backend/console.js | 2 +- .../src/backend/index.js | 4 ++ .../src/backend/types.js | 8 ++++ packages/react-devtools-shared/src/hook.js | 28 ++++++++++++- 5 files changed, 55 insertions(+), 26 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index f4665e014c..8112fdcd25 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -37,11 +37,9 @@ import type { RendererID, RendererInterface, ConsolePatchSettings, + DevToolsHookSettings, } from './types'; -import type { - ComponentFilter, - BrowserTheme, -} from 'react-devtools-shared/src/frontend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {isSynchronousXHRSupported, isReactNativeEnvironment} from './utils'; const debug = (methodName: string, ...args: Array) => { @@ -153,6 +151,7 @@ export default class Agent extends EventEmitter<{ drawTraceUpdates: [Array], disableTraceUpdates: [], getIfHasUnsupportedRendererVersion: [], + updateHookSettings: [DevToolsHookSettings], }> { _bridge: BackendBridge; _isProfiling: boolean = false; @@ -805,30 +804,22 @@ export default class Agent extends EventEmitter<{ } }; - updateConsolePatchSettings: ({ - appendComponentStack: boolean, - breakOnConsoleErrors: boolean, - browserTheme: BrowserTheme, - hideConsoleLogsInStrictMode: boolean, - showInlineWarningsAndErrors: boolean, - }) => void = ({ - appendComponentStack, - breakOnConsoleErrors, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, - browserTheme, - }: ConsolePatchSettings) => { + updateConsolePatchSettings: ( + settings: $ReadOnly, + ) => void = settings => { + // Propagate the settings, so Backend can subscribe to it and modify hook + this.emit('updateHookSettings', { + appendComponentStack: settings.appendComponentStack, + breakOnConsoleErrors: settings.breakOnConsoleErrors, + showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors, + hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode, + }); + // If the frontend preferences have changed, // or in the case of React Native- if the backend is just finding out the preferences- // then reinstall the console overrides. // It's safe to call `patchConsole` multiple times. - patchConsole({ - appendComponentStack, - breakOnConsoleErrors, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, - browserTheme, - }); + patchConsole(settings); }; updateComponentFilters: (componentFilters: Array) => void = diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 61d2e490eb..ecca4c9424 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -135,7 +135,7 @@ export function patch({ showInlineWarningsAndErrors, hideConsoleLogsInStrictMode, browserTheme, -}: ConsolePatchSettings): void { +}: $ReadOnly): void { // Settings may change after we've patched the console. // Using a shared ref allows the patch function to read the latest values. consoleSettingsRef.appendComponentStack = appendComponentStack; diff --git a/packages/react-devtools-shared/src/backend/index.js b/packages/react-devtools-shared/src/backend/index.js index 0ac7ecc468..5d84bf6bb7 100644 --- a/packages/react-devtools-shared/src/backend/index.js +++ b/packages/react-devtools-shared/src/backend/index.js @@ -83,6 +83,10 @@ export function initBackend( agent.removeListener('shutdown', onAgentShutdown); }); + agent.addListener('updateHookSettings', settings => { + hook.settings = settings; + }); + return () => { subs.forEach(fn => fn()); }; diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 7d816d403a..5c014bba80 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -527,6 +527,7 @@ export type DevToolsHook = { // Testing dangerous_setTargetConsoleForTesting?: (fakeConsole: Object) => void, + settings?: DevToolsHookSettings, ... }; @@ -537,3 +538,10 @@ export type ConsolePatchSettings = { hideConsoleLogsInStrictMode: boolean, browserTheme: BrowserTheme, }; + +export type DevToolsHookSettings = { + appendComponentStack: boolean, + breakOnConsoleErrors: boolean, + showInlineWarningsAndErrors: boolean, + hideConsoleLogsInStrictMode: boolean, +}; diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index eb81e81d46..1dd1fcbd4c 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -15,6 +15,7 @@ import type { RendererID, RendererInterface, DevToolsBackend, + DevToolsHookSettings, } from './backend/types'; import { @@ -25,7 +26,12 @@ import attachRenderer from './attachRenderer'; declare var window: any; -export function installHook(target: any): DevToolsHook | null { +export function installHook( + target: any, + maybeSettingsOrSettingsPromise?: + | DevToolsHookSettings + | Promise, +): DevToolsHook | null { if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { return null; } @@ -566,6 +572,26 @@ export function installHook(target: any): DevToolsHook | null { registerInternalModuleStop, }; + if (maybeSettingsOrSettingsPromise == null) { + // Set default settings + hook.settings = { + appendComponentStack: true, + breakOnConsoleErrors: false, + showInlineWarningsAndErrors: true, + hideConsoleLogsInStrictMode: false, + }; + } else { + Promise.resolve(maybeSettingsOrSettingsPromise) + .then(settings => { + hook.settings = settings; + }) + .catch(() => { + targetConsole.error( + "React DevTools failed to get Console Patching settings. Console won't be patched and some console features will not work.", + ); + }); + } + if (__TEST__) { hook.dangerous_setTargetConsoleForTesting = dangerous_setTargetConsoleForTesting;