peachsim 0.1.527 → 0.1.571

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/README.md +28 -28
  2. package/cli/app-fonts.ts +1 -1
  3. package/cli/app-splash.ts +1 -1
  4. package/cli/bin.ts +1 -1
  5. package/cli/bridge-diagnostics.ts +1 -1
  6. package/cli/bridge-flow-runner.ts +72 -12
  7. package/cli/cloud-client.ts +273 -6
  8. package/cli/cloud-dispatch.ts +23 -2
  9. package/cli/cloud-session.ts +60 -4
  10. package/cli/cloud-test-result-files.ts +97 -0
  11. package/cli/cloud-test-tunnel.ts +354 -0
  12. package/cli/commands/box/bundle-plane.ts +2 -2
  13. package/cli/commands/box/checkout-plane.ts +1 -1
  14. package/cli/commands/box/rnx-command.ts +2 -2
  15. package/cli/commands/box/search-command.ts +2 -2
  16. package/cli/commands/box.ts +8 -8
  17. package/cli/commands/compat.ts +1 -1
  18. package/cli/commands/control.ts +22 -3
  19. package/cli/commands/detox.ts +1 -1
  20. package/cli/commands/device.ts +1 -1
  21. package/cli/commands/diagnose.ts +1 -1
  22. package/cli/commands/film.ts +1 -1
  23. package/cli/commands/flow.ts +60 -3
  24. package/cli/commands/inspect/actions.ts +1 -1
  25. package/cli/commands/inspect/core.ts +6 -7
  26. package/cli/commands/inspect/find.ts +2 -1
  27. package/cli/commands/inspect/resolve-target.ts +5 -3
  28. package/cli/commands/inspect/shared.ts +1 -1
  29. package/cli/commands/inspect/wait-event.ts +1 -1
  30. package/cli/commands/inspect/wait-selector.ts +15 -4
  31. package/cli/commands/inspect.ts +19 -14
  32. package/cli/commands/maestro.ts +582 -69
  33. package/cli/commands/permissions.ts +1 -1
  34. package/cli/commands/platform.ts +205 -8
  35. package/cli/commands/preview.ts +2 -2
  36. package/cli/commands/record.ts +2 -2
  37. package/cli/commands/report-issue.ts +2 -2
  38. package/cli/commands/screenshot-layers.ts +1 -1
  39. package/cli/commands/screenshot.ts +1 -1
  40. package/cli/commands/setup.ts +2 -2
  41. package/cli/commands/test.ts +600 -117
  42. package/cli/commands/three-mode.ts +1 -1
  43. package/cli/commands/upload.ts +5 -5
  44. package/cli/commands/what-happened.ts +1 -1
  45. package/cli/desktop-companion.ts +3 -3
  46. package/cli/drivers/playwright-sim-host.ts +2 -2
  47. package/cli/flow-file.ts +7 -0
  48. package/cli/flow-live-status.ts +65 -8
  49. package/cli/help-core.ts +3 -3
  50. package/cli/hidden-runtime-alias.ts +1 -1
  51. package/cli/maestro-report.ts +180 -0
  52. package/cli/main.ts +1 -1
  53. package/cli/outbound-endpoints.ts +187 -136
  54. package/cli/parse-args.ts +1 -1
  55. package/cli/registry.ts +1 -1
  56. package/cli/resolve-assets.ts +1 -1
  57. package/cli/run-registry.ts +10 -1
  58. package/cli/send-to-box.ts +4 -4
  59. package/cli/ws-bridge.ts +3 -5
  60. package/detox/expectations.ts +1 -1
  61. package/detox/gestures.ts +2 -2
  62. package/detox/index.ts +71 -29
  63. package/detox/jest-preset.cjs +5 -5
  64. package/detox/jest-setup-after-env.cjs +1 -1
  65. package/dist-lib/agent-daemon-client.cjs +2 -2
  66. package/dist-lib/agent-events.cjs +1 -1
  67. package/dist-lib/agent-identity.cjs +1 -1
  68. package/dist-lib/agent-sessions.cjs +2 -2
  69. package/dist-lib/attached-projects.cjs +2 -2
  70. package/dist-lib/auth/shared-session.cjs +1 -1
  71. package/dist-lib/backend-origin.cjs +1 -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 +1 -1
  76. package/dist-lib/bridge-contract-input.mjs +1 -1
  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 +94 -3
  80. package/dist-lib/capture-contract.mjs +93 -3
  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 +116 -0
  85. package/dist-lib/cloud-test-args.mjs +86 -0
  86. package/dist-lib/cloud.cjs +2 -2
  87. package/dist-lib/cloud.mjs +2 -2
  88. package/dist-lib/config.cjs +1 -1
  89. package/dist-lib/detox/index.cjs +106 -25
  90. package/dist-lib/detox/jest-preset.cjs +5 -5
  91. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  92. package/dist-lib/home-paths.cjs +1 -1
  93. package/dist-lib/host/bridge-host.cjs +150 -65
  94. package/dist-lib/host/fetch-proxy-handler.cjs +28 -2
  95. package/dist-lib/host/fetch-proxy-overrides.cjs +33 -3
  96. package/dist-lib/host/fetch-proxy-overrides.mjs +30 -2
  97. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  98. package/dist-lib/host/websocket-proxy.cjs +1 -1
  99. package/dist-lib/index.cjs +224 -41
  100. package/dist-lib/jump-to-source-babel.cjs +1 -1
  101. package/dist-lib/jump-to-source-native.cjs +1 -1
  102. package/dist-lib/menu.cjs +2 -2
  103. package/dist-lib/menu.mjs +2 -2
  104. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  105. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  106. package/dist-lib/metro-production-bundle.cjs +3 -3
  107. package/dist-lib/metro-production-bundle.mjs +3 -3
  108. package/dist-lib/metro.cjs +8 -8
  109. package/dist-lib/profiles.cjs +1 -1
  110. package/dist-lib/public-brand.cjs +2 -2
  111. package/dist-lib/react-native-host-modules.cjs +1 -1
  112. package/dist-lib/react-native-host-modules.mjs +1 -1
  113. package/dist-lib/render-mode.cjs +1 -1
  114. package/dist-lib/scripts/dev-server-scanner.cjs +31 -15
  115. package/dist-lib/sdk.cjs +3 -2
  116. package/dist-lib/sdk.mjs +3 -2
  117. package/dist-lib/skills.cjs +478 -266
  118. package/dist-lib/swift-compile.cjs +372 -0
  119. package/dist-lib/swift-compile.mjs +339 -0
  120. package/dist-lib/swift-package.cjs +78 -0
  121. package/dist-lib/swift-package.mjs +52 -0
  122. package/dist-lib/swift-submit.cjs +1 -1
  123. package/dist-lib/swift-submit.mjs +1 -1
  124. package/dist-lib/swift.cjs +583 -16
  125. package/dist-lib/vite.cjs +483 -150
  126. package/package.json +30 -9
  127. package/scripts/dev-server-scanner.ts +41 -14
  128. package/scripts/optional-demo-registry.ts +3 -3
  129. package/skills/contrast/SKILL.md +9 -9
  130. package/skills/rnx-debug/SKILL.md +14 -4
  131. package/skills/rnx-perf/SKILL.md +5 -5
  132. package/skills/rnx-setup/SKILL.md +11 -11
  133. package/skills/rnx-test/SKILL.md +14 -14
  134. package/skills/rnx-visual/SKILL.md +6 -6
  135. package/src/agent-sessions.ts +3 -3
  136. package/src/attached-projects.ts +1 -1
  137. package/src/bridge-contract-input.ts +1 -1
  138. package/src/bridge-contract.ts +23 -5
  139. package/src/capture-contract.ts +145 -3
  140. package/src/cloud-test-args.ts +100 -0
  141. package/src/config.ts +2 -2
  142. package/src/connect.ts +2 -2
  143. package/src/home-paths.ts +3 -3
  144. package/src/host/bridge-host.ts +2 -2
  145. package/src/host/fetch-proxy-handler.ts +4 -3
  146. package/src/host/fetch-proxy-overrides.ts +41 -0
  147. package/src/host/plane-host.ts +2 -2
  148. package/src/index.ts +3 -0
  149. package/src/metro-plugin.ts +4 -4
  150. package/src/metro-production-bundle.ts +2 -2
  151. package/src/native-seam-manifest.ts +6 -6
  152. package/src/public-brand.ts +1 -1
  153. package/src/rolldown-plugin-swift.ts +16 -12
  154. package/src/runtime-assets.ts +8 -6
  155. package/src/screenshots/frame-compose.ts +24 -56
  156. package/src/screenshots/org-deck.ts +1 -1
  157. package/src/screenshots/registry.ts +4 -4
  158. package/src/screenshots/schema.ts +1 -1
  159. package/src/sdk.ts +2 -2
  160. package/src/settings-bundle.ts +1 -1
  161. package/src/sim-client.ts +2 -4
  162. package/src/skills/builtin/compat-check.ts +1 -1
  163. package/src/swift-compile.ts +359 -0
  164. package/src/swift-package.ts +69 -0
  165. package/src/swift-submit.ts +3 -2
  166. package/src/vite-plugin-one.ts +1 -1
  167. package/src/vite-plugin-swift.ts +114 -113
  168. package/src/vite-plugin.ts +30 -17
  169. package/src/worklets-babel.ts +2 -2
