peachsim 0.1.1620 → 0.1.1635

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 (185) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +45 -6
  3. package/cli/app-config.ts +4 -1
  4. package/cli/app-fonts.ts +39 -18
  5. package/cli/app-stubs.ts +224 -9
  6. package/cli/bridge-flow-runner.ts +248 -156
  7. package/cli/cloud-client.ts +68 -57
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/box/search-command.ts +3 -2
  10. package/cli/commands/box.ts +10 -29
  11. package/cli/commands/control.ts +391 -92
  12. package/cli/commands/doctor.ts +17 -184
  13. package/cli/commands/flow.ts +79 -11
  14. package/cli/commands/inspect/core.ts +10 -5
  15. package/cli/commands/inspect/settling.ts +71 -22
  16. package/cli/commands/inspect/wait-ready.ts +12 -0
  17. package/cli/commands/inspect.ts +26 -3
  18. package/cli/commands/install-desktop.ts +1 -0
  19. package/cli/commands/maestro-compare.ts +105 -67
  20. package/cli/commands/maestro.ts +9 -1
  21. package/cli/commands/platform.ts +71 -36
  22. package/cli/commands/record.ts +21 -1
  23. package/cli/commands/screenshot.ts +23 -1
  24. package/cli/commands/serve.ts +7 -2
  25. package/cli/commands/setup.ts +19 -42
  26. package/cli/commands/swift.ts +5 -0
  27. package/cli/commands/upload.ts +51 -8
  28. package/cli/desktop-companion.ts +102 -17
  29. package/cli/dev-build-freshness.ts +37 -2
  30. package/cli/drivers/desktop.ts +14 -1
  31. package/cli/drivers/playwright-sim-host.ts +44 -8
  32. package/cli/drivers/playwright.ts +9 -8
  33. package/cli/drivers/types.ts +5 -0
  34. package/cli/maestro-accessibility.ts +53 -0
  35. package/cli/maestro-compare.ts +108 -12
  36. package/cli/main.ts +2 -1
  37. package/cli/outbound-endpoints.ts +35 -6
  38. package/cli/parent-pid.ts +0 -9
  39. package/cli/pause-aware-timeout.ts +102 -0
  40. package/cli/remote-simulators.ts +112 -0
  41. package/cli/remoteSimulatorContract.ts +21 -0
  42. package/cli/scene-video.ts +89 -0
  43. package/cli/ws-bridge.ts +23 -14
  44. package/detox/attempt-events.cjs +4 -0
  45. package/detox/expectations.ts +5 -1
  46. package/detox/index.ts +241 -181
  47. package/detox/motion-change.cjs +73 -0
  48. package/detox/motion-change.d.cts +21 -0
  49. package/detox/navigation-lifecycle.ts +22 -4
  50. package/dist-lib/agent-daemon-client.cjs +3 -2
  51. package/dist-lib/agent-events.cjs +1 -1
  52. package/dist-lib/attached-projects.cjs +1 -1
  53. package/dist-lib/auth/shared-session.cjs +3 -2
  54. package/dist-lib/backend-origin.cjs +1 -1
  55. package/dist-lib/beta.cjs +1 -1
  56. package/dist-lib/beta.mjs +1 -1
  57. package/dist-lib/bridge-constants.cjs +13 -1
  58. package/dist-lib/bridge-contract-input.cjs +1 -1
  59. package/dist-lib/bridge-contract-input.mjs +1 -1
  60. package/dist-lib/bridge-contract.cjs +14 -8
  61. package/dist-lib/bridge-contract.mjs +13 -7
  62. package/dist-lib/bundle-channel.cjs +10480 -7962
  63. package/dist-lib/capture-contract.cjs +1 -1
  64. package/dist-lib/capture-contract.mjs +1 -1
  65. package/dist-lib/cloud-contract.cjs +1 -1
  66. package/dist-lib/cloud-contract.mjs +1 -1
  67. package/dist-lib/cloud-test-args.cjs +1 -1
  68. package/dist-lib/cloud-test-args.mjs +1 -1
  69. package/dist-lib/cloud.cjs +3 -2
  70. package/dist-lib/cloud.mjs +3 -2
  71. package/dist-lib/config.cjs +10 -6
  72. package/dist-lib/config.mjs +10 -6
  73. package/dist-lib/detox/index.cjs +1854 -966
  74. package/dist-lib/dev-bundle-resolution.cjs +64 -24
  75. package/dist-lib/expo-swift-bundle.cjs +8715 -6271
  76. package/dist-lib/expo-swift-metadata.cjs +7 -4
  77. package/dist-lib/expo-swift-metadata.mjs +7 -4
  78. package/dist-lib/home-paths.cjs +32 -3
  79. package/dist-lib/host/bridge-host.cjs +10135 -7060
  80. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  81. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  82. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  83. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  84. package/dist-lib/host/websocket-proxy.cjs +1 -1
  85. package/dist-lib/index.cjs +1843 -408
  86. package/dist-lib/jump-to-source-babel.cjs +1 -1
  87. package/dist-lib/jump-to-source-native.cjs +1 -1
  88. package/dist-lib/kotlin-host.cjs +1 -1
  89. package/dist-lib/menu.cjs +3 -2
  90. package/dist-lib/menu.mjs +3 -2
  91. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  92. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  93. package/dist-lib/metro-production-bundle.cjs +5 -4
  94. package/dist-lib/metro-production-bundle.mjs +5 -4
  95. package/dist-lib/metro.cjs +1182 -418
  96. package/dist-lib/native-app-identity-project.cjs +782 -23
  97. package/dist-lib/native-app-identity-project.mjs +781 -23
  98. package/dist-lib/native-app-identity.cjs +276 -9
  99. package/dist-lib/native-app-identity.mjs +273 -8
  100. package/dist-lib/native-firebase-metadata.cjs +5 -4
  101. package/dist-lib/native-firebase-metadata.mjs +5 -4
  102. package/dist-lib/plist.cjs +1 -1
  103. package/dist-lib/plist.mjs +1 -1
  104. package/dist-lib/profiles.cjs +1 -1
  105. package/dist-lib/public-brand.cjs +3 -2
  106. package/dist-lib/react-native-host-modules.cjs +8 -1
  107. package/dist-lib/react-native-host-modules.mjs +8 -1
  108. package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
  109. package/dist-lib/sdk.cjs +115 -34
  110. package/dist-lib/sdk.mjs +115 -33
  111. package/dist-lib/skills.cjs +15274 -9904
  112. package/dist-lib/swift-compile.cjs +1165 -173
  113. package/dist-lib/swift-compile.mjs +1212 -203
  114. package/dist-lib/swift-package.cjs +12 -3
  115. package/dist-lib/swift-package.mjs +12 -3
  116. package/dist-lib/swift-submit.cjs +7 -1
  117. package/dist-lib/swift-submit.mjs +5 -1
  118. package/dist-lib/swift-toolchain.cjs +133 -18
  119. package/dist-lib/swift.cjs +1 -1
  120. package/dist-lib/vite-swift.cjs +2648 -862
  121. package/dist-lib/vite.cjs +70 -14
  122. package/dist-lib/worklet-executable.cjs +25 -2
  123. package/dist-lib/worklet-executable.mjs +25 -2
  124. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  125. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  126. package/package.json +2 -2
  127. package/scripts/dev-server-scanner.ts +35 -14
  128. package/skills/peach-perf/SKILL.md +11 -5
  129. package/skills/peach-setup/SKILL.md +16 -19
  130. package/src/admitted-mac-build.ts +34 -8
  131. package/src/android-app-identity.ts +146 -0
  132. package/src/android-autolinking.ts +75 -0
  133. package/src/android-string-value.ts +36 -0
  134. package/src/android-strings.ts +5 -36
  135. package/src/androidAutolinkingContract.ts +40 -0
  136. package/src/bridge-constants.ts +11 -0
  137. package/src/bridge-contract.ts +22 -34
  138. package/src/bundleGlobalFooter.ts +26 -7
  139. package/src/codex-client.ts +3 -2
  140. package/src/config.ts +15 -0
  141. package/src/desktop-host-delivery.ts +5 -1
  142. package/src/dev-bundle-resolution.ts +60 -26
  143. package/src/expo-kotlin-host.ts +6 -36
  144. package/src/expo-manifest-body.ts +13 -0
  145. package/src/expo-swift-bundle.ts +17 -12
  146. package/src/expo-swift-host.ts +640 -192
  147. package/src/expo-swift-ready.ts +78 -0
  148. package/src/expoSwiftContract.ts +19 -0
  149. package/src/home-paths.ts +48 -2
  150. package/src/host/bridge-host.ts +12 -57
  151. package/src/host/staged-host-files.ts +29 -2
  152. package/src/installed-package-versions.ts +54 -14
  153. package/src/launchMetadataContract.ts +24 -0
  154. package/src/metro-plugin.ts +37 -42
  155. package/src/native-app-identity-project.ts +142 -5
  156. package/src/native-app-identity.ts +176 -11
  157. package/src/native-firebase-config.ts +209 -13
  158. package/src/nitro-compile.ts +2 -5
  159. package/src/nitro-source.ts +147 -63
  160. package/src/process-group.ts +114 -61
  161. package/src/process-output.ts +0 -9
  162. package/src/public-brand.ts +1 -0
  163. package/src/react-native-bridge-modules.ts +416 -0
  164. package/src/react-native-host-modules.ts +7 -0
  165. package/src/sim-client.ts +6 -4
  166. package/src/swift-binary-modules.ts +2 -2
  167. package/src/swift-compile.ts +1007 -108
  168. package/src/swift-compiler-digest.ts +14 -0
  169. package/src/swift-distribution.lock.json +40 -12
  170. package/src/swift-distribution.ts +61 -9
  171. package/src/swift-host.ts +149 -56
  172. package/src/swift-icu.ts +14 -6
  173. package/src/swift-objc-sdk.ts +133 -10
  174. package/src/swift-objc-standin.ts +38 -3
  175. package/src/swift-package-manifest.ts +21 -6
  176. package/src/swift-package.ts +15 -1
  177. package/src/swift-packages.ts +119 -60
  178. package/src/swift-standin-emit.ts +57 -9
  179. package/src/swift-standins.ts +185 -25
  180. package/src/swift-submit.ts +10 -0
  181. package/src/swift-toolchain.ts +149 -23
  182. package/src/vite-plugin.ts +20 -20
  183. package/src/worklet-executable.ts +30 -3
  184. package/src/worklets-babel.ts +35 -14
  185. package/src/xcode-project.ts +10 -0
