peachsim 0.1.1620 → 0.1.1625

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 (138) hide show
  1. package/README.md +35 -5
  2. package/cli/app-fonts.ts +2 -1
  3. package/cli/bridge-flow-runner.ts +184 -113
  4. package/cli/cloud-client.ts +68 -57
  5. package/cli/commands/box/search-command.ts +3 -2
  6. package/cli/commands/box.ts +10 -29
  7. package/cli/commands/control.ts +108 -43
  8. package/cli/commands/inspect/settling.ts +57 -20
  9. package/cli/commands/maestro-compare.ts +11 -32
  10. package/cli/commands/platform.ts +71 -36
  11. package/cli/commands/upload.ts +5 -1
  12. package/cli/dev-build-freshness.ts +37 -2
  13. package/cli/drivers/playwright-sim-host.ts +6 -5
  14. package/cli/maestro-accessibility.ts +53 -0
  15. package/cli/maestro-compare.ts +24 -0
  16. package/cli/outbound-endpoints.ts +22 -5
  17. package/cli/parent-pid.ts +0 -9
  18. package/cli/remote-simulators.ts +112 -0
  19. package/cli/remoteSimulatorContract.ts +21 -0
  20. package/detox/expectations.ts +5 -1
  21. package/detox/index.ts +133 -98
  22. package/detox/motion-change.cjs +37 -0
  23. package/detox/motion-change.d.cts +16 -0
  24. package/detox/navigation-lifecycle.ts +22 -4
  25. package/dist-lib/agent-daemon-client.cjs +3 -2
  26. package/dist-lib/agent-events.cjs +1 -1
  27. package/dist-lib/attached-projects.cjs +1 -1
  28. package/dist-lib/auth/shared-session.cjs +3 -2
  29. package/dist-lib/backend-origin.cjs +1 -1
  30. package/dist-lib/beta.cjs +1 -1
  31. package/dist-lib/beta.mjs +1 -1
  32. package/dist-lib/bridge-constants.cjs +1 -1
  33. package/dist-lib/bridge-contract-input.cjs +1 -1
  34. package/dist-lib/bridge-contract-input.mjs +1 -1
  35. package/dist-lib/bridge-contract.cjs +1 -1
  36. package/dist-lib/bridge-contract.mjs +1 -1
  37. package/dist-lib/bundle-channel.cjs +1520 -934
  38. package/dist-lib/capture-contract.cjs +1 -1
  39. package/dist-lib/capture-contract.mjs +1 -1
  40. package/dist-lib/cloud-contract.cjs +1 -1
  41. package/dist-lib/cloud-contract.mjs +1 -1
  42. package/dist-lib/cloud-test-args.cjs +1 -1
  43. package/dist-lib/cloud-test-args.mjs +1 -1
  44. package/dist-lib/cloud.cjs +3 -2
  45. package/dist-lib/cloud.mjs +3 -2
  46. package/dist-lib/config.cjs +5 -4
  47. package/dist-lib/config.mjs +5 -4
  48. package/dist-lib/detox/index.cjs +1021 -364
  49. package/dist-lib/dev-bundle-resolution.cjs +51 -23
  50. package/dist-lib/expo-swift-bundle.cjs +1343 -801
  51. package/dist-lib/expo-swift-metadata.cjs +5 -4
  52. package/dist-lib/expo-swift-metadata.mjs +5 -4
  53. package/dist-lib/home-paths.cjs +1 -1
  54. package/dist-lib/host/bridge-host.cjs +2468 -1341
  55. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  56. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  57. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  58. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  59. package/dist-lib/host/websocket-proxy.cjs +1 -1
  60. package/dist-lib/index.cjs +1262 -300
  61. package/dist-lib/jump-to-source-babel.cjs +1 -1
  62. package/dist-lib/jump-to-source-native.cjs +1 -1
  63. package/dist-lib/kotlin-host.cjs +1 -1
  64. package/dist-lib/menu.cjs +3 -2
  65. package/dist-lib/menu.mjs +3 -2
  66. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  67. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  68. package/dist-lib/metro-production-bundle.cjs +5 -4
  69. package/dist-lib/metro-production-bundle.mjs +5 -4
  70. package/dist-lib/metro.cjs +799 -398
  71. package/dist-lib/native-app-identity-project.cjs +728 -22
  72. package/dist-lib/native-app-identity-project.mjs +727 -22
  73. package/dist-lib/native-app-identity.cjs +224 -9
  74. package/dist-lib/native-app-identity.mjs +222 -8
  75. package/dist-lib/native-firebase-metadata.cjs +5 -4
  76. package/dist-lib/native-firebase-metadata.mjs +5 -4
  77. package/dist-lib/plist.cjs +1 -1
  78. package/dist-lib/plist.mjs +1 -1
  79. package/dist-lib/profiles.cjs +1 -1
  80. package/dist-lib/public-brand.cjs +3 -2
  81. package/dist-lib/react-native-host-modules.cjs +4 -1
  82. package/dist-lib/react-native-host-modules.mjs +4 -1
  83. package/dist-lib/scripts/dev-server-scanner.cjs +94 -28
  84. package/dist-lib/sdk.cjs +89 -24
  85. package/dist-lib/sdk.mjs +89 -24
  86. package/dist-lib/skills.cjs +4391 -2350
  87. package/dist-lib/swift-compile.cjs +328 -142
  88. package/dist-lib/swift-compile.mjs +342 -147
  89. package/dist-lib/swift-package.cjs +1 -1
  90. package/dist-lib/swift-package.mjs +1 -1
  91. package/dist-lib/swift-submit.cjs +1 -1
  92. package/dist-lib/swift-submit.mjs +1 -1
  93. package/dist-lib/swift-toolchain.cjs +1 -1
  94. package/dist-lib/swift.cjs +1 -1
  95. package/dist-lib/vite-swift.cjs +888 -559
  96. package/dist-lib/vite.cjs +68 -14
  97. package/dist-lib/worklet-executable.cjs +25 -2
  98. package/dist-lib/worklet-executable.mjs +25 -2
  99. package/package.json +2 -2
  100. package/scripts/dev-server-scanner.ts +33 -13
  101. package/src/admitted-mac-build.ts +34 -8
  102. package/src/android-app-identity.ts +146 -0
  103. package/src/android-autolinking.ts +75 -0
  104. package/src/android-string-value.ts +36 -0
  105. package/src/android-strings.ts +5 -36
  106. package/src/androidAutolinkingContract.ts +40 -0
  107. package/src/bundleGlobalFooter.ts +26 -7
  108. package/src/codex-client.ts +3 -2
  109. package/src/dev-bundle-resolution.ts +58 -25
  110. package/src/expo-kotlin-host.ts +6 -36
  111. package/src/expo-swift-bundle.ts +17 -12
  112. package/src/expo-swift-host.ts +477 -173
  113. package/src/expoSwiftContract.ts +6 -0
  114. package/src/host/bridge-host.ts +7 -55
  115. package/src/host/staged-host-files.ts +29 -2
  116. package/src/installed-package-versions.ts +54 -14
  117. package/src/launchMetadataContract.ts +24 -0
  118. package/src/metro-plugin.ts +37 -42
  119. package/src/native-app-identity-project.ts +116 -3
  120. package/src/native-app-identity.ts +86 -11
  121. package/src/nitro-compile.ts +2 -5
  122. package/src/nitro-source.ts +147 -63
  123. package/src/process-group.ts +90 -60
  124. package/src/process-output.ts +0 -9
  125. package/src/public-brand.ts +1 -0
  126. package/src/react-native-host-modules.ts +3 -0
  127. package/src/sim-client.ts +6 -4
  128. package/src/swift-compile.ts +176 -28
  129. package/src/swift-distribution.lock.json +37 -11
  130. package/src/swift-distribution.ts +61 -9
  131. package/src/swift-host.ts +58 -27
  132. package/src/swift-objc-standin.ts +13 -0
  133. package/src/swift-standin-emit.ts +1 -1
  134. package/src/swift-standins.ts +17 -5
  135. package/src/vite-plugin.ts +8 -19
  136. package/src/worklet-executable.ts +30 -3
  137. package/src/worklets-babel.ts +35 -14
  138. package/src/xcode-project.ts +10 -0
