peachsim 0.1.628 → 0.1.630

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 (111) hide show
  1. package/README.md +8 -4
  2. package/cli/bridge-flow-runner.ts +10 -3
  3. package/cli/cli-update.ts +2 -7
  4. package/cli/cloud-test-tunnel.ts +16 -3
  5. package/cli/commands/config.ts +1 -1
  6. package/cli/commands/control.ts +28 -3
  7. package/cli/commands/daemon-mac-app.ts +4 -2
  8. package/cli/commands/daemon.ts +4 -4
  9. package/cli/commands/device.ts +2 -3
  10. package/cli/commands/inspect/actions.ts +125 -90
  11. package/cli/commands/inspect/core.ts +5 -0
  12. package/cli/commands/inspect.ts +27 -13
  13. package/cli/commands/platform.ts +1 -1
  14. package/cli/commands/screenshot-layers.ts +1 -1
  15. package/cli/commands/screenshot.ts +1 -1
  16. package/cli/commands/skills.ts +0 -31
  17. package/cli/drivers/playwright-provisioning.ts +3 -2
  18. package/cli/drivers/playwright-sim-host.ts +25 -9
  19. package/cli/main.ts +8 -2
  20. package/cli/outbound-endpoints.ts +0 -4
  21. package/detox/index.ts +74 -43
  22. package/dist-lib/agent-daemon-client.cjs +1 -1
  23. package/dist-lib/agent-events.cjs +1 -1
  24. package/dist-lib/agent-identity.cjs +1 -1
  25. package/dist-lib/agent-sessions.cjs +1 -1
  26. package/dist-lib/attached-projects.cjs +1 -1
  27. package/dist-lib/auth/shared-session.cjs +1 -1
  28. package/dist-lib/backend-origin.cjs +1 -1
  29. package/dist-lib/beta.cjs +1 -1
  30. package/dist-lib/beta.mjs +1 -1
  31. package/dist-lib/bridge-constants.cjs +1 -1
  32. package/dist-lib/bridge-contract-input.cjs +1 -1
  33. package/dist-lib/bridge-contract-input.mjs +1 -1
  34. package/dist-lib/bridge-contract.cjs +1 -1
  35. package/dist-lib/bridge-contract.mjs +1 -1
  36. package/dist-lib/capture-contract.cjs +1 -1
  37. package/dist-lib/capture-contract.mjs +1 -1
  38. package/dist-lib/cli-constants.cjs +1 -1
  39. package/dist-lib/cloud-contract.cjs +1 -1
  40. package/dist-lib/cloud-contract.mjs +1 -1
  41. package/dist-lib/cloud-test-args.cjs +1 -1
  42. package/dist-lib/cloud-test-args.mjs +1 -1
  43. package/dist-lib/cloud.cjs +1 -1
  44. package/dist-lib/cloud.mjs +1 -1
  45. package/dist-lib/config.cjs +1 -1
  46. package/dist-lib/detox/index.cjs +129 -27
  47. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  48. package/dist-lib/home-paths.cjs +1 -1
  49. package/dist-lib/host/bridge-host.cjs +290 -315
  50. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  51. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  52. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  53. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  54. package/dist-lib/host/websocket-proxy.cjs +1 -1
  55. package/dist-lib/index.cjs +253 -121
  56. package/dist-lib/jump-to-source-babel.cjs +1 -1
  57. package/dist-lib/jump-to-source-native.cjs +1 -1
  58. package/dist-lib/{vite-kotlin.cjs → kotlin-host.cjs} +130 -19
  59. package/dist-lib/menu.cjs +1 -1
  60. package/dist-lib/menu.mjs +1 -1
  61. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  62. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  63. package/dist-lib/metro-production-bundle.cjs +1 -1
  64. package/dist-lib/metro-production-bundle.mjs +1 -1
  65. package/dist-lib/metro.cjs +132 -9
  66. package/dist-lib/native-app-identity.cjs +278 -0
  67. package/dist-lib/native-app-identity.mjs +245 -0
  68. package/dist-lib/profiles.cjs +1 -1
  69. package/dist-lib/public-brand.cjs +1 -1
  70. package/dist-lib/react-native-host-modules.cjs +1 -1
  71. package/dist-lib/react-native-host-modules.mjs +1 -1
  72. package/dist-lib/render-mode.cjs +1 -1
  73. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  74. package/dist-lib/sdk.cjs +110 -87
  75. package/dist-lib/sdk.mjs +110 -87
  76. package/dist-lib/skills.cjs +841 -643
  77. package/dist-lib/swift-compile.cjs +1 -1
  78. package/dist-lib/swift-compile.mjs +1 -1
  79. package/dist-lib/swift-package.cjs +1 -1
  80. package/dist-lib/swift-package.mjs +1 -1
  81. package/dist-lib/swift-submit.cjs +1 -1
  82. package/dist-lib/swift-submit.mjs +1 -1
  83. package/dist-lib/swift.cjs +282 -177
  84. package/dist-lib/vite-swift.cjs +1 -1
  85. package/dist-lib/vite.cjs +11 -28
  86. package/kotlin/README.md +32 -4
  87. package/kotlin/fixtures/counter/Counter.kt +26 -0
  88. package/kotlin/fixtures/counter/CounterScreen.kt +40 -0
  89. package/kotlin/fixtures/headless-counter/Headless.kt +228 -0
  90. package/kotlin/fixtures/runtime-state/RuntimeState.kt +200 -0
  91. package/kotlin/fork/LICENSE +178 -0
  92. package/kotlin/fork/NOTICE +25 -0
  93. package/kotlin/fork/README.md +59 -0
  94. package/kotlin/fork/build.sh +128 -0
  95. package/kotlin/fork/compose-worker-globals.patch +96 -0
  96. package/kotlin/fork/fork.json +33 -0
  97. package/kotlin/fork/publish.ts +77 -0
  98. package/kotlin/fork/skiko-worker-clock.patch +33 -0
  99. package/kotlin/toolchain.json +288 -0
  100. package/package.json +12 -5
  101. package/src/automatic-update-policy.ts +17 -0
  102. package/src/host/bridge-host.ts +3 -1
  103. package/src/{vite-plugin-kotlin.ts → kotlin-host.ts} +155 -12
  104. package/src/metro-plugin.ts +18 -0
  105. package/src/native-app-identity.ts +376 -0
  106. package/src/native-firebase-config.ts +90 -0
  107. package/src/screenshots/frame-compose.ts +5 -2
  108. package/src/screenshots/registry.ts +1 -1
  109. package/src/screenshots/schema.ts +2 -2
  110. package/src/vite-plugin-one.ts +11 -28
  111. package/src/vite-plugin.ts +6 -1
