peachsim 0.1.578 → 0.1.580

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +11 -9
  3. package/cli/app-catalog.ts +77 -0
  4. package/cli/app-fonts.ts +4 -20
  5. package/cli/app-splash.ts +2 -18
  6. package/cli/bridge-flow-runner.ts +505 -139
  7. package/cli/cloud-client.ts +1 -4
  8. package/cli/cloud-session.ts +1 -4
  9. package/cli/cloud-test-result-files.ts +3 -0
  10. package/cli/cloud-test-tunnel.ts +2 -5
  11. package/cli/command-registry.ts +2 -0
  12. package/cli/commands/agent.ts +149 -23
  13. package/cli/commands/assert.ts +1 -4
  14. package/cli/commands/box/peach-command.ts +62 -0
  15. package/cli/commands/box/plane-share.ts +0 -70
  16. package/cli/commands/box/rnx-command.ts +1 -60
  17. package/cli/commands/box/search-command.ts +14 -0
  18. package/cli/commands/box.ts +2 -2
  19. package/cli/commands/control.ts +121 -27
  20. package/cli/commands/cpu-profile.ts +23 -10
  21. package/cli/commands/daemon.ts +1 -9
  22. package/cli/commands/detox.ts +26 -18
  23. package/cli/commands/device.ts +129 -1
  24. package/cli/commands/film.ts +25 -16
  25. package/cli/commands/flow.ts +9 -2
  26. package/cli/commands/inspect/actions.ts +75 -1
  27. package/cli/commands/inspect/core.ts +81 -2
  28. package/cli/commands/inspect/describe.ts +2 -6
  29. package/cli/commands/inspect/env.ts +1 -1
  30. package/cli/commands/inspect/settling.ts +20 -3
  31. package/cli/commands/inspect/shared.ts +1 -4
  32. package/cli/commands/inspect/sleep.ts +1 -1
  33. package/cli/commands/inspect.ts +16 -49
  34. package/cli/commands/install-desktop.ts +8 -6
  35. package/cli/commands/login.ts +3 -5
  36. package/cli/commands/maestro-generate.ts +1 -4
  37. package/cli/commands/maestro.ts +38 -21
  38. package/cli/commands/no-bridge-hint.ts +0 -14
  39. package/cli/commands/permissions.ts +2 -2
  40. package/cli/commands/platform.ts +13 -6
  41. package/cli/commands/screenshot.ts +6 -1
  42. package/cli/commands/skills.ts +1 -1
  43. package/cli/commands/test.ts +6 -2
  44. package/cli/commands/three-mode.ts +162 -19
  45. package/cli/commands/upload.ts +3 -18
  46. package/cli/current-sim.ts +0 -1
  47. package/cli/desktop-companion.ts +40 -8
  48. package/cli/drivers/playwright-provisioning.ts +1 -4
  49. package/cli/flow-export.ts +1 -8
  50. package/cli/flow-session.ts +0 -4
  51. package/cli/hints.ts +1 -16
  52. package/cli/inspect-notice-state.ts +0 -4
  53. package/cli/main.ts +2 -4
  54. package/cli/outbound-endpoints.ts +1 -24
  55. package/cli/parse-args.ts +1 -2
  56. package/cli/rnx-in-process.ts +3 -1
  57. package/cli/run-rnx-core.ts +4 -2
  58. package/cli/run-rnx.ts +18 -10
  59. package/cli/send-to-box.ts +1 -4
  60. package/cli/telemetry.ts +1 -1
  61. package/cli/ws-bridge.ts +1 -5
  62. package/detox/gestures.ts +20 -6
  63. package/detox/index.ts +379 -123
  64. package/detox/navigation-lifecycle.ts +22 -13
  65. package/dist-lib/agent-daemon-client.cjs +21 -15
  66. package/dist-lib/agent-events.cjs +1 -1
  67. package/dist-lib/agent-identity.cjs +3 -17
  68. package/dist-lib/agent-sessions.cjs +5 -4
  69. package/dist-lib/attached-projects.cjs +1 -77
  70. package/dist-lib/auth/shared-session.cjs +1 -1
  71. package/dist-lib/backend-origin.cjs +11 -1
  72. package/dist-lib/beta.cjs +1 -1
  73. package/dist-lib/beta.mjs +1 -1
  74. package/dist-lib/bridge-constants.cjs +1 -1
  75. package/dist-lib/bridge-contract-input.cjs +7 -5
  76. package/dist-lib/bridge-contract-input.mjs +7 -5
  77. package/dist-lib/bridge-contract.cjs +1 -1
  78. package/dist-lib/bridge-contract.mjs +1 -1
  79. package/dist-lib/capture-contract.cjs +30 -7
  80. package/dist-lib/capture-contract.mjs +30 -7
  81. package/dist-lib/cli-constants.cjs +1 -1
  82. package/dist-lib/cloud-contract.cjs +1 -1
  83. package/dist-lib/cloud-contract.mjs +1 -1
  84. package/dist-lib/cloud-test-args.cjs +1 -1
  85. package/dist-lib/cloud-test-args.mjs +1 -1
  86. package/dist-lib/cloud.cjs +17 -11
  87. package/dist-lib/cloud.mjs +17 -11
  88. package/dist-lib/config.cjs +1 -1
  89. package/dist-lib/detox/index.cjs +321 -122
  90. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  91. package/dist-lib/home-paths.cjs +1 -1
  92. package/dist-lib/host/bridge-host.cjs +427 -212
  93. package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
  94. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  95. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  96. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  97. package/dist-lib/host/websocket-proxy.cjs +1 -1
  98. package/dist-lib/index.cjs +315 -58
  99. package/dist-lib/jump-to-source-babel.cjs +1 -1
  100. package/dist-lib/jump-to-source-native.cjs +1 -1
  101. package/dist-lib/menu.cjs +67 -31
  102. package/dist-lib/menu.mjs +64 -28
  103. package/dist-lib/metro-fingerprint-registry.cjs +8 -4
  104. package/dist-lib/metro-fingerprint-registry.mjs +6 -4
  105. package/dist-lib/metro-production-bundle.cjs +2 -2
  106. package/dist-lib/metro-production-bundle.mjs +2 -2
  107. package/dist-lib/metro.cjs +8 -4
  108. package/dist-lib/profiles.cjs +1 -1
  109. package/dist-lib/public-brand.cjs +7 -4
  110. package/dist-lib/react-native-host-modules.cjs +1 -1
  111. package/dist-lib/react-native-host-modules.mjs +1 -1
  112. package/dist-lib/render-mode.cjs +3 -13
  113. package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
  114. package/dist-lib/sdk.cjs +129 -10
  115. package/dist-lib/sdk.mjs +127 -10
  116. package/dist-lib/skills.cjs +1635 -1121
  117. package/dist-lib/swift-compile.cjs +277 -70
  118. package/dist-lib/swift-compile.mjs +275 -70
  119. package/dist-lib/swift-package.cjs +17 -5
  120. package/dist-lib/swift-package.mjs +15 -4
  121. package/dist-lib/swift-submit.cjs +32 -8
  122. package/dist-lib/swift-submit.mjs +29 -7
  123. package/dist-lib/swift.cjs +30912 -577
  124. package/dist-lib/vite-kotlin.cjs +356 -0
  125. package/dist-lib/vite-swift.cjs +1743 -0
  126. package/dist-lib/vite.cjs +208 -1039
  127. package/kotlin/README.md +18 -0
  128. package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
  129. package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
  130. package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
  131. package/kotlin/Sources/org/json/JSON.kt +244 -0
  132. package/kotlin/toolchain.json +19 -0
  133. package/package.json +14 -2
  134. package/scripts/dev-server-scanner.ts +0 -18
  135. package/scripts/optional-demo-registry.ts +0 -17
  136. package/skills/contrast/SKILL.md +2 -2
  137. package/skills/peach-setup/SKILL.md +16 -0
  138. package/src/agent-daemon-client.ts +36 -22
  139. package/src/agent-identity.ts +0 -15
  140. package/src/agent-prompt.ts +12 -0
  141. package/src/attached-projects.ts +0 -72
  142. package/src/backend-origin.ts +10 -0
  143. package/src/bridge-contract-input.ts +2 -5
  144. package/src/bridge-contract.ts +2 -0
  145. package/src/capture-contract.ts +53 -11
  146. package/src/cloud.ts +4 -9
  147. package/src/dev-server-open.ts +0 -99
  148. package/src/home-paths.ts +2 -3
  149. package/src/host/agent-host.ts +82 -36
  150. package/src/host/bridge-host.ts +5 -11
  151. package/src/host/fetch-proxy-handler.ts +19 -43
  152. package/src/is-record.ts +4 -0
  153. package/src/menu.ts +82 -37
  154. package/src/metro-fingerprint-registry.ts +1 -4
  155. package/src/metro-production-bundle.ts +1 -1
  156. package/src/plist.ts +1 -4
  157. package/src/public-brand.ts +5 -3
  158. package/src/render-mode.ts +0 -12
  159. package/src/runtime-assets.ts +9 -2
  160. package/src/screenshots/compose.ts +7 -1
  161. package/src/screenshots/frame-compose.ts +21 -10
  162. package/src/screenshots/schema.ts +1 -4
  163. package/src/sim-client.ts +2 -4
  164. package/src/skills/builtin/maestro-test.ts +1 -4
  165. package/src/sleep.ts +4 -0
  166. package/src/swift-compile.ts +209 -47
  167. package/src/swift-icu.ts +140 -0
  168. package/src/swift-package.ts +26 -2
  169. package/src/swift-submit.ts +84 -4
  170. package/src/swift.ts +2 -0
  171. package/src/vite-plugin-kotlin.ts +376 -0
  172. package/src/vite-plugin-one.ts +224 -36
  173. package/src/vite-plugin-swift.ts +382 -96
  174. package/src/vite-plugin.ts +121 -134
  175. package/src/vite.d.ts +10 -0
  176. package/cli/resolve-assets.ts +0 -63
  177. package/src/rolldown-plugin-swift.ts +0 -244
  178. package/src/team-machine-prompt.ts +0 -73
