peachsim 0.1.1640 → 0.1.1644

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 (125) hide show
  1. package/README.md +5 -0
  2. package/cli/app-bundle-resources.ts +57 -0
  3. package/cli/app-stubs.ts +38 -13
  4. package/cli/cloud-session.ts +16 -0
  5. package/cli/commands/assert.ts +17 -4
  6. package/cli/commands/box.ts +33 -19
  7. package/cli/commands/control.ts +12 -0
  8. package/cli/commands/doctor.ts +24 -15
  9. package/cli/commands/inspect/actions.ts +3 -2
  10. package/cli/commands/inspect/core.ts +0 -8
  11. package/cli/commands/inspect.ts +0 -14
  12. package/cli/commands/platform.ts +2 -1
  13. package/cli/commands/record.ts +64 -28
  14. package/cli/commands/swift.ts +10 -2
  15. package/cli/commands/three-mode.ts +4 -38
  16. package/cli/commands/upload.ts +8 -8
  17. package/cli/drivers/playwright.ts +39 -0
  18. package/cli/flow-live-status.ts +0 -2
  19. package/cli/main.ts +1 -1
  20. package/cli/outbound-endpoints.ts +4 -2
  21. package/cli/ws-bridge.ts +0 -6
  22. package/detox/attempt-events.cjs +41 -5
  23. package/detox/index.ts +155 -109
  24. package/detox/jest-environment.cjs +5 -1
  25. package/detox/motion-change.cjs +7 -5
  26. package/detox/motion-change.d.cts +1 -0
  27. package/detox/motion-leg-starts.cjs +20 -0
  28. package/detox/motion-leg-starts.d.cts +8 -0
  29. package/dist-lib/agent-daemon-client.cjs +1 -1
  30. package/dist-lib/agent-events.cjs +1 -1
  31. package/dist-lib/attached-projects.cjs +1 -1
  32. package/dist-lib/auth/shared-session.cjs +1 -1
  33. package/dist-lib/backend-origin.cjs +12 -3
  34. package/dist-lib/beta.cjs +1 -1
  35. package/dist-lib/beta.mjs +1 -1
  36. package/dist-lib/bridge-constants.cjs +1 -1
  37. package/dist-lib/bridge-contract-input.cjs +1 -1
  38. package/dist-lib/bridge-contract-input.mjs +1 -1
  39. package/dist-lib/bridge-contract.cjs +1 -1
  40. package/dist-lib/bridge-contract.mjs +1 -1
  41. package/dist-lib/bundle-channel.cjs +2671 -1833
  42. package/dist-lib/capture-contract.cjs +1 -1
  43. package/dist-lib/capture-contract.mjs +1 -1
  44. package/dist-lib/cloud-contract.cjs +1 -1
  45. package/dist-lib/cloud-contract.mjs +1 -1
  46. package/dist-lib/cloud-test-args.cjs +1 -1
  47. package/dist-lib/cloud-test-args.mjs +1 -1
  48. package/dist-lib/cloud.cjs +1 -1
  49. package/dist-lib/cloud.mjs +1 -1
  50. package/dist-lib/config.cjs +1 -1
  51. package/dist-lib/config.mjs +1 -1
  52. package/dist-lib/detox/index.cjs +578 -130
  53. package/dist-lib/detox/jest-environment.cjs +5 -1
  54. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  55. package/dist-lib/expo-swift-bundle.cjs +2374 -1554
  56. package/dist-lib/expo-swift-metadata.cjs +1 -1
  57. package/dist-lib/expo-swift-metadata.mjs +1 -1
  58. package/dist-lib/home-paths.cjs +1 -1
  59. package/dist-lib/host/bridge-host.cjs +2968 -2141
  60. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  61. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  62. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  63. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  64. package/dist-lib/host/websocket-proxy.cjs +1 -1
  65. package/dist-lib/index.cjs +132 -57
  66. package/dist-lib/jump-to-source-babel.cjs +1 -1
  67. package/dist-lib/jump-to-source-native.cjs +1 -1
  68. package/dist-lib/kotlin-host.cjs +1 -1
  69. package/dist-lib/menu.cjs +14 -6
  70. package/dist-lib/menu.mjs +14 -6
  71. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  72. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  73. package/dist-lib/metro-production-bundle.cjs +1 -1
  74. package/dist-lib/metro-production-bundle.mjs +1 -1
  75. package/dist-lib/metro.cjs +120 -53
  76. package/dist-lib/native-app-identity-project.cjs +96 -44
  77. package/dist-lib/native-app-identity-project.mjs +95 -44
  78. package/dist-lib/native-app-identity.cjs +1 -1
  79. package/dist-lib/native-app-identity.mjs +1 -1
  80. package/dist-lib/native-firebase-metadata.cjs +1 -1
  81. package/dist-lib/native-firebase-metadata.mjs +1 -1
  82. package/dist-lib/plist.cjs +1 -1
  83. package/dist-lib/plist.mjs +1 -1
  84. package/dist-lib/profiles.cjs +1 -1
  85. package/dist-lib/public-brand.cjs +1 -1
  86. package/dist-lib/react-native-host-modules.cjs +1 -1
  87. package/dist-lib/react-native-host-modules.mjs +1 -1
  88. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  89. package/dist-lib/sdk.cjs +5 -3
  90. package/dist-lib/sdk.mjs +5 -3
  91. package/dist-lib/skills.cjs +3932 -2868
  92. package/dist-lib/swift-compile.cjs +587 -374
  93. package/dist-lib/swift-compile.mjs +595 -369
  94. package/dist-lib/swift-package.cjs +1 -1
  95. package/dist-lib/swift-package.mjs +1 -1
  96. package/dist-lib/swift-submit.cjs +8 -3
  97. package/dist-lib/swift-submit.mjs +8 -3
  98. package/dist-lib/swift-toolchain.cjs +1 -1
  99. package/dist-lib/swift.cjs +1 -1
  100. package/dist-lib/vite-swift.cjs +2263 -1467
  101. package/dist-lib/vite.cjs +1 -1
  102. package/dist-lib/worklet-executable.cjs +1 -1
  103. package/dist-lib/worklet-executable.mjs +1 -1
  104. package/package.json +1 -1
  105. package/src/appBundleResourcesContract.ts +51 -0
  106. package/src/backend-origin.ts +11 -1
  107. package/src/host/bridge-host.ts +17 -11
  108. package/src/host/staged-host-files.ts +28 -4
  109. package/src/launchMetadataContract.ts +1 -0
  110. package/src/menu.ts +12 -4
  111. package/src/native-app-identity-project.ts +47 -38
  112. package/src/react-native-bridge-modules.ts +89 -76
  113. package/src/runtime-delivery.ts +6 -7
  114. package/src/swift-compile.ts +150 -112
  115. package/src/swift-compiler-digest.ts +5 -3
  116. package/src/swift-distribution.lock.json +9 -8
  117. package/src/swift-host.ts +6 -4
  118. package/src/swift-objc-compile.ts +416 -194
  119. package/src/swift-objc-metadata.ts +204 -0
  120. package/src/swift-objc-sdk.ts +195 -2
  121. package/src/swift-package-manifest.ts +6 -3
  122. package/src/swift-root-lock.ts +302 -0
  123. package/src/swift-submit.ts +15 -2
  124. package/src/swiftContract.ts +12 -0
  125. package/src/xcode-project.ts +77 -7
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1640 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1640 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1640 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/worklet-executable.ts
4
4
  import { parse } from "acorn";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.1640",
