@depup/react-native__dev-middleware 0.84.1-depup.1 → 0.85.0-depup.1

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.
Files changed (44) hide show
  1. package/README.md +3 -3
  2. package/changes.json +3 -3
  3. package/dist/createDevMiddleware.d.ts +18 -9
  4. package/dist/createDevMiddleware.js +7 -17
  5. package/dist/createDevMiddleware.js.flow +21 -12
  6. package/dist/index.d.ts +4 -3
  7. package/dist/index.js +4 -4
  8. package/dist/index.js.flow +4 -3
  9. package/dist/inspector-proxy/CustomMessageHandler.js.flow +3 -3
  10. package/dist/inspector-proxy/Device.d.ts +6 -3
  11. package/dist/inspector-proxy/Device.js +197 -161
  12. package/dist/inspector-proxy/Device.js.flow +9 -6
  13. package/dist/inspector-proxy/DeviceEventReporter.d.ts +0 -3
  14. package/dist/inspector-proxy/DeviceEventReporter.js +0 -10
  15. package/dist/inspector-proxy/DeviceEventReporter.js.flow +5 -8
  16. package/dist/inspector-proxy/InspectorProxy.d.ts +3 -2
  17. package/dist/inspector-proxy/InspectorProxy.js +47 -15
  18. package/dist/inspector-proxy/InspectorProxy.js.flow +3 -2
  19. package/dist/inspector-proxy/__docs__/README.md +324 -0
  20. package/dist/inspector-proxy/cdp-types/messages.js.flow +3 -3
  21. package/dist/inspector-proxy/types.d.ts +24 -9
  22. package/dist/inspector-proxy/types.js.flow +36 -22
  23. package/dist/middleware/openDebuggerMiddleware.d.ts +4 -6
  24. package/dist/middleware/openDebuggerMiddleware.js +12 -22
  25. package/dist/middleware/openDebuggerMiddleware.js.flow +5 -7
  26. package/dist/types/{BrowserLauncher.js.flow → DevToolLauncher.d.ts} +10 -13
  27. package/dist/types/{BrowserLauncher.d.ts → DevToolLauncher.js.flow} +14 -12
  28. package/dist/types/EventReporter.d.ts +24 -27
  29. package/dist/types/EventReporter.js.flow +1 -7
  30. package/dist/types/Experiments.d.ts +8 -4
  31. package/dist/types/Experiments.js.flow +9 -5
  32. package/dist/types/Logger.js.flow +1 -1
  33. package/dist/types/ReadonlyURL.d.ts +53 -0
  34. package/dist/types/ReadonlyURL.js +1 -0
  35. package/dist/types/ReadonlyURL.js.flow +54 -0
  36. package/dist/utils/{DefaultBrowserLauncher.d.ts → DefaultToolLauncher.d.ts} +7 -11
  37. package/dist/utils/{DefaultBrowserLauncher.js → DefaultToolLauncher.js} +21 -4
  38. package/dist/utils/DefaultToolLauncher.js.flow +25 -0
  39. package/dist/utils/getDevToolsFrontendUrl.d.ts +2 -3
  40. package/dist/utils/getDevToolsFrontendUrl.js +3 -6
  41. package/dist/utils/getDevToolsFrontendUrl.js.flow +3 -4
  42. package/package.json +12 -12
  43. package/dist/utils/DefaultBrowserLauncher.js.flow +0 -29
  44. /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 = $ReadOnly<{
15
+ type DeviceMetadata = Readonly<{
16
16
  appId: string,
17
17
  deviceId: string,
18
18
  deviceName: string,
19
19
  }>;
20
20
 
21
- type RequestMetadata = $ReadOnly<{
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 = $ReadOnly<{
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: $ReadOnly<{ id: number, method: string, ... }>,
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: $ReadOnly<{
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: URL;
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: string,
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 = new URL(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 = _url.default.parse(request.url).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.origin,
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 = _url.default.parse(req.url || "", true).query || {};
242
- const deviceId = query.device || fallbackDeviceId;
243
- const deviceName = query.name || "Unknown";
244
- const appName = query.app || "Unknown";
245
- const isProfilingBuild = query.profiling === "true";
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 = _url.default.parse(req.url || "", true).query || {};
368
- const deviceId = query.device;
369
- const pageId = query.page;
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: req.headers["user-agent"] ?? query.userAgent ?? null,
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: URL,
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: string,
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: $Keys<Events> = "unknown"> = {
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: $Keys<Commands> = "unknown"> = {
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: $Keys<Commands> = "unknown"> =
26
+ export type CDPResponse<TCommand: keyof Commands = "unknown"> =
27
27
  | {
28
28
  result: Commands[TCommand]["resultType"],
29
29
  id: number,