@samitouri / QOS-React-2 / commits / cbc4d40663

Typecheck React DevTools extension main script (#35519)

Sebastian "Sebbie" Silbermann committed Jan 16, 2026 at 13:08 UTC cbc4d4066333cb1a6736b93df4e0b5ff5936c849
3 files changed +64 -36
packages/react-devtools-extensions/src/main/index.js
+57 -33
@@ -1,6 +1,14 @@
1 /* global chrome */
2 -
2 +/** @flow */
3 +
4 +import type {RootType} from 'react-dom/src/client/ReactDOMRoot';
5 +import type {FrontendBridge, Message} from 'react-devtools-shared/src/bridge';
6 +import type {
7 + TabID,
8 + ViewElementSource,
9 +} from 'react-devtools-shared/src/devtools/views/DevTools';
10 import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane';
11 +import type {Element} from 'react-devtools-shared/src/frontend/types';
12
13 import {createElement} from 'react';
14 import {flushSync} from 'react-dom';
@@ -51,9 +59,9 @@ const hookNamesModuleLoaderFunction = () => resolvedParseHookNames;
59 function createBridge() {
60 bridge = new Bridge({
61 listen(fn) {
54 - const bridgeListener = message => fn(message);
62 + const bridgeListener = (message: Message) => fn(message);
63 // Store the reference so that we unsubscribe from the same object.
56 - const portOnMessage = port.onMessage;
64 + const portOnMessage = ((port: any): ExtensionPort).onMessage;
65 portOnMessage.addListener(bridgeListener);
66
67 lastSubscribedBridgeListener = bridgeListener;
@@ -71,7 +79,7 @@ function createBridge() {
79
80 bridge.addListener('reloadAppForProfiling', () => {
81 localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
74 - evalInInspectedWindow('reload', []);
82 + evalInInspectedWindow('reload', [], () => {});
83 });
84
85 bridge.addListener(
@@ -176,14 +184,20 @@ function createBridgeAndStore() {
184 // Otherwise, the Store may miss important initial tree op codes.
185 injectBackendManager(chrome.devtools.inspectedWindow.tabId);
186
179 - const viewAttributeSourceFunction = (id, path) => {
187 + const viewAttributeSourceFunction = (
188 + id: Element['id'],
189 + path: Array<string | number>,
190 + ) => {
191 const rendererID = store.getRendererIDForElement(id);
192 if (rendererID != null) {
193 viewAttributeSource(rendererID, id, path);
194 }
195 };
196
186 - const viewElementSourceFunction = (source, symbolicatedSource) => {
197 + const viewElementSourceFunction: ViewElementSource = (
198 + source,
199 + symbolicatedSource,
200 + ) => {
201 const [, sourceURL, line, column] = symbolicatedSource
202 ? symbolicatedSource
203 : source;
@@ -198,7 +212,7 @@ function createBridgeAndStore() {
212
213 root = createRoot(document.createElement('div'));
214
201 - render = (overrideTab = mostRecentOverrideTab) => {
215 + render = (overrideTab: TabID | null = mostRecentOverrideTab) => {
216 mostRecentOverrideTab = overrideTab;
217
218 root.render(
@@ -227,7 +241,9 @@ function createBridgeAndStore() {
241 };
242 }
243
230 -function ensureInitialHTMLIsCleared(container) {
244 +function ensureInitialHTMLIsCleared(
245 + container: HTMLElement & {_hasInitialHTMLBeenCleared?: boolean},
246 +) {
247 if (container._hasInitialHTMLBeenCleared) {
248 return;
249 }
@@ -397,13 +413,6 @@ function createSourcesEditorPanel() {
413 logEvent({event_name: 'selected-editor-pane'});
414 }
415 });
400 -
401 - createdPane.onShown.addListener(() => {
402 - bridge.emit('extensionEditorPaneShown');
403 - });
404 - createdPane.onHidden.addListener(() => {
405 - bridge.emit('extensionEditorPaneHidden');
406 - });
416 });
417 }
418
@@ -479,10 +488,10 @@ function performInTabNavigationCleanup() {
488 // Do not clean mostRecentOverrideTab on purpose, so we remember last opened
489 // React DevTools tab, when user does in-tab navigation
490
482 - store = null;
483 - bridge = null;
484 - render = null;
485 - root = null;
491 + store = (null: $FlowFixMe);
492 + bridge = (null: $FlowFixMe);
493 + render = (null: $FlowFixMe);
494 + root = (null: $FlowFixMe);
495 }
496
497 function performFullCleanup() {
@@ -504,18 +513,18 @@ function performFullCleanup() {
513 componentsPortalContainer = null;
514 profilerPortalContainer = null;
515 suspensePortalContainer = null;
507 - root = null;
516 + root = (null: $FlowFixMe);
517
518 mostRecentOverrideTab = null;
510 - store = null;
511 - bridge = null;
512 - render = null;
519 + store = (null: $FlowFixMe);
520 + bridge = (null: $FlowFixMe);
521 + render = (null: $FlowFixMe);
522
523 port?.disconnect();
515 - port = null;
524 + port = (null: $FlowFixMe);
525 }
526
518 -function connectExtensionPort() {
527 +function connectExtensionPort(): void {
528 if (port) {
529 throw new Error('DevTools port was already connected');
530 }
@@ -539,7 +548,7 @@ function connectExtensionPort() {
548 // so, when we call `port.disconnect()` from this script,
549 // this should not trigger this callback and port reconnection
550 port.onDisconnect.addListener(() => {
542 - port = null;
551 + port = (null: $FlowFixMe);
552 connectExtensionPort();
553 });
554 }
@@ -593,9 +602,9 @@ function mountReactDevToolsWhenReactHasLoaded() {
602 );
603 }
604
596 -let bridge = null;
605 +let bridge: FrontendBridge = (null: $FlowFixMe);
606 let lastSubscribedBridgeListener = null;
598 -let store = null;
607 +let store: Store = (null: $FlowFixMe);
608
609 let profilingData = null;
610
@@ -610,13 +619,28 @@ let suspensePortalContainer = null;
619 let editorPortalContainer = null;
620 let inspectedElementPortalContainer = null;
621
613 -let mostRecentOverrideTab = null;
614 -let render = null;
615 -let root = null;
622 +let mostRecentOverrideTab: null | TabID = null;
623 +let render: (overrideTab?: TabID) => void = (null: $FlowFixMe);
624 +let root: RootType = (null: $FlowFixMe);
625
626 let currentSelectedSource: null | SourceSelection = null;
627
619 -let port = null;
628 +type ExtensionEvent = {
629 + addListener(callback: (message: Message, port: ExtensionPort) => void): void,
630 + removeListener(
631 + callback: (message: Message, port: ExtensionPort) => void,
632 + ): void,
633 +};
634 +
635 +/** https://developer.chrome.com/docs/extensions/reference/api/runtime#type-Port */
636 +type ExtensionPort = {
637 + onDisconnect: ExtensionEvent,
638 + onMessage: ExtensionEvent,
639 + postMessage(message: mixed, transferable?: Array<mixed>): void,
640 + disconnect(): void,
641 +};
642 +
643 +let port: ExtensionPort = (null: $FlowFixMe);
644
645 // In case when multiple navigation events emitted in a short period of time
646 // This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
@@ -649,7 +673,7 @@ connectExtensionPort();
673
674 mountReactDevToolsWhenReactHasLoaded();
675
652 -function onThemeChanged(themeName) {
676 +function onThemeChanged() {
677 // Rerender with the new theme
678 render();
679 }
packages/react-devtools-shared/src/Logger.js
+5 -1
@@ -63,7 +63,11 @@ export type LoggerEvent =
63 +value: any,
64 ...
65 },
66 - };
66 + }
67 + | {
68 + +event_name: 'selected-editor-pane',
69 + }
70 + | {+event_name: 'selected-inspected-element-pane'};
71
72 export type LogFunction = LoggerEvent => void | Promise<void>;
73
packages/react-devtools-shared/src/bridge.js
+2 -2
@@ -74,7 +74,7 @@ export const currentBridgeProtocol: BridgeProtocol =
74
75 type ElementAndRendererID = {id: number, rendererID: RendererID};
76
77 -type Message = {
77 +export type Message = {
78 event: string,
79 payload: any,
80 };
@@ -239,7 +239,7 @@ export type BackendEvents = {
239 type StartProfilingParams = ProfilingSettings;
240 type ReloadAndProfilingParams = ProfilingSettings;
241
242 -type FrontendEvents = {
242 +export type FrontendEvents = {
243 clearErrorsAndWarnings: [{rendererID: RendererID}],
244 clearErrorsForElementID: [ElementAndRendererID],
245 clearHostInstanceHighlight: [],