peachsim 0.1.663 → 0.1.665

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 (83) hide show
  1. package/cli/app-fonts.ts +6 -3
  2. package/cli/commands/control.ts +27 -19
  3. package/cli/commands/inspect/core.ts +58 -32
  4. package/cli/commands/inspect/settling.ts +27 -21
  5. package/dist-lib/agent-daemon-client.cjs +1 -1
  6. package/dist-lib/agent-events.cjs +1 -1
  7. package/dist-lib/agent-identity.cjs +1 -1
  8. package/dist-lib/agent-sessions.cjs +1 -1
  9. package/dist-lib/attached-projects.cjs +1 -1
  10. package/dist-lib/auth/shared-session.cjs +1 -1
  11. package/dist-lib/backend-origin.cjs +1 -1
  12. package/dist-lib/beta.cjs +1 -1
  13. package/dist-lib/beta.mjs +1 -1
  14. package/dist-lib/bridge-constants.cjs +1 -1
  15. package/dist-lib/bridge-contract-input.cjs +2 -6
  16. package/dist-lib/bridge-contract-input.mjs +2 -6
  17. package/dist-lib/bridge-contract.cjs +6 -1
  18. package/dist-lib/bridge-contract.mjs +5 -1
  19. package/dist-lib/capture-contract.cjs +1 -1
  20. package/dist-lib/capture-contract.mjs +1 -1
  21. package/dist-lib/cli-constants.cjs +1 -1
  22. package/dist-lib/cloud-contract.cjs +1 -1
  23. package/dist-lib/cloud-contract.mjs +1 -1
  24. package/dist-lib/cloud-test-args.cjs +1 -1
  25. package/dist-lib/cloud-test-args.mjs +1 -1
  26. package/dist-lib/cloud.cjs +1 -1
  27. package/dist-lib/cloud.mjs +1 -1
  28. package/dist-lib/config.cjs +1 -1
  29. package/dist-lib/detox/index.cjs +1 -1
  30. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  31. package/dist-lib/home-paths.cjs +1 -1
  32. package/dist-lib/host/bridge-host.cjs +2191 -335
  33. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  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 +7 -7
  37. package/dist-lib/host/websocket-proxy.cjs +1 -1
  38. package/dist-lib/index.cjs +23 -30
  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 +1 -1
  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 +7 -7
  49. package/dist-lib/native-app-identity.cjs +1 -1
  50. package/dist-lib/native-app-identity.mjs +1 -1
  51. package/dist-lib/plist.cjs +1 -1
  52. package/dist-lib/plist.mjs +1 -1
  53. package/dist-lib/profiles.cjs +1 -1
  54. package/dist-lib/public-brand.cjs +1 -1
  55. package/dist-lib/react-native-host-modules.cjs +1 -1
  56. package/dist-lib/react-native-host-modules.mjs +1 -1
  57. package/dist-lib/render-mode.cjs +1 -1
  58. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  59. package/dist-lib/sdk.cjs +56 -39
  60. package/dist-lib/sdk.mjs +54 -39
  61. package/dist-lib/skills.cjs +2803 -974
  62. package/dist-lib/swift-compile.cjs +1 -1
  63. package/dist-lib/swift-compile.mjs +1 -1
  64. package/dist-lib/swift-package.cjs +1 -1
  65. package/dist-lib/swift-package.mjs +1 -1
  66. package/dist-lib/swift-submit.cjs +1 -1
  67. package/dist-lib/swift-submit.mjs +1 -1
  68. package/dist-lib/swift.cjs +1 -1
  69. package/dist-lib/vite-swift.cjs +1 -1
  70. package/dist-lib/vite.cjs +7 -7
  71. package/kotlin/ComposeSources/PeachComposeHost.kt +6 -3
  72. package/kotlin/fixtures/kotlin-app-variants/Overlays.kt +56 -0
  73. package/kotlin/fixtures/kotlin-app-variants/Scroll.kt +63 -0
  74. package/kotlin/fixtures/kotlin-app-variants/Sheet.kt +43 -0
  75. package/kotlin/fixtures/kotlin-app-variants/Tabs.kt +47 -0
  76. package/kotlin/fixtures/kotlin-app-variants/Widgets.kt +67 -0
  77. package/package.json +2 -2
  78. package/skills/peach-setup/SKILL.md +1 -1
  79. package/src/bridge-contract-input.ts +1 -5
  80. package/src/bridge-contract.ts +16 -4
  81. package/src/expo-swift-host.ts +248 -0
  82. package/src/host/bridge-host.ts +19 -0
  83. package/src/vite-plugin.ts +3 -3