3
+ "version": "0.1.1644",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -0,0 +1,51 @@
1
+ import type { PeachBundleAssetReference } from './bundle-asset-resource'
2
+
3
+ // copied native resources keep their bundle-relative namespace, independently
4
+ // of the host URLs used to deliver their immutable bytes.
5
+ export type AppBundleResources = Record<string, PeachBundleAssetReference>
6
+
7
+ export function isAppBundleResourcePath(path: string): boolean {
8
+ return (
9
+ path.length > 0 &&
10
+ !path.includes('\0') &&
11
+ !/[\\?#]/.test(path) &&
12
+ path.split('/').every((part) => part !== '' && part !== '.' && part !== '..')
13
+ )
14
+ }
15
+
16
+ export function readAppBundleResources(wire: string | null): AppBundleResources {
17
+ if (wire === null) return {}
18
+ const value: unknown = JSON.parse(wire)
19
+ if (!value || typeof value !== 'object' || Array.isArray(value))
20
+ throw new Error('invalid native app bundle resources')
21
+ const resources: AppBundleResources = Object.create(null)
22
+ for (const [path, resource] of Object.entries(value)) {
23
+ if (
24
+ !isAppBundleResourcePath(path) ||
25
+ !resource ||
26
+ typeof resource !== 'object' ||
27
+ !('uri' in resource) ||
28
+ typeof resource.uri !== 'string' ||
29
+ !('sha256' in resource) ||
30
+ typeof resource.sha256 !== 'string' ||
31
+ !/^[a-f0-9]{64}$/.test(resource.sha256) ||
32
+ !('contentType' in resource) ||
33
+ typeof resource.contentType !== 'string' ||
34
+ !('byteLength' in resource) ||
35
+ typeof resource.byteLength !== 'number' ||
36
+ !Number.isSafeInteger(resource.byteLength) ||
37
+ resource.byteLength < 0
38
+ )
39
+ throw new Error('invalid native app bundle resource')
40
+ const url = new URL(resource.uri)
41
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
42
+ throw new Error('invalid native app bundle resource URL')
43
+ resources[path] = {
44
+ uri: url.href,
45
+ sha256: resource.sha256,
46
+ contentType: resource.contentType,
47
+ byteLength: resource.byteLength,
48
+ }
49
+ }
50
+ return resources
51
+ }
@@ -1,4 +1,4 @@
1
- const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://contrast.dev'
1
+ const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://peachsim.com'
2
2
 
3
3
  export const LOOPBACK_PROBE_HOSTNAMES = ['127.0.0.1', '::1'] as const
4
4
 
@@ -29,6 +29,14 @@ export function isLoopbackHost(hostname: string): boolean {
29
29
  )
30
30
  }
