peachsim 0.1.1625 → 0.1.1635
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/LICENSE +1 -1
- package/README.md +10 -1
- package/cli/app-config.ts +4 -1
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +224 -9
- package/cli/bridge-flow-runner.ts +64 -43
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +17 -184
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +26 -3
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/record.ts +21 -1
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +5 -0
- package/cli/commands/upload.ts +46 -7
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +38 -3
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +2 -1
- package/cli/outbound-endpoints.ts +13 -1
- package/cli/pause-aware-timeout.ts +102 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +23 -14
- package/detox/attempt-events.cjs +4 -0
- package/detox/index.ts +167 -142
- package/detox/motion-change.cjs +57 -21
- package/detox/motion-change.d.cts +7 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.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 +13 -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 +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +9536 -7604
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +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 +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +895 -664
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +7818 -5916
- package/dist-lib/expo-swift-metadata.cjs +3 -1
- package/dist-lib/expo-swift-metadata.mjs +3 -1
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +8340 -6392
- 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 +812 -339
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- 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 +391 -28
- package/dist-lib/native-app-identity-project.cjs +55 -2
- package/dist-lib/native-app-identity-project.mjs +55 -2
- package/dist-lib/native-app-identity.cjs +53 -1
- package/dist-lib/native-app-identity.mjs +52 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +5 -1
- package/dist-lib/react-native-host-modules.mjs +5 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +15 -3
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +12276 -8947
- package/dist-lib/swift-compile.cjs +924 -118
- package/dist-lib/swift-compile.mjs +966 -152
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +7 -1
- package/dist-lib/swift-submit.mjs +5 -1
- package/dist-lib/swift-toolchain.cjs +133 -18
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1955 -498
- package/dist-lib/vite.cjs +3 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +191 -47
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoSwiftContract.ts +13 -0
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/native-app-identity-project.ts +26 -2
- package/src/native-app-identity.ts +90 -0
- package/src/native-firebase-config.ts +209 -13
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +416 -0
- package/src/react-native-host-modules.ts +4 -0
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +835 -84
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +14 -12
- package/src/swift-host.ts +91 -29
- package/src/swift-icu.ts +14 -6
- package/src/swift-objc-sdk.ts +133 -10
- package/src/swift-objc-standin.ts +25 -3
- package/src/swift-package-manifest.ts +21 -6
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +119 -60
- package/src/swift-standin-emit.ts +57 -9
- package/src/swift-standins.ts +168 -20
- package/src/swift-submit.ts +10 -0
- package/src/swift-toolchain.ts +149 -23
- package/src/vite-plugin.ts +12 -1
package/LICENSE
CHANGED
|
@@ -8,7 +8,7 @@ the Peach Software License Agreement: https://peachsim.com/legal/license
|
|
|
8
8
|
In summary (the agreement governs): Seed is free for individuals and companies
|
|
9
9
|
under 10 people and under $1M yearly revenue, including commercial desktop and
|
|
10
10
|
local use. A company at or above either limit needs Team while it has under
|
|
11
|
-
|
|
11
|
+
25 people and under $3M yearly revenue, or Enterprise at or above either of
|
|
12
12
|
those limits. Members, agents, API keys and local simulators are unlimited.
|
|
13
13
|
Company size is self-reported and enforced through the license terms only.
|
|
14
14
|
Embedding or redistributing the software requires a separate written agreement
|
package/README.md
CHANGED
|
@@ -323,7 +323,16 @@ resolve` re-pins it, and the CLI release publishes any bundle the bucket lacks.
|
|
|
323
323
|
On Mac, headless launches attach an installed compatible native host. With no
|
|
324
324
|
host, they render in the browser without downloading or placing the app. The
|
|
325
325
|
browser logo menu offers the app; `peach desktop install --native` remains the
|
|
326
|
-
explicit CLI installer. Linux
|
|
326
|
+
explicit CLI installer. Linux x64 headless opens acquire the native standalone
|
|
327
|
+
host under `~/.peach` by default. To choose plain Chromium for an open, use
|
|
328
|
+
`peach open <target> --driver playwright --headless --chromium`.
|
|
329
|
+
Pin the native host for reproducible Linux runs with
|
|
330
|
+
`PEACH_DESKTOP_VERSION=<build>.<attempt> peach open <target> --headless`.
|
|
331
|
+
A cached pin is used directly, even if another build is active; a missing pin
|
|
332
|
+
is downloaded exactly, and an unavailable or incompatible pin fails the open.
|
|
333
|
+
`peach list --drivers` shows installed hosts and each running sim's renderer
|
|
334
|
+
and native executable path. Renderer choices select a matching sim or launch
|
|
335
|
+
one; they never change a running sim's renderer.
|
|
327
336
|
|
|
328
337
|
Headed launches on Linux use full Chromium, whose extra libraries the bundle
|
|
329
338
|
also carries. Later opens reuse that sim and
|
package/cli/app-config.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
import { createJiti } from 'jiti'
|
|
3
4
|
import * as peachConfigModule from '../src/config'
|
|
@@ -80,7 +81,9 @@ async function loadConfigFile(
|
|
|
80
81
|
[`${rnxPublicBrand.packageName}/config`]: peachConfigModule,
|
|
81
82
|
},
|
|
82
83
|
})
|
|
83
|
-
const loaded: unknown =
|
|
84
|
+
const loaded: unknown = project.configPath.endsWith('.json')
|
|
85
|
+
? JSON.parse(readFileSync(project.configPath, 'utf8'))
|
|
86
|
+
: await jiti.import(project.configPath, { default: true })
|
|
84
87
|
if (!isPeachConfig(loaded)) {
|
|
85
88
|
throw new Error('the default export must be a peach config object')
|
|
86
89
|
}
|
package/cli/app-fonts.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
resolveAppFontsWire,
|
|
8
8
|
expoFontPathsFromPlugins,
|
|
9
9
|
nativeLinkedFontSpecsFromPaths,
|
|
10
|
-
metroFontSpecsFromPaths,
|
|
11
10
|
appFontAssetUrlsFromWire,
|
|
12
11
|
type ConfigPluginFontSpec,
|
|
13
12
|
type FontPlatform,
|
|
@@ -15,8 +14,10 @@ import {
|
|
|
15
14
|
import { resolveListeningPid, resolveProcessCwd } from '../scripts/dev-server-scanner'
|
|
16
15
|
import { loopbackOriginOf } from '../src/backend-origin'
|
|
17
16
|
import { MANIFEST_PROBE_TIMEOUT_MS } from '../src/dev-bundle-resolution'
|
|
17
|
+
import { parseExpoManifestBody } from '../src/expo-manifest-body'
|
|
18
18
|
import { stageHostFile } from '../src/host/staged-host-files'
|
|
19
19
|
import { parsePlistSource } from '../src/plist.ts'
|
|
20
|
+
import { listXcodeAppTargets, readXcodeAppTarget } from '../src/xcode-project'
|
|
20
21
|
import { resolveAppProject } from './app-project'
|
|
21
22
|
import {
|
|
22
23
|
materialSymbolFontPaths,
|
|
@@ -31,24 +32,22 @@ export type DiscoveredAppFontSpec = ConfigPluginFontSpec
|
|
|
31
32
|
|
|
32
33
|
type ExpoConfigGetter = (dir: string, opts: Record<string, unknown>) => { exp?: unknown }
|
|
33
34
|
|
|
34
|
-
// every font file is found on disk, so
|
|
35
|
-
// never
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// busy the dev server is, and a cold build answers late enough to drop fonts.
|
|
35
|
+
// every font file is found on disk, so its bytes are staged as host files and
|
|
36
|
+
// never fetched from the dev server: only metro serves project files at
|
|
37
|
+
// `/assets/<path>`, and a reachability request would make the result depend on
|
|
38
|
+
// how busy the dev server is, so a cold build could answer late and drop fonts.
|
|
39
39
|
export function discoverNativeLinkedAppFonts(opts: {
|
|
40
|
-
bundleUrl: string
|
|
41
40
|
projectDir: string
|
|
42
41
|
platform?: string
|
|
43
42
|
nativeAssetOrigin: string
|
|
44
43
|
}): DiscoveredAppFontSpec[] {
|
|
45
|
-
const metroOrigin = loopbackOriginOf(opts.bundleUrl)
|
|
46
|
-
if (!metroOrigin) return []
|
|
47
44
|
const { paths, materialSymbols } = discoverNativeLinkedAppFontAssets(opts)
|
|
48
|
-
const
|
|
45
|
+
const realProjectRoot = fs.realpathSync(opts.projectDir)
|
|
46
|
+
const specs = nativeLinkedFontSpecsFromPaths(
|
|
49
47
|
projectRelativeFontPaths(opts.projectDir, paths),
|
|
50
|
-
metroOrigin,
|
|
51
48
|
nativeFontPlatform(opts.platform),
|
|
49
|
+
(fontPath) =>
|
|
50
|
+
stageHostFile(path.resolve(realProjectRoot, fontPath), opts.nativeAssetOrigin),
|
|
52
51
|
)
|
|
53
52
|
attachMaterialSymbolMetadata(specs, materialSymbols, (asset) =>
|
|
54
53
|
stageHostFile(asset, opts.nativeAssetOrigin),
|
|
@@ -286,6 +285,23 @@ function discoverNativeLinkedAppFontAssets(opts: {
|
|
|
286
285
|
!entry.name.startsWith('.'),
|
|
287
286
|
)
|
|
288
287
|
.slice(0, 64)
|
|
288
|
+
// a bare filename in UIAppFonts names a file the target copies into
|
|
289
|
+
// its bundle, wherever the project keeps it: resolve it by the target's
|
|
290
|
+
// resource build phase before trying paths beside the plist.
|
|
291
|
+
const bundledByName = new Map<string, string>()
|
|
292
|
+
for (const entry of fs.readdirSync(iosDir, { withFileTypes: true })) {
|
|
293
|
+
if (!entry.isDirectory() || !entry.name.endsWith('.xcodeproj')) continue
|
|
294
|
+
const xcodeproj = path.join(iosDir, entry.name)
|
|
295
|
+
for (const target of listXcodeAppTargets(xcodeproj, { requireSources: false })) {
|
|
296
|
+
const members = readXcodeAppTarget(xcodeproj, target, undefined, {
|
|
297
|
+
requireSources: false,
|
|
298
|
+
})
|
|
299
|
+
for (const file of members.resources) {
|
|
300
|
+
if (!bundledByName.has(path.basename(file)))
|
|
301
|
+
bundledByName.set(path.basename(file), file)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
289
305
|
const plistPaths = [path.join(iosDir, 'Info.plist')]
|
|
290
306
|
for (const entry of targetDirs) {
|
|
291
307
|
plistPaths.push(path.join(iosDir, entry.name, 'Info.plist'))
|
|
@@ -302,15 +318,19 @@ function discoverNativeLinkedAppFontAssets(opts: {
|
|
|
302
318
|
if (!Array.isArray(declaredFonts)) continue
|
|
303
319
|
for (const declaredFont of declaredFonts) {
|
|
304
320
|
if (typeof declaredFont !== 'string') continue
|
|
321
|
+
// a file the target copies into its bundle is linked wherever it
|
|
322
|
+
// lives, including a workspace package's folder outside the app.
|
|
323
|
+
const bundled = bundledByName.get(declaredFont)
|
|
305
324
|
const candidates = [
|
|
306
325
|
path.resolve(path.dirname(plistPath), declaredFont),
|
|
307
326
|
path.resolve(iosDir, declaredFont),
|
|
308
327
|
]
|
|
309
|
-
const fontPath =
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
328
|
+
const fontPath =
|
|
329
|
+
candidates.find((candidate) => {
|
|
330
|
+
const relative = path.relative(selectedAppDir, candidate)
|
|
331
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) return false
|
|
332
|
+
return isFile(candidate)
|
|
333
|
+
}) ?? (bundled && isFile(bundled) ? bundled : undefined)
|
|
314
334
|
if (fontPath) fontPaths.push(fontPath)
|
|
315
335
|
}
|
|
316
336
|
}
|
|
@@ -383,7 +403,7 @@ export async function resolveMetroProjectRoot(bundleUrl: string): Promise<string
|
|
|
383
403
|
signal: controller.signal,
|
|
384
404
|
})
|
|
385
405
|
if (res.ok) {
|
|
386
|
-
const manifest = (await res.
|
|
406
|
+
const manifest = parseExpoManifestBody(await res.text()) as {
|
|
387
407
|
extra?: { expoClient?: { _internal?: { projectRoot?: unknown } } }
|
|
388
408
|
}
|
|
389
409
|
servedManifest = true
|
package/cli/app-stubs.ts
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
// packages, and a library's own `peach` field, lowered into the engine's
|
|
4
4
|
// nativeModules / nativeViews wire slots.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
statSync,
|
|
13
|
+
} from 'node:fs'
|
|
7
14
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
8
15
|
import { resolveCompatVersionRange } from '@peach/compat/version-range'
|
|
9
16
|
import { parseSync, type Expression } from 'oxc-parser'
|
|
@@ -19,7 +26,14 @@ import {
|
|
|
19
26
|
} from '../src/config'
|
|
20
27
|
import { isRecord } from '../src/is-record'
|
|
21
28
|
import { visitParsedProgram } from '../src/visit-parsed-program'
|
|
22
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
readProjectManifest,
|
|
31
|
+
scanRepository,
|
|
32
|
+
metadataValue,
|
|
33
|
+
readProjectJson,
|
|
34
|
+
} from './commands/detect'
|
|
35
|
+
import type { ProjectManifest } from './commands/projectReportContract'
|
|
36
|
+
import type { ScanResult } from '@peach/compat/web'
|
|
23
37
|
|
|
24
38
|
// who answers a package, in rising precedence
|
|
25
39
|
export type StubOrigin = 'library' | 'stub-package' | 'config'
|
|
@@ -125,7 +139,25 @@ export function resolveAppStubs(
|
|
|
125
139
|
baseDir: dir,
|
|
126
140
|
})
|
|
127
141
|
}
|
|
128
|
-
|
|
142
|
+
const configuredPackages = Object.entries(config?.packages ?? {})
|
|
143
|
+
const packageNames = new Set<string>()
|
|
144
|
+
if (typeof manifest?.name === 'string') packageNames.add(manifest.name)
|
|
145
|
+
for (const [target] of configuredPackages) {
|
|
146
|
+
const dir = installedPackageDir(appDir, target)
|
|
147
|
+
if (dir && readPackageJson(dir)?.name === target) packageNames.add(target)
|
|
148
|
+
}
|
|
149
|
+
if (manifest && configuredPackages.some(([target]) => !packageNames.has(target))) {
|
|
150
|
+
for (const root of [appDir, ...workspacePackageDirs(appDir, manifest)])
|
|
151
|
+
for (const dir of scanRepository(root).directories) {
|
|
152
|
+
const name = readPackageJson(dir)?.name
|
|
153
|
+
if (typeof name === 'string') packageNames.add(name)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const [target, stubs] of configuredPackages) {
|
|
157
|
+
if (manifest && !packageNames.has(target))
|
|
158
|
+
throw new Error(
|
|
159
|
+
`packages[${JSON.stringify(target)}]: no installed or local package has this name; use the name from the package's package.json`,
|
|
160
|
+
)
|
|
129
161
|
sources.push({
|
|
130
162
|
target,
|
|
131
163
|
origin: 'config',
|
|
@@ -368,12 +400,17 @@ export function wireSlotMigration(
|
|
|
368
400
|
if (!owners.has(`${request.kind}:${request.name}`))
|
|
369
401
|
owners.set(`${request.kind}:${request.name}`, name)
|
|
370
402
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
403
|
+
if (manifest)
|
|
404
|
+
for (const request of scanLocalNative(
|
|
405
|
+
appDir,
|
|
406
|
+
manifest,
|
|
407
|
+
{ module: new Set(), view: new Set() },
|
|
408
|
+
undefined,
|
|
409
|
+
new Set(),
|
|
410
|
+
true,
|
|
411
|
+
))
|
|
374
412
|
if (!owners.has(`${request.kind}:${request.name}`))
|
|
375
|
-
owners.set(`${request.kind}:${request.name}`,
|
|
376
|
-
const unknownOwner = '<package that asks for it>'
|
|
413
|
+
owners.set(`${request.kind}:${request.name}`, request.package)
|
|
377
414
|
const blocks = new Map<string, { modules: string[]; views: string[] }>()
|
|
378
415
|
const manual: string[] = []
|
|
379
416
|
for (const [kind, entries] of [
|
|
@@ -395,7 +432,13 @@ export function wireSlotMigration(
|
|
|
395
432
|
)
|
|
396
433
|
continue
|
|
397
434
|
}
|
|
398
|
-
const owner = owners.get(`${kind}:${name}`)
|
|
435
|
+
const owner = owners.get(`${kind}:${name}`)
|
|
436
|
+
if (!owner) {
|
|
437
|
+
manual.push(
|
|
438
|
+
`${name}: no requesting package was found; find the package that requests this ${kind} before adding its packages entry`,
|
|
439
|
+
)
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
399
442
|
const block = blocks.get(owner) ?? { modules: [], views: [] }
|
|
400
443
|
block[kind === 'module' ? 'modules' : 'views'].push(
|
|
401
444
|
`[${JSON.stringify(name)}]: ${stub}`,
|
|
@@ -431,3 +474,175 @@ export function readSmallFile(file: string): string | null {
|
|
|
431
474
|
return null
|
|
432
475
|
}
|
|
433
476
|
}
|
|
477
|
+
|
|
478
|
+
export function hasNativeCode(
|
|
479
|
+
dir: string,
|
|
480
|
+
platform: ScanResult['scoreSummary']['platform'],
|
|
481
|
+
): boolean {
|
|
482
|
+
const expoConfig = join(dir, 'expo-module.config.json')
|
|
483
|
+
const platforms = metadataValue(readProjectJson(expoConfig), 'platforms')
|
|
484
|
+
if (Array.isArray(platforms))
|
|
485
|
+
return (
|
|
486
|
+
platforms.includes(platform) || (platform === 'ios' && platforms.includes('apple'))
|
|
487
|
+
)
|
|
488
|
+
if ([platform, 'expo-module.config.json'].some((entry) => existsSync(join(dir, entry))))
|
|
489
|
+
return true
|
|
490
|
+
if (listDir(dir).some((name) => name.endsWith('.podspec'))) return true
|
|
491
|
+
const manifest = readSmallFile(join(dir, 'package.json'))
|
|
492
|
+
return manifest !== null && manifest.includes('"codegenConfig"')
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// an app's own native code, found from the names its JavaScript asks the native
|
|
496
|
+
// side for and from local Expo modules (a directory with expo-module.config.json).
|
|
497
|
+
// each name is a key under its package in peach.config.ts `packages`.
|
|
498
|
+
export interface LocalNative {
|
|
499
|
+
kind: 'module' | 'view'
|
|
500
|
+
name: string
|
|
501
|
+
// the package the name belongs to: the nearest package.json with a name
|
|
502
|
+
package: string
|
|
503
|
+
// first place the name appears, relative to the app, as file:line
|
|
504
|
+
path: string
|
|
505
|
+
// the local Expo module directory the name belongs to, relative to the app
|
|
506
|
+
expoModule: string | null
|
|
507
|
+
swift: boolean
|
|
508
|
+
// a stub already answers it
|
|
509
|
+
answered: boolean
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const NATIVE_SOURCE_FILE = /\.(?:swift|kt)$/
|
|
513
|
+
export type AnsweredNames = Record<LocalNative['kind'], Set<string>>
|
|
514
|
+
|
|
515
|
+
export function scanLocalNative(
|
|
516
|
+
appDir: string,
|
|
517
|
+
pkg: ProjectManifest,
|
|
518
|
+
answered: AnsweredNames,
|
|
519
|
+
platform: ScanResult['scoreSummary']['platform'] | undefined,
|
|
520
|
+
knownPackages: ReadonlySet<string> = new Set(),
|
|
521
|
+
includeLegacy = false,
|
|
522
|
+
): LocalNative[] {
|
|
523
|
+
const expoModuleDirs: string[] = []
|
|
524
|
+
const excludedModuleDirs: string[] = []
|
|
525
|
+
const sourceFiles: string[] = []
|
|
526
|
+
for (const root of [appDir, ...workspacePackageDirs(appDir, pkg)]) {
|
|
527
|
+
for (const dir of scanRepository(root).directories) {
|
|
528
|
+
if (
|
|
529
|
+
excludedModuleDirs.some(
|
|
530
|
+
(excluded) => dir === excluded || dir.startsWith(`${excluded}/`),
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
continue
|
|
534
|
+
if (existsSync(join(dir, 'expo-module.config.json'))) {
|
|
535
|
+
if (platform && !hasNativeCode(dir, platform)) {
|
|
536
|
+
excludedModuleDirs.push(dir)
|
|
537
|
+
continue
|
|
538
|
+
}
|
|
539
|
+
expoModuleDirs.push(dir)
|
|
540
|
+
}
|
|
541
|
+
for (const name of listDir(dir)) {
|
|
542
|
+
if (SOURCE_FILE.test(name) && !name.endsWith('.d.ts'))
|
|
543
|
+
sourceFiles.push(join(dir, name))
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const nativeFilesByModule = new Map(
|
|
548
|
+
expoModuleDirs.map((dir) => [
|
|
549
|
+
dir,
|
|
550
|
+
scanRepository(dir).directories.flatMap((sub) =>
|
|
551
|
+
listDir(sub)
|
|
552
|
+
.filter((name) => NATIVE_SOURCE_FILE.test(name))
|
|
553
|
+
.map((name) => join(sub, name)),
|
|
554
|
+
),
|
|
555
|
+
]),
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
const owners = new Map<string, string>()
|
|
559
|
+
const owner = (dir: string): string => {
|
|
560
|
+
const known = owners.get(dir)
|
|
561
|
+
if (known) return known
|
|
562
|
+
const name = readPackageJson(dir)?.name
|
|
563
|
+
const parent = dirname(dir)
|
|
564
|
+
const result =
|
|
565
|
+
typeof name === 'string'
|
|
566
|
+
? name
|
|
567
|
+
: parent === dir
|
|
568
|
+
? (pkg.name ?? 'app')
|
|
569
|
+
: owner(parent)
|
|
570
|
+
owners.set(dir, result)
|
|
571
|
+
return result
|
|
572
|
+
}
|
|
573
|
+
const found = new Map<string, LocalNative>()
|
|
574
|
+
const add = (kind: LocalNative['kind'], name: string, file: string, line: number) => {
|
|
575
|
+
const packageName = owner(dirname(file))
|
|
576
|
+
if (knownPackages.has(packageName)) return
|
|
577
|
+
const key = `${kind}:${name}`
|
|
578
|
+
if (found.has(key)) return
|
|
579
|
+
const moduleDir = expoModuleDirs.find((dir) => file.startsWith(`${dir}/`)) ?? null
|
|
580
|
+
found.set(key, {
|
|
581
|
+
kind,
|
|
582
|
+
name,
|
|
583
|
+
package: packageName,
|
|
584
|
+
path: `${relative(appDir, file)}:${line}`,
|
|
585
|
+
expoModule: moduleDir ? relative(appDir, moduleDir) || '.' : null,
|
|
586
|
+
swift: moduleDir
|
|
587
|
+
? (nativeFilesByModule.get(moduleDir) ?? []).some((path) =>
|
|
588
|
+
path.endsWith('.swift'),
|
|
589
|
+
)
|
|
590
|
+
: false,
|
|
591
|
+
answered: answered[kind].has(name),
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
for (const file of sourceFiles) {
|
|
595
|
+
const text = readSmallFile(file)
|
|
596
|
+
if (!text) continue
|
|
597
|
+
for (const { kind, name, line } of sourceNativeRequests(file, text, includeLegacy))
|
|
598
|
+
add(kind, name, file, line)
|
|
599
|
+
}
|
|
600
|
+
// a local expo module declares its name in its native definition, Name("X"),
|
|
601
|
+
// whether or not the app's javascript spells it out. swift first, so a
|
|
602
|
+
// module the report marks Swift points at its swift definition.
|
|
603
|
+
for (const [, files] of nativeFilesByModule) {
|
|
604
|
+
const swiftFirst = [...files].sort(
|
|
605
|
+
(a, b) => Number(b.endsWith('.swift')) - Number(a.endsWith('.swift')),
|
|
606
|
+
)
|
|
607
|
+
for (const file of swiftFirst) {
|
|
608
|
+
const text = readSmallFile(file)
|
|
609
|
+
if (!text) continue
|
|
610
|
+
for (const match of text.matchAll(/\bName\(\s*"(\w+)"\s*\)/g)) {
|
|
611
|
+
if (match[1])
|
|
612
|
+
add('module', match[1], file, text.slice(0, match.index).split('\n').length)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return [...found.values()].sort(
|
|
617
|
+
(a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name),
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// dependencies installed as links to source outside any node_modules are the
|
|
622
|
+
// app's own workspace packages, which hold its native code as often as the app
|
|
623
|
+
// directory does.
|
|
624
|
+
function workspacePackageDirs(appDir: string, pkg: ProjectManifest): string[] {
|
|
625
|
+
const dirs = new Set<string>()
|
|
626
|
+
for (const name of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
627
|
+
let current = appDir
|
|
628
|
+
while (true) {
|
|
629
|
+
const link = join(current, 'node_modules', name)
|
|
630
|
+
if (existsSync(link)) {
|
|
631
|
+
if (lstatSync(link).isSymbolicLink()) {
|
|
632
|
+
const target = realpathSync(link)
|
|
633
|
+
if (
|
|
634
|
+
!target.split('/').includes('node_modules') &&
|
|
635
|
+
!target.startsWith(`${appDir}/`)
|
|
636
|
+
) {
|
|
637
|
+
dirs.add(target)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
break
|
|
641
|
+
}
|
|
642
|
+
const parent = dirname(current)
|
|
643
|
+
if (parent === current) break
|
|
644
|
+
current = parent
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return [...dirs]
|
|
648
|
+
}
|
|
@@ -2175,48 +2175,58 @@ export class SootSimBridgeFlowRunner {
|
|
|
2175
2175
|
await sleep(120)
|
|
2176
2176
|
}
|
|
2177
2177
|
|
|
2178
|
+
// maestro's android driver sends Back. its ios driver never presses a key:
|
|
2179
|
+
// while the keyboard shows it swipes from the screen center up 3%, then
|
|
2180
|
+
// left 3%, and the app's own handling of those touches dismisses it, or
|
|
2181
|
+
// presses whatever sits under the center
|
|
2178
2182
|
async hideKeyboard() {
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
await this.callTest<boolean>('blurFocusedTextInput')
|
|
2185
|
-
} catch {
|
|
2186
|
-
// fall through to the shell-level dismiss below; older bridges may not
|
|
2187
|
-
// expose the text-input helper yet.
|
|
2183
|
+
if (this.js.maestro.platform.toLowerCase() === 'android') {
|
|
2184
|
+
await this.back()
|
|
2185
|
+
await sleep(300)
|
|
2186
|
+
await this.waitForAppToSettle()
|
|
2187
|
+
return
|
|
2188
2188
|
}
|
|
2189
|
+
if (!(await this.isKeyboardVisible())) return
|
|
2190
|
+
const screen = await this.screenBounds()
|
|
2191
|
+
const center = parsePoint('50%,50%', screen)
|
|
2192
|
+
await this.xctestSwipe(center, parsePoint('50%,47%', screen), 50)
|
|
2193
|
+
if (!(await this.isKeyboardVisible())) return
|
|
2194
|
+
await this.xctestSwipe(center, parsePoint('47%,50%', screen), 50)
|
|
2195
|
+
await this.waitForAppToSettle()
|
|
2196
|
+
}
|
|
2189
2197
|
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
}
|
|
2198
|
+
// the xctest runner's swipe: press, move to the end over its 100ms tap
|
|
2199
|
+
// duration, rest there for the requested duration, then lift, so the
|
|
2200
|
+
// finger leaves at rest rather than flinging
|
|
2201
|
+
private async xctestSwipe(
|
|
2202
|
+
from: { x: number; y: number },
|
|
2203
|
+
to: { x: number; y: number },
|
|
2204
|
+
durationMs: number,
|
|
2205
|
+
) {
|
|
2206
|
+
const moves = 6
|
|
2207
|
+
const steps: PerformStep[] = [{ type: 'touchDown', x: from.x, y: from.y }]
|
|
2208
|
+
for (let i = 1; i <= moves; i++) {
|
|
2209
|
+
steps.push({ type: 'wait', ms: 100 / moves })
|
|
2210
|
+
steps.push({
|
|
2211
|
+
type: 'touchMove',
|
|
2212
|
+
x: from.x + ((to.x - from.x) * i) / moves,
|
|
2213
|
+
y: from.y + ((to.y - from.y) * i) / moves,
|
|
2214
|
+
})
|
|
2215
|
+
}
|
|
2216
|
+
steps.push({ type: 'wait', ms: durationMs })
|
|
2217
|
+
steps.push({ type: 'touchUp', x: to.x, y: to.y })
|
|
2218
|
+
const result = await this.perform(steps)
|
|
2219
|
+
if (!result?.ok) throw new Error(`swipe failed: ${result?.error ?? 'swipe missed'}`)
|
|
2220
|
+
await this.waitForAppToSettle()
|
|
2221
|
+
}
|
|
2195
2222
|
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
}>(`(async () => {
|
|
2204
|
-
const keyboard =
|
|
2205
|
-
window.__sootsimKeyboard ??
|
|
2206
|
-
window.SootSim?.bridges?.keyboard ??
|
|
2207
|
-
null
|
|
2208
|
-
const test = window.__sootsimTest
|
|
2209
|
-
return {
|
|
2210
|
-
visible: !!keyboard?.isVisible?.(),
|
|
2211
|
-
focused: !!(await test?.isTextInputFocused?.()),
|
|
2212
|
-
}
|
|
2213
|
-
})()`)
|
|
2214
|
-
if (!state?.visible && !state?.focused) {
|
|
2215
|
-
await sleep(80)
|
|
2216
|
-
return
|
|
2217
|
-
}
|
|
2218
|
-
await sleep(80)
|
|
2219
|
-
} while (Date.now() < deadline)
|
|
2223
|
+
private async isKeyboardVisible(): Promise<boolean> {
|
|
2224
|
+
const visible = await this.evaluate<boolean>(`(() => {
|
|
2225
|
+
const keyboard =
|
|
2226
|
+
window.__sootsimKeyboard ?? window.SootSim?.bridges?.keyboard ?? null
|
|
2227
|
+
return !!keyboard?.isVisible?.()
|
|
2228
|
+
})()`)
|
|
2229
|
+
return visible === true
|
|
2220
2230
|
}
|
|
2221
2231
|
|
|
2222
2232
|
async eraseText(count: number) {
|
|
@@ -2765,8 +2775,15 @@ export class SootSimBridgeFlowRunner {
|
|
|
2765
2775
|
: { x: centerX, y: screen.height * 0.1 }
|
|
2766
2776
|
const element = await this.findElement(target)
|
|
2767
2777
|
if (element) {
|
|
2768
|
-
|
|
2769
|
-
const { width, height } =
|
|
2778
|
+
// maestro computes visibility from integer edges, as it does selection.
|
|
2779
|
+
const { x, y, width, height } = accessibilityBounds({
|
|
2780
|
+
frame: {
|
|
2781
|
+
x: element.absolutePosition.x,
|
|
2782
|
+
y: element.absolutePosition.y,
|
|
2783
|
+
width: element.layout.width,
|
|
2784
|
+
height: element.layout.height,
|
|
2785
|
+
},
|
|
2786
|
+
})
|
|
2770
2787
|
const shownX = Math.max(0, Math.min(x + width, screen.width) - Math.max(x, 0))
|
|
2771
2788
|
const shownY = Math.max(0, Math.min(y + height, screen.height) - Math.max(y, 0))
|
|
2772
2789
|
const share = width * height > 0 ? (shownX * shownY) / (width * height) : 0
|
|
@@ -2892,6 +2909,12 @@ export class SootSimBridgeFlowRunner {
|
|
|
2892
2909
|
const wantsClear = !!(opts && typeof opts === 'object' && opts.clearState)
|
|
2893
2910
|
const wantsResetRuntime = readBooleanKey(opts, 'resetRuntime')
|
|
2894
2911
|
const wantsForeground = isObjectRecord(opts) && opts.stopApp === false
|
|
2912
|
+
// maestro's launchApp clears the keychain, then app state, then sets
|
|
2913
|
+
// permissions and launches
|
|
2914
|
+
if (readBooleanKey(opts, 'clearKeychain')) {
|
|
2915
|
+
if (!this.firstLaunchDone && !this.appStopped) await this.waitForTree(120000)
|
|
2916
|
+
await this.clearKeychain()
|
|
2917
|
+
}
|
|
2895
2918
|
await this.setLaunchPermissions(isObjectRecord(opts) ? opts.permissions : undefined)
|
|
2896
2919
|
|
|
2897
2920
|
if (!this.firstLaunchDone) {
|
|
@@ -2982,9 +3005,7 @@ export class SootSimBridgeFlowRunner {
|
|
|
2982
3005
|
|
|
2983
3006
|
private async setLaunchPermissions(permissions: unknown) {
|
|
2984
3007
|
const statuses = maestroLaunchPermissions(permissions)
|
|
2985
|
-
const
|
|
2986
|
-
const appId = readStringKey(state, 'activeApp') || readRecentShellAppId(state)
|
|
2987
|
-
if (!appId) throw new Error('launchApp: no simulator app to set permissions for')
|
|
3008
|
+
const appId = await this.callTest<string>('permissionGuestAppId')
|
|
2988
3009
|
for (const [kind, status] of statuses) {
|
|
2989
3010
|
await this.callTest('permissionSetAppPermission', appId, kind, {
|
|
2990
3011
|
status,
|