@@ -0,0 +1,248 @@
1
+ // Expo's own autolinker selects app-owned Swift modules; the common Swift host
2
+ // compiles their sources and a registration entry as one app process.
3
+ import { execFile } from 'node:child_process'
4
+ import { createHash } from 'node:crypto'
5
+ import fs from 'node:fs'
6
+ import { createRequire } from 'node:module'
7
+ import os from 'node:os'
8
+ import path from 'node:path'
9
+ import { promisify } from 'node:util'
10
+ import { isLoopbackHost } from './backend-origin'
11
+ import { artifactFor, icuUrl, projectFor, serveSwiftPackageAsset } from './swift-host'
12
+ import { MAX_SWIFT_COMPILE_FILES, MAX_SWIFT_COMPILE_SOURCE_BYTES } from './swift-submit'
13
+ import type { IncomingMessage, ServerResponse } from 'node:http'
14
+
15
+ const run = promisify(execFile)
16
+ export const EXPO_SWIFT_ROUTE = '/__peach/expo-swift'
17
+ export const EXPO_SWIFT_ENTRY = 'Sources/PeachExpoApp/PeachExpoEntry.swift'
18
+
19
+ export async function collectExpoSwiftSources(
20
+ appRoot: string,
21
+ ): Promise<Record<string, string> | null> {
22
+ appRoot = fs.realpathSync(appRoot)
23
+ const appRequire = createRequire(path.join(appRoot, 'package.json'))
24
+ let autolinker: string
25
+ try {
26
+ autolinker = appRequire.resolve('expo-modules-autolinking/package.json')
27
+ } catch (error) {
28
+ if (
29
+ error &&
30
+ typeof error === 'object' &&
31
+ Reflect.get(error, 'code') === 'MODULE_NOT_FOUND'
32
+ )
33
+ return null
34
+ throw error
35
+ }
36
+ const { stdout } = await run(
37
+ process.execPath,
38
+ [
39
+ path.join(path.dirname(autolinker), 'bin/expo-modules-autolinking.js'),
40
+ 'search',
41
+ '--platform',
42
+ 'apple',
43
+ '--project-root',
44
+ appRoot,
45
+ '--json',
46
+ ],
47
+ { timeout: 30_000, maxBuffer: 8 * 1024 * 1024 },
48
+ )
49
+ const search: unknown = JSON.parse(stdout)
50
+ if (!search || typeof search !== 'object' || Array.isArray(search))
51
+ throw new Error('invalid Expo autolinking result')
52
+ const { stdout: resolvedText } = await run(
53
+ process.execPath,
54
+ [
55
+ path.join(path.dirname(autolinker), 'bin/expo-modules-autolinking.js'),
56
+ 'resolve',
57
+ '--platform',
58
+ 'apple',
59
+ '--project-root',
60
+ appRoot,
61
+ '--json',
62
+ ],
63
+ { timeout: 30_000, maxBuffer: 8 * 1024 * 1024 },
64
+ )
65
+ const resolved: unknown = JSON.parse(resolvedText)
66
+ const resolutions =
67
+ resolved && typeof resolved === 'object' ? Reflect.get(resolved, 'modules') : null
68
+ if (!Array.isArray(resolutions)) throw new Error('invalid Expo Apple resolution')
69
+ const files: Record<string, string> = {}
70
+ const targets: string[] = []
71
+ const classes: string[] = []
72
+ let bytes = 0
73
+ for (const entry of Object.values(search)) {
74
+ if (!entry || typeof entry !== 'object')
75
+ throw new Error('invalid Expo autolinking entry')
76
+ const sourceRoot = Reflect.get(entry, 'path')
77
+ if (typeof sourceRoot !== 'string')
78
+ throw new Error('Expo autolinking entry has no path')
79
+ const canonical = fs.realpathSync(sourceRoot)
80
+ // installed dependency implementations remain with their native seams.
81
+ if (canonical.split(path.sep).includes('node_modules')) continue
82
+ const resolution = resolutions.find(
83
+ (candidate) =>
84
+ candidate &&
85
+ typeof candidate === 'object' &&
86
+ Reflect.get(candidate, 'packageName') === Reflect.get(entry, 'name'),
87
+ )
88
+ const namespaces =
89
+ resolution && typeof resolution === 'object'
90
+ ? Reflect.get(resolution, 'swiftModuleNames')
91
+ : null
92
+ const modules =
93
+ resolution && typeof resolution === 'object'
94
+ ? Reflect.get(resolution, 'modules')
95
+ : null
96
+ if (!Array.isArray(modules) || modules.length === 0) continue
97
+ if (
98
+ !Array.isArray(namespaces) ||
99
+ namespaces.length !== 1 ||
100
+ typeof namespaces[0] !== 'string' ||
101
+ !/^[A-Za-z_][A-Za-z0-9_]*$/.test(namespaces[0])
102
+ )
103
+ throw new Error(
104
+ `${sourceRoot}: local Expo Swift modules need one Apple Swift module`,
105
+ )
106
+ const namespace: string = namespaces[0]
107
+ if (targets.includes(namespace))
108
+ throw new Error(`duplicate local Expo Swift module ${namespace}`)
109
+ targets.push(namespace)
110
+ for (const module of modules) {
111
+ const className =
112
+ module && typeof module === 'object' ? Reflect.get(module, 'class') : null
113
+ const name =
114
+ module && typeof module === 'object' ? Reflect.get(module, 'name') : null
115
+ if (
116
+ typeof className !== 'string' ||
117
+ !/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(className) ||
118
+ (name !== null && typeof name !== 'string')
119
+ )
120
+ throw new Error(`${sourceRoot}: invalid Expo Apple module descriptor`)
121
+ classes.push(
122
+ `(${className.includes('.') ? className : `${namespace}.${className}`}.self, ${name === null ? 'nil' : JSON.stringify(name)})`,
123
+ )
124
+ }
125
+ const ios = path.join(canonical, 'ios')
126
+ if (fs.lstatSync(ios).isSymbolicLink())
127
+ throw new Error(`Expo Swift source directory is a symlink: ${ios}`)
128
+ const walk = (dir: string) => {
129
+ for (const child of fs
130
+ .readdirSync(dir, { withFileTypes: true })
131
+ .sort((a, b) => a.name.localeCompare(b.name))) {
132
+ const file = path.join(dir, child.name)
133
+ if (child.isSymbolicLink())
134
+ throw new Error(`Expo Swift source is a symlink: ${file}`)
135
+ if (child.isDirectory()) {
136
+ if (!child.name.startsWith('.') && child.name !== 'build') walk(file)
137
+ continue
138
+ }
139
+ if (!child.name.endsWith('.swift')) continue
140
+ const size = fs.statSync(file).size
141
+ bytes += size
142
+ if (
143
+ Object.keys(files).length + 1 > MAX_SWIFT_COMPILE_FILES - 2 ||
144
+ bytes > MAX_SWIFT_COMPILE_SOURCE_BYTES
145
+ )
146
+ throw new Error('Expo Swift sources exceed compile service caps')
147
+ files[
148
+ `Sources/${namespace}/${path.relative(ios, file).split(path.sep).join('/')}`
149
+ ] =
150
+ `#sourceLocation(file: ${JSON.stringify(file)}, line: 1)\n${fs.readFileSync(file, 'utf8')}`
151
+ }
152
+ }
153
+ walk(ios)
154
+ }
155
+ if (classes.length === 0) return null
156
+ files['Package.swift'] =
157
+ `// swift-tools-version: 6.0\nimport PackageDescription\nlet package = Package(name: "PeachExpo", targets: [${targets.map((name) => `.target(name: ${JSON.stringify(name)}, path: ${JSON.stringify(`Sources/${name}`)})`).join(', ')}, .executableTarget(name: "PeachExpoApp", path: "Sources/PeachExpoApp")])\n`
158
+ files[EXPO_SWIFT_ENTRY] =
159
+ `import SwiftUI\nimport ExpoModulesCore\n${targets.map((name) => `import ${name}\n`).join('')}\n@main struct PeachExpoApp: RNXPackage {\n init() { MainActor.assumeIsolated { PeachExpoRuntime.register([${classes.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`
160
+ return files
161
+ }
162
+
163
+ const compiling = new Map<string, Promise<unknown>>()
164
+
165
+ export async function handleExpoSwiftRequest(
166
+ req: IncomingMessage,
167
+ res: ServerResponse,
168
+ ): Promise<boolean> {
169
+ const url = new URL(req.url ?? '/', 'http://localhost')
170
+ if (!url.pathname.startsWith('/__peach/swift') && url.pathname !== EXPO_SWIFT_ROUTE)
171
+ return false
172
+ const remote = req.socket.remoteAddress?.replace(/^::ffff:/, '')
173
+ const origin = req.headers.origin
174
+ if (
175
+ !remote ||
176
+ !isLoopbackHost(remote) ||
177
+ (origin && !isLoopbackHost(new URL(origin).hostname))
178
+ ) {
179
+ res.writeHead(403)
180
+ res.end('Expo Swift artifacts require a loopback client')
181
+ return true
182
+ }
183
+ res.setHeader('Access-Control-Allow-Origin', origin ?? '*')
184
+ res.setHeader('Vary', 'Origin')
185
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin')
186
+ if (serveSwiftPackageAsset(url.pathname, res)) return true
187
+ if (url.pathname !== EXPO_SWIFT_ROUTE) return false
188
+ if (req.method !== 'GET') {
189
+ res.writeHead(405)
190
+ res.end('GET required')
191
+ return true
192
+ }
193
+ const root = url.searchParams.get('root')
194
+ if (!root || !path.isAbsolute(root)) {
195
+ res.writeHead(400)
196
+ res.end('missing absolute app root')
197
+ return true
198
+ }
199
+ try {
200
+ let pending = compiling.get(root)
201
+ if (!pending) {
202
+ pending = (async () => {
203
+ const files = await collectExpoSwiftSources(root)
204
+ if (!files) return null
205
+ const key = createHash('sha256').update(fs.realpathSync(root)).digest('hex')
206
+ const packagePath = path.join(os.tmpdir(), 'peach-expo-swift', key)
207
+ const sources = path.join(packagePath, 'Sources')
208
+ // this cache belongs to this adapter; original app sources are read only.
209
+ fs.rmSync(sources, { recursive: true, force: true })
210
+ fs.mkdirSync(sources, { recursive: true })
211
+ for (const [name, content] of Object.entries(files)) {
212
+ const target = path.join(packagePath, name)
213
+ fs.mkdirSync(path.dirname(target), { recursive: true })
214
+ fs.writeFileSync(target, content)
215
+ }
216
+ const moduleRoot = path.resolve(import.meta.dirname, '../../sootsim-swift')
217
+ const local = fs.existsSync(path.join(moduleRoot, 'toolchain.json'))
218
+ const artifact = await artifactFor(
219
+ projectFor(packagePath),
220
+ EXPO_SWIFT_ENTRY,
221
+ local ? moduleRoot : undefined,
222
+ )
223
+ const host = req.headers.host
224
+ if (!host) throw new Error('Expo Swift request has no host')
225
+ const base = `http://${host}`
226
+ return {
227
+ url: `${base}/__peach/swift/${artifact.hash}.wasm`,
228
+ hash: artifact.hash,
229
+ icuUrl: icuUrl(base, artifact),
230
+ rootId: `${key}/Expo`,
231
+ }
232
+ })()
233
+ compiling.set(root, pending)
234
+ }
235
+ const metadata = await pending
236
+ res.writeHead(200, {
237
+ 'Content-Type': 'application/json',
238
+ 'Cache-Control': 'no-store',
239
+ })
240
+ res.end(JSON.stringify(metadata))
241
+ } catch (error) {
242
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
243
+ res.end(error instanceof Error ? error.message : String(error))
244
+ } finally {
245
+ compiling.delete(root)
246
+ }
247
+ return true
248
+ }
@@ -16,6 +16,7 @@ import {
16
16
  SOOTSIM_BRIDGE_SIM_CLOSE_REASON,
17
17
  } from '../bridge-constants.ts'
