peachsim 0.1.2179 → 0.1.2182

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 (104) hide show
  1. package/cli/commands/package.ts +7 -2
  2. package/cli/commands/shell-url.ts +28 -22
  3. package/dist-lib/agent-daemon-client.cjs +1 -1
  4. package/dist-lib/agent-events.cjs +1 -1
  5. package/dist-lib/attached-projects.cjs +1 -1
  6. package/dist-lib/auth/shared-session.cjs +1 -1
  7. package/dist-lib/backend-origin.cjs +1 -1
  8. package/dist-lib/beta.cjs +1 -1
  9. package/dist-lib/beta.mjs +1 -1
  10. package/dist-lib/bridge-constants.cjs +1 -1
  11. package/dist-lib/bridge-contract-input.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.mjs +1 -1
  13. package/dist-lib/bridge-contract.cjs +1 -1
  14. package/dist-lib/bridge-contract.mjs +1 -1
  15. package/dist-lib/bundle-channel.cjs +225 -85
  16. package/dist-lib/capture-contract.cjs +1 -1
  17. package/dist-lib/capture-contract.mjs +1 -1
  18. package/dist-lib/cloud-contract.cjs +1 -1
  19. package/dist-lib/cloud-contract.mjs +1 -1
  20. package/dist-lib/cloud-test-args.cjs +1 -1
  21. package/dist-lib/cloud-test-args.mjs +1 -1
  22. package/dist-lib/cloud.cjs +1 -1
  23. package/dist-lib/cloud.mjs +1 -1
  24. package/dist-lib/config.cjs +1 -1
  25. package/dist-lib/config.mjs +1 -1
  26. package/dist-lib/detox/index.cjs +2 -2
  27. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  28. package/dist-lib/expo-swift-bundle.cjs +140 -48
  29. package/dist-lib/expo-swift-metadata.cjs +1 -1
  30. package/dist-lib/expo-swift-metadata.mjs +1 -1
  31. package/dist-lib/home-paths.cjs +1 -1
  32. package/dist-lib/host/bridge-host.cjs +201 -82
  33. package/dist-lib/host/fetch-proxy-handler.cjs +4 -4
  34. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  36. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  37. package/dist-lib/host/websocket-proxy.cjs +1 -1
  38. package/dist-lib/index.cjs +4 -4
  39. package/dist-lib/jump-to-source-babel.cjs +1 -1
  40. package/dist-lib/jump-to-source-native.cjs +1 -1
  41. package/dist-lib/kotlin-host.cjs +11 -3
  42. package/dist-lib/menu.cjs +1 -1
  43. package/dist-lib/menu.mjs +1 -1
  44. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  45. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  46. package/dist-lib/metro-production-bundle.cjs +1 -1
  47. package/dist-lib/metro-production-bundle.mjs +1 -1
  48. package/dist-lib/metro.cjs +4 -4
  49. package/dist-lib/native-app-identity-project.cjs +2 -2
  50. package/dist-lib/native-app-identity-project.mjs +2 -2
  51. package/dist-lib/native-app-identity.cjs +1 -1
  52. package/dist-lib/native-app-identity.mjs +1 -1
  53. package/dist-lib/native-firebase-metadata.cjs +1 -1
  54. package/dist-lib/native-firebase-metadata.mjs +1 -1
  55. package/dist-lib/pdfium-raster.cjs +1 -1
  56. package/dist-lib/pdfium-raster.mjs +1 -1
  57. package/dist-lib/plist.cjs +1 -1
  58. package/dist-lib/plist.mjs +1 -1
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/public-origins-lib-esm.json +24 -24
  62. package/dist-lib/public-origins-lib.json +53 -53
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/reap-browser.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  67. package/dist-lib/sdk.cjs +1 -1
  68. package/dist-lib/sdk.mjs +1 -1
  69. package/dist-lib/skills.cjs +239 -176
  70. package/dist-lib/swift-compile.cjs +85 -16
  71. package/dist-lib/swift-compile.mjs +85 -16
  72. package/dist-lib/swift-package.cjs +1 -1
  73. package/dist-lib/swift-package.mjs +1 -1
  74. package/dist-lib/swift-submit.cjs +2 -2
  75. package/dist-lib/swift-submit.mjs +2 -2
  76. package/dist-lib/swift-toolchain.cjs +1 -1
  77. package/dist-lib/swift.cjs +1 -1
  78. package/dist-lib/vite-swift.cjs +87 -18
  79. package/dist-lib/vite.cjs +4 -4
  80. package/dist-lib/worklet-executable.cjs +1 -1
  81. package/dist-lib/worklet-executable.mjs +1 -1
  82. package/dist-types/packages/peach-engine/src/cli-companion.d.ts +1 -0
  83. package/dist-types/packages/peach-engine/src/ios/frame-art.d.ts +41 -0
  84. package/dist-types/packages/peach-engine/src/ios/frame-paint.d.ts +43 -0
  85. package/dist-types/packages/peach-engine/src/ios/frame-svg.d.ts +4 -0
  86. package/dist-types/packages/peach-engine/src/render-worker/map-command-channel.d.ts +14 -0
  87. package/kotlin/sdk.tar.gz +0 -0
  88. package/package.json +1 -1
  89. package/runtime-companion.json +1 -1
  90. package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UIScrollView.h +8 -25
  91. package/sdk/ios/System/Library/Frameworks/UIKitTypes.framework/Headers/UIKitTypes.h +26 -0
  92. package/src/expo-native-workspace.ts +17 -20
  93. package/src/expo-swift-bundle.ts +6 -1
  94. package/src/expo-swift-host.ts +35 -6
  95. package/src/expo-widgets-bundle.ts +12 -8
  96. package/src/expoSwiftContract.ts +3 -0
  97. package/src/kotlin-host.ts +10 -2
  98. package/src/native-bundles.ts +30 -10
  99. package/src/one-sdk/source.ts +10 -4
  100. package/src/one-sdk-bundle.ts +57 -24
  101. package/src/screenshots/frame-compose.ts +14 -109
  102. package/src/swift-compile.ts +105 -13
  103. package/src/swift-submit.ts +2 -1
  104. package/src/wasm-optimize.ts +13 -4
