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
package/src/cloud.ts CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  import { isRecord } from './is-record'
29
29
  import { rnxPublicBrand } from './public-brand'
30
30
  import { sleep } from './sleep'
31
- import type { SimStateOptions } from './bridge-contract'
31
+ import type { SemanticWaitCondition, SimStateOptions } from './bridge-contract'
32
32
 
33
33
  export {
34
34
  isRnxCloudCommandScope,
@@ -407,7 +407,10 @@ export class RemoteSimulator {
407
407
  do: (command) => this.sendCommand(command),
408
408
  wait: (condition, options) => {
409
409
  const timeoutMs = options?.maxMs ?? DEFAULT_WAIT_READY_MS
410
- const waitCondition = condition === 'ready' ? { type: 'ready' } : condition
410
+ const waitCondition: SemanticWaitCondition =
411
+ condition === 'ready'
412
+ ? { type: 'ready' }
413
+ : { type: 'present', selector: { testID: condition.selector } }
411
414
  return this.sendCommand(
412
415
  {
413
416
  type: 'waitFor',
package/src/config.ts CHANGED
@@ -8,6 +8,14 @@ import { isValidRuntimeVersion } from '@contrast/runtime-delivery/version'
8
8
  import { isRecord } from './is-record'
9
9
  import type { RevenueCatTestStoreConfig } from '@peach/compat/revenuecat-test-store'
10
10
 
11
+ export type NativeRenderer = 'paper' | 'fabric'
12
+
13
+ export function readNativeRenderer(value: unknown): NativeRenderer | undefined {
14
+ if (value === undefined) return undefined
15
+ if (value === 'paper' || value === 'fabric') return value
16
+ throw new Error('nativeRenderer must be paper or fabric')
17
+ }
18
+
11
19
  export type ModuleResolution =
12
20
  // an empty module under `modules`; under `nativeModules` / `turboModules` a
13
21
  // present-but-inert native module whose every method call is a no-op.
@@ -28,6 +36,8 @@ export type NativeViewResolution = 'noop' | { file: string }
28
36
  export interface PeachConfig<Settings extends Record<string, any> = Record<string, any>> {
29
37
  // host-only engine selection. omitted means the current stable runtime.
30
38
  runtimeVersion?: string
39
+ // native host identity. omission preserves the Fabric default.
40
+ nativeRenderer?: NativeRenderer
31
41
  modules?: Record<string, ModuleResolution>
32
42
  turboModules?: Record<string, NativeModuleResolution>
33
43
  nativeModules?: Record<string, NativeModuleResolution>
@@ -71,9 +81,14 @@ export function collectConfiguredFileModules(config?: PeachConfig): string[] {
71
81
  ]
72
82
  }
73
83
 
74
- export function isPeachConfig(value: unknown): value is PeachConfig {
84
+ export function isPeachConfig<Settings extends Record<string, any> = Record<string, any>>(
85
+ value: unknown,
86
+ ): value is PeachConfig<Settings> {
75
87
  if (!isRecord(value)) return false
76
88
  return (
89
+ (value.nativeRenderer === undefined ||
90
+ value.nativeRenderer === 'paper' ||
91
+ value.nativeRenderer === 'fabric') &&
77
92
  (value.runtimeVersion === undefined ||
78
93
  (typeof value.runtimeVersion === 'string' &&
79
94
  value.runtimeVersion === value.runtimeVersion.trim() &&
@@ -169,6 +184,7 @@ export function hasPeachConfig<
169
184
  >(config?: PeachConfig<Settings> | null): config is PeachConfig<Settings> {
170
185
  if (!config) return false
171
186
  return (
187
+ config.nativeRenderer !== undefined ||
172
188
  (typeof config.runtimeVersion === 'string' && config.runtimeVersion.length > 0) ||
173
189
  hasOwnKeys(config.modules as Record<string, unknown> | undefined) ||
174
190
  hasOwnKeys(config.turboModules as Record<string, unknown> | undefined) ||
@@ -193,6 +209,7 @@ export function mergePeachConfig<
193
209
 
194
210
  return {
195
211
  runtimeVersion: override.runtimeVersion ?? base.runtimeVersion,
212
+ nativeRenderer: override.nativeRenderer ?? base.nativeRenderer,
196
213
  modules: { ...(base.modules || {}), ...(override.modules || {}) },
197
214
  turboModules: { ...(base.turboModules || {}), ...(override.turboModules || {}) },
198
215
  nativeModules: {
@@ -217,14 +234,16 @@ export function readPeachConfigFromSearchParams<
217
234
  >(searchParams: URLSearchParams): PeachConfig<Settings> | undefined {
218
235
  const raw = searchParams.get(PEACH_CONFIG_QUERY_PARAM)
219
236
  if (!raw) return undefined
237
+ let parsed: unknown
220
238
  try {
221
- const parsed = JSON.parse(raw) as PeachConfig<Settings>
222
- return hasPeachConfig(parsed) ? parsed : undefined
239
+ parsed = JSON.parse(raw)
223
240
  } catch (error) {
224
241
  const message = error instanceof Error ? error.message : String(error)
225
242
  console.warn(`[peach] invalid ${PEACH_CONFIG_QUERY_PARAM}: ${message}`)
226
243
  return undefined
227
244
  }
245
+ if (isRecord(parsed)) readNativeRenderer(parsed.nativeRenderer)
246
+ return isPeachConfig<Settings>(parsed) && hasPeachConfig(parsed) ? parsed : undefined
228
247
  }
229
248
 
230
249
  export function applyPeachConfigToUrl<
@@ -90,7 +90,7 @@ export async function produceConfiguredBundle(
90
90
  const compact = await minify(code, {
91
91
  compress: false,
92
92
  mangle: false,
93
- format: { comments: false, beautify: false },
93
+ format: { comments: false },
94
94
  })
95
95
  if (!compact.code || /[\n\r]/.test(compact.code))
96
96
  throw new Error('configured bundle prefix must preserve the guest source lines')
@@ -79,6 +79,88 @@ export function isNativeHostBundle(app: string): boolean {
79
79
  )
80
80
  }
81
81
 
82
+ /** a default production install owns the scheme until uninstall or home purge. */
83
+ export function registerNativeDesktopLinks(): void {
84
+ if (
85
+ desktopHostPlatform() !== 'mac-arm64' ||
86
+ !isNativeHostBundle(MAC_HOST_APP) ||
87
+ bundleValue(MAC_HOST_APP, 'PeachUpdateChannel') !== 'production'
88
+ )
89
+ return
90
+ const receipt = path.join(desktopHostsDir(), 'url-handler.json')
91
+ fs.mkdirSync(desktopHostsDir(), { recursive: true })
92
+ const script = `
93
+ ObjC.import('CoreServices');
94
+ ObjC.import('Foundation');
95
+ ObjC.bindFunction('LSCopyDefaultHandlerForURLScheme', ['id', ['id']]);
96
+ ObjC.bindFunction('LSSetDefaultHandlerForURLScheme', ['int', ['id', 'id']]);
97
+ ObjC.bindFunction('LSCopyAllHandlersForURLScheme', ['id', ['id']]);
98
+ const current = ObjC.unwrap($.LSCopyDefaultHandlerForURLScheme('peach')) || null;
99
+ const handlers = ObjC.deepUnwrap($.LSCopyAllHandlersForURLScheme('peach')) || [];
100
+ const previous = current === ${JSON.stringify(HOST_BUNDLE_ID)}
101
+ ? handlers.find(handler => handler !== ${JSON.stringify(HOST_BUNDLE_ID)}) || null : current;
102
+ if (current !== ${JSON.stringify(HOST_BUNDLE_ID)} || ${!fs.existsSync(receipt)}) {
103
+ const data = $.NSString.stringWithString(JSON.stringify({ previous })).dataUsingEncoding($.NSUTF8StringEncoding);
104
+ if (!data.writeToFileAtomically(${JSON.stringify(receipt)}, true))
105
+ throw new Error('native URL handler receipt write failed');
106
+ }
107
+ const registered = $.LSRegisterURL($.NSURL.fileURLWithPath(${JSON.stringify(MAC_HOST_APP)}), true);
108
+ if (registered !== 0) throw new Error('native URL registration failed: ' + registered);
109
+ const selected = $.LSSetDefaultHandlerForURLScheme('peach', ${JSON.stringify(HOST_BUNDLE_ID)});
110
+ if (selected !== 0) throw new Error('native URL handler selection failed: ' + selected);
111
+ if (ObjC.unwrap($.LSCopyDefaultHandlerForURLScheme('peach')) !== ${JSON.stringify(HOST_BUNDLE_ID)})
112
+ throw new Error('native URL handler readback failed');
113
+ `
114
+ // runner-toolchain-platform: darwin
115
+ const result = spawnSync('/usr/bin/osascript', ['-l', 'JavaScript', '-e', script], {
116
+ encoding: 'utf8',
117
+ })
118
+ if (result.status !== 0)
119
+ throw new Error(`native deep-link registration failed: ${result.stderr.trim()}`)
120
+ }
121
+
122
+ /** restore the captured handler only while this install still owns the scheme. */
123
+ export function unregisterNativeDesktopLinks(): void {
124
+ if (desktopHostPlatform() !== 'mac-arm64') return
125
+ const receipt = path.join(desktopHostsDir(), 'url-handler.json')
126
+ if (!fs.existsSync(receipt)) return
127
+ const state: unknown = JSON.parse(fs.readFileSync(receipt, 'utf8'))
128
+ if (
129
+ typeof state !== 'object' ||
130
+ state === null ||
131
+ !('previous' in state) ||
132
+ (state.previous !== null && typeof state.previous !== 'string')
133
+ )
134
+ throw new Error('invalid native URL registration receipt')
135
+ const script = `
136
+ ObjC.import('CoreServices');
137
+ ObjC.bindFunction('LSCopyDefaultHandlerForURLScheme', ['id', ['id']]);
138
+ ObjC.bindFunction('LSSetDefaultHandlerForURLScheme', ['int', ['id', 'id']]);
139
+ const current = ObjC.unwrap($.LSCopyDefaultHandlerForURLScheme('peach'));
140
+ const previous = ${JSON.stringify(state.previous)};
141
+ if (current === ${JSON.stringify(HOST_BUNDLE_ID)} && previous) {
142
+ const status = $.LSSetDefaultHandlerForURLScheme('peach', previous);
143
+ if (status !== 0 || ObjC.unwrap($.LSCopyDefaultHandlerForURLScheme('peach')) !== previous)
144
+ throw new Error('native URL handler restore failed: ' + status);
145
+ }
146
+ `
147
+ // runner-toolchain-platform: darwin
148
+ run('/usr/bin/osascript', ['-l', 'JavaScript', '-e', script])
149
+ // runner-toolchain-platform: darwin
150
+ run(
151
+ '/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister',
152
+ ['-u', MAC_HOST_APP],
153
+ )
154
+ fs.unlinkSync(receipt)
155
+ }
156
+
157
+ export function uninstallNativeDesktopHost(): void {
158
+ unregisterNativeDesktopLinks()
159
+ if (desktopHostPlatform() === 'mac-arm64' && isNativeHostBundle(MAC_HOST_APP))
160
+ fs.rmSync(MAC_HOST_APP, { recursive: true, force: true })
161
+ fs.rmSync(desktopHostsDir(), { recursive: true, force: true })
162
+ }
163
+
82
164
  function placedHostVersion(): string | null {
83
165
  if (desktopHostPlatform() === 'linux-x64') {
84
166
  try {
@@ -105,7 +187,19 @@ export function activeDesktopHostApp(): string | null {
105
187
 
106
188
  /** the installed standalone host command; the driver appends its connection arguments. */
107
189
  export function headlessHostCommand(): string[] | null {
108
- if (desktopHostPlatform() !== 'linux-x64') return null
190
+ const platform = desktopHostPlatform()
191
+ if (!platform) return null
192
+ if (platform === 'mac-arm64') {
193
+ const app = process.env.PEACH_DESKTOP_APP
194
+ ? path.resolve(process.env.PEACH_DESKTOP_APP)
195
+ : activeDesktopHostApp()
196
+ if (!app) return null
197
+ const launcher = path.join(app, 'Contents/Frameworks/node')
198
+ const host = path.join(app, 'Contents/Resources/node/host.mjs')
199
+ // releases before the bundled launcher have no headless host
200
+ if (!fs.existsSync(launcher) || !fs.existsSync(host)) return null
201
+ return [launcher, '--experimental-vm-modules', host]
202
+ }
109
203
  const version = placedHostVersion()
110
204
  if (!version) return null
111
205
  const dir = desktopHostDir(version)
@@ -0,0 +1,29 @@
1
+ // release publishers use the same app compiler as local Android launches, then
2
+ // carry its immutable Kotlin/Wasm module beside the javascript bundle.
3
+ import { compileExpoKotlinModules } from './expo-kotlin-host'
4
+ import type { ExpoKotlinCompilationResult } from './expoKotlinContract'
5
+
6
+ export interface ExpoKotlinBundle {
7
+ // module.url is relative to the release's asset base
8
+ metadata: ExpoKotlinCompilationResult
9
+ assets: { path: string; bytes: Buffer; contentType: string }[]
10
+ }
11
+
12
+ export async function buildExpoKotlinBundle(
13
+ appRoot: string,
14
+ ): Promise<ExpoKotlinBundle | null> {
15
+ const metadata = await compileExpoKotlinModules(appRoot, '')
16
+ if (!metadata) return null
17
+ const { module } = metadata
18
+ if (!module) return { metadata, assets: [] }
19
+ const { kotlinArtifactFiles } = await import('./kotlin-host')
20
+ const dir = `__peach/kotlin/${module.hash}`
21
+ return {
22
+ metadata: { ...metadata, module: { ...module, url: `${dir}/app.mjs` } },
23
+ assets: kotlinArtifactFiles(module.hash).map((file) => ({
24
+ path: `${dir}/${file.name}`,
25
+ bytes: file.bytes,
26
+ contentType: file.contentType,
27
+ })),
28
+ }
29
+ }
@@ -0,0 +1,201 @@
1
+ // local Expo modules on an Android device: expo's own autolinker selects the
2
+ // app-owned Android modules, as its gradle plugin does, and their Kotlin
3
+ // compiles into one Kotlin/Wasm process with the expo-modules-core DSL port.
4
+ import { execFile } from 'node:child_process'
5
+ import fs from 'node:fs'
6
+ import { createRequire } from 'node:module'
7
+ import path from 'node:path'
8
+ import { promisify } from 'node:util'
9
+ import { RNX_FORKED_SCRIPT_ENV } from '../cli/internal-child'
10
+ import { isLoopbackHost } from './backend-origin'
11
+ import { isRecord } from './is-record'
12
+ import type { ExpoKotlinCompilationResult } from './expoKotlinContract'
13
+ import type { IncomingMessage, ServerResponse } from 'node:http'
14
+
15
+ const run = promisify(execFile)
16
+ // the compiled CLI's execPath is peach itself, which runs a forked `.js` as a
17
+ // script only with this set; node and bun ignore it.
18
+ const scriptEnv = { ...process.env, [RNX_FORKED_SCRIPT_ENV]: '1' }
19
+ export const EXPO_KOTLIN_ROUTE = '/__peach/expo-kotlin'
20
+ const MODULE_ROOT = path.resolve(import.meta.dirname, '../kotlin')
21
+
22
+ export interface ExpoKotlinModules {
23
+ // the registration names Android's ExpoModulesPackageList passes, in order
24
+ modules: { classifier: string; name: string | null }[]
25
+ sources: string[]
26
+ }
27
+
28
+ export async function collectExpoKotlinModules(
29
+ appRoot: string,
30
+ ): Promise<ExpoKotlinModules | null> {
31
+ appRoot = fs.realpathSync(appRoot)
32
+ let autolinker: string
33
+ try {
34
+ autolinker = createRequire(path.join(appRoot, 'package.json')).resolve(
35
+ 'expo-modules-autolinking/package.json',
36
+ )
37
+ } catch (error) {
38
+ if (isRecord(error) && error.code === 'MODULE_NOT_FOUND') return null
39
+ throw error
40
+ }
41
+ const { stdout } = await run(
42
+ process.execPath,
43
+ [
44
+ path.join(path.dirname(autolinker), 'bin/expo-modules-autolinking.js'),
45
+ 'resolve',
46
+ '--platform',
47
+ 'android',
48
+ '--project-root',
49
+ appRoot,
50
+ '--json',
51
+ ],
52
+ { timeout: 30_000, maxBuffer: 8 * 1024 * 1024, env: scriptEnv },
53
+ )
54
+ const resolved: unknown = JSON.parse(stdout)
55
+ if (!isRecord(resolved) || !Array.isArray(resolved.modules))
56
+ throw new Error('invalid Expo Android resolution')
57
+ const result: ExpoKotlinModules = { modules: [], sources: [] }
58
+ for (const entry of resolved.modules) {
59
+ if (!isRecord(entry) || !Array.isArray(entry.projects))
60
+ throw new Error('invalid Expo Android module entry')
61
+ for (const project of entry.projects) {
62
+ if (
63
+ !isRecord(project) ||
64
+ typeof project.sourceDir !== 'string' ||
65
+ !Array.isArray(project.modules)
66
+ )
67
+ throw new Error('invalid Expo Android project')
68
+ const sourceDir = fs.realpathSync(project.sourceDir)
69
+ // installed dependency implementations remain with their native seams.
70
+ if (sourceDir.split(path.sep).includes('node_modules')) continue
71
+ if (project.modules.length === 0) continue
72
+ for (const module of project.modules) {
73
+ if (
74
+ !isRecord(module) ||
75
+ typeof module.classifier !== 'string' ||
76
+ !/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)+$/.test(
77
+ module.classifier,
78
+ ) ||
79
+ (module.name !== null && typeof module.name !== 'string')
80
+ )
81
+ throw new Error(`${sourceDir}: invalid Expo Android module descriptor`)
82
+ result.modules.push({ classifier: module.classifier, name: module.name })
83
+ }
84
+ const walk = (dir: string) => {
85
+ if (!fs.existsSync(dir)) return
86
+ for (const child of fs
87
+ .readdirSync(dir, { withFileTypes: true })
88
+ .sort((a, b) => a.name.localeCompare(b.name))) {
89
+ const file = path.join(dir, child.name)
90
+ if (child.isSymbolicLink())
91
+ throw new Error(`Expo Kotlin source is a symlink: ${file}`)
92
+ if (child.isDirectory()) walk(file)
93
+ else if (child.name.endsWith('.java'))
94
+ throw new Error(`${file}: Java sources in Expo modules are not supported yet`)
95
+ else if (child.name.endsWith('.kt')) result.sources.push(file)
96
+ }
97
+ }
98
+ // the gradle android plugin's default main source sets
99
+ walk(path.join(sourceDir, 'src', 'main', 'java'))
100
+ walk(path.join(sourceDir, 'src', 'main', 'kotlin'))
101
+ }
102
+ }
103
+ return result.modules.length === 0 ? null : result
104
+ }
105
+
106
+ export async function compileExpoKotlinModules(
107
+ root: string,
108
+ origin: string,
109
+ ): Promise<ExpoKotlinCompilationResult | null> {
110
+ const found = await collectExpoKotlinModules(root)
111
+ if (!found) return null
112
+ const namespaces = found.modules.map(
113
+ (module) => module.name ?? module.classifier.split('.').pop()!,
114
+ )
115
+ const { expoKotlinArtifact } = await import('./kotlin-host')
116
+ try {
117
+ const artifact = await expoKotlinArtifact(root, MODULE_ROOT, origin, found)
118
+ return { namespaces, module: artifact, failures: [] }
119
+ } catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error)
121
+ // kotlinc names each failing source line; anything else is the host's
122
+ const errors = message.match(/^.+\.kt:\d+:\d+: error:.*$/gm)
123
+ if (!errors) throw error
124
+ return {
125
+ namespaces,
126
+ module: null,
127
+ failures: namespaces.map((namespace) => ({
128
+ namespace,
129
+ diagnostic: errors.join('\n'),
130
+ })),
131
+ }
132
+ }
133
+ }
134
+
135
+ const compiling = new Map<string, Promise<ExpoKotlinCompilationResult | null>>()
136
+
137
+ export async function handleExpoKotlinRequest(
138
+ req: IncomingMessage,
139
+ res: ServerResponse,
140
+ ): Promise<boolean> {
141
+ const url = new URL(req.url ?? '/', 'http://localhost')
142
+ if (
143
+ !url.pathname.startsWith('/__peach/kotlin-artifact/') &&
144
+ url.pathname !== EXPO_KOTLIN_ROUTE
145
+ )
146
+ return false
147
+ const remote = req.socket.remoteAddress?.replace(/^::ffff:/, '')
148
+ const requestOrigin = req.headers.origin
149
+ if (
150
+ !remote ||
151
+ !isLoopbackHost(remote) ||
152
+ (requestOrigin && !isLoopbackHost(new URL(requestOrigin).hostname))
153
+ ) {
154
+ res.writeHead(403)
155
+ res.end('Expo Kotlin artifacts require a loopback client')
156
+ return true
157
+ }
158
+ res.setHeader('Access-Control-Allow-Origin', requestOrigin ?? '*')
159
+ res.setHeader('Vary', 'Origin')
160
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin')
161
+ if (url.pathname !== EXPO_KOTLIN_ROUTE) {
162
+ const { serveKotlinArtifact } = await import('./kotlin-host')
163
+ return serveKotlinArtifact(url.pathname, res)
164
+ }
165
+ if (req.method !== 'GET') {
166
+ res.writeHead(405)
167
+ res.end('GET required')
168
+ return true
169
+ }
170
+ const root = url.searchParams.get('root')
171
+ if (!root || !path.isAbsolute(root)) {
172
+ res.writeHead(400)
173
+ res.end('missing absolute app root')
174
+ return true
175
+ }
176
+ const host = req.headers.host
177
+ if (!host) {
178
+ res.writeHead(400)
179
+ res.end('Expo Kotlin request has no host')
180
+ return true
181
+ }
182
+ try {
183
+ let pending = compiling.get(root)
184
+ if (!pending) {
185
+ pending = compileExpoKotlinModules(root, `http://${host}`)
186
+ compiling.set(root, pending)
187
+ }
188
+ const metadata = await pending
189
+ res.writeHead(200, {
190
+ 'Content-Type': 'application/json',
191
+ 'Cache-Control': 'no-store',
192
+ })
193
+ res.end(JSON.stringify(metadata))
194
+ } catch (error) {
195
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
196
+ res.end(error instanceof Error ? error.message : String(error))
197
+ } finally {
198
+ compiling.delete(root)
199
+ }
200
+ return true
201
+ }
@@ -1,5 +1,5 @@
1
1
  // expo's own autolinker selects app-owned Swift modules; the common Swift host
2
- // validates namespaces independently and links successful registrations together.
2
+ // links their registrations together and isolates namespaces only when that fails.
3
3
  import { execFile } from 'node:child_process'
4
4
  import { createHash } from 'node:crypto'
5
5
  import fs from 'node:fs'
@@ -232,7 +232,7 @@ export async function collectExpoSwiftSources(
232
232
  }
233
233
 
234
234
  function expoEntry(units: ExpoSwiftSourceUnit[]): string {
235
- return `import SwiftUI\nimport ExpoModulesCore\n${[...new Set(units.map((unit) => unit.namespace))].map((namespace) => `import ${namespace}`).join('\n')}\n\n@main struct PeachExpoApp: RNXPackage {\n init() { MainActor.assumeIsolated { PeachExpoRuntime.register([${units.flatMap((unit) => unit.classes).join(', ')}]) } }\n func view(props: JSON) -> PeachExpoNativeView { PeachExpoNativeView(props: props) }\n}\n\n@_expose(wasm, "peach_expo_call")\n@_cdecl("peach_expo_call")\n@MainActor public func peachExpoCall(_ payload: UnsafePointer<UInt8>?, _ length: Int32) -> UnsafeMutablePointer<UInt8>? {\n PeachExpoRuntime.dispatch(payload, length)\n}\n`
235
+ return `import SwiftUI\nimport ExpoModulesCore\n${[...new Set(units.map((unit) => unit.namespace))].map((namespace) => `import ${namespace}`).join('\n')}\n\n@main struct PeachExpoApp: PeachPackage {\n init() { MainActor.assumeIsolated { PeachExpoRuntime.register([${units.flatMap((unit) => unit.classes).join(', ')}]) } }\n func view(props: JSON) -> PeachExpoNativeView { PeachExpoNativeView(props: props) }\n}\n\n@_expose(wasm, "peach_expo_call")\n@_cdecl("peach_expo_call")\n@MainActor public func peachExpoCall(_ payload: UnsafePointer<UInt8>?, _ length: Int32) -> UnsafeMutablePointer<UInt8>? {\n PeachExpoRuntime.dispatch(payload, length)\n}\n`
236
236
  }
237
237
 
238
238
  const compiling = new Map<string, Promise<unknown>>()
@@ -246,7 +246,8 @@ export async function compileExpoSwiftModules(
246
246
  ): Promise<ExpoSwiftCompilationResult | null> {
247
247
  const units = await collectExpoSwiftSources(root)
248
248
  if (!units) return null
249
- // source checks isolate failures; successful modules share one linked app.
249
+ // every candidate links into one app first; only a failed link compiles each
250
+ // namespace alone to isolate the failures, so a framework change costs one build.
250
251
  const module = await resolveSwiftModule()
251
252
  if (!module && publishArtifact)
252
253
  throw new Error('No local Swift toolchain or distribution is available')
@@ -255,81 +256,42 @@ export async function compileExpoSwiftModules(
255
256
  modules: [],
256
257
  failures: [],
257
258
  }
258
- const key = createHash('sha256').update(fs.realpathSync(root)).digest('hex')
259
- const linked: ExpoSwiftSourceUnit[] = []
260
- for (const unit of units) {
261
- if (!module) {
259
+ if (!module) {
260
+ for (const unit of units)
262
261
  result.failures.push({
263
262
  namespace: unit.moduleId,
264
263
  diagnostic: 'No local Swift toolchain or distribution is available',
265
264
  })
266
- continue
267
- }
265
+ return result
266
+ }
267
+ const key = createHash('sha256').update(fs.realpathSync(root)).digest('hex')
268
+ const failures = new Map<string, string>()
269
+ const candidates: ExpoSwiftSourceUnit[] = []
270
+ const stamps = new Map<ExpoSwiftSourceUnit, string>()
271
+ for (const unit of units) {
268
272
  const packagePath = path.join(os.tmpdir(), 'peach-expo-swift', key, unit.moduleId)
269
- let stamp: string | undefined
270
- try {
271
- // preserve the compiler's input stamp across unchanged reloads.
272
- // only this adapter's build copy is synchronized; app sources are read only.
273
- syncSwiftTree(
274
- packagePath,
275
- new Map(
276
- Object.entries(unit.files).map(([name, source]) => [
277
- path.join(packagePath, name),
278
- source,
279
- ]),
280
- ),
281
- )
282
- stamp = sourceStamp(packagePath, module.root)
283
- const failed = failedCompilations.get(packagePath)
284
- if (failed?.stamp === stamp) {
285
- result.failures.push({
286
- namespace: unit.moduleId,
287
- diagnostic: failed.diagnostic,
288
- })
289
- continue
290
- }
291
- failedCompilations.delete(packagePath)
292
- await artifactFor(projectFor(packagePath), EXPO_SWIFT_ENTRY, module.root)
293
- linked.push(unit)
294
- } catch (error) {
295
- const diagnostic = error instanceof Error ? error.message : String(error)
296
- const appPaths = Object.values(unit.files).flatMap((source) => {
297
- const match = /^#sourceLocation\(file: (".*"), line: 1\)$/m.exec(source)
298
- if (!match) return []
299
- const file: unknown = JSON.parse(match[1]!)
300
- return typeof file === 'string' ? [file] : []
301
- })
302
- const errors = diagnostic.match(/^.+:\d+(?::\d+)?: error:.*$/gm) ?? []
303
- const sourceErrors = errors.filter(
304
- (line) =>
305
- appPaths.some((file) => line.startsWith(`${file}:`)) &&
306
- !/external macro implementation type|failed to load library plugin/.test(line),
307
- )
308
- if (
309
- publishArtifact &&
310
- (sourceErrors.length === 0 || sourceErrors.length !== errors.length) &&
311
- !(error instanceof SwiftMissingModulesError)
312
- )
313
- throw error
314
- if (
315
- sourceErrors.length &&
316
- sourceErrors.length === errors.length &&
317
- module &&
318
- stamp &&
319
- sourceStamp(packagePath, module.root) === stamp
320
- ) {
321
- failedCompilations.set(packagePath, {
322
- stamp,
323
- diagnostic: sourceErrors.join('\n'),
324
- })
325
- }
326
- result.failures.push({
327
- namespace: unit.moduleId,
328
- diagnostic: sourceErrors.length ? sourceErrors.join('\n') : diagnostic,
329
- })
273
+ // preserve the compiler's input stamp across unchanged reloads.
274
+ // only this adapter's build copy is synchronized; app sources are read only.
275
+ syncSwiftTree(
276
+ packagePath,
277
+ new Map(
278
+ Object.entries(unit.files).map(([name, source]) => [
279
+ path.join(packagePath, name),
280
+ source,
281
+ ]),
282
+ ),
283
+ )
284
+ const stamp = sourceStamp(packagePath, module.root)
285
+ const failed = failedCompilations.get(packagePath)
286
+ if (failed?.stamp === stamp) {
287
+ failures.set(unit.moduleId, failed.diagnostic)
288
+ continue
330
289
  }
290
+ failedCompilations.delete(packagePath)
291
+ stamps.set(unit, stamp)
292
+ candidates.push(unit)
331
293
  }
332
- if (module && linked.length) {
294
+ const link = async (linked: ExpoSwiftSourceUnit[]) => {
333
295
  const files: Record<string, string> = Object.assign(
334
296
  {},
335
297
  ...linked.map((unit) => unit.files),
@@ -355,11 +317,69 @@ export async function compileExpoSwiftModules(
355
317
  ]),
356
318
  ),
357
319
  )
358
- const artifact = await artifactFor(
359
- projectFor(packagePath),
360
- EXPO_SWIFT_ENTRY,
361
- module.root,
362
- )
320
+ return artifactFor(projectFor(packagePath), EXPO_SWIFT_ENTRY, module.root)
321
+ }
322
+ let linked = candidates
323
+ let artifact: import('./swift-host').SwiftArtifact | undefined
324
+ if (linked.length) {
325
+ try {
326
+ artifact = await link(linked)
327
+ } catch (linkError) {
328
+ linked = []
329
+ for (const unit of candidates) {
330
+ const packagePath = path.join(os.tmpdir(), 'peach-expo-swift', key, unit.moduleId)
331
+ try {
332
+ await artifactFor(projectFor(packagePath), EXPO_SWIFT_ENTRY, module.root)
333
+ linked.push(unit)
334
+ } catch (error) {
335
+ const diagnostic = error instanceof Error ? error.message : String(error)
336
+ const appPaths = Object.values(unit.files).flatMap((source) => {
337
+ const match = /^#sourceLocation\(file: (".*"), line: 1\)$/m.exec(source)
338
+ if (!match) return []
339
+ const file: unknown = JSON.parse(match[1]!)
340
+ return typeof file === 'string' ? [file] : []
341
+ })
342
+ const errors = diagnostic.match(/^.+:\d+(?::\d+)?: error:.*$/gm) ?? []
343
+ const sourceErrors = errors.filter(
344
+ (line) =>
345
+ appPaths.some((file) => line.startsWith(`${file}:`)) &&
346
+ !/external macro implementation type|failed to load library plugin/.test(
347
+ line,
348
+ ),
349
+ )
350
+ if (
351
+ publishArtifact &&
352
+ (sourceErrors.length === 0 || sourceErrors.length !== errors.length) &&
353
+ !(error instanceof SwiftMissingModulesError)
354
+ )
355
+ throw error
356
+ if (
357
+ sourceErrors.length &&
358
+ sourceErrors.length === errors.length &&
359
+ sourceStamp(packagePath, module.root) === stamps.get(unit)
360
+ ) {
361
+ failedCompilations.set(packagePath, {
362
+ stamp: stamps.get(unit)!,
363
+ diagnostic: sourceErrors.join('\n'),
364
+ })
365
+ }
366
+ failures.set(
367
+ unit.moduleId,
368
+ sourceErrors.length ? sourceErrors.join('\n') : diagnostic,
369
+ )
370
+ }
371
+ }
372
+ // every namespace compiles alone, so the link itself is what failed.
373
+ if (linked.length === candidates.length) throw linkError
374
+ if (linked.length) artifact = await link(linked)
375
+ }
376
+ }
377
+ for (const unit of units) {
378
+ const diagnostic = failures.get(unit.moduleId)
379
+ if (diagnostic !== undefined)
380
+ result.failures.push({ namespace: unit.moduleId, diagnostic })
381
+ }
382
+ if (artifact) {
363
383
  await publishArtifact?.(artifact)
364
384
  result.modules.push({
365
385
  namespaces: linked.map((unit) => unit.moduleId),