@@ -0,0 +1,376 @@
1
+ // generic native app identity: the values an installed iOS/Android binary
2
+ // carries, read from the app's own native project (`Info.plist` resolved
3
+ // against the Xcode Release build settings, or `android/app/build.gradle`), so
4
+ // a simulator tenant reports the same identity the native Release build does.
5
+ //
6
+ // the values travel in a footer a bundle builder appends to the artifact — the
7
+ // same transport shape the Metro module identity footer already uses — and the
8
+ // tenant boot reads it from the bundle text before the guest evaluates, then
9
+ // stores it on the tenant realm for the native app-information seams. a bundle
10
+ // with no native identity footer leaves every seam exactly where it was.
11
+
12
+ export type NativeAppIdentityPlatform = 'ios' | 'android'
13
+
14
+ export interface NativeAppIdentity {
15
+ applicationName: string | null
16
+ appVersion: string
17
+ buildVersion: string
18
+ bundleIdentifier: string
19
+ platform: NativeAppIdentityPlatform
20
+ }
21
+
22
+ export type NativeAppIdentityFiles = Record<string, string>
23
+
24
+ const NATIVE_APP_IDENTITY_PREFIX = 'globalThis.__sootsimNativeAppIdentity='
25
+ const METRO_MODULE_PATHS_PREFIX = 'globalThis.__sootsimModulePaths='
26
+ const APPLIED_NATIVE_APP_IDENTITY_KEY = '__sootsim_native_app_identity'
27
+
28
+ interface NativeAppIdentityGlobals {
29
+ __sootsim_native_app_identity?: NativeAppIdentity | null
30
+ }
31
+
32
+ function isRecord(value: unknown): value is Record<string, unknown> {
33
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
34
+ }
35
+
36
+ function nonEmptyString(value: unknown): string | null {
37
+ return typeof value === 'string' && value.length > 0 ? value : null
38
+ }
39
+
40
+ export function isNativeAppIdentity(value: unknown): value is NativeAppIdentity {
41
+ if (!isRecord(value)) return false
42
+ const platform = value.platform
43
+ if (platform !== 'ios' && platform !== 'android') return false
44
+ const applicationName = value.applicationName
45
+ if (applicationName !== null && nonEmptyString(applicationName) === null) return false
46
+ if (nonEmptyString(value.appVersion) === null) return false
47
+ if (nonEmptyString(value.bundleIdentifier) === null) return false
48
+ const buildVersion = nonEmptyString(value.buildVersion)
49
+ if (buildVersion === null) return false
50
+ if (platform === 'android' && !/^[1-9][0-9]*$/.test(buildVersion)) return false
51
+ return true
52
+ }
53
+
54
+ // read a host-supplied identity. absent/null clears it; a present value that is
55
+ // neither is a contract violation the caller must not paper over.
56
+ export function readNativeAppIdentity(value: unknown): NativeAppIdentity | null {
57
+ if (value === undefined || value === null) return null
58
+ if (!isNativeAppIdentity(value)) {
59
+ throw new Error('native app identity is malformed')
60
+ }
61
+ return {
62
+ applicationName: value.applicationName,
63
+ appVersion: value.appVersion,
64
+ buildVersion: value.buildVersion,
65
+ bundleIdentifier: value.bundleIdentifier,
66
+ platform: value.platform,
67
+ }
68
+ }
69
+
70
+ // the one realm store every seam reads. set by the tenant boot before the
71
+ // guest evaluates, cleared on a boot without an identity so a previous app's
72
+ // identity can never leak into the next load.
73
+ export function applyNativeAppIdentity(identity: NativeAppIdentity | null): void {
74
+ const globals = globalThis as NativeAppIdentityGlobals
75
+ if (identity) globals.__sootsim_native_app_identity = identity
76
+ else delete globals.__sootsim_native_app_identity
77
+ }
78
+
79
+ export function getAppliedNativeAppIdentity(): NativeAppIdentity | null {
80
+ const value = (globalThis as NativeAppIdentityGlobals).__sootsim_native_app_identity
81
+ return isNativeAppIdentity(value) ? value : null
82
+ }
83
+
84
+ // one line, so the footer survives a bundle whose last statement is the Metro
85
+ // module identity footer and can be found again by reading the text.
86
+ export function createNativeAppIdentityFooter(identity: NativeAppIdentity): string {
87
+ const payload = JSON.stringify(identity)
88
+ .replace(/\u2028/g, '\\u2028')
89
+ .replace(/\u2029/g, '\\u2029')
90
+ .replace(/<\//g, '<\\/')
91
+ return `\n${NATIVE_APP_IDENTITY_PREFIX}${payload};`
92
+ }
93
+
94
+ function nativeAppIdentityFooterStart(source: string): number {
95
+ return source.lastIndexOf(`\n${NATIVE_APP_IDENTITY_PREFIX}`)
96
+ }
97
+
98
+ export function readNativeAppIdentityFooter(source: string): NativeAppIdentity | null {
99
+ const start = nativeAppIdentityFooterStart(source)
100
+ if (start < 0) return null
101
+ const lineEnd = source.indexOf('\n', start + 1)
102
+ const line = source.slice(start + 1, lineEnd < 0 ? undefined : lineEnd)
103
+ if (!line.endsWith(';')) {
104
+ throw new Error('native app identity footer is truncated')
105
+ }
106
+ let parsed: unknown
107
+ try {
108
+ parsed = JSON.parse(line.slice(NATIVE_APP_IDENTITY_PREFIX.length, -1))
109
+ } catch {
110
+ throw new Error('native app identity footer is not valid JSON')
111
+ }
112
+ if (!isNativeAppIdentity(parsed)) {
113
+ throw new Error('native app identity footer is malformed')
114
+ }
115
+ return parsed
116
+ }
117
+
118
+ // remove the canonical footer from a bundle builder's input, returning the
119
+ // identity it carried. the cloud artifact normalizer detaches before
120
+ // minifying — a minifier rewrites the payload (`{key:…}`, joined statements) —
121
+ // and reattaches the same canonical statement afterwards.
122
+ export function detachNativeAppIdentityFooter(source: string): {
123
+ source: string
124
+ identity: NativeAppIdentity | null
125
+ } {
126
+ const start = nativeAppIdentityFooterStart(source)
127
+ if (start < 0) return { source, identity: null }
128
+ const identity = readNativeAppIdentityFooter(source)
129
+ const lineEnd = source.indexOf('\n', start + 1)
130
+ const end = lineEnd < 0 ? source.length : lineEnd
131
+ return { source: source.slice(0, start) + source.slice(end), identity }
132
+ }
133
+
134
+ // insert the footer ahead of the Metro module identity footer when the bundle
135
+ // carries one: readers of that contract slice the bundle at its start, and the
136
+ // native identity has to stay in the text they hand to the guest. no footer
137
+ // present (a development or legacy artifact) appends.
138
+ export function insertNativeAppIdentityFooter(
139
+ source: string,
140
+ identity: NativeAppIdentity,
141
+ ): string {
142
+ const existing = nativeAppIdentityFooterStart(source)
143
+ const existingEnd =
144
+ existing < 0
145
+ ? -1
146
+ : source.includes('\n', existing + 1)
147
+ ? source.indexOf('\n', existing + 1)
148
+ : -1
149
+ const withoutExisting =
150
+ existing < 0
151
+ ? source
152
+ : source.slice(0, existing) +
153
+ source.slice(existingEnd < 0 ? source.length : existingEnd)
154
+ const moduleFooter = withoutExisting.lastIndexOf(`\n${METRO_MODULE_PATHS_PREFIX}`)
155
+ const footer = createNativeAppIdentityFooter(identity)
156
+ if (moduleFooter < 0) return withoutExisting + footer
157
+ return (
158
+ withoutExisting.slice(0, moduleFooter) + footer + withoutExisting.slice(moduleFooter)
159
+ )
160
+ }
161
+
162
+ // ── native project extraction ────────────────────────────────────────────────
163
+
164
+ function plistString(plist: string, key: string): string | null {
165
+ const match = plist.match(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`))
166
+ return match ? match[1].trim() : null
167
+ }
168
+
169
+ // Xcode build settings support `$(VAR)` references, so a plist value can only
170
+ // be read against the settings of the configuration the binary ships with.
171
+ // references recurse because a setting may itself reference another, and a
172
+ // value whose reference cannot be fully resolved — an unknown variable or a
173
+ // modifier this reader does not implement — is rejected rather than asserted.
174
+ function resolveBuildSetting(
175
+ value: string,
176
+ settings: Record<string, string>,
177
+ depth = 0,
178
+ ): string | null {
179
+ if (depth > 8) return null
180
+ let unresolved = false
181
+ const resolved = value.replace(/\$\(([^()]+)\)/g, (_match, reference: string) => {
182
+ const separator = reference.indexOf(':')
183
+ if (separator >= 0) {
184
+ unresolved = true
185
+ return ''
186
+ }
187
+ const replacement = settings[reference]
188
+ if (replacement === undefined) {
189
+ unresolved = true
190
+ return ''
191
+ }
192
+ return replacement
193
+ })
194
+ if (unresolved) return null
195
+ if (resolved.includes('$(')) return resolveBuildSetting(resolved, settings, depth + 1)
196
+ return resolved
197
+ }
198
+
199
+ // an identity field is only usable when it is a real value: non-empty and free
200
+ // of any unresolved build-setting syntax.
201
+ function resolvedIdentityField(value: string | null): string | null {
202
+ const trimmed = value?.trim() ?? ''
203
+ if (!trimmed || trimmed.includes('$(')) return null
204
+ return trimmed
205
+ }
206
+
207
+ function identityFieldFrom(
208
+ settings: Record<string, string>,
209
+ raw: string | null,
210
+ fallbackSetting: string | undefined,
211
+ ): string | null {
212
+ if (raw !== null) return resolvedIdentityField(resolveBuildSetting(raw, settings))
213
+ return resolvedIdentityField(nonEmptyString(fallbackSetting ?? null))
214
+ }
215
+
216
+ function readBuildSettingsBlock(source: string, start: number): string {
217
+ // start is the index of `{`; walk to its matching close so nested arrays and
218
+ // dictionaries inside buildSettings stay part of the block.
219
+ let depth = 0
220
+ for (let index = start; index < source.length; index++) {
221
+ const character = source[index]
222
+ if (character === '{') depth++
223
+ else if (character === '}') {
224
+ depth--
225
+ if (depth === 0) return source.slice(start, index + 1)
226
+ }
227
+ }
228
+ return source.slice(start)
229
+ }
230
+
231
+ function parseBuildSettings(block: string): Record<string, string> {
232
+ const settings: Record<string, string> = {}
233
+ const matchPattern = /([A-Z0-9_]+) = ("(?:[^"\\]|\\.)*"|[^;{}]*);/g
234
+ let match: RegExpExecArray | null
235
+ while ((match = matchPattern.exec(block)) !== null) {
236
+ const raw = match[2].trim()
237
+ settings[match[1]] = raw.startsWith('"') ? raw.slice(1, -1) : raw.trim()
238
+ }
239
+ return settings
240
+ }
241
+
242
+ // the Release XCBuildConfiguration of the app target: the configuration whose
243
+ // INFOPLIST_FILE is the plist being resolved, else the one whose PRODUCT_NAME
244
+ // names the target directory. Debug settings (suffixes, dev bundle ids) must
245
+ // never win.
246
+ function appTargetReleaseSettings(
247
+ pbxproj: string,
248
+ targetName: string,
249
+ infoPlistRelativePath: string,
250
+ ): Record<string, string> | null {
251
+ const configurationPattern = /\/\* (Release|Debug) \*\/ = \{/g
252
+ const candidates: Array<{ name: string; settings: Record<string, string> }> = []
253
+ let match: RegExpExecArray | null
254
+ while ((match = configurationPattern.exec(pbxproj)) !== null) {
255
+ const blockStart = pbxproj.indexOf('{', match.index)
256
+ if (blockStart < 0) continue
257
+ const block = readBuildSettingsBlock(pbxproj, blockStart)
258
+ const buildSettingsStart = block.indexOf('buildSettings = {')
259
+ if (buildSettingsStart < 0) continue
260
+ const settings = parseBuildSettings(
261
+ readBuildSettingsBlock(block, block.indexOf('{', buildSettingsStart)),
262
+ )
263
+ candidates.push({ name: match[1], settings })
264
+ }
265
+ const plistMatch = (settings: Record<string, string>): boolean => {
266
+ const plist = settings.INFOPLIST_FILE
267
+ if (!plist) return false
268
+ const normalized = plist.replace(/^"|"$/g, '')
269
+ return (
270
+ normalized === infoPlistRelativePath ||
271
+ infoPlistRelativePath.endsWith(`/${normalized}`) ||
272
+ normalized.endsWith(`/${infoPlistRelativePath}`)
273
+ )
274
+ }
275
+ const named = (settings: Record<string, string>): boolean =>
276
+ resolveBuildSetting(settings.PRODUCT_NAME ?? '', settings) === targetName
277
+ return (
278
+ candidates.find(
279
+ (candidate) => candidate.name === 'Release' && plistMatch(candidate.settings),
280
+ )?.settings ??
281
+ candidates.find(
282
+ (candidate) => candidate.name === 'Release' && named(candidate.settings),
283
+ )?.settings ??
284
+ null
285
+ )
286
+ }
287
+
288
+ function nativeAppIdentityFromIos(
289
+ files: NativeAppIdentityFiles,
290
+ ): NativeAppIdentity | null {
291
+ const appDelegatePattern = /(?:^|\/)ios\/([^/]+)\/AppDelegate\.(?:swift|mm|m)$/
292
+ let targetName: string | null = null
293
+ for (const path of Object.keys(files)) {
294
+ if (path.includes('/Pods/')) continue
295
+ const match = path.match(appDelegatePattern)
296
+ if (match) {
297
+ targetName = match[1]
298
+ break
299
+ }
300
+ }
301
+ if (!targetName) return null
302
+
303
+ const plistPath = Object.keys(files).find(
304
+ (path) => path === `ios/${targetName}/Info.plist`,
305
+ )
306
+ const plist = plistPath ? files[plistPath] : null
307
+ if (!plist) return null
308
+
309
+ const pbxprojPath = Object.keys(files).find((path) =>
310
+ /(?:^|\/)ios\/[^/]+\.xcodeproj\/project\.pbxproj$/.test(path),
311
+ )
312
+ const settings = pbxprojPath
313
+ ? appTargetReleaseSettings(files[pbxprojPath], targetName, `${targetName}/Info.plist`)
314
+ : null
315
+ if (!settings) return null
316
+
317
+ const bundleIdentifier = identityFieldFrom(
318
+ settings,
319
+ plistString(plist, 'CFBundleIdentifier'),
320
+ settings.PRODUCT_BUNDLE_IDENTIFIER,
321
+ )
322
+ if (!bundleIdentifier) return null
323
+
324
+ const appVersion = identityFieldFrom(
325
+ settings,
326
+ plistString(plist, 'CFBundleShortVersionString'),
327
+ settings.MARKETING_VERSION,
328
+ )
329
+ const buildVersion = identityFieldFrom(
330
+ settings,
331
+ plistString(plist, 'CFBundleVersion'),
332
+ settings.CURRENT_PROJECT_VERSION,
333
+ )
334
+ if (!appVersion || !buildVersion) return null
335
+
336
+ const applicationName =
337
+ identityFieldFrom(settings, plistString(plist, 'CFBundleDisplayName'), undefined) ??
338
+ identityFieldFrom(
339
+ settings,
340
+ plistString(plist, 'CFBundleName'),
341
+ settings.PRODUCT_NAME,
342
+ ) ??
343
+ resolvedIdentityField(resolveBuildSetting(settings.PRODUCT_NAME ?? '', settings)) ??
344
+ resolvedIdentityField(targetName)
345
+ if (!applicationName) return null
346
+
347
+ return {
348
+ applicationName,
349
+ appVersion,
350
+ buildVersion,
351
+ bundleIdentifier,
352
+ platform: 'ios',
353
+ }
354
+ }
355
+
356
+ // Android's shipped identity requires resolving the selected flavor and
357
+ // buildType overrides (`productFlavors.prod`, `buildTypes.release`), which a
358
+ // text reader of `defaultConfig` cannot distinguish from the defaults it
359
+ // overrides. an Android project therefore ships no footer until that
360
+ // resolution exists — never a guessed identity.
361
+ function nativeAppIdentityFromAndroid(): NativeAppIdentity | null {
362
+ return null
363
+ }
364
+
365
+ // read the identity a bundle builder transports. files are project-relative
366
+ // paths to their text, so both a checked-out app directory and a stored
367
+ // project file map resolve identically. null when the project carries no
368
+ // resolvable native config — the builder then ships no footer.
369
+ export function extractNativeAppIdentityFromFiles(
370
+ files: NativeAppIdentityFiles,
371
+ platform: NativeAppIdentityPlatform,
372
+ ): NativeAppIdentity | null {
373
+ return platform === 'android'
374
+ ? nativeAppIdentityFromAndroid()
375
+ : nativeAppIdentityFromIos(files)
376
+ }
@@ -0,0 +1,90 @@
1
+ // the native installer reads Expo's googleServicesFile before Firebase starts.
2
+ // expose only FirebaseOptions, never a partner-specific extra configuration.
3
+ import fs from 'node:fs'
4
+ import { createRequire } from 'node:module'
5
+ import path from 'node:path'
6
+ import { isRecord } from './is-record'
7
+ import { parsePlistSource } from './plist'
8
+
9
+ export function readNativeFirebaseConfig(projectRoot: string) {
10
+ const result: Record<string, unknown> = {}
11
+ if (
12
+ ![
13
+ 'app.json',
14
+ 'app.config.js',
15
+ 'app.config.ts',
16
+ 'app.config.cjs',
17
+ 'app.config.mjs',
18
+ ].some((name) => fs.existsSync(path.join(projectRoot, name)))
19
+ )
20
+ return result
21
+ const require = createRequire(path.join(projectRoot, 'package.json'))
22
+ const configModule: unknown = require('@expo/config')
23
+ if (!isRecord(configModule) || typeof configModule.getConfig !== 'function') {
24
+ throw new Error('Expo native configuration is unavailable.')
25
+ }
26
+ const resolved: unknown = Reflect.apply(configModule.getConfig, configModule, [
27
+ projectRoot,
28
+ { skipSDKVersionRequirement: true },
29
+ ])
30
+ const config = isRecord(resolved) && isRecord(resolved.exp) ? resolved.exp : {}
31
+ for (const platform of ['ios', 'android']) {
32
+ const native = config[platform]
33
+ if (!isRecord(native) || typeof native.googleServicesFile !== 'string') continue
34
+ try {
35
+ const source = fs.readFileSync(
36
+ path.resolve(projectRoot, native.googleServicesFile),
37
+ 'utf8',
38
+ )
39
+ const parsed: unknown =
40
+ platform === 'ios' ? parsePlistSource(source) : JSON.parse(source)
41
+ if (!isRecord(parsed)) throw new Error('Invalid Google services file.')
42
+ if (platform === 'ios') {
43
+ result[platform] = {
44
+ apiKey: parsed.API_KEY,
45
+ appId: parsed.GOOGLE_APP_ID,
46
+ projectId: parsed.PROJECT_ID,
47
+ databaseURL: parsed.DATABASE_URL,
48
+ gaTrackingId: parsed.TRACKING_ID,
49
+ storageBucket: parsed.STORAGE_BUCKET,
50
+ messagingSenderId: parsed.GCM_SENDER_ID,
51
+ clientId: parsed.CLIENT_ID,
52
+ androidClientID: parsed.ANDROID_CLIENT_ID,
53
+ deepLinkUrlScheme: parsed.REVERSED_CLIENT_ID,
54
+ }
55
+ } else {
56
+ const project = isRecord(parsed.project_info) ? parsed.project_info : {}
57
+ const clients: unknown[] = Array.isArray(parsed.client) ? parsed.client : []
58
+ const client = clients.find((value) => {
59
+ if (!isRecord(value) || !isRecord(value.client_info)) return false
60
+ const android = value.client_info.android_client_info
61
+ return isRecord(android) && android.package_name === native.package
62
+ })
63
+ if (!isRecord(client) || !isRecord(client.client_info))
64
+ throw new Error('No Google services client matches the Android package.')
65
+ const apiKeys: unknown[] = Array.isArray(client.api_key) ? client.api_key : []
66
+ const apiKey = apiKeys.find(
67
+ (value) => isRecord(value) && typeof value.current_key === 'string',
68
+ )
69
+ const oauthClients: unknown[] = Array.isArray(client.oauth_client)
70
+ ? client.oauth_client
71
+ : []
72
+ const oauth = oauthClients.find(
73
+ (value) => isRecord(value) && value.client_type === 3,
74
+ )
75
+ result[platform] = {
76
+ apiKey: isRecord(apiKey) ? apiKey.current_key : undefined,
77
+ appId: client.client_info.mobilesdk_app_id,
78
+ projectId: project.project_id,
79
+ databaseURL: project.firebase_url,
80
+ storageBucket: project.storage_bucket,
81
+ messagingSenderId: project.project_number,
82
+ clientId: isRecord(oauth) ? oauth.client_id : undefined,
83
+ }
84
+ }
85
+ } catch (error) {
86
+ result[platform] = { error: error instanceof Error ? error.message : String(error) }
87
+ }
88
+ }
89
+ return result
90
+ }
@@ -19,7 +19,7 @@ import {
19
19
  resolveDeviceFrameGeometry,
20
20
  type CornerRadii,
21
21
  type DeviceModel,
22
- } from 'peach-engine/settings'
22
+ } from 'peach-engine/settings/devices'
23
23
  import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
24
24
 
25
25
  const METALLIC_RING_SHADOW =
@@ -90,7 +90,10 @@ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotL
90
90
  const modernGeometry = resolveDeviceFrameGeometry(spec)
91
91
  const housingLayers = isLegacyHardware
92
92
  ? null
93
- : resolveHousingArt(spec.posture ?? 'closed', modernGeometry)
93
+ : resolveHousingArt(spec.posture ?? 'closed', modernGeometry, {
94
+ orientation: spec.orientation,
95
+ landscapeSide: spec.landscapeSide,
96
+ })
94
97
  const logicalFrameWidth = isLegacyHardware
95
98
  ? spec.width + LEGACY_SIDE_BEZEL * 2
96
99
  : modernGeometry.frameWidth
@@ -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 'peach-engine/settings'
9
+ import type { DeviceModel } from 'peach-engine/settings/devices'
10
10
 
11
11
  export {
12
12
  BACKGROUND_PRESETS,
@@ -1,7 +1,8 @@
1
1
  import { readFileSync } from 'fs'
2
2
  import path from 'path'
3
- import { devices, type DeviceModel } from 'peach-engine/settings'
3
+ import { devices, type DeviceModel } from 'peach-engine/settings/devices'
4
4
  import yaml from 'yaml'
5
+ import { isRecord } from '../is-record'
5
6
  import {
6
7
  cloneBackgroundSpec,
7
8
  DEFAULT_FRAME_SHADOW,
@@ -16,7 +17,6 @@ import {
16
17
  type PosePresetName,
17
18
  type TextPresetName,
18
19
  } from './registry'
19
- import { isRecord } from '../is-record'
20
20
 
21
21
  export type CaptureMode = 'raw' | 'framed' | 'raw+framed'
22
22
  export type CapturePathMode = 'auto' | 'plan' | 'flow'
@@ -41,8 +41,8 @@ const loadSwiftHost = () => (swiftHost ??= import('peachsim/vite-swift'))
41
41
  // the Kotlin host (toolchain fetch, compile, artifact serving) loads the same
42
42
  // way, on the first /__peach/kotlin request from an Android device
43
43
  const KOTLIN_ROUTE_PREFIX = '/__peach/kotlin'
44
- let kotlinHost: Promise<typeof import('peachsim/vite-kotlin')> | undefined
45
- const loadKotlinHost = () => (kotlinHost ??= import('peachsim/vite-kotlin'))
44
+ let kotlinHost: Promise<typeof import('peachsim/kotlin-host')> | undefined
45
+ const loadKotlinHost = () => (kotlinHost ??= import('peachsim/kotlin-host'))
46
46
 
47
47
  export function rnxPlugin(options: PeachPluginOptions = {}): Plugin {
48
48
  const prefix = options.prefix || '/__soot'
@@ -140,34 +140,17 @@ export function rnxPlugin(options: PeachPluginOptions = {}): Plugin {
140
140
  if (!pathname.startsWith(KOTLIN_ROUTE_PREFIX)) return next()
141
141
  loadKotlinHost()
142
142
  .then(async (host) => {
143
- if (host.serveKotlinArtifact(pathname, res)) return
144
- if (pathname !== '/__peach/kotlin-package') return next()
145
143
  if (!kotlinModuleRoot) {
146
- const moduleRoot = path.join(
147
- path.dirname(
148
- createRequire(path.join(server.config.root, 'package.json')).resolve(
149
- 'peachsim/package.json',
150
- ),
151
- ),
152
- 'kotlin',
153
- )
154
- kotlinModuleRoot = moduleRoot
155
- server.watcher.add(path.join(moduleRoot, 'Sources'))
156
- }
157
- try {
158
- const artifact = await host.kotlinArtifact(
159
- server.config.root,
160
- kotlinModuleRoot,
161
- devOrigin(),
162
- )
163
- res.setHeader('access-control-allow-origin', '*')
164
- res.setHeader('content-type', 'application/json')
165
- res.end(JSON.stringify({ ...artifact, hmrPath }))
166
- } catch (error) {
167
- res.statusCode = 500
168
- res.setHeader('access-control-allow-origin', '*')
169
- res.end(error instanceof Error ? error.message : String(error))
144
+ kotlinModuleRoot = host.peachKotlinModuleRoot(server.config.root)
145
+ server.watcher.add(path.join(kotlinModuleRoot, 'Sources'))
170
146
  }
147
+ const served = await host.serveKotlinRequest(pathname, res, {
148
+ root: server.config.root,
149
+ moduleRoot: kotlinModuleRoot,
150
+ origin: devOrigin(),
151
+ hmrPath,
152
+ })
153
+ if (!served) next()
171
154
  })
172
155
  .catch(next)
173
156
  })
@@ -1682,6 +1682,7 @@ export function workletsBabelTransform(
1682
1682
  // scripts/build/bundler-deps.ts so esbuild-based dep prebundles stay in sync.
1683
1683
  let flowBabel: typeof import('@babel/core') | null = null
1684
1684
  let flowStripPlugin: import('@babel/core').PluginItem | null = null
1685
+ let hermesParserPlugin: import('@babel/core').PluginItem | null = null
1685
1686
  let jsxSyntaxPlugin: import('@babel/core').PluginItem | null = null
1686
1687
  function loadFlowBabel(): void {
1687
1688
  if (flowBabel) return
@@ -1689,6 +1690,10 @@ function loadFlowBabel(): void {
1689
1690
  flowStripPlugin = sootsimPluginRequire(
1690
1691
  '@babel/plugin-transform-flow-strip-types',
1691
1692
  ) as import('@babel/core').PluginItem
1693
+ // react native uses Hermes to parse current Flow syntax, including `as` casts.
1694
+ hermesParserPlugin = sootsimPluginRequire(
1695
+ 'babel-plugin-syntax-hermes-parser',
1696
+ ) as import('@babel/core').PluginItem
1692
1697
  jsxSyntaxPlugin = sootsimPluginRequire(
1693
1698
  '@babel/plugin-syntax-jsx',
1694
1699
  ) as import('@babel/core').PluginItem
@@ -1708,7 +1713,7 @@ function flowStripTransform(): Plugin {
1708
1713
  filename: id,
1709
1714
  babelrc: false,
1710
1715
  configFile: false,
1711
- plugins: [flowStripPlugin!, jsxSyntaxPlugin!],
1716
+ plugins: [hermesParserPlugin!, flowStripPlugin!, jsxSyntaxPlugin!],
1712
1717
  sourceMaps: true,
1713
1718
  compact: false,
1714
1719
  })