@@ -16,22 +16,119 @@ import fs from 'node:fs'
16
16
  import os from 'node:os'
17
17
  import path from 'node:path'
18
18
  import {
19
+ enumerateBundleColors,
20
+ enumerateBundleSymbols,
19
21
  enumerateBundleImages,
20
22
  enumerateBundleJson,
23
+ type SwiftImageSource,
21
24
  } from '../../sootsim-swift/src/bundleAssets'
22
25
  import { swiftEntryModuleEsm } from '../../sootsim-swift/src/entryModule'
23
- import { compileSwiftProjectUnit, swiftModuleInputFiles } from './swift-compile'
24
- import type { SwiftCompileInput } from './swift-compile'
25
- import type { SwiftImageUpload } from './swift-submit'
26
+ import { authHeaderValue, resolveCliAuth } from '../cli/auth'
27
+ import { resolveContrastBackendOrigin } from './backend-origin'
28
+ import {
29
+ compileSwiftProjectUnit,
30
+ withNativeSourceGlue,
31
+ swiftModuleInputFiles,
32
+ } from './swift-compile'
33
+ import {
34
+ compileSwiftProject,
35
+ httpSwiftCompileSubmit,
36
+ swiftNativeEntry,
37
+ type SwiftImageUpload,
38
+ type SwiftNativeModules,
39
+ } from './swift-submit'
40
+ import type { SwiftEntryModuleArgs } from '../../sootsim-swift/src/entryModule'
41
+ import type { SwiftCompileInput, SwiftIcuAsset } from './swift-compile'
42
+ import type { ServerResponse } from 'node:http'
43
+ import type { SfSymbolVector } from 'peach-engine/ios/sf-symbol-catalog'
44
+ import type { AppCatalogColor } from 'peach-engine/react-native/platform-colors'
26
45
  import type { Plugin, ViteDevServer } from 'vite'
