peachsim 0.1.939 → 0.1.1399

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 (396) hide show
  1. package/README.md +28 -22
  2. package/cli/app-catalog.ts +29 -14
  3. package/cli/app-fonts.ts +5 -3
  4. package/cli/bridge-flow-runner.ts +40 -29
  5. package/cli/cloud-client.ts +57 -21
  6. package/cli/cloud-test-tunnel.ts +12 -18
  7. package/cli/commands/agent.ts +1 -1
  8. package/cli/commands/app-fonts.ts +3 -3
  9. package/cli/commands/assert.ts +8 -8
  10. package/cli/commands/auth.ts +8 -3
  11. package/cli/commands/box/bundle-plane.ts +13 -1
  12. package/cli/commands/box.ts +16 -5
  13. package/cli/commands/compat.ts +1 -1
  14. package/cli/commands/config.ts +2 -2
  15. package/cli/commands/control.ts +125 -71
  16. package/cli/commands/cpu-profile.ts +45 -1
  17. package/cli/commands/daemon.ts +131 -33
  18. package/cli/commands/debug.ts +24 -17
  19. package/cli/commands/desktop.ts +7 -1
  20. package/cli/commands/detect.ts +101 -0
  21. package/cli/commands/detox.ts +6 -6
  22. package/cli/commands/device.ts +12 -12
  23. package/cli/commands/electron.ts +32 -14
  24. package/cli/commands/flow.ts +49 -41
  25. package/cli/commands/guest-host.ts +39 -1
  26. package/cli/commands/inspect/core.ts +14 -8
  27. package/cli/commands/inspect/list.ts +1 -1
  28. package/cli/commands/inspect/settle.ts +5 -3
  29. package/cli/commands/inspect/settling.ts +229 -44
  30. package/cli/commands/inspect/wait-idle.ts +3 -2
  31. package/cli/commands/inspect.ts +105 -93
  32. package/cli/commands/install-cli.ts +36 -15
  33. package/cli/commands/install-desktop.ts +68 -195
  34. package/cli/commands/kotlin.ts +6 -5
  35. package/cli/commands/login.ts +5 -5
  36. package/cli/commands/maestro-compare.ts +554 -0
  37. package/cli/commands/maestro-generate.ts +6 -3
  38. package/cli/commands/maestro.ts +22 -0
  39. package/cli/commands/mode.ts +2 -2
  40. package/cli/commands/no-bridge-hint.ts +6 -2
  41. package/cli/commands/platform.ts +35 -6
  42. package/cli/commands/preview.ts +6 -0
  43. package/cli/commands/project-report.ts +10 -12
  44. package/cli/commands/record.ts +37 -37
  45. package/cli/commands/runtime.ts +9 -9
  46. package/cli/commands/screenshot-layers.ts +4 -4
  47. package/cli/commands/screenshot-mode.ts +2 -2
  48. package/cli/commands/screenshot.ts +8 -8
  49. package/cli/commands/screenshots-capture.ts +2 -2
  50. package/cli/commands/serve.ts +9 -12
  51. package/cli/commands/setup.ts +29 -14
  52. package/cli/commands/shell-boolean-mode.ts +2 -2
  53. package/cli/commands/show-touches-mode.ts +2 -2
  54. package/cli/commands/skills.ts +11 -11
  55. package/cli/commands/swift.ts +29 -11
  56. package/cli/commands/telemetry.ts +2 -2
  57. package/cli/commands/test.ts +5 -0
  58. package/cli/commands/three-mode.ts +3 -3
  59. package/cli/commands/timeline.ts +4 -4
  60. package/cli/commands/upgrade.ts +2 -2
  61. package/cli/commands/upload.ts +15 -15
  62. package/cli/commands/what-happened.ts +2 -2
  63. package/cli/current-sim.ts +6 -3
  64. package/cli/desktop-companion.ts +55 -149
  65. package/cli/dev-build-freshness.ts +2 -2
  66. package/cli/drivers/desktop.ts +17 -9
  67. package/cli/drivers/electron.ts +1 -2
  68. package/cli/drivers/playwright-sim-host.ts +11 -2
  69. package/cli/drivers/playwright.ts +35 -2
  70. package/cli/drivers/types.ts +8 -0
  71. package/cli/maestro-compare.ts +670 -0
  72. package/cli/outbound-endpoints.ts +46 -8
  73. package/cli/recording-access.ts +6 -6
  74. package/cli/run-registry.ts +1 -1
  75. package/cli/runtime-compat.ts +5 -1
  76. package/cli/send-to-box.ts +3 -3
  77. package/cli/shell-init.ts +2 -1
  78. package/cli/ws-bridge.ts +9 -5
  79. package/detox/element-types.ts +1 -0
  80. package/detox/gestures.ts +3 -3
  81. package/detox/index.ts +67 -38
  82. package/detox/navigation-lifecycle.ts +9 -6
  83. package/detox/proof-frame.cjs +6 -4
  84. package/dist-lib/agent-daemon-client.cjs +1 -1
  85. package/dist-lib/agent-events.cjs +1 -1
  86. package/dist-lib/attached-projects.cjs +1 -1
  87. package/dist-lib/auth/shared-session.cjs +1 -1
  88. package/dist-lib/backend-origin.cjs +1 -1
  89. package/dist-lib/beta.cjs +1 -1
  90. package/dist-lib/beta.mjs +1 -1
  91. package/dist-lib/bridge-constants.cjs +1 -1
  92. package/dist-lib/bridge-contract-input.cjs +42 -17
  93. package/dist-lib/bridge-contract-input.mjs +41 -16
  94. package/dist-lib/bridge-contract.cjs +1 -4
  95. package/dist-lib/bridge-contract.mjs +1 -3
  96. package/dist-lib/bundle-channel.cjs +2918 -550
  97. package/dist-lib/capture-contract.cjs +65 -24
  98. package/dist-lib/capture-contract.mjs +63 -24
  99. package/dist-lib/cloud-contract.cjs +2 -2
  100. package/dist-lib/cloud-contract.mjs +2 -2
  101. package/dist-lib/cloud-test-args.cjs +1 -1
  102. package/dist-lib/cloud-test-args.mjs +1 -1
  103. package/dist-lib/cloud.cjs +3 -3
  104. package/dist-lib/cloud.mjs +3 -3
  105. package/dist-lib/config.cjs +15 -5
  106. package/dist-lib/config.mjs +154 -0
  107. package/dist-lib/detox/index.cjs +495 -139
  108. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  109. package/dist-lib/expo-swift-bundle.cjs +1168 -238
  110. package/dist-lib/expo-swift-metadata.cjs +34 -23
  111. package/dist-lib/expo-swift-metadata.mjs +32 -23
  112. package/dist-lib/home-paths.cjs +2 -2
  113. package/dist-lib/host/bridge-host.cjs +2938 -654
  114. package/dist-lib/host/fetch-proxy-handler.cjs +14 -1
  115. package/dist-lib/host/fetch-proxy-overrides.cjs +7 -1
  116. package/dist-lib/host/fetch-proxy-overrides.mjs +5 -1
  117. package/dist-lib/host/replacement-module-handler.cjs +2 -2
  118. package/dist-lib/host/websocket-proxy.cjs +1 -1
  119. package/dist-lib/index.cjs +999 -353
  120. package/dist-lib/jump-to-source-babel.cjs +1 -1
  121. package/dist-lib/jump-to-source-native.cjs +1 -1
  122. package/dist-lib/kotlin-host.cjs +332 -19
  123. package/dist-lib/menu.cjs +1 -1
  124. package/dist-lib/menu.mjs +1 -1
  125. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  126. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  127. package/dist-lib/metro-production-bundle.cjs +7 -7
  128. package/dist-lib/metro-production-bundle.mjs +5 -5
  129. package/dist-lib/metro.cjs +688 -250
  130. package/dist-lib/native-app-identity-project.cjs +2647 -0
  131. package/dist-lib/native-app-identity-project.mjs +1257 -0
  132. package/dist-lib/native-app-identity.cjs +325 -25
  133. package/dist-lib/native-app-identity.mjs +323 -25
  134. package/dist-lib/native-firebase-metadata.cjs +1 -1
  135. package/dist-lib/native-firebase-metadata.mjs +1 -1
  136. package/dist-lib/plist.cjs +1 -1
  137. package/dist-lib/plist.mjs +1 -1
  138. package/dist-lib/profiles.cjs +1 -1
  139. package/dist-lib/public-brand.cjs +1 -1
  140. package/dist-lib/react-native-host-modules.cjs +5 -1
  141. package/dist-lib/react-native-host-modules.mjs +5 -1
  142. package/dist-lib/scripts/dev-server-scanner.cjs +57 -32
  143. package/dist-lib/sdk.cjs +231 -51
  144. package/dist-lib/sdk.mjs +231 -51
  145. package/dist-lib/skills.cjs +4107 -1374
  146. package/dist-lib/swift-compile.cjs +290 -30
  147. package/dist-lib/swift-compile.mjs +290 -30
  148. package/dist-lib/swift-package.cjs +143 -12
  149. package/dist-lib/swift-package.mjs +137 -11
  150. package/dist-lib/swift-submit.cjs +47 -5
  151. package/dist-lib/swift-submit.mjs +42 -5
  152. package/dist-lib/swift-toolchain.cjs +7 -2
  153. package/dist-lib/swift.cjs +1 -1
  154. package/dist-lib/vite-swift.cjs +1392 -458
  155. package/dist-lib/vite.cjs +13 -17
  156. package/dist-lib/worklet-executable.cjs +248 -33
  157. package/dist-lib/worklet-executable.mjs +248 -33
  158. package/install.sh +10 -1
  159. package/kotlin/{AppSources → AndroidViewSources}/android/content/res/Resources.kt +9 -9
  160. package/kotlin/AndroidViewSources/android/graphics/Canvas.kt +20 -0
  161. package/kotlin/AndroidViewSources/android/view/View.kt +211 -0
  162. package/kotlin/AndroidViewSources/android/widget/FrameLayout.kt +61 -0
  163. package/kotlin/AndroidViewSources/android/widget/LinearLayout.kt +49 -0
  164. package/kotlin/AppSources/android/view/Window.kt +36 -0
  165. package/kotlin/AppSources/android/view/WindowInsets.kt +9 -0
  166. package/kotlin/AppSources/android/view/WindowInsetsController.kt +5 -0
  167. package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +48 -0
  168. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +20 -5
  169. package/kotlin/AppSources/androidx/compose/foundation/layout/WindowInsets.kt +15 -0
  170. package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +2 -1
  171. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +42 -50
  172. package/kotlin/ComposeSources/PeachComposeHost.kt +200 -84
  173. package/kotlin/ExpoComposeSources/PeachAndroidView.kt +87 -0
  174. package/kotlin/ExpoComposeSources/PeachExpoViewMount.kt +24 -0
  175. package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +28 -0
  176. package/kotlin/ExpoComposeSources/expo/modules/kotlin/views/ExpoComposeView.kt +70 -0
  177. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +513 -0
  178. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +173 -0
  179. package/kotlin/ExpoSources/expo/modules/kotlin/AppContext.kt +20 -0
  180. package/kotlin/ExpoSources/expo/modules/kotlin/Promise.kt +19 -0
  181. package/kotlin/ExpoSources/expo/modules/kotlin/classcomponent/ClassComponentBuilder.kt +198 -0
  182. package/kotlin/ExpoSources/expo/modules/kotlin/exception/CodedException.kt +126 -0
  183. package/kotlin/ExpoSources/expo/modules/kotlin/functions/Functions.kt +132 -0
  184. package/kotlin/ExpoSources/expo/modules/kotlin/jni/JavaScriptTypedArray.kt +81 -0
  185. package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +98 -0
  186. package/kotlin/ExpoSources/expo/modules/kotlin/objects/ObjectDefinitionBuilder.kt +236 -0
  187. package/kotlin/ExpoSources/expo/modules/kotlin/records/Record.kt +12 -0
  188. package/kotlin/ExpoSources/expo/modules/kotlin/runtime/Runtime.kt +9 -0
  189. package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedObject.kt +50 -0
  190. package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedObjectRegistry.kt +57 -0
  191. package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedRef.kt +19 -0
  192. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/ConcreteTypedArrays.kt +158 -0
  193. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/GenericTypedArray.kt +8 -0
  194. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/TypedArray.kt +29 -0
  195. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/TypedArrayIterator.kt +9 -0
  196. package/kotlin/ExpoSources/expo/modules/kotlin/types/AnyType.kt +155 -0
  197. package/kotlin/ExpoSources/expo/modules/kotlin/types/Either.kt +91 -0
  198. package/kotlin/ExpoSources/expo/modules/kotlin/types/Enumerable.kt +6 -0
  199. package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +54 -0
  200. package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +23 -0
  201. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +71 -0
  202. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +35 -0
  203. package/kotlin/README.md +13 -3
  204. package/kotlin/Sources/android/os/Build.kt +17 -0
  205. package/kotlin/Sources/android/os/Handler.kt +35 -0
  206. package/kotlin/Sources/android/os/Looper.kt +17 -0
  207. package/kotlin/Sources/android/os/MessageQueue.kt +56 -0
  208. package/kotlin/Sources/android/os/SystemClock.kt +14 -0
  209. package/kotlin/Sources/android/util/Log.kt +64 -0
  210. package/kotlin/Sources/java/security/MessageDigest.kt +114 -0
  211. package/kotlin/Sources/kotlin/text/StringFormat.kt +9 -0
  212. package/kotlin/Sources/org/json/JSON.kt +44 -0
  213. package/kotlin/fixtures/counter/Counter.d.kt.ts +1 -0
  214. package/kotlin/fixtures/counter/Counter.kt +10 -3
  215. package/kotlin/fixtures/counter/CounterApp.native.tsx +1 -1
  216. package/kotlin/fixtures/counter/vite.config.ts +2 -2
  217. package/kotlin/fixtures/headless-counter/Headless.kt +7 -6
  218. package/kotlin/fixtures/import-animation/Animation.d.kt.ts +7 -0
  219. package/kotlin/fixtures/import-animation/Animation.kt +80 -0
  220. package/kotlin/fixtures/import-animation/ImportAnimation.native.tsx +13 -0
  221. package/kotlin/fixtures/import-animation/README.md +24 -0
  222. package/kotlin/fixtures/import-animation/app/_layout.tsx +4 -0
  223. package/kotlin/fixtures/import-animation/app/index+spa.tsx +1 -0
  224. package/kotlin/fixtures/import-animation/app/routes.d.ts +18 -0
  225. package/kotlin/fixtures/import-animation/package.json +23 -0
  226. package/kotlin/fixtures/import-animation/tsconfig.json +20 -0
  227. package/kotlin/fixtures/import-animation/vite.config.ts +20 -0
  228. package/kotlin/fixtures/import-editor/Editor.d.kt.ts +7 -0
  229. package/kotlin/fixtures/import-editor/Editor.kt +59 -0
  230. package/kotlin/fixtures/import-editor/ImportEditor.native.tsx +23 -0
  231. package/kotlin/fixtures/import-editor/README.md +27 -0
  232. package/kotlin/fixtures/import-editor/app/_layout.tsx +4 -0
  233. package/kotlin/fixtures/import-editor/app/index+spa.tsx +1 -0
  234. package/kotlin/fixtures/import-editor/package.json +23 -0
  235. package/kotlin/fixtures/import-editor/tsconfig.json +20 -0
  236. package/kotlin/fixtures/import-editor/vite.config.ts +20 -0
  237. package/kotlin/fixtures/import-effects/Effects.d.kt.ts +7 -0
  238. package/kotlin/fixtures/import-effects/Effects.kt +87 -0
  239. package/kotlin/fixtures/import-effects/ImportEffects.native.tsx +13 -0
  240. package/kotlin/fixtures/import-effects/README.md +19 -0
  241. package/kotlin/fixtures/import-effects/app/_layout.tsx +4 -0
  242. package/kotlin/fixtures/import-effects/app/index+spa.tsx +1 -0
  243. package/kotlin/fixtures/import-effects/package.json +23 -0
  244. package/kotlin/fixtures/import-effects/tsconfig.json +20 -0
  245. package/kotlin/fixtures/import-effects/vite.config.ts +20 -0
  246. package/kotlin/fixtures/import-layout/ImportLayout.native.tsx +13 -0
  247. package/kotlin/fixtures/import-layout/Layout.d.kt.ts +7 -0
  248. package/kotlin/fixtures/import-layout/Layout.kt +72 -0
  249. package/kotlin/fixtures/import-layout/README.md +24 -0
  250. package/kotlin/fixtures/import-layout/app/_layout.tsx +4 -0
  251. package/kotlin/fixtures/import-layout/app/index+spa.tsx +1 -0
  252. package/kotlin/fixtures/import-layout/package.json +23 -0
  253. package/kotlin/fixtures/import-layout/tsconfig.json +20 -0
  254. package/kotlin/fixtures/import-layout/vite.config.ts +20 -0
  255. package/kotlin/fixtures/import-navigation/ImportNavigation.native.tsx +18 -0
  256. package/kotlin/fixtures/import-navigation/Navigation.d.kt.ts +7 -0
  257. package/kotlin/fixtures/import-navigation/Navigation.kt +104 -0
  258. package/kotlin/fixtures/import-navigation/README.md +31 -0
  259. package/kotlin/fixtures/import-navigation/app/_layout.tsx +4 -0
  260. package/kotlin/fixtures/import-navigation/app/index+spa.tsx +1 -0
  261. package/kotlin/fixtures/import-navigation/package.json +23 -0
  262. package/kotlin/fixtures/import-navigation/tsconfig.json +20 -0
  263. package/kotlin/fixtures/import-navigation/vite.config.ts +20 -0
  264. package/kotlin/fixtures/import-overlays/ImportOverlays.native.tsx +29 -0
  265. package/kotlin/fixtures/import-overlays/Overlays.d.kt.ts +7 -0
  266. package/kotlin/fixtures/import-overlays/Overlays.kt +93 -0
  267. package/kotlin/fixtures/import-overlays/README.md +34 -0
  268. package/kotlin/fixtures/import-overlays/app/_layout.tsx +4 -0
  269. package/kotlin/fixtures/import-overlays/app/index+spa.tsx +1 -0
  270. package/kotlin/fixtures/import-overlays/package.json +23 -0
  271. package/kotlin/fixtures/import-overlays/tsconfig.json +20 -0
  272. package/kotlin/fixtures/import-overlays/vite.config.ts +20 -0
  273. package/kotlin/fixtures/import-scroll/ImportScroll.native.tsx +13 -0
  274. package/kotlin/fixtures/import-scroll/README.md +23 -0
  275. package/kotlin/fixtures/import-scroll/Scroll.d.kt.ts +7 -0
  276. package/kotlin/fixtures/import-scroll/Scroll.kt +91 -0
  277. package/kotlin/fixtures/import-scroll/app/_layout.tsx +4 -0
  278. package/kotlin/fixtures/import-scroll/app/index+spa.tsx +1 -0
  279. package/kotlin/fixtures/import-scroll/package.json +23 -0
  280. package/kotlin/fixtures/import-scroll/tsconfig.json +20 -0
  281. package/kotlin/fixtures/import-scroll/vite.config.ts +20 -0
  282. package/kotlin/fixtures/import-selection/ImportSelection.native.tsx +44 -0
  283. package/kotlin/fixtures/import-selection/README.md +32 -0
  284. package/kotlin/fixtures/import-selection/Selection.d.kt.ts +8 -0
  285. package/kotlin/fixtures/import-selection/Selection.kt +78 -0
  286. package/kotlin/fixtures/import-selection/app/_layout.tsx +4 -0
  287. package/kotlin/fixtures/import-selection/app/index+spa.tsx +1 -0
  288. package/kotlin/fixtures/import-selection/package.json +23 -0
  289. package/kotlin/fixtures/import-selection/tsconfig.json +20 -0
  290. package/kotlin/fixtures/import-selection/vite.config.ts +20 -0
  291. package/kotlin/fixtures/import-surfaces/ImportSurfaces.native.tsx +13 -0
  292. package/kotlin/fixtures/import-surfaces/README.md +22 -0
  293. package/kotlin/fixtures/import-surfaces/Surfaces.d.kt.ts +7 -0
  294. package/kotlin/fixtures/import-surfaces/Surfaces.kt +64 -0
  295. package/kotlin/fixtures/import-surfaces/app/_layout.tsx +4 -0
  296. package/kotlin/fixtures/import-surfaces/app/index+spa.tsx +1 -0
  297. package/kotlin/fixtures/import-surfaces/package.json +23 -0
  298. package/kotlin/fixtures/import-surfaces/tsconfig.json +20 -0
  299. package/kotlin/fixtures/import-surfaces/vite.config.ts +20 -0
  300. package/kotlin/fixtures/import-values/ImportValues.native.tsx +35 -0
  301. package/kotlin/fixtures/import-values/README.md +23 -0
  302. package/kotlin/fixtures/import-values/Values.d.kt.ts +11 -0
  303. package/kotlin/fixtures/import-values/Values.kt +66 -0
  304. package/kotlin/fixtures/import-values/app/_layout.tsx +4 -0
  305. package/kotlin/fixtures/import-values/app/index+spa.tsx +1 -0
  306. package/kotlin/fixtures/import-values/package.json +23 -0
  307. package/kotlin/fixtures/import-values/tsconfig.json +20 -0
  308. package/kotlin/fixtures/import-values/vite.config.ts +20 -0
  309. package/kotlin/fixtures/kotlin-app-variants/BarBehavior.kt +38 -0
  310. package/kotlin/fixtures/kotlin-app-variants/BarVisibility.kt +36 -0
  311. package/kotlin/fixtures/kotlin-app-variants/Ime.kt +3 -1
  312. package/kotlin/fixtures/kotlin-app-variants/TransientInsets.kt +50 -0
  313. package/kotlin/fork/NOTICE +6 -1
  314. package/kotlin/fork/README.md +15 -7
  315. package/kotlin/fork/build.sh +2 -0
  316. package/kotlin/fork/compose-worker-globals.patch +186 -4
  317. package/kotlin/fork/fork.json +2 -2
  318. package/kotlin/fork/skiko-worker-clock.patch +12 -0
  319. package/kotlin/toolchain.json +12 -12
  320. package/kotlin/tools/expo-types.main.kts +115 -0
  321. package/package.json +17 -23
  322. package/scripts/dev-server-scanner.ts +92 -66
  323. package/skills/contrast/SKILL.md +2 -2
  324. package/skills/peach-debug/SKILL.md +1 -1
  325. package/skills/peach-perf/SKILL.md +1 -1
  326. package/skills/peach-setup/SKILL.md +2 -1
  327. package/skills/peach-test/SKILL.md +8 -0
  328. package/src/assetCatalogContract.ts +43 -1
  329. package/src/bridge-contract-input.ts +46 -19
  330. package/src/bridge-contract.ts +0 -1
  331. package/src/bundle-channel.mjs +197 -84
  332. package/src/bundleGlobalFooter.ts +38 -0
  333. package/src/capture-contract.ts +107 -33
  334. package/src/cloud-contract.ts +5 -1
  335. package/src/cloud.ts +5 -2
  336. package/src/config.ts +22 -3
  337. package/src/configured-bundle-producer.ts +1 -1
  338. package/src/desktop-host-delivery.ts +95 -1
  339. package/src/expo-kotlin-bundle.ts +29 -0
  340. package/src/expo-kotlin-host.ts +201 -0
  341. package/src/expo-swift-host.ts +96 -76
  342. package/src/expoKotlinContract.ts +70 -0
  343. package/src/expoSwiftContract.ts +4 -25
  344. package/src/home-paths.ts +7 -4
  345. package/src/host/bridge-host.ts +23 -3
  346. package/src/host/fetch-proxy-handler.ts +13 -0
  347. package/src/host/fetch-proxy-overrides.ts +11 -0
  348. package/src/host/replacement-module-handler.ts +5 -2
  349. package/src/index.ts +3 -1
  350. package/src/kotlin-host.ts +463 -19
  351. package/src/metro-plugin.ts +56 -28
  352. package/src/metro-production-bundle.ts +2 -2
  353. package/src/native-app-identity-project.ts +190 -0
  354. package/src/native-app-identity.ts +398 -34
  355. package/src/native-firebase-config.ts +69 -15
  356. package/src/native-seam-manifest.ts +6 -0
  357. package/src/one-widget-discovery.ts +2 -1
  358. package/src/openstep-plist.ts +107 -0
  359. package/src/react-native-host-modules.ts +4 -0
  360. package/src/runtime-assets.ts +2 -2
  361. package/src/sim-client.ts +2 -2
  362. package/src/swift-binary-modules.ts +3 -1
  363. package/src/swift-compile.ts +286 -38
  364. package/src/swift-distribution-contract.ts +4 -0
  365. package/src/swift-distribution.lock.json +123 -12
  366. package/src/swift-distribution.ts +7 -3
  367. package/src/swift-host.ts +298 -83
  368. package/src/swift-package-manifest.ts +29 -2
  369. package/src/swift-package.ts +180 -10
  370. package/src/swift-packages.ts +4 -0
  371. package/src/swift-standin-emit.ts +4 -1
  372. package/src/swift-standins.ts +274 -15
  373. package/src/swift-submit.ts +62 -7
  374. package/src/swift-toolchain.ts +6 -1
  375. package/src/swiftContract.ts +126 -3
  376. package/src/vite-plugin-one.ts +13 -16
  377. package/src/vite-plugin-swift.ts +4 -5
  378. package/src/vite-plugin.ts +360 -144
  379. package/src/vite.d.ts +1 -1
  380. package/src/worker-system-chunks.ts +221 -0
  381. package/src/worklet-executable.ts +348 -38
  382. package/src/worklets-babel.ts +1 -1
  383. package/src/xcode-project.ts +243 -127
  384. package/dist-lib/agent-identity.cjs +0 -39
  385. package/dist-lib/agent-sessions.cjs +0 -760
  386. package/dist-lib/cli-constants.cjs +0 -32
  387. package/dist-lib/render-mode.cjs +0 -44
  388. package/kotlin/AppSources/android/view/View.kt +0 -74
  389. package/kotlin/AppSources/android/widget/FrameLayout.kt +0 -6
  390. package/src/render-mode.ts +0 -15
  391. /package/kotlin/{AppSources → AndroidViewSources}/android/content/Context.kt +0 -0
  392. /package/kotlin/{AppSources → AndroidViewSources}/android/content/res/Configuration.kt +0 -0
  393. /package/kotlin/{AppSources → AndroidViewSources}/android/graphics/Color.kt +0 -0
  394. /package/kotlin/{AppSources → AndroidViewSources}/android/util/DisplayMetrics.kt +0 -0
  395. /package/kotlin/{AppSources → AndroidViewSources}/dev/peach/app/JavaFormat.kt +0 -0
  396. /package/kotlin/{AppSources → Sources}/java/lang/Runnable.kt +0 -0