31
31
 
32
+ export function isLoopbackOrigin(origin: string): boolean {
33
+ try {
34
+ return isLoopbackHost(new URL(origin).hostname)
35
+ } catch {
36
+ return false
37
+ }
38
+ }
39
+
32
40
  // a browser page may use a local peach bridge (its proxies, websocket,
33
41
  // settings and account routes) only when this machine served the page: a
34
42
  // loopback origin, or the bridge's own host when a tunnel or reverse proxy
@@ -107,6 +115,8 @@ export function isContrastBackendHost(hostname: string): boolean {
107
115
  isLoopbackHost(normalized) ||
108
116
  normalized === 'contrast.dev' ||
109
117
  normalized.endsWith('.contrast.dev') ||
118
+ normalized === 'peachsim.com' ||
119
+ normalized === 'www.peachsim.com' ||
110
120
  normalized === PEACHSIM_NATIVE_HOST
111
121
  )
112
122
  }
@@ -1530,7 +1530,21 @@ export class SootSimBridgeHost {
1530
1530
  * non-upgrade routes that don't match serve index.html (SPA behavior) so
1531
1531
  * electron's webContents can navigate freely inside the runtime. */
1532
1532
  private handleHttpRequest(req: IncomingMessage, res: ServerResponse) {
1533
- if (rejectUntrustedBridgeOrigin(req, res)) return
1533
+ const method = (req.method || 'GET').toUpperCase()
1534
+ let url: URL
1535
+ try {
1536
+ url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
1537
+ } catch {
1538
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
1539
+ res.end('invalid request URL or host')
1540
+ return
1541
+ }
1542
+ const publicEngineFont =
1543
+ (method === 'GET' || method === 'HEAD') &&
1544
+ /^\/(?:runtime\/|sootsim\/)?engine\/fonts\/[^\\]*\.(ttf|otf|woff2?)$/.test(
1545
+ url.pathname,
1546
+ )
1547
+ if (!publicEngineFont && rejectUntrustedBridgeOrigin(req, res)) return
1534
1548
  if (req.url === '/__peach/fetch-transport') {
1535
1549
  res.writeHead(200, {
1536
1550
  'Content-Type': 'application/json',
@@ -1619,16 +1633,6 @@ export class SootSimBridgeHost {
1619
1633
  return
1620
1634
  }
1621
1635
 
1622
- const method = (req.method || 'GET').toUpperCase()
1623
- let url: URL
1624
- try {
1625
- url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
1626
- } catch {
1627
- res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
1628
- res.end('invalid request URL or host')
1629
- return
1630
- }
1631
-
1632
1636
  // /__sootsim/shared-config is the one awaited HTTP write. ordinary
1633
1637
  // settings fan out over WS, but destructive reloads need confirmation
1634
1638
  // that the disk source of truth accepted their final reset first.
@@ -1972,6 +1976,8 @@ export class SootSimBridgeHost {
1972
1976
  res.writeHead(200, {
1973
1977
  'Content-Type': contentType,
1974
1978
  'Cache-Control': 'no-store',
1979
+ // native file documents have an opaque origin when reading public fonts.
1980
+ ...(publicEngineFont ? { 'Access-Control-Allow-Origin': '*' } : {}),
1975
1981
  })
1976
1982
  if (method === 'HEAD') {
1977
1983
  res.end()
@@ -51,11 +51,21 @@ export function stageHostBytes(
51
51
  origin: string,
52
52
  extension = '',
53
53
  ): string {
54
- const digest = createHash('sha256').update(bytes).digest('hex')
55
- extension = extension.toLowerCase()
56
- const name = STAGED_NAME.test(digest + extension) ? digest + extension : digest
54
+ const directory = prepareStagedDirectory()
55
+ return writeStagedBytes(bytes, origin, extension, directory)
56
+ }
57
+
58
+ // a resource pack prunes once, rather than scanning the cache for every file.
59
+ export function stageHostByteFiles(
60
+ files: readonly Uint8Array[],
61
+ origin: string,
62
+ ): string[] {
63
+ const directory = prepareStagedDirectory()
64
+ return files.map((bytes) => writeStagedBytes(bytes, origin, '', directory))
65
+ }
66
+
67
+ function prepareStagedDirectory(): string {
57
68
  const directory = stagedDir()
58
- const destination = path.join(directory, name)
59
69
  fs.mkdirSync(directory, { recursive: true })
60
70
  const now = Date.now()
61
71
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
@@ -63,6 +73,20 @@ export function stageHostBytes(
63
73
  if (entry.isFile() && now - fs.statSync(stale).mtimeMs > STAGED_RETENTION_MS)
64
74
  fs.rmSync(stale, { force: true })
65
75
  }
76
+ return directory
77
+ }
78
+
79
+ function writeStagedBytes(
80
+ bytes: Uint8Array,
81
+ origin: string,
82
+ extension: string,
83
+ directory: string,
84
+ ): string {
85
+ const digest = createHash('sha256').update(bytes).digest('hex')
86
+ extension = extension.toLowerCase()
87
+ const name = STAGED_NAME.test(digest + extension) ? digest + extension : digest
88
+ const destination = path.join(directory, name)
89
+ const now = Date.now()
66
90
  if (fs.existsSync(destination)) {
67
91
  fs.utimesSync(destination, new Date(now), new Date(now))
68
92
  } else {
@@ -6,6 +6,7 @@ export const LAUNCH_METADATA_KEYS: readonly string[] = [
6
6
  'appFonts',
7
7
  'appSplash',
8
8
  'appCatalog',
9
+ 'appBundleResources',
9
10
  'nativeAppIdentity',
10
11
  'nativeFirebase',
11
12
  'appManifest',
package/src/menu.ts CHANGED
@@ -71,7 +71,13 @@ export const SOOTSIM_RENDER_RESOLUTION_LABELS: Record<SootSimRenderResolution, s
71
71
  // engine's wireframe store draws. 'wireframe' is the clean structural sketch;
72
72
  // the others are its screen-space styles that ship as named effects.
73
73
  export type SootSimScreenEffect = 'none' | 'wireframe' | 'blueprint' | 'panel' | 'green'
74
- export const SOOTSIM_SCREEN_EFFECTS = ['none', 'wireframe', 'blueprint', 'panel', 'green'] as const
74
+ export const SOOTSIM_SCREEN_EFFECTS = [
75
+ 'none',
76
+ 'wireframe',
77
+ 'blueprint',
78
+ 'panel',
79
+ 'green',
80
+ ] as const
75
81
 
76
82
  export function isSootSimScreenEffect(value: unknown): value is SootSimScreenEffect {
77
83
  return (
@@ -373,9 +379,9 @@ function deviceMenu(
373
379
  },
374
380
  { type: 'separator' },
375
381
  { label: 'Trigger Screenshot', action: 'screenshot' },
376
- // pose controls for the current device: open/close on foldables,
377
- // rotate left/right wherever a turn changes geometry, the way ios
378
- // simulator holds them (rotate left is cmd+left, rotate right
382
+ // pose controls for the current device: open/close, book and laptop on
383
+ // foldables, rotate left/right wherever a turn changes geometry, the way
384
+ // ios simulator holds them (rotate left is cmd+left, rotate right
379
385
  // cmd+right). truthiness, not !== null: a partial state must hide
380
386
  // these, never show duo items for the wrong device.
381
387
  ...(state.devicePosture || state.canRotate
@@ -390,6 +396,8 @@ function deviceMenu(
390
396
  : 'Open iPhone Duo',
391
397
  action: 'toggle-duo-posture',
392
398
  },
399
+ { label: 'Book Pose', action: 'fold-duo-book' },
400
+ { label: 'Laptop Pose', action: 'fold-duo-laptop' },
393
401
  ]
394
402
  : []),
395
403
  ...(state.canRotate
@@ -326,44 +326,8 @@ function readIosNativeAppIdentity(
326
326
  parseInfoPlist: (source: string) => unknown,
327
327
  iosTarget: string | undefined,
328
328
  ): NativeAppIdentity | null {
329
- const iosRoot = path.join(projectRoot, 'ios')
330
- if (!fs.existsSync(iosRoot)) return null
331
- const projects = fs
332
- .readdirSync(iosRoot, { withFileTypes: true })
333
- .filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
334
- if (projects.length === 0) return null
335
- if (projects.length > 1 && !iosTarget) {
336
- throw new NativeAppTargetSelectionError(
337
- `native app identity requires a selected Xcode project; set withPeach's iosTarget to an app target in one of: ${projects.map((entry) => entry.name).join(', ')}`,
338
- )
339
- }
340
- let project = projects[0]!
341
- if (iosTarget && projects.length > 1) {
342
- const matches = projects.filter((entry) =>
343
- listXcodeAppTargets(path.join(iosRoot, entry.name), {
344
- requireSources: false,
345
- }).includes(iosTarget),
346
- )
347
- if (matches.length === 0) {
348
- throw new Error(`native app identity has no application target ${iosTarget}`)
349
- }
350
- if (matches.length > 1) {
351
- throw new Error(
352
- `native app identity target ${iosTarget} is ambiguous across ${matches.map((entry) => entry.name).join(', ')}`,
353
- )
354
- }
355
- project = matches[0]!
356
- }
357
- const xcodeproj = path.join(iosRoot, project.name)
358
- const appTargets = listXcodeAppTargets(xcodeproj, { requireSources: false })
359
- if (!iosTarget && appTargets.length > 1) {
360
- throw new NativeAppTargetSelectionError(
361
- `native app identity requires a selected app target; set withPeach's iosTarget to one of: ${appTargets.join(', ')}`,
362
- )
363
- }
364
- const app = readXcodeAppTarget(xcodeproj, iosTarget ?? appTargets[0], 'Release', {
365
- requireSources: false,
366
- })
329
+ const app = readIosAppTarget(projectRoot, iosTarget)
330
+ if (!app) return null
367
331
  const identity = readNativeAppIdentityFromXcodeTarget(app, parseInfoPlist)
368
332
  const projectManifestPath = path.join(projectRoot, 'package.json')
369
333
  const projectRequire = createRequire(projectManifestPath)
@@ -422,3 +386,48 @@ function readIosNativeAppIdentity(
422
386
  }
423
387
  return { ...identity, reanimatedStaticFeatureFlags: flags }
424
388
  }
389
+
390
+ // native identity and copied resources must select the same application target.
391
+ export function readIosAppTarget(
392
+ projectRoot: string,
393
+ iosTarget?: string,
394
+ ): XcodeAppTarget | null {
395
+ const iosRoot = path.join(projectRoot, 'ios')
396
+ if (!fs.existsSync(iosRoot)) return null
397
+ const projects = fs
398
+ .readdirSync(iosRoot, { withFileTypes: true })
399
+ .filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
400
+ if (projects.length === 0) return null
401
+ if (projects.length > 1 && !iosTarget) {
402
+ throw new NativeAppTargetSelectionError(
403
+ `native app identity requires a selected Xcode project; set withPeach's iosTarget to an app target in one of: ${projects.map((entry) => entry.name).join(', ')}`,
404
+ )
405
+ }
406
+ let project = projects[0]!
407
+ if (iosTarget && projects.length > 1) {
408
+ const matches = projects.filter((entry) =>
409
+ listXcodeAppTargets(path.join(iosRoot, entry.name), {
410
+ requireSources: false,
411
+ }).includes(iosTarget),
412
+ )
413
+ if (matches.length === 0) {
414
+ throw new Error(`native app identity has no application target ${iosTarget}`)
415
+ }
416
+ if (matches.length > 1) {
417
+ throw new Error(
418
+ `native app identity target ${iosTarget} is ambiguous across ${matches.map((entry) => entry.name).join(', ')}`,
419
+ )
420
+ }
421
+ project = matches[0]!
422
+ }
423
+ const xcodeproj = path.join(iosRoot, project.name)
424
+ const appTargets = listXcodeAppTargets(xcodeproj, { requireSources: false })
425
+ if (!iosTarget && appTargets.length > 1) {
426
+ throw new NativeAppTargetSelectionError(
427
+ `native app identity requires a selected app target; set withPeach's iosTarget to one of: ${appTargets.join(', ')}`,
428
+ )
429
+ }
430
+ return readXcodeAppTarget(xcodeproj, iosTarget ?? appTargets[0], 'Release', {
431
+ requireSources: false,
432
+ })
433
+ }
@@ -6,7 +6,11 @@
6
6
  import fs from 'node:fs'
7
7
  import path from 'node:path'
8
8
  import { swiftCodeMask, swiftSourceClosure } from './swift-compile'
9
- import { listXcodeAppTargets, readXcodeAppTarget } from './xcode-project'
9
+ import {
10
+ findXcodeProject,
11
+ listXcodeAppTargets,
12
+ readXcodeAppTarget,
13
+ } from './xcode-project'
10
14
  import type { ExpoSwiftSourceUnit } from './expoSwiftContract'
11
15
 
12
16
  interface MethodInfo {
@@ -312,9 +316,9 @@ function swiftClassFor(objcName: string, files: Record<string, string>): string
312
316
  export const BRIDGE_MODULE_FILE_PREFIX = '__peach_react_module_'
313
317
 
314
318
  /**
315
- * the app target's legacy bridge modules, one compile unit each, or null
316
- * when the app has no iOS project or registers none. a module the host
317
- * cannot provide is a unit with a diagnostic instead of files.
319
+ * the app and its framework targets' legacy bridge modules, one compile unit
320
+ * each, or null when the app has no iOS project or registers none. a module
321
+ * the host cannot provide is a unit with a diagnostic instead of files.
318
322
  */
319
323
  export function collectBridgeModuleSources(
320
324
  appRoot: string,
@@ -327,20 +331,13 @@ export function collectBridgeModuleSources(
327
331
  const iosRoot = path.join(appRoot, 'ios')
328
332
  if (!fs.existsSync(iosRoot)) return null
329
333
  const projectRoot = fs.realpathSync(appRoot)
330
- const projects = fs
331
- .readdirSync(iosRoot, { withFileTypes: true })
332
- .filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
333
- .map((entry) => path.join(iosRoot, entry.name))
334
- const targets = projects.flatMap((project) =>
335
- listXcodeAppTargets(project, { requireSources: false }).map((name) => ({
336
- project,
337
- name,
338
- })),
339
- )
334
+ const project = findXcodeProject(iosRoot)
335
+ if (!project) return null
336
+ const targets = listXcodeAppTargets(project, { requireSources: false })
340
337
  // RCTRegisterModule runs in the one app binary; several targets are
341
338
  // several apps, and nothing here says which one this bundle runs in
342
339
  if (targets.length !== 1) return null
343
- const app = readXcodeAppTarget(targets[0]!.project, targets[0]!.name, 'Release', {
340
+ const app = readXcodeAppTarget(project, targets[0]!, 'Release', {
344
341
  requireSources: false,
345
342
  })
346
343
  const own = (file: string) => {
@@ -360,72 +357,88 @@ export function collectBridgeModuleSources(
360
357
  throw new Error(`${file}: app native source outside ${projectRoot}`)
361
358
  return true
362
359
  }
363
- const objc = app.allSources.filter((file) => /\.mm?$/.test(file) && own(file))
364
- if (objc.length === 0) return null
365
- // headers declare the superclasses; they sit beside the sources that import them
366
- const headers = [...new Set(objc.map((file) => path.dirname(file)))].flatMap((dir) =>
367
- fs
368
- .readdirSync(dir)
369
- .filter((name) => name.endsWith('.h'))
370
- .sort()
371
- .map((name) => path.join(dir, name))
372
- .filter(own),
373
- )
374
- const declarations = parseObjectiveCModules(
375
- [...objc, ...headers].map((file) => ({ file, text: fs.readFileSync(file, 'utf8') })),
376
- )
377
- if (declarations.modules.length === 0) return null
378
- const namespace = app.moduleName.replace(/[^A-Za-z0-9_]/g, '_')
379
- const swiftFiles: Record<string, string> = {}
380
- for (const file of app.sources.filter(own)) {
381
- const relative = path.relative(projectRoot, fs.realpathSync(file))
382
- swiftFiles[`Sources/${namespace}/${relative.split(path.sep).join('/')}`] =
383
- `#sourceLocation(file: ${JSON.stringify(file)}, line: 1)\n${fs.readFileSync(file, 'utf8')}`
384
- }
385
360
  const units: ExpoSwiftSourceUnit[] = []
386
361
  const failures: { namespace: string; diagnostic: string }[] = []
387
- const seen = new Set<string>()
388
- for (const module of declarations.modules) {
362
+ // the reader owns dependency order and visits a shared framework only once.
363
+ // each target keeps its own Swift module identity and compilation settings.
364
+ for (const target of [...app.frameworks, app]) {
389
365
  signal?.throwIfAborted()
390
- if (seen.has(module.className)) continue
391
- seen.add(module.className)
392
- const moduleId = `${namespace}.${module.className}`
393
- const methods = declarations.methods.get(module.className) ?? []
394
- const swiftClass = module.extern ? swiftClassFor(module.className, swiftFiles) : null
395
- const unit: ExpoSwiftSourceUnit = {
396
- namespace,
397
- moduleId,
398
- classes: [],
399
- bridgeModules: [],
400
- files: {},
401
- frameworks: [],
402
- swiftLanguageMode: app.languageMode,
366
+ const objc = [
367
+ ...new Set(
368
+ target.allSources
369
+ .filter((file) => /\.mm?$/.test(file) && own(file))
370
+ .map((file) => fs.realpathSync(file)),
371
+ ),
372
+ ]
373
+ if (objc.length === 0) continue
374
+ // headers declare the superclasses; they sit beside the sources that import them
375
+ const headers = [...new Set(objc.map((file) => path.dirname(file)))].flatMap((dir) =>
376
+ fs
377
+ .readdirSync(dir)
378
+ .filter((name) => name.endsWith('.h'))
379
+ .sort()
380
+ .map((name) => path.join(dir, name))
381
+ .filter(own),
382
+ )
383
+ const declarations = parseObjectiveCModules(
384
+ [...objc, ...headers].map((file) => ({
385
+ file,
386
+ text: fs.readFileSync(file, 'utf8'),
387
+ })),
388
+ )
389
+ if (declarations.modules.length === 0) continue
390
+ const namespace = target.moduleName.replace(/[^A-Za-z0-9_]/g, '_')
391
+ const swiftFiles: Record<string, string> = {}
392
+ for (const file of target.sources.filter(own)) {
393
+ const relative = path.relative(projectRoot, fs.realpathSync(file))
394
+ swiftFiles[`Sources/${namespace}/${relative.split(path.sep).join('/')}`] =
395
+ `#sourceLocation(file: ${JSON.stringify(file)}, line: 1)\n${fs.readFileSync(file, 'utf8')}`
403
396
  }
404
- if (swiftClass) {
405
- const register = `__peachReactModule_${module.className}`
406
- unit.files = swiftSourceClosure(swiftFiles, [swiftClass])
407
- unit.files[
408
- `Sources/${namespace}/${BRIDGE_MODULE_FILE_PREFIX}${module.className}.swift`
409
- ] =
410
- `import React\n\n// ${module.file}: RCT_EXTERN_MODULE(${module.className})\npublic func ${register}() -> RCTModuleHost.Registration {\n RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, make: { ${swiftClass}() })\n}\n`
411
- unit.bridgeModules = [`${namespace}.${register}()`]
412
- } else {
413
- const superclass = compiledSuperclass(module.className, declarations.superclasses)
414
- const selectors = declarations.implementations.get(module.className)
415
- if (!selectors || !superclass) {
416
- failures.push({
417
- namespace: moduleId,
418
- diagnostic: !selectors
419
- ? `${module.file}: no Swift class or Objective-C @implementation provides ${module.className}`
420
- : `${module.file}: ${module.className}'s superclass ${declarations.superclasses.get(module.className) ?? '(none)'} is Objective-C, which Peach does not compile`,
421
- })
422
- continue
397
+ const seen = new Set<string>()
398
+ for (const module of declarations.modules) {
399
+ signal?.throwIfAborted()
400
+ if (seen.has(module.className)) continue
401
+ seen.add(module.className)
402
+ const moduleId = `${namespace}.${module.className}`
403
+ const methods = declarations.methods.get(module.className) ?? []
404
+ const swiftClass = module.extern
405
+ ? swiftClassFor(module.className, swiftFiles)
406
+ : null
407
+ const unit: ExpoSwiftSourceUnit = {
408
+ namespace,
409
+ moduleId,
410
+ classes: [],
411
+ bridgeModules: [],
412
+ files: {},
413
+ frameworks: [],
414
+ swiftLanguageMode: target.languageMode,
415
+ }
416
+ if (swiftClass) {
417
+ const register = `__peachReactModule_${module.className}`
418
+ unit.files = swiftSourceClosure(swiftFiles, [swiftClass])
419
+ unit.files[
420
+ `Sources/${namespace}/${BRIDGE_MODULE_FILE_PREFIX}${module.className}.swift`
421
+ ] =
422
+ `import React\n\n// ${module.file}: RCT_EXTERN_MODULE(${module.className})\npublic func ${register}() -> RCTModuleHost.Registration {\n RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, make: { ${swiftClass}() })\n}\n`
423
+ unit.bridgeModules = [`${namespace}.${register}()`]
424
+ } else {
425
+ const superclass = compiledSuperclass(module.className, declarations.superclasses)
426
+ const selectors = declarations.implementations.get(module.className)
427
+ if (!selectors || !superclass) {
428
+ failures.push({
429
+ namespace: moduleId,
430
+ diagnostic: !selectors
431
+ ? `${module.file}: no Swift class or Objective-C @implementation provides ${module.className}`
432
+ : `${module.file}: ${module.className}'s superclass ${declarations.superclasses.get(module.className) ?? '(none)'} is Objective-C, which Peach does not compile`,
433
+ })
434
+ continue
435
+ }
436
+ unit.bridgeModules = [
437
+ `RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, objectiveCSelectors: ${JSON.stringify([...selectors].sort())}, superclass: { ${superclass}() })`,
438
+ ]
423
439
  }
424
- unit.bridgeModules = [
425
- `RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, objectiveCSelectors: ${JSON.stringify([...selectors].sort())}, superclass: { ${superclass}() })`,
426
- ]
440
+ units.push(unit)
427
441
  }
428
- units.push(unit)
429
442
  }
430
- return { units, failures }
443
+ return units.length === 0 && failures.length === 0 ? null : { units, failures }
431
444
  }
@@ -3,7 +3,7 @@
3
3
  // the machinery itself (hosted manifest, sha256-verified tarballs, channels,
4
4
  // channel resolution, and auto-update) lives in @contrast/runtime-delivery and
5
5
  // knows nothing about peach. this file supplies peach's identity, public env
6
- // overrides, and the contrast.dev/runtimes hosting layout.
6
+ // overrides, and the peachsim.com/runtimes hosting layout.
7
7
  //
8
8
  // @contrast/runtime-delivery is an internal workspace package that is never
9
9
  // published, so both the CLI bundle and dist-lib inline it (see the `alias`
@@ -20,14 +20,13 @@ import {
20
20
  runtimesDir,
21
21
  writeActiveRuntime,
22
22
  } from './home-paths.ts'
23
- import { rnxPublicBrand } from './public-brand.ts'
23
+ import { peachPublicBrand } from './public-brand.ts'
24
24
 
25
- export const DEFAULT_RUNTIME_CDN_ORIGIN = 'https://contrast.dev'
25
+ export const DEFAULT_RUNTIME_CDN_ORIGIN = peachPublicBrand.origin
26
26
 
27
- /** the official curated changelog and the machine-readable release feed. the
28
- * peach site is static, so its /api traffic is owned by contrast.dev. */
29
- export const RNX_CHANGELOG_URL = `${rnxPublicBrand.origin}/changelog`
30
- export const RNX_CHANGELOG_API_URL = 'https://contrast.dev/api/changelog/sootsim'
27
+ /** the official curated changelog and the machine-readable release feed. */
28
+ export const RNX_CHANGELOG_URL = `${peachPublicBrand.origin}/changelog`
29
+ export const RNX_CHANGELOG_API_URL = `${peachPublicBrand.origin}/api/changelog/sootsim`
31
30
 
32
31
  export const RNX_RUNTIME_PRODUCT: RuntimeProduct = {
33
32
  id: 'peach',