27
46
 
28
47
  const ARTIFACT_PATH = '/__peach/swift/'
29
48
  const RESOURCE_PATH = '/__peach/swift-res/'
49
+ const ICU_PATH = '/__peach/swift-icu/'
30
50
  const VIRTUAL_PREFIX = '\0peach-swift:'
51
+ const SKIP_SOURCE_DIRS = new Set([
52
+ 'node_modules',
53
+ 'ios',
54
+ 'android',
55
+ 'dist',
56
+ 'types',
57
+ 'build',
58
+ ])
59
+ const NATIVE_DESCRIPTOR = '.one-native-source.json'
60
+ const NATIVE_GLUE = '.one-native-source.swift.txt'
61
+
62
+ // vxrn's `swiftPackageId`: the id One's device registry and typegen name a
63
+ // package by.
64
+ function swiftPackageId(packagePath: string): string {
65
+ return path
66
+ .basename(packagePath)
67
+ .replace(/[^A-Za-z0-9_]/g, '_')
68
+ .replace(/^(\d)/, '_$1')
69
+ }
70
+
71
+ function nativeSourceDescriptor(packagePath: string): SwiftNativeModules | null {
72
+ const file = path.join(packagePath, NATIVE_DESCRIPTOR)
73
+ if (!fs.existsSync(file)) return null
74
+ const value: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
75
+ const invalid = () =>
76
+ new Error(`${file}: invalid One native source descriptor; run one typegen`)
77
+ if (
78
+ !value ||
79
+ typeof value !== 'object' ||
80
+ Reflect.get(value, 'generatedBy') !== 'one typegen' ||
81
+ !fs.existsSync(path.join(packagePath, NATIVE_GLUE))
82
+ ) {
83
+ throw invalid()
84
+ }
85
+ const packageId = Reflect.get(value, 'packageId')
86
+ const hash = Reflect.get(value, 'hash')
87
+ const rawFiles: unknown = Reflect.get(value, 'files')
88
+ const rawModules: unknown = Reflect.get(value, 'modules')
89
+ const rawDefaults: unknown = Reflect.get(value, 'defaultViewFiles')
90
+ if (
91
+ typeof packageId !== 'string' ||
92
+ typeof hash !== 'string' ||
93
+ !rawFiles ||
94
+ typeof rawFiles !== 'object' ||
95
+ !rawModules ||
96
+ typeof rawModules !== 'object' ||
97
+ !Array.isArray(rawDefaults) ||
98
+ !rawDefaults.every((name) => typeof name === 'string')
99
+ ) {
100
+ throw invalid()
101
+ }
102
+ const files: Record<string, string[]> = {}
103
+ const modules: Record<string, string[]> = {}
104
+ for (const [key, names] of Object.entries(rawFiles)) {
105
+ if (!Array.isArray(names) || !names.every((name) => typeof name === 'string'))
106
+ throw invalid()
107
+ files[key] = names
108
+ }
109
+ for (const [key, names] of Object.entries(rawModules)) {
110
+ if (!Array.isArray(names) || !names.every((name) => typeof name === 'string'))
111
+ throw invalid()
112
+ modules[key] = names
113
+ }
114
+ if (packageId !== swiftPackageId(packagePath)) {
115
+ throw new Error(`${file}: invalid One native source descriptor; run one typegen`)
116
+ }
117
+ return { packageId, hash, files, modules, defaultViewFiles: rawDefaults }
118
+ }
119
+
120
+ function nativeSourceFiles(packagePath: string): string[] {
121
+ return [NATIVE_DESCRIPTOR, NATIVE_GLUE]
122
+ .map((name) => path.join(packagePath, name))
123
+ .filter((file) => fs.existsSync(file))
124
+ }
31
125
 