@@ -2,11 +2,33 @@
2
2
  import { createHash } from 'node:crypto'
3
3
  import { Script } from 'node:vm'
4
4
  import { terminateProcessGroup } from '../../contrast/src/runtime/process-group'
5
+ import { isLoopbackHost } from './backend-origin'
6
+ import { insertExpoKotlinFooter } from './expoKotlinContract'
5
7
  import { insertExpoSwiftFooter } from './expoSwiftContract'
6
8
 
7
9
  const digest = (bytes) => createHash('sha256').update(bytes).digest('hex')
8
10
  const segment = /^[a-zA-Z0-9._-]+$/
9
11
 
12
+ // where a dev server asset location's files live in a release. metro names a
13
+ // file outside the project root by an `unstable_path` query.
14
+ function packagedAssetDirectory(location) {
15
+ const parsed = new URL(location, 'https://packaged.invalid')
16
+ if (parsed.origin !== 'https://packaged.invalid')
17
+ throw new Error(`non-packaged asset location: ${location}`)
18
+ const outside = parsed.searchParams.get('unstable_path')
19
+ if (outside === null) return parsed.pathname.replace(/^\/+/, '')
20
+ return `assets/${outside.replace(/^\.\//, '').replace(/\.\.\//g, '_')}`
21
+ }
22
+
23
+ // each platform publishes its own channel. ios keeps the original deployed
24
+ // keys, which home screen configs already name.
25
+ const channelKey = (platform) =>
26
+ platform === 'ios' ? 'peach/channels/prod' : `peach/channels/${platform}/prod`
27
+ const nativeAssetPath = {
28
+ ios: /^__peach\/(swift|swift-icu)\/[a-f0-9]{64}(\.wasm)?$/,
29
+ android: /^__peach\/kotlin\/[a-f0-9]+\/[A-Za-z0-9._-]+\.(mjs|wasm)$/,
30
+ }
31
+
10
32
  export function prepareBundleChannel({
11
33
  prefix,
12
34
  publicOrigin,
@@ -14,16 +36,18 @@ export function prepareBundleChannel({
14
36
  runtimeVersion,
15
37
  name,
16
38
  icon,
17
- bundle,
18
- assets,
19
- expoSwift = null,
39
+ platforms,
20
40
  }) {
21
41
  if (!/^[a-f0-9]{40}$/.test(sourceSha) || !/^\d+$/.test(runtimeVersion)) {
22
42
  throw new Error('invalid bundle channel source identity')
23
43
  }
24
44
  const origin = new URL(publicOrigin)
25
45
  if (
26
- origin.protocol !== 'https:' ||
46
+ // a loopback origin serves local release proofs
47
+ !(
48
+ origin.protocol === 'https:' ||
49
+ (origin.protocol === 'http:' && isLoopbackHost(origin.hostname))
50
+ ) ||
27
51
  origin.pathname !== '/' ||
28
52
  origin.search ||
29
53
  origin.hash ||
@@ -37,6 +61,40 @@ export function prepareBundleChannel({
37
61
  }
38
62
  if (!icon.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex')))
39
63
  throw new Error('bundle channel icon must be PNG')
64
+ if (!platforms.length) throw new Error('bundle channel has no platforms')
65
+ if (new Set(platforms.map((p) => p.platform)).size !== platforms.length)
66
+ throw new Error('bundle channel repeats a platform')
67
+ const channels = platforms.map((snapshot) =>
68
+ prepareChannel({
69
+ prefix,
70
+ origin,
71
+ sourceSha,
72
+ runtimeVersion,
73
+ name,
74
+ icon,
75
+ ...snapshot,
76
+ }),
77
+ )
78
+ return { prefix, origin: origin.origin, channels }
79
+ }
80
+
81
+ function prepareChannel({
82
+ prefix,
83
+ origin,
84
+ sourceSha,
85
+ runtimeVersion,
86
+ name,
87
+ icon,
88
+ platform,
89
+ bundle,
90
+ assets,
91
+ expoSwift = null,
92
+ expoKotlin = null,
93
+ }) {
94
+ if (!(platform in nativeAssetPath)) throw new Error(`unknown platform: ${platform}`)
95
+ const native = platform === 'ios' ? expoSwift : expoKotlin
96
+ if ((platform === 'ios' ? expoKotlin : expoSwift) !== null)
97
+ throw new Error(`${platform} bundle carries another platform's native modules`)
40
98
  const files = new Map()
41
99
  for (const asset of assets) {
42
100
  if (
@@ -50,9 +108,9 @@ export function prepareBundleChannel({
50
108
  if (files.has(asset.path)) throw new Error(`duplicate packaged asset: ${asset.path}`)
51
109
  files.set(asset.path, asset.bytes)
52
110
  }
53
- for (const asset of expoSwift?.assets ?? []) {
54
- if (!/^__peach\/(swift|swift-icu)\/[a-f0-9]{64}(\.wasm)?$/.test(asset.path))
55
- throw new Error(`invalid Swift artifact path: ${asset.path}`)
111
+ for (const asset of native?.assets ?? []) {
112
+ if (!nativeAssetPath[platform].test(asset.path))
113
+ throw new Error(`invalid native artifact path: ${asset.path}`)
56
114
  if (files.has(asset.path)) throw new Error(`duplicate packaged asset: ${asset.path}`)
57
115
  files.set(asset.path, asset.bytes)
58
116
  }
@@ -64,7 +122,7 @@ export function prepareBundleChannel({
64
122
  )) {
65
123
  identity.update(JSON.stringify([path, digest(bytes)]))
66
124
  }
67
- if (expoSwift) identity.update(JSON.stringify(expoSwift.metadata))
125
+ if (native) identity.update(JSON.stringify(native.metadata))
68
126
  const releaseKey = `${prefix}/peach/releases/${identity.digest('hex')}`
69
127
  const assetBaseUrl = `${origin.origin}/${releaseKey}/`
70
128
  const directories = new Set(
@@ -76,12 +134,7 @@ export function prepareBundleChannel({
76
134
  /("?httpServerLocation"?\s*:\s*)(?:"((?:[^"\\]|\\.)*)"|`([^`\\$]*)`)/g,
77
135
  (match, property, quoted, template) => {
78
136
  const location = quoted === undefined ? template : JSON.parse(`"${quoted}"`)
79
- const parsed = new URL(location, 'https://packaged.invalid')
80
- if (parsed.origin !== 'https://packaged.invalid')
81
- throw new Error(`non-packaged asset location: ${location}`)
82
- const path = (parsed.searchParams.get('unstable_path') ?? parsed.pathname)
83
- .replace(/^\/+/, '')
84
- .replace(/\.\.\//g, '_')
137
+ const path = packagedAssetDirectory(location)
85
138
  if (!directories.has(path)) {
86
139
  throw new Error(`bundle asset location has no packaged files: ${location}`)
87
140
  }
@@ -91,25 +144,38 @@ export function prepareBundleChannel({
91
144
  )
92
145
  if (assets.length && !locations)
93
146
  throw new Error('bundle has packaged files but no asset metadata')
147
+ const release = (url) => new URL(url.replace(/^\//, ''), assetBaseUrl).href
94
148
  let nativeMetadata = null
95
149
  if (expoSwift) {
96
150
  nativeMetadata = {
97
- ...expoSwift.metadata,
98
- modules: expoSwift.metadata.modules.map((artifact) => ({
99
- ...artifact,
100
- url: new URL(artifact.url.replace(/^\//, ''), assetBaseUrl).href,
101
- icuUrl: artifact.icuUrl.startsWith('/')
102
- ? new URL(artifact.icuUrl.slice(1), assetBaseUrl).href
103
- : artifact.icuUrl,
104
- })),
151
+ expoSwift: {
152
+ ...expoSwift.metadata,
153
+ modules: expoSwift.metadata.modules.map((artifact) => ({
154
+ ...artifact,
155
+ url: release(artifact.url),
156
+ icuUrl: artifact.icuUrl.startsWith('/')
157
+ ? release(artifact.icuUrl)
158
+ : artifact.icuUrl,
159
+ })),
160
+ },
105
161
  }
106
- source = insertExpoSwiftFooter(source, nativeMetadata)
162
+ source = insertExpoSwiftFooter(source, nativeMetadata.expoSwift)
163
+ }
164
+ if (expoKotlin) {
165
+ const { module } = expoKotlin.metadata
166
+ nativeMetadata = {
167
+ expoKotlin: {
168
+ ...expoKotlin.metadata,
169
+ module: module && { ...module, url: release(module.url) },
170
+ },
171
+ }
172
+ source = insertExpoKotlinFooter(source, nativeMetadata.expoKotlin)
107
173
  }
108
174
  new Script(source, { filename: 'peach-release-bundle.js' })
109
175
  const bundleBytes = Buffer.from(source)
110
176
  const metadata = {
111
177
  schemaVersion: 1,
112
- ...(nativeMetadata ? { expoSwift: nativeMetadata } : {}),
178
+ ...nativeMetadata,
113
179
  channel: 'prod',
114
180
  name,
115
181
  iconUrl: `${assetBaseUrl}icon.png`,
@@ -117,7 +183,7 @@ export function prepareBundleChannel({
117
183
  assetBaseUrl,
118
184
  sourceSha,
119
185
  runtimeVersion,
120
- platform: 'ios',
186
+ platform,
121
187
  bundleHash: digest(bundleBytes),
122
188
  }
123
189
  const objects = [
@@ -131,7 +197,7 @@ export function prepareBundleChannel({
131
197
  key: `${releaseKey}/${path}`,
132
198
  bytes,
133
199
  contentType:
134
- expoSwift?.assets.find((asset) => asset.path === path)?.contentType ??
200
+ native?.assets.find((asset) => asset.path === path)?.contentType ??
135
201
  {
136
202
  png: 'image/png',
137
203
  jpg: 'image/jpeg',
@@ -145,11 +211,11 @@ export function prepareBundleChannel({
145
211
  'application/octet-stream',
146
212
  })),
147
213
  ]
148
- return { prefix, origin: origin.origin, metadata, objects }
214
+ return { platform, key: `${prefix}/${channelKey(platform)}`, metadata, objects }
149
215
  }
150
216
 
151
217
  export async function publishBundleChannel(release, { put, fetchObject = fetch }) {
152
- // prove every immutable object is downloadable before changing the channel.
218
+ // prove every immutable object is downloadable before changing a channel.
153
219
  async function uploadVerified(object, cacheControl) {
154
220
  await put(object.key, object.bytes, object.contentType, cacheControl)
155
221
  const response = await fetchObject(`${release.origin}/${object.key}`)
@@ -159,41 +225,110 @@ export async function publishBundleChannel(release, { put, fetchObject = fetch }
159
225
  if (digest(bytes) !== digest(object.bytes))
160
226
  throw new Error(`bundle channel download mismatch: ${object.key}`)
161
227
  }
162
- for (const object of release.objects) await uploadVerified(object)
163
- const bundle = release.objects[0]
164
- await uploadVerified(
165
- { ...bundle, key: `${release.prefix}/peach/channels/prod.js` },
166
- 'no-store',
167
- )
168
- await put(
169
- `${release.prefix}/peach/channels/prod.json`,
170
- Buffer.from(JSON.stringify(release.metadata)),
171
- 'application/json',
172
- 'no-store',
173
- )
174
- const response = await fetchObject(
175
- `${release.origin}/${release.prefix}/peach/channels/prod.json`,
176
- { cache: 'no-store' },
177
- )
178
- if (
179
- !response.ok ||
180
- JSON.stringify(await response.json()) !== JSON.stringify(release.metadata)
181
- ) {
182
- throw new Error('published bundle channel metadata did not match')
228
+ for (const channel of release.channels)
229
+ for (const object of channel.objects) await uploadVerified(object)
230
+ for (const channel of release.channels) {
231
+ await uploadVerified({ ...channel.objects[0], key: `${channel.key}.js` }, 'no-store')
232
+ await put(
233
+ `${channel.key}.json`,
234
+ Buffer.from(JSON.stringify(channel.metadata)),
235
+ 'application/json',
236
+ 'no-store',
237
+ )
238
+ const response = await fetchObject(`${release.origin}/${channel.key}.json`, {
239
+ cache: 'no-store',
240
+ })
241
+ if (
242
+ !response.ok ||
243
+ JSON.stringify(await response.json()) !== JSON.stringify(channel.metadata)
244
+ ) {
245
+ throw new Error(
246
+ `published ${channel.platform} bundle channel metadata did not match`,
247
+ )
248
+ }
249
+ console.log(
250
+ `[ota] Peach ${channel.platform} prod channel: ${release.origin}/${channel.key}.json`,
251
+ )
183
252
  }
184
- console.log(
185
- `[ota] Peach prod channel: ${release.origin}/${release.prefix}/peach/channels/prod.json`,
186
- )
187
253
  }
188
254
 
189
- // peach boots a One app from the dev server's bundle: it carries the module
190
- // graph peach maps native seams by, which a release build erases. snapshot it
191
- // from a private dev server and package the files its assets register.
255
+ // peach boots an app from the dev server's bundle: it carries the module graph
256
+ // peach maps native seams by, which a release build erases. snapshot each
257
+ // platform's bundle from a running dev server and package the files its
258
+ // assets register, with the app's local Expo native modules for that platform.
259
+ export async function snapshotDevServerBundles(
260
+ origin,
261
+ projectRoot,
262
+ { platforms = ['ios', 'android'], assertRunning = () => {} } = {},
263
+ ) {
264
+ const { readFileSync } = await import('node:fs')
265
+ const { basename } = await import('node:path')
266
+ const snapshots = []
267
+ for (const platform of platforms) {
268
+ const url = `${origin}/index.bundle?platform=${platform}&dev=true&minify=false`
269
+ const deadline = Date.now() + 10 * 60_000
270
+ let bundle
271
+ while (!bundle) {
272
+ assertRunning()
273
+ if (Date.now() > deadline)
274
+ throw new Error(`dev server ${platform} bundle timed out`)
275
+ const response = await fetch(url).catch(() => null)
276
+ if (response?.ok) bundle = Buffer.from(await response.arrayBuffer())
277
+ else await new Promise((resolve) => setTimeout(resolve, 2000))
278
+ }
279
+ const assets = new Map()
280
+ for (const [registration] of bundle
281
+ .toString('utf8')
282
+ .matchAll(/(?<=registerAsset\()\{[^{}]*"__packager_asset": true[^{}]*\}/g)) {
283
+ const asset = JSON.parse(registration)
284
+ const directory = packagedAssetDirectory(asset.httpServerLocation)
285
+ if (asset.files) {
286
+ for (const file of asset.files) {
287
+ const path = `${directory}/${basename(file)}`
288
+ if (!assets.has(path)) assets.set(path, readFileSync(file))
289
+ }
290
+ continue
291
+ }
292
+ // without local files, each scale downloads at the url react native's
293
+ // AssetSourceResolver builds for it
294
+ for (const scale of asset.scales) {
295
+ const file = `${asset.name}${scale === 1 ? '' : `@${scale}x`}.${asset.type}`
296
+ const path = `${directory}/${file}`
297
+ if (assets.has(path)) continue
298
+ const response = await fetch(
299
+ `${origin}${asset.httpServerLocation}/${file}?platform=${platform}&hash=${asset.hash}`,
300
+ )
301
+ if (!response.ok)
302
+ throw new Error(`dev server asset ${path} failed: ${response.status}`)
303
+ assets.set(path, Buffer.from(await response.arrayBuffer()))
304
+ }
305
+ }
306
+ snapshots.push({
307
+ platform,
308
+ bundle,
309
+ assets: [...assets].map(([path, bytes]) => ({ path, bytes })),
310
+ ...(platform === 'ios'
311
+ ? {
312
+ expoSwift: await (
313
+ await import('./expo-swift-bundle')
314
+ ).buildExpoSwiftBundle(projectRoot),
315
+ }
316
+ : {
317
+ expoKotlin: await (
318
+ await import('./expo-kotlin-bundle')
319
+ ).buildExpoKotlinBundle(projectRoot),
320
+ }),
321
+ })
322
+ }
323
+ return { platforms: snapshots }
324
+ }
325
+
326
+ // a One app's bundles, from a private dev server
192
327
  export async function snapshotOneDevBundle(projectRoot, env = {}) {
193
328
  const { spawn } = await import('node:child_process')
194
329
  const { readFileSync } = await import('node:fs')
195
330
  const { createServer } = await import('node:net')
196
- const { basename, dirname, join } = await import('node:path')
331
+ const { dirname, join } = await import('node:path')
197
332
  const { createRequire } = await import('node:module')
198
333
  const onePackage = createRequire(join(projectRoot, 'package.json')).resolve(
199
334
  'one/package.json',
@@ -217,34 +352,12 @@ export async function snapshotOneDevBundle(projectRoot, env = {}) {
217
352
  detached: true,
218
353
  })
219
354
  try {
220
- const url = `http://127.0.0.1:${port}/index.bundle?platform=ios&dev=true&minify=false`
221
- const deadline = Date.now() + 10 * 60_000
222
- let bundle
223
- while (!bundle) {
224
- if (child.exitCode !== null)
225
- throw new Error(`one dev exited with ${child.exitCode}`)
226
- if (Date.now() > deadline) throw new Error('one dev bundle timed out')
227
- const response = await fetch(url).catch(() => null)
228
- if (response?.ok) bundle = Buffer.from(await response.arrayBuffer())
229
- else await new Promise((resolve) => setTimeout(resolve, 2000))
230
- }
231
- const assets = new Map()
232
- for (const [registration] of bundle
233
- .toString('utf8')
234
- .matchAll(/(?<=registerAsset\()\{[^{}]*"__packager_asset": true[^{}]*\}/g)) {
235
- const asset = JSON.parse(registration)
236
- for (const file of asset.files) {
237
- const path = `${asset.httpServerLocation.replace(/^\/+/, '')}/${basename(file)}`
238
- if (!assets.has(path)) assets.set(path, readFileSync(file))
239
- }
240
- }
241
- const { buildExpoSwiftBundle } = await import('./expo-swift-bundle')
242
- const expoSwift = await buildExpoSwiftBundle(projectRoot)
243
- return {
244
- bundle,
245
- assets: [...assets].map(([path, bytes]) => ({ path, bytes })),
246
- expoSwift,
247
- }
355
+ return await snapshotDevServerBundles(`http://127.0.0.1:${port}`, projectRoot, {
356
+ assertRunning: () => {
357
+ if (child.exitCode !== null)
358
+ throw new Error(`one dev exited with ${child.exitCode}`)
359
+ },
360
+ })
248
361
  } finally {
249
362
  await terminateProcessGroup(child)
250
363
  }
@@ -0,0 +1,38 @@
1
+ // one `globalThis.<name>=<json>;` line a release publisher appends to a bundle,
2
+ // which boot analysis reads before evaluating it. it sits before the module
3
+ // path identity footer, which must stay the bundle's last line.
4
+
5
+ export function readBundleGlobalFooter(source: string, name: string): unknown {
6
+ const prefix = `globalThis.${name}=`
7
+ const start = source.lastIndexOf(`\n${prefix}`)
8
+ if (start < 0) return null
9
+ const end = source.indexOf('\n', start + 1)
10
+ const line = source.slice(start + 1, end < 0 ? undefined : end)
11
+ if (!line.endsWith(';')) throw new Error(`${name} footer is truncated`)
12
+ const value: unknown = JSON.parse(line.slice(prefix.length, -1))
13
+ if (value === null) throw new Error(`${name} footer has no metadata`)
14
+ return value
15
+ }
16
+
17
+ export function insertBundleGlobalFooter(
18
+ source: string,
19
+ name: string,
20
+ value: unknown,
21
+ ): string {
22
+ const prefix = `globalThis.${name}=`
23
+ const start = source.lastIndexOf(`\n${prefix}`)
24
+ if (start >= 0) {
25
+ readBundleGlobalFooter(source, name)
26
+ const end = source.indexOf('\n', start + 1)
27
+ source = source.slice(0, start) + (end < 0 ? '' : source.slice(end))
28
+ }
29
+ const json = JSON.stringify(value)
30
+ .replace(/\u2028/g, '\\u2028')
31
+ .replace(/\u2029/g, '\\u2029')
32
+ .replace(/<\//g, '<\\/')
33
+ const footer = `\n${prefix}${json};`
34
+ const identity = source.lastIndexOf('\nglobalThis.__sootsimModulePaths=')
35
+ return identity < 0
36
+ ? source + footer
37
+ : source.slice(0, identity) + footer + source.slice(identity)
38
+ }
@@ -155,6 +155,8 @@ export interface RnxCaptureRegionRequest {
155
155
  /** host node whose own subtree is rendered into this transparent atlas region */
156
156
  nodeId: number
157
157
  frame: RnxCaptureRect
158
+ /** the region holds the node's own layer only; its children replay over it */
159
+ ownPaint?: true
158
160
  }
159
161
 
160
162
  export interface RnxRasterRegionTree {
@@ -186,22 +188,62 @@ function isLiveRasterSource(node: RnxRasterRegionTree['nodes'][number]): boolean
186
188
  )
187
189
  }
188
190
 
189
- export function selectRnxRasterNodeIds(tree: RnxRasterRegionTree): Set<number> {
191
+ // the frame a raster layer captures, before paint outsets. a live source
192
+ // (video, a gpu scene) is cut to its visible bounds: opaque content in front
193
+ // of it replays semantically over the rest. one hidden entirely keeps its
194
+ // layout frame. the budget and the region both read this, so a layer costs
195
+ // the atlas exactly what it was budgeted.
196
+ function rasterLayerFrame(node: RnxRasterRegionTree['nodes'][number]): RnxCaptureRect {
197
+ const visible = node.visibleFrame
198
+ if (isLiveRasterSource(node) && visible && visible.width > 0 && visible.height > 0) {
199
+ return { x: visible.x, y: visible.y, width: visible.width, height: visible.height }
200
+ }
201
+ return (
202
+ node.frame ?? {
203
+ x: node.absolutePosition.x,
204
+ y: node.absolutePosition.y,
205
+ width: node.layout.width,
206
+ height: node.layout.height,
207
+ }
208
+ )
209
+ }
210
+
211
+ // a live source the app renders itself on the gpu (an expo-gl or webgpu view:
212
+ // a game, a 3d scene). its pixels are whatever the app draws next, so design
213
+ // shows a placeholder there instead of photographing a frame.
214
+ export function isGpuSceneSource(node: RnxRasterRegionTree['nodes'][number]): boolean {
215
+ if (!isLiveRasterSource(node)) return false
216
+ const source = node.props?.source
217
+ const uri = source && typeof source === 'object' ? Reflect.get(source, 'uri') : null
218
+ return (
219
+ typeof uri === 'string' &&
220
+ (uri.startsWith('live:expo-gl-') || uri.startsWith('live:wgpu-'))
221
+ )
222
+ }
223
+
224
+ // `nodeIds` is every node the atlas carries. a node in `ownPaint` carries only
225
+ // its own layer: glass larger than a layer may hold (a sheet's material) is
226
+ // captured without its children, which stay semantic and paint over it, and
227
+ // the glass controls inside it keep their own regions.
228
+ export type RnxRasterSelection = { nodeIds: Set<number>; ownPaint: Set<number> }
229
+
230
+ export function selectRnxRasterNodeIds(tree: RnxRasterRegionTree): RnxRasterSelection {
190
231
  const nodesById = new Map(tree.nodes.map((node) => [node.nodeId, node]))
191
232
  const maxLayerArea = tree.width * tree.height * 0.25
192
- // a layer over the per-layer cap never enters the atlas, so it cannot carry
193
- // its descendants: a sheet's glass is too large, and the glass controls
194
- // inside it still need their own regions.
195
- const eligible = new Map<number, { nodeId: number; area: number; liveVideo: boolean }>()
233
+ const eligible = new Map<
234
+ number,
235
+ { nodeId: number; area: number; liveVideo: boolean; ownPaint: boolean }
236
+ >()
196
237
  for (const node of tree.nodes) {
197
238
  const liveVideo = isLiveRasterSource(node)
198
- const visibleFrame =
199
- liveVideo &&
200
- node.visibleFrame &&
201
- (node.visibleFrame.width <= 0 || node.visibleFrame.height <= 0)
202
- ? node.frame
203
- : (node.visibleFrame ?? node.frame)
239
+ const visibleFrame = liveVideo
240
+ ? rasterLayerFrame(node)
241
+ : (node.visibleFrame ?? node.frame)
204
242
  if (!visibleFrame || visibleFrame.width <= 0 || visibleFrame.height <= 0) continue
243
+ const glass =
244
+ node.style._liquidGlass !== undefined ||
245
+ node.style._liquidGlassEdge !== undefined ||
246
+ node.style._liquidGlassMaterialTint !== undefined
205
247
  const unsupported =
206
248
  node.type === 'masked-view' ||
207
249
  liveVideo ||
@@ -211,40 +253,58 @@ export function selectRnxRasterNodeIds(tree: RnxRasterRegionTree): Set<number> {
211
253
  /\uFE0F|\u200D|\u20E3|\p{Emoji_Modifier}|\p{Regional_Indicator}/u.test(
212
254
  node.text,
213
255
  ))) ||
214
- node.style._liquidGlass !== undefined ||
215
- node.style._liquidGlassEdge !== undefined ||
216
- node.style._liquidGlassMaterialTint !== undefined
217
- const area = visibleFrame.width * visibleFrame.height
218
- if (unsupported && (liveVideo || area <= maxLayerArea)) {
219
- eligible.set(node.nodeId, { nodeId: node.nodeId, area, liveVideo })
256
+ glass
257
+ if (!unsupported) continue
258
+ const captured = rasterLayerFrame(node)
259
+ const area = captured.width * captured.height
260
+ if (liveVideo || area <= maxLayerArea) {
261
+ eligible.set(node.nodeId, { nodeId: node.nodeId, area, liveVideo, ownPaint: false })
262
+ } else if (glass) {
263
+ eligible.set(node.nodeId, { nodeId: node.nodeId, area, liveVideo, ownPaint: true })
220
264
  }
221
265
  }
266
+ // a whole-subtree layer absorbs the layers inside it; an own-paint layer
267
+ // leaves them their own regions.
222
268
  for (const nodeId of [...eligible.keys()]) {
223
269
  let parentNodeId = nodesById.get(nodeId)?.parentNodeId ?? null
224
270
  while (parentNodeId !== null) {
225
- if (eligible.has(parentNodeId)) {
271
+ const parent = eligible.get(parentNodeId)
272
+ if (parent && !parent.ownPaint) {
226
273
  eligible.delete(nodeId)
227
274
  break
228
275
  }
229
276
  parentNodeId = nodesById.get(parentNodeId)?.parentNodeId ?? null
230
277
  }
231
278
  }
279
+ // live video and own-paint glass (a whole sheet's material) claim the atlas
280
+ // first, up to a screen between them; the other layers follow smallest
281
+ // first, up to half a screen of their own. the whole atlas stays within a
282
+ // screen and a quarter, which the packer's pixel budget holds at 3x.
232
283
  const candidates = [...eligible.values()].sort(
233
284
  (left, right) =>
234
285
  Number(right.liveVideo) - Number(left.liveVideo) ||
286
+ Number(right.ownPaint) - Number(left.ownPaint) ||
235
287
  left.area - right.area ||
236
288
  left.nodeId - right.nodeId,
237
289
  )
238
290
  const screenArea = tree.width * tree.height
239
- const selected = new Set<number>()
291
+ const selection: RnxRasterSelection = { nodeIds: new Set(), ownPaint: new Set() }
240
292
  let atlasArea = 0
293
+ let layerArea = 0
241
294
  for (const candidate of candidates) {
242
- const maxAtlasArea = screenArea * (candidate.liveVideo ? 1 : 0.5)
243
- if (atlasArea + candidate.area > maxAtlasArea) continue
244
- selected.add(candidate.nodeId)
295
+ const leading = candidate.liveVideo || candidate.ownPaint
296
+ if (
297
+ atlasArea + candidate.area > screenArea * (leading ? 1 : 1.25) ||
298
+ (!leading && layerArea + candidate.area > screenArea * 0.5)
299
+ ) {
300
+ continue
301
+ }
302
+ selection.nodeIds.add(candidate.nodeId)
303
+ if (candidate.ownPaint) selection.ownPaint.add(candidate.nodeId)
245
304
  atlasArea += candidate.area
305
+ if (!leading) layerArea += candidate.area
246
306
  }
247
- return selected
307
+ return selection
248
308
  }
249
309
 
250
310
  export function rnxRasterFrames(
@@ -254,12 +314,7 @@ export function rnxRasterFrames(
254
314
  const frames = new Map<number, RnxCaptureRect>()
255
315
  for (const node of tree.nodes) {
256
316
  if (!nodeIds.has(node.nodeId)) continue
257
- const frame = node.frame ?? {
258
- x: node.absolutePosition.x,
259
- y: node.absolutePosition.y,
260
- width: node.layout.width,
261
- height: node.layout.height,
262
- }
317
+ const frame = rasterLayerFrame(node)
263
318
  // a region is what the node paints, and nothing else: a producer that crops
264
319
  // a composited screenshot copies every pixel inside it, so any margin past
265
320
  // the paint carries neighbours (a header title, a hairline, a list row)
@@ -371,11 +426,27 @@ export function rnxCaptureRasterRegions(
371
426
  visibleFrame: node.layout.visibleFrame,
372
427
  })),
373
428
  }
374
- const nodeIds = selectRnxRasterNodeIds(tree)
375
- const frames = rnxRasterFrames(tree, nodeIds)
376
- return [...nodeIds].flatMap((nodeId) => {
429
+ const selection = selectRnxRasterNodeIds(tree)
430
+ return rnxRasterRegionRequests(tree, selection)
431
+ }
432
+
433
+ // the region requests a producer packs for a selection
434
+ export function rnxRasterRegionRequests(
435
+ tree: RnxRasterRegionTree,
436
+ selection: RnxRasterSelection,
437
+ ): RnxCaptureRegionRequest[] {
438
+ const frames = rnxRasterFrames(tree, selection.nodeIds)
439
+ return [...selection.nodeIds].flatMap((nodeId) => {
377
440
  const frame = frames.get(nodeId)
378
- return frame ? [{ key: String(nodeId), nodeId, frame }] : []
441
+ if (!frame) return []
442
+ return [
443
+ {
444
+ key: String(nodeId),
445
+ nodeId,
446
+ frame,
447
+ ...(selection.ownPaint.has(nodeId) ? { ownPaint: true as const } : {}),
448
+ },
449
+ ]
379
450
  })
380
451
  }
381
452
 
@@ -445,6 +516,7 @@ export function packRnxCaptureRegions(
445
516
  return {
446
517
  key: region.key,
447
518
  nodeId: region.nodeId,
519
+ ...(region.ownPaint ? { ownPaint: true as const } : {}),
448
520
  frame: {
449
521
  x: left / pixelRatio,
450
522
  y: top / pixelRatio,
@@ -514,6 +586,7 @@ export function packRnxCaptureRegions(
514
586
  atlasRegions: best.regions.map((region) => ({
515
587
  key: region.key,
516
588
  nodeId: region.nodeId,
589
+ ...(region.ownPaint ? { ownPaint: true as const } : {}),
517
590
  frame: region.frame,
518
591
  // a region flush with the far edge is anchored to the logical edge so
519
592
  // its source never runs past the atlas by a rounding hair
@@ -784,6 +857,7 @@ export function isRnxCaptureRegionAtlas(value: unknown): value is RnxCaptureRegi
784
857
  keys.has(region.key) ||
785
858
  !captureInteger(region.nodeId) ||
786
859
  region.nodeId <= 0 ||
860
+ (region.ownPaint !== undefined && region.ownPaint !== true) ||
787
861
  !captureRect(region.frame) ||
788
862
  region.frame.width <= 0 ||
789
863
  region.frame.height <= 0 ||
@@ -1,6 +1,10 @@
1
1
  import type { BridgeCommandType } from './bridge-contract.ts'
2
2
 
3
- export const RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024
3
+ // largest guest artifact a nano isolate measurably boots, settles, screenshots,
4
+ // and survives repeated invocations within Cloudflare Dynamic Worker platform
5
+ // limits (67,108,864 bytes total Dynamic Worker size minus 26,347,088 bytes engine
6
+ // harness overhead).
7
+ export const RNX_CLOUD_MAX_ARTIFACT_BYTES = 40_761_776
4
8
  export const RNX_CLOUD_DESIGN_MAX_STORAGE_ENTRIES = 256
5
9
  export const RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES = 1024 * 1024
6
10
  export const RNX_CLOUD_TEST_FILES_MAX_COUNT = 256