peachsim 0.1.527

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 (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,471 @@
1
+ const RNXSIM_METRO_MODULE_PATHS_PREFIX = 'globalThis.__sootsimModulePaths='
2
+ export const RNX_METRO_MODULE_IDENTITY_VERSION = 2
3
+
4
+ export type RNXMetroModulePathMap = Record<string, string>
5
+ export type RNXMetroLogicalSpecifierMap = Record<string, string[]>
6
+ export type RNXMetroModuleIdentitySource =
7
+ | 'rnx-plugin'
8
+ | 'contrast-bundler'
9
+ | 'metro-source-map'
10
+
11
+ export interface RNXMetroModuleIdentityMetadata {
12
+ version: typeof RNX_METRO_MODULE_IDENTITY_VERSION
13
+ identitySource: RNXMetroModuleIdentitySource
14
+ modulePaths: RNXMetroModulePathMap
15
+ logicalSpecifiers: RNXMetroLogicalSpecifierMap
16
+ }
17
+
18
+ export interface RNXMetroModuleIdentity extends RNXMetroModuleIdentityMetadata {
19
+ source: string
20
+ /**
21
+ * Metro's trailing `//# sourceMappingURL=` comment when the graph carried
22
+ * one, already split off `source`. A production graph has none.
23
+ */
24
+ sourceMappingUrl: string | null
25
+ }
26
+
27
+ // the production bundle rnx loads is the app's ORDINARY production bundle.
28
+ // nothing about resolution changes, so these are plain Metro production
29
+ // options and the artifact is byte-identical to what the app ships apart
30
+ // from the identity footer the plugin's serializer appends.
31
+ export function toRNXProductionBundleUrl(bundleUrl: string): string {
32
+ const relative = bundleUrl.startsWith('/')
33
+ const url = new URL(bundleUrl, 'http://rnxsim.local')
34
+ url.searchParams.set('dev', 'false')
35
+ url.searchParams.set('hot', 'false')
36
+ url.searchParams.set('minify', 'true')
37
+ return relative ? `${url.pathname}${url.search}${url.hash}` : url.toString()
38
+ }
39
+
40
+ /**
41
+ * the ordinary development counterpart. Metro keeps `__DEV__` true, verbose
42
+ * module names, and its source map. `hot` stays false because the artifact is
43
+ * immutable once produced.
44
+ */
45
+ export function toRNXDevelopmentBundleUrl(bundleUrl: string): string {
46
+ const relative = bundleUrl.startsWith('/')
47
+ const url = new URL(bundleUrl, 'http://rnxsim.local')
48
+ url.searchParams.set('dev', 'true')
49
+ url.searchParams.set('hot', 'false')
50
+ url.searchParams.set('minify', 'false')
51
+ return relative ? `${url.pathname}${url.search}${url.hash}` : url.toString()
52
+ }
53
+
54
+ const RNXSIM_METRO_SOURCE_MAP_COMMENT = '//# sourceMappingURL='
55
+
56
+ export interface RNXMetroDetachedSourceMapUrl {
57
+ source: string
58
+ /** the comment's URL, which is a whole inline map when Metro inlined it. */
59
+ sourceMappingUrl: string | null
60
+ }
61
+
62
+ /**
63
+ * splits Metro's trailing `//# sourceMappingURL=` comment off the bundle.
64
+ *
65
+ * a development bundle ends with that comment, and Metro inlines the entire
66
+ * map into it as a data URL whenever the caller did not ask for an external
67
+ * map file. leaving it attached makes the last `__r(` statement carry the map
68
+ * as source text: the layout reader would hand megabytes of base64 to the
69
+ * minifier, and the artifact the runner loads would keep the map resident in
70
+ * child memory for the life of the simulator.
71
+ */
72
+ export function detachRNXMetroSourceMapUrl(
73
+ bundleText: string,
74
+ ): RNXMetroDetachedSourceMapUrl {
75
+ const trimmedEnd = bundleText.replace(/\s+$/, '')
76
+ const commentStart = trimmedEnd.lastIndexOf(`\n${RNXSIM_METRO_SOURCE_MAP_COMMENT}`)
77
+ if (commentStart < 0) return { source: bundleText, sourceMappingUrl: null }
78
+ const url = trimmedEnd.slice(commentStart + 1 + RNXSIM_METRO_SOURCE_MAP_COMMENT.length)
79
+ if (url.includes('\n')) return { source: bundleText, sourceMappingUrl: null }
80
+ return { source: trimmedEnd.slice(0, commentStart), sourceMappingUrl: url }
81
+ }
82
+
83
+ export function createRNXMetroModuleIdentityFooter(
84
+ identity: RNXMetroModuleIdentityMetadata,
85
+ ): string {
86
+ const payload = JSON.stringify(identity)
87
+ .replace(/\u2028/g, '\\u2028')
88
+ .replace(/\u2029/g, '\\u2029')
89
+ .replace(/<\//g, '<\\/')
90
+ return `\n${RNXSIM_METRO_MODULE_PATHS_PREFIX}${payload};`
91
+ }
92
+
93
+ /**
94
+ * writes the identity footer into a serialized bundle, keeping Metro's
95
+ * `//# sourceMappingURL=` comment on the last line when the graph has one. a
96
+ * production graph has no such comment and gets the footer appended, exactly
97
+ * as before.
98
+ */
99
+ export function appendRNXMetroModuleIdentityFooter(
100
+ bundleText: string,
101
+ identity: RNXMetroModuleIdentityMetadata,
102
+ ): string {
103
+ const footer = createRNXMetroModuleIdentityFooter(identity)
104
+ const detached = detachRNXMetroSourceMapUrl(bundleText)
105
+ return detached.sourceMappingUrl === null
106
+ ? bundleText + footer
107
+ : `${detached.source}${footer}\n${RNXSIM_METRO_SOURCE_MAP_COMMENT}${detached.sourceMappingUrl}`
108
+ }
109
+
110
+ export function readRNXMetroModuleIdentity(
111
+ bundleText: string,
112
+ ): RNXMetroModuleIdentity | null {
113
+ const detached = detachRNXMetroSourceMapUrl(bundleText)
114
+ const withoutMap = detached.source
115
+ const footerPrefix = `\n${RNXSIM_METRO_MODULE_PATHS_PREFIX}`
116
+ const footerStart = withoutMap.lastIndexOf(footerPrefix)
117
+ if (footerStart < 0) return null
118
+ if (!withoutMap.endsWith(';')) {
119
+ throw new Error('Metro module identity footer is truncated')
120
+ }
121
+
122
+ let parsed: unknown
123
+ try {
124
+ parsed = JSON.parse(withoutMap.slice(footerStart + footerPrefix.length, -1))
125
+ } catch {
126
+ throw new Error('Metro module identity footer is not valid JSON')
127
+ }
128
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
129
+ throw new Error('Metro module identity footer is malformed')
130
+ }
131
+
132
+ const version = Reflect.get(parsed, 'version')
133
+ // stored artifacts from before the versioned footer carry a bare
134
+ // `{"<moduleId>":"<path>", ...}` map with no wrapper — the exact shape
135
+ // appendRNXModulePaths wrote before the footer was versioned, and the only
136
+ // writer of that shape used identitySource 'rnx-plugin'. normalize it into
137
+ // the current in-memory identity so callers get the same exact modulePaths
138
+ // map instead of falling back to name-scanning or fingerprint inference.
139
+ // validateRNXMetroModuleIdentity still detects a missing, extra, duplicate,
140
+ // or truncated module ID against the bundle's own __d() defines, so those
141
+ // legacy-map corruptions are caught there, not trusted here — it does not
142
+ // corroborate that a path or factory is itself correct.
143
+ if (version === undefined) {
144
+ const modulePaths: RNXMetroModulePathMap = Object.fromEntries(
145
+ Object.entries(parsed).map(([moduleId, modulePath]) => {
146
+ if (typeof modulePath !== 'string') {
147
+ throw new Error(`Metro module identity has an invalid path for ${moduleId}`)
148
+ }
149
+ return [moduleId, modulePath]
150
+ }),
151
+ )
152
+ return {
153
+ source: withoutMap.slice(0, footerStart),
154
+ version: RNX_METRO_MODULE_IDENTITY_VERSION,
155
+ identitySource: 'rnx-plugin',
156
+ modulePaths,
157
+ logicalSpecifiers: {},
158
+ sourceMappingUrl: detached.sourceMappingUrl,
159
+ }
160
+ }
161
+ const identitySource = Reflect.get(parsed, 'identitySource')
162
+ const parsedModulePaths = Reflect.get(parsed, 'modulePaths')
163
+ const parsedLogicalSpecifiers = Reflect.get(parsed, 'logicalSpecifiers')
164
+ if (
165
+ version !== RNX_METRO_MODULE_IDENTITY_VERSION ||
166
+ (identitySource !== 'rnx-plugin' &&
167
+ identitySource !== 'contrast-bundler' &&
168
+ identitySource !== 'metro-source-map') ||
169
+ typeof parsedModulePaths !== 'object' ||
170
+ parsedModulePaths === null ||
171
+ Array.isArray(parsedModulePaths) ||
172
+ typeof parsedLogicalSpecifiers !== 'object' ||
173
+ parsedLogicalSpecifiers === null ||
174
+ Array.isArray(parsedLogicalSpecifiers)
175
+ ) {
176
+ throw new Error('Metro module identity footer is malformed')
177
+ }
178
+
179
+ const modulePaths: RNXMetroModulePathMap = Object.fromEntries(
180
+ Object.entries(parsedModulePaths).map(([moduleId, modulePath]) => {
181
+ if (typeof modulePath !== 'string') {
182
+ throw new Error(`Metro module identity has an invalid path for ${moduleId}`)
183
+ }
184
+ return [moduleId, modulePath]
185
+ }),
186
+ )
187
+ const logicalSpecifiers: RNXMetroLogicalSpecifierMap = Object.fromEntries(
188
+ Object.entries(parsedLogicalSpecifiers).map(([moduleId, specifiers]) => {
189
+ if (
190
+ !Array.isArray(specifiers) ||
191
+ specifiers.length === 0 ||
192
+ !specifiers.every(
193
+ (specifier) => typeof specifier === 'string' && specifier.length > 0,
194
+ )
195
+ ) {
196
+ throw new Error(`Metro module identity has invalid specifiers for ${moduleId}`)
197
+ }
198
+ const unique = [...new Set(specifiers)].sort((left, right) =>
199
+ left.localeCompare(right),
200
+ )
201
+ if (unique.length !== specifiers.length) {
202
+ throw new Error(`Metro module identity repeats a specifier for ${moduleId}`)
203
+ }
204
+ return [moduleId, unique]
205
+ }),
206
+ )
207
+ return {
208
+ source: withoutMap.slice(0, footerStart),
209
+ version,
210
+ identitySource,
211
+ modulePaths,
212
+ logicalSpecifiers,
213
+ sourceMappingUrl: detached.sourceMappingUrl,
214
+ }
215
+ }
216
+
217
+ // the exact layout Metro's baseJSBundle emits and the rnx runtime contract
218
+ // depends on: a prelude, then one `__d(` or `__r(` statement per line, each
219
+ // running until the next statement starts its own line. one reader owns this
220
+ // recognition so the loader's identity validation, the source-map attribution
221
+ // pass, and the cloud artifact producer cannot drift apart.
222
+ export interface RNXMetroDefine {
223
+ kind: 'define'
224
+ moduleId: number
225
+ start: number
226
+ end: number
227
+ lineIndex: number
228
+ endLineIndex: number
229
+ /** the factory expression, between `__d(` and the module ID. */
230
+ factory: { start: number; end: number }
231
+ /** the dependency array, brackets included. */
232
+ dependencies: { start: number; end: number }
233
+ /**
234
+ * the module's verbose name, quotes included, when the graph carries one.
235
+ * Metro emits it as a fourth `__d` argument on a development graph and omits
236
+ * it entirely on a production one.
237
+ */
238
+ verboseName?: { start: number; end: number }
239
+ }
240
+
241
+ export interface RNXMetroRun {
242
+ kind: 'run'
243
+ start: number
244
+ end: number
245
+ lineIndex: number
246
+ endLineIndex: number
247
+ }
248
+
249
+ export type RNXMetroStatement = RNXMetroDefine | RNXMetroRun
250
+
251
+ export interface RNXMetroBundleLayout {
252
+ /** everything before the first `__d(`/`__r(` line: prelude vars and polyfills. */
253
+ prelude: string
254
+ statements: RNXMetroStatement[]
255
+ /** `__d(` statements whose module ID could not be read. */
256
+ moduleIdFailures: number
257
+ }
258
+
259
+ interface DefineTail {
260
+ moduleId: number
261
+ factoryEnd: number
262
+ dependenciesStart: number
263
+ dependenciesEnd: number
264
+ verboseNameStart?: number
265
+ verboseNameEnd?: number
266
+ }
267
+
268
+ // walks back over a single-quoted or double-quoted string literal ending at
269
+ // `end`, returning the index of its opening quote. an odd run of backslashes
270
+ // before a quote escapes it, so count them before accepting a candidate.
271
+ function readStringLiteralStart(source: string, start: number, end: number): number {
272
+ const quote = source[end]
273
+ if (quote !== '"' && quote !== "'") return -1
274
+ for (let index = end - 1; index > start; index--) {
275
+ if (source[index] !== quote) continue
276
+ let backslashes = 0
277
+ while (index - 1 - backslashes > start && source[index - 1 - backslashes] === '\\') {
278
+ backslashes++
279
+ }
280
+ if (backslashes % 2 === 0) return index
281
+ }
282
+ return -1
283
+ }
284
+
285
+ // each `__d(...)` ends with `),<id>,[dep,dep,...]);?`, and a development graph
286
+ // adds a fourth verbose-name string after the dependency array. the dependency
287
+ // array can be hundreds of entries long, so walk back from `end` instead of
288
+ // searching a fixed tail window: skip trailing `;` and whitespace, consume the
289
+ // closing `)`, take the verbose name when one is there, balance `[...]` to find
290
+ // its opening bracket, then read the integer before it.
291
+ function readDefineTail(source: string, start: number, end: number): DefineTail | null {
292
+ let cursor = end
293
+ while (
294
+ cursor > start &&
295
+ (source[cursor] === ' ' ||
296
+ source[cursor] === '\n' ||
297
+ source[cursor] === '\t' ||
298
+ source[cursor] === ';')
299
+ ) {
300
+ cursor--
301
+ }
302
+ if (source[cursor] !== ')') return null
303
+ cursor--
304
+ while (cursor > start && /\s/.test(source[cursor])) cursor--
305
+ let verboseNameStart: number | undefined
306
+ let verboseNameEnd: number | undefined
307
+ if (source[cursor] === '"' || source[cursor] === "'") {
308
+ const literalStart = readStringLiteralStart(source, start, cursor)
309
+ if (literalStart < 0) return null
310
+ verboseNameStart = literalStart
311
+ verboseNameEnd = cursor + 1
312
+ cursor = literalStart - 1
313
+ while (cursor > start && /\s/.test(source[cursor])) cursor--
314
+ if (source[cursor] !== ',') return null
315
+ cursor--
316
+ while (cursor > start && /\s/.test(source[cursor])) cursor--
317
+ }
318
+ if (source[cursor] !== ']') return null
319
+ const dependenciesEnd = cursor + 1
320
+ let depth = 1
321
+ cursor--
322
+ while (cursor > start && depth > 0) {
323
+ const character = source[cursor]
324
+ if (character === ']') depth++
325
+ else if (character === '[') depth--
326
+ if (depth === 0) break
327
+ cursor--
328
+ }
329
+ if (depth !== 0) return null
330
+ const dependenciesStart = cursor
331
+ let idEnd = cursor - 1
332
+ while (idEnd > start && /\s/.test(source[idEnd])) idEnd--
333
+ if (source[idEnd] !== ',') return null
334
+ // a minifier is free to write the module ID as `1e3` or `0x3e8`, so read the
335
+ // whole numeric literal rather than a run of decimal digits. the factory
336
+ // before it always ends in `}` or `)`, which stops this walk immediately.
337
+ let idStart = idEnd
338
+ while (idStart > start && /[0-9a-fA-FxX.+-]/.test(source[idStart - 1])) idStart--
339
+ const literal = source.slice(idStart, idEnd)
340
+ if (!/^-?(?:0[xXbBoO][0-9a-fA-F]+|\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)$/.test(literal)) {
341
+ return null
342
+ }
343
+ const moduleId = Number(literal)
344
+ if (!Number.isSafeInteger(moduleId)) return null
345
+ let factoryEnd = idStart - 1
346
+ while (factoryEnd > start && /\s/.test(source[factoryEnd])) factoryEnd--
347
+ if (source[factoryEnd] !== ',') return null
348
+ return {
349
+ moduleId,
350
+ factoryEnd,
351
+ dependenciesStart,
352
+ dependenciesEnd,
353
+ verboseNameStart,
354
+ verboseNameEnd,
355
+ }
356
+ }
357
+
358
+ /** reads the canonical Metro production layout from footer-stripped source. */
359
+ export function readRNXMetroBundleLayout(source: string): RNXMetroBundleLayout {
360
+ const lineOffsets = [0]
361
+ for (let index = 0; index < source.length; index++) {
362
+ if (source.charCodeAt(index) === 10) lineOffsets.push(index + 1)
363
+ }
364
+
365
+ const boundaries: number[] = []
366
+ for (let lineIndex = 0; lineIndex < lineOffsets.length; lineIndex++) {
367
+ const start = lineOffsets[lineIndex]
368
+ if (source.startsWith('__d(', start) || source.startsWith('__r(', start)) {
369
+ boundaries.push(lineIndex)
370
+ }
371
+ }
372
+
373
+ const statements: RNXMetroStatement[] = []
374
+ let moduleIdFailures = 0
375
+ for (let index = 0; index < boundaries.length; index++) {
376
+ const lineIndex = boundaries[index]
377
+ const start = lineOffsets[lineIndex]
378
+ const nextLineIndex = boundaries[index + 1]
379
+ const endLineIndex =
380
+ nextLineIndex !== undefined ? nextLineIndex - 1 : lineOffsets.length - 1
381
+ const end = nextLineIndex !== undefined ? lineOffsets[nextLineIndex] : source.length
382
+ if (!source.startsWith('__d(', start)) {
383
+ statements.push({ kind: 'run', start, end, lineIndex, endLineIndex })
384
+ continue
385
+ }
386
+ const tail = readDefineTail(source, start, end - 1)
387
+ if (!tail) {
388
+ moduleIdFailures++
389
+ continue
390
+ }
391
+ statements.push({
392
+ kind: 'define',
393
+ moduleId: tail.moduleId,
394
+ start,
395
+ end,
396
+ lineIndex,
397
+ endLineIndex,
398
+ factory: { start: start + '__d('.length, end: tail.factoryEnd },
399
+ dependencies: { start: tail.dependenciesStart, end: tail.dependenciesEnd },
400
+ ...(tail.verboseNameStart !== undefined && tail.verboseNameEnd !== undefined
401
+ ? { verboseName: { start: tail.verboseNameStart, end: tail.verboseNameEnd } }
402
+ : {}),
403
+ })
404
+ }
405
+
406
+ return {
407
+ prelude: source.slice(
408
+ 0,
409
+ boundaries.length > 0 ? lineOffsets[boundaries[0]] : source.length,
410
+ ),
411
+ statements,
412
+ moduleIdFailures,
413
+ }
414
+ }
415
+
416
+ /**
417
+ * validates the exact identity contract before a loader installs a Metro
418
+ * factory trap or a producer ships an artifact: every define carries a unique,
419
+ * readable module ID, and the identity map names those IDs and no others.
420
+ * serializers that emit another layout need an ingestion adapter that
421
+ * normalizes them to this format.
422
+ */
423
+ export function validateRNXMetroModulePaths(
424
+ source: string,
425
+ modulePaths: RNXMetroModulePathMap,
426
+ ): void {
427
+ const layout = readRNXMetroBundleLayout(source)
428
+ if (layout.moduleIdFailures > 0) {
429
+ throw new Error(
430
+ `Metro module identity could not read ${layout.moduleIdFailures} module IDs`,
431
+ )
432
+ }
433
+ const definedIds = new Set<number>()
434
+ for (const statement of layout.statements) {
435
+ if (statement.kind !== 'define') continue
436
+ if (definedIds.has(statement.moduleId)) {
437
+ throw new Error(`Metro module identity has duplicate module ${statement.moduleId}`)
438
+ }
439
+ definedIds.add(statement.moduleId)
440
+ if (!Object.prototype.hasOwnProperty.call(modulePaths, String(statement.moduleId))) {
441
+ throw new Error(`Metro module identity is missing ${statement.moduleId}`)
442
+ }
443
+ }
444
+ if (definedIds.size === 0) {
445
+ throw new Error('Metro module identity found no canonical JavaScript modules')
446
+ }
447
+
448
+ for (const [key, modulePath] of Object.entries(modulePaths)) {
449
+ const moduleId = Number(key)
450
+ if (!Number.isSafeInteger(moduleId) || String(moduleId) !== key) {
451
+ throw new Error(`Metro module identity has invalid module ID ${key}`)
452
+ }
453
+ if (typeof modulePath !== 'string' || modulePath.length === 0) {
454
+ throw new Error(`Metro module identity has no path for ${key}`)
455
+ }
456
+ if (!definedIds.has(moduleId)) {
457
+ throw new Error(`Metro module identity has no module ${key}`)
458
+ }
459
+ }
460
+ }
461
+
462
+ export function validateRNXMetroModuleIdentity(identity: RNXMetroModuleIdentity): void {
463
+ validateRNXMetroModulePaths(identity.source, identity.modulePaths)
464
+ for (const moduleId of Object.keys(identity.logicalSpecifiers)) {
465
+ if (!Object.prototype.hasOwnProperty.call(identity.modulePaths, moduleId)) {
466
+ throw new Error(
467
+ `Metro module identity has logical specifiers for unknown module ${moduleId}`,
468
+ )
469
+ }
470
+ }
471
+ }
@@ -0,0 +1,118 @@
1
+ import { isLoopbackHost } from './backend-origin.ts'
2
+
3
+ // canonical entry path for a plain Metro packager that serves no Expo
4
+ // manifest. only used when `/status` confirms `packager-status:running` and
5
+ // the server reports nothing else — never as a guess for a manifest-capable
6
+ // dev server, whose `launchAsset.url` is always the source of truth.
7
+ export const METRO_BUNDLE_PATH =
8
+ '/index.bundle?platform=ios&dev=true&hot=true&minify=false'
9
+
10
+ function isAbsoluteHttpUrl(url: string): boolean {
11
+ return /^https?:\/\//i.test(url)
12
+ }
13
+
14
+ function isNativeDevBundlePath(pathname: string): boolean {
15
+ return pathname.endsWith('.bundle')
16
+ }
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
+
38
+ // opt-in (contrastbot preview-recording context only): serve a production
39
+ // minified bundle instead of the default dev=true unminified one. a
40
+ // preview run just records the app — it never needs fast-refresh or dev
41
+ // warnings — and the ~28MB unminified dev bundle cold-(re)boots
42
+ // unreliably on small CI runners (tens of seconds when metro is warm,
43
+ // minutes cold, sometimes past any wait-ready budget). a minified bundle
44
+ // parses/executes far faster and reboots reliably. gated on
45
+ // SOOTSIM_PREVIEW_PROD_BUNDLE so normal interactive `rnx open` keeps
46
+ // dev=true. app-agnostic: rewrites whatever metro/launchAsset advertised.
47
+ function applyPreviewProdBundle(parsed: URL): void {
48
+ if (!process.env.SOOTSIM_PREVIEW_PROD_BUNDLE) return
49
+ parsed.searchParams.set('dev', 'false')
50
+ parsed.searchParams.set('minify', 'true')
51
+ // prod bundles have no HMR; keep metro's cache key consistent.
52
+ if (parsed.searchParams.has('hot')) parsed.searchParams.set('hot', 'false')
53
+ }
54
+
55
+ export function normalizeNativeDevBundleUrl(bundleUrl: string): string {
56
+ try {
57
+ const isAbsolute = isAbsoluteHttpUrl(bundleUrl)
58
+ const parsed = new URL(bundleUrl, 'http://sootsim.local')
59
+ parsed.pathname = parsed.pathname.replace(/\.\.bundle$/, '.bundle')
60
+ if (!isNativeDevBundlePath(parsed.pathname)) return bundleUrl
61
+ // do NOT force dev=true or minify=false — metro's dev server already
62
+ // defaults to dev mode + unminified, and iOS sim's launchAsset URL
63
+ // omits both params. forcing them gives metro a different cache key
64
+ // for the same bundle. only the prod-bundle override path
65
+ // (demo-upload-specs) sets dev=false&minify=true on purpose.
66
+ // do NOT touch `hot` either. real RN iOS clients load launchAsset.url
67
+ // verbatim — expo's createBundleUrlSearchParams hardcodes hot=false
68
+ // (with a `// TODO: Is this still needed?` comment) for every manifest
69
+ // it emits, and HMR still works on iOS because RN's HMRClient.js
70
+ // polyfill connects to /hot independently. sootsim's HmrClient is the
71
+ // same — connection happens via /hot regardless of this query param —
72
+ // so matching the iOS sim's URL exactly lets metro share its bundle
73
+ // cache between the two clients (otherwise the 28MB+ bundle rebuilds
74
+ // twice on first cold metro request).
75
+ // strip transform.bytecode — metro would return hermes bytecode and our
76
+ // bundle-loader can only execute plain JS.
77
+ parsed.searchParams.delete('transform.bytecode')
78
+ // force lazy=false. sootsim loads ONE self-contained bundle into the tenant
79
+ // worker. metro's lazy=true defers code-split modules to async chunks; vxrn/
80
+ // one's emitted entry can synchronously `require` a deferred module before
81
+ // its chunk has loaded, throwing "Requiring unknown module <id>" and crashing
82
+ // the route (observed with one's React.lazy GameScene3D split). lazy=false
83
+ // ships every module in the served bundle — import()/React.lazy RUNTIME
84
+ // semantics are unchanged (the chunk is just inlined), so this is faithful,
85
+ // not a behavior change. the engine's __loadBundleAsync still handles any
86
+ // genuinely-async chunks an app fetches at runtime.
87
+ if (parsed.searchParams.get('lazy') === 'true') {
88
+ parsed.searchParams.set('lazy', 'false')
89
+ }
90
+ applyPreviewProdBundle(parsed)
91
+ if (isAbsolute) return parsed.toString()
92
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`
93
+ } catch {
94
+ return bundleUrl
95
+ }
96
+ }
97
+
98
+ // explicit override used by `rnx open` to flip metro's `hot` query when
99
+ // the caller knows their context: interactive opens want `hot=true` so metro
100
+ // includes its HMR/react-refresh runtime in the bundle prelude and pushes
101
+ // edits as `update` events; `--driver playwright` / agent-test runs want
102
+ // `hot=false` for deterministic e2e (no in-bundle refresh wiring, no
103
+ // background ws). normalizeNativeDevBundleUrl deliberately preserves
104
+ // whatever value the launchAsset URL carries so metro's bundle cache is
105
+ // shared with real iOS sims; this helper is the one place that breaks that
106
+ // rule on purpose. non-`.bundle` paths pass through unchanged.
107
+ export function applyHotMode(bundleUrl: string, hot: boolean): string {
108
+ try {
109
+ const isAbsolute = isAbsoluteHttpUrl(bundleUrl)
110
+ const parsed = new URL(bundleUrl, 'http://sootsim.local')
111
+ if (!isNativeDevBundlePath(parsed.pathname)) return bundleUrl
112
+ parsed.searchParams.set('hot', hot ? 'true' : 'false')
113
+ if (isAbsolute) return parsed.toString()
114
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`
115
+ } catch {
116
+ return bundleUrl
117
+ }
118
+ }