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
@@ -2,14 +2,16 @@
2
2
  // each linked product resolves through swiftpm's own reading of its package
3
3
  // (swift-package-manifest.ts) to the source targets that build it; remote
4
4
  // packages are fetched at the revision the app's Package.resolved pins, into
5
- // .build/peach/checkouts under the app's root. swift and c-family source
5
+ // PEACH_HOME/cache/swift-packages, isolated by the app root. swift and c-family source
6
6
  // targets use the common compiler backend. objective-c, assembly and binary
7
7
  // targets the host built no stand-in for stay missing with their reason;
8
8
  // the targets beside them still compile.
9
9
 
10
10
  import { execFileSync } from 'node:child_process'
11
+ import { createHash } from 'node:crypto'
11
12
  import fs from 'node:fs'
12
13
  import path from 'node:path'
14
+ import { cacheDir } from './home-paths'
13
15
  import {
14
16
  swiftImplicitResourcePaths,
15
17
  swiftPackageIdentity,
@@ -19,7 +21,7 @@ import {
19
21
  import {
20
22
  swiftPackageManifest,
21
23
  swiftTargetFiles,
22
- type SwiftManifestDefine,
24
+ type SwiftManifestSetting,
23
25
  type SwiftPackageManifest,
24
26
  } from './swift-package-manifest'
25
27
 
@@ -43,7 +45,8 @@ export interface SwiftPackageSourceTarget extends SwiftPackageTarget {
43
45
  packageDir: string
44
46
  remote: boolean
45
47
  /** the conditions its manifest defines, before a build configuration picks them. */
46
- packageDefines: SwiftManifestDefine[]
48
+ packageDefines: SwiftManifestSetting[]
49
+ packageLibraries: SwiftManifestSetting[]
47
50
  }
48
51
 
49
52
  /** why a product or module does not compile, and where it comes from. */
@@ -74,47 +77,68 @@ export interface SwiftPackageBinary {
74
77
  dir: string
75
78
  /** a linked product's closure reaches it, so the app links it. */
76
79
  reached: boolean
80
+ /** a source Objective-C target: public headers relative to dir. */
81
+ publicHeaders?: string
82
+ /** dependency module public headers, relative to dir. */
83
+ headerDependencies?: { module: string; path: string }[]
77
84
  }
78
85
 
79
- /** where checkouts live, relative to the app's root. */
80
- export const REMOTE_CHECKOUTS = path.join('.build', 'peach', 'checkouts')
86
+ /** dependency checkouts belong to peach, isolated by the canonical app root. */
87
+ export function remoteSwiftCheckouts(root: string): string {
88
+ const key = createHash('sha256')
89
+ .update(fs.realpathSync(root))
90
+ .digest('hex')
91
+ .slice(0, 32)
92
+ fs.mkdirSync(cacheDir(), { recursive: true })
93
+ return path.join(fs.realpathSync(cacheDir()), 'swift-packages', key, 'checkouts')
94
+ }
81
95
 
82
96
  // sources requiring an objective-c runtime or assembly lowering.
83
97
  const UNSUPPORTED_NATIVE = /\.(m|mm|s|S)$/
84
98
 
85
99
  const BINARY = 'is a binary framework'
86
- // of the reasons a target or product cannot compile, a binary framework's
87
- // leads: no source exists for it, while another target can retain its
88
- // source-specific reason
89
- const blocker = (reasons: (string | null)[]): string | null =>
90
- reasons.find((reason) => reason?.includes(BINARY)) ??
91
- reasons.find((reason) => reason !== null) ??
92
- null
100
+ // retain every independent blocker in dependency order. a product can reach
101
+ // several unsupported targets, and fixing one must not reveal a hidden next one.
102
+ const blockers = (reasons: readonly string[][]): string[] => [...new Set(reasons.flat())]
93
103
 
94
104
  interface Pin {
95
105
  location: string
96
106
  revision: string
97
107
  }
98
108
 
99
- function readPins(resolved: string): Map<string, Pin> {
100
- const data: unknown = JSON.parse(fs.readFileSync(resolved, 'utf8'))
101
- const pins =
102
- data && typeof data === 'object' && 'pins' in data && Array.isArray(data.pins)
103
- ? data.pins
104
- : null
105
- if (!pins)
106
- throw new Error(`${resolved} has no pins; resolve packages in a current Xcode`)
109
+ function readPins(resolved: readonly string[]): Map<string, Pin> {
107
110
  const out = new Map<string, Pin>()
108
- for (const pin of pins) {
109
- const identity = pin?.identity
110
- const location = pin?.location
111
- const revision = pin?.state?.revision
112
- if (
113
- typeof identity === 'string' &&
114
- typeof location === 'string' &&
115
- typeof revision === 'string'
116
- )
117
- out.set(identity, { location, revision })
111
+ const owners = new Map<string, string>()
112
+ for (const file of resolved) {
113
+ const data: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
114
+ const pins =
115
+ data && typeof data === 'object' && 'pins' in data && Array.isArray(data.pins)
116
+ ? data.pins
117
+ : null
118
+ if (!pins) throw new Error(`${file} has no pins; resolve packages in a current Xcode`)
119
+ for (const pin of pins) {
120
+ const identity = pin?.identity
121
+ const location = pin?.location
122
+ const revision = pin?.state?.revision
123
+ if (
124
+ typeof identity === 'string' &&
125
+ typeof location === 'string' &&
126
+ typeof revision === 'string'
127
+ ) {
128
+ const previous = out.get(identity)
129
+ const repository = (url: string) => url.replace(/(?:\.git)?\/?$/, '')
130
+ if (
131
+ previous &&
132
+ (previous.revision !== revision ||
133
+ repository(previous.location) !== repository(location))
134
+ )
135
+ throw new Error(
136
+ `conflicting Package.resolved pins for ${identity}: ${owners.get(identity)} pins ${previous.location} at ${previous.revision}; ${file} pins ${location} at ${revision}`,
137
+ )
138
+ out.set(identity, { location, revision })
139
+ owners.set(identity, file)
140
+ }
141
+ }
118
142
  }
119
143
  return out
120
144
  }
@@ -167,8 +191,8 @@ interface LoadedPackage {
167
191
  */
168
192
  export function resolveSwiftPackages(options: {
169
193
  rootDir: string
170
- /** the Package.resolved pinning remote revisions, when the app has one. */
171
- resolved: string | null
194
+ /** the selected workspace or local Package.resolved files. */
195
+ resolved: readonly string[]
172
196
  products: readonly SwiftLinkedProduct[]
173
197
  /** local package directories the workspace or a project references. */
174
198
  localDirs: readonly string[]
@@ -184,8 +208,8 @@ export function resolveSwiftPackages(options: {
184
208
  modules: new Map(),
185
209
  binaries: [],
186
210
  }
187
- const pins = resolved ? readPins(resolved) : null
188
- const checkouts = path.join(rootDir, REMOTE_CHECKOUTS)
211
+ const pins = resolved.length > 0 ? readPins(resolved) : null
212
+ const checkouts = remoteSwiftCheckouts(rootDir)
189
213
 
190
214
  const packages = new Map<string, LoadedPackage | string>()
191
215
  // a package by local directory or remote url, or why it cannot load
@@ -205,8 +229,6 @@ export function resolveSwiftPackages(options: {
205
229
  : 'no Package.resolved pins it; resolve packages in Xcode once'
206
230
  } else {
207
231
  fs.mkdirSync(checkouts, { recursive: true })
208
- // fetched sources never show up as changes in the app's repository
209
- fs.writeFileSync(path.join(path.dirname(checkouts), '.gitignore'), '*\n')
210
232
  loaded = {
211
233
  source: source.url,
212
234
  remote: true,
@@ -219,35 +241,40 @@ export function resolveSwiftPackages(options: {
219
241
  }
220
242
 
221
243
  const targets = new Map<string, SwiftPackageSourceTarget>()
222
- // why each target of each package cannot compile, or null once it can
223
- const targetOutcomes = new Map<string, string | null>()
224
- const resolveTarget = (pkg: LoadedPackage, name: string): string | null => {
244
+ // every reason each target cannot compile; empty once it can
245
+ const targetOutcomes = new Map<string, string[]>()
246
+ const resolveTarget = (pkg: LoadedPackage, name: string): string[] => {
225
247
  const key = `${pkg.manifest.dir}\0${name}`
226
248
  if (targetOutcomes.has(key)) return targetOutcomes.get(key)!
227
249
  // a dependency cycle resolves as compiling; swiftpm rejects real cycles
228
- targetOutcomes.set(key, null)
229
- const outcome = ((): string | null => {
230
- if (provided.has(name)) return null
250
+ targetOutcomes.set(key, [])
251
+ const outcome = ((): string[] => {
252
+ if (provided.has(name)) return []
231
253
  const target = pkg.manifest.targets.get(name)
232
- if (!target) return `${path.basename(pkg.manifest.dir)} declares no target ${name}`
233
- if (target.type === 'binary') {
234
- const gap = options.binaryGaps?.get(name)
235
- return `${name} ${BINARY}${gap ? `; ${gap}` : ''}`
236
- }
237
- if (target.type === 'system') return `${name} is a system C library`
238
- if (target.type !== 'regular') return `${name} is a ${target.type} target`
239
- const files = swiftTargetFiles(pkg.manifest, target)
240
- if (files.some((file) => UNSUPPORTED_NATIVE.test(file)))
241
- return `${name} is an Objective-C or assembly target`
254
+ if (!target)
255
+ return [`${path.basename(pkg.manifest.dir)} declares no target ${name}`]
256
+ const files =
257
+ target.type === 'regular' || target.type === 'system'
258
+ ? swiftTargetFiles(pkg.manifest, target)
259
+ : []
260
+ const gap = options.binaryGaps?.get(name)
261
+ const ownReason =
262
+ target.type === 'binary'
263
+ ? `${name} ${BINARY}${gap ? `; ${gap}` : ''}`
264
+ : target.type !== 'regular' && target.type !== 'system'
265
+ ? `${name} is a ${target.type} target`
266
+ : files.some((file) => UNSUPPORTED_NATIVE.test(file))
267
+ ? `${name} is an Objective-C or assembly target${gap ? `; ${gap}` : ''}`
268
+ : null
242
269
  // every dependency resolves, so a failing one never hides the rest
243
270
  // of the closure from the compile or from the report
244
271
  const reasons = target.dependencies.map((dependency) => {
245
272
  if (dependency.kind === 'target') return resolveTarget(pkg, dependency.name)
246
273
  const nested = resolveDependencyProduct(pkg, dependency.package, dependency.name)
247
- return nested && `${dependency.name}: ${nested}`
274
+ return nested.map((reason) => `${dependency.name}: ${reason}`)
248
275
  })
249
- const blocked = blocker(reasons)
250
- if (blocked) return blocked
276
+ const blocked = blockers([ownReason ? [ownReason] : [], ...reasons])
277
+ if (blocked.length > 0) return blocked
251
278
  const relative = files.map((file) => path.relative(target.dir, file))
252
279
  targets.set(`${pkg.manifest.dir}\0${name}`, {
253
280
  name,
@@ -257,6 +284,7 @@ export function resolveSwiftPackages(options: {
257
284
  packageDir: pkg.manifest.dir,
258
285
  remote: pkg.remote,
259
286
  packageDefines: target.defines,
287
+ packageLibraries: target.libraries,
260
288
  dependencies: target.dependencies.flatMap((dependency) => {
261
289
  if (dependency.kind === 'target') return [dependency]
262
290
  if (provided.has(dependency.name)) return []
@@ -265,32 +293,35 @@ export function resolveSwiftPackages(options: {
265
293
  source &&
266
294
  packages.get('dir' in source ? path.resolve(source.dir) : source.url)
267
295
  return nested && typeof nested !== 'string'
268
- ? (nested.manifest.products.get(dependency.name) ?? []).map((name): SwiftTargetDependency => ({
269
- kind: 'target',
270
- name,
271
- }))
296
+ ? (nested.manifest.products.get(dependency.name) ?? []).map(
297
+ (name): SwiftTargetDependency => ({
298
+ kind: 'target',
299
+ name,
300
+ }),
301
+ )
272
302
  : []
273
303
  }),
274
304
  executable: false,
305
+ ...(target.type === 'system' ? { systemLibrary: true } : {}),
275
306
  languageMode: target.languageMode,
276
307
  mainActorIsolation: target.mainActorIsolation,
277
308
  resources:
278
309
  target.declaresResources || swiftImplicitResourcePaths(relative).length > 0,
279
310
  })
280
- return null
311
+ return []
281
312
  })()
282
313
  targetOutcomes.set(key, outcome)
283
314
  return outcome
284
315
  }
285
316
  // products resolved anywhere in the closure, by package, with why one
286
317
  // cannot compile: a wrapper package's product is often the module name
287
- const productOutcomes = new Map<string, { pkg: LoadedPackage; reason: string | null }>()
288
- const resolveProduct = (pkg: LoadedPackage, product: string): string | null => {
289
- if (provided.has(product)) return null
318
+ const productOutcomes = new Map<string, { pkg: LoadedPackage; reason: string[] }>()
319
+ const resolveProduct = (pkg: LoadedPackage, product: string): string[] => {
320
+ if (provided.has(product)) return []
290
321
  const roots = pkg.manifest.products.get(product)
291
322
  const reason = !roots
292
- ? `${path.basename(pkg.manifest.dir)} has no library ${product}`
293
- : blocker(roots.map((name) => resolveTarget(pkg, name)))
323
+ ? [`${path.basename(pkg.manifest.dir)} has no library ${product}`]
324
+ : blockers(roots.map((name) => resolveTarget(pkg, name)))
294
325
  productOutcomes.set(`${pkg.manifest.dir}\0${product}`, { pkg, reason })
295
326
  return reason
296
327
  }
@@ -299,18 +330,18 @@ export function resolveSwiftPackages(options: {
299
330
  pkg: LoadedPackage,
300
331
  packageName: string,
301
332
  product: string,
302
- ): string | null => {
303
- if (provided.has(product)) return null
333
+ ): string[] => {
334
+ if (provided.has(product)) return []
304
335
  const dependency = pkg.manifest.packages.get(packageName)
305
336
  if (!dependency)
306
- return `${path.basename(pkg.manifest.dir)} names no package ${packageName}`
337
+ return [`${path.basename(pkg.manifest.dir)} names no package ${packageName}`]
307
338
  if ('dir' in dependency) {
308
339
  const loaded = load({ kind: 'local', dir: dependency.dir })
309
- return typeof loaded === 'string' ? loaded : resolveProduct(loaded, product)
340
+ return typeof loaded === 'string' ? [loaded] : resolveProduct(loaded, product)
310
341
  }
311
- if (!options.provided) return null
342
+ if (!options.provided) return []
312
343
  const loaded = load({ kind: 'remote', url: dependency.url })
313
- return typeof loaded === 'string' ? loaded : resolveProduct(loaded, product)
344
+ return typeof loaded === 'string' ? [loaded] : resolveProduct(loaded, product)
314
345
  }
315
346
 
316
347
  const unnamed = () =>
@@ -326,11 +357,11 @@ export function resolveSwiftPackages(options: {
326
357
  : load(source)
327
358
  const reason =
328
359
  pkg === undefined
329
- ? 'no package the workspace references vends it'
360
+ ? ['no package the workspace references vends it']
330
361
  : typeof pkg === 'string'
331
- ? pkg
362
+ ? [pkg]
332
363
  : resolveProduct(pkg, product)
333
- if (reason)
364
+ if (reason.length > 0)
334
365
  result.unavailable.set(product, {
335
366
  source:
336
367
  source.kind === 'remote'
@@ -340,7 +371,7 @@ export function resolveSwiftPackages(options: {
340
371
  : source.kind === 'local'
341
372
  ? source.dir
342
373
  : 'the workspace',
343
- reason,
374
+ reason: reason.join('; '),
344
375
  })
345
376
  }
346
377
 
@@ -352,24 +383,35 @@ export function resolveSwiftPackages(options: {
352
383
  if (target.type === 'test' || target.type === 'plugin' || target.type === 'macro')
353
384
  continue
354
385
  if (target.type === 'executable') continue
355
- if (target.type === 'binary')
386
+ const objc =
387
+ target.type === 'regular' &&
388
+ swiftTargetFiles(pkg.manifest, target).some((file) => /\.(m|mm)$/.test(file))
389
+ if (target.type === 'binary' || objc)
356
390
  result.binaries.push({
357
391
  target: name,
358
392
  source: pkg.source,
359
393
  url: target.url,
360
394
  checksum: target.checksum,
361
- dir: target.dir,
395
+ dir: objc ? pkg.manifest.dir : target.dir,
362
396
  reached: false,
397
+ ...(objc
398
+ ? {
399
+ publicHeaders: path.relative(
400
+ pkg.manifest.dir,
401
+ path.join(target.dir, target.publicHeadersPath),
402
+ ),
403
+ }
404
+ : {}),
363
405
  })
364
406
  const reason = resolveTarget(pkg, name)
365
- if (reason && !result.modules.has(name))
366
- result.modules.set(name, { source: pkg.source, reason })
407
+ if (reason.length > 0 && !result.modules.has(name))
408
+ result.modules.set(name, { source: pkg.source, reason: reason.join('; ') })
367
409
  }
368
410
  }
369
411
  for (const [key, { pkg, reason }] of productOutcomes) {
370
412
  const product = key.slice(key.indexOf('\0') + 1)
371
- if (reason && !result.modules.has(product))
372
- result.modules.set(product, { source: pkg.source, reason })
413
+ if (reason.length > 0 && !result.modules.has(product))
414
+ result.modules.set(product, { source: pkg.source, reason: reason.join('; ') })
373
415
  }
374
416
  // what compiles is every buildable target in a linked product's closure,
375
417
  // decided per target: a product with one target behind a binary still
@@ -410,6 +452,37 @@ export function resolveSwiftPackages(options: {
410
452
  if (pkg && typeof pkg !== 'string')
411
453
  binary.reached = reached.has(`${pkg.manifest.dir}\0${binary.target}`)
412
454
  }
455
+ for (const binary of result.binaries) {
456
+ if (binary.publicHeaders === undefined) continue
457
+ const pkg = packages.get(binary.source)
458
+ if (!pkg || typeof pkg === 'string') continue
459
+ const headers = new Map<string, string>()
460
+ const visited = new Set<string>()
461
+ const visit = (owner: LoadedPackage, name: string) => {
462
+ const key = `${owner.manifest.dir}\0${name}`
463
+ if (visited.has(key)) return
464
+ visited.add(key)
465
+ const target = owner.manifest.targets.get(name)
466
+ if (!target) return
467
+ const dir = path.join(target.dir, target.publicHeadersPath)
468
+ for (const dependency of target.dependencies) {
469
+ if (dependency.kind === 'target') visit(owner, dependency.name)
470
+ else {
471
+ const source = owner.manifest.packages.get(dependency.package)
472
+ const nested =
473
+ source &&
474
+ packages.get('dir' in source ? path.resolve(source.dir) : source.url)
475
+ if (nested && typeof nested !== 'string')
476
+ for (const root of nested.manifest.products.get(dependency.name) ?? [])
477
+ visit(nested, root)
478
+ }
479
+ }
480
+ if (name !== binary.target && fs.existsSync(dir))
481
+ headers.set(name, path.relative(binary.dir, dir))
482
+ }
483
+ visit(pkg, binary.target)
484
+ binary.headerDependencies = [...headers].map(([module, path]) => ({ module, path }))
485
+ }
413
486
  result.targets = [...targets.entries()]
414
487
  .filter(([key]) => reached.has(key))
415
488
  .map(([, target]) => target)
@@ -20,6 +20,7 @@ export type GraphSymbol = {
20
20
  pathComponents: string[]
21
21
  declarationFragments?: SymbolFragment[]
22
22
  accessLevel: string
23
+ functionSignature?: { returns?: SymbolFragment[] }
23
24
  availability?: {
24
25
  domain?: string
25
26
  isUnconditionallyUnavailable?: boolean
@@ -43,6 +44,8 @@ export type StandinOptions = {
43
44
  module: string
44
45
  /** where the declarations came from, for the banner: sdk, version, artifact. */
45
46
  source: string
47
+ /** source Objective-C targets keep declarations with inert native bodies. */
48
+ inert?: boolean
46
49
  /** modules the source imports beside Foundation and UIKit. */
47
50
  imports?: string[]
48
51
  /** precise symbol id → swift literal for enum cases and option values. */
@@ -87,7 +90,7 @@ export type StandinResult = {
87
90
  }
88
91
 
89
92
  /** bump when emitted source changes: the stand-in cache keys on it. */
90
- export const STANDIN_EMIT_VERSION = '7'
93
+ export const STANDIN_EMIT_VERSION = '12'
91
94
 
92
95
  /** the modules every stand-in source imports. */
93
96
  export const STANDIN_IMPORTS = ['Foundation', 'UIKit']
@@ -226,6 +229,11 @@ export function emitSwiftStandin(
226
229
  family.has(parts[0]!) && parts[0] !== 'SwiftUICore' ? parts.join('.') : unqualified
227
230
  return { name, module, local: false }
228
231
  }
232
+ const nsObject = nameOf('c:objc(cs)NSObject', 'ObjectiveC.NSObject')!.name
233
+ const nsObjectProtocol = nameOf(
234
+ 'c:objc(pl)NSObject',
235
+ 'ObjectiveC.NSObjectProtocol',
236
+ )!.name
229
237
  for (const graph of graphs)
230
238
  for (const edge of graph.relationships) {
231
239
  const source = byId.get(edge.source)
@@ -331,6 +339,47 @@ export function emitSwiftStandin(
331
339
  ])
332
340
  }
333
341
 
342
+ // DEGRADED(swift-objc-runtime): source objc targets retain their api with inert native bodies.
343
+ // return values follow the imported declaration, never the app's call sites.
344
+ const inertValue = (type: string): string => {
345
+ const value = type.trim().replace(/!$/, '?')
346
+ if (value.endsWith('?')) return 'nil'
347
+ if (value === 'Void' || value === '()') return '()'
348
+ if (value === 'Bool') return 'false'
349
+ if (
350
+ /^(?:U?Int(?:8|16|32|64)?|Float|Double|CGFloat|TimeInterval|NSTimeInterval)$/.test(
351
+ value,
352
+ )
353
+ )
354
+ return '0'
355
+ if (value === 'String') return '""'
356
+ if (value.startsWith('[')) return value.includes(':') ? '[:]' : '[]'
357
+ if (value === 'Any' || value === 'AnyObject') return 'NSObject()'
358
+ const typeId = typeByPath.get(value.replace(`${options.module}.`, ''))
359
+ const symbol = typeId && byId.get(typeId)
360
+ if (typeId && symbol && symbol.kind.identifier === 'swift.enum') {
361
+ const first = membersOf(typeId).find(
362
+ (member) => member.kind.identifier === 'swift.enum.case',
363
+ )
364
+ if (first) return `.${leaf(first).replace(/\(.*/, '')}`
365
+ }
366
+ return `${value}()`
367
+ }
368
+ const inertBody = (symbol: GraphSymbol): string => {
369
+ const declaration = text(symbol)
370
+ if (symbol.kind.identifier === 'swift.init') {
371
+ if (/\bconvenience\b/.test(declaration)) return 'self.init()'
372
+ const parent = owner.get(symbol.identifier.precise)
373
+ return parent && superclass.has(parent) ? 'super.init()' : ''
374
+ }
375
+ const type =
376
+ symbol.kind.identifier.includes('property') ||
377
+ symbol.kind.identifier === 'swift.var'
378
+ ? /:\s*(.*?)\s*(?:\{|$)/.exec(declaration)?.[1]
379
+ : symbol.functionSignature?.returns?.map((fragment) => fragment.spelling).join('')
380
+ return type ? `return ${inertValue(type)}` : ''
381
+ }
382
+
334
383
  const lines: string[] = []
335
384
  const owners: string[] = []
336
385
  const omit = options.omit ?? new Map<string, string>()
@@ -426,7 +475,8 @@ export function emitSwiftStandin(
426
475
  : /\bResult</.test(core)
427
476
  ? measured?.resultBody
428
477
  : undefined
429
- const body = overloadBody ?? measured?.body ?? trap(path)
478
+ const body =
479
+ overloadBody ?? measured?.body ?? (options.inert ? inertBody(member) : trap(path))
430
480
  switch (member.kind.identifier) {
431
481
  case 'swift.property':
432
482
  case 'swift.type.property':
@@ -452,7 +502,7 @@ export function emitSwiftStandin(
452
502
  return
453
503
  }
454
504
  emit(
455
- `${head}${computed} { get${effects} { ${body} }${settable && !effects ? ` ${parsed.nonmutating ? 'nonmutating ' : ''}set { ${measured?.set ?? trap(path)} }` : ''} }`,
505
+ `${head}${computed} { get${effects} { ${body} }${settable && !effects ? ` ${parsed.nonmutating ? 'nonmutating ' : ''}set { ${measured?.set ?? (options.inert ? '' : trap(path))} }` : ''} }`,
456
506
  )
457
507
  return
458
508
  }
@@ -532,7 +582,18 @@ export function emitSwiftStandin(
532
582
  const inner = `${indent} `
533
583
  switch (symbol.kind.identifier) {
534
584
  case 'swift.typealias': {
535
- const { 1: attrs, 2: rest } = LEADING_ATTRS.exec(text(symbol))!
585
+ // a graph shortens an external alias to its leaf, which can shadow itself.
586
+ const fragments = (symbol.declarationFragments ?? []).map((fragment) =>
587
+ fragment.kind === 'typeIdentifier' &&
588
+ fragment.spelling === leaf(symbol) &&
589
+ fragment.preciseIdentifier?.startsWith('s:s') &&
590
+ fragment.preciseIdentifier !== id
591
+ ? { ...fragment, spelling: `Swift.${fragment.spelling}` }
592
+ : fragment,
593
+ )
594
+ const { 1: attrs, 2: rest } = LEADING_ATTRS.exec(
595
+ text({ ...symbol, declarationFragments: fragments }),
596
+ )!
536
597
  emit(`${indent}${attrs}public ${rest}`)
537
598
  return
538
599
  }
@@ -553,7 +614,8 @@ export function emitSwiftStandin(
553
614
  ...(conforms.get(id) ?? []).filter(
554
615
  (name) =>
555
616
  !ancestors.has(name) &&
556
- !(parent && [...KEEP_EXTERNAL.values()].includes(name)),
617
+ !(parent && [...KEEP_EXTERNAL.values()].includes(name)) &&
618
+ !(name === nsObjectProtocol && externalRoot(id) === nsObject),
557
619
  ),
558
620
  ].filter((name, at, all) => all.indexOf(name) === at)
559
621
  const parentSymbol = superId.has(id) ? byId.get(superId.get(id)!) : undefined
@@ -646,7 +708,7 @@ export function emitSwiftStandin(
646
708
  externalRoot(id) !== null
647
709
  )
648
710
  emit(
649
- `${inner}public required init?(coder: NSCoder) { ${trap(`${path}.init(coder:)`)} }`,
711
+ `${inner}public required init?(coder: NSCoder) { ${options.inert ? 'return nil' : trap(`${path}.init(coder:)`)} }`,
650
712
  )
651
713
  emit(`${indent}}`)
652
714
  return
@@ -674,7 +736,8 @@ export function emitSwiftStandin(
674
736
  synthesizedEquality(member, list))
675
737
  )
676
738
  continue
677
- const value = values.get(member.identifier.precise)
739
+ const header = values.get(member.identifier.precise)
740
+ const value = header === undefined ? undefined : atWidth(header, raw)
678
741
  if (value !== undefined && member.kind.identifier === 'swift.type.property') {
679
742
  emit(
680
743
  `${inner}public static let ${tick(leaf(member))} = ${shape.name}(rawValue: ${value})`,
@@ -694,10 +757,13 @@ export function emitSwiftStandin(
694
757
  if (!shape)
695
758
  return void skipped.push({ path, reason: `unparsed enum ${text(symbol)}` })
696
759
  const members = membersOf(id)
697
- const raw = rawType(members)
698
760
  const cases = members.filter(
699
761
  (member) => member.kind.identifier === 'swift.enum.case',
700
762
  )
763
+ // payload cases use a manual RawRepresentable witness, not a raw enum.
764
+ const raw = cases.some((member) => text(member).includes('('))
765
+ ? null
766
+ : rawType(members)
701
767
  const missing = raw
702
768
  ? cases.filter((member) => !values.has(member.identifier.precise))
703
769
  : []
@@ -709,14 +775,15 @@ export function emitSwiftStandin(
709
775
  reason: `no header value for ${missing.map(leaf).join(', ')}`,
710
776
  })
711
777
  const list = inheritance(id, raw ?? undefined).filter(
712
- (name) => name !== 'RawRepresentable',
778
+ (name) => !raw || name !== 'RawRepresentable',
713
779
  )
714
780
  emit(
715
781
  `${indent}${shape.attrs}public enum ${shape.name}${shape.generics}${list.length ? `: ${list.join(', ')}` : ''}${shape.where} {`,
716
782
  )
717
783
  const seenValues = new Map<string, string>()
718
784
  for (const member of cases) {
719
- const value = values.get(member.identifier.precise)
785
+ const header = values.get(member.identifier.precise)
786
+ const value = header === undefined ? undefined : atWidth(header, raw)
720
787
  // objective-c aliases a value under a second name; swift spells
721
788
  // the alias as a static member of the first case.
722
789
  if (value !== undefined && seenValues.has(value)) {
@@ -814,7 +881,7 @@ export function emitSwiftStandin(
814
881
  .replace(/^(?:weak |unowned )/, '')
815
882
  .replace(/^((?:static )?)let /, '$1var ')
816
883
  const witness = `${owner}.${leaf(member)}`
817
- const body = trap(witness)
884
+ const body = options.inert ? inertBody(member) : trap(witness)
818
885
  if (
819
886
  member.kind.identifier === 'swift.property' ||
820
887
  member.kind.identifier === 'swift.type.property'
@@ -846,6 +913,17 @@ export function emitSwiftStandin(
846
913
  const init = members.find((member) => leaf(member) === 'init(rawValue:)')
847
914
  return init ? (/rawValue:\s*([^)]+)\)/.exec(text(init))?.[1]?.trim() ?? null) : null
848
915
  }
916
+ // header values are clang's for 64-bit iOS. NSUIntegerMax, NSIntegerMax
917
+ // and NSIntegerMin are the platform-width type's own extremes, which on
918
+ // wasm32 are the 32-bit ones.
919
+ const atWidth = (value: string, raw: string | null) =>
920
+ (raw === 'UInt' || raw === 'Swift.UInt') && value === '18446744073709551615'
921
+ ? '4294967295'
922
+ : (raw === 'Int' || raw === 'Swift.Int') && value === '9223372036854775807'
923
+ ? '2147483647'
924
+ : (raw === 'Int' || raw === 'Swift.Int') && value === '-9223372036854775808'
925
+ ? '-2147483648'
926
+ : value
849
927
 
850
928
  const emitGlobal = scoped((symbol: GraphSymbol) => {
851
929
  const path = pathOf(symbol)
@@ -872,7 +950,7 @@ export function emitSwiftStandin(
872
950
  ? measured?.resultBody
873
951
  : undefined
874
952
  emit(
875
- `${attrs}public ${spelled} { ${overloadBody ?? measured?.body ?? trap(path)} }`,
953
+ `${attrs}public ${spelled} { ${overloadBody ?? measured?.body ?? (options.inert ? inertBody(symbol) : trap(path))} }`,
876
954
  )
877
955
  } else if (kind === 'swift.var') {
878
956
  const parsed = splitAccessors(text(symbol).replace(/^let /, 'var '))
@@ -882,7 +960,9 @@ export function emitSwiftStandin(
882
960
  // as `var` with no setter.
883
961
  const measured = behavior.get(path)
884
962
  if (measured) used.add(path)
885
- emit(`public ${parsed.decl} { ${measured?.body ?? trap(path)} }`)
963
+ emit(
964
+ `public ${parsed.decl} { ${measured?.body ?? (options.inert ? inertBody(symbol) : trap(path))} }`,
965
+ )
886
966
  } else skipped.push({ path, reason: `unhandled top-level ${kind}` })
887
967
  })
888
968
 
@@ -929,8 +1009,9 @@ export function emitSwiftStandin(
929
1009
  const imports = [...STANDIN_IMPORTS, ...(options.imports ?? [])]
930
1010
  const banner = [
931
1011
  `// stand-in for the binary ${options.module} module, generated from ${options.source}.`,
932
- `// do not hand-edit: unconfigured bodies trap because the vendor binary cannot run`,
933
- `// in peach; app native stubs may supply configured behavior.`,
1012
+ options.inert
1013
+ ? '// generated from public headers; native behavior is inert in peach.'
1014
+ : '// unconfigured bodies trap because the vendor binary cannot run in peach.',
934
1015
  '',
935
1016
  ...imports.map((name) => `import ${name}`),
936
1017
  '',