rnxsim 0.1.343 → 0.1.345
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-RFD32ACU.js → agent-52G55XTO.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-2YLW2HQK.js → agent-wrapper-BOZM43ON.js} +2 -2
- package/dist-cli/chunks/{app-fonts-6OATDF3Y.js → app-fonts-A2L6V5EZ.js} +2 -2
- package/dist-cli/chunks/{assert-PSQICFOY.js → assert-CJKIP4LN.js} +2 -2
- package/dist-cli/chunks/{auth-XWLFKZT7.js → auth-VHHN3TOB.js} +2 -2
- package/dist-cli/chunks/{beta-I6WTVM6C.js → beta-EBDIGV3E.js} +2 -2
- package/dist-cli/chunks/{camera-45HKTLWG.js → camera-5F4KKCQ3.js} +2 -2
- package/dist-cli/chunks/chunk-3DBTS4MC.js +15 -0
- package/dist-cli/chunks/chunk-3N7M5HMJ.js +6 -0
- package/dist-cli/chunks/{chunk-RWYA2USN.js → chunk-4BIPA7MD.js} +1 -1
- package/dist-cli/chunks/chunk-4OKV7T5D.js +4 -0
- package/dist-cli/chunks/{chunk-T4T6DDKM.js → chunk-4ZILDTZX.js} +2 -2
- package/dist-cli/chunks/{chunk-R7X74I3I.js → chunk-5GXRRUOA.js} +26 -26
- package/dist-cli/chunks/chunk-63ASUHOV.js +4 -0
- package/dist-cli/chunks/{chunk-DASP2EKF.js → chunk-7GO2DSYQ.js} +1 -1
- package/dist-cli/chunks/{chunk-HJXIBZ35.js → chunk-7LGINKM6.js} +2 -2
- package/dist-cli/chunks/{chunk-2IPMH24A.js → chunk-7UIIAHVU.js} +2 -2
- package/dist-cli/chunks/{chunk-ES5BHPTS.js → chunk-7V4KPZ7O.js} +2 -2
- package/dist-cli/chunks/{chunk-FCHRGEHA.js → chunk-7YMQRA6L.js} +2 -2
- package/dist-cli/chunks/{chunk-DC7TS573.js → chunk-A2Q76LJV.js} +2 -2
- package/dist-cli/chunks/{chunk-SAUCA7SN.js → chunk-BCQV63BW.js} +45 -45
- package/dist-cli/chunks/{chunk-3FYJ7U3C.js → chunk-BFTDL2GX.js} +17 -17
- package/dist-cli/chunks/chunk-BHKSCNDC.js +4 -0
- package/dist-cli/chunks/{chunk-QPTJNLPT.js → chunk-DGTV3KOW.js} +1 -1
- package/dist-cli/chunks/chunk-DHM3JDCK.js +4 -0
- package/dist-cli/chunks/{chunk-TQRVLK64.js → chunk-DPP2M4AJ.js} +1 -1
- package/dist-cli/chunks/chunk-E2RGSCHT.js +4 -0
- package/dist-cli/chunks/{chunk-TQ5OPG33.js → chunk-EPLVIDPC.js} +1 -1
- package/dist-cli/chunks/{chunk-P3IDK7FR.js → chunk-GKGXULJK.js} +2 -2
- package/dist-cli/chunks/{chunk-34KDMQ6U.js → chunk-HK7YKH6N.js} +2 -2
- package/dist-cli/chunks/{chunk-BJUNXCPO.js → chunk-HXE344V3.js} +1 -1
- package/dist-cli/chunks/{chunk-HJHFC5ZZ.js → chunk-JC33TOAF.js} +1 -1
- package/dist-cli/chunks/{chunk-G3B2RZ47.js → chunk-KA4PJNTE.js} +2 -2
- package/dist-cli/chunks/{chunk-VC4TDPJQ.js → chunk-KH777UAO.js} +3 -3
- package/dist-cli/chunks/{chunk-4PFMDBLB.js → chunk-KRZPFXVN.js} +1 -1
- package/dist-cli/chunks/{chunk-6SLM6URN.js → chunk-KZFJNDH3.js} +1 -1
- package/dist-cli/chunks/{chunk-KLMWLQAO.js → chunk-LAQGSSTT.js} +1 -1
- package/dist-cli/chunks/chunk-LRM2XCEQ.js +11 -0
- package/dist-cli/chunks/{chunk-YKKVDKLE.js → chunk-NUPRHZQW.js} +2 -2
- package/dist-cli/chunks/{chunk-AKV2DGKM.js → chunk-OPHT4DUO.js} +1 -1
- package/dist-cli/chunks/{chunk-KON6XG4Y.js → chunk-P2CGGMU2.js} +2 -2
- package/dist-cli/chunks/{chunk-5FB4EA33.js → chunk-PLB6UVVX.js} +2 -2
- package/dist-cli/chunks/{chunk-FRRHCLG2.js → chunk-PLUWN2V5.js} +1 -1
- package/dist-cli/chunks/{chunk-WDA4QR6U.js → chunk-QNBBKRCA.js} +2 -2
- package/dist-cli/chunks/{chunk-G7S6T5OX.js → chunk-QNRWPAOA.js} +2 -2
- package/dist-cli/chunks/{chunk-2EVBZ37G.js → chunk-R7ZNRGQN.js} +1 -1
- package/dist-cli/chunks/{chunk-TMJGPUWJ.js → chunk-RB4VFUNM.js} +3 -3
- package/dist-cli/chunks/chunk-RT3DXTDP.js +25 -0
- package/dist-cli/chunks/{chunk-OYLIWHDB.js → chunk-RTDHX6CW.js} +2 -2
- package/dist-cli/chunks/{chunk-YLFDA4WW.js → chunk-T7SBFG5I.js} +2 -2
- package/dist-cli/chunks/{chunk-W7EXE22C.js → chunk-TEU5NGUA.js} +2 -2
- package/dist-cli/chunks/chunk-TMRD5MHL.js +99 -0
- package/dist-cli/chunks/{chunk-SU7RE3PV.js → chunk-TOF5TA5X.js} +11 -11
- package/dist-cli/chunks/{chunk-6IVEGYWV.js → chunk-U5YHXQHH.js} +2 -2
- package/dist-cli/chunks/{chunk-BA3HRFJ5.js → chunk-UVOEHIGP.js} +1 -1
- package/dist-cli/chunks/{chunk-SW4DV2TY.js → chunk-VLBN3KRC.js} +1 -1
- package/dist-cli/chunks/{chunk-7WKV6EKX.js → chunk-WPPGU5IK.js} +2 -2
- package/dist-cli/chunks/{chunk-JOE3J46E.js → chunk-WU72KL6Z.js} +2 -2
- package/dist-cli/chunks/chunk-XJBFJGNT.js +4 -0
- package/dist-cli/chunks/{chunk-3CMKNWRL.js → chunk-XXZLK4BY.js} +1 -1
- package/dist-cli/chunks/{chunk-BKMPV6N4.js → chunk-YVSUFVVB.js} +1 -1
- package/dist-cli/chunks/{chunk-RO3WVQPB.js → chunk-ZMTZ5G5H.js} +2 -2
- package/dist-cli/chunks/{cleanup-OU4VKFC3.js → cleanup-MAVZJXVW.js} +2 -2
- package/dist-cli/chunks/cli-version-2B4KRNML.js +4 -0
- package/dist-cli/chunks/{compat-GAEKP67T.js → compat-7AURLGCS.js} +2 -2
- package/dist-cli/chunks/config-TN3Y4J5N.js +13 -0
- package/dist-cli/chunks/{control-VQJURMFD.js → control-EV2ELU5R.js} +2 -2
- package/dist-cli/chunks/daemon-KKDG5MYR.js +4 -0
- package/dist-cli/chunks/{debug-F5N2LO4T.js → debug-JICITUB6.js} +3 -3
- package/dist-cli/chunks/{desktop-I4ILT4BS.js → desktop-4F5HGLLX.js} +3 -3
- package/dist-cli/chunks/{detox-2HMPYSP4.js → detox-ZC7NHLJI.js} +2 -2
- package/dist-cli/chunks/{device-5LWLOB5R.js → device-V57GHRXX.js} +2 -2
- package/dist-cli/chunks/{diagnose-5S4IQPAP.js → diagnose-FF4IWYXC.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-CJSZVJQX.js → disk-cleanup-T277L6ZO.js} +2 -2
- package/dist-cli/chunks/drivers-LCX7S76X.js +4 -0
- package/dist-cli/chunks/{film-HCPU2TDB.js → film-G7ZAPWB7.js} +3 -3
- package/dist-cli/chunks/flow-ONZGGFGD.js +4 -0
- package/dist-cli/chunks/help-DROVDXJM.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-YYUEZQPR.js → hidden-runtime-alias-VSJNNSNI.js} +2 -2
- package/dist-cli/chunks/{home-paths-3FXISM5I.js → home-paths-75HP3HKA.js} +2 -2
- package/dist-cli/chunks/inspect-MDA7ESGQ.js +4 -0
- package/dist-cli/chunks/install-desktop-EIVZHIFA.js +4 -0
- package/dist-cli/chunks/login-ZSIHGHUK.js +26 -0
- package/dist-cli/chunks/logout-Q4DXMI2C.js +4 -0
- package/dist-cli/chunks/{maestro-F5KK4RC4.js → maestro-LE5LCRBL.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-6SUU3Y5I.js → maestro-generate-D3ZIJCRL.js} +3 -3
- package/dist-cli/chunks/{mode-CSWGOJLI.js → mode-BWX6MDPJ.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-Y6VJ4SVV.js → optional-demo-registry-6CVASL3S.js} +2 -2
- package/dist-cli/chunks/{perf-NRWWAJL4.js → perf-NXQZISN6.js} +2 -2
- package/dist-cli/chunks/{permissions-OD62A3DJ.js → permissions-5PD67RRO.js} +2 -2
- package/dist-cli/chunks/privacy-H5KNRNIZ.js +4 -0
- package/dist-cli/chunks/{record-JZT2GM3G.js → record-YJF4KXMR.js} +3 -3
- package/dist-cli/chunks/{report-issue-ZWSNHOPB.js → report-issue-XOL5FQBG.js} +2 -2
- package/dist-cli/chunks/reset-5LZN5P5B.js +4 -0
- package/dist-cli/chunks/runtime-U36VWBH3.js +4 -0
- package/dist-cli/chunks/{screenshot-command-ZGJR7ICY.js → screenshot-command-VRPJXWFS.js} +3 -3
- package/dist-cli/chunks/{screenshot-layers-5AQKOJZK.js → screenshot-layers-QP4WA2F5.js} +2 -2
- package/dist-cli/chunks/{screenshots-capture-CPTWK5Y3.js → screenshots-capture-O2IBAVSP.js} +2 -2
- package/dist-cli/chunks/serve-WC6RZNJL.js +118 -0
- package/dist-cli/chunks/setup-A6BIG3KL.js +30 -0
- package/dist-cli/chunks/{skills-NVWRBMEO.js → skills-A7YC6RL5.js} +2 -2
- package/dist-cli/chunks/{state-QGLTKN6K.js → state-IJI5YROJ.js} +2 -2
- package/dist-cli/chunks/{storage-OZMEZ4AV.js → storage-Z2DBO2PQ.js} +4 -4
- package/dist-cli/chunks/store-BYTCKNIC.js +4 -0
- package/dist-cli/chunks/telemetry-JLHFMD7N.js +4 -0
- package/dist-cli/chunks/{timeline-EXLDREY3.js → timeline-N7V2M6QA.js} +2 -2
- package/dist-cli/chunks/{upgrade-4VWXQO6I.js → upgrade-P2NH3BOR.js} +2 -2
- package/dist-cli/chunks/upload-Z2WX5DCD.js +4 -0
- package/dist-cli/chunks/{version-WZM6XCD2.js → version-OUHCSCUS.js} +2 -2
- package/dist-cli/chunks/{web-ZYPEUVWL.js → web-6NRTF3UV.js} +2 -2
- package/dist-cli/chunks/welcome-6RS7PEJW.js +5 -0
- package/dist-cli/chunks/{what-happened-KF2HLWKW.js → what-happened-FUEL3BEF.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 +1 -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 +18 -4
- package/dist-lib/home-paths.cjs +145 -11
- package/dist-lib/host/bridge-host.cjs +236 -31
- 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 +25 -2
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +384 -385
- package/dist-lib/vite.cjs +193 -37
- package/package.json +2 -1
- package/scripts/dev-server-scanner.ts +12 -5
- package/skills/rnx-test/SKILL.md +4 -0
- package/src/dev-bundle-resolution.ts +13 -6
- 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-44LRQEU3.js +0 -4
- package/dist-cli/chunks/chunk-4RKTGL63.js +0 -9
- package/dist-cli/chunks/chunk-5SCIDXJD.js +0 -25
- package/dist-cli/chunks/chunk-CUVY4A2A.js +0 -4
- package/dist-cli/chunks/chunk-DPJGB7MP.js +0 -101
- package/dist-cli/chunks/chunk-HD6P3W5R.js +0 -4
- package/dist-cli/chunks/chunk-HFNESSHG.js +0 -15
- package/dist-cli/chunks/chunk-I7HHBFSD.js +0 -4
- package/dist-cli/chunks/chunk-LV2G34IX.js +0 -4
- package/dist-cli/chunks/cli-version-REQOM22G.js +0 -4
- package/dist-cli/chunks/config-QTFXJKD6.js +0 -13
- package/dist-cli/chunks/daemon-ETA375WE.js +0 -4
- package/dist-cli/chunks/drivers-R4YB2XAF.js +0 -4
- package/dist-cli/chunks/flow-3VQL6XZT.js +0 -4
- package/dist-cli/chunks/help-2RYCKZ2G.js +0 -4
- package/dist-cli/chunks/inspect-XDSX5D6D.js +0 -4
- package/dist-cli/chunks/install-desktop-OKVRLL35.js +0 -4
- package/dist-cli/chunks/login-4VCBPAY2.js +0 -28
- package/dist-cli/chunks/logout-WSL5JS5R.js +0 -4
- package/dist-cli/chunks/reset-3C52YQCV.js +0 -4
- package/dist-cli/chunks/runtime-4V2RBUOR.js +0 -4
- package/dist-cli/chunks/serve-ERAZQGCJ.js +0 -118
- package/dist-cli/chunks/setup-UG6PLYIL.js +0 -32
- package/dist-cli/chunks/store-VPBQKKWA.js +0 -4
- package/dist-cli/chunks/telemetry-FRW7OCYZ.js +0 -4
- package/dist-cli/chunks/upload-C6676NST.js +0 -4
package/src/home-paths.ts
CHANGED
|
@@ -233,8 +233,8 @@ export function readPrivacyPreferences(): PrivacyPreferences {
|
|
|
233
233
|
const productAnalytics = settings?.productAnalytics
|
|
234
234
|
const crashReports = settings?.crashReports
|
|
235
235
|
return {
|
|
236
|
-
productAnalytics: typeof productAnalytics === 'boolean' ? productAnalytics :
|
|
237
|
-
crashReports: typeof crashReports === 'boolean' ? crashReports :
|
|
236
|
+
productAnalytics: typeof productAnalytics === 'boolean' ? productAnalytics : false,
|
|
237
|
+
crashReports: typeof crashReports === 'boolean' ? crashReports : false,
|
|
238
238
|
configured:
|
|
239
239
|
typeof productAnalytics === 'boolean' && typeof crashReports === 'boolean',
|
|
240
240
|
}
|
|
@@ -401,6 +401,13 @@ export interface DevBridgeLockfile {
|
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
const DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024
|
|
404
|
+
const DAEMON_CLAIM_STALE_MS = 30_000
|
|
405
|
+
|
|
406
|
+
interface DaemonClaim {
|
|
407
|
+
pid: number
|
|
408
|
+
startedAt: number
|
|
409
|
+
createdAt: number
|
|
410
|
+
}
|
|
404
411
|
|
|
405
412
|
export function readDaemonLockfile(): DaemonLockfile | null {
|
|
406
413
|
try {
|
|
@@ -549,9 +556,28 @@ export function readLiveRuntimeVersions(): string[] {
|
|
|
549
556
|
|
|
550
557
|
export function writeDaemonLockfile(data: DaemonLockfile): void {
|
|
551
558
|
ensureSootsimHome()
|
|
552
|
-
const
|
|
559
|
+
const lockfilePath = daemonLockfilePath()
|
|
560
|
+
const existing = readDaemonLockfile()
|
|
561
|
+
if (!existing) {
|
|
562
|
+
if (createDaemonLockfileExclusive(data)) return
|
|
563
|
+
throw new Error('another rnx daemon claimed the lockfile before this write')
|
|
564
|
+
}
|
|
565
|
+
if (!sameDaemonOwner(existing, data)) {
|
|
566
|
+
throw new Error('refusing to overwrite another rnx daemon lockfile')
|
|
567
|
+
}
|
|
568
|
+
const tmp = `${lockfilePath}.${data.pid}.${randomUUID()}.tmp`
|
|
553
569
|
fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
554
|
-
|
|
570
|
+
try {
|
|
571
|
+
const current = readDaemonLockfile()
|
|
572
|
+
if (!current || !sameDaemonOwner(current, data)) {
|
|
573
|
+
throw new Error('rnx daemon lock ownership changed during heartbeat')
|
|
574
|
+
}
|
|
575
|
+
fs.renameSync(tmp, lockfilePath)
|
|
576
|
+
} finally {
|
|
577
|
+
try {
|
|
578
|
+
fs.unlinkSync(tmp)
|
|
579
|
+
} catch {}
|
|
580
|
+
}
|
|
555
581
|
}
|
|
556
582
|
|
|
557
583
|
export function writeDevBridgeLockfile(data: DevBridgeLockfile): void {
|
|
@@ -562,23 +588,150 @@ export function writeDevBridgeLockfile(data: DevBridgeLockfile): void {
|
|
|
562
588
|
fs.renameSync(tmp, lockfilePath)
|
|
563
589
|
}
|
|
564
590
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
591
|
+
function sameDaemonOwner(
|
|
592
|
+
left: Pick<DaemonLockfile, 'pid' | 'startedAt'>,
|
|
593
|
+
right: Pick<DaemonLockfile, 'pid' | 'startedAt'>,
|
|
594
|
+
): boolean {
|
|
595
|
+
return left.pid === right.pid && left.startedAt === right.startedAt
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function isAlreadyExists(error: unknown): boolean {
|
|
599
|
+
return (
|
|
600
|
+
error instanceof Error && 'code' in error && Reflect.get(error, 'code') === 'EEXIST'
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function createDaemonLockfileExclusive(data: DaemonLockfile): boolean {
|
|
605
|
+
const lockfilePath = daemonLockfilePath()
|
|
606
|
+
const tmp = `${lockfilePath}.${data.pid}.${randomUUID()}.claim`
|
|
607
|
+
fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
608
|
+
try {
|
|
609
|
+
fs.linkSync(tmp, lockfilePath)
|
|
610
|
+
return true
|
|
611
|
+
} catch (error) {
|
|
612
|
+
if (isAlreadyExists(error)) return false
|
|
613
|
+
throw error
|
|
614
|
+
} finally {
|
|
615
|
+
try {
|
|
616
|
+
fs.unlinkSync(tmp)
|
|
617
|
+
} catch {}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function daemonClaimPath(): string {
|
|
622
|
+
return `${daemonLockfilePath()}.claim`
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function parseDaemonClaim(raw: string): DaemonClaim | null {
|
|
626
|
+
try {
|
|
627
|
+
const value: unknown = JSON.parse(raw)
|
|
628
|
+
if (
|
|
629
|
+
value !== null &&
|
|
630
|
+
typeof value === 'object' &&
|
|
631
|
+
typeof Reflect.get(value, 'pid') === 'number' &&
|
|
632
|
+
typeof Reflect.get(value, 'startedAt') === 'number' &&
|
|
633
|
+
typeof Reflect.get(value, 'createdAt') === 'number'
|
|
634
|
+
) {
|
|
635
|
+
return {
|
|
636
|
+
pid: Reflect.get(value, 'pid'),
|
|
637
|
+
startedAt: Reflect.get(value, 'startedAt'),
|
|
638
|
+
createdAt: Reflect.get(value, 'createdAt'),
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
} catch {}
|
|
642
|
+
return null
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function readDaemonClaim(): DaemonClaim | null {
|
|
646
|
+
try {
|
|
647
|
+
return parseDaemonClaim(fs.readFileSync(daemonClaimPath(), 'utf8'))
|
|
648
|
+
} catch {
|
|
649
|
+
return null
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function isDaemonClaimFresh(claim: DaemonClaim, now = Date.now()): boolean {
|
|
654
|
+
if (now - claim.createdAt > DAEMON_CLAIM_STALE_MS) return false
|
|
655
|
+
try {
|
|
656
|
+
process.kill(claim.pid, 0)
|
|
657
|
+
return true
|
|
658
|
+
} catch {
|
|
659
|
+
return false
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function acquireDaemonClaim(data: DaemonLockfile): boolean {
|
|
664
|
+
const claim: DaemonClaim = {
|
|
665
|
+
pid: data.pid,
|
|
666
|
+
startedAt: data.startedAt,
|
|
667
|
+
createdAt: Date.now(),
|
|
668
|
+
}
|
|
669
|
+
const serialized = `${JSON.stringify(claim)}\n`
|
|
670
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
671
|
+
try {
|
|
672
|
+
fs.writeFileSync(daemonClaimPath(), serialized, { encoding: 'utf8', flag: 'wx' })
|
|
673
|
+
return true
|
|
674
|
+
} catch (error) {
|
|
675
|
+
if (!isAlreadyExists(error)) throw error
|
|
676
|
+
}
|
|
677
|
+
const existing = readDaemonClaim()
|
|
678
|
+
if (existing && sameDaemonOwner(existing, claim)) return true
|
|
679
|
+
if (existing && isDaemonClaimFresh(existing)) return false
|
|
680
|
+
let before: string
|
|
681
|
+
try {
|
|
682
|
+
before = fs.readFileSync(daemonClaimPath(), 'utf8')
|
|
683
|
+
const latest = parseDaemonClaim(before)
|
|
684
|
+
if (latest && isDaemonClaimFresh(latest)) return false
|
|
685
|
+
if (fs.readFileSync(daemonClaimPath(), 'utf8') !== before) continue
|
|
686
|
+
fs.unlinkSync(daemonClaimPath())
|
|
687
|
+
} catch {}
|
|
688
|
+
}
|
|
689
|
+
return false
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function releaseDaemonClaim(owner: Pick<DaemonLockfile, 'pid' | 'startedAt'>): void {
|
|
693
|
+
try {
|
|
694
|
+
const claim = readDaemonClaim()
|
|
695
|
+
if (!claim || !sameDaemonOwner(claim, owner)) return
|
|
696
|
+
fs.unlinkSync(daemonClaimPath())
|
|
697
|
+
} catch {}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** try to claim the lockfile atomically on daemon boot. a short-lived,
|
|
701
|
+
* process-owned election file serializes concurrent fallback-port binds;
|
|
702
|
+
* the complete lock snapshot is then installed with an exclusive hard link. */
|
|
570
703
|
export function claimDaemonLockfile(data: DaemonLockfile): boolean {
|
|
571
704
|
ensureSootsimHome()
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
705
|
+
if (!acquireDaemonClaim(data)) return false
|
|
706
|
+
try {
|
|
707
|
+
const existing = readDaemonLockfile()
|
|
708
|
+
if (existing && sameDaemonOwner(existing, data)) {
|
|
709
|
+
writeDaemonLockfile(data)
|
|
710
|
+
return true
|
|
711
|
+
}
|
|
712
|
+
if (existing && isDaemonLockfileFresh(existing)) return false
|
|
713
|
+
if (existing) {
|
|
714
|
+
const latest = readDaemonLockfile()
|
|
715
|
+
if (!latest || !sameDaemonOwner(latest, existing)) return false
|
|
716
|
+
if (isDaemonLockfileFresh(latest)) return false
|
|
717
|
+
try {
|
|
718
|
+
fs.unlinkSync(daemonLockfilePath())
|
|
719
|
+
} catch {
|
|
720
|
+
return false
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return createDaemonLockfileExclusive(data)
|
|
724
|
+
} finally {
|
|
725
|
+
releaseDaemonClaim(data)
|
|
575
726
|
}
|
|
576
|
-
writeDaemonLockfile(data)
|
|
577
|
-
return true
|
|
578
727
|
}
|
|
579
728
|
|
|
580
|
-
export function removeDaemonLockfile(
|
|
729
|
+
export function removeDaemonLockfile(
|
|
730
|
+
owner: Pick<DaemonLockfile, 'pid' | 'startedAt'>,
|
|
731
|
+
): void {
|
|
581
732
|
try {
|
|
733
|
+
const lockfile = readDaemonLockfile()
|
|
734
|
+
if (!lockfile || !sameDaemonOwner(lockfile, owner)) return
|
|
582
735
|
fs.unlinkSync(daemonLockfilePath())
|
|
583
736
|
} catch {}
|
|
584
737
|
}
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -1525,7 +1525,7 @@ export class SootSimBridgeHost {
|
|
|
1525
1525
|
removeLockfile() {
|
|
1526
1526
|
if (!this.shouldWriteLockfile) return
|
|
1527
1527
|
try {
|
|
1528
|
-
removeDaemonLockfile()
|
|
1528
|
+
removeDaemonLockfile({ pid: process.pid, startedAt: this.startedAt })
|
|
1529
1529
|
} catch {}
|
|
1530
1530
|
}
|
|
1531
1531
|
|
|
@@ -2229,7 +2229,7 @@ export class SootSimBridgeHost {
|
|
|
2229
2229
|
}
|
|
2230
2230
|
if (this.shouldWriteLockfile) {
|
|
2231
2231
|
try {
|
|
2232
|
-
removeDaemonLockfile()
|
|
2232
|
+
removeDaemonLockfile({ pid: process.pid, startedAt: this.startedAt })
|
|
2233
2233
|
} catch {}
|
|
2234
2234
|
}
|
|
2235
2235
|
if (this.shouldWriteDevLockfile) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { build } from 'esbuild'
|
|
4
|
+
import { isLoopbackHost } from '../backend-origin.ts'
|
|
4
5
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
5
6
|
|
|
6
7
|
export const REPLACEMENT_MODULE_ROUTE = '/__sootsim-replacement-module'
|
|
@@ -8,6 +9,9 @@ export const REPLACEMENT_RUNTIME_PREFIX = '/__sootsim-replacement-runtime'
|
|
|
8
9
|
|
|
9
10
|
export interface ReplacementModuleHandlerOptions {
|
|
10
11
|
reactBridgeUrl?: string
|
|
12
|
+
projectRoot?: string
|
|
13
|
+
requireLoopbackOrigin?: boolean
|
|
14
|
+
requestToken?: string
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
function getReplacementLoader(filePath: string): 'js' | 'jsx' | 'ts' | 'tsx' | null {
|
|
@@ -113,9 +117,19 @@ export const Appearance = rn.Appearance
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
function sendJavaScript(req: IncomingMessage, res: ServerResponse, source: string): void {
|
|
120
|
+
const origin = req.headers.origin
|
|
121
|
+
let allowedOrigin: string | undefined
|
|
122
|
+
if (origin) {
|
|
123
|
+
try {
|
|
124
|
+
if (isLoopbackHost(new URL(origin).hostname)) allowedOrigin = origin
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
116
127
|
res.writeHead(200, {
|
|
117
128
|
'Content-Type': 'application/javascript; charset=utf-8',
|
|
118
129
|
'Cache-Control': 'no-cache',
|
|
130
|
+
...(allowedOrigin
|
|
131
|
+
? { 'Access-Control-Allow-Origin': allowedOrigin, Vary: 'Origin' }
|
|
132
|
+
: {}),
|
|
119
133
|
})
|
|
120
134
|
res.end(req.method === 'HEAD' ? undefined : source)
|
|
121
135
|
}
|
|
@@ -146,6 +160,24 @@ export async function handleReplacementModuleRequest(
|
|
|
146
160
|
return false
|
|
147
161
|
}
|
|
148
162
|
|
|
163
|
+
if (options.requireLoopbackOrigin) {
|
|
164
|
+
const origin = req.headers.origin
|
|
165
|
+
const remoteAddress = req.socket.remoteAddress?.replace(/^::ffff:/, '')
|
|
166
|
+
let allowed = Boolean(remoteAddress && isLoopbackHost(remoteAddress))
|
|
167
|
+
if (allowed && origin) {
|
|
168
|
+
try {
|
|
169
|
+
allowed = isLoopbackHost(new URL(origin).hostname)
|
|
170
|
+
} catch {
|
|
171
|
+
allowed = false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!allowed) {
|
|
175
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
176
|
+
res.end('replacement modules are available only to loopback origins')
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
149
181
|
if (url.pathname === `${REPLACEMENT_RUNTIME_PREFIX}/react`) {
|
|
150
182
|
sendJavaScript(req, res, reactRuntimeSource(options.reactBridgeUrl))
|
|
151
183
|
return true
|
|
@@ -156,6 +188,12 @@ export async function handleReplacementModuleRequest(
|
|
|
156
188
|
}
|
|
157
189
|
if (url.pathname !== REPLACEMENT_MODULE_ROUTE) return false
|
|
158
190
|
|
|
191
|
+
if (options.requestToken && url.searchParams.get('token') !== options.requestToken) {
|
|
192
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
193
|
+
res.end('replacement module request token is invalid')
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
|
|
159
197
|
const targetPath = url.searchParams.get('path')
|
|
160
198
|
if (!targetPath) {
|
|
161
199
|
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
@@ -170,8 +208,27 @@ export async function handleReplacementModuleRequest(
|
|
|
170
208
|
return true
|
|
171
209
|
}
|
|
172
210
|
|
|
211
|
+
if (!path.isAbsolute(targetPath)) {
|
|
212
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
213
|
+
res.end('replacement file path must be absolute')
|
|
214
|
+
return true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let resolvedProjectRoot: string | undefined
|
|
218
|
+
if (options.projectRoot) {
|
|
219
|
+
const lexicalProjectRoot = path.resolve(options.projectRoot)
|
|
220
|
+
resolvedProjectRoot = await fs.realpath(options.projectRoot)
|
|
221
|
+
const lexicalTarget = path.relative(lexicalProjectRoot, targetPath)
|
|
222
|
+
if (lexicalTarget.startsWith('..') || path.isAbsolute(lexicalTarget)) {
|
|
223
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
224
|
+
res.end('replacement file must be inside the project root')
|
|
225
|
+
return true
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let resolvedTargetPath: string
|
|
173
230
|
try {
|
|
174
|
-
await fs.
|
|
231
|
+
resolvedTargetPath = await fs.realpath(targetPath)
|
|
175
232
|
} catch (error) {
|
|
176
233
|
const message = error instanceof Error ? error.message : String(error)
|
|
177
234
|
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
@@ -179,9 +236,18 @@ export async function handleReplacementModuleRequest(
|
|
|
179
236
|
return true
|
|
180
237
|
}
|
|
181
238
|
|
|
239
|
+
if (resolvedProjectRoot) {
|
|
240
|
+
const relativeTarget = path.relative(resolvedProjectRoot, resolvedTargetPath)
|
|
241
|
+
if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) {
|
|
242
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
243
|
+
res.end('replacement file must be inside the project root')
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
182
248
|
try {
|
|
183
249
|
const result = await build({
|
|
184
|
-
entryPoints: [
|
|
250
|
+
entryPoints: [resolvedTargetPath],
|
|
185
251
|
bundle: true,
|
|
186
252
|
write: false,
|
|
187
253
|
format: 'esm',
|
package/src/metro-plugin.ts
CHANGED
|
@@ -10,7 +10,19 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from 'fs'
|
|
12
12
|
import { createRequire } from 'module'
|
|
13
|
+
import { randomUUID } from 'node:crypto'
|
|
13
14
|
import path from 'path'
|
|
15
|
+
import {
|
|
16
|
+
openRnxDesktopWhenMetroIsReady,
|
|
17
|
+
resolveRnxDesktopAppName,
|
|
18
|
+
resolveRnxDesktopLaunchTarget,
|
|
19
|
+
tagRnxBundleForPlugin,
|
|
20
|
+
type RnxDesktopOpenOption,
|
|
21
|
+
} from './dev-server-open'
|
|
22
|
+
import {
|
|
23
|
+
handleReplacementModuleRequest,
|
|
24
|
+
isReplacementModuleRequestUrl,
|
|
25
|
+
} from './host/replacement-module-handler'
|
|
14
26
|
import {
|
|
15
27
|
RNXSIM_METRO_PRODUCTION_BUNDLE_OPTION,
|
|
16
28
|
createRNXMetroModulePathsFooter,
|
|
@@ -79,6 +91,7 @@ type MetroSerializer = (
|
|
|
79
91
|
export interface RNXMetroOptions {
|
|
80
92
|
bundleUrl?: string
|
|
81
93
|
devServer?: boolean
|
|
94
|
+
open?: RnxDesktopOpenOption
|
|
82
95
|
productionBundles?: boolean
|
|
83
96
|
}
|
|
84
97
|
|
|
@@ -97,6 +110,7 @@ type MetroConfigWithServer = {
|
|
|
97
110
|
}
|
|
98
111
|
server?: {
|
|
99
112
|
enhanceMiddleware?: (middleware: unknown, server: unknown) => unknown
|
|
113
|
+
port?: number
|
|
100
114
|
[key: string]: unknown
|
|
101
115
|
}
|
|
102
116
|
}
|
|
@@ -210,15 +224,25 @@ export function withRNX<T extends Record<string, unknown>>(
|
|
|
210
224
|
config: T,
|
|
211
225
|
options: RNXMetroOptions = {},
|
|
212
226
|
): T {
|
|
213
|
-
if (!options.devServer && !options.productionBundles) return config
|
|
227
|
+
if (!options.devServer && !options.open && !options.productionBundles) return config
|
|
214
228
|
|
|
215
|
-
const
|
|
229
|
+
const cfgProjectRoot = (config as MetroConfigWithServer).projectRoot || process.cwd()
|
|
230
|
+
const appName = resolveRnxDesktopAppName(options.open, cfgProjectRoot)
|
|
231
|
+
const configuredBundleUrl =
|
|
216
232
|
options.bundleUrl || '/index.bundle?platform=ios&dev=true&hot=true&minify=false'
|
|
217
233
|
|
|
218
234
|
const cfgServer = (config as MetroConfigWithServer).server
|
|
235
|
+
const port = resolveMetroPort(cfgServer)
|
|
236
|
+
const metroServerId = randomUUID()
|
|
237
|
+
const bundleUrl = tagRnxBundleForPlugin(
|
|
238
|
+
configuredBundleUrl,
|
|
239
|
+
`http://localhost:${port}`,
|
|
240
|
+
metroServerId,
|
|
241
|
+
cfgProjectRoot,
|
|
242
|
+
appName,
|
|
243
|
+
)
|
|
219
244
|
const cfgResolver = (config as MetroConfigWithServer).resolver
|
|
220
245
|
const cfgSerializer = (config as MetroConfigWithServer).serializer
|
|
221
|
-
const cfgProjectRoot = (config as MetroConfigWithServer).projectRoot || process.cwd()
|
|
222
246
|
const existingEnhance = cfgServer?.enhanceMiddleware
|
|
223
247
|
const existingResolveRequest = cfgResolver?.resolveRequest
|
|
224
248
|
const existingSerializer = cfgSerializer?.customSerializer
|
|
@@ -299,9 +323,40 @@ export function withRNX<T extends Record<string, unknown>>(
|
|
|
299
323
|
const server = connect()
|
|
300
324
|
|
|
301
325
|
server.use((req: any, res: any, next: any) => {
|
|
326
|
+
if (isReplacementModuleRequestUrl(req.url)) {
|
|
327
|
+
void handleReplacementModuleRequest(req, res, {
|
|
328
|
+
projectRoot: cfgProjectRoot,
|
|
329
|
+
requestToken: metroServerId,
|
|
330
|
+
requireLoopbackOrigin: true,
|
|
331
|
+
}).then((handled) => {
|
|
332
|
+
if (!handled) next()
|
|
333
|
+
})
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
302
337
|
const url = req.url || ''
|
|
303
338
|
const pathname = url.split('?')[0]
|
|
304
339
|
|
|
340
|
+
if (pathname === '/__server-scan') {
|
|
341
|
+
const absoluteBundleUrl = bundleUrl.startsWith('http')
|
|
342
|
+
? bundleUrl
|
|
343
|
+
: `http://localhost:${port}${bundleUrl}`
|
|
344
|
+
res.setHeader('content-type', 'application/json')
|
|
345
|
+
res.end(
|
|
346
|
+
JSON.stringify([
|
|
347
|
+
{
|
|
348
|
+
port,
|
|
349
|
+
framework: 'metro',
|
|
350
|
+
rnxServerId: metroServerId,
|
|
351
|
+
bundleUrl: absoluteBundleUrl,
|
|
352
|
+
...(appName ? { projectName: appName } : {}),
|
|
353
|
+
lastSeen: Date.now(),
|
|
354
|
+
},
|
|
355
|
+
]),
|
|
356
|
+
)
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
305
360
|
const runtimeRoot = resolveRuntimeRootForRequestHost(req.headers?.host)
|
|
306
361
|
|
|
307
362
|
if (!runtimeRoot) {
|
|
@@ -350,7 +405,7 @@ export function withRNX<T extends Record<string, unknown>>(
|
|
|
350
405
|
let html = fs.readFileSync(htmlPath, 'utf8')
|
|
351
406
|
html = html.replace(
|
|
352
407
|
'</head>',
|
|
353
|
-
`<script>history.replaceState(null,'','${prefix}
|
|
408
|
+
`<script>(function(){var u=new URL(location.href);if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleUrl)});history.replaceState(null,'','${prefix}/'+u.search)})()</script></head>`,
|
|
354
409
|
)
|
|
355
410
|
res.setHeader('content-type', 'text/html')
|
|
356
411
|
res.end(html)
|
|
@@ -368,11 +423,34 @@ export function withRNX<T extends Record<string, unknown>>(
|
|
|
368
423
|
},
|
|
369
424
|
}
|
|
370
425
|
|
|
426
|
+
if (appName && !process.env.CI && !process.env.SOOTSIM_NO_OPEN) {
|
|
427
|
+
void openRnxDesktopWhenMetroIsReady({
|
|
428
|
+
port,
|
|
429
|
+
bundleUrl: resolveRnxDesktopLaunchTarget(port, bundleUrl),
|
|
430
|
+
appName,
|
|
431
|
+
projectRoot: cfgProjectRoot,
|
|
432
|
+
serverId: metroServerId,
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
|
|
371
436
|
return {
|
|
372
437
|
...config,
|
|
373
438
|
...productionConfig,
|
|
374
|
-
...(options.devServer ? { server: serverConfig } : {}),
|
|
439
|
+
...(options.devServer || options.open ? { server: serverConfig } : {}),
|
|
375
440
|
} as T
|
|
376
441
|
}
|
|
377
442
|
|
|
443
|
+
function resolveMetroPort(server: MetroConfigWithServer['server']): number {
|
|
444
|
+
const inlinePort = process.argv.find((arg) => arg.startsWith('--port='))?.slice(7)
|
|
445
|
+
const portIndex = process.argv.indexOf('--port')
|
|
446
|
+
const splitPort = portIndex >= 0 ? process.argv[portIndex + 1] : undefined
|
|
447
|
+
const rawPort =
|
|
448
|
+
inlinePort || splitPort || process.env.RCT_METRO_PORT || server?.port || 8081
|
|
449
|
+
const port = Number(rawPort)
|
|
450
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
451
|
+
throw new Error(`rnx: Metro port must be a positive integer, got ${rawPort}`)
|
|
452
|
+
}
|
|
453
|
+
return port
|
|
454
|
+
}
|
|
455
|
+
|
|
378
456
|
export default withRNX
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isLoopbackHost } from './backend-origin'
|
|
2
|
+
|
|
1
3
|
// canonical entry path for a plain Metro packager that serves no Expo
|
|
2
4
|
// manifest. only used when `/status` confirms `packager-status:running` and
|
|
3
5
|
// the server reports nothing else — never as a guess for a manifest-capable
|
|
@@ -13,6 +15,26 @@ function isNativeDevBundlePath(pathname: string): boolean {
|
|
|
13
15
|
return pathname.endsWith('.bundle')
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
// expo can advertise an IPv4 loopback launch URL even when its --localhost
|
|
19
|
+
// listener resolved to IPv6 only. keep the manifest's path and query, but bind
|
|
20
|
+
// its loopback host to the base URL that answered the discovery request.
|
|
21
|
+
export function alignLoopbackBundleUrlWithProbe(
|
|
22
|
+
bundleUrl: string,
|
|
23
|
+
probeBaseUrl: string,
|
|
24
|
+
): string {
|
|
25
|
+
try {
|
|
26
|
+
const bundle = new URL(bundleUrl)
|
|
27
|
+
const probe = new URL(probeBaseUrl)
|
|
28
|
+
if (!isLoopbackHost(bundle.hostname) || !isLoopbackHost(probe.hostname)) {
|
|
29
|
+
return bundleUrl
|
|
30
|
+
}
|
|
31
|
+
bundle.hostname = probe.hostname
|
|
32
|
+
return bundle.toString()
|
|
33
|
+
} catch {
|
|
34
|
+
return bundleUrl
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
// opt-in (contrastbot preview-recording context only): serve a production
|
|
17
39
|
// minified bundle instead of the default dev=true unminified one. a
|
|
18
40
|
// preview run just records the app — it never needs fast-refresh or dev
|
package/src/vite-plugin-one.ts
CHANGED
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
// import { rnxPlugin } from 'rnxsim/vite'
|
|
7
7
|
// export default { plugins: [one(), rnxPlugin()] }
|
|
8
8
|
|
|
9
|
-
import { spawn } from 'child_process'
|
|
10
9
|
import fs from 'fs'
|
|
10
|
+
import { randomUUID } from 'node:crypto'
|
|
11
11
|
import path from 'path'
|
|
12
|
-
import { fileURLToPath } from 'url'
|
|
13
12
|
import { DEFAULT_SOOTSIM_BRIDGE_PORT } from './bridge-constants'
|
|
13
|
+
import {
|
|
14
|
+
launchRnxDesktop,
|
|
15
|
+
resolveRnxDesktopAppName,
|
|
16
|
+
resolveRnxDesktopLaunchTarget,
|
|
17
|
+
tagRnxBundleForPlugin,
|
|
18
|
+
type RnxDesktopOpenOption,
|
|
19
|
+
} from './dev-server-open'
|
|
14
20
|
import { isDaemonLockfileFresh, readDaemonLockfile } from './home-paths'
|
|
15
21
|
import { handleReplacementModuleRequest } from './host/replacement-module-handler'
|
|
16
22
|
import {
|
|
@@ -31,7 +37,7 @@ export interface RnxPluginOptions {
|
|
|
31
37
|
enabled?: boolean
|
|
32
38
|
// launch the sootsim simulator loading this server's bundle once the dev
|
|
33
39
|
// server is listening. skipped when CI or SOOTSIM_NO_OPEN is set.
|
|
34
|
-
open?:
|
|
40
|
+
open?: RnxDesktopOpenOption
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
|
|
@@ -45,6 +51,21 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
|
|
|
45
51
|
const bundleUrl =
|
|
46
52
|
options.bundleUrl ||
|
|
47
53
|
'/node_modules/one/metro-entry.bundle?platform=ios&dev=true&minify=false'
|
|
54
|
+
const appName = resolveRnxDesktopAppName(options.open, server.config.root)
|
|
55
|
+
const replacementToken = randomUUID()
|
|
56
|
+
const configuredPort = server.config.server.port || 8081
|
|
57
|
+
const listeningPort = (): number => {
|
|
58
|
+
const address = server.httpServer?.address()
|
|
59
|
+
return address && typeof address === 'object' ? address.port : configuredPort
|
|
60
|
+
}
|
|
61
|
+
const bundleForPort = (port: number): string =>
|
|
62
|
+
tagRnxBundleForPlugin(
|
|
63
|
+
bundleUrl,
|
|
64
|
+
`http://localhost:${port}`,
|
|
65
|
+
replacementToken,
|
|
66
|
+
server.config.root,
|
|
67
|
+
appName,
|
|
68
|
+
)
|
|
48
69
|
|
|
49
70
|
// build the shell HTML, injecting the bundle URL (so main.tsx loads this
|
|
50
71
|
// app's bundle) AND window.__sootsimBridgePort. the bridge-port injection
|
|
@@ -70,7 +91,7 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
|
|
|
70
91
|
// `open` timed out after 60s.
|
|
71
92
|
const inject =
|
|
72
93
|
`<script>(function(){var u=new URL(location.href);` +
|
|
73
|
-
`if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(
|
|
94
|
+
`if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleForPort(listeningPort()))});` +
|
|
74
95
|
`window.__sootsimBridgePort=${bridgePort};` +
|
|
75
96
|
`history.replaceState(null,'','${prefix}/'+u.search)})()</script>`
|
|
76
97
|
return html.replace('</head>', inject + '</head>')
|
|
@@ -107,10 +128,11 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
|
|
|
107
128
|
// internals. shape matches DiscoveredServer enough for the resolver.
|
|
108
129
|
server.middlewares.use((req, res, next) => {
|
|
109
130
|
if ((req.url || '').split('?')[0] !== '/__server-scan') return next()
|
|
110
|
-
const scanPort =
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
131
|
+
const scanPort = listeningPort()
|
|
132
|
+
const pluginBundleUrl = bundleForPort(scanPort)
|
|
133
|
+
const absoluteBundleUrl = pluginBundleUrl.startsWith('http')
|
|
134
|
+
? pluginBundleUrl
|
|
135
|
+
: `http://localhost:${scanPort}${pluginBundleUrl}`
|
|
114
136
|
res.setHeader('content-type', 'application/json')
|
|
115
137
|
res.end(
|
|
116
138
|
JSON.stringify([
|
|
@@ -118,6 +140,7 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
|
|
|
118
140
|
port: scanPort,
|
|
119
141
|
framework: 'one',
|
|
120
142
|
bundleUrl: absoluteBundleUrl,
|
|
143
|
+
...(appName ? { projectName: appName } : {}),
|
|
121
144
|
lastSeen: Date.now(),
|
|
122
145
|
},
|
|
123
146
|
]),
|
|
@@ -125,7 +148,11 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
|
|
|
125
148
|
})
|
|
126
149
|
|
|
127
150
|
server.middlewares.use((req, res, next) => {
|
|
128
|
-
void handleReplacementModuleRequest(req, res
|
|
151
|
+
void handleReplacementModuleRequest(req, res, {
|
|
152
|
+
projectRoot: server.config.root,
|
|
153
|
+
requestToken: replacementToken,
|
|
154
|
+
requireLoopbackOrigin: true,
|
|
155
|
+
}).then((handled) => {
|
|
129
156
|
if (!handled) next()
|
|
130
157
|
})
|
|
131
158
|
})
|
|
@@ -189,30 +216,17 @@ export function rnxPlugin(options: RnxPluginOptions = {}): Plugin {
|
|
|
189
216
|
next()
|
|
190
217
|
})
|
|
191
218
|
|
|
192
|
-
const port =
|
|
219
|
+
const port = configuredPort
|
|
193
220
|
const sootsimUrl = `http://localhost:${port}${prefix}/`
|
|
194
221
|
console.log(`[rnx] serving at ${sootsimUrl}`)
|
|
195
222
|
|
|
196
|
-
if (
|
|
223
|
+
if (appName && !process.env.CI && !process.env.SOOTSIM_NO_OPEN) {
|
|
197
224
|
server.httpServer?.once('listening', () => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
203
|
-
'..',
|
|
225
|
+
const openPort = listeningPort()
|
|
226
|
+
launchRnxDesktop(
|
|
227
|
+
resolveRnxDesktopLaunchTarget(openPort, bundleForPort(openPort)),
|
|
228
|
+
appName,
|
|
204
229
|
)
|
|
205
|
-
const binPath = path.join(packageRoot, 'dist-cli', 'bin.js')
|
|
206
|
-
const openPort = server.config.server.port || port
|
|
207
|
-
console.log(`[rnx] opening simulator for :${openPort}`)
|
|
208
|
-
// invoke the cli script through the current node binary rather
|
|
209
|
-
// than the bin shim so the same spawn works on windows, macos,
|
|
210
|
-
// and linux without a shell.
|
|
211
|
-
spawn(process.execPath, [binPath, 'open', String(openPort)], {
|
|
212
|
-
detached: true,
|
|
213
|
-
stdio: 'ignore',
|
|
214
|
-
windowsHide: true,
|
|
215
|
-
}).unref()
|
|
216
230
|
})
|
|
217
231
|
}
|
|
218
232
|
},
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/*! rnx v0.1.343 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
|
-
import { createRequire as __sootsimCreateRequire } from 'node:module'
|
|
3
|
-
var require = __sootsimCreateRequire(import.meta.url)
|
|
4
|
-
import{a as c}from"./chunk-W7EXE22C.js";import{J as u,l}from"./chunk-4RKTGL63.js";function m(t){return t.replace(/^\[|\]$/g,"").toLowerCase()}function R(t){let n=m(t);return n==="localhost"||n.endsWith(".localhost")||n==="0.0.0.0"||n==="::1"||/^127(?:\.\d{1,3}){3}$/.test(n)}import o from"fs";import e from"path";import{fileURLToPath as d}from"url";var s;function g(){if(s!==void 0)return s;let t=e.dirname(d(import.meta.url));for(let n=0;n<12&&t!==e.dirname(t);n++){if(o.existsSync(e.join(t,"packages/sootsim-engine/package.json"))&&o.existsSync(e.join(t,"public/sootsim")))return s=t,t;t=e.dirname(t)}return s=null,null}function p(){if(process.env.SOOTSIM_HOME&&u())return null;let t=g();if(!t)return null;let n=e.join(t,"public/sootsim");return o.existsSync(e.join(n,"index.html"))&&o.existsSync(e.join(n,"engine/index.html"))&&o.existsSync(e.join(n,"engine-tenant/worker.js"))?n:null}function x(){let t=p();if(t)return t;let n=u();if(!n)return null;let i=l(n);return o.existsSync(e.join(i,"index.html"))?i:null}var a=".runtime.localhost";function H(t){let n=t.trim();if(!c(n))throw new Error(`invalid rnx runtime version: ${t}`);let r=Buffer.from(n,"utf8").toString("hex").match(/.{1,60}/g);if(!r)throw new Error(`invalid rnx runtime version: ${t}`);return`${r.join(".")}${a}`}function f(t){return t.toLowerCase().endsWith(a)}function h(t){if(!f(t))return null;let n=t.slice(0,-a.length).split(".");if(n.length===0||n.some(r=>!/^[0-9a-f]{1,60}$/i.test(r)))return null;let i=n.join("");if(i.length%2!==0)return null;try{let r=Buffer.from(i,"hex").toString("utf8");return c(r)?r:null}catch{return null}}function v(t){let n=l(t);return o.existsSync(e.join(n,"index.html"))?n:null}function T(t){if(!f(t))return x();let n=h(t);return n?v(n):null}export{R as a,p as b,H as c,f as d,h as e,T as f};
|