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: [],