rnxsim 0.0.0 → 0.1.313

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +512 -0
  3. package/detox/colors.ts +54 -0
  4. package/detox/config-loader.ts +135 -0
  5. package/detox/element-types.ts +39 -0
  6. package/detox/expectations.ts +537 -0
  7. package/detox/gestures.ts +480 -0
  8. package/detox/index.ts +2118 -0
  9. package/detox/jest-preset.cjs +51 -0
  10. package/detox/jest-setup-after-env.cjs +18 -0
  11. package/detox/matchers.ts +30 -0
  12. package/detox/motion-change.cjs +109 -0
  13. package/detox/motion-change.d.cts +35 -0
  14. package/detox/navigation.ts +43 -0
  15. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  17. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  18. package/detox/screenshots/theme-blue-toggle.png +0 -0
  19. package/detox/screenshots/theme-blue.png +0 -0
  20. package/detox/screenshots/theme-red-toggle.png +0 -0
  21. package/detox/screenshots/theme-red.png +0 -0
  22. package/dist-cli/bin.js +15 -0
  23. package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
  24. package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
  25. package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
  26. package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
  27. package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
  28. package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
  29. package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
  30. package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
  31. package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
  32. package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
  33. package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
  34. package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
  35. package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
  36. package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
  37. package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
  38. package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
  39. package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
  40. package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
  41. package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
  42. package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
  43. package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
  44. package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
  45. package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
  46. package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
  47. package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
  48. package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
  49. package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
  50. package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
  51. package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
  52. package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
  53. package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
  54. package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
  55. package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
  56. package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
  57. package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
  58. package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
  59. package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
  60. package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
  61. package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
  62. package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
  63. package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
  64. package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
  65. package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
  66. package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
  67. package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
  68. package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
  69. package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
  70. package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
  71. package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
  72. package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
  73. package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
  74. package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
  75. package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
  76. package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
  77. package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
  78. package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
  79. package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
  80. package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
  81. package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
  82. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
  83. package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
  84. package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
  85. package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
  86. package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
  87. package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
  88. package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
  89. package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
  90. package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
  91. package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
  92. package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
  93. package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
  94. package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
  95. package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
  96. package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
  97. package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
  98. package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
  99. package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
  100. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
  101. package/dist-cli/chunks/login-FJ737MWG.js +28 -0
  102. package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
  103. package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
  104. package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
  105. package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
  106. package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
  107. package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
  108. package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
  109. package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
  110. package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
  111. package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
  112. package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
  113. package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
  114. package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
  115. package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
  116. package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
  117. package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
  118. package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
  119. package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
  120. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
  121. package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
  122. package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
  123. package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
  124. package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
  125. package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
  126. package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
  127. package/dist-lib/agent-daemon-client.cjs +468 -0
  128. package/dist-lib/agent-events.cjs +48 -0
  129. package/dist-lib/agent-identity.cjs +53 -0
  130. package/dist-lib/agent-sessions.cjs +725 -0
  131. package/dist-lib/attached-projects.cjs +526 -0
  132. package/dist-lib/auth/shared-session.cjs +195 -0
  133. package/dist-lib/backend-origin.cjs +70 -0
  134. package/dist-lib/beta.cjs +41 -0
  135. package/dist-lib/beta.mjs +13 -0
  136. package/dist-lib/bridge-constants.cjs +161 -0
  137. package/dist-lib/cli-constants.cjs +32 -0
  138. package/dist-lib/config.cjs +96 -0
  139. package/dist-lib/detox/index.cjs +2863 -0
  140. package/dist-lib/detox/jest-preset.cjs +51 -0
  141. package/dist-lib/dev-bundle-resolution.cjs +223 -0
  142. package/dist-lib/home-paths.cjs +421 -0
  143. package/dist-lib/host/bridge-host.cjs +5600 -0
  144. package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
  145. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  146. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  147. package/dist-lib/host/websocket-proxy.cjs +214 -0
  148. package/dist-lib/index.cjs +513 -0
  149. package/dist-lib/jump-to-source-babel.cjs +93 -0
  150. package/dist-lib/menu.cjs +620 -0
  151. package/dist-lib/menu.mjs +581 -0
  152. package/dist-lib/metro.cjs +297 -0
  153. package/dist-lib/profiles.cjs +246 -0
  154. package/dist-lib/public-brand.cjs +39 -0
  155. package/dist-lib/render-mode.cjs +54 -0
  156. package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
  157. package/dist-lib/sdk.cjs +2333 -0
  158. package/dist-lib/sdk.mjs +2240 -0
  159. package/dist-lib/skills.cjs +23922 -0
  160. package/dist-lib/vite.cjs +358 -0
  161. package/package.json +189 -18
  162. package/scripts/dev-server-scanner.ts +941 -0
  163. package/scripts/optional-demo-registry.ts +113 -0
  164. package/scripts/postinstall.cjs +3 -0
  165. package/scripts/preuninstall.cjs +118 -0
  166. package/skills/contrast/SKILL.md +171 -0
  167. package/skills/rnx-debug/SKILL.md +425 -0
  168. package/skills/rnx-perf/SKILL.md +295 -0
  169. package/skills/rnx-setup/SKILL.md +160 -0
  170. package/skills/rnx-test/SKILL.md +292 -0
  171. package/skills/rnx-visual/SKILL.md +122 -0
  172. package/src/agent-daemon-client.ts +410 -0
  173. package/src/agent-events.ts +71 -0
  174. package/src/agent-identity.ts +23 -0
  175. package/src/agent-prompt.ts +71 -0
  176. package/src/agent-sessions.ts +573 -0
  177. package/src/attached-projects.ts +539 -0
  178. package/src/auth/shared-session.ts +204 -0
  179. package/src/backend-origin.ts +49 -0
  180. package/src/beta.ts +19 -0
  181. package/src/bridge-constants.ts +171 -0
  182. package/src/browser-cache.ts +16 -0
  183. package/src/cli-constants.ts +1 -0
  184. package/src/cli-version.ts +32 -0
  185. package/src/codex-client.ts +215 -0
  186. package/src/config.ts +134 -0
  187. package/src/dev-bundle-resolution.ts +236 -0
  188. package/src/disk-cleanup.ts +466 -0
  189. package/src/home-paths.ts +552 -0
  190. package/src/host/agent-host.ts +666 -0
  191. package/src/host/bridge-host.ts +2626 -0
  192. package/src/host/fetch-proxy-handler.ts +397 -0
  193. package/src/host/fetch-proxy-overrides.ts +51 -0
  194. package/src/host/open-url.ts +89 -0
  195. package/src/host/websocket-proxy.ts +210 -0
  196. package/src/index.ts +12 -705
  197. package/src/jump-to-source-babel.ts +135 -0
  198. package/src/menu.ts +734 -0
  199. package/src/metro-plugin.ts +139 -0
  200. package/src/native-dev-bundle-url.ts +96 -0
  201. package/src/native-seam-manifest.ts +341 -0
  202. package/src/plist.ts +27 -0
  203. package/src/poll-until.ts +92 -0
  204. package/src/profiles.ts +179 -0
  205. package/src/public-brand.ts +9 -0
  206. package/src/render-mode.ts +27 -0
  207. package/src/runtime-assets.ts +228 -0
  208. package/src/runtime-delivery.ts +54 -0
  209. package/src/screenshots/compose.ts +422 -0
  210. package/src/screenshots/frame-compose.ts +454 -0
  211. package/src/screenshots/orchestrate.ts +256 -0
  212. package/src/screenshots/org-deck.ts +91 -0
  213. package/src/screenshots/registry.ts +58 -0
  214. package/src/screenshots/schema.ts +364 -0
  215. package/src/sdk.ts +7 -0
  216. package/src/settings-bundle.ts +418 -0
  217. package/src/skills/builtin/a11y-review.ts +132 -0
  218. package/src/skills/builtin/compat-check.ts +71 -0
  219. package/src/skills/builtin/maestro-test.ts +121 -0
  220. package/src/skills/builtin/perf-profile.ts +90 -0
  221. package/src/skills/builtin/screenshot-all.ts +52 -0
  222. package/src/skills/builtin/visual-diff.ts +100 -0
  223. package/src/skills/registry.ts +107 -0
  224. package/src/skills/types.ts +41 -0
  225. package/src/team-machine-prompt.ts +73 -0
  226. package/src/vite-plugin-one.ts +190 -0
  227. package/src/vite-plugin.ts +1750 -0
  228. package/src/worklets-babel.ts +134 -0
