[DevTools] Improve type coverage for extension runtime API (#35957)
Sebastian "Sebbie" Silbermann committed
Mar 4, 2026 at 12:47 UTC
4b568a8dbb4cb84b0067f353b9c0bec1ddb61d8e
3 files changed
+167
-47
packages/react-devtools-extensions/src/background/index.js
+59
-28
@@ -1,4 +1,12 @@
1
-/* global chrome */
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+/* global chrome, ExtensionRuntimePort */
10
11
'use strict';
12
@@ -12,20 +20,19 @@ import {
20
handleFetchResourceContentScriptMessage,
21
} from './messageHandlers';
22
15
-/*
16
- {
17
- [tabId]: {
18
- extension: ExtensionPort,
19
- proxy: ProxyPort,
20
- disconnectPipe: Function,
21
- },
22
- ...
23
- }
24
- */
25
-const ports = {};
26
-
27
-function registerTab(tabId) {
23
+const ports: {
24
+ // TODO: Check why we convert tab IDs to strings, and if we can avoid it
25
+ [tabId: string]: {
26
+ extension: ExtensionRuntimePort | null,
27
+ proxy: ExtensionRuntimePort | null,
28
+ disconnectPipe: Function | null,
29
+ },
30
+} = {};
31
+
32
+function registerTab(tabId: number) {
33
+ // $FlowFixMe[incompatible-type]
34
if (!ports[tabId]) {
35
+ // $FlowFixMe[incompatible-type]
36
ports[tabId] = {
37
extension: null,
38
proxy: null,
@@ -34,18 +41,21 @@ function registerTab(tabId) {
41
}
42
}
43
37
-function registerExtensionPort(port, tabId) {
44
+function registerExtensionPort(port: ExtensionRuntimePort, tabId: number) {
45
+ // $FlowFixMe[incompatible-type]
46
ports[tabId].extension = port;
47
48
port.onDisconnect.addListener(() => {
49
// This should delete disconnectPipe from ports dictionary
50
+ // $FlowFixMe[incompatible-type]
51
ports[tabId].disconnectPipe?.();
52
44
- delete ports[tabId].extension;
53
+ // $FlowFixMe[incompatible-type]
54
+ ports[tabId].extension = null;
55
});
56
}
57
48
-function registerProxyPort(port, tabId) {
58
+function registerProxyPort(port: ExtensionRuntimePort, tabId: string) {
59
ports[tabId].proxy = port;
60
61
// In case proxy port was disconnected from the other end, from content script
@@ -54,7 +64,7 @@ function registerProxyPort(port, tabId) {
64
port.onDisconnect.addListener(() => {
65
ports[tabId].disconnectPipe?.();
66
57
- delete ports[tabId].proxy;
67
+ ports[tabId].proxy = null;
68
});
69
}
70
@@ -73,14 +83,22 @@ chrome.runtime.onConnect.addListener(port => {
83
// Proxy content script is executed in tab, so it should have it specified.
84
const tabId = port.sender.tab.id;
85
76
- if (ports[tabId]?.proxy) {
77
- ports[tabId].disconnectPipe?.();
78
- ports[tabId].proxy.disconnect();
86
+ // $FlowFixMe[incompatible-type]
87
+ const registeredPort = ports[tabId];
88
+ const proxy = registeredPort?.proxy;
89
+ if (proxy) {
90
+ registeredPort.disconnectPipe?.();
91
+ proxy.disconnect();
92
}
93
94
registerTab(tabId);
82
- registerProxyPort(port, tabId);
95
+ registerProxyPort(
96
+ port,
97
+ // $FlowFixMe[incompatible-call]
98
+ tabId,
99
+ );
100
101
+ // $FlowFixMe[incompatible-type]
102
if (ports[tabId].extension) {
103
connectExtensionAndProxyPorts(
104
ports[tabId].extension,
@@ -97,8 +115,13 @@ chrome.runtime.onConnect.addListener(port => {
115
const tabId = +port.name;
116
117
registerTab(tabId);
100
- registerExtensionPort(port, tabId);
118
+ registerExtensionPort(
119
+ port,
120
+ // $FlowFixMe[incompatible-call]
121
+ tabId,
122
+ );
123
124
+ // $FlowFixMe[incompatible-type]
125
if (ports[tabId].proxy) {
126
connectExtensionAndProxyPorts(
127
ports[tabId].extension,
@@ -114,26 +137,33 @@ chrome.runtime.onConnect.addListener(port => {
137
console.warn(`Unknown port ${port.name} connected`);
138
});
139
117
-function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
118
- if (!extensionPort) {
140
+function connectExtensionAndProxyPorts(
141
+ maybeExtensionPort: ExtensionRuntimePort | null,
142
+ maybeProxyPort: ExtensionRuntimePort | null,
143
+ tabId: number,
144
+) {
145
+ if (!maybeExtensionPort) {
146
throw new Error(
147
`Attempted to connect ports, when extension port is not present`,
148
);
149
}
150
+ const extensionPort = maybeExtensionPort;
151
124
- if (!proxyPort) {
152
+ if (!maybeProxyPort) {
153
throw new Error(
154
`Attempted to connect ports, when proxy port is not present`,
155
);
156
}
157
+ const proxyPort = maybeProxyPort;
158
159
+ // $FlowFixMe[incompatible-type]
160
if (ports[tabId].disconnectPipe) {
161
throw new Error(
162
`Attempted to connect already connected ports for tab with id ${tabId}`,
163
);
164
}
165
136
- function extensionPortMessageListener(message) {
166
+ function extensionPortMessageListener(message: any) {
167
try {
168
proxyPort.postMessage(message);
169
} catch (e) {
@@ -145,7 +175,7 @@ function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
175
}
176
}
177
148
- function proxyPortMessageListener(message) {
178
+ function proxyPortMessageListener(message: any) {
179
try {
180
extensionPort.postMessage(message);
181
} catch (e) {
@@ -164,6 +194,7 @@ function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
194
// We handle disconnect() calls manually, based on each specific case
195
// No need to disconnect other port here
196
197
+ // $FlowFixMe[incompatible-type]
198
delete ports[tabId].disconnectPipe;
199
}
200
packages/react-devtools-extensions/src/main/index.js
+3
-18
@@ -1,4 +1,4 @@
1
-/* global chrome */
1
+/* global chrome, ExtensionRuntimePort */
2
/** @flow */
3
4
import type {RootType} from 'react-dom/src/client/ReactDOMRoot';
@@ -61,7 +61,7 @@ function createBridge() {
61
listen(fn) {
62
const bridgeListener = (message: Message) => fn(message);
63
// Store the reference so that we unsubscribe from the same object.
64
- const portOnMessage = ((port: any): ExtensionPort).onMessage;
64
+ const portOnMessage = port.onMessage;
65
portOnMessage.addListener(bridgeListener);
66
67
lastSubscribedBridgeListener = bridgeListener;
@@ -621,22 +621,7 @@ let root: RootType = (null: $FlowFixMe);
621
622
let currentSelectedSource: null | SourceSelection = null;
623
624
-type ExtensionEvent = {
625
- addListener(callback: (message: Message, port: ExtensionPort) => void): void,
626
- removeListener(
627
- callback: (message: Message, port: ExtensionPort) => void,
628
- ): void,
629
-};
630
-
631
-/** https://developer.chrome.com/docs/extensions/reference/api/runtime#type-Port */
632
-type ExtensionPort = {
633
- onDisconnect: ExtensionEvent,
634
- onMessage: ExtensionEvent,
635
- postMessage(message: mixed, transferable?: Array<mixed>): void,
636
- disconnect(): void,
637
-};
638
-
639
-let port: ExtensionPort = (null: $FlowFixMe);
624
+let port: ExtensionRuntimePort = (null: $FlowFixMe);
625
626
// In case when multiple navigation events emitted in a short period of time
627
// This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
scripts/flow/react-devtools.js
+105
-1
@@ -17,4 +17,108 @@ declare const __IS_CHROME__: boolean;
17
declare const __IS_EDGE__: boolean;
18
declare const __IS_NATIVE__: boolean;
19
20
-declare const chrome: any;
20
+interface ExtensionDevtools {
21
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools/inspectedWindow} */
22
+ inspectedWindow: $FlowFixMe;
23
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools/network} */
24
+ network: $FlowFixMe;
25
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools/panels} */
26
+ panels: $FlowFixMe;
27
+}
28
+
29
+interface ExtensionEvent<Listener: Function> {
30
+ addListener(callback: Listener): void;
31
+ removeListener(callback: Listener): void;
32
+}
33
+
34
+/** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab} */
35
+// TODO: Only covers used properties. Extend as needed.
36
+interface ExtensionTab {
37
+ id?: number;
38
+}
39
+
40
+/** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender} */
41
+// TODO: Only covers used properties. Extend as needed.
42
+interface ExtensionRuntimeSender {
43
+ tab?: ExtensionTab;
44
+}
45
+
46
+/** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/Port} */
47
+// TODO: Only covers used properties. Extend as needed.
48
+interface ExtensionRuntimePort {
49
+ disconnect(): void;
50
+ name: string;
51
+ onMessage: ExtensionEvent<(message: any, port: ExtensionRuntimePort) => void>;
52
+ onDisconnect: ExtensionEvent<(port: ExtensionRuntimePort) => void>;
53
+ postMessage(message: mixed, transferable?: Array<mixed>): void;
54
+ sender?: ExtensionRuntimeSender;
55
+}
56
+
57
+interface ExtensionMessageSender {
58
+ id?: string;
59
+ url?: string;
60
+ tab?: {
61
+ id: number,
62
+ url: string,
63
+ };
64
+}
65
+
66
+interface ExtensionRuntime {
67
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connect} */
68
+ connect(connectInfo?: {
69
+ name?: string,
70
+ includeTlsChannelId?: boolean,
71
+ }): ExtensionRuntimePort;
72
+ connect(
73
+ extensionId: string,
74
+ connectInfo?: {name?: string, includeTlsChannelId?: boolean},
75
+ ): ExtensionRuntimePort;
76
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage} */
77
+ onMessage: ExtensionEvent<
78
+ (
79
+ message: any,
80
+ sender: ExtensionMessageSender,
81
+ sendResponse: (response: any) => void,
82
+ ) => any,
83
+ >;
84
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onConnect} */
85
+ onConnect: ExtensionEvent<(port: ExtensionRuntimePort) => void>;
86
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage} */
87
+ sendMessage(
88
+ message: any,
89
+ options?: {includeTlsChannelId?: boolean},
90
+ ): Promise<any>;
91
+ sendMessage(
92
+ extensionId: string,
93
+ message: any,
94
+ // We're making this required so that we don't accidentally call the wrong overload.
95
+ options: {includeTlsChannelId?: boolean},
96
+ ): Promise<any>;
97
+}
98
+
99
+interface ExtensionTabs {
100
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/onActivated} */
101
+ onActivated: ExtensionEvent<
102
+ (activeInfo: {
103
+ previousTabId: number,
104
+ tabId: number,
105
+ windowId: number,
106
+ }) => void,
107
+ >;
108
+}
109
+
110
+interface ExtensionAPI {
111
+ devtools: ExtensionDevtools;
112
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/permissions} */
113
+ permissions: $FlowFixMe;
114
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime} */
115
+ runtime: ExtensionRuntime;
116
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting} */
117
+ scripting: $FlowFixMe;
118
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage} */
119
+ storage: $FlowFixMe;
120
+ /** @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs} */
121
+ tabs: ExtensionTabs;
122
+}
123
+
124
+declare const chrome: ExtensionAPI;