rnxsim 0.1.344 → 0.1.346

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 (211) hide show
  1. package/README.md +59 -31
  2. package/cli/app-fonts.ts +8 -28
  3. package/cli/app-project.ts +12 -184
  4. package/cli/app-splash.ts +3 -17
  5. package/cli/bin.ts +15 -28
  6. package/cli/commands/control.ts +20 -30
  7. package/cli/commands/daemon.ts +19 -44
  8. package/cli/commands/flow.ts +0 -27
  9. package/cli/commands/install-desktop.ts +2 -16
  10. package/cli/commands/login.ts +3 -43
  11. package/cli/commands/logout.ts +0 -7
  12. package/cli/commands/serve.ts +2 -32
  13. package/cli/commands/setup.ts +20 -194
  14. package/cli/commands/telemetry.ts +7 -5
  15. package/cli/commands/upload.ts +1 -48
  16. package/cli/commands/welcome.ts +64 -0
  17. package/cli/outbound-endpoints.ts +586 -0
  18. package/cli/privacy.ts +53 -0
  19. package/cli/runtime-notes.ts +2 -2
  20. package/cli/telemetry.ts +46 -149
  21. package/dist-cli/bin.js +6 -6
  22. package/dist-cli/chunks/{agent-HTSE5FA4.js → agent-U6HZUHAQ.js} +2 -2
  23. package/dist-cli/chunks/{agent-wrapper-L6X2AEZL.js → agent-wrapper-IM2VL6XP.js} +2 -2
  24. package/dist-cli/chunks/{app-fonts-FGF3ETYH.js → app-fonts-LHVTPWCN.js} +2 -2
  25. package/dist-cli/chunks/{assert-HA2ZYVDN.js → assert-UMGCZXWN.js} +2 -2
  26. package/dist-cli/chunks/{auth-HONDAV75.js → auth-PQY57FPY.js} +2 -2
  27. package/dist-cli/chunks/{beta-U7EE4GMR.js → beta-3MMNRHL7.js} +2 -2
  28. package/dist-cli/chunks/{camera-RTKNUTIX.js → camera-UKISEHMH.js} +2 -2
  29. package/dist-cli/chunks/{chunk-CNLPLCO2.js → chunk-36P343FE.js} +2 -2
  30. package/dist-cli/chunks/{chunk-IWR4HFC4.js → chunk-3KPJVR63.js} +2 -2
  31. package/dist-cli/chunks/chunk-54Y5IXWQ.js +15 -0
  32. package/dist-cli/chunks/{chunk-EPEZZ6YS.js → chunk-5KMV7NAJ.js} +2 -2
  33. package/dist-cli/chunks/{chunk-NDGAF5OF.js → chunk-5WIYZI5H.js} +2 -2
  34. package/dist-cli/chunks/{chunk-KBSVSAAE.js → chunk-6GXUQ3DM.js} +2 -2
  35. package/dist-cli/chunks/{chunk-KT6WMCUS.js → chunk-6MUEKKJ4.js} +1 -1
  36. package/dist-cli/chunks/{chunk-TOHQI2II.js → chunk-6Q6S53R7.js} +1 -1
  37. package/dist-cli/chunks/{chunk-63NZGJ4A.js → chunk-6SQCDLTJ.js} +1 -1
  38. package/dist-cli/chunks/{chunk-Q2B6EQPB.js → chunk-6UW6O575.js} +2 -2
  39. package/dist-cli/chunks/chunk-6VH3UUYQ.js +4 -0
  40. package/dist-cli/chunks/{chunk-BGLWR43R.js → chunk-6XJGGFMD.js} +1 -1
  41. package/dist-cli/chunks/{chunk-TZNRKIS3.js → chunk-75LMZXF5.js} +2 -2
  42. package/dist-cli/chunks/{chunk-FZ5DRZJE.js → chunk-7CJNUNQA.js} +3 -3
  43. package/dist-cli/chunks/{chunk-UCVQOQCY.js → chunk-BQDGFR4B.js} +2 -2
  44. package/dist-cli/chunks/{chunk-H4J7EWZC.js → chunk-BSIVS2SQ.js} +1 -1
  45. package/dist-cli/chunks/{chunk-6E6SBYIO.js → chunk-CD2LITQX.js} +1 -1
  46. package/dist-cli/chunks/{chunk-PNMCALX5.js → chunk-DGH3D6WS.js} +2 -2
  47. package/dist-cli/chunks/chunk-EP6XBKPM.js +11 -0
  48. package/dist-cli/chunks/{chunk-OCUXAA6O.js → chunk-EVX6Y7ZM.js} +2 -2
  49. package/dist-cli/chunks/chunk-EYRKIWHE.js +4 -0
  50. package/dist-cli/chunks/{chunk-N5PFDTFL.js → chunk-FSYB2S6Q.js} +17 -17
  51. package/dist-cli/chunks/{chunk-6PEZSVIG.js → chunk-HPCTBEAP.js} +1 -1
  52. package/dist-cli/chunks/chunk-HS2BME6V.js +6 -0
  53. package/dist-cli/chunks/{chunk-G6IPT2PS.js → chunk-I3GUDHG2.js} +2 -2
  54. package/dist-cli/chunks/{chunk-MWUXQIA3.js → chunk-I4HVFKLM.js} +2 -2
  55. package/dist-cli/chunks/{chunk-ZPTFL5FQ.js → chunk-IBNK7SWL.js} +2 -2
  56. package/dist-cli/chunks/chunk-IUG4GWQC.js +25 -0
  57. package/dist-cli/chunks/{chunk-XQFDEY3F.js → chunk-IUIKD2YI.js} +1 -1
  58. package/dist-cli/chunks/{chunk-JFUVWIJT.js → chunk-JPVXLY7X.js} +1 -1
  59. package/dist-cli/chunks/{chunk-JLUEQPEF.js → chunk-K4PZLT2K.js} +1 -1
  60. package/dist-cli/chunks/{chunk-CRWYKW67.js → chunk-KNIKUJPD.js} +1 -1
  61. package/dist-cli/chunks/{chunk-EZLLZDYX.js → chunk-L6GWDWGZ.js} +2 -2
  62. package/dist-cli/chunks/chunk-LAVDDUQR.js +4 -0
  63. package/dist-cli/chunks/{chunk-22WJPMTQ.js → chunk-LPXKPZ4B.js} +1 -1
  64. package/dist-cli/chunks/{chunk-IPC45HTT.js → chunk-LYLK5VRP.js} +2 -2
  65. package/dist-cli/chunks/{chunk-S6LBAUWH.js → chunk-ME46FC4L.js} +1 -1
  66. package/dist-cli/chunks/chunk-NNXSLD76.js +99 -0
  67. package/dist-cli/chunks/{chunk-7V4ULF3U.js → chunk-PNH3YBZ4.js} +2 -2
  68. package/dist-cli/chunks/{chunk-GRX6NSBG.js → chunk-PPEJMNRO.js} +2 -2
  69. package/dist-cli/chunks/{chunk-K3VNCCEJ.js → chunk-SEWGXJG6.js} +3 -3
  70. package/dist-cli/chunks/{chunk-R3UYHE6G.js → chunk-T4JPZI32.js} +1 -1
  71. package/dist-cli/chunks/{chunk-NRTHFEPM.js → chunk-TAQYETRH.js} +2 -2
  72. package/dist-cli/chunks/{chunk-7JEEO33Y.js → chunk-TBVBWJJL.js} +2 -2
  73. package/dist-cli/chunks/{chunk-HRBMD2FS.js → chunk-UA54YLWR.js} +26 -26
  74. package/dist-cli/chunks/{chunk-G3UAFZE2.js → chunk-UV3TXNMG.js} +1 -1
  75. package/dist-cli/chunks/{chunk-EH35QQG4.js → chunk-VZ6YFPIN.js} +45 -45
  76. package/dist-cli/chunks/chunk-W54SRZ5K.js +14 -0
  77. package/dist-cli/chunks/chunk-WAC75WN6.js +4 -0
  78. package/dist-cli/chunks/{chunk-S7CSBQ6G.js → chunk-X2WIEZ7Q.js} +1 -1
  79. package/dist-cli/chunks/{chunk-EN32LN2B.js → chunk-XWTYFWZ3.js} +2 -2
  80. package/dist-cli/chunks/chunk-Y2C3CZVD.js +4 -0
  81. package/dist-cli/chunks/chunk-YM4L2CFB.js +4 -0
  82. package/dist-cli/chunks/{chunk-ZZSBCGHC.js → chunk-YXDR4CAQ.js} +1 -1
  83. package/dist-cli/chunks/{chunk-6NLWKL7Y.js → chunk-Z6P427WC.js} +2 -2
  84. package/dist-cli/chunks/{cleanup-EMYD5WZG.js → cleanup-KXPS5VSK.js} +2 -2
  85. package/dist-cli/chunks/cli-version-LJ6NFLBP.js +4 -0
  86. package/dist-cli/chunks/{compat-W2N5QTVG.js → compat-JWF7N3VO.js} +2 -2
  87. package/dist-cli/chunks/config-M42VJXNK.js +13 -0
  88. package/dist-cli/chunks/{control-4SFBROUE.js → control-HMZDIS6P.js} +2 -2
  89. package/dist-cli/chunks/daemon-MQ4UYKNQ.js +4 -0
  90. package/dist-cli/chunks/{debug-HCR2KFUA.js → debug-F44GWYMF.js} +3 -3
  91. package/dist-cli/chunks/{desktop-T2KS5PBJ.js → desktop-YJMWW6MW.js} +3 -3
  92. package/dist-cli/chunks/{detox-5S7EHINH.js → detox-5B4ASTJO.js} +2 -2
  93. package/dist-cli/chunks/{device-LVM3MZRJ.js → device-2544YZNY.js} +2 -2
  94. package/dist-cli/chunks/{diagnose-WK37XKLI.js → diagnose-LS4245JR.js} +2 -2
  95. package/dist-cli/chunks/{disk-cleanup-X7VJOXMS.js → disk-cleanup-XYL2HQQY.js} +2 -2
  96. package/dist-cli/chunks/drivers-P5CRSKH6.js +4 -0
  97. package/dist-cli/chunks/{film-7KLTI3ER.js → film-LS3TIZRM.js} +3 -3
  98. package/dist-cli/chunks/flow-7V77GXAB.js +4 -0
  99. package/dist-cli/chunks/help-HRESIFZX.js +4 -0
  100. package/dist-cli/chunks/{hidden-runtime-alias-4RVGUVFO.js → hidden-runtime-alias-HGN2EEQR.js} +2 -2
  101. package/dist-cli/chunks/{home-paths-5DHSAEGZ.js → home-paths-T3NTXE4U.js} +2 -2
  102. package/dist-cli/chunks/inspect-UC44UH4M.js +4 -0
  103. package/dist-cli/chunks/install-desktop-RDV66JHG.js +4 -0
  104. package/dist-cli/chunks/login-WVP5H5NQ.js +26 -0
  105. package/dist-cli/chunks/logout-GXX5OY6X.js +4 -0
  106. package/dist-cli/chunks/{maestro-HTRLQDJE.js → maestro-MQMGBNOB.js} +3 -3
  107. package/dist-cli/chunks/{maestro-generate-GX2QNA3F.js → maestro-generate-CO355QV2.js} +3 -3
  108. package/dist-cli/chunks/{mode-GM6KZHQP.js → mode-QFB3M2AA.js} +2 -2
  109. package/dist-cli/chunks/{optional-demo-registry-B4TD7JQP.js → optional-demo-registry-UIPVSBHV.js} +2 -2
  110. package/dist-cli/chunks/{perf-3PRXJQIC.js → perf-TDOSW3LD.js} +2 -2
  111. package/dist-cli/chunks/{permissions-TJZIRF2S.js → permissions-TEVQLCGJ.js} +2 -2
  112. package/dist-cli/chunks/privacy-DVHAMHR6.js +4 -0
  113. package/dist-cli/chunks/{record-LI55DTPE.js → record-OFEQLSWA.js} +3 -3
  114. package/dist-cli/chunks/{report-issue-TBGJVQIQ.js → report-issue-B2UPOERJ.js} +2 -2
  115. package/dist-cli/chunks/reset-XQQYM55B.js +4 -0
  116. package/dist-cli/chunks/runtime-GN6HITVB.js +4 -0
  117. package/dist-cli/chunks/{screenshot-command-BEN7YYIF.js → screenshot-command-NTLAY7QN.js} +3 -3
  118. package/dist-cli/chunks/{screenshot-layers-3IU4KYNO.js → screenshot-layers-BBLKNRQU.js} +2 -2
  119. package/dist-cli/chunks/{screenshots-capture-ZRC54ST6.js → screenshots-capture-6GHIRCTG.js} +2 -2
  120. package/dist-cli/chunks/serve-H6GJ4OHW.js +118 -0
  121. package/dist-cli/chunks/setup-6YLKSMWS.js +30 -0
  122. package/dist-cli/chunks/{skills-GKBR2HQB.js → skills-FUL7D6C7.js} +2 -2
  123. package/dist-cli/chunks/{state-LIHZJSL7.js → state-O2HE7XSR.js} +2 -2
  124. package/dist-cli/chunks/{storage-4TPN3NFB.js → storage-WIZTF7RN.js} +4 -4
  125. package/dist-cli/chunks/store-NJ2MBZC2.js +4 -0
  126. package/dist-cli/chunks/telemetry-BEC6W5WP.js +4 -0
  127. package/dist-cli/chunks/{timeline-EX63UNWU.js → timeline-MA4JT7UJ.js} +2 -2
  128. package/dist-cli/chunks/{upgrade-O6Y6EINE.js → upgrade-H5LSDXBH.js} +2 -2
  129. package/dist-cli/chunks/upload-Q4DWKIA4.js +4 -0
  130. package/dist-cli/chunks/{version-HLNL3NFB.js → version-4UWEEDVP.js} +2 -2
  131. package/dist-cli/chunks/{web-4EDPYPW7.js → web-F3LXRN3K.js} +2 -2
  132. package/dist-cli/chunks/welcome-AZBOSIG2.js +5 -0
  133. package/dist-cli/chunks/{what-happened-77VNUEPK.js → what-happened-6OSNLOAY.js} +2 -2
  134. package/dist-lib/agent-daemon-client.cjs +1 -1
  135. package/dist-lib/agent-events.cjs +1 -1
  136. package/dist-lib/agent-identity.cjs +1 -1
  137. package/dist-lib/agent-sessions.cjs +1 -1
  138. package/dist-lib/attached-projects.cjs +1 -1
  139. package/dist-lib/auth/shared-session.cjs +1 -1
  140. package/dist-lib/backend-origin.cjs +13 -1
  141. package/dist-lib/beta.cjs +1 -1
  142. package/dist-lib/beta.mjs +1 -1
  143. package/dist-lib/bridge-constants.cjs +1 -1
  144. package/dist-lib/bridge-contract.cjs +1 -1
  145. package/dist-lib/cli-constants.cjs +1 -1
  146. package/dist-lib/config.cjs +1 -1
  147. package/dist-lib/detox/index.cjs +1 -1
  148. package/dist-lib/dev-bundle-resolution.cjs +43 -13
  149. package/dist-lib/home-paths.cjs +145 -11
  150. package/dist-lib/host/bridge-host.cjs +326 -78
  151. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  152. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  153. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  154. package/dist-lib/host/replacement-module-handler.cjs +71 -4
  155. package/dist-lib/host/websocket-proxy.cjs +1 -1
  156. package/dist-lib/index.cjs +337 -37
  157. package/dist-lib/jump-to-source-babel.cjs +1 -1
  158. package/dist-lib/menu.cjs +1 -1
  159. package/dist-lib/menu.mjs +1 -1
  160. package/dist-lib/metro-production-bundle.cjs +1 -1
  161. package/dist-lib/metro-production-bundle.mjs +1 -1
  162. package/dist-lib/metro.cjs +484 -14
  163. package/dist-lib/profiles.cjs +1 -1
  164. package/dist-lib/public-brand.cjs +1 -1
  165. package/dist-lib/react-native-host-modules.cjs +1 -1
  166. package/dist-lib/react-native-host-modules.mjs +1 -1
  167. package/dist-lib/render-mode.cjs +1 -1
  168. package/dist-lib/scripts/dev-server-scanner.cjs +120 -50
  169. package/dist-lib/sdk.cjs +1 -1
  170. package/dist-lib/sdk.mjs +1 -1
  171. package/dist-lib/skills.cjs +493 -439
  172. package/dist-lib/vite.cjs +193 -37
  173. package/package.json +2 -1
  174. package/scripts/dev-server-scanner.ts +112 -51
  175. package/skills/rnx-test/SKILL.md +4 -0
  176. package/src/backend-origin.ts +15 -1
  177. package/src/dev-bundle-resolution.ts +42 -18
  178. package/src/dev-server-open.ts +197 -0
  179. package/src/home-paths.ts +168 -15
  180. package/src/host/bridge-host.ts +2 -2
  181. package/src/host/replacement-module-handler.ts +68 -2
  182. package/src/metro-plugin.ts +83 -5
  183. package/src/native-dev-bundle-url.ts +22 -0
  184. package/src/vite-plugin-one.ts +42 -28
  185. package/dist-cli/chunks/chunk-3JC7H4PR.js +0 -25
  186. package/dist-cli/chunks/chunk-6DLIC7BZ.js +0 -4
  187. package/dist-cli/chunks/chunk-7TNPO2OJ.js +0 -9
  188. package/dist-cli/chunks/chunk-AMF5KTOI.js +0 -4
  189. package/dist-cli/chunks/chunk-BX5VXTOQ.js +0 -101
  190. package/dist-cli/chunks/chunk-D73H7ACZ.js +0 -4
  191. package/dist-cli/chunks/chunk-DY4O7KYI.js +0 -4
  192. package/dist-cli/chunks/chunk-GU2V4JDG.js +0 -15
  193. package/dist-cli/chunks/chunk-POOCKX5B.js +0 -14
  194. package/dist-cli/chunks/chunk-Z2HMSVSN.js +0 -4
  195. package/dist-cli/chunks/cli-version-CN3NTKLH.js +0 -4
  196. package/dist-cli/chunks/config-SELKGT4F.js +0 -13
  197. package/dist-cli/chunks/daemon-V2SFCU2E.js +0 -4
  198. package/dist-cli/chunks/drivers-3YFTJ5WA.js +0 -4
  199. package/dist-cli/chunks/flow-36ANTMJU.js +0 -4
  200. package/dist-cli/chunks/help-ZGL26SPK.js +0 -4
  201. package/dist-cli/chunks/inspect-R3B55RTS.js +0 -4
  202. package/dist-cli/chunks/install-desktop-TZ3H3VWK.js +0 -4
  203. package/dist-cli/chunks/login-4DFULI7S.js +0 -28
  204. package/dist-cli/chunks/logout-QMCVYMBB.js +0 -4
  205. package/dist-cli/chunks/reset-O6MW6X7X.js +0 -4
  206. package/dist-cli/chunks/runtime-JESXBHRQ.js +0 -4
  207. package/dist-cli/chunks/serve-QGKTWPNF.js +0 -118
  208. package/dist-cli/chunks/setup-3WPWATCX.js +0 -32
  209. package/dist-cli/chunks/store-VBX3L46S.js +0 -4
  210. package/dist-cli/chunks/telemetry-ACNNIQIG.js +0 -4
  211. package/dist-cli/chunks/upload-OD2UQHMM.js +0 -4
