From df4f5ec951e5576d1b6bb8186fa0df0784ea4854 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Wed, 21 Jan 2026 18:33:39 +0100 Subject: [PATCH 1/4] [DevTools] Apply component filters on initial load --- packages/react-devtools-core/README.md | 21 +++++++++++-- packages/react-devtools-core/src/backend.js | 25 +++++----------- .../react-devtools-core/src/standalone.js | 13 ++++---- .../contentScripts/hookSettingsInjector.js | 21 +++++++++---- .../src/contentScripts/installHook.js | 15 ++++++++-- .../src/contentScripts/messages.js | 22 +++++++------- .../src/main/index.js | 4 +-- packages/react-devtools-inline/src/backend.js | 30 +++++++++---------- .../src/__tests__/setupTests.js | 3 +- .../src/attachRenderer.js | 5 ++++ .../src/backend/fiber/renderer.js | 24 +++++---------- .../src/backend/types.js | 4 +++ .../src/devtools/store.js | 22 +++++++++++--- packages/react-devtools-shared/src/hook.js | 5 ++++ 14 files changed, 129 insertions(+), 85 deletions(-) diff --git a/packages/react-devtools-core/README.md b/packages/react-devtools-core/README.md index 21f4f697cf3..a42e569697f 100644 --- a/packages/react-devtools-core/README.md +++ b/packages/react-devtools-core/README.md @@ -25,15 +25,30 @@ if (process.env.NODE_ENV !== 'production') { > **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. | +| 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. | +| `shouldStartProfilingNow` | Optional. Whether to start profiling immediately after installing the hook. Defaults to `false`. | +| `profilingSettings` | Optional. Profiling settings used when `shouldStartProfilingNow` is `true`. Defaults to `{ recordChangeDescriptions: false, recordTimeline: false }`. | +| `componentFilters` | Optional. Array or Promise that resolves to an array of component filters to apply before DevTools connects. Defaults to the built-in host component filter. See [Component filters](#component-filters) for the full spec. | #### `Settings` | Spec | Default value | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
{
appendComponentStack: boolean,
breakOnConsoleErrors: boolean,
showInlineWarningsAndErrors: boolean,
hideConsoleLogsInStrictMode: boolean,
disableSecondConsoleLogDimmingInStrictMode: boolean
}
|
{
appendComponentStack: true,
breakOnConsoleErrors: false,
showInlineWarningsAndErrors: true,
hideConsoleLogsInStrictMode: false,
disableSecondConsoleLogDimmingInStrictMode: false
}
| +#### Component filters +Each filter object must include `type` and `isEnabled`. Some filters also require `value` or `isValid`. + +| Type | Required fields | Description | +|------|-----------------|-------------| +| `ComponentFilterElementType` (`1`) | `type`, `isEnabled`, `value: ElementType` | Hides elements of the given element type. DevTools defaults to hiding host components. | +| `ComponentFilterDisplayName` (`2`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose display name matches the provided RegExp string. | +| `ComponentFilterLocation` (`3`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose source location matches the provided RegExp string. | +| `ComponentFilterHOC` (`4`) | `type`, `isEnabled`, `isValid` | Hides higher-order components. | +| `ComponentFilterEnvironmentName` (`5`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose environment name matches the provided string. | +| `ComponentFilterActivitySlice` (`6`) | `type`, `isEnabled`, `isValid`, `activityID`, `rendererID` | Filters activity slices; usually managed by DevTools rather than user code. | + ### `connectToDevTools` options | Prop | Default | Description | |------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------| diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 4cca0887c57..262b3bc0412 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -66,9 +66,17 @@ export function initialize( | Promise, shouldStartProfilingNow: boolean = false, profilingSettings?: ProfilingSettings, + maybeComponentFiltersOrComponentFiltersPromise?: + | Array + | Promise>, ) { + const componentFiltersOrComponentFiltersPromise = + maybeComponentFiltersOrComponentFiltersPromise + ? maybeComponentFiltersOrComponentFiltersPromise + : savedComponentFilters; installHook( window, + componentFiltersOrComponentFiltersPromise, maybeSettingsOrSettingsPromise, shouldStartProfilingNow, profilingSettings, @@ -174,19 +182,6 @@ export function connectToDevTools(options: ?ConnectOptions) { }, ); - // The renderer interface doesn't read saved component filters directly, - // because they are generally stored in localStorage within the context of the extension. - // Because of this it relies on the extension to pass filters. - // In the case of the standalone DevTools being used with a website, - // saved filters are injected along with the backend script tag so we shouldn't override them here. - // This injection strategy doesn't work for React Native though. - // Ideally the backend would save the filters itself, but RN doesn't provide a sync storage solution. - // So for now we just fall back to using the default filters... - if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) { - // $FlowFixMe[incompatible-use] found when upgrading Flow - bridge.send('overrideComponentFilters', savedComponentFilters); - } - // TODO (npm-packages) Warn if "isBackendStorageAPISupported" // $FlowFixMe[incompatible-call] found when upgrading Flow const agent = new Agent(bridge, isProfiling, onReloadAndProfile); @@ -381,10 +376,6 @@ export function connectWithCustomMessagingProtocol({ }, ); - if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) { - bridge.send('overrideComponentFilters', savedComponentFilters); - } - const agent = new Agent(bridge, isProfiling, onReloadAndProfile); if (typeof onReloadAndProfileFlagsReset === 'function') { onReloadAndProfileFlagsReset(); diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index f8286783a9b..cbe3d94e568 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -356,17 +356,14 @@ function startServer( // because they are generally stored in localStorage within the context of the extension. // Because of this it relies on the extension to pass filters, so include them wth the response here. // This will ensure that saved filters are shared across different web pages. - const savedPreferencesString = ` - window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify( - getSavedComponentFilters(), - )};`; + const componentFiltersString = `Promise.resolve(${JSON.stringify( + getSavedComponentFilters(), + )})`; response.end( - savedPreferencesString + + backendFile.toString() + '\n;' + - backendFile.toString() + - '\n;' + - 'ReactDevToolsBackend.initialize();' + + `ReactDevToolsBackend.initialize(${componentFiltersString});` + '\n' + `ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${ useHttps ? 'true' : 'false' diff --git a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js index 196369e71d9..00c9b905a66 100644 --- a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js +++ b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js @@ -5,9 +5,15 @@ // This is the only purpose of this script - to send persisted settings to installHook.js content script import type {UnknownMessageEvent} from './messages'; -import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; +import type { + DevToolsHookSettings, + DevToolsSettings, +} from 'react-devtools-shared/src/backend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {postMessage} from './messages'; +import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils'; + async function messageListener(event: UnknownMessageEvent) { if (event.source !== window) { return; @@ -15,7 +21,7 @@ async function messageListener(event: UnknownMessageEvent) { if (event.data.source === 'react-devtools-hook-installer') { if (event.data.payload.handshake) { - const settings: Partial = + const settings: Partial = await chrome.storage.local.get(); // If storage was empty (first installation), define default settings const hookSettings: DevToolsHookSettings = { @@ -41,10 +47,15 @@ async function messageListener(event: UnknownMessageEvent) { ? settings.disableSecondConsoleLogDimmingInStrictMode : false, }; + const componentFilters: Array = Array.isArray( + settings.componentFilters, + ) + ? settings.componentFilters + : getDefaultComponentFilters(); postMessage({ - source: 'react-devtools-hook-settings-injector', - payload: {settings: hookSettings}, + source: 'react-devtools-settings-injector', + payload: {hookSettings, componentFilters}, }); window.removeEventListener('message', messageListener); @@ -54,6 +65,6 @@ async function messageListener(event: UnknownMessageEvent) { window.addEventListener('message', messageListener); postMessage({ - source: 'react-devtools-hook-settings-injector', + source: 'react-devtools-settings-injector', payload: {handshake: true}, }); diff --git a/packages/react-devtools-extensions/src/contentScripts/installHook.js b/packages/react-devtools-extensions/src/contentScripts/installHook.js index 8f30d6f0892..490232baf86 100644 --- a/packages/react-devtools-extensions/src/contentScripts/installHook.js +++ b/packages/react-devtools-extensions/src/contentScripts/installHook.js @@ -2,6 +2,7 @@ import type {UnknownMessageEvent} from './messages'; import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {installHook} from 'react-devtools-shared/src/hook'; import { @@ -11,13 +12,14 @@ import { import {postMessage} from './messages'; let resolveHookSettingsInjection: (settings: DevToolsHookSettings) => void; +let resolveComponentFiltersInjection: (filters: Array) => void; function messageListener(event: UnknownMessageEvent) { if (event.source !== window) { return; } - if (event.data.source === 'react-devtools-hook-settings-injector') { + if (event.data.source === 'react-devtools-settings-injector') { const payload = event.data.payload; // In case handshake message was sent prior to hookSettingsInjector execution // We can't guarantee order @@ -26,9 +28,10 @@ function messageListener(event: UnknownMessageEvent) { source: 'react-devtools-hook-installer', payload: {handshake: true}, }); - } else if (payload.settings) { + } else if (payload.hookSettings) { window.removeEventListener('message', messageListener); - resolveHookSettingsInjection(payload.settings); + resolveHookSettingsInjection(payload.hookSettings); + resolveComponentFiltersInjection(payload.componentFilters); } } } @@ -38,6 +41,11 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { const hookSettingsPromise = new Promise(resolve => { resolveHookSettingsInjection = resolve; }); + const componentFiltersPromise = new Promise>( + resolve => { + resolveComponentFiltersInjection = resolve; + }, + ); window.addEventListener('message', messageListener); postMessage({ @@ -50,6 +58,7 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { // Can't delay hook installation, inject settings lazily installHook( window, + componentFiltersPromise, hookSettingsPromise, shouldStartProfiling, profilingSettings, diff --git a/packages/react-devtools-extensions/src/contentScripts/messages.js b/packages/react-devtools-extensions/src/contentScripts/messages.js index e65d46b4b26..b93a3f0df86 100644 --- a/packages/react-devtools-extensions/src/contentScripts/messages.js +++ b/packages/react-devtools-extensions/src/contentScripts/messages.js @@ -1,6 +1,7 @@ /** @flow */ import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; export function postMessage(event: UnknownMessageEventData): void { window.postMessage(event); @@ -10,7 +11,7 @@ export interface UnknownMessageEvent extends MessageEvent {} export type UnknownMessageEventData = - | HookSettingsInjectorEventData + | SettingsInjectorEventData | HookInstallerEventData; export type HookInstallerEventData = { @@ -24,19 +25,20 @@ export type HookInstallerEventPayloadHandshake = { handshake: true, }; -export type HookSettingsInjectorEventData = { - source: 'react-devtools-hook-settings-injector', - payload: HookSettingsInjectorEventPayload, +export type SettingsInjectorEventData = { + source: 'react-devtools-settings-injector', + payload: SettingsInjectorEventPayload, }; -export type HookSettingsInjectorEventPayload = - | HookSettingsInjectorEventPayloadHandshake - | HookSettingsInjectorEventPayloadSettings; +export type SettingsInjectorEventPayload = + | SettingsInjectorEventPayloadHandshake + | SettingsInjectorEventPayloadSettings; -export type HookSettingsInjectorEventPayloadHandshake = { +export type SettingsInjectorEventPayloadHandshake = { handshake: true, }; -export type HookSettingsInjectorEventPayloadSettings = { - settings: DevToolsHookSettings, +export type SettingsInjectorEventPayloadSettings = { + hookSettings: DevToolsHookSettings, + componentFilters: Array, }; diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index c2616cf04a1..75a81c92ddc 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -171,8 +171,8 @@ function createBridgeAndStore() { createSuspensePanel(); }); - store.addListener('settingsUpdated', settings => { - chrome.storage.local.set(settings); + store.addListener('settingsUpdated', (hookSettings, componentFilters) => { + chrome.storage.local.set({...hookSettings, componentFilters}); }); if (!isProfiling) { diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js index 2dd03417121..9810d5b39cc 100644 --- a/packages/react-devtools-inline/src/backend.js +++ b/packages/react-devtools-inline/src/backend.js @@ -10,7 +10,10 @@ import type { BackendBridge, SavedPreferencesParams, } from 'react-devtools-shared/src/bridge'; -import type {Wall} from 'react-devtools-shared/src/frontend/types'; +import type { + ComponentFilter, + Wall, +} from 'react-devtools-shared/src/frontend/types'; import { getIfReloadedAndProfiling, getIsReloadAndProfileSupported, @@ -18,6 +21,11 @@ import { onReloadAndProfileFlagsReset, } from 'react-devtools-shared/src/utils'; +let resolveComponentFiltersInjection: (filters: Array) => void; +const componentFiltersPromise = new Promise>(resolve => { + resolveComponentFiltersInjection = resolve; +}); + function startActivation(contentWindow: any, bridge: BackendBridge) { const onSavedPreferences = (data: SavedPreferencesParams) => { // This is the only message we're listening for, @@ -26,21 +34,13 @@ function startActivation(contentWindow: any, bridge: BackendBridge) { const {componentFilters} = data; - contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; - - // TRICKY - // The backend entry point may be required in the context of an iframe or the parent window. - // If it's required within the parent window, store the saved values on it as well, - // since the injected renderer interface will read from window. - // Technically we don't need to store them on the contentWindow in this case, - // but it doesn't really hurt anything to store them there too. - if (contentWindow !== window) { - window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; - } - - finishActivation(contentWindow, bridge); + resolveComponentFiltersInjection(componentFilters); }; + componentFiltersPromise.then( + finishActivation.bind(null, contentWindow, bridge), + ); + bridge.addListener('savedPreferences', onSavedPreferences); // The backend may be unable to read saved preferences directly, @@ -113,5 +113,5 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge { } export function initialize(contentWindow: any): void { - installHook(contentWindow); + installHook(contentWindow, componentFiltersPromise); } diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 5774a573b31..37b07ea7c35 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -238,9 +238,8 @@ beforeEach(() => { // Initialize filters to a known good state. setSavedComponentFilters(getDefaultComponentFilters()); - global.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getDefaultComponentFilters(); - installHook(global, { + installHook(global, getDefaultComponentFilters(), { appendComponentStack: true, breakOnConsoleErrors: false, showInlineWarningsAndErrors: true, diff --git a/packages/react-devtools-shared/src/attachRenderer.js b/packages/react-devtools-shared/src/attachRenderer.js index fedf76293cb..fa230fe7eff 100644 --- a/packages/react-devtools-shared/src/attachRenderer.js +++ b/packages/react-devtools-shared/src/attachRenderer.js @@ -14,6 +14,7 @@ import type { RendererID, ProfilingSettings, } from 'react-devtools-shared/src/backend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {attach as attachFlight} from 'react-devtools-shared/src/backend/flight/renderer'; import {attach as attachFiber} from 'react-devtools-shared/src/backend/fiber/renderer'; @@ -32,6 +33,9 @@ export default function attachRenderer( global: Object, shouldStartProfilingNow: boolean, profilingSettings: ProfilingSettings, + componentFiltersOrComponentFiltersPromise: + | Array + | Promise>, ): RendererInterface | void { // only attach if the renderer is compatible with the current version of the backend if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) { @@ -58,6 +62,7 @@ export default function attachRenderer( global, shouldStartProfilingNow, profilingSettings, + componentFiltersOrComponentFiltersPromise, ); } else if (renderer.ComponentTree) { // react-dom v15 diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 892a3c6fc77..289bad6f329 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -48,13 +48,11 @@ import { deletePathInObject, getDisplayName, getWrappedDisplayName, - getDefaultComponentFilters, getInObject, getUID, renamePathInObject, setInObject, utfEncodeString, - persistableComponentFilters, } from 'react-devtools-shared/src/utils'; import { formatConsoleArgumentsToSingleString, @@ -1010,6 +1008,9 @@ export function attach( global: Object, shouldStartProfilingNow: boolean, profilingSettings: ProfilingSettings, + componentFiltersOrComponentFiltersPromise: + | Array + | Promise>, ): RendererInterface { // Newer versions of the reconciler package also specific reconciler version. // If that version number is present, use it. @@ -1516,21 +1517,12 @@ export function attach( }); } - // The renderer interface can't read saved component filters directly, - // because they are stored in localStorage within the context of the extension. - // Instead it relies on the extension to pass filters through. - if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ != null) { - const restoredComponentFilters: Array = - persistableComponentFilters(window.__REACT_DEVTOOLS_COMPONENT_FILTERS__); - applyComponentFilters(restoredComponentFilters, null); + if (Array.isArray(componentFiltersOrComponentFiltersPromise)) { + applyComponentFilters(componentFiltersOrComponentFiltersPromise, null); } else { - // Unfortunately this feature is not expected to work for React Native for now. - // It would be annoying for us to spam YellowBox warnings with unactionable stuff, - // so for now just skip this message... - //console.warn('⚛ DevTools: Could not locate saved component filters'); - - // Fallback to assuming the default filters in this case. - applyComponentFilters(getDefaultComponentFilters(), null); + componentFiltersOrComponentFiltersPromise.then(componentFilters => { + applyComponentFilters(componentFilters, null); + }); } // If necessary, we can revisit optimizing this operation. diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 00c66355b2d..a97439b7cf9 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -599,3 +599,7 @@ export type DevToolsHookSettings = { hideConsoleLogsInStrictMode: boolean, disableSecondConsoleLogDimmingInStrictMode: boolean, }; + +export type DevToolsSettings = DevToolsHookSettings & { + componentFilters: Array, +}; diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index c48d5b42fb6..6c2a328226c 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -148,7 +148,7 @@ export default class Store extends EventEmitter<{ error: [Error], hookSettings: [$ReadOnly], hostInstanceSelected: [Element['id'] | null], - settingsUpdated: [$ReadOnly], + settingsUpdated: [$ReadOnly, Array], mutated: [ [ Array, @@ -437,8 +437,22 @@ export default class Store extends EventEmitter<{ this._componentFilters = value; - // Update persisted filter preferences stored in localStorage. - setSavedComponentFilters(value); + // Update persisted filter preferences + if (this._hookSettings === null) { + // We changed filters before we got the hook settings. + // Wait for hook settings before persisting component filters to not overwrite + // persisted hook settings with defaults. + // This exists purely as a type safety check; in practice the hook settings + // should have arrived before any filter changes could be made. + const onHookSettings = (settings: $ReadOnly) => { + this._bridge.removeListener('hookSettings', onHookSettings); + this.emit('settingsUpdated', settings, value); + }; + this._bridge.addListener('hookSettings', onHookSettings); + this._bridge.send('getHookSettings'); + } else { + this.emit('settingsUpdated', this._hookSettings, value); + } // Notify the renderer that filter preferences have changed. // This is an expensive operation; it unmounts and remounts the entire tree, @@ -2419,7 +2433,7 @@ export default class Store extends EventEmitter<{ this._hookSettings = settings; this._bridge.send('updateHookSettings', settings); - this.emit('settingsUpdated', settings); + this.emit('settingsUpdated', settings, this._componentFilters); }; onHookSettings: (settings: $ReadOnly) => void = diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 97f8230b6a2..5764a8bee4b 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -18,6 +18,7 @@ import type { DevToolsHookSettings, ProfilingSettings, } from './backend/types'; +import type {ComponentFilter} from './frontend/types'; import { FIREFOX_CONSOLE_DIMMING_COLOR, @@ -57,6 +58,9 @@ const defaultProfilingSettings: ProfilingSettings = { export function installHook( target: any, + componentFiltersOrComponentFiltersPromise: + | Array + | Promise>, maybeSettingsOrSettingsPromise?: | DevToolsHookSettings | Promise, @@ -224,6 +228,7 @@ export function installHook( target, isProfiling, profilingSettings, + componentFiltersOrComponentFiltersPromise, ); if (rendererInterface != null) { hook.rendererInterfaces.set(id, rendererInterface); From f34c5b6a44d722ef540ac3c34f16732e202824c2 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Wed, 21 Jan 2026 19:05:10 +0100 Subject: [PATCH 2/4] Use same approach as react-devtools-inline instead --- packages/react-devtools-core/README.md | 21 ++----------- packages/react-devtools-core/src/backend.js | 25 +++++++++++----- .../react-devtools-core/src/standalone.js | 13 ++++---- .../src/contentScripts/backendManager.js | 19 ++++++++++++ .../contentScripts/hookSettingsInjector.js | 21 ++++--------- .../src/contentScripts/installHook.js | 15 ++-------- .../src/contentScripts/messages.js | 22 +++++++------- .../src/main/index.js | 22 ++++++++++++-- packages/react-devtools-inline/src/backend.js | 30 +++++++++---------- .../src/__tests__/setupTests.js | 3 +- .../src/attachRenderer.js | 5 ---- .../src/backend/fiber/renderer.js | 24 ++++++++++----- .../src/backend/types.js | 4 --- .../src/devtools/store.js | 22 +++----------- packages/react-devtools-shared/src/hook.js | 5 ---- 15 files changed, 122 insertions(+), 129 deletions(-) diff --git a/packages/react-devtools-core/README.md b/packages/react-devtools-core/README.md index a42e569697f..21f4f697cf3 100644 --- a/packages/react-devtools-core/README.md +++ b/packages/react-devtools-core/README.md @@ -25,30 +25,15 @@ if (process.env.NODE_ENV !== 'production') { > **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. | -| `shouldStartProfilingNow` | Optional. Whether to start profiling immediately after installing the hook. Defaults to `false`. | -| `profilingSettings` | Optional. Profiling settings used when `shouldStartProfilingNow` is `true`. Defaults to `{ recordChangeDescriptions: false, recordTimeline: false }`. | -| `componentFilters` | Optional. Array or Promise that resolves to an array of component filters to apply before DevTools connects. Defaults to the built-in host component filter. See [Component filters](#component-filters) for the full spec. | +| 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 | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
{
appendComponentStack: boolean,
breakOnConsoleErrors: boolean,
showInlineWarningsAndErrors: boolean,
hideConsoleLogsInStrictMode: boolean,
disableSecondConsoleLogDimmingInStrictMode: boolean
}
|
{
appendComponentStack: true,
breakOnConsoleErrors: false,
showInlineWarningsAndErrors: true,
hideConsoleLogsInStrictMode: false,
disableSecondConsoleLogDimmingInStrictMode: false
}
| -#### Component filters -Each filter object must include `type` and `isEnabled`. Some filters also require `value` or `isValid`. - -| Type | Required fields | Description | -|------|-----------------|-------------| -| `ComponentFilterElementType` (`1`) | `type`, `isEnabled`, `value: ElementType` | Hides elements of the given element type. DevTools defaults to hiding host components. | -| `ComponentFilterDisplayName` (`2`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose display name matches the provided RegExp string. | -| `ComponentFilterLocation` (`3`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose source location matches the provided RegExp string. | -| `ComponentFilterHOC` (`4`) | `type`, `isEnabled`, `isValid` | Hides higher-order components. | -| `ComponentFilterEnvironmentName` (`5`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose environment name matches the provided string. | -| `ComponentFilterActivitySlice` (`6`) | `type`, `isEnabled`, `isValid`, `activityID`, `rendererID` | Filters activity slices; usually managed by DevTools rather than user code. | - ### `connectToDevTools` options | Prop | Default | Description | |------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------| diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 262b3bc0412..4cca0887c57 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -66,17 +66,9 @@ export function initialize( | Promise, shouldStartProfilingNow: boolean = false, profilingSettings?: ProfilingSettings, - maybeComponentFiltersOrComponentFiltersPromise?: - | Array - | Promise>, ) { - const componentFiltersOrComponentFiltersPromise = - maybeComponentFiltersOrComponentFiltersPromise - ? maybeComponentFiltersOrComponentFiltersPromise - : savedComponentFilters; installHook( window, - componentFiltersOrComponentFiltersPromise, maybeSettingsOrSettingsPromise, shouldStartProfilingNow, profilingSettings, @@ -182,6 +174,19 @@ export function connectToDevTools(options: ?ConnectOptions) { }, ); + // The renderer interface doesn't read saved component filters directly, + // because they are generally stored in localStorage within the context of the extension. + // Because of this it relies on the extension to pass filters. + // In the case of the standalone DevTools being used with a website, + // saved filters are injected along with the backend script tag so we shouldn't override them here. + // This injection strategy doesn't work for React Native though. + // Ideally the backend would save the filters itself, but RN doesn't provide a sync storage solution. + // So for now we just fall back to using the default filters... + if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) { + // $FlowFixMe[incompatible-use] found when upgrading Flow + bridge.send('overrideComponentFilters', savedComponentFilters); + } + // TODO (npm-packages) Warn if "isBackendStorageAPISupported" // $FlowFixMe[incompatible-call] found when upgrading Flow const agent = new Agent(bridge, isProfiling, onReloadAndProfile); @@ -376,6 +381,10 @@ export function connectWithCustomMessagingProtocol({ }, ); + if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) { + bridge.send('overrideComponentFilters', savedComponentFilters); + } + const agent = new Agent(bridge, isProfiling, onReloadAndProfile); if (typeof onReloadAndProfileFlagsReset === 'function') { onReloadAndProfileFlagsReset(); diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index cbe3d94e568..f8286783a9b 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -356,14 +356,17 @@ function startServer( // because they are generally stored in localStorage within the context of the extension. // Because of this it relies on the extension to pass filters, so include them wth the response here. // This will ensure that saved filters are shared across different web pages. - const componentFiltersString = `Promise.resolve(${JSON.stringify( - getSavedComponentFilters(), - )})`; + const savedPreferencesString = ` + window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify( + getSavedComponentFilters(), + )};`; response.end( - backendFile.toString() + + savedPreferencesString + '\n;' + - `ReactDevToolsBackend.initialize(${componentFiltersString});` + + backendFile.toString() + + '\n;' + + 'ReactDevToolsBackend.initialize();' + '\n' + `ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${ useHttps ? 'true' : 'false' diff --git a/packages/react-devtools-extensions/src/contentScripts/backendManager.js b/packages/react-devtools-extensions/src/contentScripts/backendManager.js index 402a1378576..1a34c1aab89 100644 --- a/packages/react-devtools-extensions/src/contentScripts/backendManager.js +++ b/packages/react-devtools-extensions/src/contentScripts/backendManager.js @@ -12,6 +12,7 @@ import type { ReactRenderer, } from 'react-devtools-shared/src/backend/types'; import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils'; +import type {SavedPreferencesParams} from 'react-devtools-shared/src/bridge'; import {COMPACT_VERSION_NAME} from 'react-devtools-extensions/src/utils'; import {getIsReloadAndProfileSupported} from 'react-devtools-shared/src/utils'; import { @@ -145,6 +146,24 @@ function activateBackend(version: string, hook: DevToolsHook) { }, }); + const onSavedPreferences = (data: SavedPreferencesParams) => { + // This is the only message we're listening for, + // so it's safe to cleanup after we've received it. + bridge.removeListener('savedPreferences', onSavedPreferences); + + const {componentFilters} = data; + + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const rendererInterface of hook.rendererInterfaces.values()) { + rendererInterface.updateComponentFilters(componentFilters); + } + }; + bridge.addListener('savedPreferences', onSavedPreferences); + // The backend is unable to read saved preferences directly, + // because they are stored in localStorage within the context of the extension (on the frontend). + // Instead it relies on the extension to pass preferences through. + bridge.send('getSavedPreferences'); + const agent = new Agent( bridge, getIfReloadedAndProfiling(), diff --git a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js index 00c9b905a66..196369e71d9 100644 --- a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js +++ b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js @@ -5,15 +5,9 @@ // This is the only purpose of this script - to send persisted settings to installHook.js content script import type {UnknownMessageEvent} from './messages'; -import type { - DevToolsHookSettings, - DevToolsSettings, -} from 'react-devtools-shared/src/backend/types'; -import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; +import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; import {postMessage} from './messages'; -import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils'; - async function messageListener(event: UnknownMessageEvent) { if (event.source !== window) { return; @@ -21,7 +15,7 @@ async function messageListener(event: UnknownMessageEvent) { if (event.data.source === 'react-devtools-hook-installer') { if (event.data.payload.handshake) { - const settings: Partial = + const settings: Partial = await chrome.storage.local.get(); // If storage was empty (first installation), define default settings const hookSettings: DevToolsHookSettings = { @@ -47,15 +41,10 @@ async function messageListener(event: UnknownMessageEvent) { ? settings.disableSecondConsoleLogDimmingInStrictMode : false, }; - const componentFilters: Array = Array.isArray( - settings.componentFilters, - ) - ? settings.componentFilters - : getDefaultComponentFilters(); postMessage({ - source: 'react-devtools-settings-injector', - payload: {hookSettings, componentFilters}, + source: 'react-devtools-hook-settings-injector', + payload: {settings: hookSettings}, }); window.removeEventListener('message', messageListener); @@ -65,6 +54,6 @@ async function messageListener(event: UnknownMessageEvent) { window.addEventListener('message', messageListener); postMessage({ - source: 'react-devtools-settings-injector', + source: 'react-devtools-hook-settings-injector', payload: {handshake: true}, }); diff --git a/packages/react-devtools-extensions/src/contentScripts/installHook.js b/packages/react-devtools-extensions/src/contentScripts/installHook.js index 490232baf86..8f30d6f0892 100644 --- a/packages/react-devtools-extensions/src/contentScripts/installHook.js +++ b/packages/react-devtools-extensions/src/contentScripts/installHook.js @@ -2,7 +2,6 @@ import type {UnknownMessageEvent} from './messages'; import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; -import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {installHook} from 'react-devtools-shared/src/hook'; import { @@ -12,14 +11,13 @@ import { import {postMessage} from './messages'; let resolveHookSettingsInjection: (settings: DevToolsHookSettings) => void; -let resolveComponentFiltersInjection: (filters: Array) => void; function messageListener(event: UnknownMessageEvent) { if (event.source !== window) { return; } - if (event.data.source === 'react-devtools-settings-injector') { + if (event.data.source === 'react-devtools-hook-settings-injector') { const payload = event.data.payload; // In case handshake message was sent prior to hookSettingsInjector execution // We can't guarantee order @@ -28,10 +26,9 @@ function messageListener(event: UnknownMessageEvent) { source: 'react-devtools-hook-installer', payload: {handshake: true}, }); - } else if (payload.hookSettings) { + } else if (payload.settings) { window.removeEventListener('message', messageListener); - resolveHookSettingsInjection(payload.hookSettings); - resolveComponentFiltersInjection(payload.componentFilters); + resolveHookSettingsInjection(payload.settings); } } } @@ -41,11 +38,6 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { const hookSettingsPromise = new Promise(resolve => { resolveHookSettingsInjection = resolve; }); - const componentFiltersPromise = new Promise>( - resolve => { - resolveComponentFiltersInjection = resolve; - }, - ); window.addEventListener('message', messageListener); postMessage({ @@ -58,7 +50,6 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { // Can't delay hook installation, inject settings lazily installHook( window, - componentFiltersPromise, hookSettingsPromise, shouldStartProfiling, profilingSettings, diff --git a/packages/react-devtools-extensions/src/contentScripts/messages.js b/packages/react-devtools-extensions/src/contentScripts/messages.js index b93a3f0df86..e65d46b4b26 100644 --- a/packages/react-devtools-extensions/src/contentScripts/messages.js +++ b/packages/react-devtools-extensions/src/contentScripts/messages.js @@ -1,7 +1,6 @@ /** @flow */ import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; -import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; export function postMessage(event: UnknownMessageEventData): void { window.postMessage(event); @@ -11,7 +10,7 @@ export interface UnknownMessageEvent extends MessageEvent {} export type UnknownMessageEventData = - | SettingsInjectorEventData + | HookSettingsInjectorEventData | HookInstallerEventData; export type HookInstallerEventData = { @@ -25,20 +24,19 @@ export type HookInstallerEventPayloadHandshake = { handshake: true, }; -export type SettingsInjectorEventData = { - source: 'react-devtools-settings-injector', - payload: SettingsInjectorEventPayload, +export type HookSettingsInjectorEventData = { + source: 'react-devtools-hook-settings-injector', + payload: HookSettingsInjectorEventPayload, }; -export type SettingsInjectorEventPayload = - | SettingsInjectorEventPayloadHandshake - | SettingsInjectorEventPayloadSettings; +export type HookSettingsInjectorEventPayload = + | HookSettingsInjectorEventPayloadHandshake + | HookSettingsInjectorEventPayloadSettings; -export type SettingsInjectorEventPayloadHandshake = { +export type HookSettingsInjectorEventPayloadHandshake = { handshake: true, }; -export type SettingsInjectorEventPayloadSettings = { - hookSettings: DevToolsHookSettings, - componentFilters: Array, +export type HookSettingsInjectorEventPayloadSettings = { + settings: DevToolsHookSettings, }; diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 75a81c92ddc..8f2eedaa26b 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -29,6 +29,7 @@ import {logEvent} from 'react-devtools-shared/src/Logger'; import { getAlwaysOpenInEditor, getOpenInEditorURL, + getSavedComponentFilters, normalizeUrlIfValid, } from 'react-devtools-shared/src/utils'; import {checkConditions} from 'react-devtools-shared/src/devtools/views/Editor/utils'; @@ -77,6 +78,23 @@ function createBridge() { }, }); + const onGetSavedPreferences = () => { + // This is the only message we're listening for, + // so it's safe to cleanup after we've received it. + bridge.removeListener('getSavedPreferences', onGetSavedPreferences); + + const data = { + componentFilters: getSavedComponentFilters(), + }; + + // The renderer interface can't read saved preferences directly, + // because they are stored in localStorage within the context of the extension. + // Instead it relies on the extension to pass them through. + bridge.send('savedPreferences', data); + }; + + bridge.addListener('getSavedPreferences', onGetSavedPreferences); + bridge.addListener('reloadAppForProfiling', () => { localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); evalInInspectedWindow('reload', [], () => {}); @@ -171,8 +189,8 @@ function createBridgeAndStore() { createSuspensePanel(); }); - store.addListener('settingsUpdated', (hookSettings, componentFilters) => { - chrome.storage.local.set({...hookSettings, componentFilters}); + store.addListener('settingsUpdated', settings => { + chrome.storage.local.set(settings); }); if (!isProfiling) { diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js index 9810d5b39cc..2dd03417121 100644 --- a/packages/react-devtools-inline/src/backend.js +++ b/packages/react-devtools-inline/src/backend.js @@ -10,10 +10,7 @@ import type { BackendBridge, SavedPreferencesParams, } from 'react-devtools-shared/src/bridge'; -import type { - ComponentFilter, - Wall, -} from 'react-devtools-shared/src/frontend/types'; +import type {Wall} from 'react-devtools-shared/src/frontend/types'; import { getIfReloadedAndProfiling, getIsReloadAndProfileSupported, @@ -21,11 +18,6 @@ import { onReloadAndProfileFlagsReset, } from 'react-devtools-shared/src/utils'; -let resolveComponentFiltersInjection: (filters: Array) => void; -const componentFiltersPromise = new Promise>(resolve => { - resolveComponentFiltersInjection = resolve; -}); - function startActivation(contentWindow: any, bridge: BackendBridge) { const onSavedPreferences = (data: SavedPreferencesParams) => { // This is the only message we're listening for, @@ -34,12 +26,20 @@ function startActivation(contentWindow: any, bridge: BackendBridge) { const {componentFilters} = data; - resolveComponentFiltersInjection(componentFilters); - }; + contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; - componentFiltersPromise.then( - finishActivation.bind(null, contentWindow, bridge), - ); + // TRICKY + // The backend entry point may be required in the context of an iframe or the parent window. + // If it's required within the parent window, store the saved values on it as well, + // since the injected renderer interface will read from window. + // Technically we don't need to store them on the contentWindow in this case, + // but it doesn't really hurt anything to store them there too. + if (contentWindow !== window) { + window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; + } + + finishActivation(contentWindow, bridge); + }; bridge.addListener('savedPreferences', onSavedPreferences); @@ -113,5 +113,5 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge { } export function initialize(contentWindow: any): void { - installHook(contentWindow, componentFiltersPromise); + installHook(contentWindow); } diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 37b07ea7c35..5774a573b31 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -238,8 +238,9 @@ beforeEach(() => { // Initialize filters to a known good state. setSavedComponentFilters(getDefaultComponentFilters()); + global.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getDefaultComponentFilters(); - installHook(global, getDefaultComponentFilters(), { + installHook(global, { appendComponentStack: true, breakOnConsoleErrors: false, showInlineWarningsAndErrors: true, diff --git a/packages/react-devtools-shared/src/attachRenderer.js b/packages/react-devtools-shared/src/attachRenderer.js index fa230fe7eff..fedf76293cb 100644 --- a/packages/react-devtools-shared/src/attachRenderer.js +++ b/packages/react-devtools-shared/src/attachRenderer.js @@ -14,7 +14,6 @@ import type { RendererID, ProfilingSettings, } from 'react-devtools-shared/src/backend/types'; -import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {attach as attachFlight} from 'react-devtools-shared/src/backend/flight/renderer'; import {attach as attachFiber} from 'react-devtools-shared/src/backend/fiber/renderer'; @@ -33,9 +32,6 @@ export default function attachRenderer( global: Object, shouldStartProfilingNow: boolean, profilingSettings: ProfilingSettings, - componentFiltersOrComponentFiltersPromise: - | Array - | Promise>, ): RendererInterface | void { // only attach if the renderer is compatible with the current version of the backend if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) { @@ -62,7 +58,6 @@ export default function attachRenderer( global, shouldStartProfilingNow, profilingSettings, - componentFiltersOrComponentFiltersPromise, ); } else if (renderer.ComponentTree) { // react-dom v15 diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 289bad6f329..892a3c6fc77 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -48,11 +48,13 @@ import { deletePathInObject, getDisplayName, getWrappedDisplayName, + getDefaultComponentFilters, getInObject, getUID, renamePathInObject, setInObject, utfEncodeString, + persistableComponentFilters, } from 'react-devtools-shared/src/utils'; import { formatConsoleArgumentsToSingleString, @@ -1008,9 +1010,6 @@ export function attach( global: Object, shouldStartProfilingNow: boolean, profilingSettings: ProfilingSettings, - componentFiltersOrComponentFiltersPromise: - | Array - | Promise>, ): RendererInterface { // Newer versions of the reconciler package also specific reconciler version. // If that version number is present, use it. @@ -1517,12 +1516,21 @@ export function attach( }); } - if (Array.isArray(componentFiltersOrComponentFiltersPromise)) { - applyComponentFilters(componentFiltersOrComponentFiltersPromise, null); + // The renderer interface can't read saved component filters directly, + // because they are stored in localStorage within the context of the extension. + // Instead it relies on the extension to pass filters through. + if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ != null) { + const restoredComponentFilters: Array = + persistableComponentFilters(window.__REACT_DEVTOOLS_COMPONENT_FILTERS__); + applyComponentFilters(restoredComponentFilters, null); } else { - componentFiltersOrComponentFiltersPromise.then(componentFilters => { - applyComponentFilters(componentFilters, null); - }); + // Unfortunately this feature is not expected to work for React Native for now. + // It would be annoying for us to spam YellowBox warnings with unactionable stuff, + // so for now just skip this message... + //console.warn('⚛ DevTools: Could not locate saved component filters'); + + // Fallback to assuming the default filters in this case. + applyComponentFilters(getDefaultComponentFilters(), null); } // If necessary, we can revisit optimizing this operation. diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index a97439b7cf9..00c66355b2d 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -599,7 +599,3 @@ export type DevToolsHookSettings = { hideConsoleLogsInStrictMode: boolean, disableSecondConsoleLogDimmingInStrictMode: boolean, }; - -export type DevToolsSettings = DevToolsHookSettings & { - componentFilters: Array, -}; diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 6c2a328226c..c48d5b42fb6 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -148,7 +148,7 @@ export default class Store extends EventEmitter<{ error: [Error], hookSettings: [$ReadOnly], hostInstanceSelected: [Element['id'] | null], - settingsUpdated: [$ReadOnly, Array], + settingsUpdated: [$ReadOnly], mutated: [ [ Array, @@ -437,22 +437,8 @@ export default class Store extends EventEmitter<{ this._componentFilters = value; - // Update persisted filter preferences - if (this._hookSettings === null) { - // We changed filters before we got the hook settings. - // Wait for hook settings before persisting component filters to not overwrite - // persisted hook settings with defaults. - // This exists purely as a type safety check; in practice the hook settings - // should have arrived before any filter changes could be made. - const onHookSettings = (settings: $ReadOnly) => { - this._bridge.removeListener('hookSettings', onHookSettings); - this.emit('settingsUpdated', settings, value); - }; - this._bridge.addListener('hookSettings', onHookSettings); - this._bridge.send('getHookSettings'); - } else { - this.emit('settingsUpdated', this._hookSettings, value); - } + // Update persisted filter preferences stored in localStorage. + setSavedComponentFilters(value); // Notify the renderer that filter preferences have changed. // This is an expensive operation; it unmounts and remounts the entire tree, @@ -2433,7 +2419,7 @@ export default class Store extends EventEmitter<{ this._hookSettings = settings; this._bridge.send('updateHookSettings', settings); - this.emit('settingsUpdated', settings, this._componentFilters); + this.emit('settingsUpdated', settings); }; onHookSettings: (settings: $ReadOnly) => void = diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 5764a8bee4b..97f8230b6a2 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -18,7 +18,6 @@ import type { DevToolsHookSettings, ProfilingSettings, } from './backend/types'; -import type {ComponentFilter} from './frontend/types'; import { FIREFOX_CONSOLE_DIMMING_COLOR, @@ -58,9 +57,6 @@ const defaultProfilingSettings: ProfilingSettings = { export function installHook( target: any, - componentFiltersOrComponentFiltersPromise: - | Array - | Promise>, maybeSettingsOrSettingsPromise?: | DevToolsHookSettings | Promise, @@ -228,7 +224,6 @@ export function installHook( target, isProfiling, profilingSettings, - componentFiltersOrComponentFiltersPromise, ); if (rendererInterface != null) { hook.rendererInterfaces.set(id, rendererInterface); From d0b6858d3a05856b7a48aab728ff30daf844c574 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Wed, 21 Jan 2026 19:31:33 +0100 Subject: [PATCH 3/4] Go back to using chrome.storage.local in the extension Using the localStorage from the frontend has two downsides: - it comes in while we're profiling when we don't support updating filters - it comes in after the initial mount i.e. we always reconcile the full tree twice on initial load Storing it in the backend means data might desync but that complexity is worth avoiding the two issues above. If we get synchronization issues in practice we can revisit. --- packages/react-devtools-core/README.md | 21 +++++++-- packages/react-devtools-core/src/backend.js | 25 ++++------- .../react-devtools-core/src/standalone.js | 13 +++--- .../src/contentScripts/backendManager.js | 19 -------- .../contentScripts/hookSettingsInjector.js | 21 ++++++--- .../src/contentScripts/installHook.js | 15 +++++-- .../src/contentScripts/messages.js | 22 +++++----- .../src/main/index.js | 22 +--------- packages/react-devtools-inline/src/backend.js | 30 ++++++------- .../src/__tests__/setupTests.js | 3 +- .../src/attachRenderer.js | 5 +++ .../src/backend/fiber/renderer.js | 24 ++++------- .../src/backend/types.js | 4 ++ packages/react-devtools-shared/src/bridge.js | 1 - .../src/devtools/store.js | 43 ++++++++----------- packages/react-devtools-shared/src/hook.js | 5 +++ 16 files changed, 129 insertions(+), 144 deletions(-) diff --git a/packages/react-devtools-core/README.md b/packages/react-devtools-core/README.md index 21f4f697cf3..a42e569697f 100644 --- a/packages/react-devtools-core/README.md +++ b/packages/react-devtools-core/README.md @@ -25,15 +25,30 @@ if (process.env.NODE_ENV !== 'production') { > **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. | +| 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. | +| `shouldStartProfilingNow` | Optional. Whether to start profiling immediately after installing the hook. Defaults to `false`. | +| `profilingSettings` | Optional. Profiling settings used when `shouldStartProfilingNow` is `true`. Defaults to `{ recordChangeDescriptions: false, recordTimeline: false }`. | +| `componentFilters` | Optional. Array or Promise that resolves to an array of component filters to apply before DevTools connects. Defaults to the built-in host component filter. See [Component filters](#component-filters) for the full spec. | #### `Settings` | Spec | Default value | |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| |
{
appendComponentStack: boolean,
breakOnConsoleErrors: boolean,
showInlineWarningsAndErrors: boolean,
hideConsoleLogsInStrictMode: boolean,
disableSecondConsoleLogDimmingInStrictMode: boolean
}
|
{
appendComponentStack: true,
breakOnConsoleErrors: false,
showInlineWarningsAndErrors: true,
hideConsoleLogsInStrictMode: false,
disableSecondConsoleLogDimmingInStrictMode: false
}
| +#### Component filters +Each filter object must include `type` and `isEnabled`. Some filters also require `value` or `isValid`. + +| Type | Required fields | Description | +|------|-----------------|-------------| +| `ComponentFilterElementType` (`1`) | `type`, `isEnabled`, `value: ElementType` | Hides elements of the given element type. DevTools defaults to hiding host components. | +| `ComponentFilterDisplayName` (`2`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose display name matches the provided RegExp string. | +| `ComponentFilterLocation` (`3`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose source location matches the provided RegExp string. | +| `ComponentFilterHOC` (`4`) | `type`, `isEnabled`, `isValid` | Hides higher-order components. | +| `ComponentFilterEnvironmentName` (`5`) | `type`, `isEnabled`, `isValid`, `value: string` | Hides components whose environment name matches the provided string. | +| `ComponentFilterActivitySlice` (`6`) | `type`, `isEnabled`, `isValid`, `activityID`, `rendererID` | Filters activity slices; usually managed by DevTools rather than user code. | + ### `connectToDevTools` options | Prop | Default | Description | |------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------| diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 4cca0887c57..262b3bc0412 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -66,9 +66,17 @@ export function initialize( | Promise, shouldStartProfilingNow: boolean = false, profilingSettings?: ProfilingSettings, + maybeComponentFiltersOrComponentFiltersPromise?: + | Array + | Promise>, ) { + const componentFiltersOrComponentFiltersPromise = + maybeComponentFiltersOrComponentFiltersPromise + ? maybeComponentFiltersOrComponentFiltersPromise + : savedComponentFilters; installHook( window, + componentFiltersOrComponentFiltersPromise, maybeSettingsOrSettingsPromise, shouldStartProfilingNow, profilingSettings, @@ -174,19 +182,6 @@ export function connectToDevTools(options: ?ConnectOptions) { }, ); - // The renderer interface doesn't read saved component filters directly, - // because they are generally stored in localStorage within the context of the extension. - // Because of this it relies on the extension to pass filters. - // In the case of the standalone DevTools being used with a website, - // saved filters are injected along with the backend script tag so we shouldn't override them here. - // This injection strategy doesn't work for React Native though. - // Ideally the backend would save the filters itself, but RN doesn't provide a sync storage solution. - // So for now we just fall back to using the default filters... - if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) { - // $FlowFixMe[incompatible-use] found when upgrading Flow - bridge.send('overrideComponentFilters', savedComponentFilters); - } - // TODO (npm-packages) Warn if "isBackendStorageAPISupported" // $FlowFixMe[incompatible-call] found when upgrading Flow const agent = new Agent(bridge, isProfiling, onReloadAndProfile); @@ -381,10 +376,6 @@ export function connectWithCustomMessagingProtocol({ }, ); - if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) { - bridge.send('overrideComponentFilters', savedComponentFilters); - } - const agent = new Agent(bridge, isProfiling, onReloadAndProfile); if (typeof onReloadAndProfileFlagsReset === 'function') { onReloadAndProfileFlagsReset(); diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index f8286783a9b..cbe3d94e568 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -356,17 +356,14 @@ function startServer( // because they are generally stored in localStorage within the context of the extension. // Because of this it relies on the extension to pass filters, so include them wth the response here. // This will ensure that saved filters are shared across different web pages. - const savedPreferencesString = ` - window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify( - getSavedComponentFilters(), - )};`; + const componentFiltersString = `Promise.resolve(${JSON.stringify( + getSavedComponentFilters(), + )})`; response.end( - savedPreferencesString + + backendFile.toString() + '\n;' + - backendFile.toString() + - '\n;' + - 'ReactDevToolsBackend.initialize();' + + `ReactDevToolsBackend.initialize(${componentFiltersString});` + '\n' + `ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${ useHttps ? 'true' : 'false' diff --git a/packages/react-devtools-extensions/src/contentScripts/backendManager.js b/packages/react-devtools-extensions/src/contentScripts/backendManager.js index 1a34c1aab89..402a1378576 100644 --- a/packages/react-devtools-extensions/src/contentScripts/backendManager.js +++ b/packages/react-devtools-extensions/src/contentScripts/backendManager.js @@ -12,7 +12,6 @@ import type { ReactRenderer, } from 'react-devtools-shared/src/backend/types'; import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils'; -import type {SavedPreferencesParams} from 'react-devtools-shared/src/bridge'; import {COMPACT_VERSION_NAME} from 'react-devtools-extensions/src/utils'; import {getIsReloadAndProfileSupported} from 'react-devtools-shared/src/utils'; import { @@ -146,24 +145,6 @@ function activateBackend(version: string, hook: DevToolsHook) { }, }); - const onSavedPreferences = (data: SavedPreferencesParams) => { - // This is the only message we're listening for, - // so it's safe to cleanup after we've received it. - bridge.removeListener('savedPreferences', onSavedPreferences); - - const {componentFilters} = data; - - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const rendererInterface of hook.rendererInterfaces.values()) { - rendererInterface.updateComponentFilters(componentFilters); - } - }; - bridge.addListener('savedPreferences', onSavedPreferences); - // The backend is unable to read saved preferences directly, - // because they are stored in localStorage within the context of the extension (on the frontend). - // Instead it relies on the extension to pass preferences through. - bridge.send('getSavedPreferences'); - const agent = new Agent( bridge, getIfReloadedAndProfiling(), diff --git a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js index 196369e71d9..00c9b905a66 100644 --- a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js +++ b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js @@ -5,9 +5,15 @@ // This is the only purpose of this script - to send persisted settings to installHook.js content script import type {UnknownMessageEvent} from './messages'; -import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; +import type { + DevToolsHookSettings, + DevToolsSettings, +} from 'react-devtools-shared/src/backend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {postMessage} from './messages'; +import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils'; + async function messageListener(event: UnknownMessageEvent) { if (event.source !== window) { return; @@ -15,7 +21,7 @@ async function messageListener(event: UnknownMessageEvent) { if (event.data.source === 'react-devtools-hook-installer') { if (event.data.payload.handshake) { - const settings: Partial = + const settings: Partial = await chrome.storage.local.get(); // If storage was empty (first installation), define default settings const hookSettings: DevToolsHookSettings = { @@ -41,10 +47,15 @@ async function messageListener(event: UnknownMessageEvent) { ? settings.disableSecondConsoleLogDimmingInStrictMode : false, }; + const componentFilters: Array = Array.isArray( + settings.componentFilters, + ) + ? settings.componentFilters + : getDefaultComponentFilters(); postMessage({ - source: 'react-devtools-hook-settings-injector', - payload: {settings: hookSettings}, + source: 'react-devtools-settings-injector', + payload: {hookSettings, componentFilters}, }); window.removeEventListener('message', messageListener); @@ -54,6 +65,6 @@ async function messageListener(event: UnknownMessageEvent) { window.addEventListener('message', messageListener); postMessage({ - source: 'react-devtools-hook-settings-injector', + source: 'react-devtools-settings-injector', payload: {handshake: true}, }); diff --git a/packages/react-devtools-extensions/src/contentScripts/installHook.js b/packages/react-devtools-extensions/src/contentScripts/installHook.js index 8f30d6f0892..490232baf86 100644 --- a/packages/react-devtools-extensions/src/contentScripts/installHook.js +++ b/packages/react-devtools-extensions/src/contentScripts/installHook.js @@ -2,6 +2,7 @@ import type {UnknownMessageEvent} from './messages'; import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {installHook} from 'react-devtools-shared/src/hook'; import { @@ -11,13 +12,14 @@ import { import {postMessage} from './messages'; let resolveHookSettingsInjection: (settings: DevToolsHookSettings) => void; +let resolveComponentFiltersInjection: (filters: Array) => void; function messageListener(event: UnknownMessageEvent) { if (event.source !== window) { return; } - if (event.data.source === 'react-devtools-hook-settings-injector') { + if (event.data.source === 'react-devtools-settings-injector') { const payload = event.data.payload; // In case handshake message was sent prior to hookSettingsInjector execution // We can't guarantee order @@ -26,9 +28,10 @@ function messageListener(event: UnknownMessageEvent) { source: 'react-devtools-hook-installer', payload: {handshake: true}, }); - } else if (payload.settings) { + } else if (payload.hookSettings) { window.removeEventListener('message', messageListener); - resolveHookSettingsInjection(payload.settings); + resolveHookSettingsInjection(payload.hookSettings); + resolveComponentFiltersInjection(payload.componentFilters); } } } @@ -38,6 +41,11 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { const hookSettingsPromise = new Promise(resolve => { resolveHookSettingsInjection = resolve; }); + const componentFiltersPromise = new Promise>( + resolve => { + resolveComponentFiltersInjection = resolve; + }, + ); window.addEventListener('message', messageListener); postMessage({ @@ -50,6 +58,7 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { // Can't delay hook installation, inject settings lazily installHook( window, + componentFiltersPromise, hookSettingsPromise, shouldStartProfiling, profilingSettings, diff --git a/packages/react-devtools-extensions/src/contentScripts/messages.js b/packages/react-devtools-extensions/src/contentScripts/messages.js index e65d46b4b26..b93a3f0df86 100644 --- a/packages/react-devtools-extensions/src/contentScripts/messages.js +++ b/packages/react-devtools-extensions/src/contentScripts/messages.js @@ -1,6 +1,7 @@ /** @flow */ import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; export function postMessage(event: UnknownMessageEventData): void { window.postMessage(event); @@ -10,7 +11,7 @@ export interface UnknownMessageEvent extends MessageEvent {} export type UnknownMessageEventData = - | HookSettingsInjectorEventData + | SettingsInjectorEventData | HookInstallerEventData; export type HookInstallerEventData = { @@ -24,19 +25,20 @@ export type HookInstallerEventPayloadHandshake = { handshake: true, }; -export type HookSettingsInjectorEventData = { - source: 'react-devtools-hook-settings-injector', - payload: HookSettingsInjectorEventPayload, +export type SettingsInjectorEventData = { + source: 'react-devtools-settings-injector', + payload: SettingsInjectorEventPayload, }; -export type HookSettingsInjectorEventPayload = - | HookSettingsInjectorEventPayloadHandshake - | HookSettingsInjectorEventPayloadSettings; +export type SettingsInjectorEventPayload = + | SettingsInjectorEventPayloadHandshake + | SettingsInjectorEventPayloadSettings; -export type HookSettingsInjectorEventPayloadHandshake = { +export type SettingsInjectorEventPayloadHandshake = { handshake: true, }; -export type HookSettingsInjectorEventPayloadSettings = { - settings: DevToolsHookSettings, +export type SettingsInjectorEventPayloadSettings = { + hookSettings: DevToolsHookSettings, + componentFilters: Array, }; diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 8f2eedaa26b..75a81c92ddc 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -29,7 +29,6 @@ import {logEvent} from 'react-devtools-shared/src/Logger'; import { getAlwaysOpenInEditor, getOpenInEditorURL, - getSavedComponentFilters, normalizeUrlIfValid, } from 'react-devtools-shared/src/utils'; import {checkConditions} from 'react-devtools-shared/src/devtools/views/Editor/utils'; @@ -78,23 +77,6 @@ function createBridge() { }, }); - const onGetSavedPreferences = () => { - // This is the only message we're listening for, - // so it's safe to cleanup after we've received it. - bridge.removeListener('getSavedPreferences', onGetSavedPreferences); - - const data = { - componentFilters: getSavedComponentFilters(), - }; - - // The renderer interface can't read saved preferences directly, - // because they are stored in localStorage within the context of the extension. - // Instead it relies on the extension to pass them through. - bridge.send('savedPreferences', data); - }; - - bridge.addListener('getSavedPreferences', onGetSavedPreferences); - bridge.addListener('reloadAppForProfiling', () => { localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); evalInInspectedWindow('reload', [], () => {}); @@ -189,8 +171,8 @@ function createBridgeAndStore() { createSuspensePanel(); }); - store.addListener('settingsUpdated', settings => { - chrome.storage.local.set(settings); + store.addListener('settingsUpdated', (hookSettings, componentFilters) => { + chrome.storage.local.set({...hookSettings, componentFilters}); }); if (!isProfiling) { diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js index 2dd03417121..9810d5b39cc 100644 --- a/packages/react-devtools-inline/src/backend.js +++ b/packages/react-devtools-inline/src/backend.js @@ -10,7 +10,10 @@ import type { BackendBridge, SavedPreferencesParams, } from 'react-devtools-shared/src/bridge'; -import type {Wall} from 'react-devtools-shared/src/frontend/types'; +import type { + ComponentFilter, + Wall, +} from 'react-devtools-shared/src/frontend/types'; import { getIfReloadedAndProfiling, getIsReloadAndProfileSupported, @@ -18,6 +21,11 @@ import { onReloadAndProfileFlagsReset, } from 'react-devtools-shared/src/utils'; +let resolveComponentFiltersInjection: (filters: Array) => void; +const componentFiltersPromise = new Promise>(resolve => { + resolveComponentFiltersInjection = resolve; +}); + function startActivation(contentWindow: any, bridge: BackendBridge) { const onSavedPreferences = (data: SavedPreferencesParams) => { // This is the only message we're listening for, @@ -26,21 +34,13 @@ function startActivation(contentWindow: any, bridge: BackendBridge) { const {componentFilters} = data; - contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; - - // TRICKY - // The backend entry point may be required in the context of an iframe or the parent window. - // If it's required within the parent window, store the saved values on it as well, - // since the injected renderer interface will read from window. - // Technically we don't need to store them on the contentWindow in this case, - // but it doesn't really hurt anything to store them there too. - if (contentWindow !== window) { - window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; - } - - finishActivation(contentWindow, bridge); + resolveComponentFiltersInjection(componentFilters); }; + componentFiltersPromise.then( + finishActivation.bind(null, contentWindow, bridge), + ); + bridge.addListener('savedPreferences', onSavedPreferences); // The backend may be unable to read saved preferences directly, @@ -113,5 +113,5 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge { } export function initialize(contentWindow: any): void { - installHook(contentWindow); + installHook(contentWindow, componentFiltersPromise); } diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 5774a573b31..37b07ea7c35 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -238,9 +238,8 @@ beforeEach(() => { // Initialize filters to a known good state. setSavedComponentFilters(getDefaultComponentFilters()); - global.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getDefaultComponentFilters(); - installHook(global, { + installHook(global, getDefaultComponentFilters(), { appendComponentStack: true, breakOnConsoleErrors: false, showInlineWarningsAndErrors: true, diff --git a/packages/react-devtools-shared/src/attachRenderer.js b/packages/react-devtools-shared/src/attachRenderer.js index fedf76293cb..fa230fe7eff 100644 --- a/packages/react-devtools-shared/src/attachRenderer.js +++ b/packages/react-devtools-shared/src/attachRenderer.js @@ -14,6 +14,7 @@ import type { RendererID, ProfilingSettings, } from 'react-devtools-shared/src/backend/types'; +import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {attach as attachFlight} from 'react-devtools-shared/src/backend/flight/renderer'; import {attach as attachFiber} from 'react-devtools-shared/src/backend/fiber/renderer'; @@ -32,6 +33,9 @@ export default function attachRenderer( global: Object, shouldStartProfilingNow: boolean, profilingSettings: ProfilingSettings, + componentFiltersOrComponentFiltersPromise: + | Array + | Promise>, ): RendererInterface | void { // only attach if the renderer is compatible with the current version of the backend if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) { @@ -58,6 +62,7 @@ export default function attachRenderer( global, shouldStartProfilingNow, profilingSettings, + componentFiltersOrComponentFiltersPromise, ); } else if (renderer.ComponentTree) { // react-dom v15 diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 892a3c6fc77..289bad6f329 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -48,13 +48,11 @@ import { deletePathInObject, getDisplayName, getWrappedDisplayName, - getDefaultComponentFilters, getInObject, getUID, renamePathInObject, setInObject, utfEncodeString, - persistableComponentFilters, } from 'react-devtools-shared/src/utils'; import { formatConsoleArgumentsToSingleString, @@ -1010,6 +1008,9 @@ export function attach( global: Object, shouldStartProfilingNow: boolean, profilingSettings: ProfilingSettings, + componentFiltersOrComponentFiltersPromise: + | Array + | Promise>, ): RendererInterface { // Newer versions of the reconciler package also specific reconciler version. // If that version number is present, use it. @@ -1516,21 +1517,12 @@ export function attach( }); } - // The renderer interface can't read saved component filters directly, - // because they are stored in localStorage within the context of the extension. - // Instead it relies on the extension to pass filters through. - if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ != null) { - const restoredComponentFilters: Array = - persistableComponentFilters(window.__REACT_DEVTOOLS_COMPONENT_FILTERS__); - applyComponentFilters(restoredComponentFilters, null); + if (Array.isArray(componentFiltersOrComponentFiltersPromise)) { + applyComponentFilters(componentFiltersOrComponentFiltersPromise, null); } else { - // Unfortunately this feature is not expected to work for React Native for now. - // It would be annoying for us to spam YellowBox warnings with unactionable stuff, - // so for now just skip this message... - //console.warn('⚛ DevTools: Could not locate saved component filters'); - - // Fallback to assuming the default filters in this case. - applyComponentFilters(getDefaultComponentFilters(), null); + componentFiltersOrComponentFiltersPromise.then(componentFilters => { + applyComponentFilters(componentFilters, null); + }); } // If necessary, we can revisit optimizing this operation. diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 00c66355b2d..a97439b7cf9 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -599,3 +599,7 @@ export type DevToolsHookSettings = { hideConsoleLogsInStrictMode: boolean, disableSecondConsoleLogDimmingInStrictMode: boolean, }; + +export type DevToolsSettings = DevToolsHookSettings & { + componentFilters: Array, +}; diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index af3a87b5968..bc752ec4b8c 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -208,7 +208,6 @@ export type BackendEvents = { isReloadAndProfileSupportedByBackend: [boolean], operations: [Array], ownersList: [OwnersList], - overrideComponentFilters: [Array], environmentNames: [Array], profilingData: [ProfilingDataBackend], profilingStatus: [boolean], diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index c48d5b42fb6..9806692e6ba 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -148,7 +148,7 @@ export default class Store extends EventEmitter<{ error: [Error], hookSettings: [$ReadOnly], hostInstanceSelected: [Element['id'] | null], - settingsUpdated: [$ReadOnly], + settingsUpdated: [$ReadOnly, Array], mutated: [ [ Array, @@ -321,10 +321,6 @@ export default class Store extends EventEmitter<{ this._bridge = bridge; bridge.addListener('operations', this.onBridgeOperations); - bridge.addListener( - 'overrideComponentFilters', - this.onBridgeOverrideComponentFilters, - ); bridge.addListener('shutdown', this.onBridgeShutdown); bridge.addListener( 'isReloadAndProfileSupportedByBackend', @@ -437,8 +433,22 @@ export default class Store extends EventEmitter<{ this._componentFilters = value; - // Update persisted filter preferences stored in localStorage. - setSavedComponentFilters(value); + // Update persisted filter preferences + if (this._hookSettings === null) { + // We changed filters before we got the hook settings. + // Wait for hook settings before persisting component filters to not overwrite + // persisted hook settings with defaults. + // This exists purely as a type safety check; in practice the hook settings + // should have arrived before any filter changes could be made. + const onHookSettings = (settings: $ReadOnly) => { + this._bridge.removeListener('hookSettings', onHookSettings); + this.emit('settingsUpdated', settings, value); + }; + this._bridge.addListener('hookSettings', onHookSettings); + this._bridge.send('getHookSettings'); + } else { + this.emit('settingsUpdated', this._hookSettings, value); + } // Notify the renderer that filter preferences have changed. // This is an expensive operation; it unmounts and remounts the entire tree, @@ -2264,19 +2274,6 @@ export default class Store extends EventEmitter<{ return didMutate; } - // Certain backends save filters on a per-domain basis. - // In order to prevent filter preferences and applied filters from being out of sync, - // this message enables the backend to override the frontend's current ("saved") filters. - // This action should also override the saved filters too, - // else reloading the frontend without reloading the backend would leave things out of sync. - onBridgeOverrideComponentFilters: ( - componentFilters: Array, - ) => void = componentFilters => { - this._componentFilters = componentFilters; - - setSavedComponentFilters(componentFilters); - }; - onBridgeShutdown: () => void = () => { if (__DEBUG__) { debug('onBridgeShutdown', 'unsubscribing from Bridge'); @@ -2284,10 +2281,6 @@ export default class Store extends EventEmitter<{ const bridge = this._bridge; bridge.removeListener('operations', this.onBridgeOperations); - bridge.removeListener( - 'overrideComponentFilters', - this.onBridgeOverrideComponentFilters, - ); bridge.removeListener('shutdown', this.onBridgeShutdown); bridge.removeListener( 'isReloadAndProfileSupportedByBackend', @@ -2419,7 +2412,7 @@ export default class Store extends EventEmitter<{ this._hookSettings = settings; this._bridge.send('updateHookSettings', settings); - this.emit('settingsUpdated', settings); + this.emit('settingsUpdated', settings, this._componentFilters); }; onHookSettings: (settings: $ReadOnly) => void = diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 97f8230b6a2..5764a8bee4b 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -18,6 +18,7 @@ import type { DevToolsHookSettings, ProfilingSettings, } from './backend/types'; +import type {ComponentFilter} from './frontend/types'; import { FIREFOX_CONSOLE_DIMMING_COLOR, @@ -57,6 +58,9 @@ const defaultProfilingSettings: ProfilingSettings = { export function installHook( target: any, + componentFiltersOrComponentFiltersPromise: + | Array + | Promise>, maybeSettingsOrSettingsPromise?: | DevToolsHookSettings | Promise, @@ -224,6 +228,7 @@ export function installHook( target, isProfiling, profilingSettings, + componentFiltersOrComponentFiltersPromise, ); if (rendererInterface != null) { hook.rendererInterfaces.set(id, rendererInterface); From 43a0ec08b75925eeb0cc1c64001d9893db477a72 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Thu, 22 Jan 2026 19:37:13 +0100 Subject: [PATCH 4/4] Fix standalone version --- packages/react-devtools-core/src/standalone.js | 6 ++---- packages/react-devtools-shared/src/devtools/store.js | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index cbe3d94e568..df89a728c30 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -356,14 +356,12 @@ function startServer( // because they are generally stored in localStorage within the context of the extension. // Because of this it relies on the extension to pass filters, so include them wth the response here. // This will ensure that saved filters are shared across different web pages. - const componentFiltersString = `Promise.resolve(${JSON.stringify( - getSavedComponentFilters(), - )})`; + const componentFiltersString = JSON.stringify(getSavedComponentFilters()); response.end( backendFile.toString() + '\n;' + - `ReactDevToolsBackend.initialize(${componentFiltersString});` + + `ReactDevToolsBackend.initialize(undefined, undefined, undefined, ${componentFiltersString});` + '\n' + `ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${ useHttps ? 'true' : 'false' diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 9806692e6ba..3e2dde5fea1 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -434,6 +434,7 @@ export default class Store extends EventEmitter<{ this._componentFilters = value; // Update persisted filter preferences + setSavedComponentFilters(value); if (this._hookSettings === null) { // We changed filters before we got the hook settings. // Wait for hook settings before persisting component filters to not overwrite