peachsim 0.1.1625 → 0.1.1640

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 (169) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -2
  3. package/cli/app-config.ts +18 -6
  4. package/cli/app-fonts.ts +37 -17
  5. package/cli/app-stubs.ts +284 -20
  6. package/cli/bridge-flow-runner.ts +66 -45
  7. package/cli/cloud-client.ts +60 -40
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/control.ts +283 -49
  10. package/cli/commands/doctor.ts +30 -189
  11. package/cli/commands/flow.ts +79 -11
  12. package/cli/commands/inspect/core.ts +10 -5
  13. package/cli/commands/inspect/settling.ts +14 -2
  14. package/cli/commands/inspect/shared.ts +9 -4
  15. package/cli/commands/inspect/wait-ready.ts +12 -0
  16. package/cli/commands/inspect.ts +91 -50
  17. package/cli/commands/install-desktop.ts +1 -0
  18. package/cli/commands/maestro-compare.ts +97 -38
  19. package/cli/commands/maestro.ts +9 -1
  20. package/cli/commands/preview.ts +1 -1
  21. package/cli/commands/record.ts +118 -48
  22. package/cli/commands/screenshot.ts +23 -1
  23. package/cli/commands/serve.ts +7 -2
  24. package/cli/commands/setup.ts +19 -42
  25. package/cli/commands/swift.ts +14 -3
  26. package/cli/commands/upload.ts +93 -48
  27. package/cli/desktop-companion.ts +102 -17
  28. package/cli/drivers/desktop.ts +14 -1
  29. package/cli/drivers/playwright-sim-host.ts +103 -45
  30. package/cli/drivers/playwright.ts +9 -8
  31. package/cli/drivers/types.ts +5 -0
  32. package/cli/maestro-accessibility.ts +1 -1
  33. package/cli/maestro-compare.ts +84 -12
  34. package/cli/main.ts +3 -2
  35. package/cli/outbound-endpoints.ts +20 -6
  36. package/cli/pause-aware-timeout.ts +106 -0
  37. package/cli/scene-video.ts +89 -0
  38. package/cli/ws-bridge.ts +37 -19
  39. package/detox/attempt-events.cjs +66 -5
  40. package/detox/attempt-events.d.cts +7 -0
  41. package/detox/expectations.ts +7 -3
  42. package/detox/index.ts +269 -170
  43. package/detox/motion-change.cjs +206 -21
  44. package/detox/motion-change.d.cts +39 -2
  45. package/detox/navigation-lifecycle.ts +35 -3
  46. package/dist-lib/agent-daemon-client.cjs +1 -1
  47. package/dist-lib/agent-events.cjs +1 -1
  48. package/dist-lib/attached-projects.cjs +1 -1
  49. package/dist-lib/auth/shared-session.cjs +1 -1
  50. package/dist-lib/backend-origin.cjs +8 -7
  51. package/dist-lib/beta.cjs +1 -1
  52. package/dist-lib/beta.mjs +1 -1
  53. package/dist-lib/bridge-constants.cjs +13 -1
  54. package/dist-lib/bridge-contract-input.cjs +1 -1
  55. package/dist-lib/bridge-contract-input.mjs +1 -1
  56. package/dist-lib/bridge-contract.cjs +14 -8
  57. package/dist-lib/bridge-contract.mjs +13 -7
  58. package/dist-lib/bundle-channel.cjs +11887 -7900
  59. package/dist-lib/capture-contract.cjs +1 -1
  60. package/dist-lib/capture-contract.mjs +1 -1
  61. package/dist-lib/cloud-contract.cjs +1 -1
  62. package/dist-lib/cloud-contract.mjs +1 -1
  63. package/dist-lib/cloud-test-args.cjs +1 -1
  64. package/dist-lib/cloud-test-args.mjs +1 -1
  65. package/dist-lib/cloud.cjs +1 -1
  66. package/dist-lib/cloud.mjs +1 -1
  67. package/dist-lib/config.cjs +6 -3
  68. package/dist-lib/config.mjs +6 -3
  69. package/dist-lib/detox/index.cjs +1379 -725
  70. package/dist-lib/dev-bundle-resolution.cjs +14 -2
  71. package/dist-lib/expo-swift-bundle.cjs +9341 -5462
  72. package/dist-lib/expo-swift-metadata.cjs +11 -7
  73. package/dist-lib/expo-swift-metadata.mjs +11 -7
  74. package/dist-lib/home-paths.cjs +32 -3
  75. package/dist-lib/host/bridge-host.cjs +50204 -7258
  76. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  77. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  78. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  79. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  80. package/dist-lib/host/websocket-proxy.cjs +1 -1
  81. package/dist-lib/index.cjs +3541 -448
  82. package/dist-lib/jump-to-source-babel.cjs +1 -1
  83. package/dist-lib/jump-to-source-native.cjs +1 -1
  84. package/dist-lib/kotlin-host.cjs +1 -1
  85. package/dist-lib/menu.cjs +1 -1
  86. package/dist-lib/menu.mjs +1 -1
  87. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  88. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  89. package/dist-lib/metro-production-bundle.cjs +1 -1
  90. package/dist-lib/metro-production-bundle.mjs +1 -1
  91. package/dist-lib/metro.cjs +2931 -104
  92. package/dist-lib/native-app-identity-project.cjs +289 -49
  93. package/dist-lib/native-app-identity-project.mjs +289 -49
  94. package/dist-lib/native-app-identity.cjs +77 -26
  95. package/dist-lib/native-app-identity.mjs +76 -26
  96. package/dist-lib/native-firebase-metadata.cjs +1 -1
  97. package/dist-lib/native-firebase-metadata.mjs +1 -1
  98. package/dist-lib/plist.cjs +1 -1
  99. package/dist-lib/plist.mjs +1 -1
  100. package/dist-lib/profiles.cjs +1 -1
  101. package/dist-lib/public-brand.cjs +1 -1
  102. package/dist-lib/react-native-host-modules.cjs +6 -2
  103. package/dist-lib/react-native-host-modules.mjs +6 -2
  104. package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
  105. package/dist-lib/sdk.cjs +27 -11
  106. package/dist-lib/sdk.mjs +27 -10
  107. package/dist-lib/skills.cjs +72201 -26208
  108. package/dist-lib/swift-compile.cjs +1007 -140
  109. package/dist-lib/swift-compile.mjs +1049 -174
  110. package/dist-lib/swift-package.cjs +12 -3
  111. package/dist-lib/swift-package.mjs +12 -3
  112. package/dist-lib/swift-submit.cjs +13 -5
  113. package/dist-lib/swift-submit.mjs +11 -5
  114. package/dist-lib/swift-toolchain.cjs +423 -33
  115. package/dist-lib/swift.cjs +1 -1
  116. package/dist-lib/vite-swift.cjs +8628 -5268
  117. package/dist-lib/vite.cjs +2859 -12
  118. package/dist-lib/worklet-executable.cjs +14 -2
  119. package/dist-lib/worklet-executable.mjs +14 -2
  120. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  121. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  122. package/package.json +1 -1
  123. package/scripts/dev-server-scanner.ts +2 -1
  124. package/skills/peach-perf/SKILL.md +11 -5
  125. package/skills/peach-setup/SKILL.md +16 -19
  126. package/src/backend-origin.ts +13 -6
  127. package/src/bridge-constants.ts +11 -0
  128. package/src/bridge-contract.ts +22 -34
  129. package/src/config.ts +15 -0
  130. package/src/desktop-host-delivery.ts +5 -1
  131. package/src/dev-bundle-resolution.ts +2 -1
  132. package/src/expo-manifest-body.ts +13 -0
  133. package/src/expo-swift-host.ts +196 -49
  134. package/src/expo-swift-ready.ts +78 -0
  135. package/src/expoKotlinContract.ts +4 -7
  136. package/src/expoSwiftContract.ts +17 -6
  137. package/src/home-paths.ts +48 -2
  138. package/src/host/bridge-host.ts +5 -2
  139. package/src/host/staged-host-files.ts +9 -0
  140. package/src/native-app-identity-project.ts +27 -6
  141. package/src/native-app-identity.ts +91 -4
  142. package/src/native-firebase-config.ts +209 -13
  143. package/src/nativeAppIconContract.ts +1 -4
  144. package/src/output-lock-stdio.ts +36 -0
  145. package/src/process-group.ts +25 -2
  146. package/src/react-native-bridge-modules.ts +431 -0
  147. package/src/react-native-host-modules.ts +8 -1
  148. package/src/settings-bundle.ts +1 -4
  149. package/src/swift-binary-modules.ts +2 -2
  150. package/src/swift-compile.ts +918 -109
  151. package/src/swift-compiler-digest.ts +14 -0
  152. package/src/swift-distribution.lock.json +24 -12
  153. package/src/swift-distribution.ts +103 -4
  154. package/src/swift-host.ts +180 -55
  155. package/src/swift-icu.ts +27 -13
  156. package/src/swift-objc-compile.ts +355 -0
  157. package/src/swift-objc-sdk.ts +821 -27
  158. package/src/swift-objc-standin.ts +148 -14
  159. package/src/swift-package-manifest.ts +60 -7
  160. package/src/swift-package.ts +15 -1
  161. package/src/swift-packages.ts +154 -81
  162. package/src/swift-standin-emit.ts +96 -15
  163. package/src/swift-standins.ts +530 -116
  164. package/src/swift-submit.ts +13 -5
  165. package/src/swift-toolchain.ts +500 -42
  166. package/src/vite-plugin.ts +12 -1
  167. package/src/worker-system-chunks.ts +11 -12
  168. package/src/worklet-executable.ts +19 -1
  169. package/src/xcode-project.ts +270 -30
