Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1edb435

Browse files
huntiefacebook-github-bot
authored andcommittedAug 16, 2023
Move metro-inspector-proxy into dev-middleware
Summary: - Relocates `metro-inspector-proxy` source from the Metro repo into the React Native repo as part of the `react-native/dev-middleware` package. - Drops the `runInspectorProxy` entry point. - Attaches the Inspector Proxy to the `createDevMiddleware()` API as the new integration point for this functionality. - Documents migrated endpoints + usage of `createDevMiddleware()` in README. Changelog: [Internal] Metro changelog: None (`metro-inspector-proxy` is now an internal component of `react-native`, covered in the [release notes for 0.78.1](https://github.com/facebook/metro/releases/tag/v0.78.1)) Differential Revision: D48066213 fbshipit-source-id: fa93b9b116046bdf077fcd7163b57a870a3b4d46
1 parent 623f44c commit 1edb435

File tree

8 files changed

+1120
-16
lines changed

8 files changed

+1120
-16
lines changed
 

‎flow-typed/npm/@react-native-community/cli-server-api_v12.x.x.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ declare module '@react-native-community/cli-server-api' {
2323
): {
2424
middleware: Server,
2525
websocketEndpoints: {
26-
'/debugger-proxy': ws$WebSocketServer,
27-
'/message': ws$WebSocketServer,
28-
'/events': ws$WebSocketServer,
26+
[path: string]: ws$WebSocketServer,
2927
},
3028
debuggerProxyEndpoint: {
3129
server: ws$WebSocketServer,

‎packages/dev-middleware/README.md

+74-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,82 @@
11
# @react-native/dev-middleware
22

3-
![https://img.shields.io/npm/v/@react-native/dev-middleware?color=brightgreen&label=npm%20package](https://www.npmjs.com/package/@react-native/dev-middleware)
3+
![npm package](https://img.shields.io/npm/v/@react-native/dev-middleware?color=brightgreen&label=npm%20package)
44

55
Dev server middleware supporting core React Native development features. This package is preconfigured in all React Native projects.
66

7-
## Endpoints
7+
## Usage
88

9-
### `/open-debugger`
9+
Middleware can be attached to a dev server (e.g. [Metro](https://facebook.github.io/metro/docs/getting-started)) using the `createDevMiddleware` API.
10+
11+
```js
12+
import { createDevMiddleware } from '@react-native/dev-middleware';
13+
14+
function myDevServerImpl(args) {
15+
...
16+
17+
const {middleware, websocketEndpoints} = createDevMiddleware({
18+
host: args.host,
19+
port: metroConfig.server.port,
20+
projectRoot: metroConfig.projectRoot,
21+
logger,
22+
});
23+
24+
await Metro.runServer(metroConfig, {
25+
host: args.host,
26+
...,
27+
unstable_extraMiddleware: [
28+
middleware,
29+
// Optionally extend with additional HTTP middleware
30+
],
31+
websocketEndpoints: {
32+
...websocketEndpoints,
33+
// Optionally extend with additional WebSocket endpoints
34+
},
35+
});
36+
}
37+
```
38+
39+
## Included middleware
40+
41+
`@react-native/dev-middleware` is designed for integrators such as [`@expo/dev-server`](https://www.npmjs.com/package/@expo/dev-server) and [`@react-native/community-cli-plugin`](https://github.com/facebook/react-native/tree/main/packages/community-cli-plugin). It provides a common default implementation for core React Native dev server responsibilities.
42+
43+
We intend to keep this to a narrow set of functionality, based around:
44+
45+
- **Debugging** — The [Chrome DevTools protocol (CDP)](https://chromedevtools.github.io/devtools-protocol/) endpoints supported by React Native, including the Inspector Proxy, which facilitates connections with multiple devices.
46+
- **Dev actions** — Endpoints implementing core [Dev Menu](https://reactnative.dev/docs/debugging#accessing-the-dev-menu) actions, e.g. reloading the app, opening the debugger frontend.
47+
48+
### HTTP endpoints
49+
50+
<small>`DevMiddlewareAPI.middleware`</small>
51+
52+
These are exposed as a [`connect`](https://www.npmjs.com/package/connect) middleware handler, assignable to `Metro.runServer` or other compatible HTTP servers.
53+
54+
#### GET `/json/list`, `/json` ([CDP](https://chromedevtools.github.io/devtools-protocol/#endpoints))
55+
56+
Returns the list of available WebSocket targets for all connected React Native app sessions.
57+
58+
#### GET `/json/version` ([CDP](https://chromedevtools.github.io/devtools-protocol/#endpoints))
59+
60+
Returns version metadata used by Chrome DevTools.
61+
62+
#### POST `/open-debugger`
1063

1164
Open the JavaScript debugger for a given CDP target (direct Hermes debugging).
65+
66+
<details>
67+
<summary>Example</summary>
68+
69+
curl -X POST 'http://localhost:8081/open-debugger?appId=com.meta.RNTester'
70+
</details>
71+
72+
### WebSocket endpoints
73+
74+
<small>`DevMiddlewareAPI.websocketEndpoints`</small>
75+
76+
#### `/inspector/device`
77+
78+
WebSocket handler for registering device connections.
79+
80+
#### `/inspector/debug`
81+
82+
WebSocket handler that proxies CDP messages to/from the corresponding device.

‎packages/dev-middleware/src/createDevMiddleware.js

+26-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @flow strict
7+
* @flow strict-local
88
* @format
99
* @oncall react_native
1010
*/
@@ -14,18 +14,36 @@ import type {Logger} from './types/Logger';
1414

1515
import connect from 'connect';
1616
import openDebuggerMiddleware from './middleware/openDebuggerMiddleware';
17+
import InspectorProxy from './inspector-proxy/InspectorProxy';
1718

1819
type Options = $ReadOnly<{
20+
host: string,
21+
port: number,
22+
projectRoot: string,
1923
logger?: Logger,
2024
}>;
2125

22-
export default function createDevMiddleware({logger}: Options = {}): {
26+
type DevMiddlewareAPI = $ReadOnly<{
2327
middleware: NextHandleFunction,
24-
} {
25-
const middleware = connect().use(
26-
'/open-debugger',
27-
openDebuggerMiddleware({logger}),
28-
);
28+
websocketEndpoints: {[path: string]: ws$WebSocketServer},
29+
}>;
30+
31+
export default function createDevMiddleware({
32+
host,
33+
port,
34+
projectRoot,
35+
logger,
36+
}: Options): DevMiddlewareAPI {
37+
const inspectorProxy = new InspectorProxy(projectRoot);
38+
39+
const middleware = connect()
40+
.use('/open-debugger', openDebuggerMiddleware({logger}))
41+
.use((...args) => inspectorProxy.processRequest(...args));
2942

30-
return {middleware};
43+
return {
44+
middleware,
45+
websocketEndpoints: inspectorProxy.createWebSocketListeners(
46+
`${host}:${port}`,
47+
),
48+
};
3149
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @flow strict
7+
* @flow strict-local
88
* @format
99
* @oncall react_native
1010
*/

‎packages/dev-middleware/src/inspector-proxy/Device.js

+650
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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 type {
13+
JsonPagesListResponse,
14+
JsonVersionResponse,
15+
Page,
16+
PageDescription,
17+
} from './types';
18+
import type {IncomingMessage, ServerResponse} from 'http';
19+
20+
import Device from './Device';
21+
import url from 'url';
22+
import WS from 'ws';
23+
24+
const debug = require('debug')('Metro:InspectorProxy');
25+
26+
const WS_DEVICE_URL = '/inspector/device';
27+
const WS_DEBUGGER_URL = '/inspector/debug';
28+
const PAGES_LIST_JSON_URL = '/json';
29+
const PAGES_LIST_JSON_URL_2 = '/json/list';
30+
const PAGES_LIST_JSON_VERSION_URL = '/json/version';
31+
32+
const INTERNAL_ERROR_CODE = 1011;
33+
34+
/**
35+
* Main Inspector Proxy class that connects JavaScript VM inside Android/iOS apps and JS debugger.
36+
*/
37+
class InspectorProxy {
38+
// Root of the project used for relative to absolute source path conversion.
39+
_projectRoot: string;
40+
41+
// Maps device ID to Device instance.
42+
_devices: Map<string, Device>;
43+
44+
// Internal counter for device IDs -- just gets incremented for each new device.
45+
_deviceCounter: number = 0;
46+
47+
// We store server's address with port (like '127.0.0.1:8081') to be able to build URLs
48+
// (devtoolsFrontendUrl and webSocketDebuggerUrl) for page descriptions. These URLs are used
49+
// by debugger to know where to connect.
50+
_serverBaseUrl: string = '';
51+
52+
constructor(projectRoot: string) {
53+
this._projectRoot = projectRoot;
54+
this._devices = new Map();
55+
}
56+
57+
// Process HTTP request sent to server. We only respond to 2 HTTP requests:
58+
// 1. /json/version returns Chrome debugger protocol version that we use
59+
// 2. /json and /json/list returns list of page descriptions (list of inspectable apps).
60+
// This list is combined from all the connected devices.
61+
processRequest(
62+
request: IncomingMessage,
63+
response: ServerResponse,
64+
next: (?Error) => mixed,
65+
) {
66+
if (
67+
request.url === PAGES_LIST_JSON_URL ||
68+
request.url === PAGES_LIST_JSON_URL_2
69+
) {
70+
// Build list of pages from all devices.
71+
let result: Array<PageDescription> = [];
72+
Array.from(this._devices.entries()).forEach(([deviceId, device]) => {
73+
result = result.concat(
74+
device
75+
.getPagesList()
76+
.map((page: Page) =>
77+
this._buildPageDescription(deviceId, device, page),
78+
),
79+
);
80+
});
81+
82+
this._sendJsonResponse(response, result);
83+
} else if (request.url === PAGES_LIST_JSON_VERSION_URL) {
84+
this._sendJsonResponse(response, {
85+
Browser: 'Mobile JavaScript',
86+
'Protocol-Version': '1.1',
87+
});
88+
} else {
89+
next();
90+
}
91+
}
92+
93+
createWebSocketListeners(devServerBaseUrl: string): {
94+
[path: string]: WS.Server,
95+
} {
96+
this._serverBaseUrl = devServerBaseUrl;
97+
98+
return {
99+
[WS_DEVICE_URL]: this._createDeviceConnectionWSServer(),
100+
[WS_DEBUGGER_URL]: this._createDebuggerConnectionWSServer(),
101+
};
102+
}
103+
104+
// Converts page information received from device into PageDescription object
105+
// that is sent to debugger.
106+
_buildPageDescription(
107+
deviceId: string,
108+
device: Device,
109+
page: Page,
110+
): PageDescription {
111+
const debuggerUrl = `${this._serverBaseUrl}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;
112+
const webSocketDebuggerUrl = 'ws://' + debuggerUrl;
113+
const devtoolsFrontendUrl =
114+
'devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=' +
115+
encodeURIComponent(debuggerUrl);
116+
return {
117+
id: `${deviceId}-${page.id}`,
118+
description: page.app,
119+
title: page.title,
120+
faviconUrl: 'https://reactjs.org/favicon.ico',
121+
devtoolsFrontendUrl,
122+
type: 'node',
123+
webSocketDebuggerUrl,
124+
vm: page.vm,
125+
deviceName: device.getName(),
126+
};
127+
}
128+
129+
// Sends object as response to HTTP request.
130+
// Just serializes object using JSON and sets required headers.
131+
_sendJsonResponse(
132+
response: ServerResponse,
133+
object: JsonPagesListResponse | JsonVersionResponse,
134+
) {
135+
const data = JSON.stringify(object, null, 2);
136+
response.writeHead(200, {
137+
'Content-Type': 'application/json; charset=UTF-8',
138+
'Cache-Control': 'no-cache',
139+
'Content-Length': data.length.toString(),
140+
Connection: 'close',
141+
});
142+
response.end(data);
143+
}
144+
145+
// Adds websocket handler for device connections.
146+
// Device connects to /inspector/device and passes device and app names as
147+
// HTTP GET params.
148+
// For each new websocket connection we parse device and app names and create
149+
// new instance of Device class.
150+
_createDeviceConnectionWSServer(): ws$WebSocketServer {
151+
const wss = new WS.Server({
152+
noServer: true,
153+
perMessageDeflate: true,
154+
});
155+
// $FlowFixMe[value-as-type]
156+
wss.on('connection', async (socket: WS, req) => {
157+
try {
158+
const fallbackDeviceId = String(this._deviceCounter++);
159+
160+
const query = url.parse(req.url || '', true).query || {};
161+
const deviceId = query.device || fallbackDeviceId;
162+
const deviceName = query.name || 'Unknown';
163+
const appName = query.app || 'Unknown';
164+
165+
const oldDevice = this._devices.get(deviceId);
166+
const newDevice = new Device(
167+
deviceId,
168+
deviceName,
169+
appName,
170+
socket,
171+
this._projectRoot,
172+
);
173+
174+
if (oldDevice) {
175+
oldDevice.handleDuplicateDeviceConnection(newDevice);
176+
}
177+
178+
this._devices.set(deviceId, newDevice);
179+
180+
debug(
181+
`Got new connection: name=${deviceName}, app=${appName}, device=${deviceId}`,
182+
);
183+
184+
socket.on('close', () => {
185+
this._devices.delete(deviceId);
186+
debug(`Device ${deviceName} disconnected.`);
187+
});
188+
} catch (e) {
189+
console.error('error', e);
190+
socket.close(INTERNAL_ERROR_CODE, e?.toString() ?? 'Unknown error');
191+
}
192+
});
193+
return wss;
194+
}
195+
196+
// Returns websocket handler for debugger connections.
197+
// Debugger connects to webSocketDebuggerUrl that we return as part of page description
198+
// in /json response.
199+
// When debugger connects we try to parse device and page IDs from the query and pass
200+
// websocket object to corresponding Device instance.
201+
_createDebuggerConnectionWSServer(): ws$WebSocketServer {
202+
const wss = new WS.Server({
203+
noServer: true,
204+
perMessageDeflate: false,
205+
});
206+
// $FlowFixMe[value-as-type]
207+
wss.on('connection', async (socket: WS, req) => {
208+
try {
209+
const query = url.parse(req.url || '', true).query || {};
210+
const deviceId = query.device;
211+
const pageId = query.page;
212+
213+
if (deviceId == null || pageId == null) {
214+
throw new Error('Incorrect URL - must provide device and page IDs');
215+
}
216+
217+
const device = this._devices.get(deviceId);
218+
if (device == null) {
219+
throw new Error('Unknown device with ID ' + deviceId);
220+
}
221+
222+
device.handleDebuggerConnection(socket, pageId);
223+
} catch (e) {
224+
console.error(e);
225+
socket.close(INTERNAL_ERROR_CODE, e?.toString() ?? 'Unknown error');
226+
}
227+
});
228+
return wss;
229+
}
230+
}
231+
232+
module.exports = InspectorProxy;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
* @format
9+
* @oncall react_native
10+
*/
11+
12+
// Page information received from the device. New page is created for
13+
// each new instance of VM and can appear when user reloads React Native
14+
// application.
15+
export type Page = {
16+
id: string,
17+
title: string,
18+
vm: string,
19+
app: string,
20+
...
21+
};
22+
23+
// Chrome Debugger Protocol message/event passed between device and debugger.
24+
export type WrappedEvent = {
25+
event: 'wrappedEvent',
26+
payload: {
27+
pageId: string,
28+
wrappedEvent: string,
29+
...
30+
},
31+
...
32+
};
33+
34+
// Request sent from Inspector Proxy to Device when new debugger is connected
35+
// to particular page.
36+
export type ConnectRequest = {
37+
event: 'connect',
38+
payload: {pageId: string, ...},
39+
...
40+
};
41+
42+
// Request sent from Inspector Proxy to Device to notify that debugger is
43+
// disconnected.
44+
export type DisconnectRequest = {
45+
event: 'disconnect',
46+
payload: {pageId: string, ...},
47+
...
48+
};
49+
50+
// Request sent from Inspector Proxy to Device to get a list of pages.
51+
export type GetPagesRequest = {event: 'getPages', ...};
52+
53+
// Response to GetPagesRequest containing a list of page infos.
54+
export type GetPagesResponse = {
55+
event: 'getPages',
56+
payload: Array<Page>,
57+
...
58+
};
59+
60+
// Union type for all possible messages sent from device to Inspector Proxy.
61+
export type MessageFromDevice =
62+
| GetPagesResponse
63+
| WrappedEvent
64+
| DisconnectRequest;
65+
66+
// Union type for all possible messages sent from Inspector Proxy to device.
67+
export type MessageToDevice =
68+
| GetPagesRequest
69+
| WrappedEvent
70+
| ConnectRequest
71+
| DisconnectRequest;
72+
73+
// Page description object that is sent in response to /json HTTP request from debugger.
74+
export type PageDescription = {
75+
id: string,
76+
description: string,
77+
title: string,
78+
faviconUrl: string,
79+
devtoolsFrontendUrl: string,
80+
type: string,
81+
webSocketDebuggerUrl: string,
82+
...
83+
};
84+
export type JsonPagesListResponse = Array<PageDescription>;
85+
86+
// Response to /json/version HTTP request from the debugger specifying browser type and
87+
// Chrome protocol version.
88+
export type JsonVersionResponse = {
89+
Browser: string,
90+
'Protocol-Version': string,
91+
...
92+
};
93+
94+
/**
95+
* Types were exported from https://github.com/ChromeDevTools/devtools-protocol/blob/master/types/protocol.d.ts
96+
*/
97+
98+
export type SetBreakpointByUrlRequest = {
99+
id: number,
100+
method: 'Debugger.setBreakpointByUrl',
101+
params: {
102+
lineNumber: number,
103+
url?: string,
104+
urlRegex?: string,
105+
scriptHash?: string,
106+
columnNumber?: number,
107+
condition?: string,
108+
},
109+
};
110+
111+
export type GetScriptSourceRequest = {
112+
id: number,
113+
method: 'Debugger.getScriptSource',
114+
params: {
115+
scriptId: string,
116+
},
117+
};
118+
119+
export type GetScriptSourceResponse = {
120+
scriptSource: string,
121+
/**
122+
* Wasm bytecode.
123+
*/
124+
bytecode?: string,
125+
};
126+
127+
export type ErrorResponse = {
128+
error: {
129+
message: string,
130+
},
131+
};
132+
133+
export type DebuggerRequest =
134+
| SetBreakpointByUrlRequest
135+
| GetScriptSourceRequest;

‎packages/dev-middleware/src/utils/getDevServerUrl.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ export default function getDevServerUrl(req: IncomingMessage): string {
2828
? `[${localAddress}]`
2929
: localAddress;
3030

31-
return `${scheme}:${address}:${localPort}`;
31+
return `${scheme}://${address}:${localPort}`;
3232
}

0 commit comments

Comments
 (0)
Please sign in to comment.