peachsim 0.1.1635 → 0.1.1641

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 (142) hide show
  1. package/README.md +10 -1
  2. package/cli/app-config.ts +14 -5
  3. package/cli/app-stubs.ts +96 -22
  4. package/cli/bridge-flow-runner.ts +2 -2
  5. package/cli/cloud-client.ts +60 -40
  6. package/cli/cloud-session.ts +16 -0
  7. package/cli/commands/assert.ts +17 -4
  8. package/cli/commands/box.ts +33 -19
  9. package/cli/commands/doctor.ts +37 -20
  10. package/cli/commands/flow.ts +1 -1
  11. package/cli/commands/inspect/actions.ts +3 -2
  12. package/cli/commands/inspect/core.ts +0 -8
  13. package/cli/commands/inspect/shared.ts +9 -4
  14. package/cli/commands/inspect.ts +65 -61
  15. package/cli/commands/platform.ts +2 -1
  16. package/cli/commands/preview.ts +1 -1
  17. package/cli/commands/record.ts +154 -68
  18. package/cli/commands/swift.ts +18 -4
  19. package/cli/commands/three-mode.ts +4 -38
  20. package/cli/commands/upload.ts +55 -49
  21. package/cli/drivers/playwright-sim-host.ts +65 -42
  22. package/cli/flow-live-status.ts +0 -2
  23. package/cli/main.ts +2 -2
  24. package/cli/outbound-endpoints.ts +7 -5
  25. package/cli/pause-aware-timeout.ts +4 -0
  26. package/cli/ws-bridge.ts +19 -16
  27. package/detox/attempt-events.cjs +99 -6
  28. package/detox/attempt-events.d.cts +7 -0
  29. package/detox/expectations.ts +7 -3
  30. package/detox/index.ts +259 -139
  31. package/detox/jest-environment.cjs +5 -1
  32. package/detox/motion-change.cjs +170 -21
  33. package/detox/motion-change.d.cts +32 -0
  34. package/detox/navigation-lifecycle.ts +35 -3
  35. package/dist-lib/agent-daemon-client.cjs +1 -1
  36. package/dist-lib/agent-events.cjs +1 -1
  37. package/dist-lib/attached-projects.cjs +1 -1
  38. package/dist-lib/auth/shared-session.cjs +1 -1
  39. package/dist-lib/backend-origin.cjs +17 -7
  40. package/dist-lib/beta.cjs +1 -1
  41. package/dist-lib/beta.mjs +1 -1
  42. package/dist-lib/bridge-constants.cjs +1 -1
  43. package/dist-lib/bridge-contract-input.cjs +1 -1
  44. package/dist-lib/bridge-contract-input.mjs +1 -1
  45. package/dist-lib/bridge-contract.cjs +1 -1
  46. package/dist-lib/bridge-contract.mjs +1 -1
  47. package/dist-lib/bundle-channel.cjs +5014 -2268
  48. package/dist-lib/capture-contract.cjs +1 -1
  49. package/dist-lib/capture-contract.mjs +1 -1
  50. package/dist-lib/cloud-contract.cjs +1 -1
  51. package/dist-lib/cloud-contract.mjs +1 -1
  52. package/dist-lib/cloud-test-args.cjs +1 -1
  53. package/dist-lib/cloud-test-args.mjs +1 -1
  54. package/dist-lib/cloud.cjs +1 -1
  55. package/dist-lib/cloud.mjs +1 -1
  56. package/dist-lib/config.cjs +1 -1
  57. package/dist-lib/config.mjs +1 -1
  58. package/dist-lib/detox/index.cjs +1036 -166
  59. package/dist-lib/detox/jest-environment.cjs +5 -1
  60. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  61. package/dist-lib/expo-swift-bundle.cjs +4613 -1963
  62. package/dist-lib/expo-swift-metadata.cjs +9 -7
  63. package/dist-lib/expo-swift-metadata.mjs +9 -7
  64. package/dist-lib/home-paths.cjs +1 -1
  65. package/dist-lib/host/bridge-host.cjs +58072 -16400
  66. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  67. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  68. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  69. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  70. package/dist-lib/host/websocket-proxy.cjs +1 -1
  71. package/dist-lib/index.cjs +2790 -143
  72. package/dist-lib/jump-to-source-babel.cjs +1 -1
  73. package/dist-lib/jump-to-source-native.cjs +1 -1
  74. package/dist-lib/kotlin-host.cjs +1 -1
  75. package/dist-lib/menu.cjs +1 -1
  76. package/dist-lib/menu.mjs +1 -1
  77. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  78. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  79. package/dist-lib/metro-production-bundle.cjs +1 -1
  80. package/dist-lib/metro-production-bundle.mjs +1 -1
  81. package/dist-lib/metro.cjs +2579 -95
  82. package/dist-lib/native-app-identity-project.cjs +243 -53
  83. package/dist-lib/native-app-identity-project.mjs +243 -53
  84. package/dist-lib/native-app-identity.cjs +29 -30
  85. package/dist-lib/native-app-identity.mjs +29 -30
  86. package/dist-lib/native-firebase-metadata.cjs +1 -1
  87. package/dist-lib/native-firebase-metadata.mjs +1 -1
  88. package/dist-lib/plist.cjs +1 -1
  89. package/dist-lib/plist.mjs +1 -1
  90. package/dist-lib/profiles.cjs +1 -1
  91. package/dist-lib/public-brand.cjs +1 -1
  92. package/dist-lib/react-native-host-modules.cjs +2 -2
  93. package/dist-lib/react-native-host-modules.mjs +2 -2
  94. package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
  95. package/dist-lib/sdk.cjs +4 -3
  96. package/dist-lib/sdk.mjs +4 -3
  97. package/dist-lib/skills.cjs +66475 -23097
  98. package/dist-lib/swift-compile.cjs +452 -260
  99. package/dist-lib/swift-compile.mjs +451 -252
  100. package/dist-lib/swift-package.cjs +1 -1
  101. package/dist-lib/swift-package.mjs +1 -1
  102. package/dist-lib/swift-submit.cjs +14 -7
  103. package/dist-lib/swift-submit.mjs +14 -7
  104. package/dist-lib/swift-toolchain.cjs +316 -41
  105. package/dist-lib/swift.cjs +1 -1
  106. package/dist-lib/vite-swift.cjs +8188 -5636
  107. package/dist-lib/vite.cjs +2857 -12
  108. package/dist-lib/worklet-executable.cjs +14 -2
  109. package/dist-lib/worklet-executable.mjs +14 -2
  110. package/package.json +1 -1
  111. package/src/backend-origin.ts +21 -6
  112. package/src/expo-swift-host.ts +5 -2
  113. package/src/expoKotlinContract.ts +4 -7
  114. package/src/expoSwiftContract.ts +4 -6
  115. package/src/host/staged-host-files.ts +9 -0
  116. package/src/native-app-identity-project.ts +1 -4
  117. package/src/native-app-identity.ts +1 -4
  118. package/src/nativeAppIconContract.ts +1 -4
  119. package/src/output-lock-stdio.ts +36 -0
  120. package/src/react-native-bridge-modules.ts +106 -78
  121. package/src/react-native-host-modules.ts +4 -1
  122. package/src/settings-bundle.ts +1 -4
  123. package/src/swift-compile.ts +224 -139
  124. package/src/swift-distribution.lock.json +23 -12
  125. package/src/swift-distribution.ts +103 -4
  126. package/src/swift-host.ts +95 -30
  127. package/src/swift-icu.ts +16 -10
  128. package/src/swift-objc-compile.ts +547 -0
  129. package/src/swift-objc-metadata.ts +204 -0
  130. package/src/swift-objc-sdk.ts +883 -19
  131. package/src/swift-objc-standin.ts +126 -14
  132. package/src/swift-package-manifest.ts +42 -1
  133. package/src/swift-packages.ts +35 -21
  134. package/src/swift-root-lock.ts +153 -0
  135. package/src/swift-standin-emit.ts +40 -7
  136. package/src/swift-standins.ts +404 -138
  137. package/src/swift-submit.ts +18 -7
  138. package/src/swift-toolchain.ts +378 -46
  139. package/src/swiftContract.ts +12 -0
  140. package/src/worker-system-chunks.ts +11 -12
  141. package/src/worklet-executable.ts +19 -1
  142. package/src/xcode-project.ts +280 -33
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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;
@@ -185,6 +185,8 @@ function enter(node, scope) {
185
185
  const name = identifierName(id);
186
186
  if (name !== void 0 && isNode(init))
187
187
  declare(target, name, { kind: "value", init, scope });
188
+ else if (name !== void 0 && init === null)
189
+ declare(target, name, { kind: "uninitialized" });
188
190
  else declareOpaque(target, patternNames(id));
189
191
  }
