peachsim 0.1.1406 → 0.1.1465
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 +8 -4
- package/README.md +22 -2
- package/cli/app-fonts.ts +50 -44
- package/cli/bin.ts +5 -4
- package/cli/bridge-diagnostics.ts +5 -0
- package/cli/bridge-flow-runner.ts +21 -13
- package/cli/cloud-client.ts +4 -3
- package/cli/commands/control.ts +155 -123
- package/cli/commands/daemon-mac-app.ts +3 -3
- package/cli/commands/daemon.ts +2 -2
- package/cli/commands/do-chain.ts +9 -2
- package/cli/commands/electron.ts +4 -1
- package/cli/commands/inspect/core.ts +1 -1
- package/cli/commands/inspect/settle.ts +19 -7
- package/cli/commands/inspect/settling.ts +92 -56
- package/cli/commands/inspect/wait-idle.ts +21 -9
- package/cli/commands/inspect/wait-ready.ts +22 -8
- package/cli/commands/inspect.ts +101 -134
- package/cli/commands/setup.ts +26 -16
- package/cli/commands/upgrade.ts +53 -9
- package/cli/desktop-companion.ts +1 -1
- package/cli/drivers/playwright-sim-host.ts +32 -13
- package/cli/drivers/playwright.ts +3 -3
- package/cli/main.ts +14 -5
- package/cli/outbound-endpoints.ts +1 -9
- package/detox/index.ts +6 -1
- 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 +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 +3 -3
- package/dist-lib/bridge-contract.mjs +3 -3
- package/dist-lib/bundle-channel.cjs +322 -274
- 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 +1 -1
- package/dist-lib/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +512 -122
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +69 -33
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +3650 -3528
- 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 +327 -123
- 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 +10 -5
- 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 +49 -47
- package/dist-lib/native-app-identity-project.cjs +1 -1
- package/dist-lib/native-app-identity-project.mjs +1 -1
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -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 +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +23 -17
- package/dist-lib/sdk.cjs +99 -61
- package/dist-lib/sdk.mjs +97 -61
- package/dist-lib/skills.cjs +3347 -3051
- package/dist-lib/swift-compile.cjs +26 -12
- package/dist-lib/swift-compile.mjs +26 -12
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +2 -2
- package/dist-lib/swift-submit.mjs +2 -2
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +66 -31
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +5 -1
- package/dist-lib/worklet-executable.mjs +5 -1
- package/kotlin/AppSources/android/view/Window.kt +2 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +1 -1
- package/kotlin/ComposeSources/PeachComposeHost.kt +35 -9
- package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +2 -2
- package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +9 -6
- package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +4 -4
- package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +2 -2
- package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +22 -8
- package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +0 -3
- package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +10 -9
- package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +6 -5
- package/kotlin/fixtures/import-drawing/Drawing.d.kt.ts +7 -0
- package/kotlin/fixtures/import-drawing/Drawing.kt +101 -0
- package/kotlin/fixtures/import-drawing/ImportDrawing.native.tsx +18 -0
- package/kotlin/fixtures/import-drawing/README.md +34 -0
- package/kotlin/fixtures/import-drawing/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-drawing/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-drawing/package.json +23 -0
- package/kotlin/fixtures/import-drawing/tsconfig.json +20 -0
- package/kotlin/fixtures/import-drawing/vite.config.ts +20 -0
- package/kotlin/fixtures/import-scroll/ImportNested.native.tsx +10 -0
- package/kotlin/fixtures/import-scroll/Nested.d.kt.ts +4 -0
- package/kotlin/fixtures/import-scroll/README.md +15 -0
- package/kotlin/fixtures/import-scroll/app/nested+spa.tsx +1 -0
- package/kotlin/fixtures/import-values/ImportValues.native.tsx +26 -0
- package/kotlin/fixtures/import-values/README.md +6 -1
- package/kotlin/fixtures/import-values/Values.d.kt.ts +4 -0
- package/kotlin/fixtures/import-values/Values.kt +18 -0
- package/kotlin/tools/expo-types.main.kts +17 -5
- package/package.json +2 -1
- package/scripts/dev-server-scanner.ts +33 -23
- package/src/assetCatalogContract.ts +7 -0
- package/src/bridge-contract.ts +21 -3
- package/src/connect.ts +18 -5
- package/src/desktop-host-delivery.ts +33 -3
- package/src/expo-swift-host.ts +6 -3
- package/src/host/bridge-host.ts +2 -0
- package/src/host/fetch-proxy-transport.ts +8 -0
- package/src/kotlin-host.ts +11 -5
- package/src/native-firebase-config.ts +95 -54
- package/src/process-output.ts +16 -0
- package/src/sdk.ts +6 -1
- package/src/sim-client.ts +6 -1
- package/src/swift-compile.ts +36 -15
- package/src/swift-distribution.lock.json +8 -8
- package/src/swift-standin-emit.ts +10 -1
- package/src/swift-standins.ts +21 -7
- package/src/swift-submit.ts +6 -1
- package/src/worklet-executable.ts +7 -2
- /package/kotlin/fixtures/{kotlin-app-variants → import-scroll}/Nested.kt +0 -0
package/cli/commands/control.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
|
+
import http from 'node:http'
|
|
4
|
+
import https from 'node:https'
|
|
3
5
|
import { createRequire } from 'node:module'
|
|
4
6
|
import os from 'node:os'
|
|
5
7
|
import path from 'node:path'
|
|
8
|
+
import { bundleUrlForPlatform } from 'peach-engine/bundle-platform-url'
|
|
6
9
|
import { encodeAppCatalogWire } from 'peach-engine/engine/app-catalog-wire'
|
|
7
10
|
import {
|
|
8
11
|
devices,
|
|
@@ -41,7 +44,10 @@ import {
|
|
|
41
44
|
runtimeDir,
|
|
42
45
|
} from '../../src/home-paths'
|
|
43
46
|
import { applyHotMode } from '../../src/native-dev-bundle-url'
|
|
44
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
readExpoConfigAsync,
|
|
49
|
+
readNativeFirebaseConfig,
|
|
50
|
+
} from '../../src/native-firebase-config'
|
|
45
51
|
import { ensureProfile } from '../../src/profiles'
|
|
46
52
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
47
53
|
import {
|
|
@@ -57,7 +63,6 @@ import { sleep } from '../../src/sleep'
|
|
|
57
63
|
import { discoverAppCatalog } from '../app-catalog'
|
|
58
64
|
import { loadRNXAppConfig } from '../app-config'
|
|
59
65
|
import {
|
|
60
|
-
type DiscoveredAppFontSpec,
|
|
61
66
|
discoverNativeLinkedAppFonts,
|
|
62
67
|
encodeAppFontsWire,
|
|
63
68
|
resolveMetroProjectRoot,
|
|
@@ -68,6 +73,7 @@ import {
|
|
|
68
73
|
type ResolvedAppProject,
|
|
69
74
|
} from '../app-project'
|
|
70
75
|
import {
|
|
76
|
+
type DiscoveredAppSplashSpec,
|
|
71
77
|
discoverConfigPluginAppSplash,
|
|
72
78
|
discoverOneNativeAppSplash,
|
|
73
79
|
encodeAppSplashWire,
|
|
@@ -560,6 +566,22 @@ async function resolveConnectionInputForCli(target: string): Promise<ResolvedDev
|
|
|
560
566
|
)
|
|
561
567
|
}
|
|
562
568
|
|
|
569
|
+
// a dev server builds an app's graph on the first request for its bundle and
|
|
570
|
+
// finishes the build after that request goes away. `peach open` asks as soon as
|
|
571
|
+
// the bundle is resolved, so the build overlaps bridge, host and runtime boot;
|
|
572
|
+
// the engine's own request for the same url then joins it, so the cli drops its
|
|
573
|
+
// copy unread. nothing the open stamps asks the dev server, so a busy build
|
|
574
|
+
// cannot change the page. the socket is unreferenced: a pending build never
|
|
575
|
+
// holds the command open.
|
|
576
|
+
function startDevServerBuild(bundleUrl: string): void {
|
|
577
|
+
const request = (bundleUrl.startsWith('https:') ? https : http).get(
|
|
578
|
+
bundleUrl,
|
|
579
|
+
(response) => response.resume(),
|
|
580
|
+
)
|
|
581
|
+
request.on('socket', (socket) => socket.unref())
|
|
582
|
+
request.on('error', () => {})
|
|
583
|
+
}
|
|
584
|
+
|
|
563
585
|
function getShellRoutePrefix(pathname: string): string {
|
|
564
586
|
const normalized = pathname.replace(/\/+$/, '') || '/'
|
|
565
587
|
if (normalized === '/__peachsim' || normalized.startsWith('/__peachsim/')) {
|
|
@@ -743,18 +765,9 @@ export function resolveShellBaseUrlForBridgePort(bridgePort: number): string {
|
|
|
743
765
|
// loaded by JS already flow through normal runtime fetch capture; this covers
|
|
744
766
|
// UIAppFonts-style assets that Metro can serve but the guest bundle never
|
|
745
767
|
// fetches itself. auto-discovery only — scans the repo for expo-font config
|
|
746
|
-
// plugin declarations, no user-facing flag.
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
// include it on the URL. fonts whose metro asset URL 404s (typo in the
|
|
750
|
-
// expo-font plugin config, missing file, or a wrong app.config picked up
|
|
751
|
-
// by the scan) are dropped silently so the engine never tries to fetch a
|
|
752
|
-
// known-bad URL at boot — that would just log a registration failure and
|
|
753
|
-
// confuse anyone reading the page console.
|
|
754
|
-
async function stampAppFonts(
|
|
755
|
-
urlStr: string,
|
|
756
|
-
project?: ResolvedAppProject | null,
|
|
757
|
-
): Promise<string> {
|
|
768
|
+
// plugin declarations, no user-facing flag. discovery keeps only font files
|
|
769
|
+
// that exist on disk, so the stamp never waits on the dev server.
|
|
770
|
+
function stampAppFonts(urlStr: string, project?: ResolvedAppProject | null): string {
|
|
758
771
|
try {
|
|
759
772
|
const url = new URL(urlStr)
|
|
760
773
|
const bundleUrl = url.searchParams.get('bundle') || ''
|
|
@@ -764,48 +777,13 @@ async function stampAppFonts(
|
|
|
764
777
|
projectDir: project.appDir,
|
|
765
778
|
})
|
|
766
779
|
if (specs.length === 0) return urlStr
|
|
767
|
-
|
|
768
|
-
if (reachable.length === 0) return urlStr
|
|
769
|
-
url.searchParams.set('appFonts', encodeAppFontsWire(reachable))
|
|
780
|
+
url.searchParams.set('appFonts', encodeAppFontsWire(specs))
|
|
770
781
|
return url.toString()
|
|
771
782
|
} catch {
|
|
772
783
|
return urlStr
|
|
773
784
|
}
|
|
774
785
|
}
|
|
775
786
|
|
|
776
|
-
// HEAD-check each font URL with a short timeout. metro returns 200 (or
|
|
777
|
-
// sometimes a redirect) for assets it can resolve and 500 with an ENOENT
|
|
778
|
-
// trace for paths it cannot. anything not in the 2xx-3xx range gets dropped.
|
|
779
|
-
async function filterReachableAppFonts(
|
|
780
|
-
specs: DiscoveredAppFontSpec[],
|
|
781
|
-
): Promise<DiscoveredAppFontSpec[]> {
|
|
782
|
-
const results = await Promise.all(
|
|
783
|
-
specs.map(async (spec): Promise<DiscoveredAppFontSpec | null> => {
|
|
784
|
-
const controller = new AbortController()
|
|
785
|
-
const timer = setTimeout(() => controller.abort(), 1500)
|
|
786
|
-
try {
|
|
787
|
-
const res = await fetch(spec.url, {
|
|
788
|
-
method: 'HEAD',
|
|
789
|
-
signal: controller.signal,
|
|
790
|
-
})
|
|
791
|
-
if (res.status >= 200 && res.status < 400) return spec
|
|
792
|
-
return null
|
|
793
|
-
} catch {
|
|
794
|
-
return null
|
|
795
|
-
} finally {
|
|
796
|
-
clearTimeout(timer)
|
|
797
|
-
}
|
|
798
|
-
}),
|
|
799
|
-
)
|
|
800
|
-
// a font offered from more than one candidate metro root keeps the first
|
|
801
|
-
// url metro served.
|
|
802
|
-
const reachable = new Map<string, DiscoveredAppFontSpec>()
|
|
803
|
-
for (const spec of results) {
|
|
804
|
-
if (spec !== null && !reachable.has(spec.family)) reachable.set(spec.family, spec)
|
|
805
|
-
}
|
|
806
|
-
return [...reachable.values()]
|
|
807
|
-
}
|
|
808
|
-
|
|
809
787
|
async function resolveShellUrl(
|
|
810
788
|
target: string,
|
|
811
789
|
baseUrl: string,
|
|
@@ -856,6 +834,7 @@ async function resolveShellUrl(
|
|
|
856
834
|
async function stampAppSplash(
|
|
857
835
|
urlStr: string,
|
|
858
836
|
project?: ResolvedAppProject | null,
|
|
837
|
+
oneAppSplash?: Promise<DiscoveredAppSplashSpec | null>,
|
|
859
838
|
): Promise<string> {
|
|
860
839
|
try {
|
|
861
840
|
const url = new URL(urlStr)
|
|
@@ -866,11 +845,11 @@ async function stampAppSplash(
|
|
|
866
845
|
bundleUrl,
|
|
867
846
|
projectDir: project.appDir,
|
|
868
847
|
})
|
|
869
|
-
: await discoverOneNativeAppSplash(bundleUrl)
|
|
848
|
+
: await (oneAppSplash ?? discoverOneNativeAppSplash(bundleUrl))
|
|
870
849
|
if (!spec) return urlStr
|
|
871
850
|
// native splash assets are required for the first frame.
|
|
872
851
|
for (const imageUrl of splashImageUrls(spec)) {
|
|
873
|
-
if (!(
|
|
852
|
+
if (!splashImageOnDisk(imageUrl, project.appDir)) return urlStr
|
|
874
853
|
}
|
|
875
854
|
url.searchParams.set('appSplash', encodeAppSplashWire(spec))
|
|
876
855
|
return url.toString()
|
|
@@ -897,16 +876,18 @@ function stampAppCatalog(urlStr: string, project?: ResolvedAppProject | null): s
|
|
|
897
876
|
}
|
|
898
877
|
}
|
|
899
878
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
879
|
+
// a splash spec names `<origin>/assets/<project-relative-path>`, the file the
|
|
880
|
+
// dev server serves from its project root, so the file is checked on disk and
|
|
881
|
+
// the stamp never waits on a dev server busy building.
|
|
882
|
+
function splashImageOnDisk(imageUrl: string, appDir: string): boolean {
|
|
883
|
+
const { pathname } = new URL(imageUrl)
|
|
884
|
+
if (!pathname.startsWith('/assets/')) return false
|
|
903
885
|
try {
|
|
904
|
-
|
|
905
|
-
|
|
886
|
+
return fs
|
|
887
|
+
.statSync(path.join(appDir, decodeURIComponent(pathname.slice('/assets/'.length))))
|
|
888
|
+
.isFile()
|
|
906
889
|
} catch {
|
|
907
890
|
return false
|
|
908
|
-
} finally {
|
|
909
|
-
clearTimeout(timer)
|
|
910
891
|
}
|
|
911
892
|
}
|
|
912
893
|
|
|
@@ -947,6 +928,10 @@ export interface BuildShellUrlOptions {
|
|
|
947
928
|
theme?: string
|
|
948
929
|
resolvedBundle?: ResolvedDevBundle
|
|
949
930
|
project?: ResolvedAppProject | null
|
|
931
|
+
// a One app's splash, read from its dev server's manifest. `peach open`
|
|
932
|
+
// starts the read beside its early bundle request so the answer overlaps
|
|
933
|
+
// bridge and host setup instead of preceding the launch.
|
|
934
|
+
oneAppSplash?: Promise<DiscoveredAppSplashSpec | null>
|
|
950
935
|
// deterministic-capture mode, stamped onto the engine URL as ?proof=1. the
|
|
951
936
|
// shell worker, compositor worker and tenant worker each read it off the
|
|
952
937
|
// host page URL at boot (syncProofModeFromInitUrl) and pin everything that
|
|
@@ -975,20 +960,44 @@ export interface BuildShellUrlOptions {
|
|
|
975
960
|
}
|
|
976
961
|
|
|
977
962
|
function readProjectNativeFirebaseConfig(
|
|
978
|
-
project: ResolvedAppProject
|
|
979
|
-
): Record<string, unknown> | null {
|
|
980
|
-
if (!project) return null
|
|
963
|
+
project: ResolvedAppProject,
|
|
964
|
+
): Promise<Record<string, unknown> | null> {
|
|
981
965
|
const require = createRequire(path.join(project.appDir, 'package.json'))
|
|
982
966
|
try {
|
|
983
967
|
require.resolve('@expo/config')
|
|
984
968
|
} catch {
|
|
985
|
-
return null
|
|
986
|
-
}
|
|
987
|
-
try {
|
|
988
|
-
return readNativeFirebaseConfig(project.appDir)
|
|
989
|
-
} catch (error) {
|
|
990
|
-
return { error: error instanceof Error ? error.message : String(error) }
|
|
969
|
+
return Promise.resolve(null)
|
|
991
970
|
}
|
|
971
|
+
return readExpoConfigAsync(project.appDir)
|
|
972
|
+
.then((config) =>
|
|
973
|
+
readNativeFirebaseConfig(project.appDir, undefined, undefined, config),
|
|
974
|
+
)
|
|
975
|
+
.catch((error: unknown) => ({
|
|
976
|
+
error: error instanceof Error ? error.message : String(error),
|
|
977
|
+
}))
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// evaluating the Expo config spawns node, so `peach open` starts the read as
|
|
981
|
+
// soon as the project resolves and the shell url it builds takes that read. a
|
|
982
|
+
// later build reads the config again.
|
|
983
|
+
const startedNativeFirebaseConfigs = new Map<
|
|
984
|
+
string,
|
|
985
|
+
Promise<Record<string, unknown> | null>
|
|
986
|
+
>()
|
|
987
|
+
|
|
988
|
+
function startNativeFirebaseConfigRead(project: ResolvedAppProject | null) {
|
|
989
|
+
if (project)
|
|
990
|
+
startedNativeFirebaseConfigs.set(
|
|
991
|
+
project.appDir,
|
|
992
|
+
readProjectNativeFirebaseConfig(project),
|
|
993
|
+
)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function takeNativeFirebaseConfig(project: ResolvedAppProject | null | undefined) {
|
|
997
|
+
if (!project) return Promise.resolve(null)
|
|
998
|
+
const started = startedNativeFirebaseConfigs.get(project.appDir)
|
|
999
|
+
startedNativeFirebaseConfigs.delete(project.appDir)
|
|
1000
|
+
return started ?? readProjectNativeFirebaseConfig(project)
|
|
992
1001
|
}
|
|
993
1002
|
|
|
994
1003
|
function bundleUrlOf(urlStr: string): string | undefined {
|
|
@@ -1014,7 +1023,7 @@ export async function buildShellUrl(
|
|
|
1014
1023
|
opts.project ?? (await resolveAppProjectForBundleUrl(bundleUrlOf(resolved)))
|
|
1015
1024
|
const expoRoot = project?.appDir ?? null
|
|
1016
1025
|
const stamped = stampAppCatalog(
|
|
1017
|
-
await stampAppSplash(
|
|
1026
|
+
await stampAppSplash(stampAppFonts(resolved, project), project, opts.oneAppSplash),
|
|
1018
1027
|
project,
|
|
1019
1028
|
)
|
|
1020
1029
|
const expoStamped = new URL(stamped)
|
|
@@ -1046,7 +1055,7 @@ export async function buildShellUrl(
|
|
|
1046
1055
|
// own `@expo/config` (a dependency-free recorder) leaves it to the Peach
|
|
1047
1056
|
// Metro plugin's endpoint.
|
|
1048
1057
|
expoStamped.searchParams.delete('nativeFirebase')
|
|
1049
|
-
const nativeFirebase =
|
|
1058
|
+
const nativeFirebase = await takeNativeFirebaseConfig(project)
|
|
1050
1059
|
if (nativeFirebase && expoStamped.searchParams.get('bundle'))
|
|
1051
1060
|
expoStamped.searchParams.set('nativeFirebase', JSON.stringify(nativeFirebase))
|
|
1052
1061
|
if (opts.bundleOriginOverride)
|
|
@@ -1129,13 +1138,8 @@ function deriveCurrentSimBaseUrl(
|
|
|
1129
1138
|
}
|
|
1130
1139
|
}
|
|
1131
1140
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
baseUrl: string,
|
|
1135
|
-
openToken: string,
|
|
1136
|
-
opts: BuildShellUrlOptions = {},
|
|
1137
|
-
): Promise<string> {
|
|
1138
|
-
const url = new URL(await buildShellUrl(target, baseUrl, opts))
|
|
1141
|
+
function withOpenToken(shellUrl: string, openToken: string): string {
|
|
1142
|
+
const url = new URL(shellUrl)
|
|
1139
1143
|
url.searchParams.set('inspectOpen', openToken)
|
|
1140
1144
|
return url.toString()
|
|
1141
1145
|
}
|
|
@@ -1788,50 +1792,7 @@ export async function runOpenCommand(
|
|
|
1788
1792
|
resolvedBundle?.bundleUrl,
|
|
1789
1793
|
projectRoot,
|
|
1790
1794
|
)
|
|
1791
|
-
|
|
1792
|
-
const configuredRuntimeVersion = appConfig?.runtimeVersion
|
|
1793
|
-
const runtimeConfig = mergePeachConfig(
|
|
1794
|
-
mergePeachConfig(
|
|
1795
|
-
appConfig ? { ...appConfig, runtimeVersion: undefined } : undefined,
|
|
1796
|
-
opts.runtimeConfig,
|
|
1797
|
-
),
|
|
1798
|
-
commandConfig,
|
|
1799
|
-
)
|
|
1800
|
-
if (configuredRuntimeVersion) {
|
|
1801
|
-
await ensureConfiguredRuntime(configuredRuntimeVersion)
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
// keep the bridge port and the shell base URL in one world: --base-url
|
|
1805
|
-
// retargets polling at THAT world's bridge (otherwise open builds the page
|
|
1806
|
-
// against one runtime while polling the other bridge for registration and
|
|
1807
|
-
// times out after ~125s), and a pin nothing serves is refused rather than
|
|
1808
|
-
// falling back to the discovered default, which used to send
|
|
1809
|
-
// ensureBridgeForOpen below off to start a second shell.
|
|
1810
|
-
const explicitBaseUrl = args.find((_, i) => args[i - 1] === '--base-url')
|
|
1811
|
-
if (explicitBaseUrl) {
|
|
1812
|
-
const pinned = resolveBridgePortForPin({
|
|
1813
|
-
baseUrl: explicitBaseUrl,
|
|
1814
|
-
wsPort: parsed.wsPort,
|
|
1815
|
-
explicitPort: parsed.explicitPort,
|
|
1816
|
-
})
|
|
1817
|
-
if (pinned.error !== undefined) {
|
|
1818
|
-
console.error(` error: ${pinned.error}`)
|
|
1819
|
-
peachExit(1)
|
|
1820
|
-
} else {
|
|
1821
|
-
parsed.wsPort = pinned.port
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
opts.signal?.throwIfAborted()
|
|
1826
|
-
parsed.wsPort = await ensureBridgeForOpen(
|
|
1827
|
-
parsed.wsPort,
|
|
1828
|
-
parsed.explicitPort,
|
|
1829
|
-
opts.resources,
|
|
1830
|
-
explicitBaseUrl && !configuredRuntimeVersion ? 'external' : 'bridge',
|
|
1831
|
-
)
|
|
1832
|
-
if (opts.resources) opts.resources.port = parsed.wsPort
|
|
1833
|
-
opts.signal?.throwIfAborted()
|
|
1834
|
-
|
|
1795
|
+
startNativeFirebaseConfigRead(project)
|
|
1835
1796
|
// playwright is the browser-sim launcher. it owns an isolated profile and
|
|
1836
1797
|
// can reap exactly its own browser tree. desktop opens the native host and
|
|
1837
1798
|
// electron the Electron companion. none opens the user's browser profile.
|
|
@@ -1892,6 +1853,65 @@ export async function runOpenCommand(
|
|
|
1892
1853
|
peachExit(1)
|
|
1893
1854
|
}
|
|
1894
1855
|
|
|
1856
|
+
// asked before the bundle so the dev server answers it ahead of the build
|
|
1857
|
+
const oneAppSplash =
|
|
1858
|
+
resolvedBundle && project && !findExpoAppConfig(project.appDir)
|
|
1859
|
+
? discoverOneNativeAppSplash(resolvedBundle.bundleUrl)
|
|
1860
|
+
: undefined
|
|
1861
|
+
if (resolvedBundle) {
|
|
1862
|
+
// the engine fetches this url for its device's platform
|
|
1863
|
+
const bundleUrl = applyHotMode(resolvedBundle.bundleUrl, hotMode)
|
|
1864
|
+
startDevServerBuild(
|
|
1865
|
+
deviceArg && isDeviceModel(deviceArg)
|
|
1866
|
+
? bundleUrlForPlatform(bundleUrl, getRuntimePlatform(deviceArg), () => bundleUrl)
|
|
1867
|
+
: bundleUrl,
|
|
1868
|
+
)
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
const appConfig = await loadRNXAppConfig(project)
|
|
1872
|
+
const configuredRuntimeVersion = appConfig?.runtimeVersion
|
|
1873
|
+
const runtimeConfig = mergePeachConfig(
|
|
1874
|
+
mergePeachConfig(
|
|
1875
|
+
appConfig ? { ...appConfig, runtimeVersion: undefined } : undefined,
|
|
1876
|
+
opts.runtimeConfig,
|
|
1877
|
+
),
|
|
1878
|
+
commandConfig,
|
|
1879
|
+
)
|
|
1880
|
+
if (configuredRuntimeVersion) {
|
|
1881
|
+
await ensureConfiguredRuntime(configuredRuntimeVersion)
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// keep the bridge port and the shell base URL in one world: --base-url
|
|
1885
|
+
// retargets polling at THAT world's bridge (otherwise open builds the page
|
|
1886
|
+
// against one runtime while polling the other bridge for registration and
|
|
1887
|
+
// times out after ~125s), and a pin nothing serves is refused rather than
|
|
1888
|
+
// falling back to the discovered default, which used to send
|
|
1889
|
+
// ensureBridgeForOpen below off to start a second shell.
|
|
1890
|
+
const explicitBaseUrl = args.find((_, i) => args[i - 1] === '--base-url')
|
|
1891
|
+
if (explicitBaseUrl) {
|
|
1892
|
+
const pinned = resolveBridgePortForPin({
|
|
1893
|
+
baseUrl: explicitBaseUrl,
|
|
1894
|
+
wsPort: parsed.wsPort,
|
|
1895
|
+
explicitPort: parsed.explicitPort,
|
|
1896
|
+
})
|
|
1897
|
+
if (pinned.error !== undefined) {
|
|
1898
|
+
console.error(` error: ${pinned.error}`)
|
|
1899
|
+
peachExit(1)
|
|
1900
|
+
} else {
|
|
1901
|
+
parsed.wsPort = pinned.port
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
opts.signal?.throwIfAborted()
|
|
1906
|
+
parsed.wsPort = await ensureBridgeForOpen(
|
|
1907
|
+
parsed.wsPort,
|
|
1908
|
+
parsed.explicitPort,
|
|
1909
|
+
opts.resources,
|
|
1910
|
+
explicitBaseUrl && !configuredRuntimeVersion ? 'external' : 'bridge',
|
|
1911
|
+
)
|
|
1912
|
+
if (opts.resources) opts.resources.port = parsed.wsPort
|
|
1913
|
+
opts.signal?.throwIfAborted()
|
|
1914
|
+
|
|
1895
1915
|
// --posture <open|closed> seeds the fold posture for the opened sim,
|
|
1896
1916
|
// stamped on the engine URL next to ?device= so it round-trips instead of
|
|
1897
1917
|
// being swallowed into the bundle param. only foldable devices (iphone-duo)
|
|
@@ -2002,6 +2022,7 @@ export async function runOpenCommand(
|
|
|
2002
2022
|
theme: themeArg,
|
|
2003
2023
|
resolvedBundle,
|
|
2004
2024
|
project,
|
|
2025
|
+
oneAppSplash,
|
|
2005
2026
|
proof: args.includes('--proof'),
|
|
2006
2027
|
ephemeral: ephemeralProfile,
|
|
2007
2028
|
env: envArgs.length ? envArgs : undefined,
|
|
@@ -2029,6 +2050,9 @@ export async function runOpenCommand(
|
|
|
2029
2050
|
const simMatchesToken = (sim: BridgeSimInfo) =>
|
|
2030
2051
|
sim.url?.includes(`inspectOpen=${token}`) ?? false
|
|
2031
2052
|
|
|
2053
|
+
// the reuse check builds the page url a new sim loads; building it once
|
|
2054
|
+
// keeps the project's native config read, a node spawn, to one per open
|
|
2055
|
+
let newSimShellUrl: string | null = null
|
|
2032
2056
|
if (shouldReuseCurrentSim) {
|
|
2033
2057
|
const bridge = createBridgeFromParsed(parsed)
|
|
2034
2058
|
const simHint = parsed.simId ? ` --sim ${parsed.simId}` : ''
|
|
@@ -2060,7 +2084,7 @@ export async function runOpenCommand(
|
|
|
2060
2084
|
}
|
|
2061
2085
|
}
|
|
2062
2086
|
if (!targetSim && parsed.simIdSource !== 'flag') {
|
|
2063
|
-
targetShellUrl = await buildShellUrl(
|
|
2087
|
+
targetShellUrl = newSimShellUrl = await buildShellUrl(
|
|
2064
2088
|
target,
|
|
2065
2089
|
applyPeachConfigToUrl(baseUrl, runtimeConfig),
|
|
2066
2090
|
buildShellUrlOpts,
|
|
@@ -2105,6 +2129,11 @@ export async function runOpenCommand(
|
|
|
2105
2129
|
buildShellUrlOpts,
|
|
2106
2130
|
))
|
|
2107
2131
|
const navigationUrl = new URL(navigatedShellUrl)
|
|
2132
|
+
const currentEphemeral = new URL(
|
|
2133
|
+
deriveCurrentSimBaseUrl(targetSim, baseUrl),
|
|
2134
|
+
).searchParams.get('ephemeral')
|
|
2135
|
+
if (currentEphemeral) navigationUrl.searchParams.set('ephemeral', currentEphemeral)
|
|
2136
|
+
navigatedShellUrl = navigationUrl.toString()
|
|
2108
2137
|
navigationUrl.searchParams.set('inspectOpen', token)
|
|
2109
2138
|
bridge
|
|
2110
2139
|
.send({
|
|
@@ -2184,7 +2213,10 @@ export async function runOpenCommand(
|
|
|
2184
2213
|
}
|
|
2185
2214
|
|
|
2186
2215
|
const configuredBaseUrl = applyPeachConfigToUrl(baseUrl, runtimeConfig)
|
|
2187
|
-
const openUrl =
|
|
2216
|
+
const openUrl = withOpenToken(
|
|
2217
|
+
newSimShellUrl ?? (await buildShellUrl(target, configuredBaseUrl, buildShellUrlOpts)),
|
|
2218
|
+
token,
|
|
2219
|
+
)
|
|
2188
2220
|
const ownerPid = opts.ownerPid ?? getStableOwnerPid()
|
|
2189
2221
|
if (!ownerPid) {
|
|
2190
2222
|
console.error(
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// bundle Info.plist instead, so the entry reads "Peach Daemon".
|
|
11
11
|
//
|
|
12
12
|
// the bundle is regenerated every time `daemonInstall` runs — it's tiny
|
|
13
|
-
// (a plist + a shell launcher) and the launcher
|
|
14
|
-
//
|
|
15
|
-
// the
|
|
13
|
+
// (a plist + a shell launcher) and the launcher execs the executable path
|
|
14
|
+
// that installed it, which `peach upgrade` replaces in place before restarting
|
|
15
|
+
// the daemon, so an upgrade needs no reinstall. there's no notarization
|
|
16
16
|
// or Developer ID signing here yet; ad-hoc + hardened runtime is enough to
|
|
17
17
|
// fix the attribution, and a real signed bundle (shipped via the runtime
|
|
18
18
|
// CDN) is a follow-up.
|
package/cli/commands/daemon.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// wraps `peach serve` so it starts automatically on login and restarts if
|
|
4
4
|
// it crashes. the agent points at the executable that ran `daemon install`,
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// and `peach upgrade` replaces that file in place and then restarts a running
|
|
6
|
+
// daemon itself, so the plist/unit file never needs rewriting.
|
|
7
7
|
//
|
|
8
8
|
// `daemonInstall` is shared by `peach setup` and the explicit
|
|
9
9
|
// `peach daemon install` machine-bootstrap path.
|
package/cli/commands/do-chain.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BridgeInputError } from '../../src/bridge-contract-input'
|
|
2
|
+
import { IdleObservationError } from './inspect/settling'
|
|
2
3
|
import type { PerformResult } from '../../src/bridge-contract'
|
|
3
4
|
import type { SimPerformStep } from '../../src/sim-client'
|
|
4
5
|
|
|
@@ -342,7 +343,7 @@ export async function runDoChain(
|
|
|
342
343
|
let sim: Awaited<ReturnType<typeof connect>> | null = null
|
|
343
344
|
try {
|
|
344
345
|
sim = await connect({
|
|
345
|
-
port: parsed.wsPort,
|
|
346
|
+
port: parsed.explicitPort ? parsed.wsPort : undefined,
|
|
346
347
|
sim: parsed.simIdSource === 'flag' ? parsed.simId : undefined,
|
|
347
348
|
timeoutMs: parsed.commandTimeoutMs,
|
|
348
349
|
})
|
|
@@ -436,7 +437,13 @@ export async function runDoChain(
|
|
|
436
437
|
` ⚠ auto-wait timed out after ${settled.elapsed ?? budgetMs}ms (${settled.blockedBy}). next command may see mid-animation state. use \`peach do settle\` for a longer wait.\n`,
|
|
437
438
|
)
|
|
438
439
|
}
|
|
439
|
-
} catch {
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (error instanceof IdleObservationError) {
|
|
442
|
+
process.stderr.write(
|
|
443
|
+
` ⚠ ${error.message}. next command may see mid-animation state. use \`peach do settle\` to retry.\n`,
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
440
447
|
}
|
|
441
448
|
|
|
442
449
|
if (wantsJson) {
|
package/cli/commands/electron.ts
CHANGED
|
@@ -90,7 +90,10 @@ examples:
|
|
|
90
90
|
if (!install) {
|
|
91
91
|
printMissingCompanionHint(native)
|
|
92
92
|
const canPrompt =
|
|
93
|
-
process.
|
|
93
|
+
process.platform !== 'darwin' &&
|
|
94
|
+
process.stdin.isTTY &&
|
|
95
|
+
process.env.CI !== '1' &&
|
|
96
|
+
process.env.PEACH_NO_PROMPT !== '1'
|
|
94
97
|
if (canPrompt && (await confirm('run peach desktop install now?', true))) {
|
|
95
98
|
console.log()
|
|
96
99
|
const { runInstallDesktop } = await import('./install-desktop')
|
|
@@ -2236,7 +2236,7 @@ export function formatTimelinePayload(
|
|
|
2236
2236
|
case 'notification':
|
|
2237
2237
|
return `${d.title ?? ''}${d.body ? ` — ${d.body}` : ''}`
|
|
2238
2238
|
case 'fetch':
|
|
2239
|
-
return `${d.method ?? 'GET'} ${d.url ?? ''}${d.status ? ` -> ${d.status}` : ''}`
|
|
2239
|
+
return `${d.method ?? 'GET'} ${d.url ?? ''}${d.status ? ` -> ${d.status}` : ''}${d.error ? ` -> ${d.error}` : ''}`
|
|
2240
2240
|
case 'console':
|
|
2241
2241
|
case 'console-log':
|
|
2242
2242
|
return `${d.level ?? 'log'}: ${(d.message ?? '').toString().slice(0, 120)}`
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rnxExit } from '../../run-rnx'
|
|
2
|
+
import { IdleObservationError, waitForSootsimIdle } from './settling'
|
|
2
3
|
import type { WsBridge } from '../../ws-bridge'
|
|
3
4
|
|
|
4
5
|
// default: worker stillness plus mounted-root membership; budgets of a
|
|
@@ -24,11 +25,22 @@ export async function runSettleSubcommand(opts: {
|
|
|
24
25
|
return
|
|
25
26
|
}
|
|
26
27
|
const strict = args.includes('--strict')
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
try {
|
|
29
|
+
const { elapsed, settled, blockedBy } = await waitForSootsimIdle({
|
|
30
|
+
bridge,
|
|
31
|
+
maxMs,
|
|
32
|
+
strict,
|
|
33
|
+
})
|
|
34
|
+
if (settled) {
|
|
35
|
+
console.log(` settled in ${elapsed}ms`)
|
|
36
|
+
} else {
|
|
37
|
+
console.log(` timed out after ${elapsed}ms (${blockedBy})`)
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof IdleObservationError) {
|
|
41
|
+
console.error(` ⚠ ${error.message}`)
|
|
42
|
+
rnxExit(1)
|
|
43
|
+
}
|
|
44
|
+
throw error
|
|
33
45
|
}
|
|
34
46
|
}
|