peachsim 0.1.527 → 0.1.570
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/README.md +26 -26
- package/cli/app-fonts.ts +1 -1
- package/cli/app-splash.ts +1 -1
- package/cli/bin.ts +1 -1
- package/cli/bridge-diagnostics.ts +1 -1
- package/cli/bridge-flow-runner.ts +57 -5
- package/cli/cloud-client.ts +273 -6
- package/cli/cloud-dispatch.ts +22 -2
- package/cli/cloud-session.ts +60 -4
- package/cli/cloud-test-tunnel.ts +354 -0
- package/cli/commands/box/bundle-plane.ts +2 -2
- package/cli/commands/box/checkout-plane.ts +1 -1
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +2 -2
- package/cli/commands/box.ts +8 -8
- package/cli/commands/compat.ts +1 -1
- package/cli/commands/control.ts +22 -3
- package/cli/commands/detox.ts +1 -1
- package/cli/commands/device.ts +1 -1
- package/cli/commands/diagnose.ts +1 -1
- package/cli/commands/film.ts +1 -1
- package/cli/commands/flow.ts +60 -3
- package/cli/commands/inspect/actions.ts +1 -1
- package/cli/commands/inspect/core.ts +6 -7
- package/cli/commands/inspect/find.ts +2 -1
- package/cli/commands/inspect/resolve-target.ts +5 -3
- package/cli/commands/inspect/shared.ts +1 -1
- package/cli/commands/inspect/wait-event.ts +1 -1
- package/cli/commands/inspect/wait-selector.ts +15 -4
- package/cli/commands/inspect.ts +19 -14
- package/cli/commands/maestro.ts +464 -43
- package/cli/commands/permissions.ts +1 -1
- package/cli/commands/platform.ts +205 -8
- package/cli/commands/preview.ts +2 -2
- package/cli/commands/record.ts +2 -2
- package/cli/commands/report-issue.ts +2 -2
- package/cli/commands/screenshot-layers.ts +1 -1
- package/cli/commands/screenshot.ts +1 -1
- package/cli/commands/setup.ts +2 -2
- package/cli/commands/test.ts +440 -115
- package/cli/commands/three-mode.ts +1 -1
- package/cli/commands/upload.ts +5 -5
- package/cli/commands/what-happened.ts +1 -1
- package/cli/desktop-companion.ts +3 -3
- package/cli/drivers/playwright-sim-host.ts +2 -2
- package/cli/flow-file.ts +7 -0
- package/cli/flow-live-status.ts +65 -8
- package/cli/help-core.ts +3 -3
- package/cli/hidden-runtime-alias.ts +1 -1
- package/cli/maestro-report.ts +178 -0
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +185 -136
- package/cli/parse-args.ts +1 -1
- package/cli/registry.ts +1 -1
- package/cli/resolve-assets.ts +1 -1
- package/cli/run-registry.ts +8 -0
- package/cli/send-to-box.ts +4 -4
- package/cli/ws-bridge.ts +3 -5
- package/detox/expectations.ts +1 -1
- package/detox/gestures.ts +2 -2
- package/detox/index.ts +71 -29
- package/detox/jest-preset.cjs +3 -3
- package/detox/jest-setup-after-env.cjs +1 -1
- package/dist-lib/agent-daemon-client.cjs +2 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +2 -2
- package/dist-lib/attached-projects.cjs +2 -2
- 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/capture-contract.cjs +94 -3
- package/dist-lib/capture-contract.mjs +93 -3
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud.cjs +2 -2
- package/dist-lib/cloud.mjs +2 -2
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +106 -25
- package/dist-lib/detox/jest-preset.cjs +3 -3
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +137 -61
- package/dist-lib/host/fetch-proxy-handler.cjs +28 -2
- package/dist-lib/host/fetch-proxy-overrides.cjs +33 -3
- package/dist-lib/host/fetch-proxy-overrides.mjs +30 -2
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +209 -16
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +2 -2
- package/dist-lib/menu.mjs +2 -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 +3 -3
- package/dist-lib/metro-production-bundle.mjs +3 -3
- package/dist-lib/metro.cjs +8 -8
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +2 -2
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +31 -15
- package/dist-lib/sdk.cjs +3 -2
- package/dist-lib/sdk.mjs +3 -2
- package/dist-lib/skills.cjs +362 -163
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +583 -16
- package/dist-lib/vite.cjs +29 -14
- package/package.json +9 -9
- package/scripts/dev-server-scanner.ts +41 -14
- package/scripts/optional-demo-registry.ts +3 -3
- package/skills/contrast/SKILL.md +9 -9
- package/skills/rnx-debug/SKILL.md +14 -4
- package/skills/rnx-perf/SKILL.md +5 -5
- package/skills/rnx-setup/SKILL.md +11 -11
- package/skills/rnx-test/SKILL.md +11 -11
- package/skills/rnx-visual/SKILL.md +6 -6
- package/src/agent-sessions.ts +3 -3
- package/src/attached-projects.ts +1 -1
- package/src/bridge-contract-input.ts +1 -1
- package/src/bridge-contract.ts +23 -5
- package/src/capture-contract.ts +145 -3
- package/src/config.ts +2 -2
- package/src/connect.ts +2 -2
- package/src/home-paths.ts +3 -3
- package/src/host/bridge-host.ts +2 -2
- package/src/host/fetch-proxy-handler.ts +4 -3
- package/src/host/fetch-proxy-overrides.ts +41 -0
- package/src/host/plane-host.ts +2 -2
- package/src/index.ts +3 -0
- package/src/metro-plugin.ts +4 -4
- package/src/metro-production-bundle.ts +2 -2
- package/src/native-seam-manifest.ts +6 -6
- package/src/public-brand.ts +1 -1
- package/src/rolldown-plugin-swift.ts +8 -5
- package/src/runtime-assets.ts +8 -6
- package/src/screenshots/frame-compose.ts +24 -56
- package/src/screenshots/org-deck.ts +1 -1
- package/src/screenshots/registry.ts +4 -4
- package/src/screenshots/schema.ts +1 -1
- package/src/sdk.ts +2 -2
- package/src/settings-bundle.ts +1 -1
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/compat-check.ts +1 -1
- package/src/vite-plugin-one.ts +1 -1
- package/src/vite-plugin-swift.ts +11 -6
- package/src/vite-plugin.ts +30 -17
- package/src/worklets-babel.ts +2 -2
package/src/bridge-contract.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// the wire contract for the sootsim ws bridge. the engine implements it in
|
|
2
|
-
// sootsim-engine/src/mcp/ws-bridge.ts; the
|
|
2
|
+
// sootsim-engine/src/mcp/ws-bridge.ts; the peachsim cli client, the SimClient
|
|
3
3
|
// sdk, and the flow runner speak it. it is the single owner of these shapes,
|
|
4
4
|
// and neither side redeclares them.
|
|
5
5
|
//
|
|
6
|
-
// it lives in
|
|
7
|
-
// sootsim-engine is `private: true`. the engine already depends on
|
|
8
|
-
// (see its `
|
|
6
|
+
// it lives in peachsim rather than the engine because peachsim is published and
|
|
7
|
+
// sootsim-engine is `private: true`. the engine already depends on peachsim
|
|
8
|
+
// (see its `peachsim` dependency and its `peachsim/bridge-constants` import), so
|
|
9
9
|
// this is the direction that ships.
|
|
10
10
|
|
|
11
11
|
import type {
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
SootSimCaptureFilter,
|
|
14
14
|
SootSimExternalAppLifecycleOptions,
|
|
15
15
|
SootSimStorageResetStrategy,
|
|
16
|
-
} from '@
|
|
16
|
+
} from '@peach/globals'
|
|
17
17
|
|
|
18
18
|
export type BridgeCommandType =
|
|
19
19
|
| 'evaluate'
|
|
@@ -42,11 +42,29 @@ export type BridgeCommandType =
|
|
|
42
42
|
| 'diagnostics'
|
|
43
43
|
| 'storageSnapshot'
|
|
44
44
|
|
|
45
|
+
// the bridge host answers these itself, before the engine's handleCommand ever
|
|
46
|
+
// sees them. they are not engine commands and the cloud plane does not route
|
|
47
|
+
// them, so they stay out of BridgeCommandType and out of its cloud scope table.
|
|
48
|
+
export type BridgeHostCommandType = 'bridge:list-sims' | 'bridge:claim'
|
|
49
|
+
|
|
50
|
+
// what a caller hands a bridge's `send`: the command minus the correlation id,
|
|
51
|
+
// which the transport assigns. every send seam names this type rather than an
|
|
52
|
+
// index-signature bag, because an index signature erases excess-property
|
|
53
|
+
// checking and a field spelled wrong then compiles and arrives as `undefined`.
|
|
54
|
+
export type WsBridgeCommand = Omit<WsCommand, 'id' | 'type'> & {
|
|
55
|
+
type: BridgeCommandType | BridgeHostCommandType
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
export interface WsCommand {
|
|
46
59
|
id: string | number
|
|
47
60
|
type: BridgeCommandType
|
|
48
61
|
// local daemon routing; cloud transport strips it before execution.
|
|
49
62
|
simId?: string
|
|
63
|
+
// take or refresh the cli lease on the target sim before running. reads omit
|
|
64
|
+
// it; anything that mutates user-facing state sets it. read in bridge-host.
|
|
65
|
+
acquireLock?: boolean
|
|
66
|
+
// bridge:claim — take the lease even when another cli holds it.
|
|
67
|
+
force?: boolean
|
|
50
68
|
// flowStatus — live flow-runner progress payload (see flow-run-store)
|
|
51
69
|
status?: Record<string, unknown>
|
|
52
70
|
code?: string
|
package/src/capture-contract.ts
CHANGED
|
@@ -188,7 +188,8 @@ export function selectRnxRasterNodeIds(tree: RnxRasterRegionTree): Set<number> {
|
|
|
188
188
|
node.text,
|
|
189
189
|
))) ||
|
|
190
190
|
node.style._liquidGlass !== undefined ||
|
|
191
|
-
node.style._liquidGlassEdge !== undefined
|
|
191
|
+
node.style._liquidGlassEdge !== undefined ||
|
|
192
|
+
node.style._liquidGlassMaterialTint !== undefined
|
|
192
193
|
) {
|
|
193
194
|
unsupported.add(node.nodeId)
|
|
194
195
|
}
|
|
@@ -318,6 +319,146 @@ export function rnxCaptureRasterRegions(
|
|
|
318
319
|
})
|
|
319
320
|
}
|
|
320
321
|
|
|
322
|
+
/** a region request snapped to the surface pixel grid and placed in the atlas */
|
|
323
|
+
export interface RnxPackedCaptureRegion extends RnxCaptureRegionRequest {
|
|
324
|
+
/** top-left of the region on the app surface, in pixels */
|
|
325
|
+
sourceX: number
|
|
326
|
+
sourceY: number
|
|
327
|
+
pixelWidth: number
|
|
328
|
+
pixelHeight: number
|
|
329
|
+
/** top-left of the region inside the atlas, in pixels */
|
|
330
|
+
atlasX: number
|
|
331
|
+
atlasY: number
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface RnxPackedCaptureAtlas {
|
|
335
|
+
/** logical atlas dimensions; pixel dimensions are these times the pixel ratio */
|
|
336
|
+
width: number
|
|
337
|
+
height: number
|
|
338
|
+
pixelWidth: number
|
|
339
|
+
pixelHeight: number
|
|
340
|
+
regions: RnxPackedCaptureRegion[]
|
|
341
|
+
/** the wire regions a producer publishes beside the encoded atlas */
|
|
342
|
+
atlasRegions: RnxCaptureAtlasRegion[]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// the bounded selective-layer atlas every producer ships: regions snapped to
|
|
346
|
+
// whole surface pixels, shelf-packed into the smallest of a few candidate
|
|
347
|
+
// widths, and mapped back to logical atlas coordinates. one packer for the
|
|
348
|
+
// headless renderer (which draws isolated subtrees into the slots) and the
|
|
349
|
+
// browser producer (which crops them out of a screen-sized screenshot), so a
|
|
350
|
+
// consumer never learns which one made the artifact from the layout.
|
|
351
|
+
export function packRnxCaptureRegions(
|
|
352
|
+
regions: readonly RnxCaptureRegionRequest[],
|
|
353
|
+
pixelRatio: number,
|
|
354
|
+
surface: { pixelWidth: number; pixelHeight: number },
|
|
355
|
+
): RnxPackedCaptureAtlas {
|
|
356
|
+
const physical = regions.map((region) => {
|
|
357
|
+
const left = Math.floor(region.frame.x * pixelRatio)
|
|
358
|
+
const top = Math.floor(region.frame.y * pixelRatio)
|
|
359
|
+
const right = Math.ceil((region.frame.x + region.frame.width) * pixelRatio)
|
|
360
|
+
const bottom = Math.ceil((region.frame.y + region.frame.height) * pixelRatio)
|
|
361
|
+
if (
|
|
362
|
+
left < 0 ||
|
|
363
|
+
top < 0 ||
|
|
364
|
+
right > surface.pixelWidth ||
|
|
365
|
+
bottom > surface.pixelHeight ||
|
|
366
|
+
right <= left ||
|
|
367
|
+
bottom <= top
|
|
368
|
+
) {
|
|
369
|
+
throw new Error(`capture region ${region.key} is outside the app surface`)
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
key: region.key,
|
|
373
|
+
nodeId: region.nodeId,
|
|
374
|
+
frame: {
|
|
375
|
+
x: left / pixelRatio,
|
|
376
|
+
y: top / pixelRatio,
|
|
377
|
+
width: (right - left) / pixelRatio,
|
|
378
|
+
height: (bottom - top) / pixelRatio,
|
|
379
|
+
},
|
|
380
|
+
sourceX: left,
|
|
381
|
+
sourceY: top,
|
|
382
|
+
pixelWidth: right - left,
|
|
383
|
+
pixelHeight: bottom - top,
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
const largestWidth = Math.max(...physical.map((region) => region.pixelWidth))
|
|
387
|
+
const totalArea = physical.reduce(
|
|
388
|
+
(area, region) => area + region.pixelWidth * region.pixelHeight,
|
|
389
|
+
0,
|
|
390
|
+
)
|
|
391
|
+
if (totalArea > 4_194_304) {
|
|
392
|
+
throw new Error('capture region pixels exceed the atlas budget')
|
|
393
|
+
}
|
|
394
|
+
const candidates = [
|
|
395
|
+
largestWidth,
|
|
396
|
+
Math.max(largestWidth, Math.ceil(Math.sqrt(totalArea))),
|
|
397
|
+
Math.max(largestWidth, surface.pixelWidth),
|
|
398
|
+
4_096,
|
|
399
|
+
]
|
|
400
|
+
const ordered = [...physical].sort(
|
|
401
|
+
(left, right) =>
|
|
402
|
+
right.pixelHeight - left.pixelHeight ||
|
|
403
|
+
right.pixelWidth - left.pixelWidth ||
|
|
404
|
+
left.key.localeCompare(right.key),
|
|
405
|
+
)
|
|
406
|
+
let best: { width: number; height: number; regions: RnxPackedCaptureRegion[] } | null =
|
|
407
|
+
null
|
|
408
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
409
|
+
if (candidate > 4_096) continue
|
|
410
|
+
let x = 0
|
|
411
|
+
let y = 0
|
|
412
|
+
let rowHeight = 0
|
|
413
|
+
let usedWidth = 0
|
|
414
|
+
const packed: RnxPackedCaptureRegion[] = []
|
|
415
|
+
for (const region of ordered) {
|
|
416
|
+
if (x > 0 && x + region.pixelWidth > candidate) {
|
|
417
|
+
y += rowHeight
|
|
418
|
+
x = 0
|
|
419
|
+
rowHeight = 0
|
|
420
|
+
}
|
|
421
|
+
packed.push({ ...region, atlasX: x, atlasY: y })
|
|
422
|
+
x += region.pixelWidth
|
|
423
|
+
rowHeight = Math.max(rowHeight, region.pixelHeight)
|
|
424
|
+
usedWidth = Math.max(usedWidth, x)
|
|
425
|
+
}
|
|
426
|
+
const height = y + rowHeight
|
|
427
|
+
if (height > 4_096 || usedWidth * height > 4_194_304) continue
|
|
428
|
+
const result = { width: usedWidth, height, regions: packed }
|
|
429
|
+
if (!best || result.width * result.height < best.width * best.height) best = result
|
|
430
|
+
}
|
|
431
|
+
if (!best) throw new Error('capture regions cannot fit in one bounded atlas')
|
|
432
|
+
const width = best.width / pixelRatio
|
|
433
|
+
const height = best.height / pixelRatio
|
|
434
|
+
return {
|
|
435
|
+
width,
|
|
436
|
+
height,
|
|
437
|
+
pixelWidth: best.width,
|
|
438
|
+
pixelHeight: best.height,
|
|
439
|
+
regions: best.regions,
|
|
440
|
+
atlasRegions: best.regions.map((region) => ({
|
|
441
|
+
key: region.key,
|
|
442
|
+
nodeId: region.nodeId,
|
|
443
|
+
frame: region.frame,
|
|
444
|
+
// a region flush with the far edge is anchored to the logical edge so
|
|
445
|
+
// its source never runs past the atlas by a rounding hair
|
|
446
|
+
source: {
|
|
447
|
+
x:
|
|
448
|
+
region.atlasX + region.pixelWidth === best.width
|
|
449
|
+
? width - region.frame.width
|
|
450
|
+
: region.atlasX / pixelRatio,
|
|
451
|
+
y:
|
|
452
|
+
region.atlasY + region.pixelHeight === best.height
|
|
453
|
+
? height - region.frame.height
|
|
454
|
+
: region.atlasY / pixelRatio,
|
|
455
|
+
width: region.frame.width,
|
|
456
|
+
height: region.frame.height,
|
|
457
|
+
},
|
|
458
|
+
})),
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
321
462
|
export interface RnxCaptureAtlasRegion extends RnxCaptureRegionRequest {
|
|
322
463
|
/** source rectangle inside the packed atlas, in logical atlas coordinates */
|
|
323
464
|
source: RnxCaptureRect
|
|
@@ -325,7 +466,7 @@ export interface RnxCaptureAtlasRegion extends RnxCaptureRegionRequest {
|
|
|
325
466
|
|
|
326
467
|
export interface RnxCaptureRegionAtlas {
|
|
327
468
|
kind: 'png-atlas'
|
|
328
|
-
producer: 'headless-cpu-app-surface'
|
|
469
|
+
producer: 'headless-cpu-app-surface' | 'browser-compositor-app-surface'
|
|
329
470
|
dataUri: string
|
|
330
471
|
/** logical atlas dimensions; the encoded PNG uses pixelWidth × pixelHeight */
|
|
331
472
|
width: number
|
|
@@ -533,7 +674,8 @@ export function isRnxCaptureRegionAtlas(value: unknown): value is RnxCaptureRegi
|
|
|
533
674
|
if (
|
|
534
675
|
!captureRecord(value) ||
|
|
535
676
|
value.kind !== 'png-atlas' ||
|
|
536
|
-
value.producer !== 'headless-cpu-app-surface'
|
|
677
|
+
(value.producer !== 'headless-cpu-app-surface' &&
|
|
678
|
+
value.producer !== 'browser-compositor-app-surface') ||
|
|
537
679
|
typeof value.dataUri !== 'string' ||
|
|
538
680
|
!value.dataUri.startsWith('data:image/png;base64,') ||
|
|
539
681
|
!captureNumber(value.width) ||
|
package/src/config.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// rnx.config.ts — per-app module resolution, turbo modules, env, state, and settings.
|
|
2
2
|
//
|
|
3
3
|
// usage:
|
|
4
|
-
// import { defineConfig } from '
|
|
4
|
+
// import { defineConfig } from 'peachsim/config'
|
|
5
5
|
// export default defineConfig({ modules: { ... }, env: { ... } })
|
|
6
6
|
|
|
7
|
-
import type { RevenueCatTestStoreConfig } from '@
|
|
7
|
+
import type { RevenueCatTestStoreConfig } from '@peach/compat/revenuecat-test-store'
|
|
8
8
|
|
|
9
9
|
export type ModuleResolution =
|
|
10
10
|
// an empty module under `modules`; under `nativeModules` / `turboModules` a
|
package/src/connect.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `connect()` — the node entrypoint of the
|
|
1
|
+
// `connect()` — the node entrypoint of the peachsim SDK. resolves the running
|
|
2
2
|
// bridge world the same way the CLI does (development bridge registry, then
|
|
3
3
|
// daemon, then default port), picks a sim, and returns a connected SimClient
|
|
4
4
|
// over the shared ws transport. node-only by design: sim-client.ts stays
|
|
@@ -67,7 +67,7 @@ export async function connect(options: ConnectOptions = {}): Promise<SimClient>
|
|
|
67
67
|
const bridge = createCloudBridgeForParsed(target)
|
|
68
68
|
if (!bridge)
|
|
69
69
|
throw new SimConnectError('remote simulator session disappeared while connecting')
|
|
70
|
-
return new SimClient(bridge, cloud.simId, {
|
|
70
|
+
return new SimClient(bridge, cloud.service === 'sim' ? cloud.simId : cloud.boxId, {
|
|
71
71
|
commandTimeoutMs: options.timeoutMs ?? 15_000,
|
|
72
72
|
})
|
|
73
73
|
}
|
package/src/home-paths.ts
CHANGED
|
@@ -56,10 +56,10 @@ export function rnxHomeDir(): string {
|
|
|
56
56
|
// checkout, for exercising the
|
|
57
57
|
// install path from this repo
|
|
58
58
|
//
|
|
59
|
-
// signal: realpath of process.argv[1] lands inside a `packages/
|
|
59
|
+
// signal: realpath of process.argv[1] lands inside a `packages/peachsim/`
|
|
60
60
|
// directory. workspace bin symlinks (`node_modules/.bin/sootsim` →
|
|
61
|
-
// `packages/
|
|
62
|
-
// (`bun packages/
|
|
61
|
+
// `packages/peachsim/dist-cli/bin.js`) and bun-direct invocations
|
|
62
|
+
// (`bun packages/peachsim/cli/bin.ts ...`) both match; published installs
|
|
63
63
|
// resolve under `node_modules/sootsim/` instead.
|
|
64
64
|
export function isSootsimDevCheckout(): boolean {
|
|
65
65
|
if (process.env.SOOTSIM_FORCE_DAEMON_INSTALL === '1') return false
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -228,7 +228,7 @@ export interface BridgeHostOptions {
|
|
|
228
228
|
port?: number
|
|
229
229
|
openUrl?: (url: string, options?: OpenUrlOptions) => Promise<void> | void
|
|
230
230
|
/** Contrast-specific excludes for the agent host's dev-server scan. passed
|
|
231
|
-
* through to AgentHost; see packages/
|
|
231
|
+
* through to AgentHost; see packages/peach-engine/src/dev-scan-excludes.ts
|
|
232
232
|
* for the canonical list. when omitted, AgentHost reads env vars directly. */
|
|
233
233
|
agentScanExcludes?: AgentHostOptions['getExcludePorts']
|
|
234
234
|
/** try preferred port, then port+1, port+2, …, up to this many attempts
|
|
@@ -1769,7 +1769,7 @@ export class SootSimBridgeHost {
|
|
|
1769
1769
|
|
|
1770
1770
|
// /__server-scan — list local metro/expo/vxrn/one dev servers. tenant
|
|
1771
1771
|
// worker ConnectRN polls this every few seconds to populate the device
|
|
1772
|
-
// picker. mirrors packages/
|
|
1772
|
+
// picker. mirrors packages/peach-shell/src/dev-middleware.ts so both
|
|
1773
1773
|
// host environments (vite dev and the prod CLI daemon) return identical
|
|
1774
1774
|
// JSON. icon URLs are rewritten through /__bundle-proxy so the browser
|
|
1775
1775
|
// doesn't hit cross-origin loads against the discovered dev server.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// shared fetch-proxy + app-api handlers for the sootsim dev/daemon HTTP
|
|
2
|
-
// surface. both the vite dev middleware (packages/
|
|
3
|
-
// dev-middleware.ts) and the daemon bridge (packages/
|
|
2
|
+
// surface. both the vite dev middleware (packages/peach-shell/src/
|
|
3
|
+
// dev-middleware.ts) and the daemon bridge (packages/peachsim/src/host/
|
|
4
4
|
// bridge-host.ts) mount these on their respective servers so guest bundle
|
|
5
5
|
// fetches resolve the same way regardless of which surface the sim is
|
|
6
6
|
// loaded through.
|
|
@@ -30,6 +30,7 @@ import { isLoopbackHost, normalizeHostname } from '../backend-origin.ts'
|
|
|
30
30
|
import {
|
|
31
31
|
FETCH_PROXY_BROWSER_USER_AGENT,
|
|
32
32
|
getFetchProxyTargetHeaders,
|
|
33
|
+
resolveFetchProxyTargetUrl,
|
|
33
34
|
} from './fetch-proxy-overrides.ts'
|
|
34
35
|
|
|
35
36
|
const STRIP_FETCH_PROXY_HEADERS = new Set([
|
|
@@ -184,7 +185,7 @@ export async function handleFetchProxyRequest(
|
|
|
184
185
|
|
|
185
186
|
let upstreamUrl: URL
|
|
186
187
|
try {
|
|
187
|
-
upstreamUrl = new URL(targetUrl)
|
|
188
|
+
upstreamUrl = resolveFetchProxyTargetUrl(new URL(targetUrl))
|
|
188
189
|
} catch {
|
|
189
190
|
applyFetchProxyCors(res)
|
|
190
191
|
res.writeHead(400, { 'Content-Type': 'text/plain' })
|
|
@@ -39,6 +39,47 @@ function headerOverridesFor(hostname: string):
|
|
|
39
39
|
return undefined
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// an app that builds its rpc provider hostname out of a per-tenant key cannot be
|
|
43
|
+
// pointed at another node by configuration alone. demo tooling injects this
|
|
44
|
+
// tenant name when a checkout carries no usable provider credentials, and the
|
|
45
|
+
// proxy resolves it to a public node for the chain. a real tenant never matches.
|
|
46
|
+
export const PUBLIC_RPC_TENANT = 'rnx-public-rpc'
|
|
47
|
+
|
|
48
|
+
const QUICKNODE_HOST_SUFFIX = '.quiknode.pro'
|
|
49
|
+
|
|
50
|
+
// quicknode encodes the chain in the subdomain and leaves mainnet off entirely.
|
|
51
|
+
const PUBLIC_RPC_BY_QUICKNODE_CHAIN: Record<string, string> = {
|
|
52
|
+
'': 'https://ethereum-rpc.publicnode.com',
|
|
53
|
+
'arbitrum-mainnet': 'https://arbitrum-one-rpc.publicnode.com',
|
|
54
|
+
'avalanche-mainnet': 'https://avalanche-c-chain-rpc.publicnode.com',
|
|
55
|
+
'base-mainnet': 'https://base-rpc.publicnode.com',
|
|
56
|
+
'blast-mainnet': 'https://blast-rpc.publicnode.com',
|
|
57
|
+
bsc: 'https://bsc-rpc.publicnode.com',
|
|
58
|
+
'celo-mainnet': 'https://celo-rpc.publicnode.com',
|
|
59
|
+
matic: 'https://polygon-bor-rpc.publicnode.com',
|
|
60
|
+
optimism: 'https://optimism-rpc.publicnode.com',
|
|
61
|
+
'soneium-mainnet': 'https://soneium-rpc.publicnode.com',
|
|
62
|
+
'unichain-mainnet': 'https://unichain-rpc.publicnode.com',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// the url the proxy should actually fetch, which differs from the requested one
|
|
66
|
+
// only for the sentinel tenant above
|
|
67
|
+
export function resolveFetchProxyTargetUrl(requested: URL): URL {
|
|
68
|
+
const host = requested.hostname.toLowerCase()
|
|
69
|
+
if (!host.endsWith(QUICKNODE_HOST_SUFFIX)) return requested
|
|
70
|
+
|
|
71
|
+
const tenant = host.slice(0, -QUICKNODE_HOST_SUFFIX.length)
|
|
72
|
+
if (tenant !== PUBLIC_RPC_TENANT && !tenant.startsWith(`${PUBLIC_RPC_TENANT}.`)) {
|
|
73
|
+
return requested
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const chain =
|
|
77
|
+
tenant === PUBLIC_RPC_TENANT ? '' : tenant.slice(PUBLIC_RPC_TENANT.length + 1)
|
|
78
|
+
const replacement = PUBLIC_RPC_BY_QUICKNODE_CHAIN[chain]
|
|
79
|
+
// the tenant token rides in the path, which a public node does not want
|
|
80
|
+
return replacement ? new URL(replacement) : requested
|
|
81
|
+
}
|
|
82
|
+
|
|
42
83
|
export function getFetchProxyTargetHeaders(targetUrl: URL): Record<string, string> {
|
|
43
84
|
const hostOverrides = headerOverridesFor(targetUrl.hostname)
|
|
44
85
|
return {
|
package/src/host/plane-host.ts
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import fs from 'node:fs'
|
|
19
19
|
import path from 'node:path'
|
|
20
|
-
import { dispatchPlaneCommand, indexVersion } from '@
|
|
20
|
+
import { dispatchPlaneCommand, indexVersion } from '@peach/box/plane'
|
|
21
21
|
import { CheckoutFilePlane } from '../../cli/commands/box/checkout-plane.ts'
|
|
22
|
-
import type { ProjectFilePlane } from '@
|
|
22
|
+
import type { ProjectFilePlane } from '@peach/box/plane'
|
|
23
23
|
import type { WebSocket } from 'ws'
|
|
24
24
|
|
|
25
25
|
const WS_OPEN = 1
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
isRnxCaptureRegionAtlas,
|
|
40
40
|
isRnxScreenCapture,
|
|
41
41
|
rnxCaptureRasterRegions,
|
|
42
|
+
packRnxCaptureRegions,
|
|
42
43
|
rnxRasterFrames,
|
|
43
44
|
RNX_SCREEN_CAPTURE_VERSION,
|
|
44
45
|
selectRnxRasterNodeIds,
|
|
@@ -54,6 +55,8 @@ export {
|
|
|
54
55
|
type RnxCaptureSource,
|
|
55
56
|
type RnxCaptureStyleTokenBinding,
|
|
56
57
|
type RnxCaptureTextLine,
|
|
58
|
+
type RnxPackedCaptureAtlas,
|
|
59
|
+
type RnxPackedCaptureRegion,
|
|
57
60
|
type RnxCaptureTextSegment,
|
|
58
61
|
type RnxCaptureTree,
|
|
59
62
|
type RnxCaptureVisualEvidence,
|
package/src/metro-plugin.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// when active changes, so one loaded shell cannot mix runtime chunk trees.
|
|
6
6
|
//
|
|
7
7
|
// usage in metro.config.js:
|
|
8
|
-
// const { withRNX } = require('
|
|
8
|
+
// const { withRNX } = require('peachsim/metro')
|
|
9
9
|
// module.exports = withRNX(getDefaultConfig(__dirname))
|
|
10
10
|
|
|
11
11
|
import fs from 'fs'
|
|
@@ -89,7 +89,7 @@ function readCallableMetroExport(
|
|
|
89
89
|
? Reflect.get(moduleValue, 'default')
|
|
90
90
|
: undefined
|
|
91
91
|
if (typeof callable !== 'function') {
|
|
92
|
-
throw new Error(`
|
|
92
|
+
throw new Error(`peachsim: Metro ${label} module did not export a function`)
|
|
93
93
|
}
|
|
94
94
|
return (...args) => Reflect.apply(callable, undefined, args)
|
|
95
95
|
}
|
|
@@ -115,7 +115,7 @@ function createMetroDefaultSerializer(projectRoot: string): MetroSerializer {
|
|
|
115
115
|
? Reflect.get(serialized, 'code')
|
|
116
116
|
: undefined
|
|
117
117
|
if (typeof code !== 'string') {
|
|
118
|
-
throw new Error('
|
|
118
|
+
throw new Error('peachsim: Metro bundleToString did not return bundle code')
|
|
119
119
|
}
|
|
120
120
|
return code
|
|
121
121
|
}
|
|
@@ -144,7 +144,7 @@ function appendRNXModulePaths(
|
|
|
144
144
|
.replaceAll(path.sep, '/')
|
|
145
145
|
}
|
|
146
146
|
if (Object.keys(modulePaths).length !== dependencies.size) {
|
|
147
|
-
throw new Error('
|
|
147
|
+
throw new Error('peachsim: bundle module IDs were not unique')
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
const specifiersByModuleId = new Map<string, Set<string>>()
|
|
@@ -30,7 +30,7 @@ export interface RNXMetroModuleIdentity extends RNXMetroModuleIdentityMetadata {
|
|
|
30
30
|
// from the identity footer the plugin's serializer appends.
|
|
31
31
|
export function toRNXProductionBundleUrl(bundleUrl: string): string {
|
|
32
32
|
const relative = bundleUrl.startsWith('/')
|
|
33
|
-
const url = new URL(bundleUrl, 'http://
|
|
33
|
+
const url = new URL(bundleUrl, 'http://peachsim.local')
|
|
34
34
|
url.searchParams.set('dev', 'false')
|
|
35
35
|
url.searchParams.set('hot', 'false')
|
|
36
36
|
url.searchParams.set('minify', 'true')
|
|
@@ -44,7 +44,7 @@ export function toRNXProductionBundleUrl(bundleUrl: string): string {
|
|
|
44
44
|
*/
|
|
45
45
|
export function toRNXDevelopmentBundleUrl(bundleUrl: string): string {
|
|
46
46
|
const relative = bundleUrl.startsWith('/')
|
|
47
|
-
const url = new URL(bundleUrl, 'http://
|
|
47
|
+
const url = new URL(bundleUrl, 'http://peachsim.local')
|
|
48
48
|
url.searchParams.set('dev', 'true')
|
|
49
49
|
url.searchParams.set('hot', 'false')
|
|
50
50
|
url.searchParams.set('minify', 'false')
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
// 1. identify upstream's native-module file (look for files calling
|
|
11
11
|
// `requireNativeModule`, `TurboModuleRegistry.getEnforced`,
|
|
12
12
|
// `requireNativeComponent`, or in `src/specs/Native*Module.ts`)
|
|
13
|
-
// 2. write a thin stub at `packages/compat/src/stubs/native-seams/<pkg>.ts`
|
|
13
|
+
// 2. write a thin stub at `packages/peach-compat/src/stubs/native-seams/<pkg>.ts`
|
|
14
14
|
// that exports the same default + named exports upstream's seam file
|
|
15
15
|
// exports — except the implementation calls into sootsim's engine.
|
|
16
16
|
// 3. append an entry below. the plugin handles the rest.
|
|
17
17
|
//
|
|
18
18
|
// canonical example: react-native-reanimated. see
|
|
19
|
-
// - `packages/compat/src/stubs/react-native-reanimated.ts` (the seam stub)
|
|
19
|
+
// - `packages/peach-compat/src/stubs/react-native-reanimated.ts` (the seam stub)
|
|
20
20
|
// - `reanimatedNativeSeamRedirect` in vite-plugin.ts (still hard-coded
|
|
21
21
|
// because it has package-specific edge cases — fabricUtils,
|
|
22
22
|
// validate-worklets-version. new entries should use this manifest path
|
|
@@ -41,7 +41,7 @@ export interface NativeSeamEntry {
|
|
|
41
41
|
seamBasenames: string[]
|
|
42
42
|
/**
|
|
43
43
|
* absolute path to the seam stub. typically
|
|
44
|
-
* `packages/compat/src/stubs/native-seams/<pkg>.ts` — but a single stub
|
|
44
|
+
* `packages/peach-compat/src/stubs/native-seams/<pkg>.ts` — but a single stub
|
|
45
45
|
* file may export every native module the package exposes, so the same
|
|
46
46
|
* `target` can appear in multiple entries if a package has multiple native
|
|
47
47
|
* modules that share an implementation file.
|
|
@@ -61,9 +61,9 @@ import path from 'node:path'
|
|
|
61
61
|
import { fileURLToPath } from 'node:url'
|
|
62
62
|
|
|
63
63
|
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
64
|
-
const NATIVE_SEAMS_DIR = path.resolve(HERE, '../../compat/src/stubs/native-seams')
|
|
64
|
+
const NATIVE_SEAMS_DIR = path.resolve(HERE, '../../peach-compat/src/stubs/native-seams')
|
|
65
65
|
|
|
66
|
-
/** resolve a stub path under `packages/compat/src/stubs/native-seams/`.
|
|
66
|
+
/** resolve a stub path under `packages/peach-compat/src/stubs/native-seams/`.
|
|
67
67
|
* resolves to `.tsx` when only that exists (JSX-bearing seams like
|
|
68
68
|
* expo-image, react-native-webview). preferred order: `.ts`, `.tsx`. */
|
|
69
69
|
export function seamPath(basename: string): string {
|
|
@@ -217,7 +217,7 @@ export const NATIVE_SEAM_MANIFEST: NativeSeamEntry[] = [
|
|
|
217
217
|
// onto the class-based seam, whose methods throw.
|
|
218
218
|
//
|
|
219
219
|
// registering by module NAME instead is what keeps old and new package
|
|
220
|
-
// versions both working: ../../compat/src/stubs/expo-{calendar,contacts}-register.ts
|
|
220
|
+
// versions both working: ../../peach-compat/src/stubs/expo-{calendar,contacts}-register.ts
|
|
221
221
|
// installs every id either version asks for, and upstream's own
|
|
222
222
|
// `requireNativeModule` picks the right one per entry point. a file
|
|
223
223
|
// redirect can only match one layout, so it breaks the versions it was
|
package/src/public-brand.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
enumerateBundleImages,
|
|
40
40
|
enumerateBundleJson,
|
|
41
41
|
} from '../../sootsim-swift/src/bundleAssets'
|
|
42
|
+
import { swiftEntryModuleEsm } from '../../sootsim-swift/src/entryModule'
|
|
42
43
|
import { compileSwiftProject, httpSwiftCompileSubmit } from './swift-submit'
|
|
43
44
|
import { entryRootId } from './vite-plugin-swift'
|
|
44
45
|
import type { SwiftModuleAddress } from './swift-submit'
|
|
@@ -230,11 +231,13 @@ export function swiftRemotePlugin(options: SwiftRemotePluginOptions): Plugin {
|
|
|
230
231
|
// every import mounts through the one entry: the artifact answers
|
|
231
232
|
// whether it is a view or an app root at runtime, so the transform
|
|
232
233
|
// never re-derives the conformance from source.
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
return swiftEntryModuleEsm({
|
|
235
|
+
url: address.url,
|
|
236
|
+
hash: address.hash,
|
|
237
|
+
rootId: entryRootId(packagePath, file),
|
|
238
|
+
resources: address.imageUrls ?? {},
|
|
239
|
+
bundleFiles: pack.resources,
|
|
240
|
+
})
|
|
238
241
|
},
|
|
239
242
|
}
|
|
240
243
|
}
|
package/src/runtime-assets.ts
CHANGED
|
@@ -34,11 +34,11 @@ let _monorepoRootCache: string | null | undefined
|
|
|
34
34
|
function findMonorepoRoot(): string | null {
|
|
35
35
|
if (_monorepoRootCache !== undefined) return _monorepoRootCache
|
|
36
36
|
let dir = path.dirname(fileURLToPath(import.meta.url))
|
|
37
|
-
// walk up until we find the Contrast monorepo: it has packages/
|
|
37
|
+
// walk up until we find the Contrast monorepo: it has packages/peach-engine and
|
|
38
38
|
// a public/sootsim dev-build output slot. bounded by the filesystem root.
|
|
39
39
|
for (let i = 0; i < 12 && dir !== path.dirname(dir); i++) {
|
|
40
40
|
if (
|
|
41
|
-
fs.existsSync(path.join(dir, 'packages/
|
|
41
|
+
fs.existsSync(path.join(dir, 'packages/peach-engine/package.json')) &&
|
|
42
42
|
fs.existsSync(path.join(dir, 'public/sootsim'))
|
|
43
43
|
) {
|
|
44
44
|
_monorepoRootCache = dir
|
|
@@ -66,11 +66,12 @@ export function resolveDevCheckoutRuntimeRoot(): string | null {
|
|
|
66
66
|
if (!root) return null
|
|
67
67
|
const devBuild = path.join(root, 'public/sootsim')
|
|
68
68
|
// require the runtime shape the installed runtime also has, so serving it is
|
|
69
|
-
// a drop-in (index.html shell + engine/ + engine-tenant
|
|
69
|
+
// a drop-in (index.html shell + engine/ + both engine-tenant workers).
|
|
70
70
|
if (
|
|
71
71
|
fs.existsSync(path.join(devBuild, 'index.html')) &&
|
|
72
72
|
fs.existsSync(path.join(devBuild, 'engine/index.html')) &&
|
|
73
|
-
fs.existsSync(path.join(devBuild, 'engine-tenant/worker.js'))
|
|
73
|
+
fs.existsSync(path.join(devBuild, 'engine-tenant/worker.js')) &&
|
|
74
|
+
fs.existsSync(path.join(devBuild, 'engine-tenant/worker.prod.js'))
|
|
74
75
|
) {
|
|
75
76
|
return devBuild
|
|
76
77
|
}
|
|
@@ -217,9 +218,10 @@ export function serveRuntimeFile(req: any, res: any, fullPath: string) {
|
|
|
217
218
|
return
|
|
218
219
|
}
|
|
219
220
|
// these urls are STABLE across dev rebuilds and runtime version switches
|
|
220
|
-
// (worker
|
|
221
|
+
// (both worker entries, canvaskit-graphite.wasm, and the html shells are
|
|
222
|
+
// unhashed), so the
|
|
221
223
|
// browser must revalidate every load — the old year-long immutable here made every
|
|
222
|
-
// new tab run a stale cached
|
|
224
|
+
// new tab run a stale cached tenant entry against a fresh engine,
|
|
223
225
|
// which also broke `do reload` ("bridge never reconnected"). no-cache +
|
|
224
226
|
// last-modified keeps repeat loads cheap 304s on localhost.
|
|
225
227
|
res.setHeader('cache-control', 'no-cache')
|