@depup/react-native__dev-middleware 0.84.1-depup.1 → 0.85.0-depup.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/changes.json +2 -6
- package/dist/createDevMiddleware.d.ts +18 -9
- package/dist/createDevMiddleware.js +7 -17
- package/dist/createDevMiddleware.js.flow +21 -12
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -4
- package/dist/index.js.flow +4 -3
- package/dist/inspector-proxy/CustomMessageHandler.js.flow +3 -3
- package/dist/inspector-proxy/Device.d.ts +6 -3
- package/dist/inspector-proxy/Device.js +197 -161
- package/dist/inspector-proxy/Device.js.flow +9 -6
- package/dist/inspector-proxy/DeviceEventReporter.d.ts +0 -3
- package/dist/inspector-proxy/DeviceEventReporter.js +0 -10
- package/dist/inspector-proxy/DeviceEventReporter.js.flow +5 -8
- package/dist/inspector-proxy/InspectorProxy.d.ts +3 -2
- package/dist/inspector-proxy/InspectorProxy.js +47 -15
- package/dist/inspector-proxy/InspectorProxy.js.flow +3 -2
- package/dist/inspector-proxy/__docs__/README.md +324 -0
- package/dist/inspector-proxy/cdp-types/messages.js.flow +3 -3
- package/dist/inspector-proxy/types.d.ts +24 -9
- package/dist/inspector-proxy/types.js.flow +36 -22
- package/dist/middleware/openDebuggerMiddleware.d.ts +4 -6
- package/dist/middleware/openDebuggerMiddleware.js +12 -22
- package/dist/middleware/openDebuggerMiddleware.js.flow +5 -7
- package/dist/types/{BrowserLauncher.js.flow → DevToolLauncher.d.ts} +10 -13
- package/dist/types/{BrowserLauncher.d.ts → DevToolLauncher.js.flow} +14 -12
- package/dist/types/EventReporter.d.ts +24 -27
- package/dist/types/EventReporter.js.flow +1 -7
- package/dist/types/Experiments.d.ts +8 -4
- package/dist/types/Experiments.js.flow +9 -5
- package/dist/types/Logger.js.flow +1 -1
- package/dist/types/ReadonlyURL.d.ts +53 -0
- package/dist/types/ReadonlyURL.js +1 -0
- package/dist/types/ReadonlyURL.js.flow +54 -0
- package/dist/utils/{DefaultBrowserLauncher.d.ts → DefaultToolLauncher.d.ts} +7 -11
- package/dist/utils/{DefaultBrowserLauncher.js → DefaultToolLauncher.js} +21 -4
- package/dist/utils/DefaultToolLauncher.js.flow +25 -0
- package/dist/utils/getDevToolsFrontendUrl.d.ts +2 -3
- package/dist/utils/getDevToolsFrontendUrl.js +3 -6
- package/dist/utils/getDevToolsFrontendUrl.js.flow +3 -4
- package/package.json +10 -14
- package/dist/utils/DefaultBrowserLauncher.js.flow +0 -29
- /package/dist/types/{BrowserLauncher.js → DevToolLauncher.js} +0 -0
|
@@ -19,12 +19,10 @@ type DeviceMetadata = Readonly<{
|
|
|
19
19
|
type RequestMetadata = Readonly<{
|
|
20
20
|
pageId: string | null;
|
|
21
21
|
frontendUserAgent: string | null;
|
|
22
|
-
prefersFuseboxFrontend: boolean | null;
|
|
23
22
|
}>;
|
|
24
23
|
type ResponseMetadata = Readonly<{
|
|
25
24
|
pageId: string | null;
|
|
26
25
|
frontendUserAgent: string | null;
|
|
27
|
-
prefersFuseboxFrontend: boolean | null;
|
|
28
26
|
}>;
|
|
29
27
|
declare class DeviceEventReporter {
|
|
30
28
|
constructor(eventReporter: EventReporter, metadata: DeviceMetadata);
|
|
@@ -49,6 +47,5 @@ declare class DeviceEventReporter {
|
|
|
49
47
|
error: Error,
|
|
50
48
|
message: string,
|
|
51
49
|
): void;
|
|
52
|
-
logFuseboxConsoleNotice(): void;
|
|
53
50
|
}
|
|
54
51
|
export default DeviceEventReporter;
|
|
@@ -51,7 +51,6 @@ class DeviceEventReporter {
|
|
|
51
51
|
deviceName: this.#metadata.deviceName,
|
|
52
52
|
pageId: metadata.pageId,
|
|
53
53
|
frontendUserAgent: metadata.frontendUserAgent,
|
|
54
|
-
prefersFuseboxFrontend: metadata.prefersFuseboxFrontend,
|
|
55
54
|
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
|
|
56
55
|
});
|
|
57
56
|
return;
|
|
@@ -78,7 +77,6 @@ class DeviceEventReporter {
|
|
|
78
77
|
deviceName: this.#metadata.deviceName,
|
|
79
78
|
pageId: pendingCommand.metadata.pageId,
|
|
80
79
|
frontendUserAgent: pendingCommand.metadata.frontendUserAgent,
|
|
81
|
-
prefersFuseboxFrontend: metadata.prefersFuseboxFrontend,
|
|
82
80
|
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
|
|
83
81
|
});
|
|
84
82
|
return;
|
|
@@ -96,7 +94,6 @@ class DeviceEventReporter {
|
|
|
96
94
|
deviceName: this.#metadata.deviceName,
|
|
97
95
|
pageId: pendingCommand.metadata.pageId,
|
|
98
96
|
frontendUserAgent: pendingCommand.metadata.frontendUserAgent,
|
|
99
|
-
prefersFuseboxFrontend: metadata.prefersFuseboxFrontend,
|
|
100
97
|
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
|
|
101
98
|
});
|
|
102
99
|
}
|
|
@@ -145,7 +142,6 @@ class DeviceEventReporter {
|
|
|
145
142
|
deviceName: this.#metadata.deviceName,
|
|
146
143
|
pageId: pendingCommand.metadata.pageId,
|
|
147
144
|
frontendUserAgent: pendingCommand.metadata.frontendUserAgent,
|
|
148
|
-
prefersFuseboxFrontend: pendingCommand.metadata.prefersFuseboxFrontend,
|
|
149
145
|
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
|
|
150
146
|
});
|
|
151
147
|
}
|
|
@@ -166,11 +162,6 @@ class DeviceEventReporter {
|
|
|
166
162
|
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
|
|
167
163
|
});
|
|
168
164
|
}
|
|
169
|
-
logFuseboxConsoleNotice() {
|
|
170
|
-
this.#eventReporter.logEvent({
|
|
171
|
-
type: "fusebox_console_notice",
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
165
|
#logExpiredCommand(pendingCommand) {
|
|
175
166
|
this.#eventReporter.logEvent({
|
|
176
167
|
type: "debugger_command",
|
|
@@ -186,7 +177,6 @@ class DeviceEventReporter {
|
|
|
186
177
|
deviceName: this.#metadata.deviceName,
|
|
187
178
|
pageId: pendingCommand.metadata.pageId,
|
|
188
179
|
frontendUserAgent: pendingCommand.metadata.frontendUserAgent,
|
|
189
|
-
prefersFuseboxFrontend: pendingCommand.metadata.prefersFuseboxFrontend,
|
|
190
180
|
connectionUptime: this.#deviceConnectedTimestamp - Date.now(),
|
|
191
181
|
});
|
|
192
182
|
}
|
|
@@ -12,28 +12,26 @@ import type { EventReporter } from "../types/EventReporter";
|
|
|
12
12
|
import type { CDPResponse } from "./cdp-types/messages";
|
|
13
13
|
import type { DeepReadOnly } from "./types";
|
|
14
14
|
|
|
15
|
-
type DeviceMetadata =
|
|
15
|
+
type DeviceMetadata = Readonly<{
|
|
16
16
|
appId: string,
|
|
17
17
|
deviceId: string,
|
|
18
18
|
deviceName: string,
|
|
19
19
|
}>;
|
|
20
20
|
|
|
21
|
-
type RequestMetadata =
|
|
21
|
+
type RequestMetadata = Readonly<{
|
|
22
22
|
pageId: string | null,
|
|
23
23
|
frontendUserAgent: string | null,
|
|
24
|
-
prefersFuseboxFrontend: boolean | null,
|
|
25
24
|
}>;
|
|
26
25
|
|
|
27
|
-
type ResponseMetadata =
|
|
26
|
+
type ResponseMetadata = Readonly<{
|
|
28
27
|
pageId: string | null,
|
|
29
28
|
frontendUserAgent: string | null,
|
|
30
|
-
prefersFuseboxFrontend: boolean | null,
|
|
31
29
|
}>;
|
|
32
30
|
|
|
33
31
|
declare class DeviceEventReporter {
|
|
34
32
|
constructor(eventReporter: EventReporter, metadata: DeviceMetadata): void;
|
|
35
33
|
logRequest(
|
|
36
|
-
req:
|
|
34
|
+
req: Readonly<{ id: number, method: string, ... }>,
|
|
37
35
|
origin: "debugger" | "proxy",
|
|
38
36
|
metadata: RequestMetadata,
|
|
39
37
|
): void;
|
|
@@ -45,7 +43,7 @@ declare class DeviceEventReporter {
|
|
|
45
43
|
logProfilingTargetRegistered(): void;
|
|
46
44
|
logConnection(
|
|
47
45
|
connectedEntity: "debugger",
|
|
48
|
-
metadata:
|
|
46
|
+
metadata: Readonly<{
|
|
49
47
|
pageId: string,
|
|
50
48
|
frontendUserAgent: string | null,
|
|
51
49
|
}>,
|
|
@@ -56,7 +54,6 @@ declare class DeviceEventReporter {
|
|
|
56
54
|
error: Error,
|
|
57
55
|
message: string,
|
|
58
56
|
): void;
|
|
59
|
-
logFuseboxConsoleNotice(): void;
|
|
60
57
|
}
|
|
61
58
|
|
|
62
59
|
declare export default typeof DeviceEventReporter;
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
import type { EventReporter } from "../types/EventReporter";
|
|
12
12
|
import type { Experiments } from "../types/Experiments";
|
|
13
13
|
import type { Logger } from "../types/Logger";
|
|
14
|
+
import type { ReadonlyURL } from "../types/ReadonlyURL";
|
|
14
15
|
import type { CreateCustomMessageHandlerFn } from "./CustomMessageHandler";
|
|
15
16
|
import type { PageDescription } from "./types";
|
|
16
17
|
import type { IncomingMessage, ServerResponse } from "http";
|
|
17
18
|
import WS from "ws";
|
|
18
19
|
export type GetPageDescriptionsConfig = {
|
|
19
|
-
requestorRelativeBaseUrl:
|
|
20
|
+
requestorRelativeBaseUrl: ReadonlyURL;
|
|
20
21
|
logNoPagesForConnectedDevice?: boolean;
|
|
21
22
|
};
|
|
22
23
|
export interface InspectorProxyQueries {
|
|
@@ -33,7 +34,7 @@ export interface InspectorProxyQueries {
|
|
|
33
34
|
*/
|
|
34
35
|
declare class InspectorProxy implements InspectorProxyQueries {
|
|
35
36
|
constructor(
|
|
36
|
-
serverBaseUrl:
|
|
37
|
+
serverBaseUrl: ReadonlyURL,
|
|
37
38
|
eventReporter: null | undefined | EventReporter,
|
|
38
39
|
experiments: Experiments,
|
|
39
40
|
logger?: Logger,
|
|
@@ -18,12 +18,17 @@ var _InspectorProxyHeartbeat = _interopRequireDefault(
|
|
|
18
18
|
require("./InspectorProxyHeartbeat"),
|
|
19
19
|
);
|
|
20
20
|
var _nullthrows = _interopRequireDefault(require("nullthrows"));
|
|
21
|
-
var _url = _interopRequireDefault(require("url"));
|
|
22
21
|
var _ws = _interopRequireDefault(require("ws"));
|
|
23
22
|
function _interopRequireDefault(e) {
|
|
24
23
|
return e && e.__esModule ? e : { default: e };
|
|
25
24
|
}
|
|
26
25
|
const debug = require("debug")("Metro:InspectorProxy");
|
|
26
|
+
const WS_DEBUGGER_ALLOWED_ORIGIN_HOSTNAMES = new Set([
|
|
27
|
+
"localhost",
|
|
28
|
+
"127.0.0.1",
|
|
29
|
+
"0.0.0.0",
|
|
30
|
+
"[::]",
|
|
31
|
+
]);
|
|
27
32
|
const WS_DEVICE_URL = "/inspector/device";
|
|
28
33
|
const WS_DEBUGGER_URL = "/inspector/debug";
|
|
29
34
|
const PAGES_LIST_JSON_URL = "/json";
|
|
@@ -59,7 +64,7 @@ class InspectorProxy {
|
|
|
59
64
|
customMessageHandler,
|
|
60
65
|
trackEventLoopPerf = false,
|
|
61
66
|
) {
|
|
62
|
-
this.#serverBaseUrl =
|
|
67
|
+
this.#serverBaseUrl = serverBaseUrl;
|
|
63
68
|
this.#devices = new Map();
|
|
64
69
|
this.#eventReporter = eventReporter;
|
|
65
70
|
this.#experiments = experiments;
|
|
@@ -139,7 +144,7 @@ class InspectorProxy {
|
|
|
139
144
|
return result;
|
|
140
145
|
}
|
|
141
146
|
processRequest(request, response, next) {
|
|
142
|
-
const pathname =
|
|
147
|
+
const pathname = new URL(request.url, "http://example.com").pathname;
|
|
143
148
|
if (
|
|
144
149
|
pathname === PAGES_LIST_JSON_URL ||
|
|
145
150
|
pathname === PAGES_LIST_JSON_URL_2
|
|
@@ -175,10 +180,9 @@ class InspectorProxy {
|
|
|
175
180
|
const devtoolsFrontendUrl = (0, _getDevToolsFrontendUrl.default)(
|
|
176
181
|
this.#experiments,
|
|
177
182
|
webSocketDebuggerUrl,
|
|
178
|
-
this.#serverBaseUrl
|
|
183
|
+
new URL(this.#serverBaseUrl),
|
|
179
184
|
{
|
|
180
185
|
relative: true,
|
|
181
|
-
useFuseboxEntryPoint: page.capabilities.prefersFuseboxFrontend,
|
|
182
186
|
},
|
|
183
187
|
);
|
|
184
188
|
return {
|
|
@@ -238,11 +242,11 @@ class InspectorProxy {
|
|
|
238
242
|
wss.on("connection", async (socket, req) => {
|
|
239
243
|
const wssTimestamp = Date.now();
|
|
240
244
|
const fallbackDeviceId = String(this.#deviceCounter++);
|
|
241
|
-
const query =
|
|
242
|
-
const deviceId = query
|
|
243
|
-
const deviceName = query
|
|
244
|
-
const appName = query
|
|
245
|
-
const isProfilingBuild = query
|
|
245
|
+
const query = tryParseQueryParams(req.url);
|
|
246
|
+
const deviceId = query?.get("device") || fallbackDeviceId;
|
|
247
|
+
const deviceName = query?.get("name") || "Unknown";
|
|
248
|
+
const appName = query?.get("app") || "Unknown";
|
|
249
|
+
const isProfilingBuild = query?.get("profiling") === "true";
|
|
246
250
|
try {
|
|
247
251
|
const deviceRelativeBaseUrl =
|
|
248
252
|
(0, _getBaseUrlFromRequest.default)(req) ?? this.#serverBaseUrl;
|
|
@@ -258,6 +262,7 @@ class InspectorProxy {
|
|
|
258
262
|
deviceRelativeBaseUrl,
|
|
259
263
|
serverRelativeBaseUrl: this.#serverBaseUrl,
|
|
260
264
|
isProfilingBuild,
|
|
265
|
+
experiments: this.#experiments,
|
|
261
266
|
};
|
|
262
267
|
if (oldDevice) {
|
|
263
268
|
oldDevice.dangerouslyRecreateDevice(deviceOptions);
|
|
@@ -361,12 +366,31 @@ class InspectorProxy {
|
|
|
361
366
|
noServer: true,
|
|
362
367
|
perMessageDeflate: false,
|
|
363
368
|
maxPayload: 0,
|
|
369
|
+
verifyClient: (info) => {
|
|
370
|
+
if (this.#serverBaseUrl.origin === info.origin) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
if (URL.canParse(info.origin)) {
|
|
374
|
+
const { hostname } = new URL(info.origin);
|
|
375
|
+
if (WS_DEBUGGER_ALLOWED_ORIGIN_HOSTNAMES.has(hostname)) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
this.#logger?.error(
|
|
380
|
+
"Connection from DevTools failed to be established for origin '%s' and path '%s'. Was expecting origin: '%s', or origin hostname to be one of: %s",
|
|
381
|
+
info.origin,
|
|
382
|
+
info.req.url,
|
|
383
|
+
this.#serverBaseUrl.origin,
|
|
384
|
+
Array.from(WS_DEBUGGER_ALLOWED_ORIGIN_HOSTNAMES).join(", "),
|
|
385
|
+
);
|
|
386
|
+
return false;
|
|
387
|
+
},
|
|
364
388
|
});
|
|
365
389
|
wss.on("connection", async (socket, req) => {
|
|
366
390
|
const wssTimestamp = Date.now();
|
|
367
|
-
const query =
|
|
368
|
-
const deviceId = query
|
|
369
|
-
const pageId = query
|
|
391
|
+
const query = tryParseQueryParams(req.url);
|
|
392
|
+
const deviceId = query?.get("device") || null;
|
|
393
|
+
const pageId = query?.get("page") || null;
|
|
370
394
|
const debuggerRelativeBaseUrl =
|
|
371
395
|
(0, _getBaseUrlFromRequest.default)(req) ?? this.#serverBaseUrl;
|
|
372
396
|
const device = deviceId ? this.#devices.get(deviceId) : undefined;
|
|
@@ -435,7 +459,8 @@ class InspectorProxy {
|
|
|
435
459
|
);
|
|
436
460
|
device.handleDebuggerConnection(socket, pageId, {
|
|
437
461
|
debuggerRelativeBaseUrl,
|
|
438
|
-
userAgent:
|
|
462
|
+
userAgent:
|
|
463
|
+
req.headers["user-agent"] ?? query?.get("userAgent") ?? null,
|
|
439
464
|
});
|
|
440
465
|
socket.on("close", (code, reason) => {
|
|
441
466
|
debug(
|
|
@@ -459,7 +484,7 @@ class InspectorProxy {
|
|
|
459
484
|
"Connection failed to be established with DevTools for app='%s' on device='%s' and device id='%s' with error:",
|
|
460
485
|
device?.getApp() || "unknown",
|
|
461
486
|
device?.getName() || "unknown",
|
|
462
|
-
deviceId,
|
|
487
|
+
deviceId || "unknown",
|
|
463
488
|
error,
|
|
464
489
|
);
|
|
465
490
|
socket.close(INTERNAL_ERROR_CODE, error?.toString() ?? "Unknown error");
|
|
@@ -475,3 +500,10 @@ class InspectorProxy {
|
|
|
475
500
|
}
|
|
476
501
|
}
|
|
477
502
|
exports.default = InspectorProxy;
|
|
503
|
+
function tryParseQueryParams(urlString) {
|
|
504
|
+
try {
|
|
505
|
+
return new URL(urlString, "http://example.com").searchParams;
|
|
506
|
+
} catch {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { EventReporter } from "../types/EventReporter";
|
|
12
12
|
import type { Experiments } from "../types/Experiments";
|
|
13
13
|
import type { Logger } from "../types/Logger";
|
|
14
|
+
import type { ReadonlyURL } from "../types/ReadonlyURL";
|
|
14
15
|
import type { CreateCustomMessageHandlerFn } from "./CustomMessageHandler";
|
|
15
16
|
import type { PageDescription } from "./types";
|
|
16
17
|
import type { IncomingMessage, ServerResponse } from "http";
|
|
@@ -18,7 +19,7 @@ import type { IncomingMessage, ServerResponse } from "http";
|
|
|
18
19
|
import WS from "ws";
|
|
19
20
|
|
|
20
21
|
export type GetPageDescriptionsConfig = {
|
|
21
|
-
requestorRelativeBaseUrl:
|
|
22
|
+
requestorRelativeBaseUrl: ReadonlyURL,
|
|
22
23
|
logNoPagesForConnectedDevice?: boolean,
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -37,7 +38,7 @@ export interface InspectorProxyQueries {
|
|
|
37
38
|
*/
|
|
38
39
|
declare export default class InspectorProxy implements InspectorProxyQueries {
|
|
39
40
|
constructor(
|
|
40
|
-
serverBaseUrl:
|
|
41
|
+
serverBaseUrl: ReadonlyURL,
|
|
41
42
|
eventReporter: ?EventReporter,
|
|
42
43
|
experiments: Experiments,
|
|
43
44
|
logger?: Logger,
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Inspector Proxy Protocol
|
|
2
|
+
|
|
3
|
+
[🏠 Home](../../../../../__docs__/README.md)
|
|
4
|
+
|
|
5
|
+
The inspector-proxy protocol facilitates Chrome DevTools Protocol (CDP) target
|
|
6
|
+
discovery and communication between **debuggers** (e.g., Chrome DevTools, VS
|
|
7
|
+
Code) and **devices** (processes containing React Native hosts). The proxy
|
|
8
|
+
multiplexes connections over a single WebSocket per device, allowing multiple
|
|
9
|
+
debuggers to connect to multiple pages on the same device.
|
|
10
|
+
|
|
11
|
+
## 🚀 Usage
|
|
12
|
+
|
|
13
|
+
### Target Discovery (HTTP)
|
|
14
|
+
|
|
15
|
+
We implement a subset of the
|
|
16
|
+
[Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)'s
|
|
17
|
+
[HTTP endpoints](https://chromedevtools.github.io/devtools-protocol/#:~:text=a%20reconnect%20button.-,HTTP%20Endpoints,-If%20started%20with)
|
|
18
|
+
to allow debuggers to discover targets.
|
|
19
|
+
|
|
20
|
+
| Endpoint | Description |
|
|
21
|
+
| --------------------------- | ------------------------ |
|
|
22
|
+
| `GET /json` or `/json/list` | List of debuggable pages |
|
|
23
|
+
| `GET /json/version` | Protocol version info |
|
|
24
|
+
|
|
25
|
+
### Device Registration (WebSocket)
|
|
26
|
+
|
|
27
|
+
Devices register themselves with the proxy by connecting to `/inspector/device`:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
ws://{host}/inspector/device?device={id}&name={name}&app={bundle_id}&profiling={true|false}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Parameter | Required | Description |
|
|
34
|
+
| ----------- | -------- | ------------------------------------------------------------ |
|
|
35
|
+
| `device` | No\* | Logical device identifier. Auto-generated if omitted. |
|
|
36
|
+
| `name` | No | Human-readable device name. Defaults to "Unknown". |
|
|
37
|
+
| `app` | No | App bundle identifier. Defaults to "Unknown". |
|
|
38
|
+
| `profiling` | No | "true" if this is a profiling build. (Used for logging only) |
|
|
39
|
+
|
|
40
|
+
\*Recommended for connection persistence across app restarts.
|
|
41
|
+
|
|
42
|
+
#### Requirements for the `device` parameter
|
|
43
|
+
|
|
44
|
+
The intent of the logical device ID is to help with target discovery and
|
|
45
|
+
especially *re*discovery - to reduce the number of times users need to
|
|
46
|
+
explicitly close and restart the debugger frontend (e.g. after an app crash).
|
|
47
|
+
|
|
48
|
+
If provided, the logical device ID:
|
|
49
|
+
|
|
50
|
+
1. SHOULD be stable for the current combination of physical device (or emulator
|
|
51
|
+
instance) and app.
|
|
52
|
+
2. SHOULD be stable across installs/launches of the same app on the same device
|
|
53
|
+
(or emulator instance), though it MAY be user-resettable (so as to not
|
|
54
|
+
require any special privacy permissions).
|
|
55
|
+
3. MUST be unique across different apps on the same physical device (or
|
|
56
|
+
emulator).
|
|
57
|
+
4. MUST be unique across physical devices (or emulators).
|
|
58
|
+
5. MUST be unique for each concurrent _instance_ of the same app on the same
|
|
59
|
+
physical device (or emulator).
|
|
60
|
+
|
|
61
|
+
NOTE: The uniqueness requirements are stronger (MUST) than the stability
|
|
62
|
+
requirements (SHOULD). In particular, on platforms that allow multiple instances
|
|
63
|
+
of the same app to run concurrently, requirements 1 and/or 2 MAY be violated in
|
|
64
|
+
order to meet requirement 5. This is relevant, for example, on desktop
|
|
65
|
+
platforms.
|
|
66
|
+
|
|
67
|
+
### Debugger Connection (WebSocket)
|
|
68
|
+
|
|
69
|
+
Debuggers connect to `/inspector/debug` to form a CDP session with a page:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
ws://{host}/inspector/debug?device={device_id}&page={page_id}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Both `device` and `page` query parameters are required.
|
|
76
|
+
|
|
77
|
+
## 📐 Design
|
|
78
|
+
|
|
79
|
+
### Architecture
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
┌─────────────────┐ ┌─────────────────────────┐ ┌────────────────┐
|
|
83
|
+
│ Debugger │────▶│ Inspector Proxy │◀────│ Device │
|
|
84
|
+
│ (Chrome/VSCode) │ │ (Node.js) │ │ (iOS/Android) │
|
|
85
|
+
└─────────────────┘ └─────────────────────────┘ └────────────────┘
|
|
86
|
+
WebSocket HTTP + WebSocket WebSocket
|
|
87
|
+
/inspector/debug /json, /json/list /inspector/device
|
|
88
|
+
/json/version
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Device ↔ Proxy Protocol
|
|
92
|
+
|
|
93
|
+
All messages are JSON-encoded WebSocket text frames:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
interface Message {
|
|
97
|
+
event: string;
|
|
98
|
+
payload?: /* depends on event */;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Proxy → Device Messages
|
|
103
|
+
|
|
104
|
+
| Event | Payload | Description |
|
|
105
|
+
| -------------- | ------------------------------------------------------------- | --------------------------------------------- |
|
|
106
|
+
| `getPages` | _(none)_ | Request current page list. Sent periodically. |
|
|
107
|
+
| `connect` | `{ pageId: string, sessionId: string }` | Prepare for debugger connection to page. |
|
|
108
|
+
| `disconnect` | `{ pageId: string, sessionId: string }` | Terminate debugger session for page. |
|
|
109
|
+
| `wrappedEvent` | `{ pageId: string, sessionId: string, wrappedEvent: string }` | Forward CDP message (JSON string) to page. |
|
|
110
|
+
|
|
111
|
+
#### Device → Proxy Messages
|
|
112
|
+
|
|
113
|
+
| Event | Payload | Description |
|
|
114
|
+
| -------------- | -------------------------------------------------------------- | ----------------------------------------------------- |
|
|
115
|
+
| `getPages` | `Page[]` | Current list of inspectable pages. |
|
|
116
|
+
| `disconnect` | `{ pageId: string, sessionId?: string }` | Notify that page disconnected or rejected connection. |
|
|
117
|
+
| `wrappedEvent` | `{ pageId: string, sessionId?: string, wrappedEvent: string }` | Forward CDP message (JSON string) from page. |
|
|
118
|
+
|
|
119
|
+
#### Page Object
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
interface Page {
|
|
123
|
+
id: string; // Unique page identifier (typically numeric string)
|
|
124
|
+
title: string; // Display title
|
|
125
|
+
app: string; // App bundle identifier
|
|
126
|
+
description?: string; // Additional description
|
|
127
|
+
capabilities?: {
|
|
128
|
+
nativePageReloads?: boolean; // Target keeps the socket open across reloads
|
|
129
|
+
nativeSourceCodeFetching?: boolean; // Target supports Network.loadNetworkResource
|
|
130
|
+
supportsMultipleDebuggers?: boolean; // Supports concurrent debugger sessions
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Note**: The value of `supportsMultipleDebuggers` SHOULD be consistent across
|
|
136
|
+
all pages for a given device.
|
|
137
|
+
|
|
138
|
+
### Connection Lifecycle
|
|
139
|
+
|
|
140
|
+
**Device Registration:**
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
Device Proxy
|
|
144
|
+
│ │
|
|
145
|
+
│──── WS Connect ─────────────────▶│
|
|
146
|
+
│ /inspector/device?... │
|
|
147
|
+
│ │
|
|
148
|
+
│◀──── getPages ───────────────────│ (periodically)
|
|
149
|
+
│ │
|
|
150
|
+
│───── getPages response ─────────▶│
|
|
151
|
+
│ (page list) │
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Debugger Session:**
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
Debugger Proxy Device
|
|
158
|
+
│ │ │
|
|
159
|
+
│── WS Connect ───▶│ │
|
|
160
|
+
│ ?device&page │── connect ────────────────▶│
|
|
161
|
+
│ │ {pageId, sessionId} │
|
|
162
|
+
│ │ │
|
|
163
|
+
│── CDP Request ──▶│── wrappedEvent ───────────▶│
|
|
164
|
+
│ │ {pageId, sessionId, │
|
|
165
|
+
│ │ wrappedEvent} │
|
|
166
|
+
│ │ │
|
|
167
|
+
│ │◀── wrappedEvent ───────────│
|
|
168
|
+
│◀── CDP Response ─│ {pageId, sessionId, │
|
|
169
|
+
│ │ wrappedEvent} │
|
|
170
|
+
│ │ │
|
|
171
|
+
│── WS Close ─────▶│── disconnect ─────────────▶│
|
|
172
|
+
│ │ {pageId, sessionId} │
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Connection Rejection:**
|
|
176
|
+
|
|
177
|
+
If a device cannot accept a `connect` (e.g., page doesn't exist), it should send
|
|
178
|
+
a `disconnect` back to the proxy for that `pageId`.
|
|
179
|
+
|
|
180
|
+
### Connection Semantics
|
|
181
|
+
|
|
182
|
+
#### Multi-Debugger Support
|
|
183
|
+
|
|
184
|
+
Multiple debuggers can connect simultaneously to the same page when **both** the
|
|
185
|
+
proxy and device support session multiplexing:
|
|
186
|
+
|
|
187
|
+
1. **Session IDs**: The proxy assigns a unique, non-empty `sessionId` to each
|
|
188
|
+
debugger connection. All messages include this `sessionId` for routing. This
|
|
189
|
+
SHOULD be a UUID or other suitably unique and ephemeral identifier.
|
|
190
|
+
|
|
191
|
+
2. **Capability Detection**: Devices report `supportsMultipleDebuggers: true` in
|
|
192
|
+
their page capabilities to indicate session support.
|
|
193
|
+
|
|
194
|
+
3. **Backwards Compatibility**: Legacy devices ignore `sessionId` fields in
|
|
195
|
+
incoming messages and don't include them in responses.
|
|
196
|
+
|
|
197
|
+
#### Connection Rules
|
|
198
|
+
|
|
199
|
+
1. **Session-Capable Device**: Multiple debuggers can connect to the same page
|
|
200
|
+
simultaneously. Each connection has an independent session.
|
|
201
|
+
|
|
202
|
+
2. **Legacy Device (no `supportsMultipleDebuggers`)**: New debugger connections
|
|
203
|
+
to an already-connected page disconnect the existing debugger. The proxy MUST
|
|
204
|
+
NOT allow multiple debuggers to connect to the same page.
|
|
205
|
+
|
|
206
|
+
3. **Device Reconnection**: If a device reconnects with the same `device` ID
|
|
207
|
+
while debugger connections to the same logical device are open in the proxy,
|
|
208
|
+
the proxy may attempt to preserve active debugger sessions by forwarding them
|
|
209
|
+
to the new device.
|
|
210
|
+
|
|
211
|
+
### WebSocket Close Reasons
|
|
212
|
+
|
|
213
|
+
The proxy uses specific close reasons that DevTools frontends may recognize:
|
|
214
|
+
|
|
215
|
+
| Reason | Context |
|
|
216
|
+
| ----------------------- | --------------------------------------- |
|
|
217
|
+
| `[PAGE_NOT_FOUND]` | Debugger connected to non-existent page |
|
|
218
|
+
| `[CONNECTION_LOST]` | Device disconnected |
|
|
219
|
+
| `[RECREATING_DEVICE]` | Device is reconnecting |
|
|
220
|
+
| `[NEW_DEBUGGER_OPENED]` | Another debugger took over this page |
|
|
221
|
+
| `[UNREGISTERED_DEVICE]` | Device ID not found |
|
|
222
|
+
| `[INCORRECT_URL]` | Missing device/page query parameters |
|
|
223
|
+
|
|
224
|
+
### PageDescription (HTTP Response)
|
|
225
|
+
|
|
226
|
+
The `/json` endpoint returns enriched page descriptions based on those reported
|
|
227
|
+
by the device.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
interface PageDescription {
|
|
231
|
+
// Used for target selection
|
|
232
|
+
id: string; // "{deviceId}-{pageId}"
|
|
233
|
+
|
|
234
|
+
// Used for display
|
|
235
|
+
title: string;
|
|
236
|
+
description: string;
|
|
237
|
+
deviceName: string;
|
|
238
|
+
|
|
239
|
+
// Used for target matching
|
|
240
|
+
appId: string;
|
|
241
|
+
|
|
242
|
+
// Used for debugger connection
|
|
243
|
+
webSocketDebuggerUrl: string;
|
|
244
|
+
|
|
245
|
+
// React Native-specific metadata
|
|
246
|
+
reactNative: {
|
|
247
|
+
logicalDeviceId: string; // Used for target matching
|
|
248
|
+
capabilities: {
|
|
249
|
+
nativePageReloads?: boolean; // Used for target filtering
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## 🔗 Relationship with other systems
|
|
256
|
+
|
|
257
|
+
### Part of this
|
|
258
|
+
|
|
259
|
+
- **Device.js** - Per-device connection handler in the proxy
|
|
260
|
+
- **InspectorProxy.js** - Main proxy HTTP/WebSocket server
|
|
261
|
+
|
|
262
|
+
### Used by this
|
|
263
|
+
|
|
264
|
+
- **Chrome DevTools Protocol (CDP)** - The wrapped messages are CDP messages
|
|
265
|
+
exchanged between DevTools frontends and JavaScript runtimes.
|
|
266
|
+
- **WebSocket** - Transport layer for device and debugger connections.
|
|
267
|
+
|
|
268
|
+
### Uses this
|
|
269
|
+
|
|
270
|
+
- **InspectorPackagerConnection (C++)** - Shared device-side protocol
|
|
271
|
+
implementation in `ReactCommon/jsinspector-modern/`.
|
|
272
|
+
- **Platform layers** - iOS (`RCTInspectorDevServerHelper.mm`), Android
|
|
273
|
+
(`DevServerHelper.kt`), and ReactCxxPlatform (`Inspector.cpp`) provide
|
|
274
|
+
WebSocket I/O and threading.
|
|
275
|
+
- **openDebuggerMiddleware** - Uses `/json` to discover targets for the
|
|
276
|
+
`/open-debugger` endpoint.
|
|
277
|
+
- **OpenDebuggerKeyboardHandler** - Uses `/json` to display target selection in
|
|
278
|
+
the CLI.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Legacy Features
|
|
283
|
+
|
|
284
|
+
The following features exist for backward compatibility with older React Native
|
|
285
|
+
targets that lack modern capabilities. New implementations should set
|
|
286
|
+
appropriate capability flags and may ignore this section.
|
|
287
|
+
|
|
288
|
+
### Synthetic Reloadable Page (Page ID `-1`)
|
|
289
|
+
|
|
290
|
+
For targets without the `nativePageReloads` capability, the proxy exposes a
|
|
291
|
+
synthetic page with ID `-1` titled "React Native Experimental (Improved Chrome
|
|
292
|
+
Reloads)". Debuggers connecting to this page are automatically redirected to the
|
|
293
|
+
most recent React Native page, surviving page reloads.
|
|
294
|
+
|
|
295
|
+
When a new React Native page appears while a debugger is connected to `-1`:
|
|
296
|
+
|
|
297
|
+
1. Proxy sends `disconnect` for the old page, `connect` for the new page
|
|
298
|
+
2. Proxy sends `Runtime.enable` and `Debugger.enable` CDP commands to the new
|
|
299
|
+
page
|
|
300
|
+
3. When `Runtime.executionContextCreated` is received, proxy sends
|
|
301
|
+
`Runtime.executionContextsCleared` to debugger, then `Debugger.resume` to
|
|
302
|
+
device
|
|
303
|
+
|
|
304
|
+
### URL Rewriting
|
|
305
|
+
|
|
306
|
+
For targets without the `nativeSourceCodeFetching` capability, the proxy
|
|
307
|
+
rewrites URLs in CDP messages:
|
|
308
|
+
|
|
309
|
+
- **Debugger.scriptParsed** (device → debugger): Device-relative URLs are
|
|
310
|
+
rewritten to debugger-relative URLs
|
|
311
|
+
- **Debugger.setBreakpointByUrl** (debugger → device): URLs are rewritten back
|
|
312
|
+
to device-relative form
|
|
313
|
+
- **Debugger.getScriptSource**: Intercepted and handled by proxy via HTTP fetch
|
|
314
|
+
- **Network.loadNetworkResource**: Returns CDP error (code -32601) to force
|
|
315
|
+
frontend fallback
|
|
316
|
+
|
|
317
|
+
Additionally, if a script URL matches `^[0-9a-z]+$` (alphanumeric ID), the proxy
|
|
318
|
+
prepends `file://` to ensure Chrome downloads source maps.
|
|
319
|
+
|
|
320
|
+
### Legacy Reload Notification
|
|
321
|
+
|
|
322
|
+
For targets without `nativePageReloads`, when a `disconnect` event is received
|
|
323
|
+
for a page, the proxy sends `{method: 'reload'}` to the connected debugger to
|
|
324
|
+
signal a page reload.
|
|
@@ -12,18 +12,18 @@ import type { JSONSerializable } from "../types";
|
|
|
12
12
|
import type { Commands, Events } from "./protocol";
|
|
13
13
|
|
|
14
14
|
// Note: A CDP event is a JSON-RPC notification with no `id` member.
|
|
15
|
-
export type CDPEvent<TEvent:
|
|
15
|
+
export type CDPEvent<TEvent: keyof Events = "unknown"> = {
|
|
16
16
|
method: TEvent,
|
|
17
17
|
params: Events[TEvent],
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export type CDPRequest<TCommand:
|
|
20
|
+
export type CDPRequest<TCommand: keyof Commands = "unknown"> = {
|
|
21
21
|
method: TCommand,
|
|
22
22
|
params: Commands[TCommand]["paramsType"],
|
|
23
23
|
id: number,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
export type CDPResponse<TCommand:
|
|
26
|
+
export type CDPResponse<TCommand: keyof Commands = "unknown"> =
|
|
27
27
|
| {
|
|
28
28
|
result: Commands[TCommand]["resultType"],
|
|
29
29
|
id: number,
|