18
18
  import { getCliVersion } from '../cli-version.ts'
19
+ import { handleExpoSwiftRequest } from '../expo-swift-host.ts'
19
20
  import {
20
21
  activeRuntimeDir as getActiveRuntimeDir,
21
22
  claimDaemonLockfile,
@@ -1482,6 +1483,24 @@ export class SootSimBridgeHost {
1482
1483
  res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin')
1483
1484
  res.setHeader('Document-Policy', 'js-profiling')
1484
1485
 
1486
+ if (
1487
+ req.url?.startsWith('/__peach/expo-swift') ||
1488
+ req.url?.startsWith('/__peach/swift')
1489
+ ) {
1490
+ void handleExpoSwiftRequest(req, res)
1491
+ .then((handled) => {
1492
+ if (!handled) {
1493
+ res.writeHead(404)
1494
+ res.end('Swift artifact not found')
1495
+ }
1496
+ })
1497
+ .catch((error: unknown) => {
1498
+ res.writeHead(500)
1499
+ res.end(String(error))
1500
+ })
1501
+ return
1502
+ }
1503
+
1485
1504
  // proxy + app-api routes need to accept POST/PUT/DELETE/PATCH/OPTIONS
1486
1505
  // because they forward the request to a real upstream. handle them
1487
1506
  // BEFORE the read-only method check below — otherwise tenant bundles
@@ -74,7 +74,7 @@ const rnAssetRegistryPath = path.resolve(
74
74
  )
75
75
  const reactBridgePath = path.resolve(
76
76
  workspaceRoot,
77
- 'packages/contrast-kit/src/react-bridge.ts',
77
+ 'packages/peach-kit/src/react-bridge.ts',
78
78
  )
79
79
  const sootsimBrowserSourceAliases = [
80
80
  {
@@ -1344,7 +1344,7 @@ function reactBridgePlugin(): Plugin {
1344
1344
  importer.includes('/peachsim/src/') ||
1345
1345
  importer.includes('/peach-engine/src/') ||
1346
1346
  importer.includes('/peach-compat/src/') ||
1347
- importer.includes('/contrast-kit/src/') ||
1347
+ importer.includes('/peach-kit/src/') ||
1348
1348
  importer.includes('react-reconciler')
1349
1349
 
1350
1350
  if (!shouldRedirect) return null
@@ -1401,7 +1401,7 @@ export function workerReactBridgePlugin(): Plugin {
1401
1401
  importer.includes('/peachsim/src/') ||
1402
1402
  importer.includes('/peach-engine/src/') ||
1403
1403
  importer.includes('/peach-compat/src/') ||
1404
- importer.includes('/contrast-kit/src/') ||
1404
+ importer.includes('/peach-kit/src/') ||
1405
1405
  importer.includes('react-reconciler') ||
1406
1406
  importer.includes('/node_modules/')
1407
1407