@@ -0,0 +1,215 @@
1
+ // minimal client for `codex app-server` — a long-lived child process that
2
+ // speaks line-delimited JSON-RPC 2.0 over stdio.
3
+ //
4
+ // scope: just the subset sootsim needs for the attached-projects agent
5
+ // wrapper. each connection owns exactly one spawned app-server child; caller
6
+ // is responsible for lifecycle. bidirectional: client can issue requests +
7
+ // receive server-initiated notifications. we treat every method we don't
8
+ // recognize as an observable event that we pass straight to the handler map.
9
+
10
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
11
+ import readline from 'node:readline'
12
+
13
+ export interface CodexClientOptions {
14
+ /** resolved path to the codex binary (via `which codex` or user override). */
15
+ bin: string
16
+ /** working directory for the spawned process. defaults to process.cwd(). */
17
+ cwd?: string
18
+ /** extra environment overrides forwarded to the child. */
19
+ env?: Record<string, string | undefined>
20
+ }
21
+
22
+ export type CodexNotificationHandler = (params: unknown) => void
23
+
24
+ export interface CodexClient {
25
+ /** promise that resolves when the child exits. use to await shutdown. */
26
+ readonly exited: Promise<{ code: number | null; signal: NodeJS.Signals | null }>
27
+ /** register a handler for a server-sent notification method. returns
28
+ * a dispose fn. multiple handlers per method are allowed. */
29
+ on(method: string, handler: CodexNotificationHandler): () => void
30
+ /** send a JSON-RPC request and await its response. rejects on error
31
+ * response or child death. */
32
+ request<TResult = unknown>(method: string, params?: unknown): Promise<TResult>
33
+ /** send a JSON-RPC notification (no id, no response). */
34
+ notify(method: string, params?: unknown): void
35
+ /** close stdin (graceful shutdown), then SIGTERM after timeout. */
36
+ shutdown(timeoutMs?: number): Promise<void>
37
+ /** force-kill immediately. */
38
+ kill(signal?: NodeJS.Signals): void
39
+ }
40
+
41
+ interface JsonRpcResponse {
42
+ jsonrpc?: '2.0'
43
+ id?: number | string | null
44
+ result?: unknown
45
+ error?: { code: number; message: string; data?: unknown }
46
+ method?: string
47
+ params?: unknown
48
+ }
49
+
50
+ export class CodexRpcError extends Error {
51
+ code: number
52
+ data: unknown
53
+ constructor(message: string, code: number, data?: unknown) {
54
+ super(message)
55
+ this.name = 'CodexRpcError'
56
+ this.code = code
57
+ this.data = data
58
+ }
59
+ }
60
+
61
+ export function spawnCodexClient(options: CodexClientOptions): CodexClient {
62
+ const child: ChildProcessWithoutNullStreams = spawn(options.bin, ['app-server'], {
63
+ cwd: options.cwd,
64
+ env: { ...process.env, ...options.env },
65
+ stdio: ['pipe', 'pipe', 'pipe'],
66
+ })
67
+
68
+ const pending = new Map<
69
+ number,
70
+ {
71
+ resolve: (value: unknown) => void
72
+ reject: (err: unknown) => void
73
+ method: string
74
+ }
75
+ >()
76
+ const handlers = new Map<string, Set<CodexNotificationHandler>>()
77
+ let nextId = 1
78
+ let closed = false
79
+
80
+ const exited = new Promise<{
81
+ code: number | null
82
+ signal: NodeJS.Signals | null
83
+ }>((resolve) => {
84
+ child.on('exit', (code, signal) => {
85
+ closed = true
86
+ const err = new Error(
87
+ `codex app-server exited (code=${code}, signal=${signal ?? ''})`,
88
+ )
89
+ for (const { reject } of pending.values()) reject(err)
90
+ pending.clear()
91
+ resolve({ code, signal })
92
+ })
93
+ })
94
+
95
+ // stdout carries JSON-RPC messages, one per line.
96
+ const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity })
97
+ rl.on('line', (line) => {
98
+ const trimmed = line.trim()
99
+ if (!trimmed) return
100
+ let msg: JsonRpcResponse
101
+ try {
102
+ msg = JSON.parse(trimmed) as JsonRpcResponse
103
+ } catch {
104
+ // app-server occasionally emits non-JSON warnings; ignore.
105
+ return
106
+ }
107
+ if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
108
+ const idNum = typeof msg.id === 'string' ? Number(msg.id) : msg.id
109
+ const entry = idNum != null ? pending.get(idNum) : undefined
110
+ if (!entry) return
111
+ pending.delete(idNum!)
112
+ if (msg.error) {
113
+ entry.reject(new CodexRpcError(msg.error.message, msg.error.code, msg.error.data))
114
+ } else {
115
+ entry.resolve(msg.result)
116
+ }
117
+ return
118
+ }
119
+ if (msg.method) {
120
+ const set = handlers.get(msg.method)
121
+ if (!set) return
122
+ for (const h of set) {
123
+ try {
124
+ h(msg.params)
125
+ } catch (err) {
126
+ // a buggy handler must not kill the bridge, but it MUST be visible —
127
+ // silent swallowing turned a notification-handler typo into a
128
+ // "nothing happens" bug once already.
129
+ console.error(
130
+ `[codex-client] handler for "${msg.method}" threw:`,
131
+ err instanceof Error ? (err.stack ?? err.message) : err,
132
+ )
133
+ }
134
+ }
135
+ }
136
+ })
137
+
138
+ // stderr is diagnostic only; surfaced via the "stderr" notification channel
139
+ // so callers can forward it into transcripts if they want.
140
+ child.stderr.setEncoding('utf8')
141
+ child.stderr.on('data', (chunk: string) => {
142
+ const set = handlers.get('__stderr__')
143
+ if (!set) return
144
+ for (const h of set) {
145
+ try {
146
+ h({ text: chunk })
147
+ } catch {}
148
+ }
149
+ })
150
+
151
+ function write(msg: unknown): void {
152
+ if (closed) return
153
+ try {
154
+ child.stdin.write(JSON.stringify(msg) + '\n')
155
+ } catch {
156
+ // if stdin has been closed, subsequent writes will throw; ignore.
157
+ }
158
+ }
159
+
160
+ return {
161
+ exited,
162
+ on(method, handler) {
163
+ let set = handlers.get(method)
164
+ if (!set) {
165
+ set = new Set()
166
+ handlers.set(method, set)
167
+ }
168
+ set.add(handler)
169
+ return () => {
170
+ set?.delete(handler)
171
+ }
172
+ },
173
+ request<TResult>(method: string, params?: unknown): Promise<TResult> {
174
+ if (closed) {
175
+ return Promise.reject(new Error(`codex app-server closed; cannot call ${method}`))
176
+ }
177
+ const id = nextId++
178
+ return new Promise<TResult>((resolve, reject) => {
179
+ pending.set(id, {
180
+ resolve: (v) => resolve(v as TResult),
181
+ reject,
182
+ method,
183
+ })
184
+ write({ jsonrpc: '2.0', id, method, params })
185
+ })
186
+ },
187
+ notify(method, params) {
188
+ write({ jsonrpc: '2.0', method, params })
189
+ },
190
+ async shutdown(timeoutMs = 1500) {
191
+ if (closed) return
192
+ try {
193
+ child.stdin.end()
194
+ } catch {}
195
+ const t = setTimeout(() => {
196
+ if (!closed) {
197
+ try {
198
+ child.kill('SIGTERM')
199
+ } catch {}
200
+ }
201
+ }, timeoutMs)
202
+ try {
203
+ await exited
204
+ } finally {
205
+ clearTimeout(t)
206
+ }
207
+ },
208
+ kill(signal = 'SIGTERM') {
209
+ if (closed) return
210
+ try {
211
+ child.kill(signal)
212
+ } catch {}
213
+ },
214
+ }
215
+ }
package/src/config.ts ADDED
@@ -0,0 +1,134 @@
1
+ // sootsim.config.ts — per-app module resolution, turbo modules, env, state, and settings.
2
+ //
3
+ // usage:
4
+ // import { defineConfig } from 'rnxsim/config'
5
+ // export default defineConfig({ modules: { ... }, env: { ... } })
6
+
7
+ import type { RevenueCatTestStoreConfig } from '@sootsim/compat/revenuecat-test-store'
8
+
9
+ export type ModuleResolution =
10
+ // an empty module under `modules`; under `nativeModules` / `turboModules` a
11
+ // present-but-inert native module whose every method call is a no-op.
12
+ | 'noop'
13
+ | false // disable builtin stub, use original
14
+ | { use: string } // redirect to existing compat stub
15
+ | { file: string } // resolve to a file relative to the config
16
+ | { inline: Record<string, any> } // inline stub object
17
+
18
+ export type NativeModuleResolution = ModuleResolution | Record<string, any> // direct native module object for non-url configs
19
+
20
+ export interface SootSimConfig<
21
+ Settings extends Record<string, any> = Record<string, any>,
22
+ > {
23
+ // host-only engine selection. omitted means the current stable runtime.
24
+ runtimeVersion?: string
25
+ modules?: Record<string, ModuleResolution>
26
+ turboModules?: Record<string, NativeModuleResolution>
27
+ nativeModules?: Record<string, NativeModuleResolution>
28
+ env?: Record<string, string>
29
+ // generic per-sim network rewrites for the guest app's runtime fetches.
30
+ // `remap` keys/values are `host:port` (or bare `host`) — a tenant fetch whose
31
+ // url matches a key has its host:port swapped for the value before the
32
+ // request is issued. exists for third-party e2e suites that hardcode a port
33
+ // (e.g. bluesky's flows type `http://localhost:3000`) which collides with a
34
+ // host service; map it to the real bound port without editing the app.
35
+ network?: {
36
+ remap?: Record<string, string>
37
+ }
38
+ settings?: Partial<Settings>
39
+ initialState?: {
40
+ authenticated?: boolean
41
+ locale?: string
42
+ colorScheme?: 'light' | 'dark' | 'auto'
43
+ featureFlags?: Record<string, boolean | string | number>
44
+ // opt-in RevenueCat Test Store catalog consumed by the RNPurchases seam.
45
+ // omitting it preserves the empty-project result with purchases unavailable.
46
+ revenueCat?: RevenueCatTestStoreConfig
47
+ [key: string]: any
48
+ }
49
+ }
50
+
51
+ export const SOOTSIM_CONFIG_QUERY_PARAM = 'sootsimConfig'
52
+
53
+ export function defineConfig<Settings extends Record<string, any> = Record<string, any>>(
54
+ config: SootSimConfig<Settings>,
55
+ ): SootSimConfig<Settings> {
56
+ return config
57
+ }
58
+
59
+ function hasOwnKeys(value: Record<string, unknown> | undefined): boolean {
60
+ return !!value && Object.keys(value).length > 0
61
+ }
62
+
63
+ export function hasSootSimConfig<
64
+ Settings extends Record<string, any> = Record<string, any>,
65
+ >(config?: SootSimConfig<Settings> | null): config is SootSimConfig<Settings> {
66
+ if (!config) return false
67
+ return (
68
+ (typeof config.runtimeVersion === 'string' && config.runtimeVersion.length > 0) ||
69
+ hasOwnKeys(config.modules as Record<string, unknown> | undefined) ||
70
+ hasOwnKeys(config.turboModules as Record<string, unknown> | undefined) ||
71
+ hasOwnKeys(config.nativeModules as Record<string, unknown> | undefined) ||
72
+ hasOwnKeys(config.env as Record<string, unknown> | undefined) ||
73
+ hasOwnKeys(config.network?.remap as Record<string, unknown> | undefined) ||
74
+ hasOwnKeys(config.settings as Record<string, unknown> | undefined) ||
75
+ hasOwnKeys(config.initialState as Record<string, unknown> | undefined)
76
+ )
77
+ }
78
+
79
+ export function mergeSootSimConfig<
80
+ Settings extends Record<string, any> = Record<string, any>,
81
+ >(
82
+ base?: SootSimConfig<Settings> | null,
83
+ override?: SootSimConfig<Settings> | null,
84
+ ): SootSimConfig<Settings> | undefined {
85
+ if (!hasSootSimConfig(base) && !hasSootSimConfig(override)) return undefined
86
+ if (!hasSootSimConfig(base)) return override ? { ...override } : undefined
87
+ if (!hasSootSimConfig(override)) return base ? { ...base } : undefined
88
+
89
+ return {
90
+ runtimeVersion: override.runtimeVersion ?? base.runtimeVersion,
91
+ modules: { ...(base.modules || {}), ...(override.modules || {}) },
92
+ turboModules: { ...(base.turboModules || {}), ...(override.turboModules || {}) },
93
+ nativeModules: {
94
+ ...(base.nativeModules || {}),
95
+ ...(override.nativeModules || {}),
96
+ },
97
+ env: { ...(base.env || {}), ...(override.env || {}) },
98
+ network: {
99
+ remap: { ...(base.network?.remap || {}), ...(override.network?.remap || {}) },
100
+ },
101
+ settings: {
102
+ ...(base.settings || {}),
103
+ ...(override.settings || {}),
104
+ } as Partial<Settings>,
105
+ initialState: { ...(base.initialState || {}), ...(override.initialState || {}) },
106
+ }
107
+ }
108
+
109
+ export function readSootSimConfigFromSearchParams<
110
+ Settings extends Record<string, any> = Record<string, any>,
111
+ >(searchParams: URLSearchParams): SootSimConfig<Settings> | undefined {
112
+ const raw = searchParams.get(SOOTSIM_CONFIG_QUERY_PARAM)
113
+ if (!raw) return undefined
114
+ try {
115
+ const parsed = JSON.parse(raw) as SootSimConfig<Settings>
116
+ return hasSootSimConfig(parsed) ? parsed : undefined
117
+ } catch (error) {
118
+ const message = error instanceof Error ? error.message : String(error)
119
+ console.warn(`[rnx] invalid ${SOOTSIM_CONFIG_QUERY_PARAM}: ${message}`)
120
+ return undefined
121
+ }
122
+ }
123
+
124
+ export function applySootSimConfigToUrl<
125
+ Settings extends Record<string, any> = Record<string, any>,
126
+ >(url: string, config?: SootSimConfig<Settings> | null): string {
127
+ const parsed = new URL(url)
128
+ if (hasSootSimConfig(config)) {
129
+ parsed.searchParams.set(SOOTSIM_CONFIG_QUERY_PARAM, JSON.stringify(config))
130
+ } else {
131
+ parsed.searchParams.delete(SOOTSIM_CONFIG_QUERY_PARAM)
132
+ }
133
+ return parsed.toString()
134
+ }
@@ -0,0 +1,236 @@
1
+ import { isLoopbackHost } from './backend-origin'
2
+ import { METRO_BUNDLE_PATH, normalizeNativeDevBundleUrl } from './native-dev-bundle-url'
3
+
4
+ export const DEFAULT_TEST_DRIVE_PORT = 8081
5
+
6
+ export interface ResolvedDevBundle {
7
+ bundleUrl: string
8
+ port: number
9
+ framework: string
10
+ projectName?: string
11
+ }
12
+
13
+ function buildUnresolvedTargetError(targetLabel: string): Error {
14
+ return new Error(
15
+ `could not resolve a native bundle for ${targetLabel}. pass an explicit bundle URL or open Connect and choose the app there.`,
16
+ )
17
+ }
18
+
19
+ export function inferManifestFramework(
20
+ launchUrl: string | undefined,
21
+ sdkVersion: unknown,
22
+ ): string {
23
+ if (launchUrl?.includes('/one/metro-entry.bundle')) return 'one'
24
+ if (typeof sdkVersion === 'string' && sdkVersion) return 'expo'
25
+ return 'unknown'
26
+ }
27
+
28
+ function buildBaseUrl(protocol: string, host: string, port: number) {
29
+ return `${protocol}//${host}:${port}`
30
+ }
31
+
32
+ function shouldProxyLoopbackProbe(targetUrl: string) {
33
+ if (typeof window === 'undefined') return false
34
+ try {
35
+ const parsed = new URL(targetUrl)
36
+ return (
37
+ isLoopbackHost(parsed.hostname) &&
38
+ isLoopbackHost(window.location.hostname) &&
39
+ parsed.origin !== window.location.origin
40
+ )
41
+ } catch {
42
+ return false
43
+ }
44
+ }
45
+
46
+ async function fetchDevProbe(targetUrl: string, init?: RequestInit): Promise<Response> {
47
+ const requestInit: RequestInit = {
48
+ ...init,
49
+ cache: init?.cache ?? 'no-store',
50
+ }
51
+ if (!shouldProxyLoopbackProbe(targetUrl)) {
52
+ return fetch(targetUrl, requestInit)
53
+ }
54
+ return fetch(`/__fetch-proxy?url=${encodeURIComponent(targetUrl)}`, requestInit)
55
+ }
56
+
57
+ function getDefaultPortForProtocol(protocol: string) {
58
+ return protocol === 'https:' ? 443 : 80
59
+ }
60
+
61
+ function isBaseServerUrl(url: URL) {
62
+ const path = url.pathname || '/'
63
+ return (path === '/' || path === '') && !url.search && !url.hash
64
+ }
65
+
66
+ // resolve a loopback dev-server port through the SAME server-side scanner the
67
+ // 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.
70
+ //
71
+ // why this exists: in a browser/worker the direct manifest probe below is
72
+ // cross-origin (shell on :5173, dev server on :<port>), and a dev server
73
+ // answers a cross-origin `GET /` with its SSR HTML, not the expo manifest. so
74
+ // the manifest branch silently fails (`.json()` throws on HTML) and the probe
75
+ // falls back to a generic `/index.bundle` — a DIFFERENT metro entry point than
76
+ // the manifest's `metro-entry.bundle`, which makes metro build the same app
77
+ // twice. the scanner runs server-side (same-origin node fetch, sends the
78
+ // expo-platform header) and reads `launchAsset.url` correctly.
79
+ //
80
+ // env-agnostic by design: it just attempts `/__server-scan` and lets the result
81
+ // decide. in the tenant worker (which has no `location`) the patched fetch
82
+ // resolves the relative path to the shell origin and returns the scan json. in a
83
+ // node/bun CLI a relative-URL fetch with no base throws → null → the direct probe
84
+ // runs (correct there, since node can read the cross-origin manifest). in prod
85
+ // the path SPA-falls-back to HTML → the json content-type guard returns null →
86
+ // direct probe. so there is exactly one resolver per environment, no env sniff.
87
+ async function resolvePortViaServerScan(port: number): Promise<ResolvedDevBundle | null> {
88
+ try {
89
+ const res = await fetch('/__server-scan', { cache: 'no-store' })
90
+ if (!res.ok) return null
91
+ if (!(res.headers.get('content-type') || '').toLowerCase().includes('json'))
92
+ return null
93
+ const servers: unknown = await res.json()
94
+ if (!Array.isArray(servers)) return null
95
+ const match = servers.find(
96
+ (
97
+ s,
98
+ ): s is {
99
+ port: number
100
+ bundleUrl: string
101
+ framework?: string
102
+ projectName?: string
103
+ } =>
104
+ !!s &&
105
+ typeof s === 'object' &&
106
+ (s as { port?: unknown }).port === port &&
107
+ typeof (s as { bundleUrl?: unknown }).bundleUrl === 'string',
108
+ )
109
+ if (!match) return null
110
+ return {
111
+ // 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
114
+ // worker, so a deferred React.lazy chunk throws "Requiring unknown module
115
+ // <id>". normalizeNativeDevBundleUrl forces lazy=false + strips bytecode.
116
+ bundleUrl: normalizeNativeDevBundleUrl(match.bundleUrl),
117
+ port,
118
+ framework: typeof match.framework === 'string' ? match.framework : 'unknown',
119
+ projectName: typeof match.projectName === 'string' ? match.projectName : undefined,
120
+ }
121
+ } catch {
122
+ return null
123
+ }
124
+ }
125
+
126
+ async function probeBaseUrlBundle(
127
+ baseUrl: string,
128
+ port: number,
129
+ ): Promise<ResolvedDevBundle | null> {
130
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, '')
131
+
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
+ try {
142
+ const manifestRes = await fetchDevProbe(`${normalizedBaseUrl}/`, {
143
+ headers: { 'expo-platform': 'ios' },
144
+ })
145
+ if (manifestRes.ok) {
146
+ const manifest = await manifestRes.json()
147
+ const client = manifest?.extra?.expoClient || manifest?.extra || {}
148
+ const launchUrl =
149
+ typeof manifest?.launchAsset?.url === 'string'
150
+ ? manifest.launchAsset.url
151
+ : undefined
152
+ if (launchUrl || client.name) {
153
+ return {
154
+ bundleUrl: normalizeNativeDevBundleUrl(
155
+ launchUrl || `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
156
+ ),
157
+ port,
158
+ framework: inferManifestFramework(launchUrl, client.sdkVersion),
159
+ projectName: client.name,
160
+ }
161
+ }
162
+ }
163
+ } catch {}
164
+
165
+ // vanilla Metro (or any non-Expo dev server) — packager-status:running on
166
+ // /status is the canonical signature. we previously also did `HEAD
167
+ // /node_modules/one/metro-entry.bundle` as a framework discriminator, but
168
+ // Metro lazy-resolves modules at request time, so HEAD on *any* `.bundle`
169
+ // path can return 200 even when the underlying file doesn't exist. that
170
+ // caused false-positive One framework detection on non-One apps that had
171
+ // `node_modules/one` anywhere on disk (or that returned 200 HEAD + 404
172
+ // GET for missing bundles), surfacing as
173
+ // "failed to fetch bundle (404|502): .../node_modules/one/metro-entry.bundle…"
174
+ // the Expo manifest probe above already returns the correct launchAsset.url
175
+ // for One apps (which serve the Expo manifest format via vxrn), so we
176
+ // drop the bespoke One probe entirely.
177
+ try {
178
+ const statusRes = await fetchDevProbe(`${normalizedBaseUrl}/status`)
179
+ if (statusRes.ok) {
180
+ const text = await statusRes.text()
181
+ if (text.includes('packager-status:running')) {
182
+ return {
183
+ bundleUrl: `${normalizedBaseUrl}${METRO_BUNDLE_PATH}`,
184
+ port,
185
+ framework: 'metro',
186
+ }
187
+ }
188
+ }
189
+ } catch {}
190
+
191
+ return null
192
+ }
193
+
194
+ export async function probePortBundle(port: number): Promise<ResolvedDevBundle | null> {
195
+ return probeBaseUrlBundle(buildBaseUrl('http:', 'localhost', port), port)
196
+ }
197
+
198
+ export async function resolveConnectionInput(input: string): Promise<ResolvedDevBundle> {
199
+ const trimmed = input.trim()
200
+
201
+ if (/^\d+$/.test(trimmed)) {
202
+ const port = parseInt(trimmed, 10)
203
+ const resolved = await probePortBundle(port)
204
+ if (resolved) return resolved
205
+ throw buildUnresolvedTargetError(`localhost:${port}`)
206
+ }
207
+
208
+ const bundleUrl = trimmed.startsWith('http') ? trimmed : `http://${trimmed}`
209
+ let parsed: URL
210
+
211
+ try {
212
+ parsed = new URL(bundleUrl)
213
+ } catch {
214
+ throw new Error(
215
+ `could not parse "${input}". pass a dev-server port, a dev-server base URL, or a full bundle URL.`,
216
+ )
217
+ }
218
+
219
+ const protocol = parsed.protocol || 'http:'
220
+ const port = parsed.port
221
+ ? parseInt(parsed.port, 10)
222
+ : getDefaultPortForProtocol(protocol)
223
+ const baseUrl = buildBaseUrl(protocol, parsed.hostname, port)
224
+
225
+ // a bare base URL (just an origin, no path) is unambiguously a dev server
226
+ // to probe — regardless of host. probe its live manifest rather than
227
+ // guessing that the origin itself is the bundle URL. loopback vs. remote
228
+ // only affects how the probe fetch is routed (see shouldProxyLoopbackProbe).
229
+ if (isBaseServerUrl(parsed)) {
230
+ const resolved = await probeBaseUrlBundle(baseUrl, port)
231
+ if (resolved) return resolved
232
+ throw buildUnresolvedTargetError(baseUrl)
233
+ }
234
+
235
+ return { bundleUrl: parsed.toString(), port, framework: 'unknown' }
236
+ }