@@ -106,6 +106,172 @@ export async function readExpoConfigAsync(
106
106
  }
107
107
  }
108
108
 
109
+ // a launch method may guard configure() with the safe-configure idiom: an
110
+ // `if` whose comma-separated clauses read the bundled GoogleService-Info.plist
111
+ // and test its API_KEY prefix. every clause is evaluated against the plist the
112
+ // target ships, so the answer is the one native launch reaches. any other
113
+ // condition cannot be proven and stays refused.
114
+ function evaluateFirebaseGuard(header: string, resources: string[]): boolean | null {
115
+ const condition = header.replace(/^\s*if\b/, '').replace(/\{\s*$/, '')
116
+ const clauses: string[] = []
117
+ let depth = 0
118
+ let start = 0
119
+ let quoted = false
120
+ for (let index = 0; index < condition.length; index++) {
121
+ const char = condition[index]
122
+ if (char === '"') quoted = !quoted
123
+ if (quoted) continue
124
+ if (char === '(' || char === '[') depth += 1
125
+ if (char === ')' || char === ']') depth -= 1
126
+ if (char === ',' && depth === 0) {
127
+ clauses.push(condition.slice(start, index).trim())
128
+ start = index + 1
129
+ }
130
+ }
131
+ clauses.push(condition.slice(start).trim())
132
+ const sources = resources.filter(
133
+ (file) => path.basename(file) === 'GoogleService-Info.plist',
134
+ )
135
+ const bound = new Map<string, unknown>()
136
+ for (const clause of clauses) {
137
+ if (/^FirebaseApp\s*\.\s*app\s*\(\s*\)\s*==\s*nil$/.test(clause)) continue
138
+ const resource =
139
+ /^let\s+(\w+)\s*=\s*Bundle\.main\.path\s*\(\s*forResource:\s*"GoogleService-Info"\s*,\s*ofType:\s*"plist"\s*\)$/.exec(
140
+ clause,
141
+ )
142
+ if (resource) {
143
+ if (sources.length !== 1) return false
144
+ bound.set(resource[1], sources[0])
145
+ continue
146
+ }
147
+ const dictionary =
148
+ /^let\s+(\w+)\s*=\s*NSDictionary\s*\(\s*contentsOfFile:\s*(\w+)\s*\)$/.exec(clause)
149
+ if (dictionary && typeof bound.get(dictionary[2]) === 'string') {
150
+ const parsed = parsePlistSource(
151
+ fs.readFileSync(String(bound.get(dictionary[2])), 'utf8'),
152
+ )
153
+ if (!isRecord(parsed)) return false
154
+ bound.set(dictionary[1], parsed)
155
+ continue
156
+ }
157
+ const entry = /^let\s+(\w+)\s*=\s*(\w+)\s*\[\s*"(\w+)"\s*\]\s+as\?\s+String$/.exec(
158
+ clause,
159
+ )
160
+ const dictionaryValue = entry ? bound.get(entry[2]) : undefined
161
+ if (entry && isRecord(dictionaryValue)) {
162
+ const value = dictionaryValue[entry[3]]
163
+ if (typeof value !== 'string') return false
164
+ bound.set(entry[1], value)
165
+ continue
166
+ }
167
+ const prefix = /^(\w+)\.hasPrefix\s*\(\s*"([^"]*)"\s*\)$/.exec(clause)
168
+ const subject = prefix ? bound.get(prefix[1]) : undefined
169
+ if (prefix && typeof subject === 'string') {
170
+ if (!subject.startsWith(prefix[2])) return false
171
+ continue
172
+ }
173
+ return null
174
+ }
175
+ return true
176
+ }
177
+
178
+ // native source with comments and string contents blanked in place, so brace
179
+ // and keyword scans see only code and offsets still index the raw text.
180
+ function blankSwiftObjcLiterals(raw: string) {
181
+ return raw.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"/g, (token) =>
182
+ token.startsWith('"')
183
+ ? `"${' '.repeat(token.length - 2)}"`
184
+ : token.replace(/[^\n]/g, ' '),
185
+ )
186
+ }
187
+
188
+ // the text of the brace block opening at `open`, and the index after its end.
189
+ function braceBlock(source: string, open: number) {
190
+ let depth = 0
191
+ for (let index = open; index < source.length; index += 1) {
192
+ if (source[index] === '{') depth += 1
193
+ if (source[index] === '}') depth -= 1
194
+ if (depth === 0) return { body: source.slice(open + 1, index), end: index + 1 }
195
+ }
196
+ return null
197
+ }
198
+
199
+ // whether every path through the code configures the default Firebase app: a
200
+ // top-level configure call, or an if/else chain ending in else whose blocks each
201
+ // do.
202
+ function configuresDefaultApp(code: string): boolean {
203
+ const configureCall = /\bFirebaseApp\s*\.\s*configure\s*\(/
204
+ let topLevel = ''
205
+ let depth = 0
206
+ for (const char of code) {
207
+ if (char === '{') depth += 1
208
+ if (depth === 0) topLevel += char
209
+ if (char === '}') depth -= 1
210
+ }
211
+ if (configureCall.test(topLevel)) return true
212
+ for (const chain of code.matchAll(/\bif\b[^{]*\{/g)) {
213
+ let open = chain.index + chain[0].length - 1
214
+ const blocks: string[] = []
215
+ let complete = false
216
+ for (;;) {
217
+ const block = braceBlock(code, open)
218
+ if (!block) break
219
+ blocks.push(block.body)
220
+ const next = /^\s*else\s*(if\b[^{]*)?\{/.exec(code.slice(block.end))
221
+ if (!next) break
222
+ open = block.end + next[0].length - 1
223
+ if (!next[1]) {
224
+ const last = braceBlock(code, open)
225
+ if (last) {
226
+ blocks.push(last.body)
227
+ complete = true
228
+ }
229
+ break
230
+ }
231
+ }
232
+ if (complete && blocks.every(configuresDefaultApp)) return true
233
+ }
234
+ return false
235
+ }
236
+
237
+ // where a launch method configures the default Firebase app: directly, or by
238
+ // calling a static no-argument function of an app target type whose body
239
+ // configures it on every path. returns a match-like result carrying the call's
240
+ // index, or null.
241
+ function findDefaultConfigureCall(body: string, sources: readonly string[]) {
242
+ const direct =
243
+ /^\s*(?:FirebaseApp\s*\.\s*configure\s*\(\s*\)|\[FIRApp\s+configure\])/m.exec(body)
244
+ if (direct) return direct
245
+ for (const call of body.matchAll(/^\s*([A-Z]\w*)\s*\.\s*(\w+)\s*\(\s*\)/gm)) {
246
+ const [, type, name] = call
247
+ for (const file of sources) {
248
+ if (!file.endsWith('.swift')) continue
249
+ const source = blankSwiftObjcLiterals(fs.readFileSync(file, 'utf8'))
250
+ const declaration = new RegExp(
251
+ `\\b(?:enum|struct|class)\\s+${type}\\b[^{]*\\{`,
252
+ ).exec(source)
253
+ if (!declaration) continue
254
+ const owner = braceBlock(source, declaration.index + declaration[0].length - 1)
255
+ const method = owner
256
+ ? new RegExp(`\\bstatic\\s+func\\s+${name}\\s*\\(\\s*\\)[^{]*\\{`).exec(
257
+ owner.body,
258
+ )
259
+ : null
260
+ const helper =
261
+ owner && method
262
+ ? braceBlock(owner.body, method.index + method[0].length - 1)
263
+ : null
264
+ if (
265
+ helper &&
266
+ !/\b(?:return|throw)\b/.test(helper.body) &&
267
+ configuresDefaultApp(helper.body)
268
+ )
269
+ return call
270
+ }
271
+ }
272
+ return null
273
+ }
274
+
109
275
  // the GoogleService-Info.plist an app target's launch method configures as the
110
276
  // default app, relative to the project root, or null when it configures none.
111
277
  function readTargetGoogleServicesFile(
@@ -120,27 +286,43 @@ function readTargetGoogleServicesFile(
120
286
  /^AppDelegate\.(swift|mm|m)$/.test(path.basename(file)),
121
287
  )
122
288
  const defaultConfigured = delegates.some((file) => {
123
- const source = fs
124
- .readFileSync(file, 'utf8')
125
- .replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:\\.|[^"\\])*"/g, (token) =>
126
- token.startsWith('"') ? '""' : token.replace(/[^\n]/g, ' '),
127
- )
289
+ const raw = fs.readFileSync(file, 'utf8')
290
+ const source = blankSwiftObjcLiterals(raw)
128
291
  const launch =
129
292
  /(?:\bfunc\s+application\s*\([\s\S]*?didFinishLaunchingWithOptions[\s\S]*?\)\s*->\s*Bool|^-\s*\(BOOL\)\s*application\s*:[\s\S]*?didFinishLaunchingWithOptions[^{]*)\s*\{/m.exec(
130
293
  source,
131
294
  )
132
295
  if (!launch) return false
133
- const body = source.slice(launch.index + launch[0].length)
134
- const configure =
135
- /^\s*(?:FirebaseApp\s*\.\s*configure\s*\(\s*\)|\[FIRApp\s+configure\])/m.exec(body)
296
+ const bodyStart = launch.index + launch[0].length
297
+ const body = source.slice(bodyStart)
298
+ const configure = findDefaultConfigureCall(body, app.allSources)
136
299
  if (!configure) return false
137
- const before = body.slice(0, configure.index)
300
+ let before = body.slice(0, configure.index)
138
301
  let depth = 1
139
- for (const token of before) {
140
- if (token === '{') depth += 1
302
+ let guardOpen = -1
303
+ for (let index = 0; index < before.length; index++) {
304
+ const token = before[index]
305
+ if (token === '{') {
306
+ depth += 1
307
+ if (depth === 2) guardOpen = index
308
+ }
141
309
  if (token === '}') depth -= 1
142
310
  if (depth === 0) return false
143
311
  }
312
+ let guard: string | null = null
313
+ if (depth === 2) {
314
+ const headers = [...before.slice(0, guardOpen).matchAll(/^[ \t]*if\b/gm)]
315
+ const header = headers.at(-1)
316
+ if (header) {
317
+ guard = raw.slice(bodyStart + header.index, bodyStart + guardOpen + 1)
318
+ before = before.slice(0, header.index)
319
+ depth = 1
320
+ for (const token of before) {
321
+ if (token === '{') depth += 1
322
+ if (token === '}') depth -= 1
323
+ }
324
+ }
325
+ }
144
326
  let conditionalDepth = 0
145
327
  for (const directive of before.matchAll(
146
328
  /^\s*#\s*(if|ifdef|ifndef|elseif|elif|else|endif)\b/gm,
@@ -155,11 +337,25 @@ function readTargetGoogleServicesFile(
155
337
  conditionalDepth -= 1
156
338
  }
157
339
  }
158
- if (depth !== 1 || conditionalDepth !== 0 || /\b(?:return|throw)\b/.test(before))
340
+ // a return inside a nested closure leaves the launch method running
341
+ let topLevelBefore = ''
342
+ let nesting = 0
343
+ for (const token of before) {
344
+ if (token === '{') nesting += 1
345
+ if (nesting === 0) topLevelBefore += token
346
+ if (token === '}') nesting -= 1
347
+ }
348
+ const guarded = guard === null ? true : evaluateFirebaseGuard(guard, app.resources)
349
+ if (
350
+ guarded === null ||
351
+ depth !== 1 ||
352
+ conditionalDepth !== 0 ||
353
+ /\b(?:return|throw)\b/.test(topLevelBefore)
354
+ )
159
355
  throw new Error(
160
356
  'native Firebase initialization must be unconditional in the app launch method',
161
357
  )
162
- return true
358
+ return guarded
163
359
  })
164
360
  if (!defaultConfigured) return null
165
361
  const resources = app.resources.filter((file) =>
@@ -20,10 +20,7 @@ export function collectNitroCompileInput(
20
20
  ): { input: SwiftCompileInput; collection: NitroSourceCollection } {
21
21
  const collection = collectNitroSources(options)
22
22
  const files = { ...collection.files }
23
- const platform = path.join(
24
- options.moduleRoot,
25
- 'platform/react-native-nitro-modules',
26
- )
23
+ const platform = path.join(options.moduleRoot, 'platform/react-native-nitro-modules')
27
24
  const read = (file: string) =>
28
25
  readPackageFile(
29
26
  platform,
@@ -71,7 +68,7 @@ export function collectNitroCompileInput(
71
68
  'let package = Package(name: "PeachNativeModule", targets: [',
72
69
  ...collection.targets.map(
73
70
  (target) =>
74
- `.target(name: ${JSON.stringify(target.name)}, dependencies: ${JSON.stringify(target.dependencies?.map((dependency) => dependency.name) ?? [])}, path: ${JSON.stringify(target.path)}, cxxSettings: [.define("DEBUG")]),`,
71
+ `.target(name: ${JSON.stringify(target.name)}, dependencies: ${JSON.stringify(target.dependencies?.map((dependency) => dependency.name) ?? [])}, path: ${JSON.stringify(target.path)}, ${target.cxxInterop ? 'swiftSettings: [.interoperabilityMode(.Cxx)]' : 'cxxSettings: [.define("DEBUG")]'}),`,
75
72
  ),
76
73
  `.target(name: "${runtime}", dependencies: ["JsiBridge", "NitroCore"], cxxSettings: [.define("DEBUG")]),`,
77
74
  `.executableTarget(name: "PeachNitroApp", dependencies: ["${runtime}", "${implementation.name}"], swiftSettings: [.interoperabilityMode(.Cxx)]),`,
@@ -55,61 +55,72 @@ function packageInfo(root: string): NitroSourceCollection['packages'][number] {
55
55
  return { name: value.name, version: value.version, root }
56
56
  }
57
57
 
58
- function registrations(root: string): {
59
- namespace: string
60
- classes: { name: string; implementation: string }[]
61
- } {
62
- const config = readJson(root, 'nitro.json')
63
- if (
64
- !object(config) ||
65
- !Array.isArray(config.cxxNamespace) ||
66
- !config.cxxNamespace.every(
67
- (part): part is string =>
68
- typeof part === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(part),
69
- ) ||
70
- !object(config.autolinking)
71
- )
72
- throw new Error(`${root}: invalid Nitro C++ namespace or autolinking config`)
73
- const classes: { name: string; implementation: string }[] = []
74
- for (const [name, entry] of Object.entries(config.autolinking).sort(([a], [b]) =>
75
- a.localeCompare(b),
58
+ function registrations(root: string) {
59
+ // cocoapods compiles this generated initializer through +autolinking.rb.
60
+ // nitro.json is a generator input and need not ship in the package.
61
+ const directory = path.join(root, 'nitrogen/generated/ios')
62
+ const initializers = fs
63
+ .readdirSync(directory)
64
+ .filter((file) => file.endsWith('Autolinking.mm'))
65
+ if (initializers.length !== 1)
66
+ throw new Error(`${root}: expected one generated Nitro iOS initializer`)
67
+ const source = readPackageFile(
68
+ root,
69
+ path.join(directory, initializers[0]!),
70
+ MAX_SWIFT_COMPILE_SOURCE_BYTES,
71
+ ).toString('utf8')
72
+ const namespaces = [
73
+ ...source.matchAll(/\busing namespace (margelo::nitro(?:::[A-Za-z_]\w*)*)\s*;/g),
74
+ ]
75
+ const namespace = namespaces[1]?.[1]
76
+ if (namespaces.length !== 2 || namespaces[0]?.[1] !== 'margelo::nitro' || !namespace)
77
+ throw new Error(`${root}: invalid generated Nitro C++ namespace`)
78
+
79
+ const classes: (
80
+ | { name: string; implementation: string; language: 'c++' }
81
+ | { name: string; language: 'swift' }
82
+ )[] = []
83
+ let swiftModule: string | null = null
84
+ for (const registration of source.matchAll(
85
+ /\bHybridObjectRegistry::registerHybridObjectConstructor\(\s*("(?:[^"\\]|\\.)*")\s*,\s*\[\]\(\)\s*->\s*std::shared_ptr<HybridObject>\s*\{([\s\S]*?)\}\s*\);/g,
76
86
  )) {
77
- if (!object(entry)) throw new Error(`${root}: invalid Nitro autolinking ${name}`)
78
- // platform-specific registrations cannot be substituted for shared C++.
79
- // report their actual language instead of silently dropping Swift or JNI.
80
- if (
81
- !object(entry.all) ||
82
- entry.all.language !== 'c++' ||
83
- Object.keys(entry).some((platform) => platform !== 'all')
84
- ) {
85
- const languages = Object.entries(entry).map(
86
- ([platform, value]) =>
87
- `${platform}:${object(value) ? String(value.language) : 'invalid'}`,
88
- )
89
- throw new Error(
90
- `${root}: unsupported Nitro module ${name} (${languages.join(', ')}); ` +
91
- 'this collector supports shared all.language=c++ only, not Swift, Kotlin, Java or JNI',
87
+ const name: unknown = JSON.parse(registration[1]!)
88
+ const body = registration[2]!
89
+ if (typeof name !== 'string' || name.length === 0)
90
+ throw new Error(`${root}: invalid generated Nitro registration name`)
91
+ const cpp = /return\s+std::make_shared<([A-Za-z_]\w*)>\(\);/.exec(body)
92
+ const swift =
93
+ /=\s*([A-Za-z_]\w*)::([A-Za-z_]\w*)::create([A-Za-z_]\w*)\(\);\s*return hybridObject;/.exec(
94
+ body,
92
95
  )
96
+ if (cpp && !swift) {
97
+ classes.push({ name, implementation: cpp[1]!, language: 'c++' })
98
+ } else if (
99
+ swift &&
100
+ !cpp &&
101
+ swift[2] === `${swift[1]}Autolinking` &&
102
+ swift[3] === name &&
103
+ (swiftModule === null || swiftModule === swift[1])
104
+ ) {
105
+ swiftModule = swift[1]!
106
+ classes.push({ name, language: 'swift' })
107
+ } else {
108
+ throw new Error(`${root}: unsupported generated Nitro iOS registration ${name}`)
93
109
  }
94
- const implementation = entry.all.implementationClassName
95
- if (
96
- name.length === 0 ||
97
- typeof implementation !== 'string' ||
98
- !/^[A-Za-z_][A-Za-z0-9_]*$/.test(implementation)
99
- )
100
- throw new Error(`${root}: invalid Nitro C++ implementation for ${name}`)
101
- classes.push({ name, implementation })
102
- }
103
- if (classes.length === 0) throw new Error(`${root}: no Nitro C++ registrations`)
104
- return {
105
- namespace: ['margelo', 'nitro', ...config.cxxNamespace].join('::'),
106
- classes,
107
110
  }
111
+ const count = [
112
+ ...source.matchAll(/\bHybridObjectRegistry::registerHybridObjectConstructor\s*\(/g),
113
+ ].length
114
+ if (classes.length === 0 || classes.length !== count)
115
+ throw new Error(`${root}: incomplete generated Nitro iOS registrations`)
116
+ if (new Set(classes.map(({ name }) => name)).size !== classes.length)
117
+ throw new Error(`${root}: duplicate generated Nitro iOS registration`)
118
+ classes.sort((a, b) => a.name.localeCompare(b.name))
119
+ return { namespace, swiftModule, classes }
108
120
  }
109
121
 
110
- /** collect installed C++ bodies, Nitro core and RN's JSI, without reading or
111
- * rewriting package JavaScript. views and platform implementations are outside
112
- * this C++ module collector; the caller must supply real platform bindings. */
122
+ /** collect installed native bodies, generated bridges, Nitro core and RN's
123
+ * JSI without reading or rewriting package JavaScript. */
113
124
  export function collectNitroSources(options: NitroSourceOptions): NitroSourceCollection {
114
125
  const module = packageInfo(options.packageRoot)
115
126
  const config = registrations(module.root)
@@ -189,6 +200,7 @@ export function collectNitroSources(options: NitroSourceOptions): NitroSourceCol
189
200
  includeNamespace: string | null,
190
201
  skip: ReadonlySet<string> = new Set(),
191
202
  select: (file: string) => boolean = () => true,
203
+ extensions: RegExp = /\.(?:c|cc|cpp|cxx|h|hpp|hxx|inl)$/,
192
204
  ) => {
193
205
  const headers = new Map<string, string>()
194
206
  const walk = (relative: string) => {
@@ -204,7 +216,7 @@ export function collectNitroSources(options: NitroSourceOptions): NitroSourceCol
204
216
  walk(sourceRelative)
205
217
  continue
206
218
  }
207
- if (!/\.(?:c|cc|cpp|cxx|h|hpp|hxx|inl)$/.test(entry.name)) continue
219
+ if (!extensions.test(entry.name)) continue
208
220
  if (!select(entry.name)) continue
209
221
  if (/[\p{Cc}"\\]/u.test(sourceRelative))
210
222
  throw new Error(`invalid Nitro source path: ${sourceRelative}`)
@@ -259,13 +271,72 @@ export function collectNitroSources(options: NitroSourceOptions): NitroSourceCol
259
271
  (file) => (file.endsWith('.h') && file !== 'JSIDynamic.h') || file === 'jsi.cpp',
260
272
  )
261
273
  collect(nitro.root, 'cpp', coreTarget, 'NitroModules', new Set(['views']))
262
- const implementationHeaders = collect(module.root, 'cpp', targetName, null)
274
+ const implementationHeaders = fs.existsSync(path.join(module.root, 'cpp'))
275
+ ? collect(module.root, 'cpp', targetName, null)
276
+ : new Map<string, string>()
263
277
  collect(module.root, 'nitrogen/generated/shared/c++', targetName, null)
264
- for (const { implementation } of config.classes)
265
- if (!implementationHeaders.has(`${implementation}.hpp`))
278
+ for (const entry of config.classes)
279
+ if (
280
+ entry.language === 'c++' &&
281
+ !implementationHeaders.has(`${entry.implementation}.hpp`)
282
+ )
266
283
  throw new Error(
267
- `${module.root}: missing C++ implementation header ${implementation}.hpp`,
284
+ `${module.root}: missing C++ implementation header ${entry.implementation}.hpp`,
285
+ )
286
+
287
+ if (config.swiftModule) {
288
+ for (const directory of ['ios/core', 'ios/utils'])
289
+ collect(
290
+ nitro.root,
291
+ directory,
292
+ coreTarget,
293
+ 'NitroModules',
294
+ new Set(),
295
+ (file) => !file.endsWith('.swift'),
268
296
  )
297
+ const nitroSwift = target('NitroModules', [coreTarget, jsiTarget])
298
+ nitroSwift.cxxInterop = true
299
+ const packageSwift = target(config.swiftModule, [targetName, 'NitroModules'])
300
+ packageSwift.cxxInterop = true
301
+ result.targets.push(nitroSwift, packageSwift)
302
+ for (const directory of ['ios/core', 'ios/utils', 'ios/views'])
303
+ collect(
304
+ nitro.root,
305
+ directory,
306
+ 'NitroModules',
307
+ null,
308
+ new Set(['HybridView.swift']),
309
+ () => true,
310
+ /\.swift$/,
311
+ )
312
+ add(
313
+ `${targetPath('NitroModules')}/PeachNitroExports.swift`,
314
+ `@_exported import ${coreTarget}\n`,
315
+ )
316
+ collect(
317
+ module.root,
318
+ 'ios',
319
+ config.swiftModule,
320
+ null,
321
+ new Set(['Tests']),
322
+ () => true,
323
+ /\.swift$/,
324
+ )
325
+ collect(
326
+ module.root,
327
+ 'nitrogen/generated/ios',
328
+ config.swiftModule,
329
+ null,
330
+ new Set(),
331
+ () => true,
332
+ /\.swift$/,
333
+ )
334
+ collect(module.root, 'nitrogen/generated/ios', targetName, null)
335
+ add(
336
+ `${targetPath(config.swiftModule)}/PeachNitroExports.swift`,
337
+ `@_exported import ${targetName}\n`,
338
+ )
339
+ }
269
340
 
270
341
  // explicit module maps avoid importing every RN view or private JSI header.
271
342
  add(
@@ -274,7 +345,7 @@ export function collectNitroSources(options: NitroSourceOptions): NitroSourceCol
274
345
  )
275
346
  add(
276
347
  `${targetPath(coreTarget)}/include/module.modulemap`,
277
- `module ${coreTarget} { header "NitroModules/InstallNitro.hpp" export * }\n`,
348
+ `module ${coreTarget} { header "NitroModules/InstallNitro.hpp"${config.swiftModule ? ['ArrayBufferHolder.hpp', 'AnyMapUtils.hpp', 'PromiseHolder.hpp', 'Result.hpp', 'RuntimeError.hpp', 'DateToChronoDate.hpp'].map((header) => ` header "NitroModules/${header}"`).join('') : ''} export * }\n`,
278
349
  )
279
350
  add(
280
351
  result.registration.header,
@@ -282,7 +353,7 @@ export function collectNitroSources(options: NitroSourceOptions): NitroSourceCol
282
353
  )
283
354
  add(
284
355
  `${targetPath(targetName)}/include/module.modulemap`,
285
- `module ${targetName} { header "PeachNitroRegistration.hpp" export * }\n`,
356
+ `module ${targetName} { header "PeachNitroRegistration.hpp"${config.swiftModule ? ` header "${config.swiftModule}-Swift-Cxx-Bridge.hpp"` : ''} export * }\n`,
286
357
  )
287
358
  add(
288
359
  `${targetPath(targetName)}/PeachNitroRegistration.cpp`,
@@ -291,15 +362,28 @@ export function collectNitroSources(options: NitroSourceOptions): NitroSourceCol
291
362
  '#include <NitroModules/HybridObjectRegistry.hpp>',
292
363
  '#include <memory>',
293
364
  '#include <type_traits>',
294
- ...[...new Set(config.classes.map(({ implementation }) => implementation))].map(
295
- (implementation) => `#include "${implementation}.hpp"`,
296
- ),
365
+ ...(config.swiftModule
366
+ ? [`#include "${config.swiftModule}-Swift-Cxx-Umbrella.hpp"`]
367
+ : []),
368
+ ...[
369
+ ...new Set(
370
+ config.classes
371
+ .filter((entry) => entry.language === 'c++')
372
+ .map(({ implementation }) => implementation),
373
+ ),
374
+ ].map((implementation) => `#include "${implementation}.hpp"`),
297
375
  `extern "C" void ${result.registration.symbol}() {`,
298
- ...config.classes.flatMap(({ name, implementation }) => [
299
- ` static_assert(std::is_default_constructible_v<${config.namespace}::${implementation}>);`,
376
+ ...config.classes.flatMap((entry) => [
377
+ ...(entry.language === 'c++'
378
+ ? [
379
+ ` static_assert(std::is_default_constructible_v<${config.namespace}::${entry.implementation}>);`,
380
+ ]
381
+ : []),
300
382
  ' margelo::nitro::HybridObjectRegistry::registerHybridObjectConstructor(',
301
- ` ${JSON.stringify(name)}, []() -> std::shared_ptr<margelo::nitro::HybridObject> {`,
302
- ` return std::make_shared<${config.namespace}::${implementation}>();`,
383
+ ` ${JSON.stringify(entry.name)}, []() -> std::shared_ptr<margelo::nitro::HybridObject> {`,
384
+ entry.language === 'swift'
385
+ ? ` return ${config.swiftModule}::${config.swiftModule}Autolinking::create${entry.name}();`
386
+ : ` return std::make_shared<${config.namespace}::${entry.implementation}>();`,
303
387
  ' });',
304
388
  ]),
305
389
  '}',