Skip to content

Commit 4b568a8

Browse files
authored
[DevTools] Improve type coverage for extension runtime API (facebook#35957)
1 parent 9c0323e commit 4b568a8

File tree

3 files changed

+167
-47
lines changed

3 files changed

+167
-47
lines changed

packages/react-devtools-extensions/src/background/index.js

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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 */
210

311
'use strict';
412

@@ -12,20 +20,19 @@ import {
1220
handleFetchResourceContentScriptMessage,
1321
} from './messageHandlers';
1422

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]
2834
if (!ports[tabId]) {
35+
// $FlowFixMe[incompatible-type]
2936
ports[tabId] = {
3037
extension: null,
3138
proxy: null,
@@ -34,18 +41,21 @@ function registerTab(tabId) {
3441
}
3542
}
3643

37-
function registerExtensionPort(port, tabId) {
44+
function registerExtensionPort(port: ExtensionRuntimePort, tabId: number) {
45+
// $FlowFixMe[incompatible-type]
3846
ports[tabId].extension = port;
3947

4048
port.onDisconnect.addListener(() => {
4149
// This should delete disconnectPipe from ports dictionary
50+
// $FlowFixMe[incompatible-type]
4251
ports[tabId].disconnectPipe?.();
4352

44-
delete ports[tabId].extension;
53+
// $FlowFixMe[incompatible-type]
54+
ports[tabId].extension = null;
4555
});
4656
}
4757

48-
function registerProxyPort(port, tabId) {
58+
function registerProxyPort(port: ExtensionRuntimePort, tabId: string) {
4959
ports[tabId].proxy = port;
5060

5161
// In case proxy port was disconnected from the other end, from content script
@@ -54,7 +64,7 @@ function registerProxyPort(port, tabId) {
5464
port.onDisconnect.addListener(() => {
5565
ports[tabId].disconnectPipe?.();
5666

57-
delete ports[tabId].proxy;
67+
ports[tabId].proxy = null;
5868
});
5969
}
6070

@@ -73,14 +83,22 @@ chrome.runtime.onConnect.addListener(port => {
7383
// Proxy content script is executed in tab, so it should have it specified.
7484
const tabId = port.sender.tab.id;
7585

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();
7992
}
8093

8194
registerTab(tabId);
82-
registerProxyPort(port, tabId);
95+
registerProxyPort(
96+
port,
97+
// $FlowFixMe[incompatible-call]
98+
tabId,
99+
);
83100

101+
// $FlowFixMe[incompatible-type]
84102
if (ports[tabId].extension) {
85103
connectExtensionAndProxyPorts(
86104
ports[tabId].extension,
@@ -97,8 +115,13 @@ chrome.runtime.onConnect.addListener(port => {
97115
const tabId = +port.name;
98116

99117
registerTab(tabId);
100-
registerExtensionPort(port, tabId);
118+
registerExtensionPort(
119+
port,
120+
// $FlowFixMe[incompatible-call]
121+
tabId,
122+
);
101123

124+
// $FlowFixMe[incompatible-type]
102125
if (ports[tabId].proxy) {
103126
connectExtensionAndProxyPorts(
104127
ports[tabId].extension,
@@ -114,26 +137,33 @@ chrome.runtime.onConnect.addListener(port => {
114137
console.warn(`Unknown port ${port.name} connected`);
115138
});
116139

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) {
119146
throw new Error(
120147
`Attempted to connect ports, when extension port is not present`,
121148
);
122149
}
150+
const extensionPort = maybeExtensionPort;
123151

124-
if (!proxyPort) {
152+
if (!maybeProxyPort) {
125153
throw new Error(
126154
`Attempted to connect ports, when proxy port is not present`,
127155
);
128156
}
157+
const proxyPort = maybeProxyPort;
129158

159+
// $FlowFixMe[incompatible-type]
130160
if (ports[tabId].disconnectPipe) {
131161
throw new Error(
132162
`Attempted to connect already connected ports for tab with id ${tabId}`,
133163
);
134164
}
135165

136-
function extensionPortMessageListener(message) {
166+
function extensionPortMessageListener(message: any) {
137167
try {
138168
proxyPort.postMessage(message);
139169
} catch (e) {
@@ -145,7 +175,7 @@ function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
145175
}
146176
}
147177

148-
function proxyPortMessageListener(message) {
178+
function proxyPortMessageListener(message: any) {
149179
try {
150180
extensionPort.postMessage(message);
151181
} catch (e) {
@@ -164,6 +194,7 @@ function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
164194
// We handle disconnect() calls manually, based on each specific case
165195
// No need to disconnect other port here
166196

197+
// $FlowFixMe[incompatible-type]
167198
delete ports[tabId].disconnectPipe;
168199
}
169200

packages/react-devtools-extensions/src/main/index.js

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* global chrome */
1+
/* global chrome, ExtensionRuntimePort */
22
/** @flow */
33

44
import type {RootType} from 'react-dom/src/client/ReactDOMRoot';
@@ -61,7 +61,7 @@ function createBridge() {
6161
listen(fn) {
6262
const bridgeListener = (message: Message) => fn(message);
6363
// Store the reference so that we unsubscribe from the same object.
64-
const portOnMessage = ((port: any): ExtensionPort).onMessage;
64+
const portOnMessage = port.onMessage;
6565
portOnMessage.addListener(bridgeListener);
6666

6767
lastSubscribedBridgeListener = bridgeListener;
@@ -621,22 +621,7 @@ let root: RootType = (null: $FlowFixMe);
621621

622622
let currentSelectedSource: null | SourceSelection = null;
623623

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);
640625

641626
// In case when multiple navigation events emitted in a short period of time
642627
// This debounced callback primarily used to avoid mounting React DevTools multiple times, which results

scripts/flow/react-devtools.js

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,108 @@ declare const __IS_CHROME__: boolean;
1717
declare const __IS_EDGE__: boolean;
1818
declare const __IS_NATIVE__: boolean;
1919

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;

0 commit comments

Comments
 (0)