From cbc4d4066333cb1a6736b93df4e0b5ff5936c849 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Fri, 16 Jan 2026 13:08:28 +0100 Subject: [PATCH 1/2] Typecheck React DevTools extension main script (#35519) --- .../src/main/index.js | 90 ++++++++++++------- packages/react-devtools-shared/src/Logger.js | 6 +- packages/react-devtools-shared/src/bridge.js | 4 +- 3 files changed, 64 insertions(+), 36 deletions(-) diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 5d7bd37ee4..bce648a5db 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -1,6 +1,14 @@ /* global chrome */ - +/** @flow */ + +import type {RootType} from 'react-dom/src/client/ReactDOMRoot'; +import type {FrontendBridge, Message} from 'react-devtools-shared/src/bridge'; +import type { + TabID, + ViewElementSource, +} from 'react-devtools-shared/src/devtools/views/DevTools'; import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane'; +import type {Element} from 'react-devtools-shared/src/frontend/types'; import {createElement} from 'react'; import {flushSync} from 'react-dom'; @@ -51,9 +59,9 @@ const hookNamesModuleLoaderFunction = () => resolvedParseHookNames; function createBridge() { bridge = new Bridge({ listen(fn) { - const bridgeListener = message => fn(message); + const bridgeListener = (message: Message) => fn(message); // Store the reference so that we unsubscribe from the same object. - const portOnMessage = port.onMessage; + const portOnMessage = ((port: any): ExtensionPort).onMessage; portOnMessage.addListener(bridgeListener); lastSubscribedBridgeListener = bridgeListener; @@ -71,7 +79,7 @@ function createBridge() { bridge.addListener('reloadAppForProfiling', () => { localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); - evalInInspectedWindow('reload', []); + evalInInspectedWindow('reload', [], () => {}); }); bridge.addListener( @@ -176,14 +184,20 @@ function createBridgeAndStore() { // Otherwise, the Store may miss important initial tree op codes. injectBackendManager(chrome.devtools.inspectedWindow.tabId); - const viewAttributeSourceFunction = (id, path) => { + const viewAttributeSourceFunction = ( + id: Element['id'], + path: Array, + ) => { const rendererID = store.getRendererIDForElement(id); if (rendererID != null) { viewAttributeSource(rendererID, id, path); } }; - const viewElementSourceFunction = (source, symbolicatedSource) => { + const viewElementSourceFunction: ViewElementSource = ( + source, + symbolicatedSource, + ) => { const [, sourceURL, line, column] = symbolicatedSource ? symbolicatedSource : source; @@ -198,7 +212,7 @@ function createBridgeAndStore() { root = createRoot(document.createElement('div')); - render = (overrideTab = mostRecentOverrideTab) => { + render = (overrideTab: TabID | null = mostRecentOverrideTab) => { mostRecentOverrideTab = overrideTab; root.render( @@ -227,7 +241,9 @@ function createBridgeAndStore() { }; } -function ensureInitialHTMLIsCleared(container) { +function ensureInitialHTMLIsCleared( + container: HTMLElement & {_hasInitialHTMLBeenCleared?: boolean}, +) { if (container._hasInitialHTMLBeenCleared) { return; } @@ -397,13 +413,6 @@ function createSourcesEditorPanel() { logEvent({event_name: 'selected-editor-pane'}); } }); - - createdPane.onShown.addListener(() => { - bridge.emit('extensionEditorPaneShown'); - }); - createdPane.onHidden.addListener(() => { - bridge.emit('extensionEditorPaneHidden'); - }); }); } @@ -479,10 +488,10 @@ function performInTabNavigationCleanup() { // Do not clean mostRecentOverrideTab on purpose, so we remember last opened // React DevTools tab, when user does in-tab navigation - store = null; - bridge = null; - render = null; - root = null; + store = (null: $FlowFixMe); + bridge = (null: $FlowFixMe); + render = (null: $FlowFixMe); + root = (null: $FlowFixMe); } function performFullCleanup() { @@ -504,18 +513,18 @@ function performFullCleanup() { componentsPortalContainer = null; profilerPortalContainer = null; suspensePortalContainer = null; - root = null; + root = (null: $FlowFixMe); mostRecentOverrideTab = null; - store = null; - bridge = null; - render = null; + store = (null: $FlowFixMe); + bridge = (null: $FlowFixMe); + render = (null: $FlowFixMe); port?.disconnect(); - port = null; + port = (null: $FlowFixMe); } -function connectExtensionPort() { +function connectExtensionPort(): void { if (port) { throw new Error('DevTools port was already connected'); } @@ -539,7 +548,7 @@ function connectExtensionPort() { // so, when we call `port.disconnect()` from this script, // this should not trigger this callback and port reconnection port.onDisconnect.addListener(() => { - port = null; + port = (null: $FlowFixMe); connectExtensionPort(); }); } @@ -593,9 +602,9 @@ function mountReactDevToolsWhenReactHasLoaded() { ); } -let bridge = null; +let bridge: FrontendBridge = (null: $FlowFixMe); let lastSubscribedBridgeListener = null; -let store = null; +let store: Store = (null: $FlowFixMe); let profilingData = null; @@ -610,13 +619,28 @@ let suspensePortalContainer = null; let editorPortalContainer = null; let inspectedElementPortalContainer = null; -let mostRecentOverrideTab = null; -let render = null; -let root = null; +let mostRecentOverrideTab: null | TabID = null; +let render: (overrideTab?: TabID) => void = (null: $FlowFixMe); +let root: RootType = (null: $FlowFixMe); let currentSelectedSource: null | SourceSelection = null; -let port = null; +type ExtensionEvent = { + addListener(callback: (message: Message, port: ExtensionPort) => void): void, + removeListener( + callback: (message: Message, port: ExtensionPort) => void, + ): void, +}; + +/** https://developer.chrome.com/docs/extensions/reference/api/runtime#type-Port */ +type ExtensionPort = { + onDisconnect: ExtensionEvent, + onMessage: ExtensionEvent, + postMessage(message: mixed, transferable?: Array): void, + disconnect(): void, +}; + +let port: ExtensionPort = (null: $FlowFixMe); // In case when multiple navigation events emitted in a short period of time // This debounced callback primarily used to avoid mounting React DevTools multiple times, which results @@ -649,7 +673,7 @@ connectExtensionPort(); mountReactDevToolsWhenReactHasLoaded(); -function onThemeChanged(themeName) { +function onThemeChanged() { // Rerender with the new theme render(); } diff --git a/packages/react-devtools-shared/src/Logger.js b/packages/react-devtools-shared/src/Logger.js index dd9dfb6202..0d1324d0d7 100644 --- a/packages/react-devtools-shared/src/Logger.js +++ b/packages/react-devtools-shared/src/Logger.js @@ -63,7 +63,11 @@ export type LoggerEvent = +value: any, ... }, - }; + } + | { + +event_name: 'selected-editor-pane', + } + | {+event_name: 'selected-inspected-element-pane'}; export type LogFunction = LoggerEvent => void | Promise; diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index b00867cc0c..af98cb8298 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -74,7 +74,7 @@ export const currentBridgeProtocol: BridgeProtocol = type ElementAndRendererID = {id: number, rendererID: RendererID}; -type Message = { +export type Message = { event: string, payload: any, }; @@ -239,7 +239,7 @@ export type BackendEvents = { type StartProfilingParams = ProfilingSettings; type ReloadAndProfilingParams = ProfilingSettings; -type FrontendEvents = { +export type FrontendEvents = { clearErrorsAndWarnings: [{rendererID: RendererID}], clearErrorsForElementID: [ElementAndRendererID], clearHostInstanceHighlight: [], From 3d6180c616e6394fff51104d40dc772567d91d3f Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Fri, 16 Jan 2026 10:12:07 +0100 Subject: [PATCH 2/2] [DevTools] Stop setting unused global variables --- packages/react-devtools-inline/src/backend.js | 36 ++++--------------- .../src/__tests__/setupTests.js | 3 -- packages/react-devtools-shared/src/bridge.js | 2 +- 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js index 849e103f21..2dd0341712 100644 --- a/packages/react-devtools-inline/src/backend.js +++ b/packages/react-devtools-inline/src/backend.js @@ -6,7 +6,10 @@ import {initBackend} from 'react-devtools-shared/src/backend'; import {installHook} from 'react-devtools-shared/src/hook'; import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; -import type {BackendBridge} from 'react-devtools-shared/src/bridge'; +import type { + BackendBridge, + SavedPreferencesParams, +} from 'react-devtools-shared/src/bridge'; import type {Wall} from 'react-devtools-shared/src/frontend/types'; import { getIfReloadedAndProfiling, @@ -16,31 +19,14 @@ import { } from 'react-devtools-shared/src/utils'; function startActivation(contentWindow: any, bridge: BackendBridge) { - const onSavedPreferences = (data: $FlowFixMe) => { + 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 { - appendComponentStack, - breakOnConsoleErrors, - componentFilters, - showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode, - disableSecondConsoleLogDimmingInStrictMode, - } = data; - - contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = - appendComponentStack; - contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = - breakOnConsoleErrors; + const {componentFilters} = data; + contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; - contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = - showInlineWarningsAndErrors; - contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = - hideConsoleLogsInStrictMode; - contentWindow.__REACT_DEVTOOLS_DISABLE_SECOND_CONSOLE_LOG_DIMMING_IN_STRICT_MODE__ = - disableSecondConsoleLogDimmingInStrictMode; // TRICKY // The backend entry point may be required in the context of an iframe or the parent window. @@ -49,15 +35,7 @@ function startActivation(contentWindow: any, bridge: BackendBridge) { // 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_APPEND_COMPONENT_STACK__ = appendComponentStack; - window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors; window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; - window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = - showInlineWarningsAndErrors; - window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = - hideConsoleLogsInStrictMode; - window.__REACT_DEVTOOLS_DISABLE_SECOND_CONSOLE_LOG_DIMMING_IN_STRICT_MODE__ = - disableSecondConsoleLogDimmingInStrictMode; } finishActivation(contentWindow, bridge); diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 6efcefe2f7..5774a573b3 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -240,9 +240,6 @@ beforeEach(() => { setSavedComponentFilters(getDefaultComponentFilters()); global.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getDefaultComponentFilters(); - // Also initialize inline warnings so that we can test them. - global.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = true; - installHook(global, { appendComponentStack: true, breakOnConsoleErrors: false, diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index af98cb8298..16d7d3a36d 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -191,7 +191,7 @@ type NativeStyleEditor_SetValueParams = { value: string, }; -type SavedPreferencesParams = { +export type SavedPreferencesParams = { componentFilters: Array, };