peachsim 0.1.1620 → 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 +45 -6
- package/cli/app-config.ts +4 -1
- package/cli/app-fonts.ts +39 -18
- package/cli/app-stubs.ts +224 -9
- package/cli/bridge-flow-runner.ts +248 -156
- package/cli/cloud-client.ts +68 -57
- package/cli/command-registry.ts +1 -0
- package/cli/commands/box/search-command.ts +3 -2
- package/cli/commands/box.ts +10 -29
- package/cli/commands/control.ts +391 -92
- 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 +71 -22
- 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 +105 -67
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/platform.ts +71 -36
- 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 +51 -8
- package/cli/desktop-companion.ts +102 -17
- package/cli/dev-build-freshness.ts +37 -2
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +44 -8
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +53 -0
- package/cli/maestro-compare.ts +108 -12
- package/cli/main.ts +2 -1
- package/cli/outbound-endpoints.ts +35 -6
- package/cli/parent-pid.ts +0 -9
- package/cli/pause-aware-timeout.ts +102 -0
- package/cli/remote-simulators.ts +112 -0
- package/cli/remoteSimulatorContract.ts +21 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +23 -14
- package/detox/attempt-events.cjs +4 -0
- package/detox/expectations.ts +5 -1
- package/detox/index.ts +241 -181
- package/detox/motion-change.cjs +73 -0
- package/detox/motion-change.d.cts +21 -0
- package/detox/navigation-lifecycle.ts +22 -4
- package/dist-lib/agent-daemon-client.cjs +3 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +3 -2
- 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 +10480 -7962
- 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 +3 -2
- package/dist-lib/cloud.mjs +3 -2
- package/dist-lib/config.cjs +10 -6
- package/dist-lib/config.mjs +10 -6
- package/dist-lib/detox/index.cjs +1854 -966
- package/dist-lib/dev-bundle-resolution.cjs +64 -24
- package/dist-lib/expo-swift-bundle.cjs +8715 -6271
- package/dist-lib/expo-swift-metadata.cjs +7 -4
- package/dist-lib/expo-swift-metadata.mjs +7 -4
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +10135 -7060
- 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 +1843 -408
- 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 +3 -2
- package/dist-lib/menu.mjs +3 -2
- 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 +5 -4
- package/dist-lib/metro-production-bundle.mjs +5 -4
- package/dist-lib/metro.cjs +1182 -418
- package/dist-lib/native-app-identity-project.cjs +782 -23
- package/dist-lib/native-app-identity-project.mjs +781 -23
- package/dist-lib/native-app-identity.cjs +276 -9
- package/dist-lib/native-app-identity.mjs +273 -8
- package/dist-lib/native-firebase-metadata.cjs +5 -4
- package/dist-lib/native-firebase-metadata.mjs +5 -4
- 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 +3 -2
- package/dist-lib/react-native-host-modules.cjs +8 -1
- package/dist-lib/react-native-host-modules.mjs +8 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
- package/dist-lib/sdk.cjs +115 -34
- package/dist-lib/sdk.mjs +115 -33
- package/dist-lib/skills.cjs +15274 -9904
- package/dist-lib/swift-compile.cjs +1165 -173
- package/dist-lib/swift-compile.mjs +1212 -203
- 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 +2648 -862
- package/dist-lib/vite.cjs +70 -14
- package/dist-lib/worklet-executable.cjs +25 -2
- package/dist-lib/worklet-executable.mjs +25 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +2 -2
- package/scripts/dev-server-scanner.ts +35 -14
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/admitted-mac-build.ts +34 -8
- package/src/android-app-identity.ts +146 -0
- package/src/android-autolinking.ts +75 -0
- package/src/android-string-value.ts +36 -0
- package/src/android-strings.ts +5 -36
- package/src/androidAutolinkingContract.ts +40 -0
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/bundleGlobalFooter.ts +26 -7
- package/src/codex-client.ts +3 -2
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +60 -26
- package/src/expo-kotlin-host.ts +6 -36
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-bundle.ts +17 -12
- package/src/expo-swift-host.ts +640 -192
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoSwiftContract.ts +19 -0
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +12 -57
- package/src/host/staged-host-files.ts +29 -2
- package/src/installed-package-versions.ts +54 -14
- package/src/launchMetadataContract.ts +24 -0
- package/src/metro-plugin.ts +37 -42
- package/src/native-app-identity-project.ts +142 -5
- package/src/native-app-identity.ts +176 -11
- package/src/native-firebase-config.ts +209 -13
- package/src/nitro-compile.ts +2 -5
- package/src/nitro-source.ts +147 -63
- package/src/process-group.ts +114 -61
- package/src/process-output.ts +0 -9
- package/src/public-brand.ts +1 -0
- package/src/react-native-bridge-modules.ts +416 -0
- package/src/react-native-host-modules.ts +7 -0
- package/src/sim-client.ts +6 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +1007 -108
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +40 -12
- package/src/swift-distribution.ts +61 -9
- package/src/swift-host.ts +149 -56
- package/src/swift-icu.ts +14 -6
- package/src/swift-objc-sdk.ts +133 -10
- package/src/swift-objc-standin.ts +38 -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 +185 -25
- package/src/swift-submit.ts +10 -0
- package/src/swift-toolchain.ts +149 -23
- package/src/vite-plugin.ts +20 -20
- package/src/worklet-executable.ts +30 -3
- package/src/worklets-babel.ts +35 -14
- package/src/xcode-project.ts +10 -0
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
|
@@ -231,6 +231,11 @@ actions discover the sim's owning world. Each world probe has one two-second
|
|
|
231
231
|
wall deadline covering both connection and response. An inferred default port
|
|
232
232
|
does not override the named sim's world.
|
|
233
233
|
|
|
234
|
+
Bridge sockets use TCP keepalive so a governor-stopped browser can resume an
|
|
235
|
+
in-flight render without losing its connection to an unanswered application
|
|
236
|
+
ping. Command deadlines still bound stalled work; idle leases and abandoned
|
|
237
|
+
sim cleanup remain separate from transport liveness.
|
|
238
|
+
|
|
234
239
|
**The sim** is a browser/Electron *page* launched by a CLI driver. Inside that
|
|
235
240
|
page run three workers: the **shell worker** owns device and native state, the
|
|
236
241
|
**compositor worker** paints every visible CanvasKit surface, and the **tenant
|
|
@@ -279,7 +284,7 @@ therefore runs before assertions in each flow.
|
|
|
279
284
|
| runtime delivery + Peach home | Versioned engine runtime management. The shared machinery owns manifest fetch, sha256 verification, channels, and auto-update. Peach binds it to `https://contrast.dev` and `~/.peach/runtimes/<version>`. A repo can select `runtimeVersion` without changing the machine default. | `src/runtime-delivery.ts`, `packages/contrast-runtime-delivery/`, `src/home-paths.ts`, `src/runtime-assets.ts` |
|
|
280
285
|
| standalone CLI delivery | Cached stable-version checks, bounded interactive notices, and verified atomic executable replacement. | `cli/cli-update.ts`, `cli/commands/upgrade.ts`, `cli/commands/version.ts`, `src/cli-version.ts` |
|
|
281
286
|
| `peachsim/vite` + `peachsim/metro` (runtime serving) | Published bundler plugins. Both serve the installed engine runtime at `/__peachsim/` from `~/.peach/runtimes/<version>` so an app's own dev server can host the sim shell; the Vite plugin can opt into a named Electron window. | `src/vite-plugin-one.ts`, `src/metro-plugin.ts`, `src/runtime-assets.ts` |
|
|
282
|
-
| `sootsim()` vite resolver plugin (internal) | The large RN-resolution / native-stub vite plugin: aliases `react-native` to the engine shim, native packages to `@sootsim/compat` stubs, applies worklets/babel transforms
|
|
287
|
+
| `sootsim()` vite resolver plugin (internal) | The large RN-resolution / native-stub vite plugin: aliases `react-native` to the engine shim, native packages to `@sootsim/compat` stubs, and applies worklets/babel transforms. Distinct from the published `peachsim/vite` export. | `src/vite-plugin.ts`, `src/worklets-babel.ts`, `packages/peach-compat/src/stub-manifest.ts` |
|
|
283
288
|
| skills registry | Peach agent skills (`peach-setup`, `peach-debug`, `peach-test`, `peach-visual`, and the generated `contrast` index) installable with `peach skill`. | `skills/*.md`, `src/skills/registry.ts`, `src/skills/types.ts` |
|
|
284
289
|
|
|
285
290
|
## Getting started
|
|
@@ -318,7 +323,16 @@ resolve` re-pins it, and the CLI release publishes any bundle the bucket lacks.
|
|
|
318
323
|
On Mac, headless launches attach an installed compatible native host. With no
|
|
319
324
|
host, they render in the browser without downloading or placing the app. The
|
|
320
325
|
browser logo menu offers the app; `peach desktop install --native` remains the
|
|
321
|
-
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.
|
|
322
336
|
|
|
323
337
|
Headed launches on Linux use full Chromium, whose extra libraries the bundle
|
|
324
338
|
also carries. Later opens reuse that sim and
|
|
@@ -510,8 +524,8 @@ a supported input; pass Peach the JavaScript bundle produced before Hermes
|
|
|
510
524
|
compilation.
|
|
511
525
|
|
|
512
526
|
If an app has unusual dependencies or fingerprint inference fails, you can
|
|
513
|
-
optionally wrap your Metro configuration with `withPeach(config)`. It
|
|
514
|
-
|
|
527
|
+
optionally wrap your Metro configuration with `withPeach(config)`. It enables
|
|
528
|
+
two integrations:
|
|
515
529
|
|
|
516
530
|
1. **Deterministic module identity:** appends Metro's exact module identity map
|
|
517
531
|
as a trailing `globalThis.__sootsimModulePaths = { ... }` footer to every
|
|
@@ -521,6 +535,23 @@ options and enables two integrations:
|
|
|
521
535
|
2. **In-browser dev shell:** serves the Peach simulator shell at `/__peachsim/` on
|
|
522
536
|
the Metro dev server.
|
|
523
537
|
|
|
538
|
+
Native app identity also travels in the bundle. Android permissions come only
|
|
539
|
+
from a completed app manifest merger output. A sole output under
|
|
540
|
+
`android/<module>/build/intermediates/merged_manifests/` is selected automatically;
|
|
541
|
+
select one explicitly when multiple variants exist:
|
|
542
|
+
|
|
543
|
+
```js
|
|
544
|
+
module.exports = withPeach(config, {
|
|
545
|
+
androidMergedManifest:
|
|
546
|
+
'android/app/build/intermediates/merged_manifests/debug/processDebugManifest/AndroidManifest.xml',
|
|
547
|
+
})
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Without resolved merged output, Android permissions contain only the explicit
|
|
551
|
+
Expo `android.permissions` set minus `android.blockedPermissions`. Defaults,
|
|
552
|
+
plugins and linked library additions require completed merged output. See the
|
|
553
|
+
[native identity contract](../../specs/sootsim-engine/native-app-identity.md).
|
|
554
|
+
|
|
524
555
|
The URL helpers request ordinary Metro graphs: `dev=false&minify=true` for
|
|
525
556
|
production and `dev=true&minify=false` for development.
|
|
526
557
|
|
|
@@ -571,6 +602,12 @@ and video recording create a new browser context; cold launches still use the
|
|
|
571
602
|
supplied shell URL. When synchronization is enabled, URL delivery waits for
|
|
572
603
|
existing work before dispatch and for navigation and painting afterward.
|
|
573
604
|
|
|
605
|
+
Element taps finish viewport measurement before resolving the selected node's
|
|
606
|
+
current center, visible frame, and relative offset in the browser task that
|
|
607
|
+
sends the physical tap. Stillness samples do not supply later tap coordinates:
|
|
608
|
+
startup layout can change while other bridge reads finish. A removed node
|
|
609
|
+
rejects the action instead of reusing its old geometry.
|
|
610
|
+
|
|
574
611
|
See `../../docs/migrating-from-detox.md` for details.
|
|
575
612
|
|
|
576
613
|
## Coming from Maestro
|
|
@@ -751,8 +788,10 @@ fails instead of loading empty native configuration. Bare Metro, which has no
|
|
|
751
788
|
Expo manifest project root, uses
|
|
752
789
|
the listening development process's cwd inside that same resolver. The invoking
|
|
753
790
|
CLI's cwd is never an asset/config fallback. Asset wires stay on each opened
|
|
754
|
-
project URL
|
|
755
|
-
|
|
791
|
+
project URL. Font catalogs use content-addressed bridge asset references, so
|
|
792
|
+
the navigation request stays bounded as the number of fonts grows. Recordings
|
|
793
|
+
resolve these references before capturing the catalog and its assets. One daemon
|
|
794
|
+
can serve several attached projects without sharing font or splash state.
|
|
756
795
|
|
|
757
796
|
Bare iOS projects carry default Firebase options from their selected app
|
|
758
797
|
target's Google services build resource when the app delegate initializes
|
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
|
@@ -4,9 +4,9 @@ import { createRequire } from 'node:module'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import {
|
|
6
6
|
encodeAppFontsWire,
|
|
7
|
+
resolveAppFontsWire,
|
|
7
8
|
expoFontPathsFromPlugins,
|
|
8
9
|
nativeLinkedFontSpecsFromPaths,
|
|
9
|
-
metroFontSpecsFromPaths,
|
|
10
10
|
appFontAssetUrlsFromWire,
|
|
11
11
|
type ConfigPluginFontSpec,
|
|
12
12
|
type FontPlatform,
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
import { resolveListeningPid, resolveProcessCwd } from '../scripts/dev-server-scanner'
|
|
15
15
|
import { loopbackOriginOf } from '../src/backend-origin'
|
|
16
16
|
import { MANIFEST_PROBE_TIMEOUT_MS } from '../src/dev-bundle-resolution'
|
|
17
|
+
import { parseExpoManifestBody } from '../src/expo-manifest-body'
|
|
17
18
|
import { stageHostFile } from '../src/host/staged-host-files'
|
|
18
19
|
import { parsePlistSource } from '../src/plist.ts'
|
|
20
|
+
import { listXcodeAppTargets, readXcodeAppTarget } from '../src/xcode-project'
|
|
19
21
|
import { resolveAppProject } from './app-project'
|
|
20
22
|
import {
|
|
21
23
|
materialSymbolFontPaths,
|
|
@@ -30,24 +32,22 @@ export type DiscoveredAppFontSpec = ConfigPluginFontSpec
|
|
|
30
32
|
|
|
31
33
|
type ExpoConfigGetter = (dir: string, opts: Record<string, unknown>) => { exp?: unknown }
|
|
32
34
|
|
|
33
|
-
// every font file is found on disk, so
|
|
34
|
-
// never
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// 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.
|
|
38
39
|
export function discoverNativeLinkedAppFonts(opts: {
|
|
39
|
-
bundleUrl: string
|
|
40
40
|
projectDir: string
|
|
41
41
|
platform?: string
|
|
42
42
|
nativeAssetOrigin: string
|
|
43
43
|
}): DiscoveredAppFontSpec[] {
|
|
44
|
-
const metroOrigin = loopbackOriginOf(opts.bundleUrl)
|
|
45
|
-
if (!metroOrigin) return []
|
|
46
44
|
const { paths, materialSymbols } = discoverNativeLinkedAppFontAssets(opts)
|
|
47
|
-
const
|
|
45
|
+
const realProjectRoot = fs.realpathSync(opts.projectDir)
|
|
46
|
+
const specs = nativeLinkedFontSpecsFromPaths(
|
|
48
47
|
projectRelativeFontPaths(opts.projectDir, paths),
|
|
49
|
-
metroOrigin,
|
|
50
48
|
nativeFontPlatform(opts.platform),
|
|
49
|
+
(fontPath) =>
|
|
50
|
+
stageHostFile(path.resolve(realProjectRoot, fontPath), opts.nativeAssetOrigin),
|
|
51
51
|
)
|
|
52
52
|
attachMaterialSymbolMetadata(specs, materialSymbols, (asset) =>
|
|
53
53
|
stageHostFile(asset, opts.nativeAssetOrigin),
|
|
@@ -285,6 +285,23 @@ function discoverNativeLinkedAppFontAssets(opts: {
|
|
|
285
285
|
!entry.name.startsWith('.'),
|
|
286
286
|
)
|
|
287
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
|
+
}
|
|
288
305
|
const plistPaths = [path.join(iosDir, 'Info.plist')]
|
|
289
306
|
for (const entry of targetDirs) {
|
|
290
307
|
plistPaths.push(path.join(iosDir, entry.name, 'Info.plist'))
|
|
@@ -301,15 +318,19 @@ function discoverNativeLinkedAppFontAssets(opts: {
|
|
|
301
318
|
if (!Array.isArray(declaredFonts)) continue
|
|
302
319
|
for (const declaredFont of declaredFonts) {
|
|
303
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)
|
|
304
324
|
const candidates = [
|
|
305
325
|
path.resolve(path.dirname(plistPath), declaredFont),
|
|
306
326
|
path.resolve(iosDir, declaredFont),
|
|
307
327
|
]
|
|
308
|
-
const fontPath =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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)
|
|
313
334
|
if (fontPath) fontPaths.push(fontPath)
|
|
314
335
|
}
|
|
315
336
|
}
|
|
@@ -382,7 +403,7 @@ export async function resolveMetroProjectRoot(bundleUrl: string): Promise<string
|
|
|
382
403
|
signal: controller.signal,
|
|
383
404
|
})
|
|
384
405
|
if (res.ok) {
|
|
385
|
-
const manifest = (await res.
|
|
406
|
+
const manifest = parseExpoManifestBody(await res.text()) as {
|
|
386
407
|
extra?: { expoClient?: { _internal?: { projectRoot?: unknown } } }
|
|
387
408
|
}
|
|
388
409
|
servedManifest = true
|
|
@@ -448,7 +469,7 @@ export async function fetchAppFontFiles(
|
|
|
448
469
|
): Promise<FetchedAppFont[]> {
|
|
449
470
|
const out: FetchedAppFont[] = []
|
|
450
471
|
const seen = new Set<string>()
|
|
451
|
-
for (const url of appFontAssetUrlsFromWire(wire)) {
|
|
472
|
+
for (const url of appFontAssetUrlsFromWire(await resolveAppFontsWire(wire, fetch))) {
|
|
452
473
|
const urlhash = createHash('sha256').update(url).digest('hex')
|
|
453
474
|
if (seen.has(urlhash)) continue
|
|
454
475
|
seen.add(urlhash)
|
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
|
+
}
|