32
126
  interface SwiftArtifact {
33
127
  hash: string
34
128
  bytes: Buffer
129
+ // the icu common data the artifact reserves: built here and served from
130
+ // this server, or already published by the Contrast compile that built it
131
+ icu: SwiftIcuAsset | { url: string }
35
132
  // the sources this artifact was built from. a watcher event and a `load`
36
133
  // reach the project in either order, so freshness is read from the sources
37
134
  // rather than from whichever of them ran first.
@@ -74,14 +171,18 @@ function sourceFiles(root: string): string[] {
74
171
  const found: string[] = []
75
172
  const walk = (dir: string): void => {
76
173
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
77
- if (entry.name.startsWith('.') || entry.name === 'node_modules') continue
174
+ if (entry.name.startsWith('.') || SKIP_SOURCE_DIRS.has(entry.name)) continue
78
175
  const full = path.join(dir, entry.name)
79
176
  // a nested Package.swift is a separate package with its own compile:
80
177
  // its sources belong to that unit's stamp, not this one's
81
178
  if (entry.isDirectory()) {
82
179
  if (full !== root && fs.existsSync(path.join(full, 'Package.swift'))) continue
83
180
  walk(full)
84
- } else if (entry.name.endsWith('.swift')) found.push(full)
181
+ } else if (
182
+ entry.name.endsWith('.swift') &&
183
+ (entry.isFile() || (entry.isSymbolicLink() && fs.statSync(full).isFile()))
184
+ )
185
+ found.push(full)
85
186
  }
86
187
  }
87
188
  walk(root)
