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.
- package/README.md +8 -4
- package/cli/bridge-flow-runner.ts +10 -3
- package/cli/cli-update.ts +2 -7
- package/cli/cloud-test-tunnel.ts +16 -3
- package/cli/commands/config.ts +1 -1
- package/cli/commands/control.ts +28 -3
- package/cli/commands/daemon-mac-app.ts +4 -2
- package/cli/commands/daemon.ts +4 -4
- package/cli/commands/device.ts +2 -3
- package/cli/commands/inspect/actions.ts +125 -90
- package/cli/commands/inspect/core.ts +5 -0
- package/cli/commands/inspect.ts +27 -13
- package/cli/commands/platform.ts +1 -1
- package/cli/commands/screenshot-layers.ts +1 -1
- package/cli/commands/screenshot.ts +1 -1
- package/cli/commands/skills.ts +0 -31
- package/cli/drivers/playwright-provisioning.ts +3 -2
- package/cli/drivers/playwright-sim-host.ts +25 -9
- package/cli/main.ts +8 -2
- package/cli/outbound-endpoints.ts +0 -4
- package/detox/index.ts +74 -43
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +129 -27
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +290 -315
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +253 -121
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/{vite-kotlin.cjs → kotlin-host.cjs} +130 -19
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +132 -9
- package/dist-lib/native-app-identity.cjs +278 -0
- package/dist-lib/native-app-identity.mjs +245 -0
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +110 -87
- package/dist-lib/sdk.mjs +110 -87
- package/dist-lib/skills.cjs +841 -643
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +282 -177
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +11 -28
- package/kotlin/README.md +32 -4
- package/kotlin/fixtures/counter/Counter.kt +26 -0
- package/kotlin/fixtures/counter/CounterScreen.kt +40 -0
- package/kotlin/fixtures/headless-counter/Headless.kt +228 -0
- package/kotlin/fixtures/runtime-state/RuntimeState.kt +200 -0
- package/kotlin/fork/LICENSE +178 -0
- package/kotlin/fork/NOTICE +25 -0
- package/kotlin/fork/README.md +59 -0
- package/kotlin/fork/build.sh +128 -0
- package/kotlin/fork/compose-worker-globals.patch +96 -0
- package/kotlin/fork/fork.json +33 -0
- package/kotlin/fork/publish.ts +77 -0
- package/kotlin/fork/skiko-worker-clock.patch +33 -0
- package/kotlin/toolchain.json +288 -0
- package/package.json +12 -5
- package/src/automatic-update-policy.ts +17 -0
- package/src/host/bridge-host.ts +3 -1
- package/src/{vite-plugin-kotlin.ts → kotlin-host.ts} +155 -12
- package/src/metro-plugin.ts +18 -0
- package/src/native-app-identity.ts +376 -0
- package/src/native-firebase-config.ts +90 -0
- package/src/screenshots/frame-compose.ts +5 -2
- package/src/screenshots/registry.ts +1 -1
- package/src/screenshots/schema.ts +2 -2
- package/src/vite-plugin-one.ts +11 -28
- 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
|
|
@@ -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'
|
package/src/vite-plugin-one.ts
CHANGED
|
@@ -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/
|
|
45
|
-
const loadKotlinHost = () => (kotlinHost ??= import('peachsim/
|
|
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
|
-
|
|
147
|
-
|
|
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
|
})
|
package/src/vite-plugin.ts
CHANGED
|
@@ -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
|
})
|