@@ -1,11 +1,34 @@
1
1
  {
2
- "id": "64fd4ea71bc39e4f",
2
+ "id": "1fda1fb984f25758",
3
3
  "release": "swift-6.4.0-RELEASE",
4
- "wasmSdkId": "swift-6.4.0-RELEASE_wasm-peach-dc9c8c67",
5
- "wasmSdkUrl": "https://r2.sootsim.com/swift-wasm-sdk/swift-6.4.0-RELEASE_wasm-peach-dc9c8c67.artifactbundle.tar.gz",
6
- "wasmSdkChecksum": "5d38f795f516ccfef3d7ac869b447cdedcd82a35af9761a65b9f88924eb6a89a",
7
- "source": "77fbca997182e53ceb1944e27d8689f469f632c5",
4
+ "wasmSdkId": "swift-6.4.0-RELEASE_wasm-peach-34b989c2",
5
+ "wasmSdkUrl": "https://r2.sootsim.com/swift-wasm-sdk/swift-6.4.0-RELEASE_wasm-peach-34b989c2.artifactbundle.tar.gz",
6
+ "wasmSdkChecksum": "1598ac9d063e07b20aa6232acb03c7147c9a94abf989d51f1e55688eb4aba854",
7
+ "source": "078b4f63d3734871dc1fbea898005e2ef95a73cb",
8
8
  "targets": [
9
+ "System",
10
+ "_LocationEssentials",
11
+ "GeoToolbox",
12
+ "DataDetection",
13
+ "Accessibility",
14
+ "Contacts",
15
+ "CoreML",
16
+ "EventKit",
17
+ "CloudKit",
18
+ "IOSurface",
19
+ "OpenGLES",
20
+ "ContactsUI",
21
+ "CoreLocationUI",
22
+ "MediaPlayer",
23
+ "MusicKit",
24
+ "RealityKit",
25
+ "SecureElementCredential",
26
+ "Social",
27
+ "Speech",
28
+ "Vision",
29
+ "VisionKit",
30
+ "WorkoutKit",
31
+ "_PassKit_SwiftUI",
9
32
  "ExpoModulesCore",
10
33
  "_MapKit_SwiftUI",
11
34
  "PeachJSON",
@@ -21,6 +44,7 @@
21
44
  "PeachObjC",
22
45
  "UIKitCore",
23
46
  "PeachICU",
47
+ "PeachCMark",
24
48
  "CoreImage",
25
49
  "QuartzCore",
26
50
  "CoreGraphics",
@@ -51,6 +75,7 @@
51
75
  "ActivityKit",
52
76
  "SwiftData",
53
77
  "UIKit",
78
+ "React",
54
79
  "Combine",
55
80
  "PeachFoundationCombine",
56
81
  "AppIntents",
@@ -132,6 +157,7 @@
132
157
  "systemVersion": ["string"],
133
158
  "colorScheme": ["string:light", "string:dark"],
134
159
  "semanticColors": ["object"],
160
+ "imageAssets": ["object"],
135
161
  "locale": ["string"],
136
162
  "dynamicTypeSize": [
137
163
  "string:xSmall",
@@ -153,16 +179,16 @@
153
179
  },
154
180
  "hosts": {
155
181
  "darwin-arm64": {
156
- "sha256": "6b60bae2bc91957af3baee5e9cad44200f15ff3840d8e70afa5ddc63413d880f",
157
- "size": 58565898
182
+ "sha256": "6a3b956b13bcc76dbf0b55533088251e2ae29badcd5c8d66394a2459e0942a32",
183
+ "size": 63180761
158
184
  },
159
185
  "linux-x64": {
160
- "sha256": "d97e287ca5d7ea0d00dfc9db60be746aec35a9b7412299461ce0d5e77f38b7a8",
161
- "size": 58569694
186
+ "sha256": "37f26adff0a90c0647065b3c2e70bfdae05c84832b27add00915d441f2f79359",
187
+ "size": 63183751
162
188
  },
163
189
  "linux-arm64": {
164
- "sha256": "495fe6adc7cf1c8ec9fc8de3410c976e65a7ff051ca50ee3f6d0d3fee8aa2c5d",
165
- "size": 58567328
190
+ "sha256": "e329424d19ede87f4c9257de40f88220cdaff42bd82f1ab47616394ec7f32f35",
191
+ "size": 63181548
166
192
  }
167
193
  }
168
194
  }
@@ -27,13 +27,23 @@ export interface SwiftModule {
27
27
  toolchainBin: string
28
28
  }
29
29
 
30
- const installs = new Map<string, Promise<string>>()
30
+ const installs = new Map<
31
+ string,
32
+ {
33
+ promise: Promise<string>
34
+ controller: AbortController
35
+ clients: number
36
+ }
37
+ >()
31
38
 
32
39
  /** the installed distribution's root, downloading it on first use, or null
33
40
  * when PEACH_SWIFT_ENDPOINT is unset. */
34
- export function swiftDistributionRoot(): Promise<string | null> {
41
+ export async function swiftDistributionRoot(
42
+ signal?: AbortSignal,
43
+ ): Promise<string | null> {
44
+ signal?.throwIfAborted()
35
45
  const endpoint = process.env[SWIFT_ENDPOINT_ENV]?.trim().replace(/\/+$/, '')
36
- if (!endpoint) return Promise.resolve(null)
46
+ if (!endpoint) return null
37
47
  const lock = parseSwiftDistributionLock(lockJson)
38
48
  const host = swiftDistributionHost(process.platform, process.arch)
39
49
  if (!host) {
@@ -46,13 +56,16 @@ export function swiftDistributionRoot(): Promise<string | null> {
46
56
  swiftToolchain(lock.release, lock)
47
57
  const dir = path.join(rnxHomeDir(), 'swift', `${lock.id}-${host}`)
48
58
  let pending = installs.get(dir)
59
+ if (pending?.controller.signal.aborted) pending = undefined
49
60
  if (!pending) {
50
- pending = (async () => {
61
+ const controller = new AbortController()
62
+ const promise = (async () => {
51
63
  if (!fs.existsSync(path.join(dir, SWIFT_DISTRIBUTION_MANIFEST))) {
52
64
  process.stderr.write(
53
65
  `peach: downloading Swift distribution ${lock.id} (${host})…\n`,
54
66
  )
55
67
  await installVerifiedTarball({
68
+ signal: controller.signal,
56
69
  url: `${endpoint}/${swiftDistributionKey(lock.id, host)}`,
57
70
  digest: {
58
71
  algorithm: 'sha256',
@@ -69,11 +82,48 @@ export function swiftDistributionRoot(): Promise<string | null> {
69
82
  throw new Error(`${dir} does not hold Swift distribution ${lock.id} (${host})`)
70
83
  }
71
84
  return dir
72
- })()
73
- pending.catch(() => installs.delete(dir))
85
+ })().finally(() => {
86
+ if (installs.get(dir)?.controller === controller) installs.delete(dir)
87
+ })
88
+ pending = { promise, controller, clients: 0 }
74
89
  installs.set(dir, pending)
75
90
  }
76
- return pending
91
+ const current = pending
92
+ current.clients++
93
+ let released = false
94
+ const release = () => {
95
+ if (released) return
96
+ released = true
97
+ if (--current.clients === 0) current.controller.abort(signal?.reason)
98
+ }
99
+ let abort: (() => void) | undefined
100
+ try {
101
+ const root = await new Promise<string>((resolve, reject) => {
102
+ abort = () => {
103
+ release()
104
+ const reason: unknown = signal?.reason
105
+ const error =
106
+ reason instanceof Error
107
+ ? reason
108
+ : new Error('Swift distribution installation canceled', { cause: reason })
109
+ // other callers retain ownership; the last caller waits for cleanup.
110
+ if (current.clients > 0) reject(error)
111
+ else
112
+ current.promise.then(
113
+ () => reject(error),
114
+ () => reject(error),
115
+ )
116
+ }
117
+ signal?.addEventListener('abort', abort, { once: true })
118
+ current.promise.then(resolve, reject)
119
+ if (signal?.aborted) abort()
120
+ })
121
+ signal?.throwIfAborted()
122
+ return root
123
+ } finally {
124
+ if (abort) signal?.removeEventListener('abort', abort)
125
+ release()
126
+ }
77
127
  }
78
128
 
79
129
  /** the module this process compiles app Swift against: the checkout beside a
@@ -81,7 +131,9 @@ export function swiftDistributionRoot(): Promise<string | null> {
81
131
  * nothing without it, so a checkout's toolchain error surfaces to them. */
82
132
  export async function resolveSwiftModule({
83
133
  required = false,
84
- }: { required?: boolean } = {}): Promise<SwiftModule | null> {
134
+ signal,
135
+ }: { required?: boolean; signal?: AbortSignal } = {}): Promise<SwiftModule | null> {
136
+ signal?.throwIfAborted()
85
137
  const checkout = path.resolve(import.meta.dirname, '../../sootsim-swift')
86
138
  if (fs.existsSync(path.join(checkout, 'toolchain.json'))) {
87
139
  // a checkout is implicit: without its toolchain, apps run without their
@@ -93,7 +145,7 @@ export async function resolveSwiftModule({
93
145
  return null
94
146
  }
95
147
  }
96
- const root = await swiftDistributionRoot()
148
+ const root = await swiftDistributionRoot(signal)
97
149
  return root ? { root, ...swiftModuleToolchain(root) } : null
98
150
  }
99
151
 
package/src/swift-host.ts CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  standinGenerationInputs,
52
52
  standinLibrary,
53
53
  swiftStandins,
54
+ SWIFT_STANDINS_DIR,
54
55
  type SwiftStandin,
55
56
  } from './swift-standins'
56
57
  import {
@@ -172,13 +173,18 @@ export interface SwiftProject {
172
173
  packagePath: string
173
174
  artifact: SwiftArtifact | null
174
175
  // the build in flight and the sources it was started from
175
- building: { stamp: string; promise: Promise<SwiftArtifact> } | null
176
+ building: {
177
+ stamp: string
178
+ promise: Promise<SwiftArtifact>
179
+ signal?: AbortSignal
180
+ } | null
176
181
  // two bounded generations of immutable bytes cover in-flight asset reads.
177
182
  resourceFiles: Map<string, { bytes: Buffer; contentType: string }>
178
183
  resourcesPrevious: Map<string, { bytes: Buffer; contentType: string }>
179
184
  artifacts: SwiftArtifact[]
180
185
  hosted?: SwiftHostedCompileOptions
181
186
  signal?: AbortSignal
187
+ libraries?: Record<string, string>
182
188
  }
183
189
 
184
190
  // a project is its swiftpm package: the build, the artifact, and the dev
@@ -402,9 +408,6 @@ const XCODE_FRAMEWORKS_DIR = '__frameworks__'
402
408
  // remote package checkouts compile here: swiftpm owns `.build` in the
403
409
  // compile's package root as its scratch directory.
404
410
  const XCODE_PACKAGES_DIR = '__packages__'
405
- // each binary target's stand-in compiles as a target under its module's
406
- // directory here, from sources generated on this host.
407
- const XCODE_STANDINS_DIR = '__standins__'
408
411
  const CHECKOUTS_PREFIX = `${REMOTE_CHECKOUTS.split(path.sep).join(path.posix.sep)}/`
409
412
 
410
413
  // the compile input key of a root-relative path in an xcode layout.
@@ -485,7 +488,7 @@ function xcodeLayout(root: string): XcodeLayout | null {
485
488
  for (const standin of standins)
486
489
  targets.push({
487
490
  name: standin.module,
488
- path: path.posix.join(XCODE_STANDINS_DIR, standin.module),
491
+ path: path.posix.join(SWIFT_STANDINS_DIR, standin.module),
489
492
  executable: false,
490
493
  resources: false,
491
494
  exclude: [],
@@ -1033,6 +1036,8 @@ export function sourceStamp(
1033
1036
  const hash = createHash('sha256')
1034
1037
  hash.update(packagePath)
1035
1038
  hash.update(JSON.stringify(xcodeProjects.get(packagePath)?.behavior ?? {}))
1039
+ const libraries = projects.get(packagePath)?.libraries
1040
+ if (libraries) hash.update(JSON.stringify(libraries))
1036
1041
  const projectInputs = new Set(packageFiles(packagePath))
1037
1042
  for (const file of [...projectInputs].sort()) {
1038
1043
  hash.update(path.relative(packagePath, file))
@@ -1092,15 +1097,19 @@ export function icuUrl(origin: string, artifact: SwiftArtifact): string {
1092
1097
  // keyed by the stand-in target it links into; its own source then defines
1093
1098
  // nothing, so the library's functions run instead of the traps. one library
1094
1099
  // links once, through the first module its framework ships.
1095
- function standinLibraries(layout: XcodeLayout): Map<string, SwiftStandin[]> {
1100
+ export function standinLibraries(
1101
+ binaries: readonly SwiftPackageBinary[],
1102
+ standins: readonly SwiftStandin[],
1103
+ rootDir: string,
1104
+ ): Map<string, SwiftStandin[]> {
1096
1105
  const linked = new Map<string, SwiftStandin[]>()
1097
- for (const binary of layout.binaries) {
1098
- const modules = layout.standins.filter(
1106
+ for (const binary of binaries) {
1107
+ const modules = standins.filter(
1099
1108
  (standin) =>
1100
1109
  standin.target === binary.target && `${standin.module}.c` in standin.files,
1101
1110
  )
1102
1111
  if (modules.length === 0) continue
1103
- const library = standinLibrary(binary, layout.app.rootDir)
1112
+ const library = standinLibrary(binary, rootDir)
1104
1113
  if (!library) continue
1105
1114
  linked.set(library, modules)
1106
1115
  }
@@ -1131,7 +1140,7 @@ function packageSources(
1131
1140
  files['Package.swift'] = xcodeManifest(xcode)
1132
1141
  for (const standin of xcode.standins)
1133
1142
  for (const [file, text] of Object.entries(standin.files))
1134
- files[path.posix.join(XCODE_STANDINS_DIR, standin.module, file)] =
1143
+ files[path.posix.join(SWIFT_STANDINS_DIR, standin.module, file)] =
1135
1144
  linked.has(standin) && file === `${standin.module}.c`
1136
1145
  ? `// ${standin.target}'s WebAssembly library defines ${standin.module}'s functions.\n`
1137
1146
  : text
@@ -1152,7 +1161,9 @@ interface SwiftPackageSnapshot {
1152
1161
 
1153
1162
  function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnapshot {
1154
1163
  const layout = xcodeLayout(project.packagePath)
1155
- const linked = layout ? standinLibraries(layout) : new Map<string, SwiftStandin[]>()
1164
+ const linked = layout
1165
+ ? standinLibraries(layout.binaries, layout.standins, layout.app.rootDir)
1166
+ : new Map<string, SwiftStandin[]>()
1156
1167
  const files = packageSources(project.packagePath, new Set([...linked.values()].flat()))
1157
1168
  const native = nativeSourceDescriptor(project.packagePath)
1158
1169
  return {
@@ -1178,14 +1189,17 @@ function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnap
1178
1189
  files,
1179
1190
  ),
1180
1191
  native,
1181
- ...(linked.size > 0
1192
+ ...(linked.size > 0 || project.libraries
1182
1193
  ? {
1183
- libraries: Object.fromEntries(
1184
- [...linked].map(([library, [first]]) => [
1185
- first!.module,
1186
- fs.readFileSync(library).toString('base64'),
1187
- ]),
1188
- ),
1194
+ libraries: {
1195
+ ...project.libraries,
1196
+ ...Object.fromEntries(
1197
+ [...linked].map(([library, [first]]) => [
1198
+ first!.module,
1199
+ fs.readFileSync(library).toString('base64'),
1200
+ ]),
1201
+ ),
1202
+ },
1189
1203
  }
1190
1204
  : {}),
1191
1205
  ...(native
@@ -1252,7 +1266,7 @@ async function compile(
1252
1266
  linkedModuleRoots(packagePath).find((root) =>
1253
1267
  fs.existsSync(path.join(root, 'toolchain.json')),
1254
1268
  ) ??
1255
- (await swiftDistributionRoot())
1269
+ (await swiftDistributionRoot(signal))
1256
1270
  if (!moduleRoot)
1257
1271
  return {
1258
1272
  ...(await compileRemote(packagePath, snapshot, undefined, signal)),
@@ -1559,8 +1573,11 @@ export function artifactFor(
1559
1573
  project: SwiftProject,
1560
1574
  entry: string,
1561
1575
  moduleRoot?: string,
1576
+ signal: AbortSignal | undefined = project.signal,
1562
1577
  ): Promise<SwiftArtifact> {
1563
- project.signal?.throwIfAborted()
1578
+ if (project.signal && signal !== project.signal)
1579
+ signal = signal ? AbortSignal.any([project.signal, signal]) : project.signal
1580
+ signal?.throwIfAborted()
1564
1581
  const stamp = sourceStamp(project.packagePath, moduleRoot, Boolean(project.hosted))
1565
1582
  if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact)
1566
1583
  if (project.building) {
@@ -1569,13 +1586,27 @@ export function artifactFor(
1569
1586
  // and ask again, which starts the build these sources need; joining it
1570
1587
  // would emit the older artifact and nothing would trigger another load.
1571
1588
  const running = project.building
1572
- if (running.stamp === stamp) return running.promise
1573
- const again = () => artifactFor(project, entry, moduleRoot)
1574
- return running.promise.then(again, again)
1589
+ if (running.stamp === stamp && running.signal === signal) return running.promise
1590
+ const again = () => artifactFor(project, entry, moduleRoot, signal)
1591
+ const queued = running.promise.then(again, again)
1592
+ if (!signal) return queued
1593
+ return new Promise<SwiftArtifact>((resolve, reject) => {
1594
+ const stop = () =>
1595
+ reject(
1596
+ signal.reason instanceof Error
1597
+ ? signal.reason
1598
+ : new Error('Swift compilation canceled', { cause: signal.reason }),
1599
+ )
1600
+ signal.addEventListener('abort', stop, { once: true })
1601
+ if (signal.aborted) stop()
1602
+ queued
1603
+ .then(resolve, reject)
1604
+ .finally(() => signal.removeEventListener('abort', stop))
1605
+ })
1575
1606
  }
1576
1607
  const snapshot = packageSnapshot(project, entry)
1577
1608
  if (sourceStamp(project.packagePath, moduleRoot, Boolean(project.hosted)) !== stamp) {
1578
- return artifactFor(project, entry, moduleRoot)
1609
+ return artifactFor(project, entry, moduleRoot, signal)
1579
1610
  }
1580
1611
  // a save that changed only view builder literals keeps the running bytes
1581
1612
  const compiled = project.artifact?.compiled
@@ -1600,7 +1631,7 @@ export function artifactFor(
1600
1631
  moduleRoot,
1601
1632
  project.hosted,
1602
1633
  snapshot,
1603
- project.signal,
1634
+ signal,
1604
1635
  ).then(
1605
1636
  (artifact) => {
1606
1637
  project.artifact = artifact
@@ -1620,9 +1651,9 @@ export function artifactFor(
1620
1651
  sourceStamp(project.packagePath, moduleRoot, Boolean(project.hosted)) ===
1621
1652
  artifact.stamp
1622
1653
  ? artifact
1623
- : artifactFor(project, entry, moduleRoot),
1654
+ : artifactFor(project, entry, moduleRoot, signal),
1624
1655
  )
1625
- project.building = { stamp, promise: fresh }
1656
+ project.building = { stamp, promise: fresh, signal }
1626
1657
  return fresh
1627
1658
  }
1628
1659
 
@@ -174,6 +174,19 @@ export function objcView(
174
174
  const slash = include.indexOf('/')
175
175
  if (slash > 0 && /^[A-Z]/.test(include)) {
176
176
  const name = include.slice(0, slash)
177
+ const umbrella = `${name}/${name}.h`
178
+ if (
179
+ Object.hasOwn(
180
+ IOS_SDK_HEADERS,
181
+ `System/Library/Frameworks/${name}.framework/Headers/${name}.h`,
182
+ )
183
+ ) {
184
+ // retain the sdk module's owner when a vendor imports a leaf header.
185
+ const header = path.join(stubIncludes, include)
186
+ fs.mkdirSync(path.dirname(header), { recursive: true })
187
+ fs.writeFileSync(header, `#import <${umbrella}>\n`)
188
+ return true
189
+ }
177
190
  const root = path.join(stubs, `${name}.framework`)
178
191
  const header = path.join(root, 'Headers', include.slice(slash + 1))
179
192
  fs.mkdirSync(path.dirname(header), { recursive: true })
@@ -87,7 +87,7 @@ export type StandinResult = {
87
87
  }
88
88
 
89
89
  /** bump when emitted source changes: the stand-in cache keys on it. */
90
- export const STANDIN_EMIT_VERSION = '6'
90
+ export const STANDIN_EMIT_VERSION = '7'
91
91
 
92
92
  /** the modules every stand-in source imports. */
93
93
  export const STANDIN_IMPORTS = ['Foundation', 'UIKit']
@@ -38,6 +38,8 @@ import type { SwiftPackageBinary } from './swift-packages'
38
38
  const TRIPLE = 'wasm32-unknown-wasip1'
39
39
  // builder workspaces share this persistent cache; temporary directories hold only work.
40
40
  const cacheDir = path.join(os.homedir(), 'builders/cache/peach-swift-standins')
41
+ // each binary stand-in is a target under its module's directory here.
42
+ export const SWIFT_STANDINS_DIR = '__standins__'
41
43
 
42
44
  export interface SwiftStandin {
43
45
  /** the module it stands in for, which names its compile target. */
@@ -993,7 +995,7 @@ function sourceBinaryStandins(
993
995
  release: toolchain.release,
994
996
  }),
995
997
  )
996
- execFileSync(
998
+ const build = spawnSync(
997
999
  'bun',
998
1000
  [
999
1001
  path.join(repo, 'scripts/build/swift-standins-submit.ts'),
@@ -1004,11 +1006,20 @@ function sourceBinaryStandins(
1004
1006
  '--delivery-dir',
1005
1007
  delivery,
1006
1008
  ],
1007
- { cwd: repo, stdio: 'inherit' },
1008
- )
1009
- const receipt: unknown = JSON.parse(
1010
- fs.readFileSync(path.join(delivery, 'standins.json'), 'utf8'),
1009
+ { cwd: repo, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 },
1011
1010
  )
1011
+ if (build.stdout) process.stdout.write(build.stdout)
1012
+ if (build.stderr) process.stderr.write(build.stderr)
1013
+ if (build.error || build.status !== 0)
1014
+ throw new Error(
1015
+ `source stand-in build for ${binary.target} failed (${build.error?.message ?? build.signal ?? `exit ${build.status}`}): ${build.stderr?.trim().slice(-4000) || build.stdout?.trim().slice(-4000) || 'builder returned no diagnostic'}`,
1016
+ )
1017
+ const output = path.join(delivery, 'standins.json')
1018
+ if (!fs.existsSync(output))
1019
+ throw new Error(
1020
+ `source stand-in builder returned no standins.json for ${binary.target}`,
1021
+ )
1022
+ const receipt: unknown = JSON.parse(fs.readFileSync(output, 'utf8'))
1012
1023
  if (
1013
1024
  !receipt ||
1014
1025
  typeof receipt !== 'object' ||
@@ -1174,6 +1185,7 @@ function binaryStandins(
1174
1185
  try {
1175
1186
  result =
1176
1187
  process.platform === 'darwin' &&
1188
+ process.env.TM_EXEC_BUILD_PLATFORM !== 'mac' &&
1177
1189
  process.env.TM_SESSION &&
1178
1190
  process.env.CI !== 'true' &&
1179
1191
  toolchain.moduleRoot === path.resolve(import.meta.dirname, '../../sootsim-swift') &&
@@ -17,6 +17,7 @@ import { fileURLToPath, pathToFileURL } from 'url'
17
17
  import { parseSync } from 'oxc-parser'
18
18
  import { minifySync, transformSync } from 'rolldown/utils'
19
19
  import { type Plugin, transformWithOxc } from 'vite'
20
+ import { resolveOneNativeBackend } from '../../contrast-bundler/src/oneFramework.ts'
20
21
  import { transformFlowSource } from '../../peach-compat/src/flow-strip.ts'
21
22
  import { renderWorkerSystemChunk } from './worker-system-chunks.ts'
22
23
 
@@ -34,7 +35,7 @@ import {
34
35
  import { resolveExportsHiddenSubpath } from '../../peach-compat/src/resolve-hidden-subpath.ts'
35
36
  import {
36
37
  SOOTSIM_COMPAT_PUBLISHED_BROWSER_ENTRIES,
37
- SOOTSIM_COMPAT_WRAPPER_REAL_PACKAGES,
38
+ PEACH_UPSTREAM_MODULES,
38
39
  SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SOURCE,
39
40
  SOOTSIM_HAPTIC_FEEDBACK_TOUCHABLE_SPECIFIER,
40
41
  compatStubsForBuildResolver,
@@ -291,9 +292,7 @@ const compatWrapperRealPackageAliases: Array<{
291
292
  find: string
292
293
  replacement: string
293
294
  }> = []
294
- for (const { specifier, packageName } of Object.values(
295
- SOOTSIM_COMPAT_WRAPPER_REAL_PACKAGES,
296
- )) {
295
+ for (const { specifier, packageName } of Object.values(PEACH_UPSTREAM_MODULES)) {
297
296
  let replacement = resolveExportsHiddenSubpath(packageName, (specifier) =>
298
297
  sootsimPluginRequire.resolve(specifier),
299
298
  )
@@ -366,7 +365,6 @@ export function sootsim(options: SootSimOptions = {}): Plugin[] {
366
365
  }
367
366
 
368
367
  return [
369
- wsBridgePlugin(),
370
368
  reactBridgePlugin(),
371
369
  {
372
370
  name: 'sootsim-published-browser-internals',
@@ -1319,9 +1317,9 @@ declare global {
1319
1317
  // across the overlap so every connected sim stays on the same websocket.
1320
1318
  const viteBridgeProcesses = (globalThis.__sootsimViteBridgeProcesses ??= new Map())
1321
1319
 
1322
- // start WS bridge server for debug CLI connectivity. exported so shell
1323
- // vite can install it standalone (engine builds via `vite build --watch`,
1324
- // which doesn't fire configureServer — the bridge has to run on shell).
1320
+ // start WS bridge server for debug CLI connectivity. the shell vite server
1321
+ // installs it explicitly and owns the one shared bridge; sootsim() never
1322
+ // does, so an isolated dev server cannot bind the shared default port.
1325
1323
  export function wsBridgePlugin(options: { ownedBridgePort?: number } = {}): Plugin {
1326
1324
  if (
1327
1325
  options.ownedBridgePort !== undefined &&
@@ -1726,17 +1724,6 @@ function sootsimConfigPlugin(
1726
1724
  'react-native/src/private/featureflags/ReactNativeFeatureFlags.js',
1727
1725
  ),
1728
1726
  },
1729
- ...[
1730
- 'BatchedBridge/BatchedBridge',
1731
- 'Components/View/ReactNativeStyleAttributes',
1732
- 'StyleSheet/processColor',
1733
- ].map((module) => ({
1734
- find: `@peach-internal/react-native/${module}`,
1735
- replacement: path.resolve(
1736
- workspaceNodeModules,
1737
- `react-native/Libraries/${module}.js`,
1738
- ),
1739
- })),
1740
1727
  ...Object.entries(rnLibraryStubs).map(([find, replacement]) => ({
1741
1728
  find: new RegExp(
1742
1729
  `^${find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\.[cm]?[jt]sx?)?$`,
@@ -2227,6 +2214,8 @@ function metroNativeResolve(isAppSource: (p: string) => boolean): Plugin {
2227
2214
  filter: { id: /^\./ },
2228
2215
  handler(source, importer) {
2229
2216
  if (!importer) return null
2217
+ const oneBackend = resolveOneNativeBackend(source, importer)
2218
+ if (oneBackend) return oneBackend
2230
2219
  if (!importer.includes('node_modules') && !isAppSource(importer)) return null
2231
2220
  if (platformExts.some((ext) => source.endsWith(ext))) return null
2232
2221
 
@@ -92,7 +92,7 @@ interface Scope {
92
92
 
93
93
  type Binding =
94
94
  | { kind: 'value'; init: SyntaxNode; scope: Scope }
95
- // `key` is set when the parameter is a destructured property of that argument.
95
+ // `key` is set when the parameter is a destructured member of that argument.
96
96
  | { kind: 'param'; fn: SyntaxNode; index: number; key?: string }
97
97
  | { kind: 'opaque' }
98
98
 
@@ -147,7 +147,7 @@ function lookup(scope: Scope, name: string): Binding | undefined {
147
147
  return undefined
148
148
  }
149
149
 
150
- // a destructured parameter binds each plain property to that key of its argument.
150
+ // a destructured parameter binds each plain member to that key of its argument.
151
151
  // rest elements, defaults and nested patterns stay opaque.
152
152
  function declareParam(inner: Scope, fn: SyntaxNode, index: number, param: unknown): void {
153
153
  const name = identifierName(param)
@@ -155,6 +155,17 @@ function declareParam(inner: Scope, fn: SyntaxNode, index: number, param: unknow
155
155
  declare(inner, name, { kind: 'param', fn, index })
156
156
  return
157
157
  }
158
+ if (isNode(param) && param.type === 'ArrayPattern') {
159
+ const elements: unknown = Reflect.get(param, 'elements')
160
+ if (!Array.isArray(elements)) return
161
+ elements.forEach((element: unknown, position) => {
162
+ const name = identifierName(element)
163
+ if (name !== undefined)
164
+ declare(inner, name, { kind: 'param', fn, index, key: String(position) })
165
+ else declareOpaque(inner, patternNames(element))
166
+ })
167
+ return
168
+ }
158
169
  if (!isNode(param) || param.type !== 'ObjectPattern') {
159
170
  declareOpaque(inner, patternNames(param))
160
171
  return
@@ -334,6 +345,21 @@ function immediateArgument(
334
345
  // the value a literal holds under a static name. the last duplicate wins, as it
335
346
  // does at run time. a spread makes the literal's keys unknown.
336
347
  function propertyValue(object: SyntaxNode, name: string): SyntaxNode | undefined {
348
+ if (object.type === 'ArrayExpression') {
349
+ const index = Number(name)
350
+ const elements: unknown = Reflect.get(object, 'elements')
351
+ if (!Number.isSafeInteger(index) || index < 0 || String(index) !== name)
352
+ return undefined
353
+ if (!Array.isArray(elements)) return undefined
354
+ if (
355
+ elements
356
+ .slice(0, index + 1)
357
+ .some((element: unknown) => isNode(element) && element.type === 'SpreadElement')
358
+ )
359
+ return undefined
360
+ const value: unknown = elements[index]
361
+ return isNode(value) ? value : undefined
362
+ }
337
363
  let value: SyntaxNode | undefined
338
364
  for (const property of childNodes(Reflect.get(object, 'properties'))) {
339
365
  if (property.type === 'SpreadElement') return undefined
@@ -373,7 +399,8 @@ function resolveObject(
373
399
  depth: number,
374
400
  ): ResolvedObject | undefined {
375
401
  if (depth > MAX_RESOLUTION_DEPTH) return undefined
376
- if (expression.type === 'ObjectExpression') return { object: expression, scope }
402
+ if (expression.type === 'ObjectExpression' || expression.type === 'ArrayExpression')
403
+ return { object: expression, scope }
377
404
  if (expression.type === 'SequenceExpression') {
378
405
  const last = childNodes(Reflect.get(expression, 'expressions')).at(-1)
379
406
  return last && resolveObject(last, scope, collection, depth + 1)