@samitouri / QOS-React-2 / commits / 4b568a8dbb

[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;