190
192
  return scope;
@@ -254,12 +256,18 @@ function collectInitData(root) {
254
256
  immediate: /* @__PURE__ */ new Map(),
255
257
  writes: [],
256
258
  reassigned: /* @__PURE__ */ new Set(),
259
+ assigned: /* @__PURE__ */ new Map(),
257
260
  free: /* @__PURE__ */ new Map()
258
261
  };
259
262
  visit(root, newScope(void 0, true), collection);
260
263
  for (const { name, scope, value } of collection.writes) {
261
264
  const binding = lookup(scope, name);
262
- if (binding) collection.reassigned.add(binding);
265
+ if (binding?.kind === "uninitialized")
266
+ collection.assigned.set(
267
+ binding,
268
+ collection.assigned.has(binding) || !value ? null : { value, scope }
269
+ );
270
+ else if (binding) collection.reassigned.add(binding);
263
271
  else
264
272
  collection.free.set(
265
273
  name,
@@ -320,6 +328,10 @@ function resolveObject(expression, scope, collection, depth) {
320
328
  return free ? resolveObject(free.value, free.scope, collection, depth + 1) : void 0;
321
329
  }
322
330
  if (collection.reassigned.has(binding)) return void 0;
331
+ if (binding.kind === "uninitialized") {
332
+ const assigned = collection.assigned.get(binding);
333
+ return assigned ? resolveObject(assigned.value, assigned.scope, collection, depth + 1) : void 0;
334
+ }
323
335
  if (binding.kind === "value")
324
336
  return resolveObject(binding.init, binding.scope, collection, depth + 1);
325
337
  if (binding.kind === "param") {
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/worklet-executable.ts
4
4
  import { parse } from "acorn";
@@ -160,6 +160,8 @@ function enter(node, scope) {
160
160
  const name = identifierName(id);
161
161
  if (name !== void 0 && isNode(init))
162
162
  declare(target, name, { kind: "value", init, scope });
163
+ else if (name !== void 0 && init === null)
164
+ declare(target, name, { kind: "uninitialized" });
163
165
  else declareOpaque(target, patternNames(id));
164
166
  }
165
167
  return scope;
@@ -229,12 +231,18 @@ function collectInitData(root) {
229
231
  immediate: /* @__PURE__ */ new Map(),
230
232
  writes: [],
231
233
  reassigned: /* @__PURE__ */ new Set(),
234
+ assigned: /* @__PURE__ */ new Map(),
232
235
  free: /* @__PURE__ */ new Map()
233
236
  };
234
237
  visit(root, newScope(void 0, true), collection);
235
238
  for (const { name, scope, value } of collection.writes) {
236
239
  const binding = lookup(scope, name);
237
- if (binding) collection.reassigned.add(binding);
240
+ if (binding?.kind === "uninitialized")
241
+ collection.assigned.set(
242
+ binding,
243
+ collection.assigned.has(binding) || !value ? null : { value, scope }
244
+ );
245
+ else if (binding) collection.reassigned.add(binding);
238
246
  else
239
247
  collection.free.set(
240
248
  name,
@@ -295,6 +303,10 @@ function resolveObject(expression, scope, collection, depth) {
295
303
  return free ? resolveObject(free.value, free.scope, collection, depth + 1) : void 0;
296
304
  }
297
305
  if (collection.reassigned.has(binding)) return void 0;
306
+ if (binding.kind === "uninitialized") {
307
+ const assigned = collection.assigned.get(binding);
308
+ return assigned ? resolveObject(assigned.value, assigned.scope, collection, depth + 1) : void 0;
309
+ }
298
310
  if (binding.kind === "value")
299
311
  return resolveObject(binding.init, binding.scope, collection, depth + 1);
300
312
  if (binding.kind === "param") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.1635",
3
+ "version": "0.1.1641",
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",
@@ -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
@@ -98,12 +106,16 @@ export function isBrowserAssetProxyUrl(value: string): boolean {
98
106
  }
99
107
  }
100
108
 
109
+ // the PeachSim app's host; the backend serves every path on it
110
+ export const PEACHSIM_NATIVE_HOST = 'native.peachsim.com'
111
+
101
112
  export function isContrastBackendHost(hostname: string): boolean {
102
113
  const normalized = normalizeHostname(hostname)
103
114
  return (
104
115
  isLoopbackHost(normalized) ||
105
116
  normalized === 'contrast.dev' ||
106
- normalized.endsWith('.contrast.dev')
117
+ normalized.endsWith('.contrast.dev') ||
118
+ normalized === PEACHSIM_NATIVE_HOST
107
119
  )
108
120
  }
109
121
 
@@ -115,15 +127,18 @@ export function isContrastBackendOrigin(origin: string): boolean {
115
127
  }
116
128
  }
117
129
 
118
- export function resolveContrastBackendOrigin(currentOrigin?: string | null): string {
119
- const origin = currentOrigin?.trim()
130
+ // a page's `?backend=` names another backend origin, judged by the same rule
131
+ export function resolveContrastBackendOrigin(
132
+ currentOrigin?: string | null,
133
+ search?: string | null,
134
+ ): string {
135
+ const origin =
136
+ new URLSearchParams(search ?? '').get('backend')?.trim() || currentOrigin?.trim()
120
137
  if (!origin) return DEFAULT_CONTRAST_BACKEND_ORIGIN
121
138
 
122
139
  try {
123
140
  const parsed = new URL(origin)
124
- if (isLoopbackHost(parsed.hostname) || isContrastBackendHost(parsed.hostname)) {
125
- return parsed.origin
126
- }
141
+ if (isContrastBackendHost(parsed.hostname)) return parsed.origin
127
142
  } catch {}
128
143
 
129
144
  return DEFAULT_CONTRAST_BACKEND_ORIGIN
@@ -136,7 +136,10 @@ export async function collectExpoSwiftSources(
136
136
  let bytes = 0
137
137
  let fileCount = 0
138
138
  // native Expo resolves packages by name before generating its Apple provider.
139
- for (const [, entry] of Object.entries(search).sort(([a], [b]) => a.localeCompare(b))) {
139
+ // the search result is keyed by that name; older autolinkers add no name field.
140
+ for (const [packageName, entry] of Object.entries(search).sort(([a], [b]) =>
141
+ a.localeCompare(b),
142
+ )) {
140
143
  if (!entry || typeof entry !== 'object')
141
144
  throw new Error('invalid Expo autolinking entry')
142
145
  const sourceRoot = Reflect.get(entry, 'path')
@@ -149,7 +152,7 @@ export async function collectExpoSwiftSources(
149
152
  (candidate) =>
150
153
  candidate &&
151
154
  typeof candidate === 'object' &&
152
- Reflect.get(candidate, 'packageName') === Reflect.get(entry, 'name'),
155
+ Reflect.get(candidate, 'packageName') === packageName,
153
156
  )
154
157
  const namespaces =
155
158
  resolution && typeof resolution === 'object'
@@ -1,4 +1,5 @@
1
1
  import { insertBundleGlobalFooter, readBundleGlobalFooter } from './bundleGlobalFooter'
2
+ import { isRecord } from './is-record'
2
3
 
3
4
  // what the dev server answers for an app's local Expo Kotlin modules on an
4
5
  // Android device: one linked Kotlin/Wasm process, or each module's diagnostic.
@@ -10,14 +11,10 @@ export interface ExpoKotlinCompilationResult {
10
11
  failures: { namespace: string; diagnostic: string }[]
11
12
  }
12
13
 
13
- function record(value: unknown): value is Record<string, unknown> {
14
- return value !== null && typeof value === 'object' && !Array.isArray(value)
15
- }
16
-
17
14
  export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationResult {
18
15
  const invalid = () => new Error('invalid Expo Kotlin compilation metadata')
19
16
  if (
20
- !record(value) ||
17
+ !isRecord(value) ||
21
18
  !Array.isArray(value.namespaces) ||
22
19
  !Array.isArray(value.failures)
23
20
  )
@@ -30,7 +27,7 @@ export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationRes
30
27
  const failures: ExpoKotlinCompilationResult['failures'] = []
31
28
  for (const failure of value.failures) {
32
29
  if (
33
- !record(failure) ||
30
+ !isRecord(failure) ||
34
31
  typeof failure.namespace !== 'string' ||
35
32
  typeof failure.diagnostic !== 'string'
36
33
  )
@@ -40,7 +37,7 @@ export function readExpoKotlinMetadata(value: unknown): ExpoKotlinCompilationRes
40
37
  const module = value.module
41
38
  if (module === null) return { namespaces, module: null, failures }
42
39
  if (
43
- !record(module) ||
40
+ !isRecord(module) ||
44
41
  typeof module.url !== 'string' ||
45
42
  typeof module.hash !== 'string' ||
46
43
  typeof module.compose !== 'boolean'
@@ -1,4 +1,5 @@
1
1
  import { insertBundleGlobalFooter, readBundleGlobalFooter } from './bundleGlobalFooter'
2
+ import { isRecord } from './is-record'
2
3
 
3
4
  export interface ExpoSwiftPreparationOptions {
4
5
  signal?: AbortSignal
@@ -33,9 +34,6 @@ export interface ExpoSwiftCompilationResult {
33
34
  failures: { namespace: string; diagnostic: string }[]
34
35
  }
35
36
 
36
- function record(value: unknown): value is Record<string, unknown> {
37
- return value !== null && typeof value === 'object' && !Array.isArray(value)
38
- }
39
37
  function strings(value: unknown): value is string[] {
40
38
  return Array.isArray(value) && value.every((item) => typeof item === 'string')
41
39
  }
@@ -44,7 +42,7 @@ export function isExpoSwiftCompilationResult(
44
42
  value: unknown,
45
43
  ): value is ExpoSwiftCompilationResult {
46
44
  if (
47
- !record(value) ||
45
+ !isRecord(value) ||
48
46
  !strings(value.namespaces) ||
49
47
  !Array.isArray(value.modules) ||
50
48
  value.modules.length > 1 ||
@@ -62,7 +60,7 @@ export function isExpoSwiftCompilationResult(
62
60
  return false
63
61
  for (const artifact of value.modules) {
64
62
  if (
65
- !record(artifact) ||
63
+ !isRecord(artifact) ||
66
64
  !strings(artifact.namespaces) ||
67
65
  artifact.namespaces.length === 0 ||
68
66
  !artifact.namespaces.every((namespace) => remaining.delete(namespace)) ||
@@ -75,7 +73,7 @@ export function isExpoSwiftCompilationResult(
75
73
  }
76
74
  for (const failure of value.failures) {
77
75
  if (
78
- !record(failure) ||
76
+ !isRecord(failure) ||
79
77
  typeof failure.namespace !== 'string' ||
80
78
  !remaining.delete(failure.namespace) ||
81
79
  typeof failure.diagnostic !== 'string'
@@ -95,6 +95,15 @@ export async function handleStagedHostFileRequest(
95
95
  res.end('staged file not found')
96
96
  return
97
97
  }
98
+ if (req.method === 'OPTIONS') {
99
+ res.writeHead(204, {
100
+ 'Access-Control-Allow-Methods': 'GET, HEAD',
101
+ 'Access-Control-Allow-Headers':
102
+ req.headers['access-control-request-headers'] ?? '*',
103
+ })
104
+ res.end()
105
+ return
106
+ }
98
107
  if (req.method !== 'GET' && req.method !== 'HEAD') {
99
108
  res.writeHead(405, { Allow: 'GET, HEAD' })
100
109
  res.end('method not allowed')
@@ -2,6 +2,7 @@ import fs from 'node:fs'
2
2
  import { createRequire } from 'node:module'
3
3
  import path from 'node:path'
4
4
  import { androidMergedResourceValuesDirectory } from './android-app-identity'
5
+ import { isRecord } from './is-record'
5
6
  import {
6
7
  extractNativeAppIdentityFromFiles,
7
8
  type NativeAppIdentity,
@@ -17,10 +18,6 @@ import {
17
18
  type XcodeAppTarget,
18
19
  } from './xcode-project'
19
20
 
20
- function isRecord(value: unknown): value is Record<string, unknown> {
21
- return typeof value === 'object' && value !== null && !Array.isArray(value)
22
- }
23
-
24
21
  function nonEmptyString(value: unknown): string | undefined {
25
22
  return typeof value === 'string' && value.length > 0 ? value : undefined
26
23
  }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { nativeAppIdentityFromAndroidManifest } from './android-app-identity'
12
12
  import { findBundleFooter } from './bundleGlobalFooter'
13
+ import { isRecord } from './is-record'
13
14
  import {
14
15
  resolveNativeAppIconArtwork,
15
16
  isNativeAppIconArtwork,
@@ -99,10 +100,6 @@ interface NativeAppIdentityGlobals {
99
100
  __sootsim_native_app_identity?: NativeAppIdentity | null
100
101
  }
101
102
 
102
- function isRecord(value: unknown): value is Record<string, unknown> {
103
- return typeof value === 'object' && value !== null && !Array.isArray(value)
104
- }
105
-
106
103
  function nonEmptyString(value: unknown): string | null {
107
104
  return typeof value === 'string' && value.length > 0 ? value : null
108
105
  }
@@ -1,14 +1,11 @@
1
1
  import { isSwiftImageVariants, type SwiftImageVariants } from './assetCatalogContract'
2
+ import { isRecord } from './is-record'
2
3
 
3
4
  export interface NativeAppIconArtwork {
4
5
  primary?: SwiftImageVariants
5
6
  alternates: Record<string, SwiftImageVariants>
6
7
  }
7
8
 
8
- function isRecord(value: unknown): value is Record<string, unknown> {
9
- return typeof value === 'object' && value !== null && !Array.isArray(value)
10
- }
11
-
12
9
  export function isNativeAppIconArtwork(value: unknown): value is NativeAppIconArtwork {
13
10
  return (
14
11
  isRecord(value) &&
@@ -0,0 +1,36 @@
1
+ import { fstatSync } from 'node:fs'
2
+ import type { IOType, StdioOptions } from 'node:child_process'
3
+
4
+ // lock identities name actual inherited descriptors, never paths to reopen.
5
+ export function outputLockStdio(
6
+ standard: [IOType, IOType, IOType],
7
+ env: NodeJS.ProcessEnv = process.env,
8
+ ): StdioOptions {
9
+ const stdio: StdioOptions = [...standard]
10
+ for (const [key, value] of Object.entries(process.env)) {
11
+ if (key.startsWith('REMOTE_OUTPUT_LOCK_') && env[key] !== value)
12
+ throw new Error(`inherited output lock identity changed for ${key}`)
13
+ }
14
+ for (const [key, value] of Object.entries(env)) {
15
+ if (!key.startsWith('REMOTE_OUTPUT_LOCK_')) continue
16
+ const fd = Number(value)
17
+ if (
18
+ !/^REMOTE_OUTPUT_LOCK_[a-f0-9]{64}$/.test(key) ||
19
+ !value ||
20
+ !/^\d+$/.test(value) ||
21
+ !Number.isSafeInteger(fd) ||
22
+ fd < 3
23
+ )
24
+ throw new Error(`invalid inherited output lock descriptor for ${key}`)
25
+ try {
26
+ fstatSync(fd)
27
+ } catch (cause) {
28
+ throw new Error(`inherited output lock descriptor ${fd} unavailable for ${key}`, {
29
+ cause,
30
+ })
31
+ }
32
+ while (stdio.length <= fd) stdio.push('ignore')
33
+ stdio[fd] = fd
34
+ }
35
+ return stdio
36
+ }
@@ -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,
@@ -326,91 +330,115 @@ export function collectBridgeModuleSources(
326
330
  signal?.throwIfAborted()
327
331
  const iosRoot = path.join(appRoot, 'ios')
328
332
  if (!fs.existsSync(iosRoot)) return null
329
- const projects = fs
330
- .readdirSync(iosRoot, { withFileTypes: true })
331
- .filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
332
- .map((entry) => path.join(iosRoot, entry.name))
333
- const targets = projects.flatMap((project) =>
334
- listXcodeAppTargets(project, { requireSources: false }).map((name) => ({
335
- project,
336
- name,
337
- })),
338
- )
333
+ const projectRoot = fs.realpathSync(appRoot)
334
+ const project = findXcodeProject(iosRoot)
335
+ if (!project) return null
336
+ const targets = listXcodeAppTargets(project, { requireSources: false })
339
337
  // RCTRegisterModule runs in the one app binary; several targets are
340
338
  // several apps, and nothing here says which one this bundle runs in
341
339
  if (targets.length !== 1) return null
342
- const app = readXcodeAppTarget(targets[0]!.project, targets[0]!.name, 'Release', {
340
+ const app = readXcodeAppTarget(project, targets[0]!, 'Release', {
343
341
  requireSources: false,
344
342
  })
345
- const own = (file: string) =>
346
- !file.split(path.sep).some((part) => part === 'Pods' || part === 'node_modules')
347
- const objc = app.allSources.filter((file) => /\.mm?$/.test(file) && own(file))
348
- if (objc.length === 0) return null
349
- // headers declare the superclasses; they sit beside the sources that import them
350
- const headers = [...new Set(objc.map((file) => path.dirname(file)))].flatMap((dir) =>
351
- fs
352
- .readdirSync(dir)
353
- .filter((name) => name.endsWith('.h'))
354
- .sort()
355
- .map((name) => path.join(dir, name)),
356
- )
357
- const declarations = parseObjectiveCModules(
358
- [...objc, ...headers].map((file) => ({ file, text: fs.readFileSync(file, 'utf8') })),
359
- )
360
- if (declarations.modules.length === 0) return null
361
- const namespace = app.moduleName.replace(/[^A-Za-z0-9_]/g, '_')
362
- const swiftFiles: Record<string, string> = {}
363
- for (const file of app.sources.filter(own)) {
364
- const relative = path.relative(iosRoot, file)
365
- if (relative.startsWith('..') || path.isAbsolute(relative))
366
- throw new Error(`${file}: app Swift source outside ${iosRoot}`)
367
- swiftFiles[`Sources/${namespace}/${relative.split(path.sep).join('/')}`] =
368
- `#sourceLocation(file: ${JSON.stringify(file)}, line: 1)\n${fs.readFileSync(file, 'utf8')}`
343
+ const own = (file: string) => {
344
+ const resolved = fs.realpathSync(file)
345
+ if (
346
+ [file, resolved].some((name) =>
347
+ name.split(path.sep).some((part) => part === 'Pods' || part === 'node_modules'),
348
+ )
349
+ )
350
+ return false
351
+ const relative = path.relative(projectRoot, resolved)
352
+ if (
353
+ relative === '..' ||
354
+ relative.startsWith(`..${path.sep}`) ||
355
+ path.isAbsolute(relative)
356
+ )
357
+ throw new Error(`${file}: app native source outside ${projectRoot}`)
358
+ return true
369
359
  }
370
360
  const units: ExpoSwiftSourceUnit[] = []
371
361
  const failures: { namespace: string; diagnostic: string }[] = []
372
- const seen = new Set<string>()
373
- 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]) {
374
365
  signal?.throwIfAborted()
375
- if (seen.has(module.className)) continue
376
- seen.add(module.className)
377
- const moduleId = `${namespace}.${module.className}`
378
- const methods = declarations.methods.get(module.className) ?? []
379
- const swiftClass = module.extern ? swiftClassFor(module.className, swiftFiles) : null
380
- const unit: ExpoSwiftSourceUnit = {
381
- namespace,
382
- moduleId,
383
- classes: [],
384
- bridgeModules: [],
385
- files: {},
386
- frameworks: [],
387
- 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')}`
388
396
  }
389
- if (swiftClass) {
390
- const register = `__peachReactModule_${module.className}`
391
- unit.files = swiftSourceClosure(swiftFiles, [swiftClass])
392
- unit.files[
393
- `Sources/${namespace}/${BRIDGE_MODULE_FILE_PREFIX}${module.className}.swift`
394
- ] =
395
- `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`
396
- unit.bridgeModules = [`${namespace}.${register}()`]
397
- } else {
398
- const superclass = compiledSuperclass(module.className, declarations.superclasses)
399
- const selectors = declarations.implementations.get(module.className)
400
- if (!selectors || !superclass) {
401
- failures.push({
402
- namespace: moduleId,
403
- diagnostic: !selectors
404
- ? `${module.file}: no Swift class or Objective-C @implementation provides ${module.className}`
405
- : `${module.file}: ${module.className}'s superclass ${declarations.superclasses.get(module.className) ?? '(none)'} is Objective-C, which Peach does not compile`,
406
- })
407
- 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
+ ]
408
439
  }
409
- unit.bridgeModules = [
410
- `RCTModuleHost.Registration(className: ${swiftString(module.className)}, moduleName: ${swiftString(module.jsName)}, methods: ${methodsLiteral(methods)}, objectiveCSelectors: ${JSON.stringify([...selectors].sort())}, superclass: { ${superclass}() })`,
411
- ]
440
+ units.push(unit)
412
441
  }
413
- units.push(unit)
414
442
  }
415
- return { units, failures }
443
+ return units.length === 0 && failures.length === 0 ? null : { units, failures }
416
444
  }
@@ -43,7 +43,10 @@ export const REACT_NATIVE_PASSTHROUGH_SPECIFIERS: readonly string[] = [
43
43
  ]
44
44
 
45
45
  export function isReactNativePassthroughSpecifier(specifier: string): boolean {
46
+ // checked in place: every module of a bundle asks at boot
46
47
  return REACT_NATIVE_PASSTHROUGH_SPECIFIERS.some(
47
- (passthrough) => specifier === passthrough || specifier.startsWith(`${passthrough}/`),
48
+ (passthrough) =>
49
+ specifier.startsWith(passthrough) &&
50
+ (specifier.length === passthrough.length || specifier[passthrough.length] === '/'),
48
51
  )
49
52
  }
@@ -1,5 +1,6 @@
1
1
  import { readFile, readdir, stat } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
+ import { isRecord } from './is-record'
3
4
  import { parsePlistSource } from './plist.ts'
4
5
  import type { AppPreferenceValue, AppSettingsSpecifier } from '@peach/globals'
5
6
 
@@ -22,10 +23,6 @@ interface CachedSettingsBundle {
22
23
 
23
24
  const settingsBundleCache = new Map<string, CachedSettingsBundle>()
24
25
 
25
- function isRecord(value: unknown): value is PlistRecord {
26
- return typeof value === 'object' && value !== null && !Array.isArray(value)
27
- }
28
-
29
26
  function optionalString(value: unknown): string | undefined {
30
27
  return typeof value === 'string' ? value : undefined
31
28
  }