@@ -98,7 +199,7 @@ function resourceCandidateFiles(root: string): string[] {
98
199
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
99
200
  if (
100
201
  entry.name.startsWith('.') ||
101
- entry.name === 'node_modules' ||
202
+ SKIP_SOURCE_DIRS.has(entry.name) ||
102
203
  entry.name === '.build'
103
204
  )
104
205
  continue
@@ -127,11 +228,14 @@ const RESOURCE_CONTENT_TYPES: Record<string, string> = {
127
228
  }
128
229
 
129
230
  export interface SwiftResourceTable {
130
- /** bundle asset name to absolute serving url. */
131
- table: Record<string, string>
231
+ /** bundle asset name to its absolute serving url and scale. */
232
+ table: Record<string, SwiftImageSource>
132
233
  /** the same rasters as compile-address inputs. */
133
234
  images: Record<string, SwiftImageUpload>
134
- /** project-relative image files this load's table covers (for watching). */
235
+ /** asset-catalog color name to its value. */
236
+ colors: Record<string, AppCatalogColor>
237
+ symbols: Record<string, SfSymbolVector>
238
+ /** project-relative image and color files this load's table covers (for watching). */
135
239
  watched: string[]
136
240
  }
137
241
 
@@ -148,20 +252,25 @@ export function buildSwiftResourceTable(
148
252
  path.relative(project.packagePath, file).split(path.sep).join(path.posix.sep),
149
253
  )
150
254
  const byRelative = new Map(absolute.map((file, index) => [relative[index], file]))
151
- const found = enumerateBundleImages(relative, (candidate) => {
255
+ const readContents = (candidate: string): string | null => {
152
256
  const file = byRelative.get(candidate)
153
- if (!file || path.basename(file) !== 'Contents.json') return null
257
+ // catalog json, and the glyph template a symbolset names
258
+ if (!file || !/(^|\/)Contents\.json$|\.symbolset\/[^/]+\.svg$/.test(candidate))
259
+ return null
154
260
  try {
155
261
  return fs.readFileSync(file, 'utf8')
156
262
  } catch {
157
263
  return null
158
264
  }
159
- })
160
- const table: Record<string, string> = {}
265
+ }
266
+ const found = enumerateBundleImages(relative, readContents)
267
+ const colors = enumerateBundleColors(relative, readContents)
268
+ const symbols = enumerateBundleSymbols(relative, readContents)
269
+ const table: Record<string, SwiftImageSource> = {}
161
270
  const images: Record<string, SwiftImageUpload> = {}
162
271
  const watched: string[] = []
163
272
  const registered = new Map<string, string>()
164
- for (const { name, file } of found) {
273
+ for (const { name, file, scale } of found) {
165
274
  const disk = byRelative.get(file)
166
275
  if (!disk) continue
167
276
  let bytes: Buffer
@@ -172,7 +281,7 @@ export function buildSwiftResourceTable(
172
281
  }
173
282
  const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 16)
174
283
  const ext = path.posix.extname(file).toLowerCase()
175
- table[name] = `${baseUrl}${RESOURCE_PATH}${hash}${ext}`
284
+ table[name] = { uri: `${baseUrl}${RESOURCE_PATH}${hash}${ext}`, scale }
176
285
  images[name] = {
177
286
  file: path.posix.basename(file),
178
287
  data: bytes.toString('base64'),
@@ -184,22 +293,28 @@ export function buildSwiftResourceTable(
184
293
  if (byRelative.has(contents)) watched.push(contents)
185
294
  registered.set(`${hash}${ext}`, disk)
186
295
  }
296
+ // a colorset or symbolset edit re-resolves like an imageset edit.
297
+ for (const file of relative) {
298
+ if (file.endsWith('.colorset/Contents.json') || file.includes('.symbolset/'))
299
+ watched.push(file)
300
+ }
187
301
  project.resourceFiles = registered
188
- return { table, images, watched }
302
+ return { table, images, colors, symbols, watched }
189
303
  }
190
304
 
191
305
  // the /res pack: every Resources/ json in the package, basename-keyed and
192
306
  // base64, riding the entry module so the mount serves it without a fetch.
193
307
  // the namespace is flat like the bundle the wasm side reads: shallower
194
308
  // paths pack first (the Resources root is the bundle root) with a lexical
195
- // tiebreak, and the first spelling of a basename wins a collision. caps are
196
- // INFERRED (no app in either rung is near them; landmarkData-scale json is
197
- // tens of KB): a file past them is skipped and named in `skipped` so the
198
- // load warns instead of silently serving a partial bundle. binary bytes
199
- // travel the same map when a port reaches past json; the worker half packs
200
- // text only until then (see the gaps ledger).
309
+ // tiebreak, and the first spelling of a basename wins a collision. data json
310
+ // runs tens of KB, but a string catalog carries every language it ships (a
311
+ // 19-language app catalog measured 2.2MB), which sets the per-file cap: a
312
+ // file past a cap is skipped and named in `skipped` so the load warns
313
+ // instead of silently serving a partial bundle. binary bytes travel the same
314
+ // map when a port reaches past json; the worker half packs text only until
315
+ // then (see the gaps ledger).
201
316
  const BUNDLE_MAX_FILES = 128
202
- const BUNDLE_MAX_FILE_BYTES = 2 * 1024 * 1024
317
+ const BUNDLE_MAX_FILE_BYTES = 4 * 1024 * 1024
203
318
  const BUNDLE_MAX_TOTAL_BYTES = 8 * 1024 * 1024
204
319
 
205
320
  export interface SwiftBundlePack {
@@ -295,12 +410,13 @@ export function linkedModuleRoots(packagePath: string): string[] {
295
410
  // signal hashes mtimes while that stable address hashes content. a checkout
296
411
  // restoring byte-identical files rebuilds here and reuses the same address
297
412
  // there, by design.
298
- export function sourceStamp(packagePath: string): string {
413
+ export function sourceStamp(packagePath: string, moduleRootOverride?: string): string {
299
414
  const hash = createHash('sha256')
300
415
  hash.update(packagePath)
301
416
  const projectInputs = new Set([
302
417
  path.join(packagePath, 'Package.swift'),
303
418
  ...sourceFiles(packagePath),
419
+ ...nativeSourceFiles(packagePath),
304
420
  ...buildSwiftBundleFiles(packagePath).watched.map((file) =>
305
421
  path.join(packagePath, ...file.split(path.posix.sep)),
306
422
  ),
@@ -312,15 +428,14 @@ export function sourceStamp(packagePath: string): string {
312
428
  hash.update(path.relative(packagePath, file))
313
429
  hash.update(String(fs.statSync(file).mtimeMs))
314
430
  }
315
- const moduleRoot = linkedModuleRoots(packagePath).find((root) =>
316
- fs.existsSync(path.join(root, 'toolchain.json')),
317
- )
431
+ const moduleRoot =
432
+ moduleRootOverride ??
433
+ linkedModuleRoots(packagePath).find((root) =>
434
+ fs.existsSync(path.join(root, 'toolchain.json')),
435
+ )
318
436
  if (moduleRoot) {
319
437
  hash.update(moduleRoot)
320
- for (const relative of [
321
- ...swiftModuleInputFiles(moduleRoot),
322
- 'toolchain.json',
323
- ].sort()) {
438
+ for (const relative of swiftModuleInputFiles(moduleRoot)) {
324
439
  hash.update(relative)
325
440
  hash.update(String(fs.statSync(path.join(moduleRoot, relative)).mtimeMs))
326
441
  }
@@ -353,7 +468,13 @@ function artifactUrl(project: SwiftProject, artifact: SwiftArtifact): string {
353
468
  return `${serverOrigin()}${ARTIFACT_PATH}${artifact.hash}.wasm`
354
469
  }
355
470
 
356
- function compileInput(packagePath: string, entry: string): SwiftCompileInput {
471
+ function icuUrl(origin: string, artifact: SwiftArtifact): string {
472
+ return 'url' in artifact.icu
473
+ ? artifact.icu.url
474
+ : `${origin}${ICU_PATH}${artifact.icu.sha256}`
475
+ }
476
+
477
+ function packageSources(packagePath: string): Record<string, string> {
357
478
  const files: Record<string, string> = {}
358
479
  for (const file of sourceFiles(packagePath)) {
359
480
  const relative = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
@@ -361,29 +482,37 @@ function compileInput(packagePath: string, entry: string): SwiftCompileInput {
361
482
  }
362
483
  const manifest = path.join(packagePath, 'Package.swift')
363
484
  if (fs.existsSync(manifest)) files['Package.swift'] = fs.readFileSync(manifest, 'utf8')
364
- const resources = buildSwiftBundleFiles(packagePath).files
365
- const images = buildSwiftResourceTable(projectFor(packagePath), '').images
366
- return {
367
- files,
368
- resources,
369
- images,
485
+ return files
486
+ }
487
+
488
+ export function compileInput(packagePath: string, entry: string): SwiftCompileInput {
489
+ const input: SwiftCompileInput = {
490
+ files: packageSources(packagePath),
491
+ resources: buildSwiftBundleFiles(packagePath).files,
492
+ images: buildSwiftResourceTable(projectFor(packagePath), '').images,
370
493
  entry,
494
+ oneModule: true,
371
495
  }
496
+ return nativeSourceDescriptor(packagePath)
497
+ ? withNativeSourceGlue(
498
+ input,
499
+ fs.readFileSync(path.join(packagePath, NATIVE_GLUE), 'utf8'),
500
+ )
501
+ : input
372
502
  }
373
503
 
374
504
  async function compile(
375
505
  packagePath: string,
376
506
  entry: string,
377
507
  stamp: string,
508
+ moduleRootOverride?: string,
378
509
  ): Promise<SwiftArtifact> {
379
- const moduleRoot = linkedModuleRoots(packagePath).find((root) =>
380
- fs.existsSync(path.join(root, 'toolchain.json')),
381
- )
382
- if (!moduleRoot) {
383
- throw new Error(
384
- `swift project ${packagePath} has no linked rnx SwiftUI package with toolchain.json`,
510
+ const moduleRoot =
511
+ moduleRootOverride ??
512
+ linkedModuleRoots(packagePath).find((root) =>
513
+ fs.existsSync(path.join(root, 'toolchain.json')),
385
514
  )
386
- }
515
+ if (!moduleRoot) return { ...(await compileRemote(packagePath, entry)), stamp }
387
516
  const toolchain: unknown = JSON.parse(
388
517
  fs.readFileSync(path.join(moduleRoot, 'toolchain.json'), 'utf8'),
389
518
  )
@@ -395,9 +524,21 @@ async function compile(
395
524
  throw new Error(`swift toolchain manifest at ${moduleRoot} has no release`)
396
525
  }
397
526
  const artifact = await compileSwiftProjectUnit(compileInput(packagePath, entry), {
527
+ // the compile rewrites Sources/App in its root before swiftpm builds, and
528
+ // its lock orders one process only: two vites on one root (a lane's dev
529
+ // server and a capture, or two checkouts) build each other's sources. so
530
+ // each app and module checkout gets its own root, which keeps the same
531
+ // app's incremental build across restarts.
398
532
  buildRoot:
399
533
  process.env.CONTRAST_SWIFT_BUILD_ROOT ??
400
- path.join(os.tmpdir(), 'contrast-swift-build'),
534
+ path.join(
535
+ os.tmpdir(),
536
+ 'contrast-swift-build',
537
+ createHash('sha256')
538
+ .update(`${moduleRoot}\0${path.resolve(packagePath)}`)
539
+ .digest('hex')
540
+ .slice(0, 16),
541
+ ),
401
542
  moduleRoot,
402
543
  release,
403
544
  toolchainBin: path.join(
@@ -408,8 +549,64 @@ async function compile(
408
549
  return { ...artifact, stamp }
409
550
  }
410
551
 
411
- function artifactFor(project: SwiftProject, entry: string): Promise<SwiftArtifact> {
412
- const stamp = sourceStamp(project.packagePath)
552
+ // an npm-installed peachsim ships neither the SwiftUI module nor a toolchain,
553
+ // so the package compiles where a Contrast project's does: Contrast's compile
554
+ // route, authorized by `peach login` or PEACH_API_KEY. the service renders the
555
+ // native dispatch glue itself under the package's id and answers the wasm at a
556
+ // public content address, which is fetched once and served from this dev
557
+ // server like a local build. bundle images, colors and json stay local.
558
+ async function compileRemote(
559
+ packagePath: string,
560
+ entry: string,
561
+ ): Promise<Omit<SwiftArtifact, 'stamp'>> {
562
+ const auth = resolveCliAuth()
563
+ if (!auth || auth.kind === 'github') {
564
+ throw new Error(
565
+ `compiling ${path.basename(packagePath)} for Peach needs \`peach login\` or PEACH_API_KEY: this peachsim has no local SwiftUI module, so Contrast compiles it`,
566
+ )
567
+ }
568
+ const origin = resolveContrastBackendOrigin(
569
+ auth.kind === 'session' ? auth.origin : null,
570
+ )
571
+ const submit = httpSwiftCompileSubmit({
572
+ endpoint: origin,
573
+ authorization: authHeaderValue(auth),
574
+ })
575
+ if (!submit) throw new Error('swift compile: no credential to submit with')
576
+ const descriptor = nativeSourceDescriptor(packagePath)
577
+ const address = await compileSwiftProject(
578
+ submit,
579
+ {
580
+ files: packageSources(packagePath),
581
+ entry,
582
+ packageId: descriptor?.packageId ?? swiftPackageId(packagePath),
583
+ },
584
+ origin,
585
+ )
586
+ // the app's JS calls with the hash its own `one typegen` wrote; glue the
587
+ // service rendered from another One would reject every call as stale.
588
+ if (descriptor && address.native?.hash !== descriptor.hash) {
589
+ throw new Error(
590
+ `${path.basename(packagePath)}: Contrast compiled native contract ${address.native?.hash ?? 'none'} but one typegen wrote ${descriptor.hash}; the app's One and the compile host's differ, or the compile host is older than this peachsim`,
591
+ )
592
+ }
593
+ const response = await fetch(address.url)
594
+ if (!response.ok) {
595
+ throw new Error(`swift compile: ${address.url} answered ${response.status}`)
596
+ }
597
+ return {
598
+ hash: address.hash,
599
+ bytes: Buffer.from(await response.arrayBuffer()),
600
+ icu: { url: address.icuUrl },
601
+ }
602
+ }
603
+
604
+ function artifactFor(
605
+ project: SwiftProject,
606
+ entry: string,
607
+ moduleRoot?: string,
608
+ ): Promise<SwiftArtifact> {
609
+ const stamp = sourceStamp(project.packagePath, moduleRoot)
413
610
  if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact)
414
611
  if (project.building) {
415
612
  // a second edit during a build: the build in flight was started from
@@ -418,10 +615,10 @@ function artifactFor(project: SwiftProject, entry: string): Promise<SwiftArtifac
418
615
  // would emit the older artifact and nothing would trigger another load.
419
616
  const running = project.building
420
617
  if (running.stamp === stamp) return running.promise
421
- const again = () => artifactFor(project, entry)
618
+ const again = () => artifactFor(project, entry, moduleRoot)
422
619
  return running.promise.then(again, again)
423
620
  }
424
- const promise = compile(project.packagePath, entry, stamp).then(
621
+ const promise = compile(project.packagePath, entry, stamp, moduleRoot).then(
425
622
  (artifact) => {
426
623
  project.artifact = artifact
427
624
  project.building = null
@@ -436,6 +633,111 @@ function artifactFor(project: SwiftProject, entry: string): Promise<SwiftArtifac
436
633
  return promise
437
634
  }
438
635
 
636
+ export function swiftPackageWatchFiles(
637
+ packagePath: string,
638
+ moduleRoot: string | undefined,
639
+ ): string[] {
640
+ return [
641
+ path.join(packagePath, 'Package.swift'),
642
+ ...sourceFiles(packagePath),
643
+ ...nativeSourceFiles(packagePath),
644
+ ...resourceCandidateFiles(packagePath),
645
+ ...(moduleRoot
646
+ ? swiftModuleInputFiles(moduleRoot).map((file) => path.join(moduleRoot, file))
647
+ : []),
648
+ ]
649
+ }
650
+
651
+ export async function swiftPackageMetadata(
652
+ packagePath: string,
653
+ packageName: string,
654
+ moduleRoot: string | undefined,
655
+ origin: string,
656
+ ): Promise<SwiftEntryModuleArgs> {
657
+ const mains = sourceFiles(packagePath).filter((file) =>
658
+ /@main\s+(?:public\s+)?struct\s+\w+\s*:/.test(fs.readFileSync(file, 'utf8')),
659
+ )
660
+ const native = nativeSourceDescriptor(packagePath)
661
+ if (mains.length > 1 || (mains.length === 0 && !native)) {
662
+ throw new Error(
663
+ `swift package ${packageName} needs exactly one @main source or generated native modules; found ${mains.length}`,
664
+ )
665
+ }
666
+ const project = projectFor(packagePath)
667
+ const entry =
668
+ mains.length === 1
669
+ ? path.relative(packagePath, mains[0]).split(path.sep).join(path.posix.sep)
670
+ : Object.entries(native?.files ?? {}).find(
671
+ ([_file, names]) => names.length > 0,
672
+ )?.[0]
673
+ if (!entry) throw new Error(`swift package ${packageName} has no native module source`)
674
+ const artifact = await artifactFor(project, entry, moduleRoot)
675
+ const resources = buildSwiftResourceTable(project, origin)
676
+ const bundleFiles = buildSwiftBundleFiles(packagePath)
677
+ return {
678
+ url: `${origin}${ARTIFACT_PATH}${artifact.hash}.wasm`,
679
+ hash: artifact.hash,
680
+ icuUrl: icuUrl(origin, artifact),
681
+ rootId: `${packageName}/${entry}`,
682
+ resources: resources.table,
683
+ bundleFiles: bundleFiles.files,
684
+ colors: resources.colors,
685
+ symbols: resources.symbols,
686
+ native: native
687
+ ? {
688
+ packageId: native.packageId,
689
+ contractHash: native.hash,
690
+ modules: native.modules,
691
+ defaultView: mains.length === 1,
692
+ }
693
+ : undefined,
694
+ }
695
+ }
696
+
697
+ export function serveSwiftPackageAsset(pathname: string, res: ServerResponse): boolean {
698
+ if (pathname.startsWith(RESOURCE_PATH)) {
699
+ for (const project of projects.values()) {
700
+ const served = serveSwiftResource(project, pathname.slice(RESOURCE_PATH.length))
701
+ if (!served) continue
702
+ res.setHeader('access-control-allow-origin', '*')
703
+ res.setHeader('content-type', served.contentType)
704
+ res.end(served.bytes)
705
+ return true
706
+ }
707
+ res.statusCode = 404
708
+ res.end('swift resource is stale')
709
+ return true
710
+ }
711
+ if (pathname.startsWith(ICU_PATH)) {
712
+ for (const project of projects.values()) {
713
+ const icu = project.artifact?.icu
714
+ if (!icu || 'url' in icu || pathname !== `${ICU_PATH}${icu.sha256}`) continue
715
+ // the address is the content, so the browser keeps it across reloads,
716
+ // restarts and every app built with this toolchain
717
+ res.setHeader('access-control-allow-origin', '*')
718
+ res.setHeader('content-type', 'application/gzip')
719
+ res.setHeader('cache-control', 'public, max-age=31536000, immutable')
720
+ res.end(icu.gzip)
721
+ return true
722
+ }
723
+ res.statusCode = 404
724
+ res.end('swift icu data is stale')
725
+ return true
726
+ }
727
+ if (!pathname.startsWith(ARTIFACT_PATH)) return false
728
+ for (const project of projects.values()) {
729
+ const artifact = project.artifact
730
+ if (!artifact || pathname !== `${ARTIFACT_PATH}${artifact.hash}.wasm`) continue
731
+ res.setHeader('access-control-allow-origin', '*')
732
+ res.setHeader('content-type', 'application/wasm')
733
+ res.end(artifact.bytes)
734
+ return true
735
+ }
736
+ res.statusCode = 404
737
+ res.end('swift artifact is stale')
738
+ return true
739
+ }
740
+
439
741
  // the swiftpm package that owns an imported file: the nearest directory at
440
742
  // or above the file that holds a Package.swift, stopping at the vite root
441
743
  // once the dev server has named it. a swift-only project keeps its sources at
@@ -481,7 +783,20 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
481
783
  if (!source.endsWith('.swift')) return null
482
784
  const resolved = await this.resolve(source, importer, { skipSelf: true })
483
785
  if (!resolved) throw new Error(`cannot resolve swift source ${source}`)
484
- return VIRTUAL_PREFIX + resolved.id
786
+ // a symlinked source (the showcase app links the files Xcode builds)
787
+ // resolves to its target, and the package walk then finds the target's
788
+ // package instead of the importing one. keep the importer's own path
789
+ // when it names the same file, so the import's package owns the compile.
790
+ let id = resolved.id
791
+ if (importer && (source.startsWith('./') || source.startsWith('../'))) {
792
+ const raw = path.resolve(path.dirname(importer.split('?')[0] ?? importer), source)
793
+ try {
794
+ if (fs.realpathSync(raw) === fs.realpathSync(resolved.id)) id = raw
795
+ } catch {
796
+ // fall through with the resolver's answer
797
+ }
798
+ }
799
+ return VIRTUAL_PREFIX + id
485
800
  },
486
801
 
487
802
  async load(id) {
@@ -498,7 +813,11 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
498
813
  // bundler watching this module learns about a `.swift` edit and re-runs
499
814
  // load. that is the whole hot reload: the new hash in the emitted module
500
815
  // is what tells the mount it is looking at a different app.
501
- for (const source of sourceFiles(packagePath)) this.addWatchFile(source)
816
+ for (const source of [
817
+ ...sourceFiles(packagePath),
818
+ ...nativeSourceFiles(packagePath),
819
+ ])
820
+ this.addWatchFile(source)
502
821
  this.addWatchFile(path.join(packagePath, 'Package.swift'))
503
822
  // an edit to the linked SwiftUI module rebuilds the artifact without
504
823
  // touching the project's own sources, so those files gate the reload
@@ -523,10 +842,14 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
523
842
  // the bundle image table rides the same module: a photo edit moves
524
843
  // its urls without rebuilding the artifact, and the watched files
525
844
  // re-run this load so the mount sees the new table on hot reload.
526
- let resources: Record<string, string> = {}
845
+ let resources: Record<string, SwiftImageSource> = {}
846
+ let colors: Record<string, AppCatalogColor> = {}
847
+ let symbols: Record<string, SfSymbolVector> = {}
527
848
  try {
528
849
  const built = buildSwiftResourceTable(project, serverOrigin())
529
850
  resources = built.table
851
+ colors = built.colors
852
+ symbols = built.symbols
530
853
  for (const watched of built.watched) {
531
854
  this.addWatchFile(path.join(packagePath, ...watched.split(path.posix.sep)))
532
855
  }
@@ -559,12 +882,17 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
559
882
  // whether it is a view or an app root at runtime, so the transform
560
883
  // never re-derives the conformance from source. the entry is rooted by
561
884
  // its package and source path so a hot reload finds the same instance.
885
+ const native = nativeSourceDescriptor(packagePath)
562
886
  return swiftEntryModuleEsm({
563
887
  url: artifactUrl(project, artifact),
564
888
  hash: artifact.hash,
889
+ icuUrl: icuUrl(serverOrigin(), artifact),
565
890
  rootId: entryRootId(packagePath, file),
566
891
  resources,
567
892
  bundleFiles,
893
+ colors,
894
+ symbols,
895
+ native: native ? swiftNativeEntry(native, entry) : undefined,
568
896
  })
569
897
  },
570
898
 
@@ -572,49 +900,7 @@ export function swiftPlugin(options: SwiftPluginOptions = {}): Plugin {
572
900
  devServer = server
573
901
 
574
902
  server.middlewares.use((req, res, next) => {
575
- const url = (req.url ?? '').split('?')[0]
576
- if (url.startsWith(RESOURCE_PATH)) {
577
- // hash-addressed: the table this load emitted names these hashes,
578
- // so a hit serves known bytes and anything else is a 404, never a
579
- // neighboring file. the tenant worker is cross-origin like the
580
- // artifact fetch, hence the same cors header.
581
- for (const project of projects.values()) {
582
- const served = serveSwiftResource(project, url.slice(RESOURCE_PATH.length))
583
- if (served) {
584
- res.setHeader('access-control-allow-origin', '*')
585
- res.setHeader('content-type', served.contentType)
586
- res.end(served.bytes)
587
- return
588
- }
589
- }
590
- res.statusCode = 404
591
- res.end('swift resource is stale')
592
- return
593
- }
594
- if (!url.startsWith(ARTIFACT_PATH)) {
595
- next()
596
- return
597
- }
598
- // the url names a content hash, not a package: the request is answered
599
- // by whichever known project built it, so a React Native app whose
600
- // swift package sits below its root serves the same path its imports
601
- // compiled. served from what is already built, never rebuilt here: the
602
- // page only knows a hash that `load` emitted, and `load` under a compile
603
- // error emits the last good hash, whose bytes are still the project's
604
- // artifact. rebuilding would fail that fetch with the compile error and
605
- // fail every other project's artifact with it.
606
- for (const project of projects.values()) {
607
- const artifact = project.artifact
608
- if (artifact && url === `${ARTIFACT_PATH}${artifact.hash}.wasm`) {
609
- // the tenant worker's origin is the shell, never this dev server
610
- res.setHeader('access-control-allow-origin', '*')
611
- res.setHeader('content-type', 'application/wasm')
612
- res.end(artifact.bytes)
613
- return
614
- }
615
- }
616
- res.statusCode = 404
617
- res.end('swift artifact is stale')
903
+ if (!serveSwiftPackageAsset((req.url ?? '').split('?')[0], res)) next()
618
904
  })
619
905
  },
620
906
  }