@react-native/dev-middleware 0.75.0-rc.2 → 0.76.0-nightly-20240628-TEMP

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.
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { EventReporter } from "../types/EventReporter";
12
12
  import type { CDPResponse } from "./cdp-types/messages";
13
+ import type { DeepReadOnly } from "./types";
13
14
  type DeviceMetadata = Readonly<{
14
15
  appId: string;
15
16
  deviceId: string;
@@ -33,7 +34,7 @@ declare class DeviceEventReporter {
33
34
  metadata: RequestMetadata
34
35
  ): void;
35
36
  logResponse(
36
- res: CDPResponse<>,
37
+ res: DeepReadOnly<CDPResponse<>>,
37
38
  origin: "device" | "proxy",
38
39
  metadata: ResponseMetadata
39
40
  ): void;
@@ -8,23 +8,12 @@ var _ttlcache = _interopRequireDefault(require("@isaacs/ttlcache"));
8
8
  function _interopRequireDefault(obj) {
9
9
  return obj && obj.__esModule ? obj : { default: obj };
10
10
  }
11
- /**
12
- * Copyright (c) Meta Platforms, Inc. and affiliates.
13
- *
14
- * This source code is licensed under the MIT license found in the
15
- * LICENSE file in the root directory of this source tree.
16
- *
17
- *
18
- * @format
19
- */
20
-
21
11
  class DeviceEventReporter {
22
12
  #eventReporter;
23
13
  #pendingCommands = new _ttlcache.default({
24
14
  ttl: 10000,
25
15
  dispose: (command, id, reason) => {
26
16
  if (reason === "delete" || reason === "set") {
27
- // TODO: Report clobbering ('set') using a dedicated error code
28
17
  return;
29
18
  }
30
19
  this.#logExpiredCommand(command);
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { EventReporter } from "../types/EventReporter";
12
12
  import type { CDPResponse } from "./cdp-types/messages";
13
+ import type { DeepReadOnly } from "./types";
13
14
 
14
15
  type DeviceMetadata = $ReadOnly<{
15
16
  appId: string,
@@ -37,7 +38,7 @@ declare class DeviceEventReporter {
37
38
  metadata: RequestMetadata
38
39
  ): void;
39
40
  logResponse(
40
- res: CDPResponse<>,
41
+ res: DeepReadOnly<CDPResponse<>>,
41
42
  origin: "device" | "proxy",
42
43
  metadata: ResponseMetadata
43
44
  ): void;
@@ -16,6 +16,10 @@ import type { PageDescription } from "./types";
16
16
  import type { IncomingMessage, ServerResponse } from "http";
17
17
  import WS from "ws";
18
18
  export interface InspectorProxyQueries {
19
+ /**
20
+ * Returns list of page descriptions ordered by device connection order, then
21
+ * page addition order.
22
+ */
19
23
  getPageDescriptions(): Array<PageDescription>;
20
24
  }
21
25
  /**
@@ -12,22 +12,6 @@ var _ws = _interopRequireDefault(require("ws"));
12
12
  function _interopRequireDefault(obj) {
13
13
  return obj && obj.__esModule ? obj : { default: obj };
14
14
  }
15
- /**
16
- * Copyright (c) Meta Platforms, Inc. and affiliates.
17
- *
18
- * This source code is licensed under the MIT license found in the
19
- * LICENSE file in the root directory of this source tree.
20
- *
21
- *
22
- * @format
23
- * @oncall react_native
24
- */
25
-
26
- // $FlowFixMe[cannot-resolve-module] libdef missing in RN OSS
27
-
28
- // Import these from node:timers to get the correct Flow types.
29
- // $FlowFixMe[cannot-resolve-module] libdef missing in RN OSS
30
-
31
15
  const debug = require("debug")("Metro:InspectorProxy");
32
16
  const WS_DEVICE_URL = "/inspector/device";
33
17
  const WS_DEBUGGER_URL = "/inspector/debug";
@@ -37,25 +21,13 @@ const PAGES_LIST_JSON_VERSION_URL = "/json/version";
37
21
  const MAX_PONG_LATENCY_MS = 5000;
38
22
  const DEBUGGER_HEARTBEAT_INTERVAL_MS = 10000;
39
23
  const INTERNAL_ERROR_CODE = 1011;
40
- /**
41
- * Main Inspector Proxy class that connects JavaScript VM inside Android/iOS apps and JS debugger.
42
- */
43
24
  class InspectorProxy {
44
- // Root of the project used for relative to absolute source path conversion.
45
25
  #projectRoot;
46
-
47
- /** The base URL to the dev server from the developer machine. */
48
26
  #serverBaseUrl;
49
-
50
- // Maps device ID to Device instance.
51
27
  #devices;
52
-
53
- // Internal counter for device IDs -- just gets incremented for each new device.
54
28
  #deviceCounter = 0;
55
29
  #eventReporter;
56
30
  #experiments;
57
-
58
- // custom message handler factory allowing implementers to handle unsupported CDP messages.
59
31
  #customMessageHandler;
60
32
  constructor(
61
33
  projectRoot,
@@ -72,7 +44,6 @@ class InspectorProxy {
72
44
  this.#customMessageHandler = customMessageHandler;
73
45
  }
74
46
  getPageDescriptions() {
75
- // Build list of pages from all devices.
76
47
  let result = [];
77
48
  Array.from(this.#devices.entries()).forEach(([deviceId, device]) => {
78
49
  result = result.concat(
@@ -83,11 +54,6 @@ class InspectorProxy {
83
54
  });
84
55
  return result;
85
56
  }
86
-
87
- // Process HTTP request sent to server. We only respond to 2 HTTP requests:
88
- // 1. /json/version returns Chrome debugger protocol version that we use
89
- // 2. /json and /json/list returns list of page descriptions (list of inspectable apps).
90
- // This list is combined from all the connected devices.
91
57
  processRequest(request, response, next) {
92
58
  const pathname = _url.default.parse(request.url).pathname;
93
59
  if (
@@ -110,16 +76,10 @@ class InspectorProxy {
110
76
  [WS_DEBUGGER_URL]: this.#createDebuggerConnectionWSServer(),
111
77
  };
112
78
  }
113
-
114
- // Converts page information received from device into PageDescription object
115
- // that is sent to debugger.
116
79
  #buildPageDescription(deviceId, device, page) {
117
80
  const { host, protocol } = new URL(this.#serverBaseUrl);
118
81
  const webSocketScheme = protocol === "https:" ? "wss" : "ws";
119
82
  const webSocketUrlWithoutProtocol = `${host}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`;
120
- // For now, `/json/list` returns the legacy built-in `devtools://` URL, to
121
- // preserve existing handling by Flipper. This may return a placeholder in
122
- // future -- please use the `/open-debugger` endpoint.
123
83
  const devtoolsFrontendUrl =
124
84
  `devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&${webSocketScheme}=` +
125
85
  encodeURIComponent(webSocketUrlWithoutProtocol);
@@ -142,9 +102,6 @@ class InspectorProxy {
142
102
  },
143
103
  };
144
104
  }
145
-
146
- // Sends object as response to HTTP request.
147
- // Just serializes object using JSON and sets required headers.
148
105
  #sendJsonResponse(response, object) {
149
106
  const data = JSON.stringify(object, null, 2);
150
107
  response.writeHead(200, {
@@ -155,21 +112,12 @@ class InspectorProxy {
155
112
  });
156
113
  response.end(data);
157
114
  }
158
-
159
- // Adds websocket handler for device connections.
160
- // Device connects to /inspector/device and passes device and app names as
161
- // HTTP GET params.
162
- // For each new websocket connection we parse device and app names and create
163
- // new instance of Device class.
164
115
  #createDeviceConnectionWSServer() {
165
116
  const wss = new _ws.default.Server({
166
117
  noServer: true,
167
118
  perMessageDeflate: true,
168
- // Don't crash on exceptionally large messages - assume the device is
169
- // well-behaved and the debugger is prepared to handle large messages.
170
119
  maxPayload: 0,
171
120
  });
172
- // $FlowFixMe[value-as-type]
173
121
  wss.on("connection", async (socket, req) => {
174
122
  try {
175
123
  const fallbackDeviceId = String(this.#deviceCounter++);
@@ -178,24 +126,37 @@ class InspectorProxy {
178
126
  const deviceName = query.name || "Unknown";
179
127
  const appName = query.app || "Unknown";
180
128
  const oldDevice = this.#devices.get(deviceId);
181
- const newDevice = new _Device.default(
182
- deviceId,
183
- deviceName,
184
- appName,
185
- socket,
186
- this.#projectRoot,
187
- this.#eventReporter,
188
- this.#customMessageHandler
189
- );
129
+ let newDevice;
190
130
  if (oldDevice) {
191
- oldDevice.handleDuplicateDeviceConnection(newDevice);
131
+ oldDevice.dangerouslyRecreateDevice(
132
+ deviceId,
133
+ deviceName,
134
+ appName,
135
+ socket,
136
+ this.#projectRoot,
137
+ this.#eventReporter,
138
+ this.#customMessageHandler
139
+ );
140
+ newDevice = oldDevice;
141
+ } else {
142
+ newDevice = new _Device.default(
143
+ deviceId,
144
+ deviceName,
145
+ appName,
146
+ socket,
147
+ this.#projectRoot,
148
+ this.#eventReporter,
149
+ this.#customMessageHandler
150
+ );
192
151
  }
193
152
  this.#devices.set(deviceId, newDevice);
194
153
  debug(
195
154
  `Got new connection: name=${deviceName}, app=${appName}, device=${deviceId}`
196
155
  );
197
156
  socket.on("close", () => {
198
- this.#devices.delete(deviceId);
157
+ if (this.#devices.get(deviceId)?.dangerouslyGetSocket() === socket) {
158
+ this.#devices.delete(deviceId);
159
+ }
199
160
  debug(`Device ${deviceName} disconnected.`);
200
161
  });
201
162
  } catch (e) {
@@ -205,21 +166,12 @@ class InspectorProxy {
205
166
  });
206
167
  return wss;
207
168
  }
208
-
209
- // Returns websocket handler for debugger connections.
210
- // Debugger connects to webSocketDebuggerUrl that we return as part of page description
211
- // in /json response.
212
- // When debugger connects we try to parse device and page IDs from the query and pass
213
- // websocket object to corresponding Device instance.
214
169
  #createDebuggerConnectionWSServer() {
215
170
  const wss = new _ws.default.Server({
216
171
  noServer: true,
217
172
  perMessageDeflate: false,
218
- // Don't crash on exceptionally large messages - assume the debugger is
219
- // well-behaved and the device is prepared to handle large messages.
220
173
  maxPayload: 0,
221
174
  });
222
- // $FlowFixMe[value-as-type]
223
175
  wss.on("connection", async (socket, req) => {
224
176
  try {
225
177
  const query = _url.default.parse(req.url || "", true).query || {};
@@ -248,30 +200,17 @@ class InspectorProxy {
248
200
  });
249
201
  return wss;
250
202
  }
251
-
252
- // Starts pinging the socket at the given interval. Compliant clients will
253
- // respond with pong frame. This serves both to detect when the client
254
- // has gone away without sending a close frame, and as a keepalive in cases
255
- // where proxies may drop idle connections (e.g., VS Code tunnels).
256
- //
257
- // https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2
258
203
  #startHeartbeat(socket, intervalMs) {
259
204
  let shouldSetTerminateTimeout = false;
260
205
  let terminateTimeout = null;
261
206
  const pingTimeout = (0, _timers.setTimeout)(() => {
262
207
  if (socket.readyState !== _ws.default.OPEN) {
263
- // May be connecting or closing, try again later.
264
208
  pingTimeout.refresh();
265
209
  return;
266
210
  }
267
211
  shouldSetTerminateTimeout = true;
268
212
  socket.ping(() => {
269
213
  if (!shouldSetTerminateTimeout) {
270
- // Sometimes, this `sent` callback fires later than
271
- // the actual pong reply.
272
- //
273
- // If any message came in between ping `sending` and `sent`,
274
- // then the connection exists; and we don't need to do anything.
275
214
  return;
276
215
  }
277
216
  shouldSetTerminateTimeout = false;
@@ -279,12 +218,6 @@ class InspectorProxy {
279
218
  if (socket.readyState !== _ws.default.OPEN) {
280
219
  return;
281
220
  }
282
- // We don't use close() here because that initiates a closing handshake,
283
- // which will not complete if the other end has gone away - 'close'
284
- // would not be emitted.
285
- //
286
- // terminate() emits 'close' immediately, allowing us to handle it and
287
- // inform any clients.
288
221
  socket.terminate();
289
222
  }, MAX_PONG_LATENCY_MS).unref();
290
223
  });
@@ -17,6 +17,10 @@ import type { IncomingMessage, ServerResponse } from "http";
17
17
  import WS from "ws";
18
18
 
19
19
  export interface InspectorProxyQueries {
20
+ /**
21
+ * Returns list of page descriptions ordered by device connection order, then
22
+ * page addition order.
23
+ */
20
24
  getPageDescriptions(): Array<PageDescription>;
21
25
  }
22
26
 
@@ -9,24 +9,25 @@
9
9
  * @oncall react_native
10
10
  */
11
11
 
12
+ import type { JSONSerializable } from "../types";
12
13
  import type { Commands, Events } from "./protocol";
13
- export type CDPEvent<TEvent extends keyof Events = "unknown"> = Readonly<{
14
+ export type CDPEvent<TEvent extends keyof Events = "unknown"> = {
14
15
  method: TEvent;
15
16
  params: Events[TEvent];
16
- }>;
17
- export type CDPRequest<TCommand extends keyof Commands = "unknown"> = Readonly<{
17
+ };
18
+ export type CDPRequest<TCommand extends keyof Commands = "unknown"> = {
18
19
  method: TCommand;
19
20
  params: Commands[TCommand]["paramsType"];
20
21
  id: number;
21
- }>;
22
+ };
22
23
  export type CDPResponse<TCommand extends keyof Commands = "unknown"> =
23
- | Readonly<{ result: Commands[TCommand]["resultType"]; id: number }>
24
- | Readonly<{ error: CDPRequestError; id: number }>;
25
- export type CDPRequestError = Readonly<{
24
+ | { result: Commands[TCommand]["resultType"]; id: number }
25
+ | { error: CDPRequestError; id: number };
26
+ export type CDPRequestError = {
26
27
  code: number;
27
28
  message: string;
28
- data?: unknown;
29
- }>;
29
+ data?: JSONSerializable;
30
+ };
30
31
  export type CDPClientMessage =
31
32
  | CDPRequest<"Debugger.getScriptSource">
32
33
  | CDPRequest<"Debugger.scriptParsed">
@@ -9,35 +9,36 @@
9
9
  * @oncall react_native
10
10
  */
11
11
 
12
+ import type { JSONSerializable } from "../types";
12
13
  import type { Commands, Events } from "./protocol";
13
14
 
14
15
  // Note: A CDP event is a JSON-RPC notification with no `id` member.
15
- export type CDPEvent<TEvent: $Keys<Events> = "unknown"> = $ReadOnly<{
16
+ export type CDPEvent<TEvent: $Keys<Events> = "unknown"> = {
16
17
  method: TEvent,
17
18
  params: Events[TEvent],
18
- }>;
19
+ };
19
20
 
20
- export type CDPRequest<TCommand: $Keys<Commands> = "unknown"> = $ReadOnly<{
21
+ export type CDPRequest<TCommand: $Keys<Commands> = "unknown"> = {
21
22
  method: TCommand,
22
23
  params: Commands[TCommand]["paramsType"],
23
24
  id: number,
24
- }>;
25
+ };
25
26
 
26
27
  export type CDPResponse<TCommand: $Keys<Commands> = "unknown"> =
27
- | $ReadOnly<{
28
+ | {
28
29
  result: Commands[TCommand]["resultType"],
29
30
  id: number,
30
- }>
31
- | $ReadOnly<{
31
+ }
32
+ | {
32
33
  error: CDPRequestError,
33
34
  id: number,
34
- }>;
35
+ };
35
36
 
36
- export type CDPRequestError = $ReadOnly<{
37
+ export type CDPRequestError = {
37
38
  code: number,
38
39
  message: string,
39
- data?: mixed,
40
- }>;
40
+ data?: JSONSerializable,
41
+ };
41
42
 
42
43
  export type CDPClientMessage =
43
44
  | CDPRequest<"Debugger.getScriptSource">
@@ -9,15 +9,16 @@
9
9
  * @oncall react_native
10
10
  */
11
11
 
12
+ import type { JSONSerializable } from "../types";
12
13
  type integer = number;
13
14
  export interface Debugger {
14
- GetScriptSourceParams: Readonly<{
15
+ GetScriptSourceParams: {
15
16
  /**
16
17
  * Id of the script to get source for.
17
18
  */
18
19
  scriptId: string;
19
- }>;
20
- GetScriptSourceResult: Readonly<{
20
+ };
21
+ GetScriptSourceResult: {
21
22
  /**
22
23
  * Script source (empty in case of Wasm bytecode).
23
24
  */
@@ -26,8 +27,8 @@ export interface Debugger {
26
27
  * Wasm bytecode. (Encoded as a base64 string when passed over JSON)
27
28
  */
28
29
  bytecode?: string;
29
- }>;
30
- SetBreakpointByUrlParams: Readonly<{
30
+ };
31
+ SetBreakpointByUrlParams: {
31
32
  /**
32
33
  * Line number to set breakpoint at.
33
34
  */
@@ -54,8 +55,8 @@ export interface Debugger {
54
55
  * breakpoint if this expression evaluates to true.
55
56
  */
56
57
  condition?: string;
57
- }>;
58
- ScriptParsedEvent: Readonly<{
58
+ };
59
+ ScriptParsedEvent: {
59
60
  /**
60
61
  * Identifier of the script parsed.
61
62
  */
@@ -68,11 +69,11 @@ export interface Debugger {
68
69
  * URL of source map associated with script (if any).
69
70
  */
70
71
  sourceMapURL: string;
71
- }>;
72
+ };
72
73
  }
73
74
  export type Events = {
74
75
  "Debugger.scriptParsed": Debugger["ScriptParsedEvent"];
75
- [method: string]: unknown;
76
+ [method: string]: JSONSerializable;
76
77
  };
77
78
  export type Commands = {
78
79
  "Debugger.getScriptSource": {
@@ -83,5 +84,8 @@ export type Commands = {
83
84
  paramsType: Debugger["SetBreakpointByUrlParams"];
84
85
  resultType: void;
85
86
  };
86
- [method: string]: { paramsType: unknown; resultType: unknown };
87
+ [method: string]: {
88
+ paramsType: JSONSerializable;
89
+ resultType: JSONSerializable;
90
+ };
87
91
  };
@@ -11,17 +11,19 @@
11
11
 
12
12
  // Adapted from https://github.com/ChromeDevTools/devtools-protocol/blob/master/types/protocol.d.ts
13
13
 
14
+ import type { JSONSerializable } from "../types";
15
+
14
16
  type integer = number;
15
17
 
16
18
  export interface Debugger {
17
- GetScriptSourceParams: $ReadOnly<{
19
+ GetScriptSourceParams: {
18
20
  /**
19
21
  * Id of the script to get source for.
20
22
  */
21
23
  scriptId: string,
22
- }>;
24
+ };
23
25
 
24
- GetScriptSourceResult: $ReadOnly<{
26
+ GetScriptSourceResult: {
25
27
  /**
26
28
  * Script source (empty in case of Wasm bytecode).
27
29
  */
@@ -31,9 +33,9 @@ export interface Debugger {
31
33
  * Wasm bytecode. (Encoded as a base64 string when passed over JSON)
32
34
  */
33
35
  bytecode?: string,
34
- }>;
36
+ };
35
37
 
36
- SetBreakpointByUrlParams: $ReadOnly<{
38
+ SetBreakpointByUrlParams: {
37
39
  /**
38
40
  * Line number to set breakpoint at.
39
41
  */
@@ -65,9 +67,9 @@ export interface Debugger {
65
67
  * breakpoint if this expression evaluates to true.
66
68
  */
67
69
  condition?: string,
68
- }>;
70
+ };
69
71
 
70
- ScriptParsedEvent: $ReadOnly<{
72
+ ScriptParsedEvent: {
71
73
  /**
72
74
  * Identifier of the script parsed.
73
75
  */
@@ -82,12 +84,12 @@ export interface Debugger {
82
84
  * URL of source map associated with script (if any).
83
85
  */
84
86
  sourceMapURL: string,
85
- }>;
87
+ };
86
88
  }
87
89
 
88
90
  export type Events = {
89
91
  "Debugger.scriptParsed": Debugger["ScriptParsedEvent"],
90
- [method: string]: mixed,
92
+ [method: string]: JSONSerializable,
91
93
  };
92
94
 
93
95
  export type Commands = {
@@ -100,7 +102,7 @@ export type Commands = {
100
102
  resultType: void,
101
103
  },
102
104
  [method: string]: {
103
- paramsType: mixed,
104
- resultType: mixed,
105
+ paramsType: JSONSerializable,
106
+ resultType: JSONSerializable,
105
107
  },
106
108
  };
@@ -109,3 +109,9 @@ export type JSONSerializable =
109
109
  | null
110
110
  | ReadonlyArray<JSONSerializable>
111
111
  | { readonly [$$Key$$: string]: JSONSerializable };
112
+ export type DeepReadOnly<T> =
113
+ T extends ReadonlyArray<infer V>
114
+ ? ReadonlyArray<DeepReadOnly<V>>
115
+ : T extends {}
116
+ ? { readonly [K in keyof T]: DeepReadOnly<T[K]> }
117
+ : T;
@@ -146,3 +146,10 @@ export type JSONSerializable =
146
146
  | null
147
147
  | $ReadOnlyArray<JSONSerializable>
148
148
  | { +[string]: JSONSerializable };
149
+
150
+ export type DeepReadOnly<T> =
151
+ T extends $ReadOnlyArray<infer V>
152
+ ? $ReadOnlyArray<DeepReadOnly<V>>
153
+ : T extends { ... }
154
+ ? { +[K in keyof T]: DeepReadOnly<T[K]> }
155
+ : T;
@@ -8,26 +8,8 @@ var _open = _interopRequireDefault(require("open"));
8
8
  function _interopRequireDefault(obj) {
9
9
  return obj && obj.__esModule ? obj : { default: obj };
10
10
  }
11
- /**
12
- * Copyright (c) Meta Platforms, Inc. and affiliates.
13
- *
14
- * This source code is licensed under the MIT license found in the
15
- * LICENSE file in the root directory of this source tree.
16
- *
17
- *
18
- * @format
19
- * @oncall react_native
20
- */
21
-
22
11
  const FLIPPER_SELF_CONNECT_URL =
23
12
  "flipper://null/Hermesdebuggerrn?device=React%20Native";
24
- /**
25
- * Open the legacy Flipper debugger (Hermes).
26
- *
27
- * @deprecated This replicates the pre-0.73 workflow of opening Flipper via the
28
- * `flipper://` URL scheme, failing if Flipper is not installed locally. This
29
- * flow will be removed in a future version.
30
- */
31
13
  function deprecated_openFlipperMiddleware({ logger }) {
32
14
  return async (req, res, next) => {
33
15
  if (req.method === "POST") {
@@ -11,25 +11,6 @@ var _url = _interopRequireDefault(require("url"));
11
11
  function _interopRequireDefault(obj) {
12
12
  return obj && obj.__esModule ? obj : { default: obj };
13
13
  }
14
- /**
15
- * Copyright (c) Meta Platforms, Inc. and affiliates.
16
- *
17
- * This source code is licensed under the MIT license found in the
18
- * LICENSE file in the root directory of this source tree.
19
- *
20
- *
21
- * @format
22
- * @oncall react_native
23
- */
24
-
25
- /**
26
- * Open the JavaScript debugger for a given CDP target (direct Hermes debugging).
27
- *
28
- * Currently supports Hermes targets, opening debugger websocket URL in Chrome
29
- * DevTools.
30
- *
31
- * @see https://chromedevtools.github.io/devtools-protocol/
32
- */
33
14
  function openDebuggerMiddleware({
34
15
  serverBaseUrl,
35
16
  logger,
@@ -44,34 +25,40 @@ function openDebuggerMiddleware({
44
25
  (experiments.enableOpenDebuggerRedirect && req.method === "GET")
45
26
  ) {
46
27
  const { query } = _url.default.parse(req.url, true);
47
- const { appId, device, launchId } = query;
48
- const targets = inspectorProxy.getPageDescriptions().filter(
49
- // Only use targets with better reloading support
50
- (app) =>
51
- app.title === "React Native Experimental (Improved Chrome Reloads)" ||
52
- app.reactNative.capabilities?.nativePageReloads === true
53
- );
28
+ const { appId, device, launchId, target: targetId } = query;
29
+ const targets = inspectorProxy
30
+ .getPageDescriptions()
31
+ .filter(
32
+ (app) =>
33
+ app.title ===
34
+ "React Native Experimental (Improved Chrome Reloads)" ||
35
+ app.reactNative.capabilities?.nativePageReloads === true
36
+ );
54
37
  let target;
55
38
  const launchType = req.method === "POST" ? "launch" : "redirect";
56
- if (typeof appId === "string" || typeof device === "string") {
39
+ if (
40
+ typeof targetId === "string" ||
41
+ typeof appId === "string" ||
42
+ typeof device === "string"
43
+ ) {
57
44
  logger?.info(
58
45
  (launchType === "launch" ? "Launching" : "Redirecting to") +
59
46
  " JS debugger (experimental)..."
60
47
  );
61
- if (typeof device === "string") {
62
- target = targets.find(
63
- (_target) => _target.reactNative.logicalDeviceId === device
64
- );
65
- }
66
- if (!target && typeof appId === "string") {
67
- target = targets.find((_target) => _target.description === appId);
68
- }
69
- } else {
48
+ target = targets.find(
49
+ (_target) =>
50
+ (targetId == null || _target.id === targetId) &&
51
+ (appId == null || _target.description === appId) &&
52
+ (device == null || _target.reactNative.logicalDeviceId === device)
53
+ );
54
+ } else if (targets.length > 0) {
70
55
  logger?.info(
71
56
  (launchType === "launch" ? "Launching" : "Redirecting to") +
72
- " JS debugger for first available target..."
57
+ ` JS debugger${
58
+ targets.length === 1 ? "" : " for most recently connected target"
59
+ }...`
73
60
  );
74
- target = targets[0];
61
+ target = targets[targets.length - 1];
75
62
  }
76
63
  if (!target) {
77
64
  res.writeHead(404);