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/cli/commands/control.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs'
|
|
|
3
3
|
import http from 'node:http'
|
|
4
4
|
import https from 'node:https'
|
|
5
5
|
import { createRequire } from 'node:module'
|
|
6
|
+
import net from 'node:net'
|
|
6
7
|
import os from 'node:os'
|
|
7
8
|
import path from 'node:path'
|
|
8
9
|
import { bundleUrlForPlatform } from 'peach-engine/bundle-platform-url'
|
|
@@ -27,26 +28,40 @@ import {
|
|
|
27
28
|
readPeachConfigFromSearchParams,
|
|
28
29
|
type PeachConfig,
|
|
29
30
|
} from '../../src/config'
|
|
31
|
+
import {
|
|
32
|
+
desktopHostDelivery,
|
|
33
|
+
desktopHostPlatform,
|
|
34
|
+
headlessHostCommand,
|
|
35
|
+
} from '../../src/desktop-host-delivery'
|
|
30
36
|
import {
|
|
31
37
|
resolveConnectionInput,
|
|
32
38
|
type ResolvedDevBundle,
|
|
33
39
|
} from '../../src/dev-bundle-resolution'
|
|
34
40
|
import { readProjectDotEnv } from '../../src/dot-env'
|
|
41
|
+
import { awaitStampedExpoSwiftReady } from '../../src/expo-swift-ready'
|
|
35
42
|
import { flowTimeoutScale } from '../../src/flow-timeout-scale'
|
|
36
43
|
import {
|
|
37
44
|
isDaemonLockfileFresh,
|
|
38
45
|
isDevBridgeLockfileFresh,
|
|
46
|
+
PEACH_HOME_ENV,
|
|
39
47
|
readActiveRuntime,
|
|
40
48
|
readDaemonLockfile,
|
|
41
49
|
readDevBridgeLockfiles,
|
|
42
50
|
readLiveRuntimeVersions,
|
|
43
51
|
readSimulatorDriverPreference,
|
|
52
|
+
rnxHomeDir,
|
|
44
53
|
runtimeDir,
|
|
45
54
|
runtimesDir,
|
|
46
55
|
} from '../../src/home-paths'
|
|
47
56
|
import { projectLoopbackUrl } from '../../src/host/loopback-ports'
|
|
48
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
isStagedHostFileRequest,
|
|
59
|
+
stageHostBytes,
|
|
60
|
+
stageHostFile,
|
|
61
|
+
stageHostLaunchMetadata,
|
|
62
|
+
} from '../../src/host/staged-host-files'
|
|
49
63
|
import { readInstalledNativePackageVersions } from '../../src/installed-package-versions'
|
|
64
|
+
import { LAUNCH_METADATA_QUERY_PARAM } from '../../src/launchMetadataContract'
|
|
50
65
|
import { detectBundleModeFromText } from '../../src/metro-production-bundle'
|
|
51
66
|
import {
|
|
52
67
|
canExtractNativeAppIdentity,
|
|
@@ -96,10 +111,11 @@ import {
|
|
|
96
111
|
readCurrentSimId,
|
|
97
112
|
saveCurrentSimId,
|
|
98
113
|
} from '../current-sim'
|
|
114
|
+
import { findDesktopCompanion, nativeDesktopIsDefault } from '../desktop-companion'
|
|
99
115
|
import { validateDeviceOption } from '../device-option'
|
|
100
116
|
import { getDriver } from '../drivers/registry'
|
|
101
117
|
import { isTeamMachineSession, openUrl as openUrlInBrowser } from '../open-url'
|
|
102
|
-
import { diagnosticsAllowedForCurrentProcess } from '../privacy'
|
|
118
|
+
import { diagnosticsAllowedForCurrentProcess, isCi } from '../privacy'
|
|
103
119
|
import { rethrowIfExit, peachExit } from '../run-rnx'
|
|
104
120
|
import { collectRuntimeDiagnostics } from '../runtime-diagnostics'
|
|
105
121
|
import { rnxSelfInvocation } from '../self-invocation'
|
|
@@ -117,8 +133,8 @@ import {
|
|
|
117
133
|
} from '../ws-bridge'
|
|
118
134
|
import { readNativeProjectRenderer } from './detect'
|
|
119
135
|
import { inspectDescribe, inspectReadySnapshot, waitReadyReason } from './inspect/core'
|
|
120
|
-
import { isAgentEnv } from './inspect/env'
|
|
121
136
|
import { waitForSimReady } from './inspect/shared'
|
|
137
|
+
import { runInstallDesktop } from './install-desktop'
|
|
122
138
|
import { hasInstalledPeachSkills } from './skills'
|
|
123
139
|
import type { DriverId, DriverLaunchResult } from '../drivers/types'
|
|
124
140
|
|
|
@@ -146,8 +162,13 @@ interface DriverConnectWaitOptions {
|
|
|
146
162
|
const DEFAULT_DRIVER_CONNECT_TIMEOUT_MS = 120_000
|
|
147
163
|
const DRIVER_CONNECT_INTERVAL_MS = 100
|
|
148
164
|
const DRIVER_CONNECT_HOST_GRACE_MS = 5_000
|
|
149
|
-
|
|
150
|
-
|
|
165
|
+
// ceiling for a bridge that is alive but never becomes ready. a bridge that
|
|
166
|
+
// exits fails at once and a slow one is judged by its own lockfile and
|
|
167
|
+
// handshake, so this is not the expected startup time.
|
|
168
|
+
const OPEN_BRIDGE_SPAWN_TIMEOUT_MS = 60_000
|
|
169
|
+
const OPEN_BRIDGE_RUNTIME_TIMEOUT_MS = 600_000
|
|
170
|
+
const OPEN_BRIDGE_HEALTH_TIMEOUT_MS = 10_000
|
|
171
|
+
const OPEN_BRIDGE_SILENCE_MS = 30_000
|
|
151
172
|
// a closed port refuses at once, so this window only matters when something
|
|
152
173
|
// listens: a live bridge on a loaded machine can take longer than 250 ms to
|
|
153
174
|
// finish the handshake, and missing it spawned a second bridge or reported a
|
|
@@ -191,9 +212,19 @@ function isDriverId(value: string): value is DriverId {
|
|
|
191
212
|
return value === 'desktop' || value === 'electron' || value === 'playwright'
|
|
192
213
|
}
|
|
193
214
|
|
|
194
|
-
function simUsesDriver(
|
|
215
|
+
function simUsesDriver(
|
|
216
|
+
sim: BridgeSimInfo,
|
|
217
|
+
driverId: DriverId,
|
|
218
|
+
renderer?: 'chromium' | 'native',
|
|
219
|
+
hostExecutable?: string | null,
|
|
220
|
+
): boolean {
|
|
195
221
|
if (driverId === 'playwright') {
|
|
196
|
-
return
|
|
222
|
+
return (
|
|
223
|
+
sim.meta?.sootsimHostDriver === 'playwright' &&
|
|
224
|
+
(!renderer || sim.meta?.peachRenderer === renderer) &&
|
|
225
|
+
(hostExecutable === undefined ||
|
|
226
|
+
(hostExecutable !== null && sim.meta?.peachHostExecutable === hostExecutable))
|
|
227
|
+
)
|
|
197
228
|
}
|
|
198
229
|
if (driverId === 'desktop') {
|
|
199
230
|
return Boolean(
|
|
@@ -230,11 +261,21 @@ function normalizeKnownTarget(target: string): string {
|
|
|
230
261
|
return target.trim()
|
|
231
262
|
}
|
|
232
263
|
|
|
264
|
+
type BridgeOpenGate = {
|
|
265
|
+
newShell: boolean
|
|
266
|
+
simId?: string
|
|
267
|
+
simIdSource?: 'flag' | 'saved' | 'none'
|
|
268
|
+
driverId?: DriverId
|
|
269
|
+
renderer?: 'chromium' | 'native'
|
|
270
|
+
pinnedHostExecutable?: string | null
|
|
271
|
+
}
|
|
272
|
+
|
|
233
273
|
export async function ensureBridgeForOpen(
|
|
234
274
|
preferredPort: number,
|
|
235
275
|
explicitPort = false,
|
|
236
276
|
resources?: OpenCommandResources,
|
|
237
277
|
runtimeSource: 'bridge' | 'external' = 'bridge',
|
|
278
|
+
gate: BridgeOpenGate = { newShell: true },
|
|
238
279
|
): Promise<number> {
|
|
239
280
|
const port = await connectBridgeForOpen(
|
|
240
281
|
preferredPort,
|
|
@@ -243,22 +284,107 @@ export async function ensureBridgeForOpen(
|
|
|
243
284
|
runtimeSource,
|
|
244
285
|
)
|
|
245
286
|
// a bridge accepts connections while its first runtime download is still
|
|
246
|
-
// running, and
|
|
287
|
+
// running, and a shell page served by that bridge answers 503 until the
|
|
288
|
+
// runtime directory exists. a registered page is not that runtime: --new
|
|
289
|
+
// would hand the driver the 503 url. skip the wait only when this open
|
|
290
|
+
// will reuse a page whose origin already answers. a new shell waits until
|
|
291
|
+
// the bridge install settles or the shell itself answers, and does not
|
|
292
|
+
// start a second install over the one already in flight.
|
|
247
293
|
if (runtimeSource === 'bridge') {
|
|
248
|
-
|
|
249
|
-
|
|
294
|
+
const reusingReadyOrigin =
|
|
295
|
+
!gate.newShell && (await provenReadyReuseOrigin(port, gate))
|
|
296
|
+
if (!reusingReadyOrigin) {
|
|
297
|
+
const readiness = await waitForBridgeRuntime(port)
|
|
298
|
+
if (readiness === 'settled') await ensureRuntimeForOpen()
|
|
299
|
+
}
|
|
250
300
|
}
|
|
251
301
|
return port
|
|
252
302
|
}
|
|
253
303
|
|
|
254
|
-
async function
|
|
304
|
+
async function provenReadyReuseOrigin(
|
|
305
|
+
port: number,
|
|
306
|
+
gate: BridgeOpenGate,
|
|
307
|
+
): Promise<boolean> {
|
|
308
|
+
if (!gate.driverId || !gate.simId || gate.simIdSource === 'none' || !gate.simIdSource) {
|
|
309
|
+
return false
|
|
310
|
+
}
|
|
311
|
+
const bridge = createBridge(port, { commandTimeoutMs: 2_000 })
|
|
312
|
+
try {
|
|
313
|
+
const sims = await bridge.listSims()
|
|
314
|
+
const sim = sims.find((candidate) => candidate.id === gate.simId)
|
|
315
|
+
if (!sim?.url || sim.readyState !== 'open') return false
|
|
316
|
+
if (
|
|
317
|
+
!simUsesDriver(
|
|
318
|
+
sim,
|
|
319
|
+
gate.driverId,
|
|
320
|
+
gate.renderer,
|
|
321
|
+
gate.pinnedHostExecutable ?? undefined,
|
|
322
|
+
)
|
|
323
|
+
) {
|
|
324
|
+
return false
|
|
325
|
+
}
|
|
326
|
+
return await originServesShell(sim.url)
|
|
327
|
+
} catch {
|
|
328
|
+
return false
|
|
329
|
+
} finally {
|
|
330
|
+
bridge.close()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function originServesShell(pageOrOrigin: string): Promise<boolean> {
|
|
335
|
+
try {
|
|
336
|
+
const origin = new URL(pageOrOrigin).origin
|
|
337
|
+
const res = await fetch(origin, { signal: AbortSignal.timeout(2_000) })
|
|
338
|
+
return res.status !== 503
|
|
339
|
+
} catch {
|
|
340
|
+
return false
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// the install runs inside the bridge and reports completion through
|
|
345
|
+
// /healthz. a bridge that answers is installing, so only a bridge that is gone
|
|
346
|
+
// (connection refused) or silent for a while fails the wait; the ceiling only
|
|
347
|
+
// bounds an install that never finishes.
|
|
348
|
+
function portRefusesConnections(port: number): Promise<boolean> {
|
|
349
|
+
return new Promise((resolve) => {
|
|
350
|
+
let refused = false
|
|
351
|
+
const socket = net.connect(port, '127.0.0.1')
|
|
352
|
+
socket.setTimeout(OPEN_BRIDGE_HEALTH_TIMEOUT_MS, () => socket.destroy())
|
|
353
|
+
socket.once('connect', () => socket.destroy())
|
|
354
|
+
socket.once('close', () => resolve(refused))
|
|
355
|
+
socket.once('error', (err: NodeJS.ErrnoException) => {
|
|
356
|
+
refused = err.code === 'ECONNREFUSED'
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function waitForBridgeRuntime(port: number): Promise<'settled' | 'serving'> {
|
|
255
362
|
const deadline = Date.now() + OPEN_BRIDGE_RUNTIME_TIMEOUT_MS
|
|
363
|
+
let lastAnswerAt = Date.now()
|
|
256
364
|
while (Date.now() < deadline) {
|
|
257
365
|
try {
|
|
258
|
-
const res = await fetch(`http://127.0.0.1:${port}/healthz
|
|
366
|
+
const res = await fetch(`http://127.0.0.1:${port}/healthz`, {
|
|
367
|
+
signal: AbortSignal.timeout(OPEN_BRIDGE_HEALTH_TIMEOUT_MS),
|
|
368
|
+
})
|
|
259
369
|
const health = (await res.json()) as { bootstrapping?: boolean }
|
|
260
|
-
if (!health.bootstrapping) return
|
|
261
|
-
|
|
370
|
+
if (!health.bootstrapping) return 'settled'
|
|
371
|
+
lastAnswerAt = Date.now()
|
|
372
|
+
// the install flag can stay true after the shell directory is already
|
|
373
|
+
// being served. a new page can use that origin, and installing again
|
|
374
|
+
// would race the download still in flight.
|
|
375
|
+
if (await originServesShell(`http://127.0.0.1:${port}/`)) return 'serving'
|
|
376
|
+
} catch {
|
|
377
|
+
if (await portRefusesConnections(port)) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
`peach bridge on port ${port} exited while installing its runtime`,
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
if (Date.now() - lastAnswerAt > OPEN_BRIDGE_SILENCE_MS) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`peach bridge on port ${port} stopped answering for ${Math.round(OPEN_BRIDGE_SILENCE_MS / 1000)}s while installing its runtime`,
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
262
388
|
await sleep(150)
|
|
263
389
|
}
|
|
264
390
|
throw new Error(
|
|
@@ -272,15 +398,18 @@ async function connectBridgeForOpen(
|
|
|
272
398
|
resources: OpenCommandResources | undefined,
|
|
273
399
|
runtimeSource: 'bridge' | 'external',
|
|
274
400
|
): Promise<number> {
|
|
401
|
+
isolateHomeForExplicitPort(preferredPort, explicitPort, readDaemonLockfile())
|
|
275
402
|
const lock = readDaemonLockfile()
|
|
276
|
-
assertExplicitBridgeLock(preferredPort, explicitPort, lock)
|
|
277
403
|
const preferredWorld = worldForBridgePort(preferredPort)
|
|
278
|
-
const
|
|
404
|
+
const handshakeTimeoutMs = LIVE_BRIDGE_HANDSHAKE_TIMEOUT_MS * flowTimeoutScale()
|
|
405
|
+
const handshake = await bridgeHandshake(preferredPort, handshakeTimeoutMs)
|
|
279
406
|
if ((explicitPort || preferredWorld !== null) && handshake === 'open') {
|
|
280
407
|
return preferredPort
|
|
281
408
|
}
|
|
282
409
|
if (explicitPort && preferredWorld !== null) {
|
|
283
|
-
throw new Error(
|
|
410
|
+
throw new Error(
|
|
411
|
+
unreachableBridgeMessage(preferredPort, handshake, handshakeTimeoutMs),
|
|
412
|
+
)
|
|
284
413
|
}
|
|
285
414
|
if (
|
|
286
415
|
!explicitPort &&
|
|
@@ -288,7 +417,7 @@ async function connectBridgeForOpen(
|
|
|
288
417
|
isDaemonLockfileFresh(lock) &&
|
|
289
418
|
(lock.bridgePort === preferredPort
|
|
290
419
|
? handshake === 'open'
|
|
291
|
-
: await canConnectToBridge(lock.bridgePort,
|
|
420
|
+
: await canConnectToBridge(lock.bridgePort, handshakeTimeoutMs))
|
|
292
421
|
) {
|
|
293
422
|
return lock.bridgePort
|
|
294
423
|
}
|
|
@@ -331,8 +460,10 @@ async function connectBridgeForOpen(
|
|
|
331
460
|
resources.port = preferredPort
|
|
332
461
|
}
|
|
333
462
|
let childExit: string | null = null
|
|
463
|
+
const exited = new AbortController()
|
|
334
464
|
child.once('exit', (code, signal) => {
|
|
335
465
|
childExit = String(code ?? signal ?? 'unknown')
|
|
466
|
+
exited.abort()
|
|
336
467
|
})
|
|
337
468
|
child.unref()
|
|
338
469
|
|
|
@@ -355,11 +486,18 @@ async function connectBridgeForOpen(
|
|
|
355
486
|
)
|
|
356
487
|
}
|
|
357
488
|
const selectedPort = explicitPort ? preferredPort : nextLock?.bridgePort
|
|
489
|
+
// ready is the spawned bridge's own lockfile plus a completed handshake. a
|
|
490
|
+
// loaded machine needs the full handshake budget, not a 250ms probe.
|
|
358
491
|
if (
|
|
359
492
|
nextLock &&
|
|
493
|
+
nextLock.pid === child.pid &&
|
|
360
494
|
isDaemonLockfileFresh(nextLock) &&
|
|
361
495
|
selectedPort !== undefined &&
|
|
362
|
-
(await canConnectToBridge(
|
|
496
|
+
(await canConnectToBridge(
|
|
497
|
+
selectedPort,
|
|
498
|
+
Math.max(1, Math.min(handshakeTimeoutMs, deadline - Date.now())),
|
|
499
|
+
exited.signal,
|
|
500
|
+
))
|
|
363
501
|
) {
|
|
364
502
|
console.error(
|
|
365
503
|
` Started a local bridge on port ${selectedPort}. Run \`peach daemon install\` to keep it ready between commands.`,
|
|
@@ -371,7 +509,7 @@ async function connectBridgeForOpen(
|
|
|
371
509
|
return selectedPort
|
|
372
510
|
}
|
|
373
511
|
if (childExit) break
|
|
374
|
-
await sleep(150)
|
|
512
|
+
await sleep(Math.max(0, Math.min(150, deadline - Date.now())))
|
|
375
513
|
}
|
|
376
514
|
|
|
377
515
|
const reason = childExit
|
|
@@ -383,7 +521,10 @@ async function connectBridgeForOpen(
|
|
|
383
521
|
)
|
|
384
522
|
}
|
|
385
523
|
|
|
386
|
-
|
|
524
|
+
// one bridge per peach home, so an explicit port the active daemon does not own
|
|
525
|
+
// gets its own home: this command and the bridge it starts both run in it,
|
|
526
|
+
// sharing the daemon's installed runtimes.
|
|
527
|
+
function isolateHomeForExplicitPort(
|
|
387
528
|
preferredPort: number,
|
|
388
529
|
explicitPort: boolean,
|
|
389
530
|
lock: ReturnType<typeof readDaemonLockfile>,
|
|
@@ -404,9 +545,14 @@ function assertExplicitBridgeLock(
|
|
|
404
545
|
) {
|
|
405
546
|
return
|
|
406
547
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
)
|
|
548
|
+
const sharedRuntimes = runtimesDir()
|
|
549
|
+
const home = path.join(rnxHomeDir(), 'port-homes', String(preferredPort))
|
|
550
|
+
fs.mkdirSync(home, { recursive: true })
|
|
551
|
+
const runtimes = path.join(home, 'runtimes')
|
|
552
|
+
if (fs.existsSync(sharedRuntimes) && !fs.existsSync(runtimes)) {
|
|
553
|
+
fs.symlinkSync(sharedRuntimes, runtimes)
|
|
554
|
+
}
|
|
555
|
+
process.env[PEACH_HOME_ENV] = home
|
|
410
556
|
}
|
|
411
557
|
|
|
412
558
|
async function ensureRuntimeForOpen() {
|
|
@@ -430,7 +576,11 @@ async function ensureRuntimeForOpen() {
|
|
|
430
576
|
|
|
431
577
|
type BridgeHandshake = 'open' | 'refused' | 'timeout'
|
|
432
578
|
|
|
433
|
-
function bridgeHandshake(
|
|
579
|
+
function bridgeHandshake(
|
|
580
|
+
port: number,
|
|
581
|
+
timeoutMs: number,
|
|
582
|
+
signal?: AbortSignal,
|
|
583
|
+
): Promise<BridgeHandshake> {
|
|
434
584
|
return new Promise((resolve) => {
|
|
435
585
|
const ws = new WebSocket(`ws://127.0.0.1:${port}`, { handshakeTimeout: timeoutMs })
|
|
436
586
|
let settled = false
|
|
@@ -438,11 +588,15 @@ function bridgeHandshake(port: number, timeoutMs: number): Promise<BridgeHandsha
|
|
|
438
588
|
if (settled) return
|
|
439
589
|
settled = true
|
|
440
590
|
clearTimeout(timer)
|
|
591
|
+
signal?.removeEventListener('abort', onAbort)
|
|
441
592
|
if (ws.readyState === WebSocket.OPEN) ws.close()
|
|
442
593
|
else if (ws.readyState === WebSocket.CONNECTING) ws.terminate()
|
|
443
594
|
resolve(result)
|
|
444
595
|
}
|
|
445
596
|
const timer = setTimeout(() => finish('timeout'), timeoutMs)
|
|
597
|
+
const onAbort = () => finish('timeout')
|
|
598
|
+
if (signal?.aborted) onAbort()
|
|
599
|
+
else signal?.addEventListener('abort', onAbort)
|
|
446
600
|
ws.once('open', () => finish('open'))
|
|
447
601
|
ws.on('error', (err: NodeJS.ErrnoException) => {
|
|
448
602
|
finish(
|
|
@@ -452,19 +606,27 @@ function bridgeHandshake(port: number, timeoutMs: number): Promise<BridgeHandsha
|
|
|
452
606
|
})
|
|
453
607
|
}
|
|
454
608
|
|
|
455
|
-
function unreachableBridgeMessage(
|
|
609
|
+
function unreachableBridgeMessage(
|
|
610
|
+
port: number,
|
|
611
|
+
handshake: BridgeHandshake,
|
|
612
|
+
timeoutMs: number,
|
|
613
|
+
): string {
|
|
456
614
|
if (handshake === 'timeout') {
|
|
457
615
|
const cores = Math.max(1, os.cpus().length)
|
|
458
616
|
const load = os.loadavg()[0]
|
|
459
617
|
if (load >= cores) {
|
|
460
|
-
return `peach open: requested bridge port ${port} did not finish its handshake within
|
|
618
|
+
return `peach open: requested bridge port ${port} did not finish its handshake within ${timeoutMs / 1000}s while load average was ${load.toFixed(1)} on ${cores} cores.`
|
|
461
619
|
}
|
|
462
620
|
}
|
|
463
621
|
return `peach open: requested bridge port ${port} is not reachable.`
|
|
464
622
|
}
|
|
465
623
|
|
|
466
|
-
function canConnectToBridge(
|
|
467
|
-
|
|
624
|
+
function canConnectToBridge(
|
|
625
|
+
port: number,
|
|
626
|
+
timeoutMs: number,
|
|
627
|
+
signal?: AbortSignal,
|
|
628
|
+
): Promise<boolean> {
|
|
629
|
+
return bridgeHandshake(port, timeoutMs, signal).then((result) => result === 'open')
|
|
468
630
|
}
|
|
469
631
|
|
|
470
632
|
function looksLikeSootsimUrl(target: string): boolean {
|
|
@@ -585,11 +747,12 @@ async function resolveConnectionInputForCli(target: string): Promise<ResolvedDev
|
|
|
585
747
|
const releaseListenerScan = pinSharedListenerScan()
|
|
586
748
|
try {
|
|
587
749
|
for (let attempt = 0; attempt < DEV_BUNDLE_RESOLVE_ATTEMPTS; attempt++) {
|
|
750
|
+
let resolutionTarget = normalized
|
|
588
751
|
if (registeredPort && registeredPort > 0) {
|
|
589
752
|
const discovered = await probePort(registeredPort)
|
|
590
753
|
// a provisional bundle URL is enough to LIST the server but not to open
|
|
591
|
-
// it;
|
|
592
|
-
//
|
|
754
|
+
// it; resolveConnectionInput joins the scanner's pending manifest with
|
|
755
|
+
// the full startup budget instead of starting the same work again.
|
|
593
756
|
if (discovered && !discovered.bundleUrlProvisional) {
|
|
594
757
|
return {
|
|
595
758
|
bundleUrl: projectLoopbackUrl(discovered.bundleUrl),
|
|
@@ -598,10 +761,15 @@ async function resolveConnectionInputForCli(target: string): Promise<ResolvedDev
|
|
|
598
761
|
projectName: discovered.projectName,
|
|
599
762
|
}
|
|
600
763
|
}
|
|
764
|
+
// keep the scanner's answering address and join its manifest request.
|
|
765
|
+
// probing both loopback families again duplicates cold config work.
|
|
766
|
+
if (discovered?.bundleUrlProvisional && /^\d+$/.test(normalized)) {
|
|
767
|
+
resolutionTarget = new URL(discovered.bundleUrl).origin
|
|
768
|
+
}
|
|
601
769
|
}
|
|
602
770
|
|
|
603
771
|
try {
|
|
604
|
-
const resolved = await resolveConnectionInput(
|
|
772
|
+
const resolved = await resolveConnectionInput(resolutionTarget)
|
|
605
773
|
return { ...resolved, bundleUrl: projectLoopbackUrl(resolved.bundleUrl) }
|
|
606
774
|
} catch (error) {
|
|
607
775
|
if (
|
|
@@ -850,7 +1018,7 @@ export function resolveShellBaseUrlForBridgePort(bridgePort: number): string {
|
|
|
850
1018
|
)
|
|
851
1019
|
}
|
|
852
1020
|
|
|
853
|
-
// stamp the engine page url
|
|
1021
|
+
// stamp a bounded catalog reference on the engine page url so the
|
|
854
1022
|
// engine's app-font-loader registers native config-only fonts at boot. fonts
|
|
855
1023
|
// loaded by JS already flow through normal runtime fetch capture; this covers
|
|
856
1024
|
// UIAppFonts-style assets that Metro can serve but the guest bundle never
|
|
@@ -865,16 +1033,18 @@ function stampAppFonts(
|
|
|
865
1033
|
): string {
|
|
866
1034
|
try {
|
|
867
1035
|
const url = new URL(urlStr)
|
|
868
|
-
|
|
869
|
-
if (!bundleUrl || !project) return urlStr
|
|
1036
|
+
if (!url.searchParams.get('bundle') || !project) return urlStr
|
|
870
1037
|
const specs = discoverNativeLinkedAppFonts({
|
|
871
|
-
bundleUrl,
|
|
872
1038
|
projectDir: project.appDir,
|
|
873
1039
|
platform,
|
|
874
1040
|
nativeAssetOrigin,
|
|
875
1041
|
})
|
|
876
1042
|
if (specs.length === 0) return urlStr
|
|
877
|
-
|
|
1043
|
+
const catalogUrl = stageHostBytes(
|
|
1044
|
+
new TextEncoder().encode(encodeAppFontsWire(specs)),
|
|
1045
|
+
nativeAssetOrigin,
|
|
1046
|
+
)
|
|
1047
|
+
url.searchParams.set('appFonts', `@${catalogUrl}`)
|
|
878
1048
|
return url.toString()
|
|
879
1049
|
} catch {
|
|
880
1050
|
return urlStr
|
|
@@ -1012,6 +1182,10 @@ export interface BuildShellUrlOptions {
|
|
|
1012
1182
|
// after the app path lands inside the bundle param and never reaches the
|
|
1013
1183
|
// shell. non-foldable devices resolve it to closed at boot.
|
|
1014
1184
|
posture?: string
|
|
1185
|
+
// raster quality (high|medium|low) stamped as ?renderResolution=. high
|
|
1186
|
+
// renders at the device's native scale, so a native-parity capture is not
|
|
1187
|
+
// left at the interactive medium default of 2.
|
|
1188
|
+
renderResolution?: string
|
|
1015
1189
|
// interface orientation (portrait|landscape) stamped as ?orientation=,
|
|
1016
1190
|
// which the shell reads at boot over the device's default: an open duo
|
|
1017
1191
|
// boots landscape unless this asks for it upright.
|
|
@@ -1059,45 +1233,74 @@ export interface BuildShellUrlOptions {
|
|
|
1059
1233
|
bridgePort?: number
|
|
1060
1234
|
}
|
|
1061
1235
|
|
|
1062
|
-
|
|
1236
|
+
type ProjectExpoConfigRead = { config: Record<string, unknown> } | { error: string }
|
|
1237
|
+
|
|
1238
|
+
function errorMessage(error: unknown): string {
|
|
1239
|
+
return error instanceof Error ? error.message : String(error)
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function readProjectExpoConfig(
|
|
1063
1243
|
project: ResolvedAppProject,
|
|
1064
|
-
): Promise<
|
|
1244
|
+
): Promise<ProjectExpoConfigRead | null> {
|
|
1065
1245
|
const require = createRequire(path.join(project.appDir, 'package.json'))
|
|
1066
1246
|
try {
|
|
1067
1247
|
require.resolve('@expo/config')
|
|
1068
1248
|
} catch {
|
|
1069
1249
|
return Promise.resolve(null)
|
|
1070
1250
|
}
|
|
1071
|
-
return readExpoConfigAsync(project.appDir)
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
.catch((error: unknown) => ({
|
|
1076
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1077
|
-
}))
|
|
1251
|
+
return readExpoConfigAsync(project.appDir).then(
|
|
1252
|
+
(config) => ({ config }),
|
|
1253
|
+
(error: unknown) => ({ error: errorMessage(error) }),
|
|
1254
|
+
)
|
|
1078
1255
|
}
|
|
1079
1256
|
|
|
1080
1257
|
// evaluating the Expo config spawns node, so `peach open` starts the read as
|
|
1081
1258
|
// soon as the project resolves and the shell url it builds takes that read. a
|
|
1082
1259
|
// later build reads the config again.
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
function startNativeFirebaseConfigRead(project: ResolvedAppProject | null) {
|
|
1089
|
-
if (project)
|
|
1090
|
-
startedNativeFirebaseConfigs.set(
|
|
1091
|
-
project.appDir,
|
|
1092
|
-
readProjectNativeFirebaseConfig(project),
|
|
1093
|
-
)
|
|
1260
|
+
const startedExpoConfigReads = new Map<string, Promise<ProjectExpoConfigRead | null>>()
|
|
1261
|
+
|
|
1262
|
+
function startExpoConfigRead(project: ResolvedAppProject | null) {
|
|
1263
|
+
if (project) startedExpoConfigReads.set(project.appDir, readProjectExpoConfig(project))
|
|
1094
1264
|
}
|
|
1095
1265
|
|
|
1096
|
-
function
|
|
1266
|
+
function takeExpoConfigRead(project: ResolvedAppProject | null | undefined) {
|
|
1097
1267
|
if (!project) return Promise.resolve(null)
|
|
1098
|
-
const started =
|
|
1099
|
-
|
|
1100
|
-
return started ??
|
|
1268
|
+
const started = startedExpoConfigReads.get(project.appDir)
|
|
1269
|
+
startedExpoConfigReads.delete(project.appDir)
|
|
1270
|
+
return started ?? readProjectExpoConfig(project)
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function nativeFirebaseFromExpoConfig(
|
|
1274
|
+
project: ResolvedAppProject,
|
|
1275
|
+
read: ProjectExpoConfigRead,
|
|
1276
|
+
): Record<string, unknown> {
|
|
1277
|
+
if ('error' in read) return read
|
|
1278
|
+
try {
|
|
1279
|
+
return readNativeFirebaseConfig(project.appDir, undefined, undefined, read.config)
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
return { error: errorMessage(error) }
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// a built bundle staged on the bridge has no packager to serve its manifest.
|
|
1286
|
+
// the built binary embeds its evaluated app config (expo's `app.config`
|
|
1287
|
+
// resource, and the Info.plist prebuild writes from it), so the bundle's
|
|
1288
|
+
// manifest is that config, staged beside it
|
|
1289
|
+
function stageBuiltBundleManifest(
|
|
1290
|
+
bundleUrl: string,
|
|
1291
|
+
config: Record<string, unknown>,
|
|
1292
|
+
): string | null {
|
|
1293
|
+
const bundle = new URL(bundleUrl)
|
|
1294
|
+
if (!isStagedHostFileRequest(bundle.pathname) || Object.keys(config).length === 0)
|
|
1295
|
+
return null
|
|
1296
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'peach-app-manifest-'))
|
|
1297
|
+
try {
|
|
1298
|
+
const file = path.join(directory, 'manifest.json')
|
|
1299
|
+
fs.writeFileSync(file, JSON.stringify({ extra: { expoClient: config } }))
|
|
1300
|
+
return stageHostFile(file, bundle.origin)
|
|
1301
|
+
} finally {
|
|
1302
|
+
fs.rmSync(directory, { recursive: true, force: true })
|
|
1303
|
+
}
|
|
1101
1304
|
}
|
|
1102
1305
|
|
|
1103
1306
|
// a plain Metro carries no identity footer or header, so the page carries the
|
|
@@ -1200,6 +1403,7 @@ export async function buildShellUrl(
|
|
|
1200
1403
|
// own `@expo/config` (a dependency-free recorder) leaves it to the Peach
|
|
1201
1404
|
// Metro plugin's endpoint.
|
|
1202
1405
|
expoStamped.searchParams.delete('nativeFirebase')
|
|
1406
|
+
expoStamped.searchParams.delete('appManifest')
|
|
1203
1407
|
expoStamped.searchParams.delete('packageVersions')
|
|
1204
1408
|
if (project && expoStamped.searchParams.get('bundle')) {
|
|
1205
1409
|
expoStamped.searchParams.set(
|
|
@@ -1207,9 +1411,25 @@ export async function buildShellUrl(
|
|
|
1207
1411
|
JSON.stringify(readInstalledNativePackageVersions(project.appDir)),
|
|
1208
1412
|
)
|
|
1209
1413
|
}
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1414
|
+
const expoConfigRead = await takeExpoConfigRead(project)
|
|
1415
|
+
const stampedBundle = expoStamped.searchParams.get('bundle')
|
|
1416
|
+
if (project && !expoConfigRead && stampedBundle) {
|
|
1417
|
+
// no Expo config tooling, but the native project still configures Firebase
|
|
1418
|
+
const nativeFirebase = nativeFirebaseFromExpoConfig(project, { config: {} })
|
|
1419
|
+
if (Object.keys(nativeFirebase).length > 0)
|
|
1420
|
+
expoStamped.searchParams.set('nativeFirebase', JSON.stringify(nativeFirebase))
|
|
1421
|
+
}
|
|
1422
|
+
if (project && expoConfigRead && stampedBundle) {
|
|
1423
|
+
expoStamped.searchParams.set(
|
|
1424
|
+
'nativeFirebase',
|
|
1425
|
+
JSON.stringify(nativeFirebaseFromExpoConfig(project, expoConfigRead)),
|
|
1426
|
+
)
|
|
1427
|
+
const manifest =
|
|
1428
|
+
'config' in expoConfigRead
|
|
1429
|
+
? stageBuiltBundleManifest(stampedBundle, expoConfigRead.config)
|
|
1430
|
+
: null
|
|
1431
|
+
if (manifest) expoStamped.searchParams.set('appManifest', manifest)
|
|
1432
|
+
}
|
|
1213
1433
|
if (opts.bundleOriginOverride)
|
|
1214
1434
|
expoStamped.searchParams.set('bundleOriginOverride', opts.bundleOriginOverride)
|
|
1215
1435
|
if (opts.hostDriver) expoStamped.searchParams.set('peachHostDriver', opts.hostDriver)
|
|
@@ -1229,24 +1449,27 @@ export async function buildShellUrl(
|
|
|
1229
1449
|
!opts.proof &&
|
|
1230
1450
|
!opts.ephemeral &&
|
|
1231
1451
|
!opts.posture &&
|
|
1452
|
+
!opts.renderResolution &&
|
|
1232
1453
|
!opts.orientation &&
|
|
1233
1454
|
!opts.osVersion &&
|
|
1234
1455
|
!opts.theme &&
|
|
1235
1456
|
!opts.env?.length &&
|
|
1236
1457
|
!opts.dotEnv
|
|
1237
1458
|
)
|
|
1238
|
-
return expoStamped.
|
|
1459
|
+
return stageHostLaunchMetadata(expoStamped, `http://localhost:${port.port}`)
|
|
1239
1460
|
const url = expoStamped
|
|
1240
1461
|
if (opts.dotEnv) url.searchParams.set('dotEnv', JSON.stringify(opts.dotEnv))
|
|
1241
1462
|
if (opts.device) url.searchParams.set('device', opts.device)
|
|
1242
1463
|
if (opts.posture) url.searchParams.set('posture', opts.posture)
|
|
1464
|
+
if (opts.renderResolution)
|
|
1465
|
+
url.searchParams.set('renderResolution', opts.renderResolution)
|
|
1243
1466
|
if (opts.orientation) url.searchParams.set('orientation', opts.orientation)
|
|
1244
1467
|
if (opts.osVersion) url.searchParams.set('osVersion', opts.osVersion)
|
|
1245
1468
|
if (opts.theme) url.searchParams.set('theme', opts.theme)
|
|
1246
1469
|
if (opts.proof) url.searchParams.set('proof', '1')
|
|
1247
1470
|
if (opts.ephemeral) url.searchParams.set('ephemeral', '1')
|
|
1248
1471
|
for (const entry of opts.env ?? []) url.searchParams.append('env', entry)
|
|
1249
|
-
return url.
|
|
1472
|
+
return stageHostLaunchMetadata(url, `http://localhost:${port.port}`)
|
|
1250
1473
|
}
|
|
1251
1474
|
|
|
1252
1475
|
function deriveCurrentSimBaseUrl(
|
|
@@ -1278,6 +1501,7 @@ function deriveCurrentSimBaseUrl(
|
|
|
1278
1501
|
url.searchParams.delete('nativeFirebase')
|
|
1279
1502
|
url.searchParams.delete('packageVersions')
|
|
1280
1503
|
url.searchParams.delete('nativeAppIdentity')
|
|
1504
|
+
url.searchParams.delete(LAUNCH_METADATA_QUERY_PARAM)
|
|
1281
1505
|
url.searchParams.delete('expoSwift')
|
|
1282
1506
|
url.searchParams.delete('expoKotlin')
|
|
1283
1507
|
return url.toString()
|
|
@@ -1331,13 +1555,27 @@ export async function waitForSimMatchOrHostExit(
|
|
|
1331
1555
|
commandTimeoutMs: number,
|
|
1332
1556
|
predicate: (sim: BridgeSimInfo) => boolean,
|
|
1333
1557
|
hostPid: number | undefined,
|
|
1334
|
-
opts: {
|
|
1558
|
+
opts: {
|
|
1559
|
+
attempts?: number
|
|
1560
|
+
intervalMs?: number
|
|
1561
|
+
signal?: AbortSignal
|
|
1562
|
+
admittedFile?: string
|
|
1563
|
+
} = {},
|
|
1335
1564
|
): Promise<WaitForSimMatchOrHostExitResult> {
|
|
1336
1565
|
const attempts = opts.attempts ?? 30
|
|
1337
1566
|
const intervalMs = opts.intervalMs ?? 500
|
|
1338
1567
|
const watchHost = !!hostPid && Number.isInteger(hostPid) && hostPid > 1
|
|
1339
1568
|
const hostExited = () =>
|
|
1340
1569
|
watchHost && hostPid !== undefined && !isProcessRunning(hostPid)
|
|
1570
|
+
// a host queued behind the machine's browser cap has not started its connect
|
|
1571
|
+
// budget, so neither does this wait; the caller's signal still bounds it.
|
|
1572
|
+
if (opts.admittedFile && watchHost) {
|
|
1573
|
+
while (!fs.existsSync(opts.admittedFile)) {
|
|
1574
|
+
opts.signal?.throwIfAborted()
|
|
1575
|
+
if (hostExited()) return { kind: 'host-exit' }
|
|
1576
|
+
await sleep(intervalMs)
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1341
1579
|
for (let i = 0; i < attempts; i++) {
|
|
1342
1580
|
opts.signal?.throwIfAborted()
|
|
1343
1581
|
if (hostExited()) {
|
|
@@ -1745,6 +1983,7 @@ export async function runOpenCommand(
|
|
|
1745
1983
|
stripBooleanFlags: [
|
|
1746
1984
|
'--new',
|
|
1747
1985
|
'--headless',
|
|
1986
|
+
'--chromium',
|
|
1748
1987
|
'--headed',
|
|
1749
1988
|
'--ephemeral',
|
|
1750
1989
|
'--hot',
|
|
@@ -1764,6 +2003,7 @@ export async function runOpenCommand(
|
|
|
1764
2003
|
'--cdp-port',
|
|
1765
2004
|
'--device',
|
|
1766
2005
|
'--posture',
|
|
2006
|
+
'--render-resolution',
|
|
1767
2007
|
'--orientation',
|
|
1768
2008
|
'--os-version',
|
|
1769
2009
|
'--ios-target',
|
|
@@ -1836,7 +2076,9 @@ export async function runOpenCommand(
|
|
|
1836
2076
|
peachExit(1)
|
|
1837
2077
|
}
|
|
1838
2078
|
|
|
1839
|
-
|
|
2079
|
+
// an absolute path to a built bundle is a file, not an app path
|
|
2080
|
+
const prebuiltBundle = prebuiltBundleFile(target)
|
|
2081
|
+
if (!prebuiltBundle && isAppDeepLinkTarget(target)) {
|
|
1840
2082
|
if (openInNewSim) {
|
|
1841
2083
|
console.error(
|
|
1842
2084
|
' peach open: deep links target the current app; remove --new, --profile, or --ephemeral',
|
|
@@ -1879,7 +2121,6 @@ export async function runOpenCommand(
|
|
|
1879
2121
|
return
|
|
1880
2122
|
}
|
|
1881
2123
|
|
|
1882
|
-
const prebuiltBundle = prebuiltBundleFile(target)
|
|
1883
2124
|
// a bundle file gets its url once the bridge that serves it is settled
|
|
1884
2125
|
let resolvedBundle =
|
|
1885
2126
|
!prebuiltBundle && shouldResolveBundleTargetInCli(target)
|
|
@@ -1895,7 +2136,7 @@ export async function runOpenCommand(
|
|
|
1895
2136
|
resolvedBundle?.bundleUrl,
|
|
1896
2137
|
projectRoot ?? (prebuiltBundle ? appDirAbove(prebuiltBundle) : undefined),
|
|
1897
2138
|
)
|
|
1898
|
-
|
|
2139
|
+
startExpoConfigRead(project)
|
|
1899
2140
|
// playwright is the browser-sim launcher. it owns an isolated profile and
|
|
1900
2141
|
// can reap exactly its own browser tree. desktop opens the native host and
|
|
1901
2142
|
// electron the Electron companion. none opens the user's browser profile.
|
|
@@ -1906,25 +2147,49 @@ export async function runOpenCommand(
|
|
|
1906
2147
|
)
|
|
1907
2148
|
peachExit(1)
|
|
1908
2149
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
isAgentEnv() ||
|
|
1918
|
-
!process.stdin.isTTY
|
|
2150
|
+
// a person watching the sim, directly or through their coding agent, gets the
|
|
2151
|
+
// native app where it ships. CI and Team Machine fleet sessions stay in the
|
|
2152
|
+
// headless browser, as do the browser-only --headless and --chromium.
|
|
2153
|
+
const defaultDriverId: DriverId =
|
|
2154
|
+
isCi() ||
|
|
2155
|
+
isTeamMachineSession() ||
|
|
2156
|
+
args.includes('--headless') ||
|
|
2157
|
+
args.includes('--chromium')
|
|
1919
2158
|
? 'playwright'
|
|
1920
|
-
: readSimulatorDriverPreference()
|
|
1921
|
-
? 'desktop'
|
|
1922
|
-
|
|
2159
|
+
: (readSimulatorDriverPreference() ??
|
|
2160
|
+
(nativeDesktopIsDefault() ? 'desktop' : 'playwright'))
|
|
2161
|
+
const selectedDriverId = isDriverId(requestedDriverId)
|
|
2162
|
+
? requestedDriverId
|
|
2163
|
+
: defaultDriverId
|
|
2164
|
+
const driverId: DriverId =
|
|
2165
|
+
selectedDriverId === 'electron' && process.platform !== 'win32'
|
|
2166
|
+
? 'desktop'
|
|
2167
|
+
: selectedDriverId
|
|
1923
2168
|
if (requestedDriverId === 'electron' && driverId === 'desktop')
|
|
1924
2169
|
console.error(
|
|
1925
2170
|
' Electron is Windows-only; using the native desktop app (peach desktop install).',
|
|
1926
2171
|
)
|
|
2172
|
+
// the first open on a machine without the native app installs it.
|
|
2173
|
+
if (driverId === 'desktop' && !findDesktopCompanion())
|
|
2174
|
+
await runInstallDesktop(['--yes'])
|
|
1927
2175
|
const driverHeadless = resolveDriverHeadless(args, driverId)
|
|
2176
|
+
const chromium = args.includes('--chromium')
|
|
2177
|
+
if (chromium && driverId !== 'playwright') {
|
|
2178
|
+
console.error(' peach open: --chromium requires the playwright driver')
|
|
2179
|
+
peachExit(1)
|
|
2180
|
+
}
|
|
2181
|
+
const renderer = chromium
|
|
2182
|
+
? 'chromium'
|
|
2183
|
+
: driverHeadless && desktopHostPlatform() === 'linux-x64'
|
|
2184
|
+
? 'native'
|
|
2185
|
+
: undefined
|
|
2186
|
+
const pinnedHostExecutable =
|
|
2187
|
+
!chromium &&
|
|
2188
|
+
driverHeadless &&
|
|
2189
|
+
desktopHostPlatform() === 'linux-x64' &&
|
|
2190
|
+
desktopHostDelivery().resolvePin()
|
|
2191
|
+
? (headlessHostCommand()?.[0] ?? null)
|
|
2192
|
+
: undefined
|
|
1928
2193
|
if (cdpPort && driverId !== 'playwright') {
|
|
1929
2194
|
console.error(' peach open: --cdp-port is only honored by the playwright driver')
|
|
1930
2195
|
peachExit(1)
|
|
@@ -2006,9 +2271,21 @@ export async function runOpenCommand(
|
|
|
2006
2271
|
parsed.wsPort,
|
|
2007
2272
|
parsed.explicitPort,
|
|
2008
2273
|
opts.resources,
|
|
2009
|
-
!configuredRuntimeVersion &&
|
|
2274
|
+
!configuredRuntimeVersion &&
|
|
2275
|
+
(explicitBaseUrl ||
|
|
2276
|
+
looksLikeSootsimUrl(target) ||
|
|
2277
|
+
(parsed.explicitPort &&
|
|
2278
|
+
worldForBridgePort(parsed.wsPort)?.source === 'dev-bridge'))
|
|
2010
2279
|
? 'external'
|
|
2011
2280
|
: 'bridge',
|
|
2281
|
+
{
|
|
2282
|
+
newShell: openInNewSim,
|
|
2283
|
+
simId: parsed.simId,
|
|
2284
|
+
simIdSource: parsed.simIdSource,
|
|
2285
|
+
driverId,
|
|
2286
|
+
renderer,
|
|
2287
|
+
pinnedHostExecutable,
|
|
2288
|
+
},
|
|
2012
2289
|
)
|
|
2013
2290
|
if (opts.resources) opts.resources.port = parsed.wsPort
|
|
2014
2291
|
opts.signal?.throwIfAborted()
|
|
@@ -2035,6 +2312,18 @@ export async function runOpenCommand(
|
|
|
2035
2312
|
console.error(` peach open: --posture must be open or closed, got "${postureArg}"`)
|
|
2036
2313
|
peachExit(1)
|
|
2037
2314
|
}
|
|
2315
|
+
const renderResolutionArg = args.find((_, i) => args[i - 1] === '--render-resolution')
|
|
2316
|
+
if (
|
|
2317
|
+
args.includes('--render-resolution') &&
|
|
2318
|
+
renderResolutionArg !== 'high' &&
|
|
2319
|
+
renderResolutionArg !== 'medium' &&
|
|
2320
|
+
renderResolutionArg !== 'low'
|
|
2321
|
+
) {
|
|
2322
|
+
console.error(
|
|
2323
|
+
` peach open: --render-resolution must be high, medium, or low, got "${renderResolutionArg ?? ''}"`,
|
|
2324
|
+
)
|
|
2325
|
+
peachExit(1)
|
|
2326
|
+
}
|
|
2038
2327
|
const orientationFlag = args.find((_, i) => args[i - 1] === '--orientation')
|
|
2039
2328
|
const orientationArg =
|
|
2040
2329
|
orientationFlag === 'portrait' || orientationFlag === 'landscape'
|
|
@@ -2131,6 +2420,7 @@ export async function runOpenCommand(
|
|
|
2131
2420
|
hostDriver: driverId === 'desktop' ? 'desktop' : undefined,
|
|
2132
2421
|
device: deviceArg,
|
|
2133
2422
|
posture: postureArg,
|
|
2423
|
+
renderResolution: renderResolutionArg,
|
|
2134
2424
|
orientation: orientationArg,
|
|
2135
2425
|
osVersion: osVersionArg,
|
|
2136
2426
|
theme: themeArg,
|
|
@@ -2185,14 +2475,14 @@ export async function runOpenCommand(
|
|
|
2185
2475
|
(sim) =>
|
|
2186
2476
|
sim.id === savedSimId &&
|
|
2187
2477
|
sim.readyState === 'open' &&
|
|
2188
|
-
simUsesDriver(sim, driverId),
|
|
2478
|
+
simUsesDriver(sim, driverId, renderer, pinnedHostExecutable),
|
|
2189
2479
|
) ?? null
|
|
2190
2480
|
if (!targetSim) {
|
|
2191
2481
|
clearCurrentSimId()
|
|
2192
2482
|
}
|
|
2193
2483
|
} else if (parsed.simIdSource === 'flag') {
|
|
2194
2484
|
targetSim = resolveTargetSim(sims, parsed.simId)
|
|
2195
|
-
if (!simUsesDriver(targetSim, driverId)) {
|
|
2485
|
+
if (!simUsesDriver(targetSim, driverId, renderer, pinnedHostExecutable)) {
|
|
2196
2486
|
throw new Error(
|
|
2197
2487
|
`sim ${targetSim.id} is not managed by the ${driverId} driver`,
|
|
2198
2488
|
)
|
|
@@ -2211,7 +2501,7 @@ export async function runOpenCommand(
|
|
|
2211
2501
|
if (
|
|
2212
2502
|
sim.readyState !== 'open' ||
|
|
2213
2503
|
!sim.url ||
|
|
2214
|
-
!simUsesDriver(sim, driverId)
|
|
2504
|
+
!simUsesDriver(sim, driverId, renderer, pinnedHostExecutable)
|
|
2215
2505
|
) {
|
|
2216
2506
|
return false
|
|
2217
2507
|
}
|
|
@@ -2279,7 +2569,7 @@ export async function runOpenCommand(
|
|
|
2279
2569
|
parsed.commandTimeoutMs,
|
|
2280
2570
|
(sim) =>
|
|
2281
2571
|
sim.readyState === 'open' &&
|
|
2282
|
-
simUsesDriver(sim, driverId) &&
|
|
2572
|
+
simUsesDriver(sim, driverId, renderer, pinnedHostExecutable) &&
|
|
2283
2573
|
simMatchesToken(sim),
|
|
2284
2574
|
{ attempts: Math.round(30 * flowTimeoutScale()) },
|
|
2285
2575
|
)
|
|
@@ -2330,11 +2620,11 @@ export async function runOpenCommand(
|
|
|
2330
2620
|
}
|
|
2331
2621
|
|
|
2332
2622
|
const configuredBaseUrl = applyPeachConfigToUrl(baseUrl, runtimeConfig)
|
|
2333
|
-
const
|
|
2623
|
+
const builtShellUrl =
|
|
2334
2624
|
newSimShellUrl ??
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
)
|
|
2625
|
+
(await buildShellUrl(shellTarget, configuredBaseUrl, buildShellUrlOpts))
|
|
2626
|
+
await awaitStampedExpoSwiftReady(builtShellUrl)
|
|
2627
|
+
const openUrl = withOpenToken(builtShellUrl, token)
|
|
2338
2628
|
const ownerPid = opts.ownerPid ?? getStableOwnerPid()
|
|
2339
2629
|
if (!ownerPid) {
|
|
2340
2630
|
console.error(
|
|
@@ -2359,6 +2649,7 @@ export async function runOpenCommand(
|
|
|
2359
2649
|
// live world of its own.
|
|
2360
2650
|
bridgePort: parsed.wsPort,
|
|
2361
2651
|
headless: driverHeadless,
|
|
2652
|
+
chromium,
|
|
2362
2653
|
profileId,
|
|
2363
2654
|
ephemeralProfile,
|
|
2364
2655
|
cdpPort,
|
|
@@ -2386,6 +2677,7 @@ export async function runOpenCommand(
|
|
|
2386
2677
|
attempts: connectWait.attempts,
|
|
2387
2678
|
intervalMs: connectWait.intervalMs,
|
|
2388
2679
|
signal: opts.signal,
|
|
2680
|
+
admittedFile: result.admittedFile,
|
|
2389
2681
|
},
|
|
2390
2682
|
)
|
|
2391
2683
|
if (outcome.kind === 'host-exit') {
|
|
@@ -2436,6 +2728,13 @@ export async function runOpenCommand(
|
|
|
2436
2728
|
simId: match.id,
|
|
2437
2729
|
simIdSource: 'flag',
|
|
2438
2730
|
})
|
|
2731
|
+
const attached = await bridge
|
|
2732
|
+
.listSims()
|
|
2733
|
+
.then((sims) => sims.find((sim) => sim.id === match.id)?.readyState ?? 'gone')
|
|
2734
|
+
.catch(() => 'unknown')
|
|
2735
|
+
console.error(
|
|
2736
|
+
` sim ${match.id} bridge state: ${attached}; host load ${os.loadavg()[0].toFixed(1)} on ${os.cpus().length} cpus (a loaded host slows a cold first bundle past the budget; a sim that is open but never answers has a blocked page)`,
|
|
2737
|
+
)
|
|
2439
2738
|
await printBridgeFailureDiagnostics(bridge, {
|
|
2440
2739
|
errorsCommand: `peach get errors 5${hint}`,
|
|
2441
2740
|
warningsCommand: `peach get warnings 5${hint}`,
|