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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// an app with local Expo Swift modules only renders once the bridge compiles
|
|
2
|
+
// them, and a cold /__peach/expo-swift artifact compiles for minutes (a linked
|
|
3
|
+
// build plus per-namespace isolation builds in sequence) while a flow waits
|
|
4
|
+
// 120s for its first tree. so `peach open` GETs the same endpoint the engine
|
|
5
|
+
// will fetch and returns only once it answers; the bridge shares one compile
|
|
6
|
+
// per root, so the engine's own fetch then finds it done. a warm artifact
|
|
7
|
+
// answers in ~1s and an app without local modules answers null fast.
|
|
8
|
+
export const EXPO_SWIFT_READY_TIMEOUT_MS = 30 * 60_000
|
|
9
|
+
const EXPO_SWIFT_READY_PROGRESS_MS = 30_000
|
|
10
|
+
|
|
11
|
+
export type ExpoSwiftReadyOptions = {
|
|
12
|
+
timeoutMs?: number
|
|
13
|
+
progressMs?: number
|
|
14
|
+
signal?: AbortSignal
|
|
15
|
+
onProgress?: (elapsedMs: number) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function awaitExpoSwiftReady(
|
|
19
|
+
endpointUrl: string,
|
|
20
|
+
options: ExpoSwiftReadyOptions = {},
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const timeoutMs = options.timeoutMs ?? EXPO_SWIFT_READY_TIMEOUT_MS
|
|
23
|
+
const progressMs = options.progressMs ?? EXPO_SWIFT_READY_PROGRESS_MS
|
|
24
|
+
const start = Date.now()
|
|
25
|
+
const report =
|
|
26
|
+
options.onProgress ??
|
|
27
|
+
((elapsedMs: number) => {
|
|
28
|
+
console.log(
|
|
29
|
+
` still waiting on ${endpointUrl} after ${Math.round(elapsedMs / 1000)}s — expo swift compile in progress`,
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
const progress = setInterval(() => report(Date.now() - start), progressMs)
|
|
33
|
+
try {
|
|
34
|
+
options.signal?.throwIfAborted()
|
|
35
|
+
const timeout = AbortSignal.timeout(timeoutMs)
|
|
36
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout
|
|
37
|
+
let res: Response | undefined
|
|
38
|
+
while (!res) {
|
|
39
|
+
try {
|
|
40
|
+
// bun ends an unanswered fetch at six minutes. that close is the last
|
|
41
|
+
// client, so the bridge aborts the shared compile and the retry starts
|
|
42
|
+
// over. hold the socket for the caller's bound instead.
|
|
43
|
+
const init: RequestInit = { signal }
|
|
44
|
+
Object.assign(init, { timeout: false })
|
|
45
|
+
res = await fetch(endpointUrl, init)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (options.signal?.aborted) throw error
|
|
48
|
+
if (timeout.aborted) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`timed out after ${timeoutMs}ms waiting for ${endpointUrl} — the expo swift compile did not answer within the bound`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
// a dropped socket still rejoins the same compile when the bridge has
|
|
54
|
+
// another client. retry only that case.
|
|
55
|
+
if (error instanceof Error && error.name === 'TimeoutError') continue
|
|
56
|
+
throw new Error(`could not reach ${endpointUrl}`, { cause: error })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
const body = (await res.text()).trim().slice(0, 500)
|
|
61
|
+
throw new Error(`${endpointUrl} answered ${res.status}${body ? `: ${body}` : ''}`)
|
|
62
|
+
}
|
|
63
|
+
await res.text()
|
|
64
|
+
console.log(
|
|
65
|
+
` expo swift modules ready in ${((Date.now() - start) / 1000).toFixed(1)}s`,
|
|
66
|
+
)
|
|
67
|
+
} finally {
|
|
68
|
+
clearInterval(progress)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// the expoSwift endpoint a built shell url carries, or null for an app with no
|
|
73
|
+
// project root
|
|
74
|
+
export async function awaitStampedExpoSwiftReady(shellUrl: string): Promise<void> {
|
|
75
|
+
const endpoint = new URL(shellUrl).searchParams.get('expoSwift')
|
|
76
|
+
if (!endpoint) return
|
|
77
|
+
await awaitExpoSwiftReady(endpoint)
|
|
78
|
+
}
|
package/src/expoSwiftContract.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { insertBundleGlobalFooter, readBundleGlobalFooter } from './bundleGlobalFooter'
|
|
2
2
|
|
|
3
|
+
export interface ExpoSwiftPreparationOptions {
|
|
4
|
+
signal?: AbortSignal
|
|
5
|
+
timeoutMs?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export interface ExpoSwiftSourceUnit {
|
|
4
9
|
namespace: string
|
|
5
10
|
moduleId: string
|
|
6
11
|
classes: string[]
|
|
12
|
+
// legacy bridge module registrations (react-native-bridge-modules.ts)
|
|
13
|
+
bridgeModules: string[]
|
|
7
14
|
files: Record<string, string>
|
|
15
|
+
frameworks: string[]
|
|
16
|
+
// the app target's Swift language mode; Expo packages compile in the default
|
|
17
|
+
swiftLanguageMode?: number
|
|
8
18
|
}
|
|
9
19
|
|
|
10
20
|
export interface ExpoSwiftCompiledModule {
|
|
@@ -17,6 +27,8 @@ export interface ExpoSwiftCompiledModule {
|
|
|
17
27
|
|
|
18
28
|
export interface ExpoSwiftCompilationResult {
|
|
19
29
|
namespaces: string[]
|
|
30
|
+
// the namespaces that are legacy bridge modules rather than Expo modules
|
|
31
|
+
bridgeModules?: string[]
|
|
20
32
|
modules: ExpoSwiftCompiledModule[]
|
|
21
33
|
failures: { namespace: string; diagnostic: string }[]
|
|
22
34
|
}
|
|
@@ -41,6 +53,13 @@ export function isExpoSwiftCompilationResult(
|
|
|
41
53
|
return false
|
|
42
54
|
const remaining = new Set(value.namespaces)
|
|
43
55
|
if (remaining.size !== value.namespaces.length) return false
|
|
56
|
+
// bundles published before legacy bridge modules carry no list
|
|
57
|
+
if (
|
|
58
|
+
value.bridgeModules !== undefined &&
|
|
59
|
+
(!strings(value.bridgeModules) ||
|
|
60
|
+
!value.bridgeModules.every((name) => remaining.has(name)))
|
|
61
|
+
)
|
|
62
|
+
return false
|
|
44
63
|
for (const artifact of value.modules) {
|
|
45
64
|
if (
|
|
46
65
|
!record(artifact) ||
|
package/src/home-paths.ts
CHANGED
|
@@ -185,6 +185,15 @@ export function cacheDir(): string {
|
|
|
185
185
|
return path.join(rnxHomeDir(), 'cache')
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// swiftpm otherwise uses shared caches and configuration outside PEACH_HOME.
|
|
189
|
+
export function swiftPackageCacheOptions(): string[] {
|
|
190
|
+
const root = path.join(cacheDir(), 'swiftpm')
|
|
191
|
+
return ['cache', 'config', 'security'].flatMap((name) => [
|
|
192
|
+
`--${name}-path`,
|
|
193
|
+
path.join(root, name),
|
|
194
|
+
])
|
|
195
|
+
}
|
|
196
|
+
|
|
188
197
|
export function daemonLockfilePath(): string {
|
|
189
198
|
return path.join(rnxHomeDir(), DAEMON_LOCKFILE)
|
|
190
199
|
}
|
|
@@ -261,7 +270,10 @@ export function writeSharedConfig(patch: Partial<SharedConfig>): SharedConfig {
|
|
|
261
270
|
return next
|
|
262
271
|
}
|
|
263
272
|
|
|
264
|
-
|
|
273
|
+
// version 2 made the native app the default simulator window. version 1 setup
|
|
274
|
+
// saved the browser without asking on a Mac lacking the app, so its saved
|
|
275
|
+
// driver is not read back and those machines run setup again.
|
|
276
|
+
const CURRENT_ONBOARDING_VERSION = 2
|
|
265
277
|
|
|
266
278
|
export type SimulatorDriverPreference = DriverId
|
|
267
279
|
|
|
@@ -270,7 +282,9 @@ export function isOnboardingComplete(): boolean {
|
|
|
270
282
|
}
|
|
271
283
|
|
|
272
284
|
export function readSimulatorDriverPreference(): SimulatorDriverPreference | null {
|
|
273
|
-
const
|
|
285
|
+
const config = readSharedConfig()
|
|
286
|
+
if (config.onboardingVersion !== CURRENT_ONBOARDING_VERSION) return null
|
|
287
|
+
const driver = config.simulatorDriver
|
|
274
288
|
return driver === 'desktop' || driver === 'electron' || driver === 'playwright'
|
|
275
289
|
? driver
|
|
276
290
|
: null
|
|
@@ -634,6 +648,38 @@ export function isDevBridgeLockfileFresh(
|
|
|
634
648
|
}
|
|
635
649
|
}
|
|
636
650
|
|
|
651
|
+
function hostBridgePortNumber(value: number | null | undefined): number | null {
|
|
652
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 65535)
|
|
653
|
+
return null
|
|
654
|
+
return value
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** port a desktop host's page should register on.
|
|
658
|
+
*
|
|
659
|
+
* the launch url's wsPort pin wins. an ambient PEACH_BRIDGE_PORT wins only
|
|
660
|
+
* when this home has no fresh daemon, the daemon already bound that port, or
|
|
661
|
+
* a fresh dev-bridge lockfile in this home claims it. otherwise the fresh
|
|
662
|
+
* daemon lock wins, so a second PEACH_HOME is not pulled onto another world's
|
|
663
|
+
* bridge. the mac user script and the native seed use the same rule. */
|
|
664
|
+
export function desktopHostBridgePort(input: {
|
|
665
|
+
pinnedPort?: number | null
|
|
666
|
+
daemonPort?: number | null
|
|
667
|
+
envPort?: number | null
|
|
668
|
+
devBridgePorts?: readonly number[]
|
|
669
|
+
}): number | null {
|
|
670
|
+
const pinned = hostBridgePortNumber(input.pinnedPort)
|
|
671
|
+
if (pinned != null) return pinned
|
|
672
|
+
const daemon = hostBridgePortNumber(input.daemonPort)
|
|
673
|
+
const env = hostBridgePortNumber(input.envPort)
|
|
674
|
+
const claimed = new Set(
|
|
675
|
+
(input.devBridgePorts ?? [])
|
|
676
|
+
.map((port) => hostBridgePortNumber(port))
|
|
677
|
+
.filter((port): port is number => port != null),
|
|
678
|
+
)
|
|
679
|
+
if (env != null && (daemon == null || env === daemon || claimed.has(env))) return env
|
|
680
|
+
return daemon
|
|
681
|
+
}
|
|
682
|
+
|
|
637
683
|
export function readLiveRuntimeVersions(): string[] {
|
|
638
684
|
const versions = new Set<string>()
|
|
639
685
|
const daemon = readDaemonLockfile()
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -474,9 +474,9 @@ export class SootSimBridgeHost {
|
|
|
474
474
|
// can't immediately reclaim while the user gets oriented.
|
|
475
475
|
private static USER_BOOT_LEASE_TTL_MS = 60_000
|
|
476
476
|
private static SIM_RECONNECT_TTL_MS = 30_000
|
|
477
|
-
// abandoned-tab GC.
|
|
478
|
-
//
|
|
479
|
-
// keeps its socket alive
|
|
477
|
+
// abandoned-tab GC. transport keepalive only reaps broken connections,
|
|
478
|
+
// but a background tab from a prior `peach open` / QA / test run
|
|
479
|
+
// keeps its socket alive forever, so `peach list`
|
|
480
480
|
// accretes dozens of zombie sims that all time out on every command (QA
|
|
481
481
|
// F21-3, carried F19-2). reap a sim only when it is provably nobody's:
|
|
482
482
|
// not primary, not user-focused, no CLI attached, no active lease, and no
|
|
@@ -496,15 +496,6 @@ export class SootSimBridgeHost {
|
|
|
496
496
|
private startedAt = 0
|
|
497
497
|
private heartbeatTimer: NodeJS.Timeout | null = null
|
|
498
498
|
private devHeartbeatTimer: NodeJS.Timeout | null = null
|
|
499
|
-
// ws-level heartbeat: sims that hang up uncleanly (page navigated, network
|
|
500
|
-
// dropped, sim crashed) leave their server-side WebSocket sitting "open"
|
|
501
|
-
// forever. ping every WS_HEARTBEAT_INTERVAL_MS; if the previous round's
|
|
502
|
-
// ping was never answered, terminate(). that fires 'close' which runs the
|
|
503
|
-
// sim-cleanup path and stops `peach list` from showing 8 zombie
|
|
504
|
-
// sims that all time out on every command.
|
|
505
|
-
private wsHeartbeatTimer: NodeJS.Timeout | null = null
|
|
506
|
-
private wsIsAlive = new WeakMap<WebSocket, boolean>()
|
|
507
|
-
private static WS_HEARTBEAT_INTERVAL_MS = 30_000
|
|
508
499
|
private runtimeUpdateTimer: NodeJS.Timeout | null = null
|
|
509
500
|
private runtimeUpdateInFlight: Promise<void> | null = null
|
|
510
501
|
private readonly handleServerScan = createServerScanHandler({
|
|
@@ -552,10 +543,13 @@ export class SootSimBridgeHost {
|
|
|
552
543
|
if (this.httpServer || this.wss) return this.effectivePort
|
|
553
544
|
|
|
554
545
|
// an installed runtime can serve while the startup update runs. only a
|
|
555
|
-
// machine without usable assets needs to wait for that download.
|
|
546
|
+
// machine without usable assets needs to wait for that download. only the
|
|
547
|
+
// daemon's runtime updater ever clears the flag, so a dev bridge (which
|
|
548
|
+
// serves the checkout build and runs no updater) never sets it.
|
|
556
549
|
const bootRuntimeDir = this.getActiveRuntime().runtimeDir
|
|
557
550
|
this.bootstrapping =
|
|
558
|
-
|
|
551
|
+
this.shouldWriteLockfile &&
|
|
552
|
+
(!bootRuntimeDir || !fs.existsSync(path.join(bootRuntimeDir, 'index.html')))
|
|
559
553
|
|
|
560
554
|
for (let attempt = 0; attempt < this.portFallbackCount; attempt++) {
|
|
561
555
|
const candidate = this.preferredPort === 0 ? 0 : this.preferredPort + attempt
|
|
@@ -670,14 +664,10 @@ export class SootSimBridgeHost {
|
|
|
670
664
|
// 'close' fires right after and runs the cleanup below.
|
|
671
665
|
ws.on('error', () => {})
|
|
672
666
|
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
this.wsIsAlive.set(ws, true)
|
|
678
|
-
ws.on('pong', () => {
|
|
679
|
-
this.wsIsAlive.set(ws, true)
|
|
680
|
-
})
|
|
667
|
+
// the kernel answers keepalive during SIGSTOP. application pings would
|
|
668
|
+
// mistake a governor-paused render for a dead connection and discard it.
|
|
669
|
+
// command deadlines still bound an unresponsive application.
|
|
670
|
+
req.socket.setKeepAlive(true, 30_000)
|
|
681
671
|
|
|
682
672
|
// register the socket with the agent host so session-status pushes
|
|
683
673
|
// reach it even before it subscribes to any specific session, and so
|
|
@@ -1211,12 +1201,6 @@ export class SootSimBridgeHost {
|
|
|
1211
1201
|
) as unknown as NodeJS.Timeout
|
|
1212
1202
|
this.cliIdleTimer.unref()
|
|
1213
1203
|
|
|
1214
|
-
this.wsHeartbeatTimer = setInterval(
|
|
1215
|
-
() => this.sweepDeadWebSockets(),
|
|
1216
|
-
SootSimBridgeHost.WS_HEARTBEAT_INTERVAL_MS,
|
|
1217
|
-
) as unknown as NodeJS.Timeout
|
|
1218
|
-
this.wsHeartbeatTimer.unref()
|
|
1219
|
-
|
|
1220
1204
|
if (this.shouldWriteLockfile) {
|
|
1221
1205
|
try {
|
|
1222
1206
|
ensureRnxHome()
|
|
@@ -2131,31 +2115,6 @@ export class SootSimBridgeHost {
|
|
|
2131
2115
|
return queue.tickets.length > queue.limit
|
|
2132
2116
|
}
|
|
2133
2117
|
|
|
2134
|
-
// ping every connected ws; if the previous round's ping went unanswered,
|
|
2135
|
-
// terminate the socket so 'close' fires and the sim cleanup path
|
|
2136
|
-
// runs. matches the recommended ws-library heartbeat pattern.
|
|
2137
|
-
private sweepDeadWebSockets() {
|
|
2138
|
-
if (!this.wss) return
|
|
2139
|
-
for (const ws of this.wss.clients) {
|
|
2140
|
-
if (ws.readyState !== WebSocket.OPEN) continue
|
|
2141
|
-
const alive = this.wsIsAlive.get(ws)
|
|
2142
|
-
if (alive === false) {
|
|
2143
|
-
try {
|
|
2144
|
-
ws.terminate()
|
|
2145
|
-
} catch {}
|
|
2146
|
-
continue
|
|
2147
|
-
}
|
|
2148
|
-
this.wsIsAlive.set(ws, false)
|
|
2149
|
-
try {
|
|
2150
|
-
ws.ping()
|
|
2151
|
-
} catch {
|
|
2152
|
-
try {
|
|
2153
|
-
ws.terminate()
|
|
2154
|
-
} catch {}
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
2118
|
private closeSimSocketFromHost(ws: WebSocket, initiatedBy?: string) {
|
|
2160
2119
|
if (ws.readyState !== WebSocket.OPEN) return
|
|
2161
2120
|
if (initiatedBy) this.hostCloseCauseBySocket.set(ws, initiatedBy)
|
|
@@ -2302,10 +2261,6 @@ export class SootSimBridgeHost {
|
|
|
2302
2261
|
clearInterval(this.devHeartbeatTimer)
|
|
2303
2262
|
this.devHeartbeatTimer = null
|
|
2304
2263
|
}
|
|
2305
|
-
if (this.wsHeartbeatTimer) {
|
|
2306
|
-
clearInterval(this.wsHeartbeatTimer)
|
|
2307
|
-
this.wsHeartbeatTimer = null
|
|
2308
|
-
}
|
|
2309
2264
|
if (this.runtimeUpdateTimer) {
|
|
2310
2265
|
clearInterval(this.runtimeUpdateTimer)
|
|
2311
2266
|
this.runtimeUpdateTimer = null
|
|
@@ -2,6 +2,10 @@ import { createHash, randomUUID } from 'node:crypto'
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { cacheDir } from '../home-paths'
|
|
5
|
+
import {
|
|
6
|
+
LAUNCH_METADATA_KEYS,
|
|
7
|
+
LAUNCH_METADATA_QUERY_PARAM,
|
|
8
|
+
} from '../launchMetadataContract'
|
|
5
9
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
6
10
|
|
|
7
11
|
// local files a sim loads from the bridge: native-linked app fonts and
|
|
@@ -22,10 +26,33 @@ function contentType(extension: string | undefined): string {
|
|
|
22
26
|
: 'application/octet-stream'
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
export function stageHostLaunchMetadata(url: URL, origin: string): string {
|
|
30
|
+
const metadata = new URLSearchParams()
|
|
31
|
+
for (const key of LAUNCH_METADATA_KEYS) {
|
|
32
|
+
for (const value of url.searchParams.getAll(key)) metadata.append(key, value)
|
|
33
|
+
url.searchParams.delete(key)
|
|
34
|
+
}
|
|
35
|
+
if (metadata.size > 0) {
|
|
36
|
+
const reference = stageHostBytes(
|
|
37
|
+
new TextEncoder().encode(metadata.toString()),
|
|
38
|
+
origin,
|
|
39
|
+
)
|
|
40
|
+
url.searchParams.set(LAUNCH_METADATA_QUERY_PARAM, reference)
|
|
41
|
+
}
|
|
42
|
+
return url.toString()
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
export function stageHostFile(filePath: string, origin: string): string {
|
|
26
|
-
|
|
46
|
+
return stageHostBytes(fs.readFileSync(filePath), origin, path.extname(filePath))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function stageHostBytes(
|
|
50
|
+
bytes: Uint8Array,
|
|
51
|
+
origin: string,
|
|
52
|
+
extension = '',
|
|
53
|
+
): string {
|
|
27
54
|
const digest = createHash('sha256').update(bytes).digest('hex')
|
|
28
|
-
|
|
55
|
+
extension = extension.toLowerCase()
|
|
29
56
|
const name = STAGED_NAME.test(digest + extension) ? digest + extension : digest
|
|
30
57
|
const directory = stagedDir()
|
|
31
58
|
const destination = path.join(directory, name)
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
import fs from 'fs'
|
|
7
7
|
import path from 'path'
|
|
8
8
|
import { LAZY_STUB_PATTERNS } from '@peach/compat/lazy-stub-patterns'
|
|
9
|
+
import { NATIVE_RUNTIME_PACKAGE_NAMES } from '@peach/compat/native-package-names'
|
|
9
10
|
import { COMPAT_STUBS } from '@peach/compat/stub-manifest'
|
|
10
11
|
import { extractPackageFromPath } from '../../peach-engine/src/stub-pattern-matcher'
|
|
11
12
|
import type { PackageVersions } from './packageVersionsContract'
|
|
12
13
|
|
|
13
|
-
function
|
|
14
|
+
function readInstalledPackage(projectRoot: string, name: string) {
|
|
14
15
|
let dir = projectRoot
|
|
15
16
|
for (;;) {
|
|
16
17
|
let manifest: unknown
|
|
@@ -27,11 +28,9 @@ function readInstalledVersion(projectRoot: string, name: string): string | null
|
|
|
27
28
|
dir = parent
|
|
28
29
|
continue
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
: undefined
|
|
34
|
-
return typeof version === 'string' && version.length > 0 ? version : null
|
|
31
|
+
return typeof manifest === 'object' && manifest !== null
|
|
32
|
+
? { root: path.join(dir, 'node_modules', name), manifest }
|
|
33
|
+
: null
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
36
|
|
|
@@ -46,21 +45,62 @@ export function readInstalledPackageVersions(
|
|
|
46
45
|
}
|
|
47
46
|
const versions: PackageVersions = {}
|
|
48
47
|
for (const name of [...names].sort()) {
|
|
49
|
-
const
|
|
50
|
-
|
|
48
|
+
const installed = readInstalledPackage(projectRoot, name)
|
|
49
|
+
const version = installed && Reflect.get(installed.manifest, 'version')
|
|
50
|
+
if (typeof version === 'string' && version.length > 0) versions[name] = version
|
|
51
51
|
}
|
|
52
52
|
return versions
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// plain expo and one servers need no peach plugin. the launcher reads their
|
|
56
|
-
// installed native packages from the
|
|
56
|
+
// installed native packages from the app's dependency graph. an unrelated
|
|
57
|
+
// package hoisted beside a workspace app is resolvable but is not linked.
|
|
57
58
|
export function readInstalledNativePackageVersions(projectRoot: string): PackageVersions {
|
|
59
|
+
const appRoot = fs.realpathSync(projectRoot)
|
|
60
|
+
const nativeNames = new Set<string>()
|
|
61
|
+
for (const specifier of [
|
|
62
|
+
'react-native',
|
|
63
|
+
...NATIVE_RUNTIME_PACKAGE_NAMES,
|
|
64
|
+
...LAZY_STUB_PATTERNS,
|
|
65
|
+
...Object.values(COMPAT_STUBS).map((entry) => entry.specifier),
|
|
66
|
+
]) {
|
|
67
|
+
const name = extractPackageFromPath(`node_modules/${specifier}`)
|
|
68
|
+
if (name) nativeNames.add(name)
|
|
69
|
+
}
|
|
70
|
+
const manifest: unknown = JSON.parse(
|
|
71
|
+
fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'),
|
|
72
|
+
)
|
|
73
|
+
if (typeof manifest !== 'object' || manifest === null) {
|
|
74
|
+
throw new Error('app package.json must contain an object')
|
|
75
|
+
}
|
|
76
|
+
const pending = [{ root: appRoot, manifest }]
|
|
77
|
+
const visited = new Set<string>()
|
|
78
|
+
const needed = new Set<string>()
|
|
79
|
+
for (let next = pending.pop(); next; next = pending.pop()) {
|
|
80
|
+
const root = fs.realpathSync(next.root)
|
|
81
|
+
if (visited.has(root)) continue
|
|
82
|
+
visited.add(root)
|
|
83
|
+
const fields = ['dependencies', 'optionalDependencies', 'peerDependencies']
|
|
84
|
+
if (root === appRoot) fields.push('devDependencies')
|
|
85
|
+
const peers = Reflect.get(next.manifest, 'peerDependenciesMeta')
|
|
86
|
+
for (const field of fields) {
|
|
87
|
+
const dependencies = Reflect.get(next.manifest, field)
|
|
88
|
+
if (typeof dependencies !== 'object' || dependencies === null) continue
|
|
89
|
+
for (const name of Object.keys(dependencies)) {
|
|
90
|
+
if (field === 'peerDependencies' && typeof peers === 'object' && peers !== null) {
|
|
91
|
+
const peer = Reflect.get(peers, name)
|
|
92
|
+
if (typeof peer === 'object' && peer !== null && Reflect.get(peer, 'optional'))
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
const installed = readInstalledPackage(root, name)
|
|
96
|
+
if (!installed) continue
|
|
97
|
+
if (nativeNames.has(name)) needed.add(name)
|
|
98
|
+
pending.push(installed)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
58
102
|
return readInstalledPackageVersions(
|
|
59
103
|
projectRoot,
|
|
60
|
-
[
|
|
61
|
-
'react-native',
|
|
62
|
-
...LAZY_STUB_PATTERNS,
|
|
63
|
-
...Object.values(COMPAT_STUBS).map((entry) => entry.specifier),
|
|
64
|
-
].map((specifier) => `node_modules/${specifier}`),
|
|
104
|
+
[...needed].map((specifier) => `node_modules/${specifier}`),
|
|
65
105
|
)
|
|
66
106
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// app resources and native configuration travel as a body, never in an http
|
|
2
|
+
// request target. routing and display options remain on the navigable url.
|
|
3
|
+
export const LAUNCH_METADATA_QUERY_PARAM = 'launchMetadata'
|
|
4
|
+
|
|
5
|
+
export const LAUNCH_METADATA_KEYS: readonly string[] = [
|
|
6
|
+
'appFonts',
|
|
7
|
+
'appSplash',
|
|
8
|
+
'appCatalog',
|
|
9
|
+
'nativeAppIdentity',
|
|
10
|
+
'nativeFirebase',
|
|
11
|
+
'appManifest',
|
|
12
|
+
'packageVersions',
|
|
13
|
+
'dotEnv',
|
|
14
|
+
'env',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export function readLaunchMetadata(wire: string): URLSearchParams {
|
|
18
|
+
const metadata = new URLSearchParams(wire)
|
|
19
|
+
const keys: ReadonlySet<string> = new Set(LAUNCH_METADATA_KEYS)
|
|
20
|
+
for (const key of metadata.keys()) {
|
|
21
|
+
if (!keys.has(key)) throw new Error(`invalid launch metadata field: ${key}`)
|
|
22
|
+
}
|
|
23
|
+
return metadata
|
|
24
|
+
}
|
package/src/metro-plugin.ts
CHANGED
|
@@ -12,7 +12,13 @@ import fs from 'fs'
|
|
|
12
12
|
import { createRequire } from 'module'
|
|
13
13
|
import { randomUUID } from 'node:crypto'
|
|
14
14
|
import path from 'path'
|
|
15
|
+
import {
|
|
16
|
+
getRuntimePlatformAdapter,
|
|
17
|
+
isRuntimePlatform,
|
|
18
|
+
} from '../../peach-engine/src/platform/adapter'
|
|
15
19
|
import { resolveAppProject } from '../cli/app-project'
|
|
20
|
+
import { collectAndroidAutolinkedPackages } from './android-autolinking'
|
|
21
|
+
import { insertAndroidAutolinkingFooter } from './androidAutolinkingContract'
|
|
16
22
|
import { tagRnxBundleForPlugin } from './dev-server-open'
|
|
17
23
|
import {
|
|
18
24
|
handleReplacementModuleRequest,
|
|
@@ -26,12 +32,11 @@ import {
|
|
|
26
32
|
toPeachProductionBundleUrl,
|
|
27
33
|
type RNXMetroModuleIdentityMetadata,
|
|
28
34
|
} from './metro-production-bundle'
|
|
35
|
+
import { insertNativeAppIdentityFooter } from './native-app-identity'
|
|
29
36
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} from './native-app-identity'
|
|
34
|
-
import { readNativeAppIdentityFromProject } from './native-app-identity-project'
|
|
37
|
+
readNativeAppIdentityFromProject,
|
|
38
|
+
readNativeAppIdentityFromSources,
|
|
39
|
+
} from './native-app-identity-project'
|
|
35
40
|
import { readNativeFirebaseConfig } from './native-firebase-config'
|
|
36
41
|
import {
|
|
37
42
|
insertNativeFirebaseFooter,
|
|
@@ -279,7 +284,7 @@ async function appendRNXModulePaths(
|
|
|
279
284
|
|
|
280
285
|
export function withPeach<T extends Record<string, unknown>>(
|
|
281
286
|
config: T,
|
|
282
|
-
options: { iosTarget?: string } = {},
|
|
287
|
+
options: { iosTarget?: string; androidMergedManifest?: string } = {},
|
|
283
288
|
): T {
|
|
284
289
|
const cfgProjectRoot = (config as MetroConfigWithServer).projectRoot || process.cwd()
|
|
285
290
|
const cfgWatchFolders = (config as MetroConfigWithServer).watchFolders ?? []
|
|
@@ -318,7 +323,14 @@ export function withPeach<T extends Record<string, unknown>>(
|
|
|
318
323
|
(defaultSerializer ||= createMetroDefaultSerializer(
|
|
319
324
|
serializerOptions.asyncRequireModulePath,
|
|
320
325
|
))
|
|
321
|
-
const
|
|
326
|
+
const bundlePlatform = graph.transformOptions?.platform
|
|
327
|
+
const [result, androidAutolinkedPackages] = await Promise.all([
|
|
328
|
+
serializer(entryPoint, preModules, graph, serializerOptions),
|
|
329
|
+
isRuntimePlatform(bundlePlatform) &&
|
|
330
|
+
getRuntimePlatformAdapter(bundlePlatform).isAndroid
|
|
331
|
+
? collectAndroidAutolinkedPackages(cfgProjectRoot)
|
|
332
|
+
: Promise.resolve(null),
|
|
333
|
+
])
|
|
322
334
|
const staticJson =
|
|
323
335
|
typeof result === 'string' &&
|
|
324
336
|
(serializerOptions.serializerOptions?.output === 'static' ||
|
|
@@ -347,46 +359,29 @@ export function withPeach<T extends Record<string, unknown>>(
|
|
|
347
359
|
'empty-module.js',
|
|
348
360
|
),
|
|
349
361
|
)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
362
|
+
if (androidAutolinkedPackages !== null) {
|
|
363
|
+
annotated = await mapMetroSerializerCode(annotated, (code) =>
|
|
364
|
+
insertAndroidAutolinkingFooter(code, androidAutolinkedPackages),
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
const identity =
|
|
368
|
+
readNativeAppIdentityFromProject(
|
|
369
|
+
cfgProjectRoot,
|
|
370
|
+
bundlePlatform,
|
|
371
|
+
parsePlistSource,
|
|
372
|
+
options.iosTarget,
|
|
373
|
+
options.androidMergedManifest,
|
|
374
|
+
) ??
|
|
375
|
+
readNativeAppIdentityFromSources(
|
|
354
376
|
cfgProjectRoot,
|
|
355
377
|
bundlePlatform,
|
|
356
378
|
parsePlistSource,
|
|
357
379
|
options.iosTarget,
|
|
358
380
|
)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (entry.isDirectory()) {
|
|
364
|
-
const names = entry.name.endsWith('.xcodeproj')
|
|
365
|
-
? ['project.pbxproj']
|
|
366
|
-
: ['Info.plist', 'AppDelegate.swift', 'AppDelegate.mm', 'AppDelegate.m']
|
|
367
|
-
for (const name of names) {
|
|
368
|
-
const relative = `ios/${entry.name}/${name}`
|
|
369
|
-
const file = path.join(cfgProjectRoot, relative)
|
|
370
|
-
if (fs.existsSync(file)) files[relative] = fs.readFileSync(file, 'utf8')
|
|
371
|
-
}
|
|
372
|
-
} else if (/AppDelegate\.(?:swift|mm|m)$/.test(entry.name)) {
|
|
373
|
-
const relative = `ios/${entry.name}`
|
|
374
|
-
const file = path.join(cfgProjectRoot, relative)
|
|
375
|
-
if (fs.existsSync(file)) files[relative] = fs.readFileSync(file, 'utf8')
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
identity = extractNativeAppIdentityFromFiles(
|
|
379
|
-
files,
|
|
380
|
-
bundlePlatform,
|
|
381
|
-
parsePlistSource,
|
|
382
|
-
options.iosTarget,
|
|
383
|
-
)
|
|
384
|
-
}
|
|
385
|
-
if (identity) {
|
|
386
|
-
annotated = await mapMetroSerializerCode(annotated, (code) =>
|
|
387
|
-
insertNativeAppIdentityFooter(code, identity),
|
|
388
|
-
)
|
|
389
|
-
}
|
|
381
|
+
if (identity) {
|
|
382
|
+
annotated = await mapMetroSerializerCode(annotated, (code) =>
|
|
383
|
+
insertNativeAppIdentityFooter(code, identity),
|
|
384
|
+
)
|
|
390
385
|
}
|
|
391
386
|
if (
|
|
392
387
|
[...(graph.dependencies?.values() ?? [])].some((module) =>
|