@@ -5,12 +5,13 @@ import {
5
5
  LEGACY_HOME_BUTTON_SIZE,
6
6
  LEGACY_SIDE_BEZEL,
7
7
  LEGACY_TOP_BEZEL,
8
- HINGE_BARREL_BACKGROUND,
9
- HINGE_BARREL_SHADOW,
10
- HOUSING_RAIL_COLOR,
11
- HOUSING_RAIL_OUTLINE,
12
8
  SIDE_BUTTON_RESERVE,
13
- } from 'sootsim-engine/ios/chrome-metrics'
9
+ } from 'peach-engine/ios/chrome-metrics'
10
+ import {
11
+ housingLayerHtml,
12
+ resolveHousingArt,
13
+ type HousingLayer,
14
+ } from 'peach-engine/ios/duo-housing'
14
15
  import {
15
16
  cornerRadii,
16
17
  formatCornerRadii,
@@ -18,7 +19,7 @@ import {
18
19
  resolveDeviceFrameGeometry,
19
20
  type CornerRadii,
20
21
  type DeviceModel,
21
- } from 'sootsim-engine/settings'
22
+ } from 'peach-engine/settings'
22
23
  import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
23
24
 
24
25
  const METALLIC_RING_SHADOW =
@@ -56,8 +57,8 @@ export interface FramedScreenshotLayout {
56
57
  logicalScreenTop: number
57
58
  logicalScreenRadius: CornerRadii
58
59
  logicalFrameRadius: CornerRadii
59
- // light rail around a black glass ring; null draws one black body
60
- housing: ReturnType<typeof resolveDeviceFrameGeometry>['housing']
60
+ // folding body art, painted before the screen; null draws one black body
61
+ housingLayers: HousingLayer[] | null
61
62
  frameBackground: string
62
63
  frameOutline: string
63
64
  metallicRingShadow: string | null
@@ -87,7 +88,9 @@ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotL
87
88
  const logicalScreenWidth = spec.width
88
89
  const logicalScreenHeight = spec.height
89
90
  const modernGeometry = resolveDeviceFrameGeometry(spec)
90
- const housing = isLegacyHardware ? null : modernGeometry.housing
91
+ const housingLayers = isLegacyHardware
92
+ ? null
93
+ : resolveHousingArt(spec.posture ?? 'closed', modernGeometry)
91
94
  const logicalFrameWidth = isLegacyHardware
92
95
  ? spec.width + LEGACY_SIDE_BEZEL * 2
93
96
  : modernGeometry.frameWidth
@@ -148,18 +151,20 @@ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotL
148
151
  logicalScreenTop,
149
152
  logicalScreenRadius: isLegacyHardware ? cornerRadii(0) : modernGeometry.screenRadius,
150
153
  logicalFrameRadius,
151
- housing,
154
+ housingLayers,
155
+ // the housing paints its own body layer, shadow included: its frame box is
156
+ // wider than the body, because the hinge barrel protrudes past the spine.
152
157
  frameBackground: isLegacyHardware
153
158
  ? 'linear-gradient(180deg, #1b1c20 0%, #0f1012 42%, #050608 100%)'
154
- : housing
155
- ? HOUSING_RAIL_COLOR
159
+ : housingLayers
160
+ ? 'transparent'
156
161
  : '#000000',
157
162
  frameOutline: isLegacyHardware
158
163
  ? '0 0 0 1px #1f2125'
159
- : housing
160
- ? HOUSING_RAIL_OUTLINE
164
+ : housingLayers
165
+ ? 'none'
161
166
  : '0 0 0 1px #333',
162
- metallicRingShadow: isLegacyHardware || housing ? null : METALLIC_RING_SHADOW,
167
+ metallicRingShadow: isLegacyHardware || housingLayers ? null : METALLIC_RING_SHADOW,
163
168
  buttons,
164
169
  legacyHomeButton: isLegacyHardware
165
170
  ? {
@@ -230,46 +235,9 @@ function renderFrameHtml(
230
235
  `
231
236
  : ''
232
237
 
233
- // black glass ring inside the rail, then the hinge barrel over the leading
234
- // rail. the ring carries no z-index so the screen div after it paints on top.
235
- const housingHtml = layout.housing
236
- ? `
237
- <div
238
- aria-hidden="true"
239
- style="
240
- position:absolute;
241
- top:${layout.housing.rail.top}px;
242
- right:${layout.housing.rail.right}px;
243
- bottom:${layout.housing.rail.bottom}px;
244
- left:${layout.housing.rail.left}px;
245
- border-radius:${formatCornerRadii(layout.housing.glassRadius)};
246
- background:#000000;
247
- pointer-events:none;
248
- "
249
- ></div>
250
- ${
251
- layout.housing.hingeBarrel
252
- ? `
253
- <div
254
- aria-hidden="true"
255
- style="
256
- position:absolute;
257
- top:${layout.housing.hingeBarrel.endInset}px;
258
- bottom:${layout.housing.hingeBarrel.endInset}px;
259
- left:0;
260
- width:${layout.housing.hingeBarrel.width}px;
261
- border-radius:${layout.housing.hingeBarrel.width / 2}px 0 0 ${layout.housing.hingeBarrel.width / 2}px;
262
- background:${HINGE_BARREL_BACKGROUND};
263
- box-shadow:${HINGE_BARREL_SHADOW};
264
- pointer-events:none;
265
- z-index:11;
266
- "
267
- ></div>
268
- `
269
- : ''
270
- }
271
- `
272
- : ''
238
+ // the folding body's rail, glass ring, corner art, and hinge barrel. no
239
+ // z-index, so the screen div after it paints on top.
240
+ const housingHtml = (layout.housingLayers ?? []).map(housingLayerHtml).join('')
273
241
 
274
242
  const legacyHomeButtonHtml = layout.legacyHomeButton
275
243
  ? `
@@ -390,7 +358,7 @@ function renderFrameHtml(
390
358
  left:${layout.logicalFrameLeft}px;
391
359
  width:${layout.logicalFrameWidth}px;
392
360
  height:${layout.logicalFrameHeight}px;
393
- border-radius:${formatCornerRadii(layout.logicalFrameRadius)};
361
+ border-radius:${layout.housingLayers ? '0' : formatCornerRadii(layout.logicalFrameRadius)};
394
362
  box-sizing:border-box;
395
363
  overflow:visible;
396
364
  background:${layout.frameBackground};
@@ -6,7 +6,7 @@
6
6
  // colors. 3d scenes don't translate to the cli's 2d composer, so only copy and
7
7
  // colors carry over (the 3d hero look stays the in-browser screenshot mode).
8
8
 
9
- import { resolveBgColorValue } from 'sootsim-engine/screenshots/tokens'
9
+ import { resolveBgColorValue } from 'peach-engine/screenshots/tokens'
10
10
  import type { BackgroundSpec } from './registry'
11
11
  import type { NormalizedScreenshotsPlan } from './schema'
12
12
 
@@ -6,7 +6,7 @@
6
6
  // this module re-exports those tokens plus the composer-specific
7
7
  // `FrameComposeSpec` shape.
8
8
 
9
- import type { DeviceModel } from 'sootsim-engine/settings'
9
+ import type { DeviceModel } from 'peach-engine/settings'
10
10
 
11
11
  export {
12
12
  BACKGROUND_PRESETS,
@@ -19,7 +19,7 @@ export {
19
19
  cloneBackgroundSpec,
20
20
  resolveBackgroundPreset,
21
21
  resolveCanvasPreset,
22
- } from 'sootsim-engine/screenshots/tokens'
22
+ } from 'peach-engine/screenshots/tokens'
23
23
 
24
24
  export type {
25
25
  BackgroundPresetName,
@@ -30,9 +30,9 @@ export type {
30
30
  PosePresetName,
31
31
  ScreenshotCanvasName,
32
32
  TextPresetName,
33
- } from 'sootsim-engine/screenshots/tokens'
33
+ } from 'peach-engine/screenshots/tokens'
34
34
 
35
- import type { FrameShadowSpec, PosePresetName } from 'sootsim-engine/screenshots/tokens'
35
+ import type { FrameShadowSpec, PosePresetName } from 'peach-engine/screenshots/tokens'
36
36
 
37
37
  export interface FrameComposeSpec {
38
38
  show: boolean
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'fs'
2
2
  import path from 'path'
3
- import { devices, type DeviceModel } from 'sootsim-engine/settings'
3
+ import { devices, type DeviceModel } from 'peach-engine/settings'
4
4
  import yaml from 'yaml'
5
5
  import {
6
6
  cloneBackgroundSpec,
package/src/sdk.ts CHANGED
@@ -1,9 +1,9 @@
1
- // rnxsim/sdk — the scriptable sim-driving surface.
1
+ // peachsim/sdk — the scriptable sim-driving surface.
2
2
  //
3
3
  // exports the SimClient class plus the transport-agnostic inspect verbs it is
4
4
  // built from. this entry MUST stay browser-safe (Contrast's in-browser agent
5
5
  // imports it against an in-process preview bridge), so the node-only ws
6
- // `connect()` lives on the ROOT `rnxsim` entry, not here.
6
+ // `connect()` lives on the ROOT `peachsim` entry, not here.
7
7
 
8
8
  export * from './sim-client'
9
9
  export * from '../cli/commands/inspect/core'
@@ -1,7 +1,7 @@
1
1
  import { readFile, readdir, stat } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
  import { parsePlistSource } from './plist.ts'
4
- import type { AppPreferenceValue, AppSettingsSpecifier } from '@rnx/globals'
4
+ import type { AppPreferenceValue, AppSettingsSpecifier } from '@peach/globals'
5
5
 
6
6
  const MAX_PLIST_BYTES = 1_048_576
7
7
  const MAX_CHILD_DEPTH = 8
package/src/sim-client.ts CHANGED
@@ -60,6 +60,7 @@ import type {
60
60
  SimSemanticResolveSelector,
61
61
  SimStateOptions,
62
62
  SimStateResult,
63
+ WsBridgeCommand,
63
64
  } from './bridge-contract'
64
65
 
65
66
  // ---------------------------------------------------------------------------
@@ -250,10 +251,7 @@ export class SimClient implements WsBridge, InspectBridge {
250
251
 
251
252
  // --- transport (WsBridge/InspectBridge) ---
252
253
 
253
- send(
254
- cmd: { type: string; [k: string]: any },
255
- opts?: { timeoutMs?: number },
256
- ): Promise<any> {
254
+ send(cmd: WsBridgeCommand, opts?: { timeoutMs?: number }): Promise<any> {
257
255
  if (this.closed) {
258
256
  return Promise.reject(new SimConnectError('sim client is closed'))
259
257
  }
@@ -22,7 +22,7 @@ export const skill: RNXSkill = {
22
22
  const fs = await import('node:fs')
23
23
  const path = await import('node:path')
24
24
  const { COMPAT_RESULT_CAVEAT, formatPackageStatus, formatScanCounts, scanDeps } =
25
- await import('../../../../compat/src/web.ts')
25
+ await import('../../../../peach-compat/src/web.ts')
26
26
 
27
27
  const packagePath = path.join(context.projectDir, 'package.json')
28
28
  if (!fs.existsSync(packagePath)) {
@@ -0,0 +1,359 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ realpathSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from 'node:fs'
13
+ import path from 'node:path'
14
+ import { promisify } from 'node:util'
15
+ import { swiftManifestTargets, type SwiftPackageTarget } from './swift-package'
16
+ import type { SwiftImageUpload } from './swift-submit'
17
+
18
+ const run = promisify(execFile)
19
+ const WASM_TRIPLE = 'wasm32-unknown-wasip1'
20
+
21
+ export interface SwiftCompileInput {
22
+ files: Record<string, string>
23
+ resources?: Record<string, string>
24
+ images?: Record<string, SwiftImageUpload>
25
+ entry: string
26
+ }
27
+
28
+ export interface SwiftCompileOptions {
29
+ buildRoot: string
30
+ moduleRoot: string
31
+ release: string
32
+ toolchainBin: string
33
+ timeoutMs?: number
34
+ }
35
+
36
+ export interface SwiftCompileArtifact {
37
+ hash: string
38
+ bytes: Buffer
39
+ }
40
+
41
+ export interface SwiftProjectLayout {
42
+ root: string
43
+ sources: string
44
+ multi: boolean
45
+ }
46
+
47
+ export function swiftModuleInputFiles(root: string): string[] {
48
+ const files: string[] = ['Package.swift', 'scripts/contrast-app-Package.swift']
49
+ const walk = (dir: string) => {
50
+ for (const entry of readdirSync(path.join(root, dir), { withFileTypes: true })) {
51
+ const relative = `${dir}/${entry.name}`
52
+ if (entry.isDirectory()) walk(relative)
53
+ else if (entry.name.endsWith('.swift')) files.push(relative)
54
+ }
55
+ }
56
+ walk('Sources')
57
+ return files.sort()
58
+ }
59
+
60
+ const linkedModuleDigests = new Map<string, { stamp: string; digest: Buffer }>()
61
+
62
+ export function clearLinkedModuleDigests(root?: string): void {
63
+ if (root === undefined) linkedModuleDigests.clear()
64
+ else linkedModuleDigests.delete(root)
65
+ }
66
+
67
+ function linkedModuleDigest(root: string): Buffer {
68
+ const files = swiftModuleInputFiles(root)
69
+ const stampHash = createHash('sha256')
70
+ for (const file of files) {
71
+ const stat = statSync(path.join(root, file), { bigint: true })
72
+ stampHash.update(file)
73
+ stampHash.update(
74
+ `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}\n`,
75
+ )
76
+ }
77
+ const stamp = stampHash.digest('hex')
78
+ const cached = linkedModuleDigests.get(root)
79
+ if (cached?.stamp === stamp) return cached.digest
80
+ const hash = createHash('sha256')
81
+ for (const file of files) {
82
+ hash.update(createHash('sha256').update(file).digest())
83
+ hash.update(
84
+ createHash('sha256')
85
+ .update(readFileSync(path.join(root, file)))
86
+ .digest(),
87
+ )
88
+ }
89
+ const digest = hash.digest()
90
+ linkedModuleDigests.set(root, { stamp, digest })
91
+ return digest
92
+ }
93
+
94
+ export function compileTargets(input: SwiftCompileInput): SwiftPackageTarget[] {
95
+ const manifest = input.files['Package.swift']
96
+ return manifest === undefined ? [] : swiftManifestTargets(manifest)
97
+ }
98
+
99
+ function swiftSiblingDependencies(
100
+ files: Record<string, string>,
101
+ targets: SwiftPackageTarget[],
102
+ ): Map<string, string[]> {
103
+ const names = new Set(targets.map((target) => target.name))
104
+ const siblings = new Map<string, string[]>()
105
+ for (const target of targets) {
106
+ const prefix = `${target.path}/`
107
+ const deps = new Set<string>()
108
+ for (const [file, contents] of Object.entries(files)) {
109
+ if (file === 'Package.swift' || !file.startsWith(prefix)) continue
110
+ for (const match of contents.matchAll(/^\s*import\s+([A-Za-z_][A-Za-z0-9_]*)/gm)) {
111
+ if (names.has(match[1]) && match[1] !== target.name) deps.add(match[1])
112
+ }
113
+ }
114
+ siblings.set(target.name, [...deps].sort())
115
+ }
116
+ return siblings
117
+ }
118
+
119
+ /** bump with any change to the emitted multi-target manifest text. */
120
+ export const MULTI_MANIFEST_VERSION = 1
121
+
122
+ export function swiftArtifactHash(
123
+ input: SwiftCompileInput,
124
+ release: string,
125
+ moduleRoot: string,
126
+ ): string {
127
+ const hash = createHash('sha256')
128
+ hash.update(`swift-compile/v1\n${release}\n`)
129
+ hash.update(linkedModuleDigest(moduleRoot))
130
+ for (const file of Object.keys(input.files).sort()) {
131
+ hash.update(createHash('sha256').update(file).digest())
132
+ hash.update(createHash('sha256').update(input.files[file]).digest())
133
+ }
134
+ for (const base of Object.keys(input.resources ?? {}).sort()) {
135
+ hash.update(createHash('sha256').update(base).digest())
136
+ hash.update(
137
+ createHash('sha256')
138
+ .update(input.resources?.[base] ?? '')
139
+ .digest(),
140
+ )
141
+ }
142
+ for (const name of Object.keys(input.images ?? {}).sort()) {
143
+ const image = input.images?.[name]
144
+ if (!image) continue
145
+ hash.update(createHash('sha256').update(name).digest())
146
+ hash.update(createHash('sha256').update(image.file).digest())
147
+ hash.update(createHash('sha256').update(image.data).digest())
148
+ }
149
+ const targets = compileTargets(input)
150
+ if (targets.length > 0) {
151
+ hash.update(`swift-multi/v${MULTI_MANIFEST_VERSION}\n`)
152
+ hash.update(
153
+ JSON.stringify({
154
+ targets,
155
+ siblings: [...swiftSiblingDependencies(input.files, targets)].sort((a, b) =>
156
+ String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0,
157
+ ),
158
+ }),
159
+ )
160
+ }
161
+ return hash.digest('hex')
162
+ }
163
+
164
+ export function emitSwiftPackageManifest(args: {
165
+ targets: SwiftPackageTarget[]
166
+ siblings: Map<string, string[]>
167
+ moduleRoot: string
168
+ }): string {
169
+ const { targets, siblings, moduleRoot } = args
170
+ const executable = targets.find((target) => target.executable)
171
+ if (!executable) throw new Error('swift-compile: no executable target to emit')
172
+ const swiftSettings =
173
+ 'swiftSettings: [.swiftLanguageMode(.v5), .unsafeFlags(["-Osize"])]'
174
+ const stanza = (target: SwiftPackageTarget): string => {
175
+ const deps = [
176
+ '.product(name: "SwiftUI", package: "sootsim-swift")',
177
+ ...(siblings.get(target.name) ?? []).map((sibling) => `"${sibling}"`),
178
+ ].join(', ')
179
+ const kind = target.executable ? '.executableTarget' : '.target'
180
+ const link = target.executable
181
+ ? `,
182
+ linkerSettings: [
183
+ .unsafeFlags([
184
+ "-Xclang-linker", "-mexec-model=reactor",
185
+ "-Xlinker", "--export-if-defined=__main_argc_argv",
186
+ ]),
187
+ .unsafeFlags(["-Xlinker", "--strip-all"], .when(configuration: .release)),
188
+ ]`
189
+ : ''
190
+ return [
191
+ ` ${kind}(`,
192
+ ` name: "${target.name}",`,
193
+ ` dependencies: [${deps}],`,
194
+ ` path: "${target.path}",`,
195
+ ` ${swiftSettings}${link}`,
196
+ ` ),`,
197
+ ].join('\n')
198
+ }
199
+ return [
200
+ '// swift-tools-version: 6.0',
201
+ 'import PackageDescription',
202
+ '',
203
+ '// generated by swift-compile from the project package partition.',
204
+ 'let package = Package(',
205
+ ' name: "app",',
206
+ ` products: [.executable(name: "${executable.name}", targets: ["${executable.name}"])],`,
207
+ ` dependencies: [.package(path: "${moduleRoot}")],`,
208
+ ' targets: [',
209
+ ...targets.map(stanza),
210
+ ' ]',
211
+ ')',
212
+ '',
213
+ ].join('\n')
214
+ }
215
+
216
+ export function writeSwiftProject(
217
+ layout: SwiftProjectLayout,
218
+ input: SwiftCompileInput,
219
+ moduleRoot: string,
220
+ ): void {
221
+ const targets = compileTargets(input)
222
+ if (targets.length === 0) {
223
+ rmSync(layout.sources, { recursive: true, force: true })
224
+ for (const [file, contents] of Object.entries(input.files)) {
225
+ const target = path.join(layout.sources, file)
226
+ mkdirSync(path.dirname(target), { recursive: true })
227
+ writeFileSync(target, contents, 'utf8')
228
+ }
229
+ const manifest = readFileSync(
230
+ path.join(moduleRoot, 'scripts/contrast-app-Package.swift'),
231
+ 'utf8',
232
+ ).replaceAll('__SOOTSIM_SWIFT_PACKAGE__', moduleRoot)
233
+ writeFileSync(path.join(layout.root, 'Package.swift'), manifest, 'utf8')
234
+ return
235
+ }
236
+ mkdirSync(layout.root, { recursive: true })
237
+ for (const child of readdirSync(layout.root)) {
238
+ if (child === '.build') continue
239
+ rmSync(path.join(layout.root, child), { recursive: true, force: true })
240
+ }
241
+ for (const [file, contents] of Object.entries(input.files)) {
242
+ if (file === 'Package.swift') continue
243
+ const target = path.join(layout.root, file)
244
+ mkdirSync(path.dirname(target), { recursive: true })
245
+ writeFileSync(target, contents, 'utf8')
246
+ }
247
+ writeFileSync(
248
+ path.join(layout.root, 'Package.swift'),
249
+ emitSwiftPackageManifest({
250
+ targets,
251
+ siblings: swiftSiblingDependencies(input.files, targets),
252
+ moduleRoot,
253
+ }),
254
+ 'utf8',
255
+ )
256
+ }
257
+
258
+ function streamText(value: unknown): string {
259
+ if (typeof value === 'string') return value.trim()
260
+ if (Buffer.isBuffer(value)) return value.toString('utf8').trim()
261
+ return ''
262
+ }
263
+
264
+ function outputOf(error: unknown): string {
265
+ if (error && typeof error === 'object') {
266
+ const streams = [
267
+ streamText(Reflect.get(error, 'stdout')),
268
+ streamText(Reflect.get(error, 'stderr')),
269
+ ].filter(Boolean)
270
+ if (streams.length > 0) return streams.join('\n')
271
+ }
272
+ return error instanceof Error ? error.message : String(error)
273
+ }
274
+
275
+ function builtArtifact(layout: SwiftProjectLayout): string {
276
+ const binDir = path.join(layout.root, '.build', WASM_TRIPLE, 'release')
277
+ const artifacts = existsSync(binDir)
278
+ ? readdirSync(binDir)
279
+ .filter((name) => name.endsWith('.wasm'))
280
+ .sort()
281
+ : []
282
+ if (artifacts.length !== 1) {
283
+ throw new Error(
284
+ `swift-compile: the build produced ${artifacts.length} wasm artifacts; exactly one executable is the app`,
285
+ )
286
+ }
287
+ return path.join(binDir, artifacts[0])
288
+ }
289
+
290
+ function projectDiagnostic(output: string, layout: SwiftProjectLayout): string {
291
+ const base = layout.multi ? layout.root : layout.sources
292
+ const prefix = `${base}${path.sep}`
293
+ return output
294
+ .split('\n')
295
+ .map((line) => (line.startsWith(prefix) ? line.slice(prefix.length) : line))
296
+ .join('\n')
297
+ .trim()
298
+ }
299
+
300
+ const ROOT_LOCKS = Symbol.for('peachsim.swiftCompile.rootLocks')
301
+ const host = globalThis as typeof globalThis & {
302
+ [ROOT_LOCKS]?: Map<string, Promise<unknown>>
303
+ }
304
+ const rootLocks = (host[ROOT_LOCKS] ??= new Map())
305
+
306
+ function serialize<T>(root: string, work: () => Promise<T>): Promise<T> {
307
+ const current = rootLocks.get(root) ?? Promise.resolve()
308
+ const next = current.then(work, work)
309
+ rootLocks.set(
310
+ root,
311
+ next.catch(() => {}),
312
+ )
313
+ return next
314
+ }
315
+
316
+ /** the one local compiler used by the route host and Vite host. */
317
+ export function compileSwiftProjectUnit(
318
+ input: SwiftCompileInput,
319
+ options: SwiftCompileOptions,
320
+ ): Promise<SwiftCompileArtifact> {
321
+ mkdirSync(options.buildRoot, { recursive: true })
322
+ const root = realpathSync(options.buildRoot)
323
+ const moduleRoot = path.resolve(options.moduleRoot)
324
+ const layout: SwiftProjectLayout = {
325
+ root,
326
+ sources: path.join(root, 'Sources', 'App'),
327
+ multi: compileTargets(input).length > 0,
328
+ }
329
+ const hash = swiftArtifactHash(input, options.release, moduleRoot)
330
+ return serialize(root, async () => {
331
+ writeSwiftProject(layout, input, moduleRoot)
332
+ const swift = path.join(options.toolchainBin, 'swift')
333
+ if (!existsSync(swift)) {
334
+ throw new Error(
335
+ `swift-compile: no swift.org toolchain at ${options.toolchainBin}; install ${options.release} and its matching wasm sdk`,
336
+ )
337
+ }
338
+ try {
339
+ await run(
340
+ swift,
341
+ [
342
+ 'build',
343
+ '--package-path',
344
+ root,
345
+ '--swift-sdk',
346
+ `${options.release}_wasm`,
347
+ '-c',
348
+ 'release',
349
+ '-Xswiftc',
350
+ '-Osize',
351
+ ],
352
+ { maxBuffer: 64 * 1024 * 1024, timeout: options.timeoutMs },
353
+ )
354
+ } catch (error) {
355
+ throw new Error(projectDiagnostic(outputOf(error), layout), { cause: error })
356
+ }
357
+ return { hash, bytes: readFileSync(builtArtifact(layout)) }
358
+ })
359
+ }
@@ -0,0 +1,69 @@
1
+ /** one SwiftPM target a sent root manifest declares. */
2
+ export interface SwiftPackageTarget {
3
+ /** the target name, as `import <name>` spells it. */
4
+ name: string
5
+ /** project-relative directory holding the target's sources. */
6
+ path: string
7
+ /** true for `.executableTarget`: the one that links the wasm artifact. */
8
+ executable: boolean
9
+ }
10
+
11
+ /** whether a project-relative key is a SwiftPM manifest, not a source. */
12
+ export function isSwiftManifestFile(file: string): boolean {
13
+ const segments = file.split('/')
14
+ return segments[segments.length - 1] === 'Package.swift'
15
+ }
16
+
17
+ /**
18
+ * the library and executable targets a sent root manifest declares, in order.
19
+ *
20
+ * A manifest is Swift code, not data, so this is a scanner, not a parser:
21
+ * each `.target(` / `.executableTarget(` call is matched to its balanced
22
+ * close paren (string-aware, so a paren inside a literal cannot end it
23
+ * early), and `name:` / `path:` are read from the call's own depth only, so
24
+ * a nested `.product(name: "SwiftUI", ...)` inside dependencies never wins.
25
+ */
26
+ export function swiftManifestTargets(manifest: string): SwiftPackageTarget[] {
27
+ const code = manifest.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '')
28
+ const targets: SwiftPackageTarget[] = []
29
+ const pattern = /\.(executableTarget|target)\s*\(/g
30
+ let match: RegExpExecArray | null
31
+ while ((match = pattern.exec(code)) !== null) {
32
+ const executable = match[1] === 'executableTarget'
33
+ let depth = 1
34
+ let i = pattern.lastIndex
35
+ let inString = false
36
+ let escaped = false
37
+ let flat = ''
38
+ while (i < code.length && depth > 0) {
39
+ const ch = code[i]
40
+ if (inString) {
41
+ if (escaped) escaped = false
42
+ else if (ch === '\\') escaped = true
43
+ else if (ch === '"') inString = false
44
+ if (depth === 1) flat += ch
45
+ } else if (ch === '"') {
46
+ inString = true
47
+ if (depth === 1) flat += ch
48
+ } else if (ch === '(') {
49
+ depth++
50
+ if (depth === 1) flat += ch
51
+ } else if (ch === ')') {
52
+ depth--
53
+ if (depth >= 1) flat += ch
54
+ } else if (depth === 1) {
55
+ flat += ch
56
+ }
57
+ i++
58
+ }
59
+ const name = /\bname\s*:\s*"([^"]+)"/.exec(flat)?.[1]
60
+ if (!name) continue
61
+ const rawPath = /\bpath\s*:\s*"([^"]*)"/.exec(flat)?.[1]
62
+ const targetPath =
63
+ rawPath == null || rawPath === ''
64
+ ? `Sources/${name}`
65
+ : rawPath.replace(/^\.\//, '').replace(/\/+$/, '')
66
+ targets.push({ name, path: targetPath, executable })
67
+ }
68
+ return targets
69
+ }
@@ -7,8 +7,9 @@
7
7
  * its own origin's `/api/swift-compile`), the node bundle builder used by
8
8
  * the test-drive CLI and the deploy publisher (posts an absolute endpoint),
9
9
  * and the One/rolldown `.swift` transform (same, with a `ccdn_` credential).
10
- * A caller never compiles in-process: the container is the one compiler, and
11
- * no bundler is that instance.
10
+ * Remote callers never compile in-process: the container is the compiler for
11
+ * this path. The local Vite host uses the same project-unit compiler directly
12
+ * and does not pass through this HTTP adapter.
12
13
  *
13
14
  * It lives here (rather than in the app's `src/bundler/`) because the One
14
15
  * transform ships in this package and cannot import app code; the app's own
@@ -3,7 +3,7 @@
3
3
  // bound to that directory across an active-runtime update.
4
4
  //
5
5
  // usage in vite.config.ts:
6
- // import { rnxPlugin } from 'rnxsim/vite'
6
+ // import { rnxPlugin } from 'peachsim/vite'
7
7
  // export default { plugins: [one(), rnxPlugin()] }
8
8
 
9
9
  import fs from 'fs'