peachsim 0.1.1469 → 0.1.1481
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/cli/commands/control.ts +68 -42
- package/cli/commands/detox.ts +15 -10
- package/cli/commands/flow.ts +1 -2
- package/cli/commands/inspect/settling.ts +5 -1
- package/cli/commands/setup.ts +20 -13
- package/cli/commands/test.ts +2 -1
- package/cli/dev-build-freshness.ts +14 -14
- package/cli/drivers/playwright-sim-host.ts +7 -2
- package/cli/drivers/playwright.ts +6 -6
- package/cli/outbound-endpoints.ts +80 -6
- package/detox/index.ts +27 -14
- package/detox/matchers.ts +17 -6
- 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 +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +54 -15
- 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 +19 -3
- package/dist-lib/config.mjs +19 -3
- package/dist-lib/detox/index.cjs +125 -52
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +30 -9
- package/dist-lib/expo-swift-metadata.cjs +18 -6
- package/dist-lib/expo-swift-metadata.mjs +18 -6
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +621 -390
- 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 +46 -19
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +24 -6
- package/dist-lib/metro-production-bundle.mjs +22 -6
- package/dist-lib/metro.cjs +41 -18
- 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 +19 -5
- package/dist-lib/native-app-identity.mjs +19 -5
- package/dist-lib/native-firebase-metadata.cjs +22 -7
- package/dist-lib/native-firebase-metadata.mjs +20 -7
- 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 +3 -1
- package/dist-lib/react-native-host-modules.mjs +3 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +404 -279
- package/dist-lib/sdk.cjs +6 -2
- package/dist-lib/sdk.mjs +6 -2
- package/dist-lib/skills.cjs +757 -531
- package/dist-lib/swift-compile.cjs +7 -2
- package/dist-lib/swift-compile.mjs +7 -2
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +7 -2
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +27 -7
- package/dist-lib/worklet-executable.mjs +27 -7
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +493 -188
- package/src/admitted-mac-build.ts +12 -0
- package/src/bridge-constants.ts +5 -3
- package/src/bundleGlobalFooter.ts +26 -3
- package/src/config.ts +3 -2
- package/src/configured-bundle-producer.ts +2 -1
- package/src/expo-swift-host.ts +33 -7
- package/src/host/bridge-host.ts +34 -6
- package/src/metro-production-bundle.ts +11 -4
- package/src/native-app-identity.ts +3 -2
- package/src/native-firebase-metadata.ts +5 -3
- package/src/react-native-host-modules.ts +2 -0
- package/src/swift-compile.ts +5 -4
- package/src/worker-system-chunks.ts +61 -58
- package/src/worklet-executable.ts +38 -6
package/cli/commands/control.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type DeviceModel,
|
|
17
17
|
} from 'peach-engine/settings/devices'
|
|
18
18
|
import { WebSocket } from 'ws'
|
|
19
|
-
import { probePort } from '../../scripts/dev-server-scanner'
|
|
19
|
+
import { pinSharedListenerScan, probePort } from '../../scripts/dev-server-scanner'
|
|
20
20
|
import { isAppDeepLinkTarget, openAppUrl } from '../../src/app-url'
|
|
21
21
|
import { isLoopbackHost, loopbackOriginOf } from '../../src/backend-origin'
|
|
22
22
|
import { resolveSootsimShellBaseUrlForBridgePort } from '../../src/bridge-constants'
|
|
@@ -260,16 +260,12 @@ async function connectBridgeForOpen(
|
|
|
260
260
|
// a closed port refuses at once, so this window only matters when something
|
|
261
261
|
// listens: a live bridge on a loaded machine can take longer than 250 ms to
|
|
262
262
|
// finish the handshake, and missing it spawned a second bridge.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
(await canConnectToBridge(preferredPort, 2_000))
|
|
266
|
-
) {
|
|
263
|
+
const handshake = await bridgeHandshake(preferredPort, 2_000)
|
|
264
|
+
if ((explicitPort || preferredWorld !== null) && handshake === 'open') {
|
|
267
265
|
return preferredPort
|
|
268
266
|
}
|
|
269
267
|
if (explicitPort && preferredWorld !== null) {
|
|
270
|
-
throw new Error(
|
|
271
|
-
`peach open: requested bridge port ${preferredPort} is not reachable.`,
|
|
272
|
-
)
|
|
268
|
+
throw new Error(unreachableBridgeMessage(preferredPort, handshake))
|
|
273
269
|
}
|
|
274
270
|
if (
|
|
275
271
|
!explicitPort &&
|
|
@@ -404,23 +400,45 @@ async function ensureRuntimeForOpen() {
|
|
|
404
400
|
await runRuntime(['install'], {})
|
|
405
401
|
}
|
|
406
402
|
|
|
407
|
-
|
|
403
|
+
type BridgeHandshake = 'open' | 'refused' | 'timeout'
|
|
404
|
+
|
|
405
|
+
function bridgeHandshake(port: number, timeoutMs: number): Promise<BridgeHandshake> {
|
|
408
406
|
return new Promise((resolve) => {
|
|
409
407
|
const ws = new WebSocket(`ws://127.0.0.1:${port}`, { handshakeTimeout: timeoutMs })
|
|
410
408
|
let settled = false
|
|
411
|
-
const finish = (
|
|
409
|
+
const finish = (result: BridgeHandshake) => {
|
|
412
410
|
if (settled) return
|
|
413
411
|
settled = true
|
|
412
|
+
clearTimeout(timer)
|
|
414
413
|
if (ws.readyState === WebSocket.OPEN) ws.close()
|
|
415
414
|
else if (ws.readyState === WebSocket.CONNECTING) ws.terminate()
|
|
416
|
-
resolve(
|
|
415
|
+
resolve(result)
|
|
417
416
|
}
|
|
418
|
-
|
|
419
|
-
ws.
|
|
420
|
-
|
|
417
|
+
const timer = setTimeout(() => finish('timeout'), timeoutMs)
|
|
418
|
+
ws.once('open', () => finish('open'))
|
|
419
|
+
ws.on('error', (err: NodeJS.ErrnoException) => {
|
|
420
|
+
finish(
|
|
421
|
+
err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' ? 'refused' : 'timeout',
|
|
422
|
+
)
|
|
423
|
+
})
|
|
421
424
|
})
|
|
422
425
|
}
|
|
423
426
|
|
|
427
|
+
function unreachableBridgeMessage(port: number, handshake: BridgeHandshake): string {
|
|
428
|
+
if (handshake === 'timeout') {
|
|
429
|
+
const cores = Math.max(1, os.cpus().length)
|
|
430
|
+
const load = os.loadavg()[0]
|
|
431
|
+
if (load >= cores) {
|
|
432
|
+
return `peach open: requested bridge port ${port} did not finish its handshake within 2s while load average was ${load.toFixed(1)} on ${cores} cores.`
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return `peach open: requested bridge port ${port} is not reachable.`
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function canConnectToBridge(port: number, timeoutMs: number): Promise<boolean> {
|
|
439
|
+
return bridgeHandshake(port, timeoutMs).then((result) => result === 'open')
|
|
440
|
+
}
|
|
441
|
+
|
|
424
442
|
function looksLikeSootsimUrl(target: string): boolean {
|
|
425
443
|
try {
|
|
426
444
|
const url = new URL(target)
|
|
@@ -531,39 +549,46 @@ async function resolveConnectionInputForCli(target: string): Promise<ResolvedDev
|
|
|
531
549
|
|
|
532
550
|
console.log(` resolving native bundle for ${normalized} (manifest timeout: 30s)…`)
|
|
533
551
|
let lastError: Error | null = null
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
552
|
+
const releaseListenerScan = pinSharedListenerScan()
|
|
553
|
+
try {
|
|
554
|
+
for (let attempt = 0; attempt < DEV_BUNDLE_RESOLVE_ATTEMPTS; attempt++) {
|
|
555
|
+
if (registeredPort && registeredPort > 0) {
|
|
556
|
+
const discovered = await probePort(registeredPort)
|
|
557
|
+
// a provisional bundle URL is enough to LIST the server but not to open
|
|
558
|
+
// it; fall through to resolveConnectionInput, which asks the manifest
|
|
559
|
+
// again with a budget that fits a real dev server.
|
|
560
|
+
if (discovered && !discovered.bundleUrlProvisional) {
|
|
561
|
+
return {
|
|
562
|
+
bundleUrl: discovered.bundleUrl,
|
|
563
|
+
port: discovered.port,
|
|
564
|
+
framework: discovered.framework,
|
|
565
|
+
projectName: discovered.projectName,
|
|
566
|
+
}
|
|
546
567
|
}
|
|
547
568
|
}
|
|
548
|
-
}
|
|
549
569
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
570
|
+
try {
|
|
571
|
+
return await resolveConnectionInput(normalized)
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (
|
|
574
|
+
!(error instanceof Error) ||
|
|
575
|
+
!error.message.startsWith('could not resolve a native bundle')
|
|
576
|
+
) {
|
|
577
|
+
throw error
|
|
578
|
+
}
|
|
579
|
+
lastError = error
|
|
558
580
|
}
|
|
559
|
-
|
|
581
|
+
await sleep(DEV_BUNDLE_RESOLVE_INTERVAL_MS)
|
|
560
582
|
}
|
|
561
|
-
|
|
583
|
+
throw (
|
|
584
|
+
lastError ??
|
|
585
|
+
new Error(
|
|
586
|
+
'could not resolve a native bundle for ' + normalized + ' after retrying.',
|
|
587
|
+
)
|
|
588
|
+
)
|
|
589
|
+
} finally {
|
|
590
|
+
releaseListenerScan()
|
|
562
591
|
}
|
|
563
|
-
throw (
|
|
564
|
-
lastError ??
|
|
565
|
-
new Error('could not resolve a native bundle for ' + normalized + ' after retrying.')
|
|
566
|
-
)
|
|
567
592
|
}
|
|
568
593
|
|
|
569
594
|
// a dev server builds an app's graph on the first request for its bundle and
|
|
@@ -2132,7 +2157,8 @@ export async function runOpenCommand(
|
|
|
2132
2157
|
const currentEphemeral = new URL(
|
|
2133
2158
|
deriveCurrentSimBaseUrl(targetSim, baseUrl),
|
|
2134
2159
|
).searchParams.get('ephemeral')
|
|
2135
|
-
if (currentEphemeral)
|
|
2160
|
+
if (currentEphemeral)
|
|
2161
|
+
navigationUrl.searchParams.set('ephemeral', currentEphemeral)
|
|
2136
2162
|
navigatedShellUrl = navigationUrl.toString()
|
|
2137
2163
|
navigationUrl.searchParams.set('inspectOpen', token)
|
|
2138
2164
|
bridge
|
package/cli/commands/detox.ts
CHANGED
|
@@ -19,8 +19,8 @@ import { rnxPublicBrand } from '../../src/public-brand'
|
|
|
19
19
|
import { resolveCliAuth } from '../auth'
|
|
20
20
|
import { registerRun, shouldRegisterRun } from '../run-registry'
|
|
21
21
|
import { peachExit } from '../run-rnx'
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { resolveDefaultBridgePort } from '../ws-bridge'
|
|
23
|
+
import { buildShellUrl, resolveShellBaseUrlForBridgePort } from './control'
|
|
24
24
|
import { resolveDefaultUploadOrigin } from './upload'
|
|
25
25
|
|
|
26
26
|
interface RunDetoxOpts {
|
|
@@ -35,7 +35,7 @@ usage:
|
|
|
35
35
|
peach detox [testFiles...] run all tests under e2e/, test/e2e/, or detox/
|
|
36
36
|
peach detox --app <port|url> the app each device.launchApp() opens
|
|
37
37
|
peach detox --config <path> use a specific jest config
|
|
38
|
-
peach detox --port, -p <port>
|
|
38
|
+
peach detox --port, -p <port> bridge port (defaults to the active local bridge)
|
|
39
39
|
peach detox init scaffold peach-detox.config.cjs + sample test
|
|
40
40
|
peach detox --watch jest watch mode
|
|
41
41
|
peach detox --headed keep the peach shell window visible
|
|
@@ -77,7 +77,8 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
|
|
|
77
77
|
Number(getFlag('--port') || getFlag('-p')) ||
|
|
78
78
|
opts.port ||
|
|
79
79
|
Number(process.env.SOOTSIM_PORT) ||
|
|
80
|
-
|
|
80
|
+
resolveDefaultBridgePort()
|
|
81
|
+
const shellBaseUrl = process.env.PEACH_URL || resolveShellBaseUrlForBridgePort(port)
|
|
81
82
|
if (getFlag('--sim')) {
|
|
82
83
|
throw new Error(
|
|
83
84
|
'Detox owns its browser context; pin its shell with PEACH_URL and --device instead of --sim',
|
|
@@ -169,7 +170,7 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
|
|
|
169
170
|
// the driver navigates to a running peach shell; warn before jest starts
|
|
170
171
|
// when none is reachable.
|
|
171
172
|
if (!noLaunch) {
|
|
172
|
-
await ensureShellRunning(
|
|
173
|
+
await ensureShellRunning(shellBaseUrl)
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
// build the jest command. prefer the user's config if they passed --config
|
|
@@ -251,7 +252,6 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
|
|
|
251
252
|
console.log(` port: ${port}${headed ? ' (headed)' : ''}`)
|
|
252
253
|
if (localConfig) console.log(` config: ${localConfig}`)
|
|
253
254
|
|
|
254
|
-
const shellBaseUrl = process.env.PEACH_URL || `http://localhost:${port}`
|
|
255
255
|
const app = getFlag('--app')
|
|
256
256
|
const shellUrl = new URL(app ? await buildShellUrl(app, shellBaseUrl) : shellBaseUrl)
|
|
257
257
|
const device = getFlag('--device')
|
|
@@ -447,12 +447,17 @@ function firstExisting(names: string[]): string | null {
|
|
|
447
447
|
return null
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
async function ensureShellRunning(
|
|
451
|
-
|
|
452
|
-
|
|
450
|
+
async function ensureShellRunning(shellUrl: string): Promise<void> {
|
|
451
|
+
try {
|
|
452
|
+
const response = await fetch(shellUrl, { signal: AbortSignal.timeout(3000) })
|
|
453
|
+
await response.body?.cancel()
|
|
454
|
+
if (response.ok) return
|
|
455
|
+
} catch {
|
|
456
|
+
// an unreachable selected shell warns before jest owns its launch failure.
|
|
457
|
+
}
|
|
453
458
|
|
|
454
459
|
console.warn(
|
|
455
|
-
` warn: no peach shell reachable
|
|
460
|
+
` warn: no peach shell reachable at ${shellUrl}. start your app, then run \`peach open <app-port>\`\n` +
|
|
456
461
|
` or pass --no-launch if you're managing the shell yourself.`,
|
|
457
462
|
)
|
|
458
463
|
}
|
package/cli/commands/flow.ts
CHANGED
|
@@ -197,8 +197,7 @@ export async function resolveAppFrontmatterTarget(
|
|
|
197
197
|
return { target: trimmed }
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// discover a peach shell URL
|
|
201
|
-
// exported so `detox.ts` and `maestro.ts` can reuse the same discovery logic.
|
|
200
|
+
// discover a peach shell URL from running dev servers, preferring patched ones.
|
|
202
201
|
export async function discoverSootsimUrl(): Promise<string | null> {
|
|
203
202
|
const servers = await scanDevServers()
|
|
204
203
|
const patched = servers.find((s) => s.patched)
|
|
@@ -265,7 +265,11 @@ export async function waitForSootsimIdle({
|
|
|
265
265
|
for (const n of hierarchy) {
|
|
266
266
|
const layout = n && n.layout
|
|
267
267
|
if (layout && layout.width > 0) {
|
|
268
|
-
|
|
268
|
+
// painted bounds catch a scroll fling. background playback
|
|
269
|
+
// keeps a view's painted bounds moving forever, so it and its
|
|
270
|
+
// subtree count by their own layout; the rest of the screen
|
|
271
|
+
// still catches the fling.
|
|
272
|
+
const abs = n.backgroundPlayback ? layout : n.absolute || n.absolutePosition || layout
|
|
269
273
|
nodes.push(Math.round(abs.x) + ',' + Math.round(abs.y) + ',' + Math.round(layout.width))
|
|
270
274
|
}
|
|
271
275
|
notePreviewProgress(
|
package/cli/commands/setup.ts
CHANGED
|
@@ -177,11 +177,9 @@ export async function runSetup(
|
|
|
177
177
|
const productAnalytics = decidePrivacy(options, privacy)
|
|
178
178
|
const crashReports = productAnalytics
|
|
179
179
|
|
|
180
|
-
const repositoryPlan = packageRoot
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const repository = await decideRepository(options, repositoryPlan)
|
|
184
|
-
if (repository && repositoryPlan) {
|
|
180
|
+
const repositoryPlan = await decideRepository(options, packageRoot, cliVersion)
|
|
181
|
+
const repository = repositoryPlan !== null
|
|
182
|
+
if (repositoryPlan) {
|
|
185
183
|
if (!options.json) {
|
|
186
184
|
for (const operation of formatRepositoryOperations(repositoryPlan)) {
|
|
187
185
|
console.log(` ${muted(options.dryRun ? 'Would run' : 'Running')} ${operation}`)
|
|
@@ -376,22 +374,31 @@ function decidePrivacy(
|
|
|
376
374
|
|
|
377
375
|
async function decideRepository(
|
|
378
376
|
options: SetupOptions,
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
377
|
+
appDir: string | null,
|
|
378
|
+
cliVersion: string,
|
|
379
|
+
): Promise<RepositorySetupPlan | null> {
|
|
380
|
+
if (!appDir) {
|
|
382
381
|
if (options.repository === true) {
|
|
383
382
|
throw new Error('--repo requires a package.json inside a Git repository')
|
|
384
383
|
}
|
|
385
|
-
return
|
|
384
|
+
return null
|
|
385
|
+
}
|
|
386
|
+
if (options.repository === false) return null
|
|
387
|
+
if (
|
|
388
|
+
options.repository === undefined &&
|
|
389
|
+
(options.yes || options.ci || !process.stdin.isTTY)
|
|
390
|
+
) {
|
|
391
|
+
return null
|
|
386
392
|
}
|
|
387
|
-
|
|
388
|
-
if (options.repository
|
|
389
|
-
if (options.yes || options.ci || !process.stdin.isTTY) return false
|
|
393
|
+
const plan = planRepositorySetup({ appDir, cliVersion })
|
|
394
|
+
if (plan.operations.length === 0 || options.repository === true) return plan
|
|
390
395
|
printPromptBlock('Share peach with your team', [
|
|
391
396
|
'peach already works here with no project setup.',
|
|
392
397
|
'This only adds an exact peachsim dev dependency and a peach script for teammates.',
|
|
393
398
|
])
|
|
394
|
-
return confirm(`Add peach to ${path.basename(plan.appDir)}?`, false)
|
|
399
|
+
return (await confirm(`Add peach to ${path.basename(plan.appDir)}?`, false))
|
|
400
|
+
? plan
|
|
401
|
+
: null
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
function requireInteractive(options: SetupOptions, flags: string): void {
|
package/cli/commands/test.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
publishLiveDevStack,
|
|
16
16
|
} from '../cloud-test-tunnel'
|
|
17
17
|
import { parseFlowFile } from '../flow-file'
|
|
18
|
+
import { resolveDefaultBridgePort } from '../ws-bridge'
|
|
18
19
|
import { buildShellUrl, resolveShellBaseUrlForBridgePort } from './control'
|
|
19
20
|
import { FLOW_ARG_VALUE_FLAGS, hoistLeadingSimFlag } from './flow'
|
|
20
21
|
import { resolveDefaultUploadOrigin } from './upload'
|
|
@@ -1271,7 +1272,7 @@ export async function runTest(
|
|
|
1271
1272
|
process.env.PEACH_URL = await buildShellUrl(
|
|
1272
1273
|
appTarget.target,
|
|
1273
1274
|
resolveShellBaseUrlForBridgePort(
|
|
1274
|
-
opts.port ?? (Number(process.env.SOOTSIM_PORT) ||
|
|
1275
|
+
opts.port ?? (Number(process.env.SOOTSIM_PORT) || resolveDefaultBridgePort()),
|
|
1275
1276
|
),
|
|
1276
1277
|
)
|
|
1277
1278
|
}
|
|
@@ -8,14 +8,11 @@
|
|
|
8
8
|
// artifact mtimes per build route and reports which routes are stale, so
|
|
9
9
|
// serve can refuse before binding instead of serving yesterday's engine.
|
|
10
10
|
//
|
|
11
|
-
// the
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// fresh. deliberately omitted: the canvaskit binary inputs (resolved from a
|
|
17
|
-
// manifest at watch time, never hand-edited) and the deps-web inputs (a
|
|
18
|
-
// different served tree, not the engine serve() serves).
|
|
11
|
+
// the watcher routes individual native inputs through its canonical providers.
|
|
12
|
+
// this guard covers vendor glue/manifests and public assets as directories so
|
|
13
|
+
// a native artifact update also refuses stale served workers. importing the
|
|
14
|
+
// watcher would bundle chokidar and private engine/bundler modules into the cli.
|
|
15
|
+
// deps-web belongs to a different served tree.
|
|
19
16
|
|
|
20
17
|
import { readdirSync, statSync, type Dirent } from 'node:fs'
|
|
21
18
|
import { join, relative, sep } from 'node:path'
|
|
@@ -53,8 +50,11 @@ const PEACHSIM_ENGINE_INPUTS = [
|
|
|
53
50
|
const ENGINE_SOURCES = [
|
|
54
51
|
'packages/peach-engine/src',
|
|
55
52
|
'packages/peach-engine/public',
|
|
53
|
+
'packages/peach-engine/vendor',
|
|
56
54
|
'packages/peach-engine/scripts/license-banner.ts',
|
|
57
55
|
'packages/peach-engine/scripts/canvaskit-graphite-variant.ts',
|
|
56
|
+
'packages/peach-engine/scripts/canvaskit-ganesh-variant.ts',
|
|
57
|
+
'packages/peach-engine/scripts/canvaskit-kotlin-variant.ts',
|
|
58
58
|
'packages/peach-engine/vite.build.config.ts',
|
|
59
59
|
'packages/peach-engine/package.json',
|
|
60
60
|
'packages/peach-compat/src',
|
|
@@ -176,7 +176,7 @@ export function findStaleDevBuildRoutes(monorepoRoot: string): StaleDevBuildRout
|
|
|
176
176
|
newestSource = found
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
let
|
|
179
|
+
let oldestArtifact: NewestFile | null = null
|
|
180
180
|
let artifactMissing = false
|
|
181
181
|
for (const artifact of route.artifacts) {
|
|
182
182
|
const found = newestForEntry(monorepoRoot, artifact)
|
|
@@ -184,20 +184,20 @@ export function findStaleDevBuildRoutes(monorepoRoot: string): StaleDevBuildRout
|
|
|
184
184
|
artifactMissing = true
|
|
185
185
|
continue
|
|
186
186
|
}
|
|
187
|
-
if (!
|
|
188
|
-
|
|
187
|
+
if (!oldestArtifact || found.mtimeMs < oldestArtifact.mtimeMs) {
|
|
188
|
+
oldestArtifact = found
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
if (!newestSource) continue
|
|
192
192
|
if (
|
|
193
193
|
artifactMissing ||
|
|
194
|
-
!
|
|
195
|
-
newestSource.mtimeMs >
|
|
194
|
+
!oldestArtifact ||
|
|
195
|
+
newestSource.mtimeMs > oldestArtifact.mtimeMs
|
|
196
196
|
) {
|
|
197
197
|
stale.push({
|
|
198
198
|
route: route.id,
|
|
199
199
|
newestSourcePath: toRepoPath(monorepoRoot, newestSource.absolutePath),
|
|
200
|
-
artifactMtimeMs:
|
|
200
|
+
artifactMtimeMs: oldestArtifact ? oldestArtifact.mtimeMs : null,
|
|
201
201
|
})
|
|
202
202
|
}
|
|
203
203
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
acquireChromiumAdmission,
|
|
3
|
+
readChromiumQueue,
|
|
4
|
+
} from '../../../../scripts/lib/chromium-admission'
|
|
2
5
|
// the detached browser host, as CJS source.
|
|
3
6
|
//
|
|
4
7
|
// it resolves playwright from the absolute path the driver hands it, launches
|
|
5
8
|
// Chrome for Testing, opens the target url, then keeps itself (and therefore
|
|
6
9
|
// the browser) alive until the page asks it to close (`peach close`, or the
|
|
7
|
-
// bridge
|
|
10
|
+
// bridge reclaiming an idle sim for a queued chromium launch),
|
|
8
11
|
// the browser disconnects, or its owning session exits. all inputs arrive
|
|
9
12
|
// through the environment.
|
|
10
13
|
//
|
|
@@ -119,6 +122,7 @@ const webViewAutomationHelpers = ${webViewAutomationHelpersSource()};
|
|
|
119
122
|
const readBrowserCompositeCaptureRequest = ${readBrowserCompositeCaptureRequest.toString()};
|
|
120
123
|
const resolveBrowserCompositeCdpClip = ${resolveBrowserCompositeCdpClip.toString()};
|
|
121
124
|
const acquireChromiumAdmission = ${acquireChromiumAdmission.toString()};
|
|
125
|
+
const readChromiumQueue = ${readChromiumQueue.toString()};
|
|
122
126
|
// chrome tree reaper — playwright's browser.close() and even SIGKILLing the
|
|
123
127
|
// root chrome process do NOT reliably take down the helper tree (gpu, renderer,
|
|
124
128
|
// network/storage utilities). they reparent to launchd/init and survive, each
|
|
@@ -513,6 +517,7 @@ const cleanupProfile = () => {
|
|
|
513
517
|
path: require('path'),
|
|
514
518
|
childProcess: require('child_process'),
|
|
515
519
|
label: 'peach sim host ' + profileDir,
|
|
520
|
+
readQueue: readChromiumQueue,
|
|
516
521
|
});
|
|
517
522
|
let launchErr = null;
|
|
518
523
|
let launchedChannel = null;
|
|
@@ -226,14 +226,14 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
226
226
|
})
|
|
227
227
|
child.unref()
|
|
228
228
|
|
|
229
|
-
// name both lifetime bounds: the
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
229
|
+
// name both lifetime bounds: the host watchdog closes chrome ~5s after the
|
|
230
|
+
// owning session pid exits, which surprises one-shot/detached shells (their
|
|
231
|
+
// topmost non-init ancestor is the shell itself, so the sim dies the moment
|
|
232
|
+
// the command finishes), and the bridge reclaims a sim idle past its window
|
|
233
|
+
// only for a chromium launch waiting on the machine's cap.
|
|
234
234
|
const ownerComm = getProcessComm(ownerPid)
|
|
235
235
|
const idleMinutes = AUTOMATION_SIM_IDLE_REAP_TTL_MS / 60_000
|
|
236
|
-
const ownerNote = ` — browser closes
|
|
236
|
+
const ownerNote = ` — browser closes when pid ${ownerPid}${ownerComm ? ` (${ownerComm})` : ''} exits, or after ${idleMinutes} min with no peach command or input if another browser launch is waiting for its slot`
|
|
237
237
|
return {
|
|
238
238
|
launched: true,
|
|
239
239
|
message: `playwright chrome launched → ${targetUrl.href}${ownerNote}`,
|
|
@@ -121,7 +121,7 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
|
|
|
121
121
|
category: 'same-origin-runtime',
|
|
122
122
|
destination: 'the origin already serving the peach runtime',
|
|
123
123
|
preConsent: true,
|
|
124
|
-
payload: 'runtime assets and shared local settings',
|
|
124
|
+
payload: 'runtime assets, selected-shell reachability, and shared local settings',
|
|
125
125
|
},
|
|
126
126
|
guest_app_network: {
|
|
127
127
|
category: 'guest-app-network',
|
|
@@ -552,12 +552,25 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
552
552
|
{ category: 'same_origin_runtime', count: 1 },
|
|
553
553
|
// the tenant build's built-in app chunk prefetch list and the chunk bodies
|
|
554
554
|
// it names, resolved against the worker chunk's own url.
|
|
555
|
-
'packages/peach-engine/src/render-worker/app-in-worker.tsx :: fetch :: listUrl': {
|
|
555
|
+
'packages/peach-engine/src/render-worker/app-in-worker.tsx :: fetch-alias :: listUrl': {
|
|
556
556
|
category: 'same_origin_runtime',
|
|
557
557
|
count: 1,
|
|
558
558
|
},
|
|
559
|
-
'packages/peach-engine/src/render-worker/app-in-worker.tsx :: fetch :: new URL(file, listUrl)':
|
|
559
|
+
'packages/peach-engine/src/render-worker/app-in-worker.tsx :: fetch-alias :: new URL(file, listUrl)':
|
|
560
560
|
{ category: 'same_origin_runtime', count: 1 },
|
|
561
|
+
// the tenant build's own chunks a dynamic import names, resolved against the
|
|
562
|
+
// importing chunk's url.
|
|
563
|
+
'packages/peach-engine/src/render-worker/module-preload.ts :: fetch-alias :: url': {
|
|
564
|
+
category: 'same_origin_runtime',
|
|
565
|
+
count: 1,
|
|
566
|
+
},
|
|
567
|
+
// guest fetch wrappers forward the guest's own request to the host fetch.
|
|
568
|
+
'packages/peach-engine/src/headless/tenant.ts :: fetch-alias :: activeInput as RequestInfo':
|
|
569
|
+
{ category: 'guest_app_network', count: 1 },
|
|
570
|
+
'packages/peach-engine/src/render-worker/worker.ts :: fetch-alias :: input': {
|
|
571
|
+
category: 'guest_app_network',
|
|
572
|
+
count: 1,
|
|
573
|
+
},
|
|
561
574
|
'packages/peach-engine/src/render-worker/rive-animation-clock.ts :: fetch :: wasmUrl': {
|
|
562
575
|
category: 'same_origin_runtime',
|
|
563
576
|
count: 1,
|
|
@@ -630,6 +643,10 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
630
643
|
category: 'local_development',
|
|
631
644
|
count: 1,
|
|
632
645
|
},
|
|
646
|
+
'packages/peachsim/cli/commands/detox.ts :: fetch :: shellUrl': {
|
|
647
|
+
category: 'same_origin_runtime',
|
|
648
|
+
count: 1,
|
|
649
|
+
},
|
|
633
650
|
'packages/peachsim/cli/commands/cpu-profile.ts :: fetch :: `http://localhost:${cdpPort}/json/list`':
|
|
634
651
|
{ category: 'local_development', count: 1 },
|
|
635
652
|
'packages/peachsim/cli/commands/cpu-profile.ts :: websocket :: wsUrl': {
|
|
@@ -821,7 +838,51 @@ interface NetworkNames {
|
|
|
821
838
|
nodeGet: Set<string>
|
|
822
839
|
}
|
|
823
840
|
|
|
824
|
-
|
|
841
|
+
// `<owner>.fetch.bind(...)` captures the host fetch under another name
|
|
842
|
+
function isFetchBinding(node: Expression): boolean {
|
|
843
|
+
return (
|
|
844
|
+
node.type === 'CallExpression' &&
|
|
845
|
+
node.callee.type === 'MemberExpression' &&
|
|
846
|
+
!node.callee.computed &&
|
|
847
|
+
node.callee.property.type === 'Identifier' &&
|
|
848
|
+
node.callee.property.name === 'bind' &&
|
|
849
|
+
node.callee.object.type === 'MemberExpression' &&
|
|
850
|
+
!node.callee.object.computed &&
|
|
851
|
+
node.callee.object.property.type === 'Identifier' &&
|
|
852
|
+
node.callee.object.property.name === 'fetch'
|
|
853
|
+
)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// names a relative module exports as fetch aliases, so a call through the
|
|
857
|
+
// imported binding is declared like a direct fetch.
|
|
858
|
+
const exportedFetchAliasCache = new Map<string, Set<string>>()
|
|
859
|
+
function exportedFetchAliases(importer: string, specifier: string): Set<string> {
|
|
860
|
+
if (!specifier.startsWith('.')) return new Set()
|
|
861
|
+
const base = path.resolve(path.dirname(importer), specifier)
|
|
862
|
+
const file = [`${base}.ts`, `${base}.tsx`, base, path.join(base, 'index.ts')].find(
|
|
863
|
+
(candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile(),
|
|
864
|
+
)
|
|
865
|
+
if (!file) return new Set()
|
|
866
|
+
const cached = exportedFetchAliasCache.get(file)
|
|
867
|
+
if (cached) return cached
|
|
868
|
+
const aliases = new Set<string>()
|
|
869
|
+
exportedFetchAliasCache.set(file, aliases)
|
|
870
|
+
const program = parseSync(file, fs.readFileSync(file, 'utf8'), {
|
|
871
|
+
lang: file.endsWith('.tsx') ? 'tsx' : 'ts',
|
|
872
|
+
}).program
|
|
873
|
+
const names = networkNames(program, file)
|
|
874
|
+
for (const statement of program.body) {
|
|
875
|
+
if (statement.type !== 'ExportNamedDeclaration') continue
|
|
876
|
+
if (statement.declaration?.type !== 'VariableDeclaration') continue
|
|
877
|
+
for (const declarator of statement.declaration.declarations) {
|
|
878
|
+
if (declarator.id.type === 'Identifier' && names.fetch.has(declarator.id.name))
|
|
879
|
+
aliases.add(declarator.id.name)
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return aliases
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function networkNames(source: Program, file: string): NetworkNames {
|
|
825
886
|
const names: NetworkNames = {
|
|
826
887
|
fetch: new Set(['fetch']),
|
|
827
888
|
httpClient: new Set(['http', 'https']),
|
|
@@ -864,6 +925,16 @@ function networkNames(source: Program): NetworkNames {
|
|
|
864
925
|
},
|
|
865
926
|
ImportDeclaration(node) {
|
|
866
927
|
const moduleName = node.source.value.replace(/^node:/, '')
|
|
928
|
+
const fetchAliases = exportedFetchAliases(file, node.source.value)
|
|
929
|
+
for (const specifier of node.specifiers) {
|
|
930
|
+
if (
|
|
931
|
+
specifier.type === 'ImportSpecifier' &&
|
|
932
|
+
specifier.imported.type === 'Identifier' &&
|
|
933
|
+
fetchAliases.has(specifier.imported.name)
|
|
934
|
+
) {
|
|
935
|
+
names.fetch.add(specifier.local.name)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
867
938
|
if (moduleName === 'http' || moduleName === 'https') {
|
|
868
939
|
for (const specifier of node.specifiers) {
|
|
869
940
|
if (specifier.type === 'ImportSpecifier') {
|
|
@@ -895,7 +966,10 @@ function networkNames(source: Program): NetworkNames {
|
|
|
895
966
|
},
|
|
896
967
|
VariableDeclarator(node) {
|
|
897
968
|
if (node.id.type !== 'Identifier' || !node.init) return
|
|
898
|
-
if (
|
|
969
|
+
if (
|
|
970
|
+
(node.init.type === 'Identifier' && names.fetch.has(node.init.name)) ||
|
|
971
|
+
isFetchBinding(node.init)
|
|
972
|
+
) {
|
|
899
973
|
names.fetch.add(node.id.name)
|
|
900
974
|
}
|
|
901
975
|
if (isClientExpression(node.init, names.httpClient)) {
|
|
@@ -1007,7 +1081,7 @@ export function detectOutboundCalls(repositoryRoot: string): DetectedOutboundCal
|
|
|
1007
1081
|
const program = parseSync(file, source, {
|
|
1008
1082
|
lang: file.endsWith('.tsx') ? 'tsx' : 'ts',
|
|
1009
1083
|
}).program
|
|
1010
|
-
const names = networkNames(program)
|
|
1084
|
+
const names = networkNames(program, file)
|
|
1011
1085
|
const relative = path.relative(repositoryRoot, file).split(path.sep).join('/')
|
|
1012
1086
|
const recordCall = (node: CallExpression | NewExpression): void => {
|
|
1013
1087
|
const kind = callKind(node, names, source)
|