Skip to content

Commit 3f41fb5

Browse files
byCedricfacebook-github-bot
authored andcommitted
feature(dev-middleware): add custom message handlers to extend CDP capabilities (#43291)
Summary: This is a proposal for the `react-native/dev-middleware` package, to allow implementers to extend the CDP capabilities of the `InspectorProxy`. It's unfortunately needed until we can move to the native Hermes CDP layer. At Expo, we extend the CDP capabilities of this `InspectorProxy` by injecting functionality on the device level. This proposed API does the same, but without having to overwrite internal functions of both the `InspectorProxy` and `InspectorDevice`. A good example of this is the network inspector's capabilities. This currently works through the inspection proxy, and roughly like: - Handle any incoming `Expo(Network.receivedResponseBody)` from the _**device**_, store it, and stop event from propagating - Handle the incoming `Network.getResponseBody` from the _**debugger**_, return the data, and stop event from propagating. This API brings back that capability in a more structured way. ## API: ```ts import { createDevMiddleware } from 'react-native/dev-middleware'; const { middleware, websocketEndpoints } = createDevMiddleware({ unstable_customInspectorMessageHandler: ({ page, deviceInfo, debuggerInfo }) => { // Do not enable handler for page other than "SOMETHING", or for vscode debugging // Can also include `page.capabilities` to determine if handler is required if (page.title !== 'SOMETHING' || debuggerInfo.userAgent?.includes('vscode')) { return null; } return { handleDeviceMessage(message) { if (message.type === 'CDP_MESSAGE') { // Do something and stop message from propagating with return `true` return true; } }, handleDebuggerMessage(message) { if (message.type === 'CDP_MESSAGE') { // Do something and stop message from propagating with return `true` return true; } }, }; }, }); ``` ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [GENERAL] [ADDED] - Add inspector proxy device message middleware API Pull Request resolved: #43291 Test Plan: See added tests and code above Reviewed By: huntie Differential Revision: D54804503 Pulled By: motiz88 fbshipit-source-id: ae918dcd5b7e76d3fb31db4c84717567ae60fa96
1 parent 5833eb5 commit 3f41fb5

6 files changed

Lines changed: 466 additions & 14 deletions

File tree

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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 strict-local
8+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
import {createAndConnectTarget} from './InspectorProtocolUtils';
13+
import {withAbortSignalForEachTest} from './ResourceUtils';
14+
import {baseUrlForServer, createServer} from './ServerUtils';
15+
import until from 'wait-for-expect';
16+
17+
// WebSocket is unreliable when using fake timers.
18+
jest.useRealTimers();
19+
20+
jest.setTimeout(10000);
21+
22+
describe('inspector proxy device message middleware', () => {
23+
const autoCleanup = withAbortSignalForEachTest();
24+
const page = {
25+
id: 'page1',
26+
app: 'bar-app',
27+
title: 'bar-title',
28+
vm: 'bar-vm',
29+
};
30+
31+
afterEach(() => {
32+
jest.clearAllMocks();
33+
});
34+
35+
test('middleware is created with device, debugger, and page information', async () => {
36+
const createCustomMessageHandler = jest.fn().mockImplementation(() => null);
37+
const {server} = await createServer({
38+
logger: undefined,
39+
projectRoot: '',
40+
unstable_customInspectorMessageHandler: createCustomMessageHandler,
41+
});
42+
43+
let device, debugger_;
44+
try {
45+
({device, debugger_} = await createAndConnectTarget(
46+
serverRefUrls(server),
47+
autoCleanup.signal,
48+
page,
49+
));
50+
51+
// Ensure the middleware was created with the device information
52+
await until(() =>
53+
expect(createCustomMessageHandler).toBeCalledWith(
54+
expect.objectContaining({
55+
page: expect.objectContaining({
56+
...page,
57+
capabilities: expect.any(Object),
58+
}),
59+
device: expect.objectContaining({
60+
appId: expect.any(String),
61+
id: expect.any(String),
62+
name: expect.any(String),
63+
sendMessage: expect.any(Function),
64+
}),
65+
debugger: expect.objectContaining({
66+
userAgent: null,
67+
sendMessage: expect.any(Function),
68+
}),
69+
}),
70+
),
71+
);
72+
} finally {
73+
device?.close();
74+
debugger_?.close();
75+
await closeServer(server);
76+
}
77+
});
78+
79+
test('send message functions are passing messages to sockets', async () => {
80+
const handleDebuggerMessage = jest.fn();
81+
const handleDeviceMessage = jest.fn();
82+
const createCustomMessageHandler = jest.fn().mockImplementation(() => ({
83+
handleDebuggerMessage,
84+
handleDeviceMessage,
85+
}));
86+
87+
const {server} = await createServer({
88+
logger: undefined,
89+
projectRoot: '',
90+
unstable_customInspectorMessageHandler: createCustomMessageHandler,
91+
});
92+
93+
let device, debugger_;
94+
try {
95+
({device, debugger_} = await createAndConnectTarget(
96+
serverRefUrls(server),
97+
autoCleanup.signal,
98+
page,
99+
));
100+
101+
// Ensure the middleware was created with the send message methods
102+
await until(() =>
103+
expect(createCustomMessageHandler).toBeCalledWith(
104+
expect.objectContaining({
105+
device: expect.objectContaining({
106+
sendMessage: expect.any(Function),
107+
}),
108+
debugger: expect.objectContaining({
109+
sendMessage: expect.any(Function),
110+
}),
111+
}),
112+
),
113+
);
114+
115+
// Send a message to the device
116+
createCustomMessageHandler.mock.calls[0][0].device.sendMessage({
117+
id: 1,
118+
});
119+
// Ensure the device received the message
120+
await until(() =>
121+
expect(device.wrappedEvent).toBeCalledWith({
122+
event: 'wrappedEvent',
123+
payload: {
124+
pageId: page.id,
125+
wrappedEvent: JSON.stringify({id: 1}),
126+
},
127+
}),
128+
);
129+
130+
// Send a message to the debugger
131+
createCustomMessageHandler.mock.calls[0][0].debugger.sendMessage({
132+
id: 2,
133+
});
134+
// Ensure the debugger received the message
135+
await until(() =>
136+
expect(debugger_.handle).toBeCalledWith({
137+
id: 2,
138+
}),
139+
);
140+
} finally {
141+
device?.close();
142+
debugger_?.close();
143+
await closeServer(server);
144+
}
145+
});
146+
147+
test('device message is passed to message middleware', async () => {
148+
const handleDeviceMessage = jest.fn();
149+
const {server} = await createServer({
150+
logger: undefined,
151+
projectRoot: '',
152+
unstable_customInspectorMessageHandler: () => ({
153+
handleDeviceMessage,
154+
handleDebuggerMessage() {},
155+
}),
156+
});
157+
158+
let device, debugger_;
159+
try {
160+
({device, debugger_} = await createAndConnectTarget(
161+
serverRefUrls(server),
162+
autoCleanup.signal,
163+
page,
164+
));
165+
166+
// Send a message from the device, and ensure the middleware received it
167+
device.sendWrappedEvent(page.id, {id: 1337});
168+
169+
// Ensure the debugger received the message
170+
await until(() => expect(debugger_.handle).toBeCalledWith({id: 1337}));
171+
// Ensure the middleware received the message
172+
await until(() => expect(handleDeviceMessage).toBeCalled());
173+
} finally {
174+
device?.close();
175+
debugger_?.close();
176+
await closeServer(server);
177+
}
178+
});
179+
180+
test('device message stops propagating when handled by middleware', async () => {
181+
const handleDeviceMessage = jest.fn();
182+
const {server} = await createServer({
183+
logger: undefined,
184+
projectRoot: '',
185+
unstable_customInspectorMessageHandler: () => ({
186+
handleDeviceMessage,
187+
handleDebuggerMessage() {},
188+
}),
189+
});
190+
191+
let device, debugger_;
192+
try {
193+
({device, debugger_} = await createAndConnectTarget(
194+
serverRefUrls(server),
195+
autoCleanup.signal,
196+
page,
197+
));
198+
199+
// Stop the first message from propagating by returning true (once) from middleware
200+
handleDeviceMessage.mockReturnValueOnce(true);
201+
202+
// Send the first message which should NOT be received by the debugger
203+
device.sendWrappedEvent(page.id, {id: -1});
204+
await until(() => expect(handleDeviceMessage).toBeCalled());
205+
206+
// Send the second message which should be received by the debugger
207+
device.sendWrappedEvent(page.id, {id: 1337});
208+
209+
// Ensure only the last message was received by the debugger
210+
await until(() => expect(debugger_.handle).toBeCalledWith({id: 1337}));
211+
// Ensure the first message was not received by the debugger
212+
expect(debugger_.handle).not.toBeCalledWith({id: -1});
213+
} finally {
214+
device?.close();
215+
debugger_?.close();
216+
await closeServer(server);
217+
}
218+
});
219+
220+
test('debugger message is passed to message middleware', async () => {
221+
const handleDebuggerMessage = jest.fn();
222+
const {server} = await createServer({
223+
logger: undefined,
224+
projectRoot: '',
225+
unstable_customInspectorMessageHandler: () => ({
226+
handleDeviceMessage() {},
227+
handleDebuggerMessage,
228+
}),
229+
});
230+
231+
let device, debugger_;
232+
try {
233+
({device, debugger_} = await createAndConnectTarget(
234+
serverRefUrls(server),
235+
autoCleanup.signal,
236+
page,
237+
));
238+
239+
// Send a message from the debugger
240+
const message = {
241+
method: 'Runtime.enable',
242+
id: 1337,
243+
};
244+
debugger_.send(message);
245+
246+
// Ensure the device received the message
247+
await until(() => expect(device.wrappedEvent).toBeCalled());
248+
// Ensure the middleware received the message
249+
await until(() => expect(handleDebuggerMessage).toBeCalledWith(message));
250+
} finally {
251+
device?.close();
252+
debugger_?.close();
253+
await closeServer(server);
254+
}
255+
});
256+
257+
test('debugger message stops propagating when handled by middleware', async () => {
258+
const handleDebuggerMessage = jest.fn();
259+
const {server} = await createServer({
260+
logger: undefined,
261+
projectRoot: '',
262+
unstable_customInspectorMessageHandler: () => ({
263+
handleDeviceMessage() {},
264+
handleDebuggerMessage,
265+
}),
266+
});
267+
268+
let device, debugger_;
269+
try {
270+
({device, debugger_} = await createAndConnectTarget(
271+
serverRefUrls(server),
272+
autoCleanup.signal,
273+
page,
274+
));
275+
276+
// Stop the first message from propagating by returning true (once) from middleware
277+
handleDebuggerMessage.mockReturnValueOnce(true);
278+
279+
// Send the first emssage which should not be received by the device
280+
debugger_.send({id: -1});
281+
// Send the second message which should be received by the device
282+
debugger_.send({id: 1337});
283+
284+
// Ensure only the last message was received by the device
285+
await until(() =>
286+
expect(device.wrappedEvent).toBeCalledWith({
287+
event: 'wrappedEvent',
288+
payload: {pageId: page.id, wrappedEvent: JSON.stringify({id: 1337})},
289+
}),
290+
);
291+
// Ensure the first message was not received by the device
292+
expect(device.wrappedEvent).not.toBeCalledWith({id: -1});
293+
} finally {
294+
device?.close();
295+
debugger_?.close();
296+
await closeServer(server);
297+
}
298+
});
299+
});
300+
301+
function serverRefUrls(server: http$Server | https$Server) {
302+
return {
303+
serverBaseUrl: baseUrlForServer(server, 'http'),
304+
serverBaseWsUrl: baseUrlForServer(server, 'ws'),
305+
};
306+
}
307+
308+
async function closeServer(server: http$Server | https$Server): Promise<void> {
309+
return new Promise(resolve => server.close(() => resolve()));
310+
}

packages/dev-middleware/src/createDevMiddleware.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* @oncall react_native
1010
*/
1111

12+
import type {CreateCustomMessageHandlerFn} from './inspector-proxy/CustomMessageHandler';
1213
import type {BrowserLauncher} from './types/BrowserLauncher';
1314
import type {EventReporter} from './types/EventReporter';
1415
import type {Experiments, ExperimentsConfig} from './types/Experiments';
@@ -61,11 +62,12 @@ type Options = $ReadOnly<{
6162
unstable_experiments?: ExperimentsConfig,
6263

6364
/**
64-
* An interface for using a modified inspector proxy implementation.
65+
* Create custom handler to add support for unsupported CDP events, or debuggers.
66+
* This handler is instantiated per logical device and debugger pair.
6567
*
6668
* This is an unstable API with no semver guarantees.
6769
*/
68-
unstable_InspectorProxy?: Class<InspectorProxy>,
70+
unstable_customInspectorMessageHandler?: CreateCustomMessageHandlerFn,
6971
}>;
7072

7173
type DevMiddlewareAPI = $ReadOnly<{
@@ -80,16 +82,16 @@ export default function createDevMiddleware({
8082
unstable_browserLauncher = DefaultBrowserLauncher,
8183
unstable_eventReporter,
8284
unstable_experiments: experimentConfig = {},
83-
unstable_InspectorProxy,
85+
unstable_customInspectorMessageHandler,
8486
}: Options): DevMiddlewareAPI {
8587
const experiments = getExperiments(experimentConfig);
8688

87-
const InspectorProxyClass = unstable_InspectorProxy ?? InspectorProxy;
88-
const inspectorProxy = new InspectorProxyClass(
89+
const inspectorProxy = new InspectorProxy(
8990
projectRoot,
9091
serverBaseUrl,
9192
unstable_eventReporter,
9293
experiments,
94+
unstable_customInspectorMessageHandler,
9395
);
9496

9597
const middleware = connect()

packages/dev-middleware/src/index.flow.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export {default as createDevMiddleware} from './createDevMiddleware';
1313

1414
export type {BrowserLauncher, LaunchedBrowser} from './types/BrowserLauncher';
1515
export type {EventReporter, ReportableEvent} from './types/EventReporter';
16-
17-
export {default as unstable_InspectorProxy} from './inspector-proxy/InspectorProxy';
18-
export {default as unstable_Device} from './inspector-proxy/Device';
16+
export type {
17+
CustomMessageHandler,
18+
CustomMessageHandlerConnection,
19+
CreateCustomMessageHandlerFn,
20+
} from './inspector-proxy/CustomMessageHandler';

0 commit comments

Comments
 (0)