@@ -22,8 +22,16 @@ import { promisify } from 'util'
22
22
  import type { AppSettingsSpecifier } from '@sootsim/globals'
23
23
 
24
24
  const execP = promisify(exec)
25
+ import {
26
+ buildLoopbackProbeBaseUrl,
27
+ LOOPBACK_PROBE_HOSTNAMES,
28
+ normalizeHostname,
29
+ } from '../src/backend-origin.ts'
25
30
  import { applySootSimConfigToUrl } from '../src/config.ts'
26
- import { normalizeNativeDevBundleUrl } from '../src/native-dev-bundle-url.ts'
31
+ import {
32
+ alignLoopbackBundleUrlWithProbe,
33
+ normalizeNativeDevBundleUrl,
34
+ } from '../src/native-dev-bundle-url.ts'
27
35
  import {
28
36
  __resetAppSettingsBundleCacheForTests,
29
37
  readAppSettingsBundle,
@@ -71,30 +79,42 @@ interface HttpResult {
71
79
  statusCode: number
72
80
  body: string
73
81
  contentType?: string
82
+ hostname: string
74
83
  }
75
84
 
76
85
  // cheap TCP-connect gate. lsof already says the socket is LISTEN, but the
77
86
  // process can be closing, the fd can be a different protocol, or a zombie
78
87
  // can linger — none of those cases are worth firing 5 HTTP probes at.
79
- function tcpPing(port: number, timeout = TCP_GATE_MS): Promise<boolean> {
80
- return new Promise((resolve) => {
81
- const sock = new net.Socket()
82
- let settled = false
83
- const done = (ok: boolean) => {
84
- if (settled) return
85
- settled = true
86
- sock.destroy()
87
- resolve(ok)
88
- }
89
- sock.setTimeout(timeout)
90
- sock.once('connect', () => done(true))
91
- sock.once('timeout', () => done(false))
92
- sock.once('error', () => done(false))
93
- sock.connect(port, 'localhost')
94
- })
88
+ async function tcpPing(port: number, timeout = TCP_GATE_MS): Promise<string | null> {
89
+ try {
90
+ return await Promise.any(
91
+ LOOPBACK_PROBE_HOSTNAMES.map(
92
+ (hostname) =>
93
+ new Promise<string>((resolve, reject) => {
94
+ const sock = new net.Socket()
95
+ let settled = false
96
+ const done = (reachable: boolean) => {
97
+ if (settled) return
98
+ settled = true
99
+ sock.destroy()
100
+ if (reachable) resolve(hostname)
101
+ else reject(new Error(`unreachable loopback host ${hostname}`))
102
+ }
103
+ sock.setTimeout(timeout)
104
+ sock.once('connect', () => done(true))
105
+ sock.once('timeout', () => done(false))
106
+ sock.once('error', () => done(false))
107
+ sock.connect(port, hostname)
108
+ }),
109
+ ),
110
+ )
111
+ } catch {
112
+ return null
113
+ }
95
114
  }
96
115
 
97
116
  function httpGet(
117
+ hostname: string,
98
118
  port: number,
99
119
  path: string,
100
120
  method: 'GET' | 'HEAD' = 'GET',
@@ -103,7 +123,7 @@ function httpGet(
103
123
  ): Promise<HttpResult | null> {
104
124
  return new Promise((resolve) => {
105
125
  const req = http.request(
106
- { hostname: 'localhost', port, path, method, timeout, headers },
126
+ { hostname, port, path, method, timeout, headers },
107
127
  (res) => {
108
128
  let body = ''
109
129
  res.on('data', (c: Buffer) => (body += c.toString()))
@@ -114,7 +134,12 @@ function httpGet(
114
134
  return undefined
115
135
  })()
116
136
  res.on('end', () =>
117
- resolve({ statusCode: res.statusCode || 0, body, contentType }),
137
+ resolve({
138
+ statusCode: res.statusCode || 0,
139
+ body,
140
+ contentType,
141
+ hostname,
142
+ }),
118
143
  )
119
144
  },
120
145
  )
@@ -339,6 +364,7 @@ export async function resolveProcessCwd(pid: number): Promise<string | null> {
339
364
  // ── per-port probe orchestration ────────────────────────────────────────────
340
365
 
341
366
  function makeResult(
367
+ hostname: string,
342
368
  port: number,
343
369
  framework: DiscoveredServer['framework'],
344
370
  owner: Pick<DiscoveredServer, 'cwd' | 'pid'>,
@@ -348,10 +374,10 @@ function makeResult(
348
374
  framework,
349
375
  bundleUrl: withRuntimeConfig(
350
376
  port,
351
- `http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`,
377
+ `${buildLoopbackProbeBaseUrl('http:', hostname, port)}/index.bundle?platform=ios&dev=true&hot=true&minify=false`,
352
378
  owner.cwd,
353
379
  ),
354
- hmrUrl: `ws://localhost:${port}/hot`,
380
+ hmrUrl: `${buildLoopbackProbeBaseUrl('ws:', hostname, port)}/hot`,
355
381
  lastSeen: Date.now(),
356
382
  ...owner,
357
383
  }
@@ -399,10 +425,17 @@ function applyManifest(
399
425
  if (result.framework === 'metro' && client.sdkVersion) result.framework = 'expo'
400
426
  // preserve a confirmed direct one/vxrn metro-entry bundle. expo manifests
401
427
  // and one/vxrn manifests point at a launchAsset URL with `hot=false`
402
- // (hardcoded by @expo/cli — see metroOptions.ts). that's the same URL
403
- // real iOS sims load, so sootsim uses it verbatim to share metro's
404
- // bundle cache; HMR still works via sootsim's HmrClient.
405
- const launchUrl = manifest?.launchAsset?.url
428
+ // (hardcoded by @expo/cli — see metroOptions.ts). keep its path and query
429
+ // so metro shares the iOS bundle cache. when both hosts are loopback, keep
430
+ // the host that answered discovery so address-family drift stays reachable.
431
+ const rawLaunchUrl = manifest?.launchAsset?.url
432
+ const launchUrl =
433
+ typeof rawLaunchUrl === 'string'
434
+ ? alignLoopbackBundleUrlWithProbe(
435
+ rawLaunchUrl,
436
+ buildLoopbackProbeBaseUrl('http:', manifestRes.hostname, result.port),
437
+ )
438
+ : rawLaunchUrl
406
439
  if (launchUrl && !result.patched && !isDirectOneBundleUrl(result.bundleUrl)) {
407
440
  result.bundleUrl = withRuntimeConfig(result.port, launchUrl, result.cwd)
408
441
  }
@@ -416,14 +449,14 @@ function applyManifest(
416
449
  } else {
417
450
  const cleanPath = rawIconUrl.replace(/^\.\//, '')
418
451
  result.iconUrl = buildIconProxyUrl(
419
- `http://localhost:${result.port}/assets/${cleanPath}`,
452
+ `${buildLoopbackProbeBaseUrl('http:', manifestRes.hostname, result.port)}/assets/${cleanPath}`,
420
453
  )
421
454
  }
422
455
  } else {
423
456
  // no proxy requested — use the raw URL
424
457
  result.iconUrl = rawIconUrl.startsWith('http')
425
458
  ? rawIconUrl
426
- : `http://localhost:${result.port}/assets/${rawIconUrl.replace(/^\.\//, '')}`
459
+ : `${buildLoopbackProbeBaseUrl('http:', manifestRes.hostname, result.port)}/assets/${rawIconUrl.replace(/^\.\//, '')}`
427
460
  }
428
461
  }
429
462
  } catch {}
@@ -434,7 +467,11 @@ export function __applyManifestForTests(
434
467
  result: DiscoveredServer,
435
468
  manifestBody: string,
436
469
  ): DiscoveredServer {
437
- return applyManifest(result, { statusCode: 200, body: manifestBody })
470
+ return applyManifest(result, {
471
+ statusCode: 200,
472
+ body: manifestBody,
473
+ hostname: new URL(result.bundleUrl).hostname,
474
+ })
438
475
  }
439
476
 
440
477
  // ── name/icon enrichment ────────────────────────────────────────────────────
@@ -483,30 +520,42 @@ function enrichWithManifest(
483
520
 
484
521
  if (!enrichmentInFlight.has(result.port)) {
485
522
  enrichmentInFlight.add(result.port)
486
- const port = result.port
487
- void fetchManifestEnrichment(port, buildIconProxyUrl).finally(() =>
488
- enrichmentInFlight.delete(port),
523
+ void fetchManifestEnrichment(result, buildIconProxyUrl).finally(() =>
524
+ enrichmentInFlight.delete(result.port),
489
525
  )
490
526
  }
491
527
  return result
492
528
  }
493
529
 
494
530
  async function fetchManifestEnrichment(
495
- port: number,
531
+ result: DiscoveredServer,
496
532
  buildIconProxyUrl?: (externalUrl: string) => string,
497
533
  ): Promise<void> {
498
- const manifestRes = await httpGet(port, '/', 'GET', ENRICH_TIMEOUT_MS, {
499
- 'expo-platform': 'ios',
500
- })
534
+ const hostname = normalizeHostname(new URL(result.bundleUrl).hostname)
535
+ const manifestRes = await httpGet(
536
+ hostname,
537
+ result.port,
538
+ '/',
539
+ 'GET',
540
+ ENRICH_TIMEOUT_MS,
541
+ {
542
+ 'expo-platform': 'ios',
543
+ },
544
+ )
501
545
  if (!manifestRes || manifestRes.statusCode !== 200) return
502
546
  // reuse applyManifest's exact field extraction against a throwaway result.
503
547
  const parsed = applyManifest(
504
- { port, framework: 'one', bundleUrl: '', lastSeen: 0 },
548
+ {
549
+ port: result.port,
550
+ framework: 'one',
551
+ bundleUrl: `${buildLoopbackProbeBaseUrl('http:', hostname, result.port)}/index.bundle`,
552
+ lastSeen: 0,
553
+ },
505
554
  manifestRes,
506
555
  buildIconProxyUrl,
507
556
  )
508
557
  if (!parsed.projectName && !parsed.iconUrl) return
509
- enrichmentCache.set(port, {
558
+ enrichmentCache.set(result.port, {
510
559
  projectName: parsed.projectName,
511
560
  iconUrl: parsed.iconUrl,
512
561
  iconPath: parsed.iconPath,
@@ -514,7 +563,7 @@ async function fetchManifestEnrichment(
514
563
  })
515
564
  // the nameless result cached as a strong entry and won't re-probe on its own;
516
565
  // drop it so the next scan re-runs probePort and publishes the enriched name.
517
- portCache.delete(port)
566
+ portCache.delete(result.port)
518
567
  }
519
568
 
520
569
  // ports confirmed to be running a non-sootsim dev server — safe to skip the
@@ -562,7 +611,8 @@ export async function probePort(
562
611
  // too, or a cold CLI process can build the known app's bundle url before its
563
612
  // runtime config is available.
564
613
  await loadOptionalDemoApps()
565
- if (!(await tcpPing(port))) return null
614
+ const probeHostname = await tcpPing(port)
615
+ if (!probeHostname) return null
566
616
 
567
617
  const suppliedPid = listeningProcess?.pid
568
618
  const ownerPid =
@@ -577,12 +627,18 @@ export async function probePort(
577
627
 
578
628
  // wave 1 — safe parallel probes. none of these mutate metro state.
579
629
  const [sootsimRes, statusRes, manifestRes, expoRes] = await Promise.all([
580
- knownNonPatched.has(port) ? Promise.resolve(null) : httpGet(port, '/__soot/'),
581
- httpGet(port, '/status'),
630
+ knownNonPatched.has(port)
631
+ ? Promise.resolve(null)
632
+ : httpGet(probeHostname, port, '/__soot/'),
633
+ httpGet(probeHostname, port, '/status'),
582
634
  knownOne.has(port)
583
635
  ? Promise.resolve(null)
584
- : httpGet(port, '/', 'GET', MANIFEST_TIMEOUT_MS, { 'expo-platform': 'ios' }),
585
- knownNonExpo.has(port) ? Promise.resolve(null) : httpGet(port, '/_expo/status'),
636
+ : httpGet(probeHostname, port, '/', 'GET', MANIFEST_TIMEOUT_MS, {
637
+ 'expo-platform': 'ios',
638
+ }),
639
+ knownNonExpo.has(port)
640
+ ? Promise.resolve(null)
641
+ : httpGet(probeHostname, port, '/_expo/status'),
586
642
  ])
587
643
 
588
644
  // remember negative /_expo/status responses so we don't keep probing this
@@ -620,7 +676,7 @@ export async function probePort(
620
676
  knownNonPatched.add(port)
621
677
  const launchUrl =
622
678
  manifestLaunchUrl ||
623
- `http://localhost:${port}/index.bundle?platform=ios&dev=true&hot=true&minify=false`
679
+ `${buildLoopbackProbeBaseUrl('http:', probeHostname, port)}/index.bundle?platform=ios&dev=true&hot=true&minify=false`
624
680
  // call the framework "one" only when launchAsset clearly points at
625
681
  // a One metro entry; otherwise treat it as plain expo so downstream
626
682
  // bundle handling uses the right path semantics.
@@ -634,7 +690,7 @@ export async function probePort(
634
690
  port,
635
691
  framework,
636
692
  bundleUrl: withRuntimeConfig(port, launchUrl, owner.cwd),
637
- hmrUrl: `ws://localhost:${port}/hot`,
693
+ hmrUrl: `${buildLoopbackProbeBaseUrl('ws:', probeHostname, port)}/hot`,
638
694
  lastSeen: Date.now(),
639
695
  ...owner,
640
696
  },
@@ -654,7 +710,12 @@ export async function probePort(
654
710
  knownNonPatched.add(port)
655
711
  return enrichWithManifest(
656
712
  applyManifest(
657
- makeResult(port, expoRes && expoRes.statusCode === 200 ? 'expo' : 'metro', owner),
713
+ makeResult(
714
+ probeHostname,
715
+ port,
716
+ expoRes && expoRes.statusCode === 200 ? 'expo' : 'metro',
717
+ owner,
718
+ ),
658
719
  manifestRes,
659
720
  buildIconProxyUrl,
660
721
  ),
@@ -677,10 +738,10 @@ export async function probePort(
677
738
  framework: 'one',
678
739
  bundleUrl: withRuntimeConfig(
679
740
  port,
680
- `http://localhost:${port}/__soot/bundle.js`,
741
+ `${buildLoopbackProbeBaseUrl('http:', probeHostname, port)}/__soot/bundle.js`,
681
742
  owner.cwd,
682
743
  ),
683
- hmrUrl: `ws://localhost:${port}/hot`,
744
+ hmrUrl: `${buildLoopbackProbeBaseUrl('ws:', probeHostname, port)}/hot`,
684
745
  lastSeen: Date.now(),
685
746
  patched: true,
686
747
  ...owner,
@@ -719,7 +780,7 @@ export async function probePort(
719
780
  return null
720
781
  }
721
782
 
722
- const oneRes = await httpGet(port, onePath, 'HEAD')
783
+ const oneRes = await httpGet(probeHostname, port, onePath, 'HEAD')
723
784
  if (
724
785
  oneRes &&
725
786
  oneRes.statusCode > 0 &&
@@ -735,10 +796,10 @@ export async function probePort(
735
796
  framework: 'one',
736
797
  bundleUrl: withRuntimeConfig(
737
798
  port,
738
- `http://localhost:${port}${onePath}&minify=false`,
799
+ `${buildLoopbackProbeBaseUrl('http:', probeHostname, port)}${onePath}&minify=false`,
739
800
  owner.cwd,
740
801
  ),
741
- hmrUrl: `ws://localhost:${port}/hot`,
802
+ hmrUrl: `${buildLoopbackProbeBaseUrl('ws:', probeHostname, port)}/hot`,
742
803
  lastSeen: Date.now(),
743
804
  ...owner,
744
805
  },
@@ -12,6 +12,10 @@ Detox runs existing imperative Jest suites through the rnx driver.
12
12
  > needs a connected, pinned sim. if `rnx describe` errors, load
13
13
  > `/rnx-setup` first.
14
14
 
15
+ Set `SOOTSIM_NO_OPEN=1` before starting an app dev server for a Detox,
16
+ Maestro, or iOS-only pass. Apps can opt into a named Electron window through
17
+ the official Vite or Metro plugin, and test-only work must not open that window.
18
+
15
19
  ## route in (decision tree)
16
20
 
17
21
  - have an existing `.maestro/` directory? → [Maestro](#branch--maestro)
@@ -1,9 +1,23 @@
1
1
  const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://contrast.dev'
2
2
 
3
- function normalizeHostname(hostname: string): string {
3
+ export const LOOPBACK_PROBE_HOSTNAMES = ['::1', '127.0.0.1'] as const
4
+
5
+ export function normalizeHostname(hostname: string): string {
4
6
  return hostname.replace(/^\[|\]$/g, '').toLowerCase()
5
7
  }
6
8
 
9
+ export function buildLoopbackProbeBaseUrl(
10
+ protocol: 'http:' | 'ws:',
11
+ hostname: string,
12
+ port: number,
13
+ ): string {
14
+ const normalizedHostname = normalizeHostname(hostname)
15
+ const urlHost = normalizedHostname.includes(':')
16
+ ? `[${normalizedHostname}]`
17
+ : normalizedHostname
18
+ return `${protocol}//${urlHost}:${port}`
19
+ }
20
+
7
21
  export function isLoopbackHost(hostname: string): boolean {
8
22
  const normalized = normalizeHostname(hostname)
9
23
  return (
@@ -1,5 +1,13 @@
1
- import { isLoopbackHost } from './backend-origin'
2
- import { METRO_BUNDLE_PATH, normalizeNativeDevBundleUrl } from './native-dev-bundle-url'
1
+ import {
2
+ buildLoopbackProbeBaseUrl,
3
+ isLoopbackHost,
4
+ LOOPBACK_PROBE_HOSTNAMES,
5
+ } from './backend-origin'
6
+ import {
7
+ alignLoopbackBundleUrlWithProbe,
8
+ METRO_BUNDLE_PATH,
9
+ normalizeNativeDevBundleUrl,
10
+ } from './native-dev-bundle-url'
3
11
 
4
12
  export const DEFAULT_TEST_DRIVE_PORT = 8081
5
13
 
@@ -65,8 +73,8 @@ function isBaseServerUrl(url: URL) {
65
73
 
66
74
  // resolve a loopback dev-server port through the SAME server-side scanner the
67
75
  // dev-server list uses (`/__server-scan`), so an explicit port (`/rn/<port>`, a
68
- // typed port) gets the EXACT manifest `launchAsset.url` the list resolves —
69
- // byte-identical, one resolver.
76
+ // typed port) gets the same manifest path and query the list resolves. loopback
77
+ // hosts stay aligned to the address that answered discovery.
70
78
  //
71
79
  // why this exists: in a browser/worker the direct manifest probe below is
72
80
  // cross-origin (shell on :5173, dev server on :<port>), and a dev server
@@ -109,8 +117,8 @@ async function resolvePortViaServerScan(port: number): Promise<ResolvedDevBundle
109
117
  if (!match) return null
110
118
  return {
111
119
  // normalize exactly like the manifest-probe branch below — the scan
112
- // returns metro's raw launchAsset url (lazy=true for code-split one/vxrn
113
- // apps), but sootsim loads ONE self-contained bundle into the tenant
120
+ // returns metro's manifest launchAsset url (lazy=true for code-split
121
+ // one/vxrn apps), but sootsim loads ONE self-contained bundle into the tenant
114
122
  // worker, so a deferred React.lazy chunk throws "Requiring unknown module
115
123
  // <id>". normalizeNativeDevBundleUrl forces lazy=false + strips bytecode.
116
124
  bundleUrl: normalizeNativeDevBundleUrl(match.bundleUrl),
@@ -123,21 +131,12 @@ async function resolvePortViaServerScan(port: number): Promise<ResolvedDevBundle
123
131
  }
124
132
  }
125
133
 
126
- async function probeBaseUrlBundle(
134
+ async function probeBaseUrlBundleDirect(
127
135
  baseUrl: string,
128
136
  port: number,
129
137
  ): Promise<ResolvedDevBundle | null> {
130
138
  const normalizedBaseUrl = baseUrl.replace(/\/+$/, '')
131
139
 
132
- // browser/worker loopback targets resolve through the server-side scanner so
133
- // they match the dev-server list exactly — see resolvePortViaServerScan.
134
- try {
135
- if (isLoopbackHost(new URL(normalizedBaseUrl).hostname)) {
136
- const viaScan = await resolvePortViaServerScan(port)
137
- if (viaScan) return viaScan
138
- }
139
- } catch {}
140
-
141
140
  try {
142
141
  const manifestRes = await fetchDevProbe(`${normalizedBaseUrl}/`, {
143
142
  headers: { 'expo-platform': 'ios' },
@@ -145,10 +144,13 @@ async function probeBaseUrlBundle(
145
144
  if (manifestRes.ok) {
146
145
  const manifest = await manifestRes.json()
147
146
  const client = manifest?.extra?.expoClient || manifest?.extra || {}
148
- const launchUrl =
147
+ const rawLaunchUrl =
149
148
  typeof manifest?.launchAsset?.url === 'string'
150
149
  ? manifest.launchAsset.url
151
150
  : undefined
151
+ const launchUrl = rawLaunchUrl
152
+ ? alignLoopbackBundleUrlWithProbe(rawLaunchUrl, normalizedBaseUrl)
153
+ : undefined
152
154
  if (launchUrl || client.name) {
153
155
  return {
154
156
  bundleUrl: normalizeNativeDevBundleUrl(
@@ -191,8 +193,30 @@ async function probeBaseUrlBundle(
191
193
  return null
192
194
  }
193
195
 
196
+ async function probeBaseUrlBundle(
197
+ baseUrl: string,
198
+ port: number,
199
+ ): Promise<ResolvedDevBundle | null> {
200
+ // browser/worker loopback targets resolve through the server-side scanner so
201
+ // they match the dev-server list exactly — see resolvePortViaServerScan.
202
+ try {
203
+ if (isLoopbackHost(new URL(baseUrl).hostname)) {
204
+ const viaScan = await resolvePortViaServerScan(port)
205
+ if (viaScan) return viaScan
206
+ }
207
+ } catch {}
208
+ return probeBaseUrlBundleDirect(baseUrl, port)
209
+ }
210
+
194
211
  export async function probePortBundle(port: number): Promise<ResolvedDevBundle | null> {
195
- return probeBaseUrlBundle(buildBaseUrl('http:', 'localhost', port), port)
212
+ const viaScan = await resolvePortViaServerScan(port)
213
+ if (viaScan) return viaScan
214
+ const candidates = await Promise.all(
215
+ LOOPBACK_PROBE_HOSTNAMES.map((hostname) =>
216
+ probeBaseUrlBundleDirect(buildLoopbackProbeBaseUrl('http:', hostname, port), port),
217
+ ),
218
+ )
219
+ return candidates.find((candidate) => candidate !== null) ?? null
196
220
  }
197
221
 
198
222
  export async function resolveConnectionInput(input: string): Promise<ResolvedDevBundle> {
@@ -0,0 +1,197 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import fs from 'node:fs'
4
+ import http from 'node:http'
5
+ import path from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ export type RnxDesktopOpenOption = boolean | { appName: string }
9
+
10
+ const scheduledMetroOpens = new Set<string>()
11
+
12
+ export function resolveRnxDesktopAppName(
13
+ open: RnxDesktopOpenOption | undefined,
14
+ projectRoot: string,
15
+ ): string | null {
16
+ if (!open) return null
17
+ if (typeof open === 'object') {
18
+ const appName = open.appName.trim()
19
+ if (!appName) throw new Error('rnx: open.appName must not be empty')
20
+ return appName
21
+ }
22
+
23
+ const packageJsonPath = path.join(projectRoot, 'package.json')
24
+ const packageJson: unknown = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
25
+ const packageName =
26
+ packageJson && typeof packageJson === 'object'
27
+ ? Reflect.get(packageJson, 'name')
28
+ : undefined
29
+ if (typeof packageName !== 'string' || !packageName.trim()) {
30
+ throw new Error(`rnx: ${packageJsonPath} must declare a package name`)
31
+ }
32
+ return packageName.trim()
33
+ }
34
+
35
+ export function tagRnxBundleForPlugin(
36
+ bundleUrl: string,
37
+ replacementOrigin: string,
38
+ replacementToken: string,
39
+ projectRoot: string,
40
+ appName?: string | null,
41
+ ): string {
42
+ const absolute = /^[a-z][a-z\d+.-]*:\/\//i.test(bundleUrl)
43
+ const url = new URL(bundleUrl, absolute ? undefined : 'http://rnx.local')
44
+ if (appName) {
45
+ url.searchParams.set('rnxAppName', appName)
46
+ url.searchParams.set(
47
+ 'rnxProjectId',
48
+ createHash('sha256')
49
+ .update(fs.realpathSync(projectRoot))
50
+ .digest('hex')
51
+ .slice(0, 32),
52
+ )
53
+ }
54
+ url.searchParams.set('rnxReplacementOrigin', replacementOrigin)
55
+ url.searchParams.set('rnxReplacementToken', replacementToken)
56
+ return absolute ? url.toString() : `${url.pathname}${url.search}${url.hash}`
57
+ }
58
+
59
+ export function resolveRnxDesktopLaunchTarget(port: number, bundleUrl: string): string {
60
+ return new URL(bundleUrl, `http://localhost:${port}`).toString()
61
+ }
62
+
63
+ export function buildRnxDesktopOpenArgs(bundleUrl: string): string[] {
64
+ return ['open', bundleUrl, '--new', '--driver', 'electron', '--no-describe']
65
+ }
66
+
67
+ export function observeRnxDesktopLaunch(child: ChildProcess, appName: string): void {
68
+ let stderr = ''
69
+ let spawnFailed = false
70
+ child.stderr?.setEncoding('utf8')
71
+ child.stderr?.on('data', (chunk: string) => {
72
+ stderr = `${stderr}${chunk}`.slice(-16_384)
73
+ })
74
+ child.once('error', (error) => {
75
+ spawnFailed = true
76
+ console.error(`[rnx] could not open ${appName}: ${error.message}`)
77
+ })
78
+ child.once('close', (code, signal) => {
79
+ if (spawnFailed) return
80
+ if (code === 0) {
81
+ console.log(`[rnx] opened ${appName} in the desktop simulator`)
82
+ return
83
+ }
84
+ const detail = stderr.trim() || (signal ? `signal ${signal}` : `exit ${code}`)
85
+ console.error(`[rnx] could not open ${appName}: ${detail}`)
86
+ })
87
+ }
88
+
89
+ export function launchRnxDesktop(bundleUrl: string, appName: string): void {
90
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
91
+ const binPath = path.join(packageRoot, 'dist-cli', 'bin.js')
92
+ console.log(`[rnx] opening ${appName} in the desktop simulator`)
93
+ const child = spawn(
94
+ process.execPath,
95
+ [binPath, ...buildRnxDesktopOpenArgs(bundleUrl)],
96
+ {
97
+ stdio: ['ignore', 'ignore', 'pipe'],
98
+ windowsHide: true,
99
+ },
100
+ )
101
+ observeRnxDesktopLaunch(child, appName)
102
+ }
103
+
104
+ interface MetroDesktopOpenOptions {
105
+ port: number
106
+ bundleUrl: string
107
+ appName: string
108
+ projectRoot: string
109
+ serverId: string
110
+ intervalMs?: number
111
+ timeoutMs?: number
112
+ launch?: (bundleUrl: string, appName: string) => void
113
+ }
114
+
115
+ export function openRnxDesktopWhenMetroIsReady({
116
+ port,
117
+ bundleUrl,
118
+ appName,
119
+ projectRoot,
120
+ serverId,
121
+ intervalMs = 100,
122
+ timeoutMs = 30_000,
123
+ launch = launchRnxDesktop,
124
+ }: MetroDesktopOpenOptions): Promise<boolean> {
125
+ const key = `${projectRoot}\0${port}`
126
+ if (scheduledMetroOpens.has(key)) return Promise.resolve(false)
127
+ scheduledMetroOpens.add(key)
128
+ const deadline = Date.now() + timeoutMs
129
+
130
+ return new Promise((resolve) => {
131
+ let finished = false
132
+ const finish = (opened: boolean) => {
133
+ if (finished) return
134
+ finished = true
135
+ scheduledMetroOpens.delete(key)
136
+ resolve(opened)
137
+ }
138
+ const retry = () => {
139
+ if (finished) return
140
+ if (Date.now() >= deadline) {
141
+ console.error(
142
+ `[rnx] could not open ${appName}: Metro did not become ready on port ${port}`,
143
+ )
144
+ finish(false)
145
+ return
146
+ }
147
+ const timer = setTimeout(check, intervalMs)
148
+ timer.unref()
149
+ }
150
+ const check = () => {
151
+ const request = http.get(
152
+ `http://127.0.0.1:${port}/__server-scan`,
153
+ { headers: { Accept: 'application/json' } },
154
+ (response) => {
155
+ let body = ''
156
+ response.setEncoding('utf8')
157
+ response.on('data', (chunk: string) => {
158
+ body = `${body}${chunk}`.slice(0, 65_536)
159
+ })
160
+ response.once('end', () => {
161
+ try {
162
+ const scan: unknown = JSON.parse(body)
163
+ const matches =
164
+ response.statusCode === 200 &&
165
+ Array.isArray(scan) &&
166
+ scan.some((entry: unknown) => {
167
+ if (!entry || typeof entry !== 'object') return false
168
+ const scannedPort = Reflect.get(entry, 'port')
169
+ const projectName = Reflect.get(entry, 'projectName')
170
+ const scannedServerId = Reflect.get(entry, 'rnxServerId')
171
+ const scannedBundle = Reflect.get(entry, 'bundleUrl')
172
+ if (
173
+ scannedPort !== port ||
174
+ projectName !== appName ||
175
+ scannedServerId !== serverId ||
176
+ typeof scannedBundle !== 'string'
177
+ ) {
178
+ return false
179
+ }
180
+ return new URL(scannedBundle).searchParams.get('rnxAppName') === appName
181
+ })
182
+ if (matches) {
183
+ launch(bundleUrl, appName)
184
+ finish(true)
185
+ return
186
+ }
187
+ } catch {}
188
+ retry()
189
+ })
190
+ },
191
+ )
192
+ request.setTimeout(Math.min(1_000, timeoutMs), () => request.destroy())
193
+ request.once('error', retry)
194
+ }
195
+ check()
196
+ })
197
+ }