@@ -1999,7 +1999,16 @@ function targetSettings(units: ExpoSwiftSourceUnit[]) {
1999
1999
  return settings
2000
2000
  }
2001
2001
 
2002
- function expoEntry(units: ExpoSwiftSourceUnit[]): string {
2002
+ function oneSDKStamp(oneSDK: ExpoSwiftPreparationOptions['oneSDK']): string {
2003
+ if (!oneSDK) return ''
2004
+ const hash = createHash('sha256')
2005
+ for (const files of [oneSDK.targets, oneSDK.entry])
2006
+ for (const [name, source] of [...files].sort(([a], [b]) => a.localeCompare(b)))
2007
+ hash.update(`${name}\0${source}\0`)
2008
+ return hash.digest('hex')
2009
+ }
2010
+
2011
+ function expoEntry(units: ExpoSwiftSourceUnit[], oneSDK = false): string {
2003
2012
  // an Objective-C bridge module registers with no Swift of its own
2004
2013
  const namespaces = [...new Set(units.map((unit) => unit.namespace))]
2005
2014
  .filter((namespace) => namespace !== 'PeachExpoApp')
@@ -2015,7 +2024,11 @@ function expoEntry(units: ExpoSwiftSourceUnit[]): string {
2015
2024
  subscribers.length + handlers.length > 0
2016
2025
  ? `; PeachExpoRuntime.registerDelegates(subscribers: [${subscribers.join(', ')}], handlers: [${handlers.join(', ')}])`
2017
2026
  : ''
2018
- return `import SwiftUI\ninternal import ExpoModulesCore\n${bridgeModules.length ? 'import React\n' : ''}${namespaces.map((namespace) => `import ${namespace}`).join('\n')}\n\n@main struct PeachExpoApp: PeachPackage {\n init() { MainActor.assumeIsolated { PeachExpoRuntime.register([${units.flatMap((unit) => unit.classes).join(', ')}])${delegates}${bridgeModules.length ? `; RCTModuleHost.register([${bridgeModules.join(', ')}])` : ''} } }\n func view(props: JSON) -> PeachExpoNativeView { PeachExpoNativeView(props: props) }\n}\n\n@_expose(wasm, "peach_expo_call")\n@_cdecl("peach_expo_call")\n@MainActor public func peachExpoCall(_ payload: UnsafePointer<UInt8>?, _ length: Int32) -> UnsafeMutablePointer<UInt8>? {\n PeachExpoRuntime.dispatch(payload, length)\n}\n`
2027
+ // a One SDK host component marks its view; every other view is an Expo view
2028
+ const view = oneSDK
2029
+ ? '@ViewBuilder func view(props: JSON) -> some View {\n if props["oneSDKHost"] != nil { OneSDKUIKitHost(props: props) } else { PeachExpoNativeView(props: props) }\n }'
2030
+ : 'func view(props: JSON) -> PeachExpoNativeView { PeachExpoNativeView(props: props) }'
2031
+ return `import SwiftUI\ninternal import ExpoModulesCore\n${bridgeModules.length ? 'import React\n' : ''}${oneSDK ? 'import One\nimport OneSDKBindings\n' : ''}${namespaces.map((namespace) => `import ${namespace}`).join('\n')}\n\n@main struct PeachExpoApp: PeachPackage {\n init() { MainActor.assumeIsolated { PeachExpoRuntime.register([${units.flatMap((unit) => unit.classes).join(', ')}])${delegates}${bridgeModules.length ? `; RCTModuleHost.register([${bridgeModules.join(', ')}])` : ''} } }\n ${view}\n}\n\n@_expose(wasm, "peach_expo_call")\n@_cdecl("peach_expo_call")\n@MainActor public func peachExpoCall(_ payload: UnsafePointer<UInt8>?, _ length: Int32) -> UnsafeMutablePointer<UInt8>? {\n PeachExpoRuntime.dispatch(payload, length)\n}\n`
2019
2032
  }
2020
2033
 
2021
2034
  // a compile outlives its client: a dropped open leaves the work running so the
@@ -2153,7 +2166,7 @@ export async function compileExpoSwiftModules(
2153
2166
  installSwiftInputTrace()
2154
2167
  let key: string
2155
2168
  try {
2156
- key = `${await discoveryKey(root, options.installedWorkspace, true, options.iosTarget, options.moduleRoot)}\0${options.only?.join(',') ?? ''}`
2169
+ key = `${await discoveryKey(root, options.installedWorkspace, true, options.iosTarget, options.moduleRoot)}\0${options.only?.join(',') ?? ''}\0${oneSDKStamp(options.oneSDK)}`
2157
2170
  } catch {
2158
2171
  return compileExpoSwiftModulesTraced(root, base, publishArtifact, options)
2159
2172
  }
@@ -2406,11 +2419,23 @@ async function compileExpoSwiftModulesTraced(
2406
2419
  throw new Error(`conflicting Expo Swift binary source ${name}`)
2407
2420
  files[name] = source
2408
2421
  }
2409
- files[EXPO_SWIFT_ENTRY] = expoEntry(linked)
2422
+ const settings = targetSettings(linked)
2423
+ if (options.oneSDK) {
2424
+ for (const [name, source] of options.oneSDK.targets) files[name] = source
2425
+ for (const [name, source] of options.oneSDK.entry)
2426
+ files[`Sources/PeachExpoApp/${name}`] = source
2427
+ settings.set('OneSDKBindings', {
2428
+ languageMode: 5,
2429
+ mainActorIsolation: false,
2430
+ swiftFlags: ['-suppress-warnings'],
2431
+ dependencies: [{ kind: 'target', name: 'One' }],
2432
+ })
2433
+ }
2434
+ files[EXPO_SWIFT_ENTRY] = expoEntry(linked, options.oneSDK !== undefined)
2410
2435
  files['Package.swift'] = expoManifest(
2411
2436
  files,
2412
2437
  linked.map((unit) => generations.get(unit) ?? '').join(','),
2413
- targetSettings(linked),
2438
+ settings,
2414
2439
  )
2415
2440
  const provider = linked.some((unit) => unit.reactNativeProvider)
2416
2441
  ? reactProvider
@@ -2648,7 +2673,11 @@ async function compileExpoSwiftModulesTraced(
2648
2673
  stamps.set(unit, stamp)
2649
2674
  candidates.push(unit)
2650
2675
  }
2651
- const preparedKey = expoSwiftPreparedKey(base, units, reactProviderStamp)
2676
+ const preparedKey = expoSwiftPreparedKey(
2677
+ base,
2678
+ units,
2679
+ reactProviderStamp + oneSDKStamp(options.oneSDK),
2680
+ )
2652
2681
  if (!publishArtifact) {
2653
2682
  const prepared = preparedExpoSwift.get(preparedKey)
2654
2683
  if (prepared) {
@@ -5,6 +5,7 @@ import path from 'node:path'
5
5
  import { promisify } from 'node:util'
6
6
  import { stageExpoWidgetAssets } from './expo-widget-assets'
7
7
  import { compiledModuleRequestHandler } from './nitro-bundle'
8
+ import { withSwiftRootLock } from './swift-root-lock'
8
9
  import type { ExpoWidgetExtension } from '../../peach-engine/src/render-worker/expo-widgetContract'
9
10
 
10
11
  export const EXPO_WIDGETS_ROUTE = '/__peach/expo-widgets'
@@ -29,18 +30,21 @@ export async function buildExpoWidgetsBundle(
29
30
  // output path. 58 reads an optional platform and output and otherwise writes
30
31
  // the same package bundle, so one root argument covers both.
31
32
  const packageDir = path.dirname(manifest)
32
- await promisify(execFile)(
33
- process.env.NODE_BINARY || 'node',
34
- [path.join(packageDir, 'scripts/build-bundle.mjs'), appRoot],
35
- { cwd: appRoot, maxBuffer: 16 * 1024 * 1024 },
33
+ const runtimeBundle = await withSwiftRootLock(
34
+ path.join(await fs.realpath(packageDir), 'bundle'),
35
+ async () => {
36
+ await promisify(execFile)(
37
+ process.env.NODE_BINARY || 'node',
38
+ [path.join(packageDir, 'scripts/build-bundle.mjs'), appRoot],
39
+ { cwd: appRoot, maxBuffer: 16 * 1024 * 1024 },
40
+ )
41
+ return fs.readFile(path.join(packageDir, 'bundle/build/ExpoWidgets.bundle'), 'utf8')
42
+ },
36
43
  )
37
44
  const catalog = await stageExpoWidgetAssets(appRoot, origin)
38
45
  return {
39
46
  metadata: {
40
- runtimeBundle: await fs.readFile(
41
- path.join(packageDir, 'bundle/build/ExpoWidgets.bundle'),
42
- 'utf8',
43
- ),
47
+ runtimeBundle,
44
48
  images: catalog?.images ?? {},
45
49
  },
46
50
  }
@@ -5,6 +5,7 @@ import {
5
5
  import { isRecord } from './is-record'
6
6
  import { isNativeAppIdentity, type NativeAppIdentity } from './native-app-identity'
7
7
  import { isSwiftResourcePath } from './swiftContract'
8
+ import type { OneSDKLinkSources } from './one-sdk-bundle'
8
9
  import type {
9
10
  SwiftNativeTarget,
10
11
  SwiftHeaderAliases,
@@ -19,6 +20,8 @@ export interface ExpoSwiftPreparationOptions {
19
20
  moduleRoot?: string
20
21
  // compile only these Swift modules and the modules they import
21
22
  only?: readonly string[]
23
+ // the app's One SDK, linked into the same executable: one process per app
24
+ oneSDK?: Pick<OneSDKLinkSources, 'targets' | 'entry'>
22
25
  }
23
26
 
24
27
  export interface ExpoSwiftSourceOptions {
@@ -131,7 +131,11 @@ function entrySource(ids: string[]): string {
131
131
  '',
132
132
  '@OptIn(kotlin.js.ExperimentalJsExport::class)',
133
133
  '@JsExport',
134
- 'fun peachSetAndroidBuildInfo(model: String, release: String, apiLevel: Int) = Build.setDevice(model, release, apiLevel)',
134
+ 'fun peachSetAndroidDeviceInfo(model: String, release: String, apiLevel: Int, locale: String, timeZone: String) {',
135
+ ' Build.setDevice(model, release, apiLevel)',
136
+ ' java.util.Locale.setDefault(java.util.Locale.forLanguageTag(locale))',
137
+ ' java.util.TimeZone.setDevice(timeZone)',
138
+ '}',
135
139
  '',
136
140
  '@OptIn(kotlin.js.ExperimentalJsExport::class, kotlinx.coroutines.DelicateCoroutinesApi::class)',
137
141
  '@JsExport',
@@ -726,8 +730,12 @@ export async function expoKotlinArtifact(
726
730
  ' ))',
727
731
  '}',
728
732
  '',
729
- '@JsExport fun peachSetAndroidBuildInfo(model: String, release: String, apiLevel: Int) =',
733
+ '@JsExport fun peachSetAndroidDeviceInfo(model: String, release: String, apiLevel: Int, locale: String, timeZone: String) {',
730
734
  ' android.os.Build.setDevice(model, release, apiLevel)',
735
+ ' java.util.Locale.setDefault(java.util.Locale.forLanguageTag(locale))',
736
+ ' java.util.TimeZone.setDevice(timeZone)',
737
+ ' PeachExpoRuntime.configureProcessLocale(locale)',
738
+ '}',
731
739
  '',
732
740
  '@JsExport fun peachExpoListen(notify: () -> Unit) {',
733
741
  ' registered',
@@ -47,17 +47,37 @@ export async function buildNativeBundles(
47
47
  configuration: 'Debug' | 'Release',
48
48
  ): Promise<NativeBundles> {
49
49
  return getRuntimePlatformAdapter(platform).select<() => Promise<NativeBundles>>({
50
- ios: async () => ({
51
- oneSDK: await (await import('./one-sdk-bundle')).buildOneSDKBundle(projectRoot),
52
- expoSwift: await (
50
+ ios: async () => {
51
+ const { appOneSDKLinkSources, buildOneSDKBundle } = await import('./one-sdk-bundle')
52
+ // on iOS an app is one process: its One SDK links into the Expo executable
53
+ // when the app has one, and its process serves both
54
+ const oneSDK = await appOneSDKLinkSources(projectRoot)
55
+ const expoSwift = await (
53
56
  await import('./expo-swift-bundle')
54
- ).buildExpoSwiftBundle(projectRoot),
55
- nitro: await (
56
- await import('./nitro-bundle')
57
- ).buildNitroBundle(projectRoot, configuration),
58
- turbo: await (await import('./turbo-bundle')).buildTurboBundle(projectRoot),
59
- expoKotlin: null,
60
- }),
57
+ ).buildExpoSwiftBundle(projectRoot, oneSDK ?? undefined)
58
+ const linked = oneSDK && expoSwift?.metadata.modules[0]
59
+ return {
60
+ oneSDK: linked
61
+ ? {
62
+ metadata: {
63
+ url: linked.url,
64
+ hash: linked.hash,
65
+ icuUrl: linked.icuUrl,
66
+ rootId: `${linked.rootId.split('/')[0]}/OneSDK`,
67
+ bundleFiles: linked.bundleFiles ?? {},
68
+ modules: oneSDK.modules,
69
+ },
70
+ assets: [],
71
+ }
72
+ : await buildOneSDKBundle(projectRoot),
73
+ expoSwift,
74
+ nitro: await (
75
+ await import('./nitro-bundle')
76
+ ).buildNitroBundle(projectRoot, configuration),
77
+ turbo: await (await import('./turbo-bundle')).buildTurboBundle(projectRoot),
78
+ expoKotlin: null,
79
+ }
80
+ },
61
81
  android: async () => ({
62
82
  oneSDK: null,
63
83
  expoSwift: null,
@@ -30,12 +30,13 @@ extension OneNativeComposable {
30
30
  }
31
31
  `
32
32
 
33
- export const oneSDKApp = String.raw`import SwiftUI
33
+ // the view a One SDK host component mounts, in whichever executable links the
34
+ // app's One SDK
35
+ export const oneSDKHost = String.raw`import SwiftUI
34
36
  @_spi(Peach) import UIKit
35
37
  import One
36
- import OneSDKBindings
37
38
 
38
- private struct UIKitHost: UIViewRepresentable {
39
+ struct OneSDKUIKitHost: UIViewRepresentable {
39
40
  let props: JSON
40
41
  func makeUIView(context: Context) -> OneNativeUIKitHostView { OneNativeUIKitHostView() }
41
42
  func updateUIView(_ view: OneNativeUIKitHostView, context: Context) {
@@ -47,10 +48,15 @@ private struct UIKitHost: UIViewRepresentable {
47
48
  }
48
49
  static func dismantleUIView(_ view: OneNativeUIKitHostView, coordinator: Coordinator) { view.reset() }
49
50
  }
51
+ `
52
+
53
+ export const oneSDKApp = String.raw`import SwiftUI
54
+ import One
55
+ import OneSDKBindings
50
56
 
51
57
  @main
52
58
  struct SDKGuest: PeachPackage {
53
- func view(props: JSON) -> some View { UIKitHost(props: props) }
59
+ func view(props: JSON) -> some View { OneSDKUIKitHost(props: props) }
54
60
  }
55
61
  `
56
62
 
@@ -4,7 +4,7 @@ import path from 'node:path'
4
4
  import { prepareExpoNativeProject } from './expo-native-workspace'
5
5
  import { compiledModuleAssets, compiledModuleRequestHandler } from './nitro-bundle'
6
6
  import { selectOneSDK } from './one-sdk-preparation'
7
- import { oneSDKApp, oneSDKModule, oneSDKTransport } from './one-sdk/source'
7
+ import { oneSDKApp, oneSDKHost, oneSDKModule, oneSDKTransport } from './one-sdk/source'
8
8
  import { ONE_SDK_SOURCE_DIRECTORY, readOneSDKSourceManifest } from './oneSdkContract'
9
9
  import { syncSwiftTree } from './swift-compile'
10
10
  import { resolveSwiftModule } from './swift-distribution'
@@ -24,12 +24,19 @@ export async function compileOneSDKApp(
24
24
  return compilePreparedOneSDKApp(appRoot, prepared.app, origin, moduleRootOverride)
25
25
  }
26
26
 
27
- export async function compilePreparedOneSDKApp(
27
+ // the One and OneSDKBindings targets an executable links for the app's One SDK
28
+ // imports, the host view and transport its executable target adds, and the
29
+ // module names the bundle registers
30
+ export interface OneSDKLinkSources {
31
+ targets: Map<string, string>
32
+ entry: Map<string, string>
33
+ modules: string[]
34
+ }
35
+
36
+ export async function oneSDKLinkSources(
28
37
  appRoot: string,
29
38
  preparedRoot: string,
30
- origin: string,
31
- moduleRootOverride?: string,
32
- ) {
39
+ ): Promise<OneSDKLinkSources> {
33
40
  const selected = await selectOneSDK(appRoot)
34
41
  if (!selected) throw new Error('prepared app has no One SDK imports')
35
42
  const { one, frameworks } = selected
@@ -47,6 +54,47 @@ export async function compilePreparedOneSDKApp(
47
54
  throw new Error(
48
55
  'prepared One SDK sources differ from the installed facades or app imports',
49
56
  )
57
+ const targets = new Map<string, string>()
58
+ targets.set('Sources/One/One.swift', oneSDKModule)
59
+ for (const source of [
60
+ 'SDK/OneSDKRuntime.swift',
61
+ 'OneNativeUIKitHostView.swift',
62
+ 'OneNativeContent.swift',
63
+ 'OneNativeAccessibility.swift',
64
+ 'SDKViews/OneNativeViewsTable.swift',
65
+ ])
66
+ targets.set(
67
+ `Sources/One/${path.basename(source)}`,
68
+ fs.readFileSync(path.join(one, 'ios', source), 'utf8'),
69
+ )
70
+ for (const name of generated.files)
71
+ targets.set(
72
+ `Sources/OneSDKBindings/${name}`,
73
+ fs.readFileSync(path.join(sourceRoot, name), 'utf8'),
74
+ )
75
+ return {
76
+ targets,
77
+ entry: new Map([
78
+ ['OneSDKHost.swift', oneSDKHost],
79
+ ['OneSDKTransport.swift', oneSDKTransport],
80
+ ]),
81
+ modules: frameworks.map((name) => `${name}_${generated.generation}`),
82
+ }
83
+ }
84
+
85
+ // the app's One SDK sources, when it imports One's SDK
86
+ export async function appOneSDKLinkSources(appRoot: string) {
87
+ if (!(await selectOneSDK(appRoot))) return null
88
+ return oneSDKLinkSources(appRoot, (await prepareExpoNativeProject(appRoot)).app)
89
+ }
90
+
91
+ export async function compilePreparedOneSDKApp(
92
+ appRoot: string,
93
+ preparedRoot: string,
94
+ origin: string,
95
+ moduleRootOverride?: string,
96
+ ) {
97
+ const sources = await oneSDKLinkSources(appRoot, preparedRoot)
50
98
  const moduleRoot = moduleRootOverride ?? (await resolveSwiftModule()).root
51
99
  const packagePath = path.join(
52
100
  swiftCacheDir(),
@@ -66,25 +114,9 @@ let package = Package(name: "OneSDKGuest", targets: [
66
114
  ])
67
115
  `,
68
116
  )
69
- files.set('Sources/One/One.swift', oneSDKModule)
70
- for (const source of [
71
- 'SDK/OneSDKRuntime.swift',
72
- 'OneNativeUIKitHostView.swift',
73
- 'OneNativeContent.swift',
74
- 'OneNativeAccessibility.swift',
75
- 'SDKViews/OneNativeViewsTable.swift',
76
- ])
77
- files.set(
78
- `Sources/One/${path.basename(source)}`,
79
- fs.readFileSync(path.join(one, 'ios', source), 'utf8'),
80
- )
81
- for (const name of generated.files)
82
- files.set(
83
- `Sources/OneSDKBindings/${name}`,
84
- fs.readFileSync(path.join(sourceRoot, name), 'utf8'),
85
- )
117
+ for (const [name, source] of sources.targets) files.set(name, source)
118
+ for (const [name, source] of sources.entry) files.set(`Sources/App/${name}`, source)
86
119
  files.set('Sources/App/App.swift', oneSDKApp)
87
- files.set('Sources/App/Transport.swift', oneSDKTransport)
88
120
  fs.mkdirSync(packagePath, { recursive: true })
89
121
  syncSwiftTree(
90
122
  packagePath,
@@ -92,6 +124,7 @@ let package = Package(name: "OneSDKGuest", targets: [
92
124
  )
93
125
  const project = projectFor(packagePath)
94
126
  project.oneModule = false
127
+ project.optimize = true
95
128
  const artifact = await artifactFor(project, 'Sources/App/App.swift', moduleRoot)
96
129
  const metadata: OneSDKCompilation = {
97
130
  url: `${origin}/__peach/swift/${artifact.hash}.wasm`,
@@ -99,7 +132,7 @@ let package = Package(name: "OneSDKGuest", targets: [
99
132
  icuUrl: icuUrl(origin, artifact),
100
133
  rootId: `one-sdk-${artifact.hash.slice(0, 16)}`,
101
134
  bundleFiles: artifact.package.bundleFiles.files,
102
- modules: frameworks.map((name) => `${name}_${generated.generation}`),
135
+ modules: sources.modules,
103
136
  }
104
137
  return { metadata, artifact }
105
138
  }
@@ -2,9 +2,8 @@ import { launchProvisionedPlaywright } from '../../cli/drivers/playwright-provis
2
2
  import { getCliRuntime } from '../cli-runtime.ts'
3
3
  import { cornerRadii, type CornerRadii, type DeviceModel } from '../device-catalog.ts'
4
4
  import type { HousingLayer } from '#peach-cli-contract/ios/duo-housing'
5
+ import type { FramePaintSpec } from '#peach-cli-contract/ios/frame-paint'
5
6
 
6
- const METALLIC_RING_SHADOW =
7
- 'inset 0 0 0 0.5px #000, inset 0 0 0 2px #757575, inset 0 0 0 5px #212121'
8
7
  const FRAME_OUTLINE_BLEED = 1
9
8
 
10
9
  type FrameButtonLayout = {
@@ -40,9 +39,7 @@ export interface FramedScreenshotLayout {
40
39
  logicalFrameRadius: CornerRadii
41
40
  // folding body art, painted before the screen; null draws one black body
42
41
  housingLayers: HousingLayer[] | null
43
- frameBackground: string
44
42
  frameOutline: string
45
- metallicRingShadow: string | null
46
43
  buttons: FrameButtonLayout[]
47
44
  legacyHomeButton: LegacyHomeButtonLayout | null
48
45
  showHomeIndicator: boolean
@@ -149,17 +146,11 @@ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotL
149
146
  housingLayers,
150
147
  // the housing paints its own body layer, shadow included: its frame box is
151
148
  // wider than the body, because the hinge barrel protrudes past the spine.
152
- frameBackground: isLegacyHardware
153
- ? 'linear-gradient(180deg, #1b1c20 0%, #0f1012 42%, #050608 100%)'
154
- : housingLayers
155
- ? 'transparent'
156
- : '#000000',
157
149
  frameOutline: isLegacyHardware
158
150
  ? '0 0 0 1px #1f2125'
159
151
  : housingLayers
160
152
  ? 'none'
161
153
  : '0 0 0 1px #333',
162
- metallicRingShadow: isLegacyHardware || housingLayers ? null : METALLIC_RING_SHADOW,
163
154
  buttons,
164
155
  legacyHomeButton: isLegacyHardware
165
156
  ? {
@@ -182,69 +173,22 @@ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotL
182
173
  }
183
174
  }
184
175
 
185
- function renderSideButtonHtml(button: FrameButtonLayout): string {
186
- const isRight = button.side === 'right'
187
- const isBottom = button.side === 'bottom'
188
- const horizontal = button.side === 'top' || isBottom
189
- const clip = horizontal
190
- ? isBottom
191
- ? 'inset(0 -3px -3px -3px)'
192
- : 'inset(-3px -3px 0 -3px)'
193
- : isRight
194
- ? 'inset(-3px -3px -3px 0)'
195
- : 'inset(-3px 0 -3px -3px)'
196
- return `
197
- <div
198
- aria-hidden="true"
199
- style="
200
- position:absolute;
201
- ${
202
- horizontal
203
- ? `left:${button.top}px;${isBottom ? `bottom:-${button.width}px;` : `top:-${button.width}px;`}`
204
- : `${isRight ? `right:-${button.width}px;` : `left:-${button.width}px;`}top:${button.top}px;`
205
- }
206
- width:${horizontal ? button.height : button.width}px;
207
- height:${horizontal ? button.width : button.height}px;
208
- background-color:#212121;
209
- border-top-left-radius:${isRight || isBottom ? 0 : 1.5}px;
210
- border-bottom-left-radius:${isRight || button.side === 'top' ? 0 : 1.5}px;
211
- border-top-right-radius:${button.side === 'left' || isBottom ? 0 : 1.5}px;
212
- border-bottom-right-radius:${button.side === 'left' || button.side === 'top' ? 0 : 1.5}px;
213
- box-shadow:
214
- inset 0 0.5px 0 #616161,
215
- inset 0 -0.5px 0 #3F3F3F,
216
- ${isRight ? 'inset -0.5px 0 0 #3F3F3F,' : 'inset 0.5px 0 0 #3F3F3F,'}
217
- 0 0 0 0.5px #676767,
218
- 0 0 0 1.5px #000;
219
- clip-path:${clip};
220
- pointer-events:none;
221
- z-index:2;
222
- "
223
- ></div>
224
- `
225
- }
226
-
227
176
  function renderFrameHtml(
228
177
  layout: FramedScreenshotLayout,
229
178
  rawImageDataUrl: string,
230
179
  ): string {
231
- const buttonsHtml = layout.buttons.map(renderSideButtonHtml).join('')
232
-
233
- const ringHtml = layout.metallicRingShadow
234
- ? `
235
- <div
236
- aria-hidden="true"
237
- style="
238
- position:absolute;
239
- inset:0;
240
- border-radius:${getCliRuntime().settings_devices.formatCornerRadii(layout.logicalFrameRadius)};
241
- box-shadow:${layout.metallicRingShadow};
242
- pointer-events:none;
243
- z-index:10;
244
- "
245
- ></div>
246
- `
247
- : ''
180
+ const painter = getCliRuntime().ios_frame_svg
181
+ const paintSpec = {
182
+ width: layout.logicalFrameWidth,
183
+ height: layout.logicalFrameHeight,
184
+ radii: layout.logicalFrameRadius,
185
+ body: layout.legacyHomeButton ? 'legacy' : layout.housingLayers ? null : 'solid',
186
+ ring: layout.legacyHomeButton || layout.housingLayers ? null : painter.METALLIC_RING,
187
+ buttons: layout.buttons
188
+ .filter((button) => button.height > 0)
189
+ .map((button) => ({ ...button, hover: 0 })),
190
+ home: layout.legacyHomeButton,
191
+ } satisfies FramePaintSpec
248
192
 
249
193
  // the folding body's rail, glass ring, corner art, and hinge barrel. no
250
194
  // z-index, so the screen div after it paints on top.
@@ -252,42 +196,6 @@ function renderFrameHtml(
252
196
  .map(getCliRuntime().ios_duo_housing.housingLayerHtml)
253
197
  .join('')
254
198
 
255
- const legacyHomeButtonHtml = layout.legacyHomeButton
256
- ? `
257
- <div
258
- aria-hidden="true"
259
- style="
260
- position:absolute;
261
- top:${layout.legacyHomeButton.top}px;
262
- left:${layout.legacyHomeButton.left}px;
263
- width:${layout.legacyHomeButton.size}px;
264
- height:${layout.legacyHomeButton.size}px;
265
- border-radius:50%;
266
- background:
267
- radial-gradient(circle at 35% 32%, rgba(62,64,68,0.35) 0%, rgba(17,18,20,0.98) 72%, rgba(8,9,11,1) 100%);
268
- box-shadow:
269
- inset 0 0 0 1px rgba(255,255,255,0.06),
270
- inset 0 0 0 ${layout.legacyHomeButton.ring}px rgba(184,192,204,0.45),
271
- 0 0 0 1px rgba(0,0,0,0.5);
272
- z-index:3;
273
- pointer-events:none;
274
- display:flex;
275
- align-items:center;
276
- justify-content:center;
277
- "
278
- >
279
- <div
280
- style="
281
- width:16px;
282
- height:16px;
283
- border-radius:4px;
284
- box-shadow:inset 0 0 0 1.5px rgba(188,194,202,0.5);
285
- "
286
- ></div>
287
- </div>
288
- `
289
- : ''
290
-
291
199
  const homeIndicatorHtml = layout.showHomeIndicator
292
200
  ? `
293
201
  <div
@@ -374,14 +282,11 @@ function renderFrameHtml(
374
282
  border-radius:${layout.housingLayers ? '0' : getCliRuntime().settings_devices.formatCornerRadii(layout.logicalFrameRadius)};
375
283
  box-sizing:border-box;
376
284
  overflow:visible;
377
- background:${layout.frameBackground};
378
285
  box-shadow:${layout.frameOutline};
379
286
  "
380
287
  >
381
- ${buttonsHtml}
382
- ${ringHtml}
288
+ ${painter.frameSvg(paintSpec)}
383
289
  ${housingHtml}
384
- ${legacyHomeButtonHtml}
385
290
  <div
386
291
  id="screen"
387
292
  style="