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,139 @@
1
+ // sootsim metro plugin — serves the sootsim shell at /__soot/ on metro's server.
2
+ // shell assets come from an engine runtime under ~/.sootsim/runtimes/, fetched
3
+ // by `sootsim runtime install` / `sootsim setup`. ordinary localhost requests
4
+ // follow active; a version-owned localhost origin stays on its exact directory
5
+ // when active changes, so one loaded shell cannot mix runtime chunk trees.
6
+ //
7
+ // usage in metro.config.js:
8
+ // const { withSootsim } = require('sootsim/metro')
9
+ // module.exports = withSootsim(getDefaultConfig(__dirname))
10
+
11
+ import fs from 'fs'
12
+ import path from 'path'
13
+ import {
14
+ isRootRuntimeAssetPath,
15
+ resolveRuntimeFilePath,
16
+ resolveRuntimeRootForRequestHost,
17
+ serveRuntimeFile,
18
+ SOOTSIM_RUNTIME_MISSING_MESSAGE,
19
+ } from './runtime-assets'
20
+
21
+ const prefix = '/__soot'
22
+
23
+ export interface SootsimMetroOptions {
24
+ bundleUrl?: string
25
+ enabled?: boolean
26
+ }
27
+
28
+ // the slice of metro's config we touch — keeps `T` open for whatever
29
+ // shape consumers pass while letting us extend `server.enhanceMiddleware`.
30
+ type MetroConfigWithServer = {
31
+ server?: {
32
+ enhanceMiddleware?: (middleware: unknown, server: unknown) => unknown
33
+ [key: string]: unknown
34
+ }
35
+ }
36
+
37
+ export function withSootsim<T extends Record<string, unknown>>(
38
+ config: T,
39
+ options: SootsimMetroOptions = {},
40
+ ): T {
41
+ if (options.enabled === false) return config
42
+
43
+ const bundleUrl =
44
+ options.bundleUrl || '/index.bundle?platform=ios&dev=true&hot=true&minify=false'
45
+
46
+ const cfgServer = (config as MetroConfigWithServer).server
47
+ const existingEnhance = cfgServer?.enhanceMiddleware
48
+
49
+ const serverConfig = {
50
+ ...cfgServer,
51
+ enhanceMiddleware: (metroMiddleware: any, metroServer: any) => {
52
+ let middleware = metroMiddleware
53
+ if (existingEnhance) {
54
+ middleware = existingEnhance(metroMiddleware, metroServer)
55
+ }
56
+
57
+ let connect: any
58
+ try {
59
+ connect = require('connect')
60
+ } catch {
61
+ console.warn('[rnx] connect not available')
62
+ return middleware
63
+ }
64
+
65
+ const server = connect()
66
+
67
+ server.use((req: any, res: any, next: any) => {
68
+ const url = req.url || ''
69
+ const pathname = url.split('?')[0]
70
+ const runtimeRoot = resolveRuntimeRootForRequestHost(req.headers?.host)
71
+
72
+ if (!runtimeRoot) {
73
+ if (pathname === prefix || pathname === prefix + '/') {
74
+ res.statusCode = 500
75
+ res.end(SOOTSIM_RUNTIME_MISSING_MESSAGE)
76
+ return
77
+ }
78
+ next()
79
+ return
80
+ }
81
+
82
+ // root-relative runtime assets emitted by the shell and engine vite
83
+ // builds. if a user app has its own /assets/foo request, we only
84
+ // intercept when that exact file exists in the runtime; otherwise
85
+ // metro handles it normally.
86
+ if (isRootRuntimeAssetPath(pathname)) {
87
+ const fullPath = resolveRuntimeFilePath(runtimeRoot, pathname)
88
+ if (fullPath) {
89
+ serveRuntimeFile(req, res, fullPath)
90
+ return
91
+ }
92
+ }
93
+
94
+ // /__soot/* — serve runtime assets
95
+ if (pathname.startsWith(prefix + '/')) {
96
+ const fullPath = resolveRuntimeFilePath(
97
+ runtimeRoot,
98
+ pathname.slice(prefix.length),
99
+ )
100
+ if (fullPath) {
101
+ serveRuntimeFile(req, res, fullPath)
102
+ return
103
+ }
104
+ }
105
+
106
+ // serve HTML shell for /__soot and extensionless in-shell routes.
107
+ if (
108
+ pathname === prefix ||
109
+ pathname === prefix + '/' ||
110
+ pathname.startsWith(prefix + '/')
111
+ ) {
112
+ const ext = path.extname(pathname)
113
+ if (!ext) {
114
+ const htmlPath = path.join(runtimeRoot, 'index.html')
115
+ let html = fs.readFileSync(htmlPath, 'utf8')
116
+ html = html.replace(
117
+ '</head>',
118
+ `<script>history.replaceState(null,'','${prefix}/?bundle=${encodeURIComponent(bundleUrl)}')</script></head>`,
119
+ )
120
+ res.setHeader('content-type', 'text/html')
121
+ res.end(html)
122
+ return
123
+ }
124
+ }
125
+
126
+ next()
127
+ })
128
+
129
+ server.use(middleware)
130
+
131
+ console.log(`[rnx] serving at ${prefix}/`)
132
+ return server
133
+ },
134
+ }
135
+
136
+ return { ...config, server: serverConfig } as T
137
+ }
138
+
139
+ export default withSootsim
@@ -0,0 +1,96 @@
1
+ // canonical entry path for a plain Metro packager that serves no Expo
2
+ // manifest. only used when `/status` confirms `packager-status:running` and
3
+ // the server reports nothing else — never as a guess for a manifest-capable
4
+ // dev server, whose `launchAsset.url` is always the source of truth.
5
+ export const METRO_BUNDLE_PATH =
6
+ '/index.bundle?platform=ios&dev=true&hot=true&minify=false'
7
+
8
+ function isAbsoluteHttpUrl(url: string): boolean {
9
+ return /^https?:\/\//i.test(url)
10
+ }
11
+
12
+ function isNativeDevBundlePath(pathname: string): boolean {
13
+ return pathname.endsWith('.bundle')
14
+ }
15
+
16
+ // opt-in (contrastbot preview-recording context only): serve a production
17
+ // minified bundle instead of the default dev=true unminified one. a
18
+ // preview run just records the app — it never needs fast-refresh or dev
19
+ // warnings — and the ~28MB unminified dev bundle cold-(re)boots
20
+ // unreliably on small CI runners (tens of seconds when metro is warm,
21
+ // minutes cold, sometimes past any wait-ready budget). a minified bundle
22
+ // parses/executes far faster and reboots reliably. gated on
23
+ // SOOTSIM_PREVIEW_PROD_BUNDLE so normal interactive `sootsim open` keeps
24
+ // dev=true. app-agnostic: rewrites whatever metro/launchAsset advertised.
25
+ function applyPreviewProdBundle(parsed: URL): void {
26
+ if (!process.env.SOOTSIM_PREVIEW_PROD_BUNDLE) return
27
+ parsed.searchParams.set('dev', 'false')
28
+ parsed.searchParams.set('minify', 'true')
29
+ // prod bundles have no HMR; keep metro's cache key consistent.
30
+ if (parsed.searchParams.has('hot')) parsed.searchParams.set('hot', 'false')
31
+ }
32
+
33
+ export function normalizeNativeDevBundleUrl(bundleUrl: string): string {
34
+ try {
35
+ const isAbsolute = isAbsoluteHttpUrl(bundleUrl)
36
+ const parsed = new URL(bundleUrl, 'http://sootsim.local')
37
+ parsed.pathname = parsed.pathname.replace(/\.\.bundle$/, '.bundle')
38
+ if (!isNativeDevBundlePath(parsed.pathname)) return bundleUrl
39
+ // do NOT force dev=true or minify=false — metro's dev server already
40
+ // defaults to dev mode + unminified, and iOS sim's launchAsset URL
41
+ // omits both params. forcing them gives metro a different cache key
42
+ // for the same bundle. only the prod-bundle override path
43
+ // (demo-upload-specs) sets dev=false&minify=true on purpose.
44
+ // do NOT touch `hot` either. real RN iOS clients load launchAsset.url
45
+ // verbatim — expo's createBundleUrlSearchParams hardcodes hot=false
46
+ // (with a `// TODO: Is this still needed?` comment) for every manifest
47
+ // it emits, and HMR still works on iOS because RN's HMRClient.js
48
+ // polyfill connects to /hot independently. sootsim's HmrClient is the
49
+ // same — connection happens via /hot regardless of this query param —
50
+ // so matching the iOS sim's URL exactly lets metro share its bundle
51
+ // cache between the two clients (otherwise the 28MB+ bundle rebuilds
52
+ // twice on first cold metro request).
53
+ // strip transform.bytecode — metro would return hermes bytecode and our
54
+ // bundle-loader can only execute plain JS.
55
+ parsed.searchParams.delete('transform.bytecode')
56
+ // force lazy=false. sootsim loads ONE self-contained bundle into the tenant
57
+ // worker. metro's lazy=true defers code-split modules to async chunks; vxrn/
58
+ // one's emitted entry can synchronously `require` a deferred module before
59
+ // its chunk has loaded, throwing "Requiring unknown module <id>" and crashing
60
+ // the route (observed with one's React.lazy GameScene3D split). lazy=false
61
+ // ships every module in the served bundle — import()/React.lazy RUNTIME
62
+ // semantics are unchanged (the chunk is just inlined), so this is faithful,
63
+ // not a behavior change. the engine's __loadBundleAsync still handles any
64
+ // genuinely-async chunks an app fetches at runtime.
65
+ if (parsed.searchParams.get('lazy') === 'true') {
66
+ parsed.searchParams.set('lazy', 'false')
67
+ }
68
+ applyPreviewProdBundle(parsed)
69
+ if (isAbsolute) return parsed.toString()
70
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`
71
+ } catch {
72
+ return bundleUrl
73
+ }
74
+ }
75
+
76
+ // explicit override used by `sootsim open` to flip metro's `hot` query when
77
+ // the caller knows their context: interactive opens want `hot=true` so metro
78
+ // includes its HMR/react-refresh runtime in the bundle prelude and pushes
79
+ // edits as `update` events; `--driver playwright` / agent-test runs want
80
+ // `hot=false` for deterministic e2e (no in-bundle refresh wiring, no
81
+ // background ws). normalizeNativeDevBundleUrl deliberately preserves
82
+ // whatever value the launchAsset URL carries so metro's bundle cache is
83
+ // shared with real iOS sims; this helper is the one place that breaks that
84
+ // rule on purpose. non-`.bundle` paths pass through unchanged.
85
+ export function applyHotMode(bundleUrl: string, hot: boolean): string {
86
+ try {
87
+ const isAbsolute = isAbsoluteHttpUrl(bundleUrl)
88
+ const parsed = new URL(bundleUrl, 'http://sootsim.local')
89
+ if (!isNativeDevBundlePath(parsed.pathname)) return bundleUrl
90
+ parsed.searchParams.set('hot', hot ? 'true' : 'false')
91
+ if (isAbsolute) return parsed.toString()
92
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`
93
+ } catch {
94
+ return bundleUrl
95
+ }
96
+ }
@@ -0,0 +1,341 @@
1
+ // native-seam manifest — the canonical registry of upstream packages whose
2
+ // native-module spec files we override.
3
+ //
4
+ // the rule: when a guest bundle imports from one of these packages, vite lets
5
+ // the package's JS resolve from node_modules normally. only the listed
6
+ // `seamBasenames` are redirected to our stub. this keeps the upstream JS layer
7
+ // (hooks, helpers, transforms, listeners) running unchanged.
8
+ //
9
+ // adding a new entry:
10
+ // 1. identify upstream's native-module file (look for files calling
11
+ // `requireNativeModule`, `TurboModuleRegistry.getEnforced`,
12
+ // `requireNativeComponent`, or in `src/specs/Native*Module.ts`)
13
+ // 2. write a thin stub at `packages/compat/src/stubs/native-seams/<pkg>.ts`
14
+ // that exports the same default + named exports upstream's seam file
15
+ // exports — except the implementation calls into sootsim's engine.
16
+ // 3. append an entry below. the plugin handles the rest.
17
+ //
18
+ // canonical example: react-native-reanimated. see
19
+ // - `packages/compat/src/stubs/react-native-reanimated.ts` (the seam stub)
20
+ // - `reanimatedNativeSeamRedirect` in vite-plugin.ts (still hard-coded
21
+ // because it has package-specific edge cases — fabricUtils,
22
+ // validate-worklets-version. new entries should use this manifest path
23
+ // for the simple basename-match case.)
24
+
25
+ export interface NativeSeamEntry {
26
+ /**
27
+ * upstream npm package name. all imports whose importer path includes
28
+ * `/<pkg>/` are candidates for redirect.
29
+ */
30
+ pkg: string
31
+ /**
32
+ * basenames of upstream files to redirect. each entry matches the file
33
+ * name (no extension, no platform suffix like `.ios` / `.android`) of any
34
+ * upstream module imported transitively from within `pkg`.
35
+ *
36
+ * example: if upstream has
37
+ * `node_modules/expo-notifications/src/NotificationsHandler.ts`, list
38
+ * `'NotificationsHandler'` here and we'll redirect any import resolving
39
+ * to that file to our seam stub.
40
+ */
41
+ seamBasenames: string[]
42
+ /**
43
+ * absolute path to the seam stub. typically
44
+ * `packages/compat/src/stubs/native-seams/<pkg>.ts` — but a single stub
45
+ * file may export every native module the package exposes, so the same
46
+ * `target` can appear in multiple entries if a package has multiple native
47
+ * modules that share an implementation file.
48
+ *
49
+ * authors set this via the `seamPath(<pkg>)` helper below.
50
+ */
51
+ target: string
52
+ /**
53
+ * notes — short reason this entry exists. shown by the debug logger when
54
+ * `SOOTSIM_NATIVE_SEAM_DEBUG=1` is set.
55
+ */
56
+ notes?: string
57
+ }
58
+
59
+ import fs from 'node:fs'
60
+ import path from 'node:path'
61
+ import { fileURLToPath } from 'node:url'
62
+
63
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
64
+ const NATIVE_SEAMS_DIR = path.resolve(HERE, '../../compat/src/stubs/native-seams')
65
+
66
+ /** resolve a stub path under `packages/compat/src/stubs/native-seams/`.
67
+ * resolves to `.tsx` when only that exists (JSX-bearing seams like
68
+ * expo-image, react-native-webview). preferred order: `.ts`, `.tsx`. */
69
+ export function seamPath(basename: string): string {
70
+ const ts = path.resolve(NATIVE_SEAMS_DIR, `${basename}.ts`)
71
+ if (fs.existsSync(ts)) return ts
72
+ const tsx = path.resolve(NATIVE_SEAMS_DIR, `${basename}.tsx`)
73
+ if (fs.existsSync(tsx)) return tsx
74
+ // fall through to .ts so vite's "Could not load" error names the .ts
75
+ // path the author probably intended to create, instead of silently
76
+ // pointing at something that doesn't exist with no extension.
77
+ return ts
78
+ }
79
+
80
+ /**
81
+ * the manifest. agents append entries here during the native-seam migration
82
+ * (plans/sootsim/compat/compat-stubs-native-seam-audit.md). keep alphabetical by `pkg` to
83
+ * minimise merge conflicts.
84
+ */
85
+ export const NATIVE_SEAM_MANIFEST: NativeSeamEntry[] = [
86
+ // entries land here during the Phase 1 migration. example shape:
87
+ //
88
+ // {
89
+ // pkg: 'expo-notifications',
90
+ // seamBasenames: [
91
+ // 'NotificationsHandler',
92
+ // 'NotificationsEmitter',
93
+ // 'NotificationScheduler',
94
+ // 'NotificationCategoriesService',
95
+ // 'ServerRegistrationModule',
96
+ // ],
97
+ // target: seamPath('expo-notifications'),
98
+ // notes: 'route through engine NotificationBus',
99
+ // },
100
+ {
101
+ pkg: '@metamask/react-native-search-api',
102
+ // upstream is single-file `index.js`; no sub-imports inside the package
103
+ // to redirect at the basename layer. listed here for documentation parity
104
+ // — the bundle-loader's stub-registry routes the package-entry directly to
105
+ // `stubs/metamask-react-native-search-api.ts`, which pairs the upstream
106
+ // JS wrapper with the native seam below.
107
+ seamBasenames: ['index'],
108
+ target: seamPath('metamask-react-native-search-api'),
109
+ notes:
110
+ 'iOS CoreSpotlight + NSUserActivity native seam; pure-JS wrapper class lives at stubs/metamask-react-native-search-api.ts',
111
+ },
112
+ {
113
+ pkg: '@ua/react-native-airship',
114
+ seamBasenames: [
115
+ // src/NativeRNAirship.ts — the RNAirship TurboModule
116
+ 'NativeRNAirship',
117
+ // codegen host components (no native backing in sootsim)
118
+ 'RNAirshipEmbeddedViewNativeComponent',
119
+ 'RNAirshipMessageViewNativeComponent',
120
+ ],
121
+ target: seamPath('react-native-airship'),
122
+ notes:
123
+ 'push-notification SAAS — noop the SAAS layer, route banners through NotificationBus',
124
+ },
125
+ {
126
+ pkg: 'react-native-background-fetch',
127
+ // src/NativeBackgroundFetch.ts — TurboModuleRegistry.getEnforcing('RNBackgroundFetch')
128
+ seamBasenames: ['NativeBackgroundFetch'],
129
+ target: seamPath('react-native-background-fetch'),
130
+ notes:
131
+ 'iOS BGTaskScheduler / Android WorkManager TurboModule — no real scheduler in browser; acknowledge registrations, never fire periodic events',
132
+ },
133
+ {
134
+ pkg: 'expo-apple-authentication',
135
+ // src/ExpoAppleAuthenticationButton.ts is the package's native-view
136
+ // boundary. direct-compiled apps do not run the fetched-bundle lazy seam
137
+ // loader, so route that boundary to the same view manager implementation.
138
+ seamBasenames: ['ExpoAppleAuthenticationButton'],
139
+ target: seamPath('expo-apple-authentication-view'),
140
+ notes:
141
+ 'ASAuthorizationAppleIDButton native view — render the system button surface while upstream AppleAuthenticationButton.tsx remains unchanged',
142
+ },
143
+ {
144
+ pkg: 'expo-asset',
145
+ // src/ExpoAsset.ts — `requireNativeModule('ExpoAsset')`; only
146
+ // `downloadAsync` is on the native surface. all the rest (`Asset`,
147
+ // `AssetHooks`, `AssetSourceResolver`, ...) is pure JS that composes on
148
+ // top.
149
+ seamBasenames: ['ExpoAsset'],
150
+ target: seamPath('expo-asset'),
151
+ notes:
152
+ 'expo-modules-core requireNativeModule(ExpoAsset) — only downloadAsync; mirror upstream web build (return remote URL)',
153
+ },
154
+ {
155
+ pkg: '@bam.tech/react-native-image-resizer',
156
+ // upstream is not shipped to most guest bundles via node_modules — it
157
+ // arrives through the bundle-loader stub-registry which routes the
158
+ // package entry to `stubs/bam-react-native-image-resizer.ts`. listed here
159
+ // for documentation parity; vite-path redirect would key on
160
+ // `NativeImageResizer`.
161
+ seamBasenames: ['NativeImageResizer'],
162
+ target: seamPath('bam-react-native-image-resizer'),
163
+ notes:
164
+ 'ImageResizer TurboModule — browser-side canvas resize implementing the iOS contain/cover/stretch + onlyScaleDown semantics from ios/ImageResizer.mm',
165
+ },
166
+ {
167
+ pkg: 'react-native-adjust',
168
+ // upstream's `index.js` reads `NativeModules.Adjust` and
169
+ // `NativeModules.AdjustEventEmitter` directly — there is no NativeAdjust
170
+ // spec file. listed for documentation parity; the actual seam delivery is
171
+ // through the bundle-loader stub-registry routing the package entry to
172
+ // `stubs/react-native-adjust.ts`, which imports the native seam from this
173
+ // target.
174
+ seamBasenames: ['index'],
175
+ target: seamPath('react-native-adjust'),
176
+ notes:
177
+ 'Adjust attribution SAAS — noop native side (Adjust + AdjustEventEmitter NativeModules), pure-JS classes (AdjustConfig/AdjustEvent/...) live in stubs/react-native-adjust.ts',
178
+ },
179
+ {
180
+ pkg: 'expo-clipboard',
181
+ // src/ExpoClipboard.ts — `requireNativeModule<ExpoClipboardModule>('ExpoClipboard')`.
182
+ // upstream also ships `ExpoClipboard.web.ts` (which we mirror); the
183
+ // redirect catches both `.ts` and `.web.ts` since basenames match.
184
+ seamBasenames: ['ExpoClipboard'],
185
+ target: seamPath('expo-clipboard'),
186
+ notes:
187
+ 'expo-modules-core requireNativeModule(ExpoClipboard) — real navigator.clipboard text + ClipboardItem image; mirrors upstream ExpoClipboard.web.ts',
188
+ },
189
+ {
190
+ pkg: 'expo-font',
191
+ // src/ExpoFontLoader.ts — `requireNativeModule<ExpoFontLoaderModule>('ExpoFontLoader')`.
192
+ // we mirror upstream's web variant `ExpoFontLoader.web.ts` (FontFace API
193
+ // + managed <style id="expo-generated-fonts">).
194
+ seamBasenames: ['ExpoFontLoader'],
195
+ target: seamPath('expo-font'),
196
+ notes:
197
+ 'expo-modules-core requireNativeModule(ExpoFontLoader) — FontFace API registration; mirrors upstream ExpoFontLoader.web.ts',
198
+ },
199
+ {
200
+ pkg: '@react-native-async-storage/async-storage',
201
+ // src/NativeAsyncStorageModule.ts — TurboModule spec for `RNCAsyncStorage`.
202
+ // src/RCTAsyncStorage.ts — legacy `NativeModules['RNCAsyncStorage']` resolver.
203
+ // both basenames redirect to the same seam; upstream's AsyncStorage.ts
204
+ // imports default from either depending on bridgeless vs paper.
205
+ seamBasenames: ['NativeAsyncStorageModule', 'RCTAsyncStorage'],
206
+ target: seamPath('async-storage'),
207
+ notes:
208
+ 'RNCAsyncStorage TurboModule — localStorage-backed; preserves multiMerge JSON deep-merge semantics from upstream iOS module',
209
+ },
210
+ // expo-calendar and expo-contacts deliberately have NO entry here. expo
211
+ // renamed their native module ids across majors (`ExpoCalendar` →
212
+ // `CalendarNext`, `ExpoContacts` → `ExpoContactsNext`) and kept the old
213
+ // ids serving the `/legacy` subpath, so one package now asks for two
214
+ // different native modules depending on which entry point you import.
215
+ // basename matching cannot tell `src/ExpoCalendar.ts` from
216
+ // `src/legacy/ExpoCalendar.ts`, so a redirect here forces the legacy API
217
+ // onto the class-based seam, whose methods throw.
218
+ //
219
+ // registering by module NAME instead is what keeps old and new package
220
+ // versions both working: ../../compat/src/stubs/expo-{calendar,contacts}-register.ts
221
+ // installs every id either version asks for, and upstream's own
222
+ // `requireNativeModule` picks the right one per entry point. a file
223
+ // redirect can only match one layout, so it breaks the versions it was
224
+ // not written for.
225
+ {
226
+ pkg: 'expo-media-library',
227
+ // src/ExpoMediaLibrary.ts — `requireNativeModule('ExpoMediaLibrary')`.
228
+ // seam mirrors upstream's `ExpoMediaLibrary.web.ts` (refuse permissions
229
+ // + expose MediaType/SortBy constants).
230
+ seamBasenames: ['ExpoMediaLibrary'],
231
+ target: seamPath('expo-media-library'),
232
+ notes:
233
+ 'expo-modules-core requireNativeModule(ExpoMediaLibrary) — refuses permissions + exposes MediaType/SortBy enums per upstream ExpoMediaLibrary.web.ts',
234
+ },
235
+ {
236
+ pkg: 'expo-file-system',
237
+ // src/ExpoFileSystem.ts — `requireNativeModule('ExpoFileSystem')`. seam
238
+ // mirrors upstream's `ExpoFileSystem.web.ts` (warn-not-supported stubs).
239
+ seamBasenames: ['ExpoFileSystem'],
240
+ target: seamPath('expo-file-system'),
241
+ notes:
242
+ 'expo-modules-core requireNativeModule(ExpoFileSystem) — warn-not-supported stubs per upstream ExpoFileSystem.web.ts; no general browser fs access',
243
+ },
244
+ {
245
+ pkg: 'expo-image-picker',
246
+ // src/ExponentImagePicker.ts — `requireNativeModule('ExponentImagePicker')`.
247
+ // seam routes selection requests through sootsim's engine-owned image
248
+ // picker (`SootSim.bridges.imagePicker.pickFromLibrary`) so the same
249
+ // canvas-rendered iOS picker handles every image-source library.
250
+ seamBasenames: ['ExponentImagePicker'],
251
+ target: seamPath('expo-image-picker'),
252
+ notes:
253
+ "expo-modules-core requireNativeModule(ExponentImagePicker) — routes through engine SootSim.bridges.imagePicker (shell-worker ImagePickerView) instead of upstream web build's DOM <input type=file>",
254
+ },
255
+ {
256
+ pkg: 'expo-camera',
257
+ // upstream splits the native surface across two files:
258
+ // - src/ExpoCamera.ts → requireNativeViewManager('ExpoCamera')
259
+ // - src/ExpoCameraManager.ts → requireNativeModule('ExpoCamera')
260
+ // each basename redirects to a different seam — the view component vs.
261
+ // the imperative module. live preview + capture route through the same
262
+ // engine `camera-stream` module the built-in sootsim Camera app uses,
263
+ // so guest <CameraView> sees the same feed as the OS camera app.
264
+ seamBasenames: ['ExpoCamera'],
265
+ target: seamPath('expo-camera'),
266
+ notes:
267
+ 'expo-camera native view manager — host component backed by engine camera-stream (same getUserMedia pipeline as the built-in CameraApp); exposes CameraViewRef methods (takePicture/...) via useImperativeHandle',
268
+ },
269
+ {
270
+ pkg: 'expo-camera',
271
+ seamBasenames: ['ExpoCameraManager'],
272
+ target: seamPath('expo-camera-manager'),
273
+ notes:
274
+ 'expo-camera native module — permissions/feature-gates/codecs/Type/FlashMode constants; barcode scanner is noop (no engine scanner integration)',
275
+ },
276
+ {
277
+ pkg: 'expo-image',
278
+ // upstream splits the native surface across two files:
279
+ // - src/ExpoImage.tsx → requireNativeViewManager('ExpoImage')
280
+ // - src/ImageModule.ts → requireNativeModule('ExpoImage')
281
+ // the view-component seam translates expo-image's prop shape
282
+ // (contentFit, tintColor, source) onto sootsim's existing
283
+ // <sootsim-image> host. no new canvas component is needed — sootsim's
284
+ // image-loader + canvaskit-renderer already handle resizeMode,
285
+ // tintColor, and onLoad/onError lifecycle.
286
+ seamBasenames: ['ExpoImage'],
287
+ target: seamPath('expo-image'),
288
+ notes:
289
+ 'expo-image native view — thin prop translator onto sootsim-image host (contentFit→resizeMode, tintColor passthrough); upstream Image.tsx + ImageBackground + useImage resolve from node_modules unchanged',
290
+ },
291
+ {
292
+ pkg: 'expo-image',
293
+ seamBasenames: ['ImageModule'],
294
+ target: seamPath('expo-image-module'),
295
+ notes:
296
+ 'expo-image native module — prefetch/loadAsync route through sootsim image-loader; cache controls noop (browser owns HTTP cache); blurhash encode not yet implemented',
297
+ },
298
+ {
299
+ pkg: '@notifee/react-native',
300
+ // src/NotifeeNativeModule.ts — the base class NotifeeApiModule extends.
301
+ // upstream also ships .web.ts (returns `{}` from `native`); both
302
+ // basenames redirect to the same seam — a real implementation that
303
+ // routes `this.native.*` calls through the engine NotificationBus.
304
+ seamBasenames: ['NotifeeNativeModule'],
305
+ target: seamPath('notifee'),
306
+ notes:
307
+ 'Notifee NotifeeApiModule base class — routes display/cancel/channel/badge calls through engine NotificationBus; tap events fan out via the shared NotifeeJSEventEmitter so onForegroundEvent listeners fire',
308
+ },
309
+ {
310
+ pkg: 'expo-notifications',
311
+ // five native module spec files, each `requireNativeModule(...)`-shaped.
312
+ // every one redirects to the same `expo-notifications` seam — see
313
+ // `stubs/native-seams/expo-notifications.ts` for the union surface.
314
+ seamBasenames: [
315
+ // src/NotificationsHandlerModule.ts — presentation policy
316
+ 'NotificationsHandlerModule',
317
+ // src/NotificationsEmitterModule.ts — last-tap response + listener
318
+ 'NotificationsEmitterModule',
319
+ // src/NotificationScheduler.ts — schedule / cancel
320
+ 'NotificationScheduler',
321
+ // src/BadgeModule.ts — app badge count
322
+ 'BadgeModule',
323
+ // src/ServerRegistrationModule.ts — expo push registration
324
+ 'ServerRegistrationModule',
325
+ ],
326
+ target: seamPath('expo-notifications'),
327
+ notes:
328
+ 'expo-notifications native modules — routes scheduling/badge/installation-id through engine NotificationBus; tap callbacks fan out via the bus event emitters',
329
+ },
330
+ {
331
+ pkg: 'react-native-enriched-markdown',
332
+ seamBasenames: [
333
+ 'EnrichedMarkdownTextNativeComponent',
334
+ 'EnrichedMarkdownNativeComponent',
335
+ 'EnrichedMarkdownTextInputNativeComponent',
336
+ ],
337
+ target: seamPath('react-native-enriched-markdown'),
338
+ notes:
339
+ 'generated Fabric markdown host components; upstream Streamdown/enriched-markdown JS runs unchanged',
340
+ },
341
+ ]
package/src/plist.ts ADDED
@@ -0,0 +1,27 @@
1
+ import plistModule from '@expo/plist'
2
+
3
+ function isRecord(value: unknown): value is Record<string, unknown> {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
5
+ }
6
+
7
+ function resolvePlistParser(value: unknown): ((source: string) => unknown) | null {
8
+ if (!isRecord(value)) return null
9
+ const direct = value.parse
10
+ if (typeof direct === 'function') {
11
+ return (source) => Reflect.apply(direct, value, [source])
12
+ }
13
+ const nested = value.default
14
+ if (!isRecord(nested)) return null
15
+ const nestedParse = nested.parse
16
+ return typeof nestedParse === 'function'
17
+ ? (source) => Reflect.apply(nestedParse, nested, [source])
18
+ : null
19
+ }
20
+
21
+ // bun consumes @expo/plist's default export directly, while the compiled commonjs
22
+ // scanner receives the package namespace with that export nested under default.
23
+ const parsePlist = resolvePlistParser(plistModule)
24
+
25
+ export function parsePlistSource(source: string): unknown {
26
+ return parsePlist?.(source) ?? null
27
+ }