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.
- package/README.md +59 -31
- package/cli/app-fonts.ts +8 -28
- package/cli/app-project.ts +12 -184
- package/cli/app-splash.ts +3 -17
- package/cli/bin.ts +15 -28
- package/cli/commands/control.ts +20 -30
- package/cli/commands/daemon.ts +19 -44
- package/cli/commands/flow.ts +0 -27
- package/cli/commands/install-desktop.ts +2 -16
- package/cli/commands/login.ts +3 -43
- package/cli/commands/logout.ts +0 -7
- package/cli/commands/serve.ts +2 -32
- package/cli/commands/setup.ts +20 -194
- package/cli/commands/telemetry.ts +7 -5
- package/cli/commands/upload.ts +1 -48
- package/cli/commands/welcome.ts +64 -0
- package/cli/outbound-endpoints.ts +586 -0
- package/cli/privacy.ts +53 -0
- package/cli/runtime-notes.ts +2 -2
- package/cli/telemetry.ts +46 -149
- package/dist-cli/bin.js +6 -6
- package/dist-cli/chunks/{agent-HTSE5FA4.js → agent-U6HZUHAQ.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-L6X2AEZL.js → agent-wrapper-IM2VL6XP.js} +2 -2
- package/dist-cli/chunks/{app-fonts-FGF3ETYH.js → app-fonts-LHVTPWCN.js} +2 -2
- package/dist-cli/chunks/{assert-HA2ZYVDN.js → assert-UMGCZXWN.js} +2 -2
- package/dist-cli/chunks/{auth-HONDAV75.js → auth-PQY57FPY.js} +2 -2
- package/dist-cli/chunks/{beta-U7EE4GMR.js → beta-3MMNRHL7.js} +2 -2
- package/dist-cli/chunks/{camera-RTKNUTIX.js → camera-UKISEHMH.js} +2 -2
- package/dist-cli/chunks/{chunk-CNLPLCO2.js → chunk-36P343FE.js} +2 -2
- package/dist-cli/chunks/{chunk-IWR4HFC4.js → chunk-3KPJVR63.js} +2 -2
- package/dist-cli/chunks/chunk-54Y5IXWQ.js +15 -0
- package/dist-cli/chunks/{chunk-EPEZZ6YS.js → chunk-5KMV7NAJ.js} +2 -2
- package/dist-cli/chunks/{chunk-NDGAF5OF.js → chunk-5WIYZI5H.js} +2 -2
- package/dist-cli/chunks/{chunk-KBSVSAAE.js → chunk-6GXUQ3DM.js} +2 -2
- package/dist-cli/chunks/{chunk-KT6WMCUS.js → chunk-6MUEKKJ4.js} +1 -1
- package/dist-cli/chunks/{chunk-TOHQI2II.js → chunk-6Q6S53R7.js} +1 -1
- package/dist-cli/chunks/{chunk-63NZGJ4A.js → chunk-6SQCDLTJ.js} +1 -1
- package/dist-cli/chunks/{chunk-Q2B6EQPB.js → chunk-6UW6O575.js} +2 -2
- package/dist-cli/chunks/chunk-6VH3UUYQ.js +4 -0
- package/dist-cli/chunks/{chunk-BGLWR43R.js → chunk-6XJGGFMD.js} +1 -1
- package/dist-cli/chunks/{chunk-TZNRKIS3.js → chunk-75LMZXF5.js} +2 -2
- package/dist-cli/chunks/{chunk-FZ5DRZJE.js → chunk-7CJNUNQA.js} +3 -3
- package/dist-cli/chunks/{chunk-UCVQOQCY.js → chunk-BQDGFR4B.js} +2 -2
- package/dist-cli/chunks/{chunk-H4J7EWZC.js → chunk-BSIVS2SQ.js} +1 -1
- package/dist-cli/chunks/{chunk-6E6SBYIO.js → chunk-CD2LITQX.js} +1 -1
- package/dist-cli/chunks/{chunk-PNMCALX5.js → chunk-DGH3D6WS.js} +2 -2
- package/dist-cli/chunks/chunk-EP6XBKPM.js +11 -0
- package/dist-cli/chunks/{chunk-OCUXAA6O.js → chunk-EVX6Y7ZM.js} +2 -2
- package/dist-cli/chunks/chunk-EYRKIWHE.js +4 -0
- package/dist-cli/chunks/{chunk-N5PFDTFL.js → chunk-FSYB2S6Q.js} +17 -17
- package/dist-cli/chunks/{chunk-6PEZSVIG.js → chunk-HPCTBEAP.js} +1 -1
- package/dist-cli/chunks/chunk-HS2BME6V.js +6 -0
- package/dist-cli/chunks/{chunk-G6IPT2PS.js → chunk-I3GUDHG2.js} +2 -2
- package/dist-cli/chunks/{chunk-MWUXQIA3.js → chunk-I4HVFKLM.js} +2 -2
- package/dist-cli/chunks/{chunk-ZPTFL5FQ.js → chunk-IBNK7SWL.js} +2 -2
- package/dist-cli/chunks/chunk-IUG4GWQC.js +25 -0
- package/dist-cli/chunks/{chunk-XQFDEY3F.js → chunk-IUIKD2YI.js} +1 -1
- package/dist-cli/chunks/{chunk-JFUVWIJT.js → chunk-JPVXLY7X.js} +1 -1
- package/dist-cli/chunks/{chunk-JLUEQPEF.js → chunk-K4PZLT2K.js} +1 -1
- package/dist-cli/chunks/{chunk-CRWYKW67.js → chunk-KNIKUJPD.js} +1 -1
- package/dist-cli/chunks/{chunk-EZLLZDYX.js → chunk-L6GWDWGZ.js} +2 -2
- package/dist-cli/chunks/chunk-LAVDDUQR.js +4 -0
- package/dist-cli/chunks/{chunk-22WJPMTQ.js → chunk-LPXKPZ4B.js} +1 -1
- package/dist-cli/chunks/{chunk-IPC45HTT.js → chunk-LYLK5VRP.js} +2 -2
- package/dist-cli/chunks/{chunk-S6LBAUWH.js → chunk-ME46FC4L.js} +1 -1
- package/dist-cli/chunks/chunk-NNXSLD76.js +99 -0
- package/dist-cli/chunks/{chunk-7V4ULF3U.js → chunk-PNH3YBZ4.js} +2 -2
- package/dist-cli/chunks/{chunk-GRX6NSBG.js → chunk-PPEJMNRO.js} +2 -2
- package/dist-cli/chunks/{chunk-K3VNCCEJ.js → chunk-SEWGXJG6.js} +3 -3
- package/dist-cli/chunks/{chunk-R3UYHE6G.js → chunk-T4JPZI32.js} +1 -1
- package/dist-cli/chunks/{chunk-NRTHFEPM.js → chunk-TAQYETRH.js} +2 -2
- package/dist-cli/chunks/{chunk-7JEEO33Y.js → chunk-TBVBWJJL.js} +2 -2
- package/dist-cli/chunks/{chunk-HRBMD2FS.js → chunk-UA54YLWR.js} +26 -26
- package/dist-cli/chunks/{chunk-G3UAFZE2.js → chunk-UV3TXNMG.js} +1 -1
- package/dist-cli/chunks/{chunk-EH35QQG4.js → chunk-VZ6YFPIN.js} +45 -45
- package/dist-cli/chunks/chunk-W54SRZ5K.js +14 -0
- package/dist-cli/chunks/chunk-WAC75WN6.js +4 -0
- package/dist-cli/chunks/{chunk-S7CSBQ6G.js → chunk-X2WIEZ7Q.js} +1 -1
- package/dist-cli/chunks/{chunk-EN32LN2B.js → chunk-XWTYFWZ3.js} +2 -2
- package/dist-cli/chunks/chunk-Y2C3CZVD.js +4 -0
- package/dist-cli/chunks/chunk-YM4L2CFB.js +4 -0
- package/dist-cli/chunks/{chunk-ZZSBCGHC.js → chunk-YXDR4CAQ.js} +1 -1
- package/dist-cli/chunks/{chunk-6NLWKL7Y.js → chunk-Z6P427WC.js} +2 -2
- package/dist-cli/chunks/{cleanup-EMYD5WZG.js → cleanup-KXPS5VSK.js} +2 -2
- package/dist-cli/chunks/cli-version-LJ6NFLBP.js +4 -0
- package/dist-cli/chunks/{compat-W2N5QTVG.js → compat-JWF7N3VO.js} +2 -2
- package/dist-cli/chunks/config-M42VJXNK.js +13 -0
- package/dist-cli/chunks/{control-4SFBROUE.js → control-HMZDIS6P.js} +2 -2
- package/dist-cli/chunks/daemon-MQ4UYKNQ.js +4 -0
- package/dist-cli/chunks/{debug-HCR2KFUA.js → debug-F44GWYMF.js} +3 -3
- package/dist-cli/chunks/{desktop-T2KS5PBJ.js → desktop-YJMWW6MW.js} +3 -3
- package/dist-cli/chunks/{detox-5S7EHINH.js → detox-5B4ASTJO.js} +2 -2
- package/dist-cli/chunks/{device-LVM3MZRJ.js → device-2544YZNY.js} +2 -2
- package/dist-cli/chunks/{diagnose-WK37XKLI.js → diagnose-LS4245JR.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-X7VJOXMS.js → disk-cleanup-XYL2HQQY.js} +2 -2
- package/dist-cli/chunks/drivers-P5CRSKH6.js +4 -0
- package/dist-cli/chunks/{film-7KLTI3ER.js → film-LS3TIZRM.js} +3 -3
- package/dist-cli/chunks/flow-7V77GXAB.js +4 -0
- package/dist-cli/chunks/help-HRESIFZX.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-4RVGUVFO.js → hidden-runtime-alias-HGN2EEQR.js} +2 -2
- package/dist-cli/chunks/{home-paths-5DHSAEGZ.js → home-paths-T3NTXE4U.js} +2 -2
- package/dist-cli/chunks/inspect-UC44UH4M.js +4 -0
- package/dist-cli/chunks/install-desktop-RDV66JHG.js +4 -0
- package/dist-cli/chunks/login-WVP5H5NQ.js +26 -0
- package/dist-cli/chunks/logout-GXX5OY6X.js +4 -0
- package/dist-cli/chunks/{maestro-HTRLQDJE.js → maestro-MQMGBNOB.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-GX2QNA3F.js → maestro-generate-CO355QV2.js} +3 -3
- package/dist-cli/chunks/{mode-GM6KZHQP.js → mode-QFB3M2AA.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-B4TD7JQP.js → optional-demo-registry-UIPVSBHV.js} +2 -2
- package/dist-cli/chunks/{perf-3PRXJQIC.js → perf-TDOSW3LD.js} +2 -2
- package/dist-cli/chunks/{permissions-TJZIRF2S.js → permissions-TEVQLCGJ.js} +2 -2
- package/dist-cli/chunks/privacy-DVHAMHR6.js +4 -0
- package/dist-cli/chunks/{record-LI55DTPE.js → record-OFEQLSWA.js} +3 -3
- package/dist-cli/chunks/{report-issue-TBGJVQIQ.js → report-issue-B2UPOERJ.js} +2 -2
- package/dist-cli/chunks/reset-XQQYM55B.js +4 -0
- package/dist-cli/chunks/runtime-GN6HITVB.js +4 -0
- package/dist-cli/chunks/{screenshot-command-BEN7YYIF.js → screenshot-command-NTLAY7QN.js} +3 -3
- package/dist-cli/chunks/{screenshot-layers-3IU4KYNO.js → screenshot-layers-BBLKNRQU.js} +2 -2
- package/dist-cli/chunks/{screenshots-capture-ZRC54ST6.js → screenshots-capture-6GHIRCTG.js} +2 -2
- package/dist-cli/chunks/serve-H6GJ4OHW.js +118 -0
- package/dist-cli/chunks/setup-6YLKSMWS.js +30 -0
- package/dist-cli/chunks/{skills-GKBR2HQB.js → skills-FUL7D6C7.js} +2 -2
- package/dist-cli/chunks/{state-LIHZJSL7.js → state-O2HE7XSR.js} +2 -2
- package/dist-cli/chunks/{storage-4TPN3NFB.js → storage-WIZTF7RN.js} +4 -4
- package/dist-cli/chunks/store-NJ2MBZC2.js +4 -0
- package/dist-cli/chunks/telemetry-BEC6W5WP.js +4 -0
- package/dist-cli/chunks/{timeline-EX63UNWU.js → timeline-MA4JT7UJ.js} +2 -2
- package/dist-cli/chunks/{upgrade-O6Y6EINE.js → upgrade-H5LSDXBH.js} +2 -2
- package/dist-cli/chunks/upload-Q4DWKIA4.js +4 -0
- package/dist-cli/chunks/{version-HLNL3NFB.js → version-4UWEEDVP.js} +2 -2
- package/dist-cli/chunks/{web-4EDPYPW7.js → web-F3LXRN3K.js} +2 -2
- package/dist-cli/chunks/welcome-AZBOSIG2.js +5 -0
- package/dist-cli/chunks/{what-happened-77VNUEPK.js → what-happened-6OSNLOAY.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +13 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +43 -13
- package/dist-lib/home-paths.cjs +145 -11
- package/dist-lib/host/bridge-host.cjs +326 -78
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +71 -4
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +337 -37
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +484 -14
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +120 -50
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +493 -439
- package/dist-lib/vite.cjs +193 -37
- package/package.json +2 -1
- package/scripts/dev-server-scanner.ts +112 -51
- package/skills/rnx-test/SKILL.md +4 -0
- package/src/backend-origin.ts +15 -1
- package/src/dev-bundle-resolution.ts +42 -18
- package/src/dev-server-open.ts +197 -0
- package/src/home-paths.ts +168 -15
- package/src/host/bridge-host.ts +2 -2
- package/src/host/replacement-module-handler.ts +68 -2
- package/src/metro-plugin.ts +83 -5
- package/src/native-dev-bundle-url.ts +22 -0
- package/src/vite-plugin-one.ts +42 -28
- package/dist-cli/chunks/chunk-3JC7H4PR.js +0 -25
- package/dist-cli/chunks/chunk-6DLIC7BZ.js +0 -4
- package/dist-cli/chunks/chunk-7TNPO2OJ.js +0 -9
- package/dist-cli/chunks/chunk-AMF5KTOI.js +0 -4
- package/dist-cli/chunks/chunk-BX5VXTOQ.js +0 -101
- package/dist-cli/chunks/chunk-D73H7ACZ.js +0 -4
- package/dist-cli/chunks/chunk-DY4O7KYI.js +0 -4
- package/dist-cli/chunks/chunk-GU2V4JDG.js +0 -15
- package/dist-cli/chunks/chunk-POOCKX5B.js +0 -14
- package/dist-cli/chunks/chunk-Z2HMSVSN.js +0 -4
- package/dist-cli/chunks/cli-version-CN3NTKLH.js +0 -4
- package/dist-cli/chunks/config-SELKGT4F.js +0 -13
- package/dist-cli/chunks/daemon-V2SFCU2E.js +0 -4
- package/dist-cli/chunks/drivers-3YFTJ5WA.js +0 -4
- package/dist-cli/chunks/flow-36ANTMJU.js +0 -4
- package/dist-cli/chunks/help-ZGL26SPK.js +0 -4
- package/dist-cli/chunks/inspect-R3B55RTS.js +0 -4
- package/dist-cli/chunks/install-desktop-TZ3H3VWK.js +0 -4
- package/dist-cli/chunks/login-4DFULI7S.js +0 -28
- package/dist-cli/chunks/logout-QMCVYMBB.js +0 -4
- package/dist-cli/chunks/reset-O6MW6X7X.js +0 -4
- package/dist-cli/chunks/runtime-JESXBHRQ.js +0 -4
- package/dist-cli/chunks/serve-QGKTWPNF.js +0 -118
- package/dist-cli/chunks/setup-3WPWATCX.js +0 -32
- package/dist-cli/chunks/store-VBX3L46S.js +0 -4
- package/dist-cli/chunks/telemetry-ACNNIQIG.js +0 -4
- 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 {
|
|
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<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
377
|
+
`${buildLoopbackProbeBaseUrl('http:', hostname, port)}/index.bundle?platform=ios&dev=true&hot=true&minify=false`,
|
|
352
378
|
owner.cwd,
|
|
353
379
|
),
|
|
354
|
-
hmrUrl:
|
|
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).
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
const
|
|
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
|
-
|
|
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
|
-
:
|
|
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, {
|
|
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
|
-
|
|
487
|
-
|
|
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
|
-
|
|
531
|
+
result: DiscoveredServer,
|
|
496
532
|
buildIconProxyUrl?: (externalUrl: string) => string,
|
|
497
533
|
): Promise<void> {
|
|
498
|
-
const
|
|
499
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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)
|
|
581
|
-
|
|
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, {
|
|
585
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
741
|
+
`${buildLoopbackProbeBaseUrl('http:', probeHostname, port)}/__soot/bundle.js`,
|
|
681
742
|
owner.cwd,
|
|
682
743
|
),
|
|
683
|
-
hmrUrl:
|
|
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
|
-
|
|
799
|
+
`${buildLoopbackProbeBaseUrl('http:', probeHostname, port)}${onePath}&minify=false`,
|
|
739
800
|
owner.cwd,
|
|
740
801
|
),
|
|
741
|
-
hmrUrl:
|
|
802
|
+
hmrUrl: `${buildLoopbackProbeBaseUrl('ws:', probeHostname, port)}/hot`,
|
|
742
803
|
lastSeen: Date.now(),
|
|
743
804
|
...owner,
|
|
744
805
|
},
|
package/skills/rnx-test/SKILL.md
CHANGED
|
@@ -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)
|
package/src/backend-origin.ts
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://contrast.dev'
|
|
2
2
|
|
|
3
|
-
|
|
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 {
|
|
2
|
-
|
|
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
|
|
69
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|