@@ -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
+ }
@@ -13,6 +13,7 @@ import { setTimeout as sleep } from 'node:timers/promises'
13
13
 
14
14
  const GRACEFUL_TIMEOUT_MS = 3_000
15
15
  const FORCED_EXIT_TIMEOUT_MS = 1_000
16
+ const FREEZE_GUARD_NAME = 'process-freeze-guard'
16
17
 
17
18
  // keep this self-contained: the detached browser host embeds its source.
18
19
  export function listProcessChildren(parents: number[], timeoutMs = 3_000): number[] {
@@ -107,7 +108,7 @@ async function protectProcessFreeze(rootPid: number) {
107
108
  printf 'ready\n'
108
109
  if ! IFS= read -r command || [ "$command" != disarm ]; then cleanup; fi
109
110
  `,
110
- 'process-freeze-guard',
111
+ FREEZE_GUARD_NAME,
111
112
  journal,
112
113
  ],
113
114
  {
@@ -151,6 +152,28 @@ async function protectProcessFreeze(rootPid: number) {
151
152
  }
152
153
  }
153
154
 
155
+ // a nested capture's guard is a descendant of this tree but must keep running
156
+ // to resume its own targets, so a freeze never reaches it. ps exits 1 when
157
+ // some pids already vanished, so only a spawn failure or timeout is an error.
158
+ function withoutFreezeGuards(pids: number[]): number[] {
159
+ if (pids.length === 0) return pids
160
+ const result = spawnSync('ps', ['-o', 'pid=,command=', '-p', pids.join(',')], {
161
+ encoding: 'utf8',
162
+ stdio: ['ignore', 'pipe', 'pipe'],
163
+ timeout: FORCED_EXIT_TIMEOUT_MS,
164
+ killSignal: 'SIGKILL',
165
+ })
166
+ if (result.error || (result.status !== 0 && result.status !== 1))
167
+ throw new Error(result.error?.message || `ps exited ${result.status}`)
168
+ const guards = new Set(
169
+ result.stdout
170
+ .split('\n')
171
+ .filter((line) => line.includes(FREEZE_GUARD_NAME))
172
+ .map((line) => Number(line.trim().split(/\s+/)[0])),
173
+ )
174
+ return pids.filter((pid) => !guards.has(pid))
175
+ }
176
+
154
177
  // reparented members remain in the group; detached descendants need their
155
178
  // recorded pids. both can leave zombies, which signal zero cannot distinguish
156
179
  // from running processes. inspect their states together before confirming exit.
@@ -243,7 +266,7 @@ export async function terminateProcessGroup(
243
266
  }
244
267
  let children: number[]
245
268
  try {
246
- children = listProcessChildren(parents, remaining)
269
+ children = withoutFreezeGuards(listProcessChildren(parents, remaining))
247
270
  } catch (error) {
248
271
  inspectionError = new Error(`could not inspect descendants: ${String(error)}`)
249
272
  break
@@ -0,0 +1,431 @@
1
+ // an app's own legacy React Native modules: the classes its Objective-C
2
+ // sources register with RCT_EXPORT_MODULE or RCT_EXTERN_MODULE, read from the
3
+ // app target Xcode builds. the macros' arguments are what clang records for
4
+ // RCTBridge (RCTMethodInfo's js and objc names); the Swift host parses each
5
+ // signature at runtime as RCTModuleMethod does (sootsim-swift React target).
6
+ import fs from 'node:fs'
7
+ import path from 'node:path'
8
+ import { swiftCodeMask, swiftSourceClosure } from './swift-compile'
9
+ import { listXcodeAppTargets, readXcodeAppTarget } from './xcode-project'
10
+ import type { ExpoSwiftSourceUnit } from './expoSwiftContract'
11
+
12
+ interface MethodInfo {
13
+ jsName: string
14
+ objcName: string
15
+ isSync: boolean
16
+ }
17
+
18
+ interface ModuleDeclaration {
19
+ className: string
20
+ jsName: string
21
+ // RCT_EXTERN_MODULE declares a class implemented elsewhere, usually Swift
22
+ extern: boolean
23
+ file: string
24
+ }
25
+
26
+ export interface ObjectiveCDeclarations {
27
+ modules: ModuleDeclaration[]
28
+ methods: Map<string, MethodInfo[]>
29
+ superclasses: Map<string, string>
30
+ // the instance selectors each class's @implementation blocks define
31
+ implementations: Map<string, Set<string>>
32
+ }
33
+
34
+ // Objective-C with its comments as spaces, offsets kept
35
+ function stripComments(source: string): string {
36
+ let out = ''
37
+ let i = 0
38
+ while (i < source.length) {
39
+ const char = source[i]!
40
+ const next = source[i + 1]
41
+ if (char === '/' && next === '/') {
42
+ const end = source.indexOf('\n', i)
43
+ const stop = end === -1 ? source.length : end
44
+ out += ' '.repeat(stop - i)
45
+ i = stop
46
+ } else if (char === '/' && next === '*') {
47
+ const end = source.indexOf('*/', i + 2)
48
+ const stop = end === -1 ? source.length : end + 2
49
+ out += source.slice(i, stop).replace(/[^\n]/g, ' ')
50
+ i = stop
51
+ } else if (char === '"' || char === "'") {
52
+ let j = i + 1
53
+ while (j < source.length && source[j] !== char && source[j] !== '\n')
54
+ j += source[j] === '\\' ? 2 : 1
55
+ out += source.slice(i, j + 1)
56
+ i = j + 1
57
+ } else {
58
+ out += char
59
+ i++
60
+ }
61
+ }
62
+ return out
63
+ }
64
+
65
+ // a macro's arguments split at top-level commas, as the preprocessor does
66
+ function macroArguments(
67
+ source: string,
68
+ open: number,
69
+ ): { args: string[]; end: number } | null {
70
+ const args: string[] = []
71
+ let depth = 0
72
+ let start = open + 1
73
+ for (let i = open; i < source.length; i++) {
74
+ const char = source[i]
75
+ if (char === '"' || char === "'") {
76
+ let j = i + 1
77
+ while (j < source.length && source[j] !== char) j += source[j] === '\\' ? 2 : 1
78
+ i = j
79
+ } else if (char === '(') depth++
80
+ else if (char === ')') {
81
+ depth--
82
+ if (depth === 0) {
83
+ args.push(source.slice(start, i))
84
+ return { args: args.map((arg) => arg.replace(/\s+/g, ' ').trim()), end: i + 1 }
85
+ }
86
+ } else if (char === ',' && depth === 1) {
87
+ args.push(source.slice(start, i))
88
+ start = i + 1
89
+ }
90
+ }
91
+ return null
92
+ }
93
+
94
+ // the selector a method definition's signature names, skipping its types
95
+ function definitionSelector(signature: string): string {
96
+ let selector = ''
97
+ let rest = signature.trim()
98
+ for (;;) {
99
+ const part = /^([A-Za-z_]\w*)?\s*(:)?\s*/.exec(rest)
100
+ if (!part || (!part[1] && !part[2])) break
101
+ selector += (part[1] ?? '') + (part[2] ?? '')
102
+ rest = rest.slice(part[0].length)
103
+ if (!part[2]) break
104
+ if (rest.startsWith('(')) {
105
+ const type = macroArguments(rest, 0)
106
+ if (!type) break
107
+ rest = rest.slice(type.end).trimStart()
108
+ }
109
+ rest = rest.replace(/^[A-Za-z_]\w*\s*/, '')
110
+ }
111
+ return selector
112
+ }
113
+
114
+ const DIRECTIVE =
115
+ /@interface\b|@implementation\b|@end\b|\b(_RCT_EXTERN_REMAP_METHOD|RCT_EXTERN_REMAP_MODULE|RCT_EXTERN_MODULE|RCT_EXPORT_MODULE_NO_LOAD|RCT_EXPORT_MODULE|RCT_EXTERN_METHOD|RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD|RCT_EXPORT_METHOD|RCT_REMAP_METHOD|RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD|RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD|RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD)\s*\(/g
116
+
117
+ export function parseObjectiveCModules(
118
+ sources: { file: string; text: string }[],
119
+ ): ObjectiveCDeclarations {
120
+ const declarations: ObjectiveCDeclarations = {
121
+ modules: [],
122
+ methods: new Map(),
123
+ superclasses: new Map(),
124
+ implementations: new Map(),
125
+ }
126
+ const addMethod = (className: string, method: MethodInfo) => {
127
+ const methods = declarations.methods.get(className) ?? []
128
+ methods.push(method)
129
+ declarations.methods.set(className, methods)
130
+ }
131
+ const addSelector = (className: string, selector: string) => {
132
+ declarations.implementations.get(className)?.add(selector)
133
+ }
134
+ for (const { file, text } of sources) {
135
+ const source = stripComments(text)
136
+ // the class whose @interface or @implementation encloses a macro
137
+ let owner: { className: string; implementation: boolean; start: number } | null = null
138
+ const closeImplementation = (end: number) => {
139
+ if (!owner?.implementation) return
140
+ // the instance methods the implementation defines: `- (type)selector {`
141
+ const body = source.slice(owner.start, end)
142
+ for (const match of body.matchAll(/^[ \t]*-\s*\(/gm)) {
143
+ const open = match.index + match[0].length - 1
144
+ const type = macroArguments(body, open)
145
+ if (!type) continue
146
+ const close = body.slice(type.end).search(/[{;]/)
147
+ if (close === -1) continue
148
+ const selector = definitionSelector(body.slice(type.end, type.end + close))
149
+ if (selector) addSelector(owner.className, selector)
150
+ }
151
+ }
152
+ for (const match of source.matchAll(DIRECTIVE)) {
153
+ const at = match.index
154
+ const directive = match[0]
155
+ if (directive === '@end') {
156
+ closeImplementation(at)
157
+ owner = null
158
+ continue
159
+ }
160
+ if (directive === '@interface' || directive === '@implementation') {
161
+ const rest = source.slice(at + directive.length)
162
+ const extern = /^\s*(RCT_EXTERN_REMAP_MODULE|RCT_EXTERN_MODULE)\s*\(/.exec(rest)
163
+ if (extern) {
164
+ const parsed = macroArguments(rest, extern[0].length - 1)
165
+ if (!parsed) continue
166
+ const [jsName, className, superclass] =
167
+ extern[1] === 'RCT_EXTERN_MODULE' ? ['', ...parsed.args] : parsed.args
168
+ if (!className) continue
169
+ declarations.modules.push({
170
+ className,
171
+ jsName: jsName ?? '',
172
+ extern: true,
173
+ file,
174
+ })
175
+ if (superclass) declarations.superclasses.set(className, superclass)
176
+ owner = { className, implementation: false, start: at }
177
+ continue
178
+ }
179
+ const named = /^\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z_]\w*))?/.exec(rest)
180
+ if (!named) continue
181
+ if (named[2]) declarations.superclasses.set(named[1]!, named[2])
182
+ if (
183
+ directive === '@implementation' &&
184
+ !declarations.implementations.has(named[1]!)
185
+ )
186
+ declarations.implementations.set(named[1]!, new Set())
187
+ owner = {
188
+ className: named[1]!,
189
+ implementation: directive === '@implementation',
190
+ start: at + directive.length + named[0].length,
191
+ }
192
+ continue
193
+ }
194
+ if (!owner) continue
195
+ const parsed = macroArguments(source, at + directive.length - 1)
196
+ if (!parsed) continue
197
+ const args = parsed.args
198
+ switch (match[1]) {
199
+ case 'RCT_EXPORT_MODULE':
200
+ if (owner.implementation)
201
+ declarations.modules.push({
202
+ className: owner.className,
203
+ jsName: args[0] ?? '',
204
+ extern: false,
205
+ file,
206
+ })
207
+ break
208
+ case 'RCT_EXPORT_MODULE_NO_LOAD':
209
+ declarations.modules.push({
210
+ className: args[1] || owner.className,
211
+ jsName: args[0] ?? '',
212
+ extern: false,
213
+ file,
214
+ })
215
+ break
216
+ case 'RCT_EXTERN_METHOD':
217
+ case 'RCT_EXPORT_METHOD':
218
+ addMethod(owner.className, {
219
+ jsName: '',
220
+ objcName: args[0] ?? '',
221
+ isSync: false,
222
+ })
223
+ break
224
+ case 'RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD':
225
+ case 'RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD':
226
+ addMethod(owner.className, {
227
+ jsName: '',
228
+ objcName: args[0] ?? '',
229
+ isSync: true,
230
+ })
231
+ break
232
+ case 'RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD':
233
+ addMethod(owner.className, {
234
+ jsName: '',
235
+ objcName: args[1] ?? '',
236
+ isSync: true,
237
+ })
238
+ break
239
+ case 'RCT_REMAP_METHOD':
240
+ addMethod(owner.className, {
241
+ jsName: args[0] ?? '',
242
+ objcName: args[1] ?? '',
243
+ isSync: false,
244
+ })
245
+ break
246
+ case 'RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD':
247
+ addMethod(owner.className, {
248
+ jsName: args[0] ?? '',
249
+ objcName: args[2] ?? '',
250
+ isSync: true,
251
+ })
252
+ break
253
+ case '_RCT_EXTERN_REMAP_METHOD':
254
+ addMethod(owner.className, {
255
+ jsName: args[0] ?? '',
256
+ objcName: args[1] ?? '',
257
+ isSync: args[2] === 'YES',
258
+ })
259
+ break
260
+ }
261
+ }
262
+ closeImplementation(source.length)
263
+ }
264
+ return declarations
265
+ }
266
+
267
+ // the superclass Peach compiles that a class's chain reaches
268
+ function compiledSuperclass(
269
+ className: string,
270
+ superclasses: Map<string, string>,
271
+ ): 'RCTEventEmitter' | 'NSObject' | null {
272
+ const seen = new Set<string>()
273
+ let current = superclasses.get(className)
274
+ while (current && !seen.has(current)) {
275
+ if (current === 'RCTEventEmitter' || current === 'NSObject') return current
276
+ seen.add(current)
277
+ current = superclasses.get(current)
278
+ }
279
+ return null
280
+ }
281
+
282
+ const swiftString = (value: string) => JSON.stringify(value)
283
+
284
+ function methodsLiteral(methods: MethodInfo[]): string {
285
+ return `[${methods
286
+ .map(
287
+ (method) =>
288
+ `RCTMethodInfo(jsName: ${swiftString(method.jsName)}, objcName: ${swiftString(method.objcName)}, isSync: ${method.isSync})`,
289
+ )
290
+ .join(', ')}]`
291
+ }
292
+
293
+ // a Swift class exposed to Objective-C as `objcName`: `@objc(objcName)` on it,
294
+ // else a class of that name
295
+ function swiftClassFor(objcName: string, files: Record<string, string>): string | null {
296
+ const modifiers =
297
+ '(?:\\s*(?:@\\w+(?:\\([^)]*\\))?|public|open|final|internal|fileprivate|private))*'
298
+ const exposed = new RegExp(
299
+ `@objc\\s*\\(\\s*${objcName}\\s*\\)${modifiers}\\s*class\\s+([A-Za-z_]\\w*)`,
300
+ )
301
+ const named = new RegExp(`\\bclass\\s+${objcName}\\b`)
302
+ let fallback: string | null = null
303
+ for (const source of Object.values(files)) {
304
+ const code = swiftCodeMask(source)
305
+ const match = exposed.exec(code)
306
+ if (match) return match[1]!
307
+ if (named.test(code)) fallback = objcName
308
+ }
309
+ return fallback
310
+ }
311
+
312
+ export const BRIDGE_MODULE_FILE_PREFIX = '__peach_react_module_'
313
+
314
+ /**
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.
318
+ */
319
+ export function collectBridgeModuleSources(
320
+ appRoot: string,
321
+ signal?: AbortSignal,
322
+ ): {
323
+ units: ExpoSwiftSourceUnit[]
324
+ failures: { namespace: string; diagnostic: string }[]
325
+ } | null {
326
+ signal?.throwIfAborted()
327
+ const iosRoot = path.join(appRoot, 'ios')
328
+ if (!fs.existsSync(iosRoot)) return null
329
+ 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
+ )
340
+ // RCTRegisterModule runs in the one app binary; several targets are
341
+ // several apps, and nothing here says which one this bundle runs in
342
+ if (targets.length !== 1) return null
343
+ const app = readXcodeAppTarget(targets[0]!.project, targets[0]!.name, 'Release', {
344
+ requireSources: false,
345
+ })
346
+ const own = (file: string) => {
347
+ const resolved = fs.realpathSync(file)
348
+ if (
349
+ [file, resolved].some((name) =>
350
+ name.split(path.sep).some((part) => part === 'Pods' || part === 'node_modules'),
351
+ )
352
+ )
353
+ return false
354
+ const relative = path.relative(projectRoot, resolved)
355
+ if (
356
+ relative === '..' ||
357
+ relative.startsWith(`..${path.sep}`) ||
358
+ path.isAbsolute(relative)
359
+ )
360
+ throw new Error(`${file}: app native source outside ${projectRoot}`)
361
+ return true
362
+ }
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
+ const units: ExpoSwiftSourceUnit[] = []
386
+ const failures: { namespace: string; diagnostic: string }[] = []
387
+ const seen = new Set<string>()
388
+ for (const module of declarations.modules) {
389
+ 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,
403
+ }
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
423
+ }
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
+ ]
427
+ }
428
+ units.push(unit)
429
+ }
430
+ return { units, failures }
431
+ }
@@ -18,6 +18,10 @@ export const REACT_NATIVE_PASSTHROUGH_SPECIFIERS: readonly string[] = [
18
18
  // expo's metro-runtime dials metro's message socket with this WebSocket,
19
19
  // which runs against the engine's WebSocketModule and BlobModule.
20
20
  'react-native/Libraries/WebSocket',
21
+ // that WebSocket extends upstream's EventTarget and builds its MessageEvent
22
+ // and CloseEvent from the same dom event classes; a stubbed class there is
23
+ // not a constructor to extend.
24
+ 'react-native/src/private/webapis',
21
25
  'react-native/Libraries/Blob/Blob',
22
26
  'react-native/Libraries/Blob/BlobManager',
23
27
  'react-native/Libraries/Blob/BlobRegistry',
@@ -39,7 +43,10 @@ export const REACT_NATIVE_PASSTHROUGH_SPECIFIERS: readonly string[] = [
39
43
  ]
40
44
 
41
45
  export function isReactNativePassthroughSpecifier(specifier: string): boolean {
46
+ // checked in place: every module of a bundle asks at boot
42
47
  return REACT_NATIVE_PASSTHROUGH_SPECIFIERS.some(
43
- (passthrough) => specifier === passthrough || specifier.startsWith(`${passthrough}/`),
48
+ (passthrough) =>
49
+ specifier.startsWith(passthrough) &&
50
+ (specifier.length === passthrough.length || specifier[passthrough.length] === '/'),
44
51
  )
45
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
  }
@@ -6,11 +6,11 @@
6
6
 
7
7
  import { createHash } from 'node:crypto'
8
8
  import fs from 'node:fs'
9
- import os from 'node:os'
10
9
  import path from 'node:path'
10
+ import { cacheDir as peachCacheDir } from './home-paths'
11
11
  import type { SwiftPackageBinary } from './swift-packages'
12
12
 
13
- const cacheDir = path.join(os.tmpdir(), 'peach-swift-binary-modules')
13
+ const cacheDir = path.join(peachCacheDir(), 'swift-binary-modules')
14
14
 
15
15
  // the first `<name>.framework` segment of each path: a framework's own
16
16
  // nested frameworks and macOS Versions/ trees are not modules of the target