peachsim 0.1.1635 → 0.1.1641
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 +10 -1
- package/cli/app-config.ts +14 -5
- package/cli/app-stubs.ts +96 -22
- package/cli/bridge-flow-runner.ts +2 -2
- package/cli/cloud-client.ts +60 -40
- package/cli/cloud-session.ts +16 -0
- package/cli/commands/assert.ts +17 -4
- package/cli/commands/box.ts +33 -19
- package/cli/commands/doctor.ts +37 -20
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect.ts +65 -61
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +154 -68
- package/cli/commands/swift.ts +18 -4
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +55 -49
- package/cli/drivers/playwright-sim-host.ts +65 -42
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +2 -2
- package/cli/outbound-endpoints.ts +7 -5
- package/cli/pause-aware-timeout.ts +4 -0
- package/cli/ws-bridge.ts +19 -16
- package/detox/attempt-events.cjs +99 -6
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +259 -139
- package/detox/jest-environment.cjs +5 -1
- package/detox/motion-change.cjs +170 -21
- package/detox/motion-change.d.cts +32 -0
- package/detox/navigation-lifecycle.ts +35 -3
- 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 +17 -7
- 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 +5014 -2268
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +1036 -166
- package/dist-lib/detox/jest-environment.cjs +5 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +4613 -1963
- package/dist-lib/expo-swift-metadata.cjs +9 -7
- package/dist-lib/expo-swift-metadata.mjs +9 -7
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +58072 -16400
- 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 +2790 -143
- 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 +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +2579 -95
- package/dist-lib/native-app-identity-project.cjs +243 -53
- package/dist-lib/native-app-identity-project.mjs +243 -53
- package/dist-lib/native-app-identity.cjs +29 -30
- package/dist-lib/native-app-identity.mjs +29 -30
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +2 -2
- package/dist-lib/react-native-host-modules.mjs +2 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
- package/dist-lib/sdk.cjs +4 -3
- package/dist-lib/sdk.mjs +4 -3
- package/dist-lib/skills.cjs +66475 -23097
- package/dist-lib/swift-compile.cjs +452 -260
- package/dist-lib/swift-compile.mjs +451 -252
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +14 -7
- package/dist-lib/swift-submit.mjs +14 -7
- package/dist-lib/swift-toolchain.cjs +316 -41
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8188 -5636
- package/dist-lib/vite.cjs +2857 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/package.json +1 -1
- package/src/backend-origin.ts +21 -6
- package/src/expo-swift-host.ts +5 -2
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +4 -6
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +1 -4
- package/src/native-app-identity.ts +1 -4
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/react-native-bridge-modules.ts +106 -78
- package/src/react-native-host-modules.ts +4 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-compile.ts +224 -139
- package/src/swift-distribution.lock.json +23 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +95 -30
- package/src/swift-icu.ts +16 -10
- package/src/swift-objc-compile.ts +547 -0
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +883 -19
- package/src/swift-objc-standin.ts +126 -14
- package/src/swift-package-manifest.ts +42 -1
- package/src/swift-packages.ts +35 -21
- package/src/swift-root-lock.ts +153 -0
- package/src/swift-standin-emit.ts +40 -7
- package/src/swift-standins.ts +404 -138
- package/src/swift-submit.ts +18 -7
- package/src/swift-toolchain.ts +378 -46
- package/src/swiftContract.ts +12 -0
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +280 -33
package/README.md
CHANGED
|
@@ -599,7 +599,11 @@ foreground or background state. Both retain its browser session and app state.
|
|
|
599
599
|
`sourceApp` does not add fields to React Native's `{ url }` event. A
|
|
600
600
|
stopped guest rejects URL delivery. Explicit `newInstance: true`, deletion,
|
|
601
601
|
and video recording create a new browser context; cold launches still use the
|
|
602
|
-
supplied shell URL.
|
|
602
|
+
supplied shell URL. Process termination and `newInstance: true` preserve
|
|
603
|
+
cookies, local storage and IndexedDB for the next launch. `delete: true` and
|
|
604
|
+
driver cleanup clear that retained storage. Relaunch creates a fresh runtime,
|
|
605
|
+
so input focus and other in-memory component state do not survive termination.
|
|
606
|
+
When synchronization is enabled, URL delivery waits for
|
|
603
607
|
existing work before dispatch and for navigation and painting afterward.
|
|
604
608
|
|
|
605
609
|
Element taps finish viewport measurement before resolving the selected node's
|
|
@@ -652,6 +656,11 @@ peach screenshot --with-frame --output /tmp/peach-framed.png
|
|
|
652
656
|
peach record --duration 5 --output /tmp/peach-anim.mp4
|
|
653
657
|
```
|
|
654
658
|
|
|
659
|
+
`peach record status` queries the recorder on the tracked simulator. A video
|
|
660
|
+
whose stop reply timed out remains available: status reports processing or
|
|
661
|
+
ready to save, and another `peach record stop --output <path>` joins the same
|
|
662
|
+
finalization and saves its bytes. `peach record cancel` discards them.
|
|
663
|
+
|
|
655
664
|
`peach screenshot --with-frame` composes the real Peach shell device chrome
|
|
656
665
|
around the raw screen bitmap (reusing the shell bezel/button geometry, excluding
|
|
657
666
|
the Electron top bar, rail gutter, and other window chrome). Flows can request
|
package/cli/app-config.ts
CHANGED
|
@@ -89,12 +89,21 @@ async function loadConfigFile(
|
|
|
89
89
|
}
|
|
90
90
|
const wire = WIRE_SLOTS.filter((slot) => loaded[slot] !== undefined)
|
|
91
91
|
if (wire.length > 0) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
const slots: readonly string[] = WIRE_SLOTS
|
|
93
|
+
const kept = Object.entries(loaded)
|
|
94
|
+
.filter(([key, value]) => !slots.includes(key) && value !== undefined)
|
|
95
|
+
.map(([key]) => key)
|
|
96
|
+
const migration = wireSlotMigration(
|
|
97
|
+
project.appDir,
|
|
98
|
+
project.configPath,
|
|
99
|
+
{
|
|
100
|
+
modules: { ...loaded.turboModules, ...loaded.nativeModules },
|
|
101
|
+
views: { ...loaded.nativeViews },
|
|
102
|
+
},
|
|
103
|
+
kept,
|
|
104
|
+
)
|
|
96
105
|
throw new Error(
|
|
97
|
-
`${wire.join(', ')} moved under packages, keyed by the package whose native side they answer.
|
|
106
|
+
`${wire.join(', ')} moved under packages, keyed by the package whose native side they answer. ${kept.length ? 'remove them and add' : 'replace the config with'}:\n${migration}`,
|
|
98
107
|
)
|
|
99
108
|
}
|
|
100
109
|
return resolveConfigReplacementFiles(loaded, project.configPath)
|
package/cli/app-stubs.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type PeachConfig,
|
|
26
26
|
} from '../src/config'
|
|
27
27
|
import { isRecord } from '../src/is-record'
|
|
28
|
+
import { rnxPublicBrand } from '../src/public-brand'
|
|
28
29
|
import { visitParsedProgram } from '../src/visit-parsed-program'
|
|
29
30
|
import {
|
|
30
31
|
readProjectManifest,
|
|
@@ -380,26 +381,77 @@ export function packageNativeRequests(dir: string): NativeRequest[] {
|
|
|
380
381
|
)
|
|
381
382
|
}
|
|
382
383
|
|
|
383
|
-
// the
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
384
|
+
// resolve the installed runtime dependency graph, including transitive native
|
|
385
|
+
// packages. workspace source is scanned as app code instead. a transitive
|
|
386
|
+
// package is scanned only when it ships native code: the graph runs to
|
|
387
|
+
// thousands of packages, and the rest name nothing a direct dependency does not.
|
|
388
|
+
function dependencyNativeOwners(
|
|
388
389
|
appDir: string,
|
|
389
|
-
|
|
390
|
-
): string {
|
|
390
|
+
manifest: ProjectManifest | null,
|
|
391
|
+
): Map<string, string> {
|
|
391
392
|
const owners = new Map<string, string>()
|
|
392
|
-
const
|
|
393
|
-
for (const name of Object.keys({
|
|
393
|
+
const pending = Object.keys({
|
|
394
394
|
...manifest?.devDependencies,
|
|
395
395
|
...manifest?.dependencies,
|
|
396
|
-
})) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
396
|
+
}).map((name) => ({ from: appDir, name }))
|
|
397
|
+
const visited = new Set<string>()
|
|
398
|
+
for (let index = 0; index < pending.length; index++) {
|
|
399
|
+
const { from, name } = pending[index]
|
|
400
|
+
const dir = installedPackageDir(from, name)
|
|
401
|
+
if (!dir || visited.has(dir)) continue
|
|
402
|
+
visited.add(dir)
|
|
403
|
+
const dependency = readProjectManifest(dir)
|
|
404
|
+
const optional = readPackageJson(dir)?.optionalDependencies
|
|
405
|
+
if (
|
|
406
|
+
dir.split('/').includes('node_modules') &&
|
|
407
|
+
(from === appDir || hasNativeCode(dir, 'ios'))
|
|
408
|
+
)
|
|
409
|
+
for (const request of packageNativeRequests(dir))
|
|
410
|
+
if (!owners.has(`${request.kind}:${request.name}`))
|
|
411
|
+
owners.set(`${request.kind}:${request.name}`, dependency?.name ?? name)
|
|
412
|
+
for (const child of Object.keys({
|
|
413
|
+
...dependency?.dependencies,
|
|
414
|
+
...(isRecord(optional) ? optional : {}),
|
|
415
|
+
}))
|
|
416
|
+
pending.push({ from: dir, name: child })
|
|
402
417
|
}
|
|
418
|
+
return owners
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// a complete config module in the form the app's config file already uses, so
|
|
422
|
+
// the printed text pastes over it
|
|
423
|
+
function configModule(configPath: string | null, packages: string[]): string {
|
|
424
|
+
const body = [' packages: {', ...packages, ' },']
|
|
425
|
+
if (
|
|
426
|
+
configPath &&
|
|
427
|
+
(configPath.endsWith('.cjs') ||
|
|
428
|
+
(configPath.endsWith('.js') &&
|
|
429
|
+
/\bmodule\.exports\b/.test(readSmallFile(configPath) ?? '')))
|
|
430
|
+
)
|
|
431
|
+
return ['module.exports = {', ...body, '}'].join('\n')
|
|
432
|
+
return [
|
|
433
|
+
`import { defineConfig } from '${rnxPublicBrand.packageName}/config'`,
|
|
434
|
+
'',
|
|
435
|
+
'export default defineConfig({',
|
|
436
|
+
...body,
|
|
437
|
+
'})',
|
|
438
|
+
].join('\n')
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// what replaces a config's old top-level wire slots: each name keyed by the
|
|
442
|
+
// package whose JavaScript asks for it. a config holding only those slots gets
|
|
443
|
+
// a complete module; one with other settings (`kept`) gets just the packages
|
|
444
|
+
// entries, since a whole module would drop them. `packages` takes only 'noop'
|
|
445
|
+
// or a file path, so any other old value is named for a hand move instead of
|
|
446
|
+
// guessed.
|
|
447
|
+
export function wireSlotMigration(
|
|
448
|
+
appDir: string,
|
|
449
|
+
configPath: string | null,
|
|
450
|
+
wire: { modules: Record<string, unknown>; views: Record<string, unknown> },
|
|
451
|
+
kept: readonly string[] = [],
|
|
452
|
+
): string {
|
|
453
|
+
const manifest = readProjectManifest(appDir)
|
|
454
|
+
const owners = dependencyNativeOwners(appDir, manifest)
|
|
403
455
|
if (manifest)
|
|
404
456
|
for (const request of scanLocalNative(
|
|
405
457
|
appDir,
|
|
@@ -408,6 +460,7 @@ export function wireSlotMigration(
|
|
|
408
460
|
undefined,
|
|
409
461
|
new Set(),
|
|
410
462
|
true,
|
|
463
|
+
owners,
|
|
411
464
|
))
|
|
412
465
|
if (!owners.has(`${request.kind}:${request.name}`))
|
|
413
466
|
owners.set(`${request.kind}:${request.name}`, request.package)
|
|
@@ -453,9 +506,17 @@ export function wireSlotMigration(
|
|
|
453
506
|
].filter(Boolean)
|
|
454
507
|
return ` [${JSON.stringify(owner)}]: { ${parts.join(', ')} },`
|
|
455
508
|
})
|
|
509
|
+
if (lines.length && kept.length)
|
|
510
|
+
return [
|
|
511
|
+
...manual.map((line) => `// ${line}`),
|
|
512
|
+
`// keep ${kept.join(', ')}; merge these into packages`,
|
|
513
|
+
' packages: {',
|
|
514
|
+
...lines,
|
|
515
|
+
' },',
|
|
516
|
+
].join('\n')
|
|
456
517
|
return [
|
|
457
|
-
...(
|
|
458
|
-
...
|
|
518
|
+
...manual.map((line) => `// ${line}`),
|
|
519
|
+
...(lines.length ? [configModule(configPath, lines)] : []),
|
|
459
520
|
].join('\n')
|
|
460
521
|
}
|
|
461
522
|
|
|
@@ -519,6 +580,7 @@ export function scanLocalNative(
|
|
|
519
580
|
platform: ScanResult['scoreSummary']['platform'] | undefined,
|
|
520
581
|
knownPackages: ReadonlySet<string> = new Set(),
|
|
521
582
|
includeLegacy = false,
|
|
583
|
+
providers?: ReadonlyMap<string, string>,
|
|
522
584
|
): LocalNative[] {
|
|
523
585
|
const expoModuleDirs: string[] = []
|
|
524
586
|
const excludedModuleDirs: string[] = []
|
|
@@ -549,7 +611,12 @@ export function scanLocalNative(
|
|
|
549
611
|
dir,
|
|
550
612
|
scanRepository(dir).directories.flatMap((sub) =>
|
|
551
613
|
listDir(sub)
|
|
552
|
-
.filter(
|
|
614
|
+
.filter(
|
|
615
|
+
(name) =>
|
|
616
|
+
NATIVE_SOURCE_FILE.test(name) &&
|
|
617
|
+
(!platform ||
|
|
618
|
+
(platform === 'ios' ? name.endsWith('.swift') : name.endsWith('.kt'))),
|
|
619
|
+
)
|
|
553
620
|
.map((name) => join(sub, name)),
|
|
554
621
|
),
|
|
555
622
|
]),
|
|
@@ -571,11 +638,14 @@ export function scanLocalNative(
|
|
|
571
638
|
return result
|
|
572
639
|
}
|
|
573
640
|
const found = new Map<string, LocalNative>()
|
|
641
|
+
// a name an installed dependency asks for belongs to that dependency, whoever
|
|
642
|
+
// in the app spells it out
|
|
574
643
|
const add = (kind: LocalNative['kind'], name: string, file: string, line: number) => {
|
|
575
|
-
const packageName = owner(dirname(file))
|
|
576
|
-
if (knownPackages.has(packageName)) return
|
|
577
644
|
const key = `${kind}:${name}`
|
|
578
645
|
if (found.has(key)) return
|
|
646
|
+
providers ??= dependencyNativeOwners(appDir, pkg)
|
|
647
|
+
const packageName = providers.get(key) ?? owner(dirname(file))
|
|
648
|
+
if (knownPackages.has(packageName)) return
|
|
579
649
|
const moduleDir = expoModuleDirs.find((dir) => file.startsWith(`${dir}/`)) ?? null
|
|
580
650
|
found.set(key, {
|
|
581
651
|
kind,
|
|
@@ -584,8 +654,12 @@ export function scanLocalNative(
|
|
|
584
654
|
path: `${relative(appDir, file)}:${line}`,
|
|
585
655
|
expoModule: moduleDir ? relative(appDir, moduleDir) || '.' : null,
|
|
586
656
|
swift: moduleDir
|
|
587
|
-
? (nativeFilesByModule.get(moduleDir) ?? []).some(
|
|
588
|
-
path
|
|
657
|
+
? (nativeFilesByModule.get(moduleDir) ?? []).some(
|
|
658
|
+
(path) =>
|
|
659
|
+
path.endsWith('.swift') &&
|
|
660
|
+
[...(readSmallFile(path) ?? '').matchAll(/\bName\(\s*"(\w+)"\s*\)/g)].some(
|
|
661
|
+
(match) => match[1] === name,
|
|
662
|
+
),
|
|
589
663
|
)
|
|
590
664
|
: false,
|
|
591
665
|
answered: answered[kind].has(name),
|
|
@@ -3103,11 +3103,11 @@ export class SootSimBridgeFlowRunner {
|
|
|
3103
3103
|
}
|
|
3104
3104
|
|
|
3105
3105
|
async startProfile() {
|
|
3106
|
-
const result = await this.evaluate<any>(`(() => {
|
|
3106
|
+
const result = await this.evaluate<any>(`(async () => {
|
|
3107
3107
|
if (!window.__sootsimShellPerf) {
|
|
3108
3108
|
return { error: "shell frame profile unavailable (__sootsimShellPerf missing on the page)" }
|
|
3109
3109
|
}
|
|
3110
|
-
window.__sootsimShellPerf.start()
|
|
3110
|
+
await window.__sootsimShellPerf.start()
|
|
3111
3111
|
return { started: true }
|
|
3112
3112
|
})()`)
|
|
3113
3113
|
if (result?.error) {
|
package/cli/cloud-client.ts
CHANGED
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
import { rewriteGuestImportMeta } from '../../peach-engine/src/guest-import-meta.ts'
|
|
28
28
|
import { loadMetroFingerprintRegistry } from '../../peach-engine/src/metro-fingerprint-registry-client.ts'
|
|
29
29
|
import { inferMetroModuleIdentity } from '../../peach-engine/src/metro-fingerprint.ts'
|
|
30
|
+
import {
|
|
31
|
+
isNativeJavaScriptBundle,
|
|
32
|
+
isRolldownBundle,
|
|
33
|
+
} from '../../peach-engine/src/preview/bundle-layout'
|
|
30
34
|
import { isLoopbackHost } from '../src/backend-origin.ts'
|
|
31
35
|
import {
|
|
32
36
|
isRnxCloudCommandType,
|
|
@@ -172,7 +176,7 @@ function resolveCloudOrigin(value?: string): string {
|
|
|
172
176
|
function readImmutableBundleSource(bundlePath: string, command: string): string {
|
|
173
177
|
const filepath = resolve(bundlePath)
|
|
174
178
|
if (extname(filepath) !== '.js') {
|
|
175
|
-
throw new Error(`${command} requires one local
|
|
179
|
+
throw new Error(`${command} requires one local .js bundle`)
|
|
176
180
|
}
|
|
177
181
|
const descriptor = openSync(filepath, 'r')
|
|
178
182
|
try {
|
|
@@ -200,9 +204,6 @@ function readImmutableBundleSource(bundlePath: string, command: string): string
|
|
|
200
204
|
} catch {
|
|
201
205
|
throw new Error(`${command} bundle must be valid UTF-8 JavaScript`)
|
|
202
206
|
}
|
|
203
|
-
if (source.includes('\0') || !source.includes('__d(') || !source.includes('__r(')) {
|
|
204
|
-
throw new Error(`${command} requires a Metro JavaScript bundle`)
|
|
205
|
-
}
|
|
206
207
|
return source
|
|
207
208
|
} finally {
|
|
208
209
|
closeSync(descriptor)
|
|
@@ -231,9 +232,6 @@ async function fetchBundleSource(bundleUrl: string, command: string): Promise<st
|
|
|
231
232
|
if (text.length === 0) {
|
|
232
233
|
throw new Error(`${command} bundle is empty`)
|
|
233
234
|
}
|
|
234
|
-
if (text.includes('\0') || !text.includes('__d(') || !text.includes('__r(')) {
|
|
235
|
-
throw new Error(`${command} requires a Metro JavaScript bundle`)
|
|
236
|
-
}
|
|
237
235
|
return text
|
|
238
236
|
}
|
|
239
237
|
|
|
@@ -260,6 +258,9 @@ async function loadBundleSource(bundleInput: string, command: string): Promise<s
|
|
|
260
258
|
const source = isRemoteBundleInput(bundleInput)
|
|
261
259
|
? await fetchBundleSource(bundleInput, command)
|
|
262
260
|
: readImmutableBundleSource(bundleInput, command)
|
|
261
|
+
if (!isNativeJavaScriptBundle(source)) {
|
|
262
|
+
throw new Error(`${command} requires a supported React Native JavaScript bundle`)
|
|
263
|
+
}
|
|
263
264
|
// validate the actual runtime script form without executing guest code or
|
|
264
265
|
// allocating a host.
|
|
265
266
|
try {
|
|
@@ -728,25 +729,44 @@ export async function produceCloudArtifact(
|
|
|
728
729
|
maxBytes: number = MAX_RNX_CLOUD_BUNDLE_BYTES,
|
|
729
730
|
assetsDirectory?: string,
|
|
730
731
|
): Promise<CloudArtifactProduction> {
|
|
731
|
-
|
|
732
|
+
const prepared = await embedBundleAssets(
|
|
732
733
|
bundlePath,
|
|
733
|
-
maxBytes,
|
|
734
734
|
REMOTE_COMMAND,
|
|
735
735
|
assetsDirectory ? 'require-assets' : 'allow-placeholders',
|
|
736
736
|
assetsDirectory,
|
|
737
737
|
)
|
|
738
|
+
return produceArtifact(prepared, maxBytes, REMOTE_COMMAND)
|
|
738
739
|
}
|
|
739
740
|
|
|
740
741
|
/** prepares a standalone preview whose bytes must represent every Metro asset. */
|
|
741
|
-
export async function producePreviewArtifact(
|
|
742
|
-
bundlePath
|
|
743
|
-
)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
)
|
|
742
|
+
export async function producePreviewArtifact(bundlePath: string) {
|
|
743
|
+
const prepared = await embedBundleAssets(bundlePath, PREVIEW_COMMAND, 'require-assets')
|
|
744
|
+
if (!isRolldownBundle(prepared.source)) {
|
|
745
|
+
return {
|
|
746
|
+
...(await produceArtifact(prepared, Number.POSITIVE_INFINITY, PREVIEW_COMMAND)),
|
|
747
|
+
minified: true,
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const bytes = Buffer.from(prepared.code, 'utf8')
|
|
751
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex')
|
|
752
|
+
return {
|
|
753
|
+
bytes,
|
|
754
|
+
sha256,
|
|
755
|
+
minified: false,
|
|
756
|
+
receipt: {
|
|
757
|
+
source: {
|
|
758
|
+
sha256: createHash('sha256').update(prepared.source).digest('hex'),
|
|
759
|
+
bytes: Buffer.byteLength(prepared.source, 'utf8'),
|
|
760
|
+
},
|
|
761
|
+
artifact: { sha256, bytes: bytes.byteLength },
|
|
762
|
+
},
|
|
763
|
+
warnings:
|
|
764
|
+
prepared.search.unconfirmed && prepared.dest
|
|
765
|
+
? [
|
|
766
|
+
`peach preview could not authenticate asset hashes in ${prepared.dest.directory}; confirm these are the assets this bundle was built against.`,
|
|
767
|
+
]
|
|
768
|
+
: [],
|
|
769
|
+
}
|
|
750
770
|
}
|
|
751
771
|
|
|
752
772
|
export interface CloudBoxArtifactProduction {
|
|
@@ -890,18 +910,10 @@ async function embedBundleAssets(
|
|
|
890
910
|
}
|
|
891
911
|
|
|
892
912
|
async function produceArtifact(
|
|
893
|
-
|
|
913
|
+
prepared: Awaited<ReturnType<typeof embedBundleAssets>>,
|
|
894
914
|
maxBytes: number,
|
|
895
915
|
command: string,
|
|
896
|
-
assetPolicy: 'allow-placeholders' | 'require-assets',
|
|
897
|
-
assetsDirectory?: string,
|
|
898
916
|
): Promise<CloudArtifactProduction> {
|
|
899
|
-
const prepared = await embedBundleAssets(
|
|
900
|
-
bundlePath,
|
|
901
|
-
command,
|
|
902
|
-
assetPolicy,
|
|
903
|
-
assetsDirectory,
|
|
904
|
-
)
|
|
905
917
|
const artifact = await buildRnxCloudArtifact(prepared.code, {
|
|
906
918
|
inferModuleIdentity: (source) => inferCloudModuleIdentity(source, command),
|
|
907
919
|
})
|
|
@@ -1453,12 +1465,17 @@ function cloudCommand(command: {
|
|
|
1453
1465
|
class CloudBridge implements WsBridge {
|
|
1454
1466
|
readonly plane = 'cloud' as const
|
|
1455
1467
|
|
|
1456
|
-
constructor(
|
|
1468
|
+
constructor(
|
|
1469
|
+
private readonly session: CloudSimSession,
|
|
1470
|
+
private readonly explicitTimeoutMs?: number,
|
|
1471
|
+
) {}
|
|
1457
1472
|
|
|
1458
1473
|
async send(
|
|
1459
1474
|
command: { type: string; [key: string]: unknown },
|
|
1460
|
-
options?: { timeoutMs?: number },
|
|
1475
|
+
options?: { timeoutMs?: number; defaultTimeoutMs?: number },
|
|
1461
1476
|
): Promise<unknown> {
|
|
1477
|
+
const timeoutMs =
|
|
1478
|
+
options?.timeoutMs ?? this.explicitTimeoutMs ?? options?.defaultTimeoutMs
|
|
1462
1479
|
const body: CloudCommandEnvelope = {
|
|
1463
1480
|
id: randomUUID(),
|
|
1464
1481
|
claimId: this.session.claimId,
|
|
@@ -1475,9 +1492,7 @@ class CloudBridge implements WsBridge {
|
|
|
1475
1492
|
'content-type': 'application/json',
|
|
1476
1493
|
},
|
|
1477
1494
|
body: JSON.stringify(body),
|
|
1478
|
-
...(
|
|
1479
|
-
? { signal: AbortSignal.timeout(options.timeoutMs) }
|
|
1480
|
-
: {}),
|
|
1495
|
+
...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
|
|
1481
1496
|
},
|
|
1482
1497
|
)
|
|
1483
1498
|
} catch (error) {
|
|
@@ -1538,12 +1553,17 @@ class CloudBridge implements WsBridge {
|
|
|
1538
1553
|
class CloudBoxBridge implements WsBridge {
|
|
1539
1554
|
readonly plane = 'cloud' as const
|
|
1540
1555
|
|
|
1541
|
-
constructor(
|
|
1556
|
+
constructor(
|
|
1557
|
+
private readonly session: CloudBoxSession,
|
|
1558
|
+
private readonly explicitTimeoutMs?: number,
|
|
1559
|
+
) {}
|
|
1542
1560
|
|
|
1543
1561
|
async send(
|
|
1544
1562
|
command: { type: string; [key: string]: unknown },
|
|
1545
|
-
options?: { timeoutMs?: number },
|
|
1563
|
+
options?: { timeoutMs?: number; defaultTimeoutMs?: number },
|
|
1546
1564
|
): Promise<unknown> {
|
|
1565
|
+
const timeoutMs =
|
|
1566
|
+
options?.timeoutMs ?? this.explicitTimeoutMs ?? options?.defaultTimeoutMs
|
|
1547
1567
|
const id = randomUUID()
|
|
1548
1568
|
// the wire id is this request's, so the command carries none. `simId` names
|
|
1549
1569
|
// a local sim among several and a box has exactly one.
|
|
@@ -1562,9 +1582,7 @@ class CloudBoxBridge implements WsBridge {
|
|
|
1562
1582
|
'content-type': 'application/json',
|
|
1563
1583
|
},
|
|
1564
1584
|
body: JSON.stringify({ id, command: payload }),
|
|
1565
|
-
...(
|
|
1566
|
-
? { signal: AbortSignal.timeout(options.timeoutMs) }
|
|
1567
|
-
: {}),
|
|
1585
|
+
...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
|
|
1568
1586
|
},
|
|
1569
1587
|
)
|
|
1570
1588
|
} catch (error) {
|
|
@@ -1619,13 +1637,15 @@ class CloudBoxBridge implements WsBridge {
|
|
|
1619
1637
|
}
|
|
1620
1638
|
|
|
1621
1639
|
export function createCloudBridgeForParsed(
|
|
1622
|
-
parsed: Pick<ParsedBridgeCliArgs, 'simId' | 'simIdSource'
|
|
1640
|
+
parsed: Pick<ParsedBridgeCliArgs, 'simId' | 'simIdSource'> &
|
|
1641
|
+
Partial<Pick<ParsedBridgeCliArgs, 'explicitTimeout' | 'commandTimeoutMs'>>,
|
|
1623
1642
|
): WsBridge | null {
|
|
1624
1643
|
const session = resolveCloudSessionForParsed(parsed)
|
|
1625
1644
|
if (!session) return null
|
|
1645
|
+
const timeoutMs = parsed.explicitTimeout ? parsed.commandTimeoutMs : undefined
|
|
1626
1646
|
return session.service === 'box'
|
|
1627
|
-
? new CloudBoxBridge(session)
|
|
1628
|
-
: new CloudBridge(session)
|
|
1647
|
+
? new CloudBoxBridge(session, timeoutMs)
|
|
1648
|
+
: new CloudBridge(session, timeoutMs)
|
|
1629
1649
|
}
|
|
1630
1650
|
|
|
1631
1651
|
// deletes the box, which ends the simulator it holds. a cloud session box is
|
package/cli/cloud-session.ts
CHANGED
|
@@ -60,6 +60,7 @@ export interface CloudBoxSession {
|
|
|
60
60
|
platform?: RemotePlatform
|
|
61
61
|
device?: string
|
|
62
62
|
artifact?: CloudSessionArtifactIdentity
|
|
63
|
+
appFontsWireSha256?: string
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export function normalizeCloudOrigin(source: string): string {
|
|
@@ -128,7 +129,13 @@ export function createCloudBoxSession(input: {
|
|
|
128
129
|
platform?: RemotePlatform
|
|
129
130
|
device?: string
|
|
130
131
|
artifact?: CloudSessionArtifactIdentity
|
|
132
|
+
appFontsWireSha256?: string
|
|
131
133
|
}): CloudBoxSession {
|
|
134
|
+
if (
|
|
135
|
+
input.appFontsWireSha256 !== undefined &&
|
|
136
|
+
!/^[a-f0-9]{64}$/.test(input.appFontsWireSha256)
|
|
137
|
+
)
|
|
138
|
+
throw new Error('cloud box session has an invalid app-font wire digest')
|
|
132
139
|
const session: CloudBoxSession = {
|
|
133
140
|
version: CLOUD_SESSION_VERSION,
|
|
134
141
|
service: 'box',
|
|
@@ -138,6 +145,9 @@ export function createCloudBoxSession(input: {
|
|
|
138
145
|
...(input.platform ? { platform: input.platform } : {}),
|
|
139
146
|
...(input.device?.trim() ? { device: input.device.trim() } : {}),
|
|
140
147
|
...(input.artifact ? { artifact: input.artifact } : {}),
|
|
148
|
+
...(input.appFontsWireSha256 !== undefined
|
|
149
|
+
? { appFontsWireSha256: input.appFontsWireSha256 }
|
|
150
|
+
: {}),
|
|
141
151
|
}
|
|
142
152
|
if (!session.boxId || !session.boxToken) {
|
|
143
153
|
throw new Error('cloud box session requires a box, an origin, and a box token')
|
|
@@ -198,6 +208,11 @@ export function parseCloudSession(encoded: string): CloudSession {
|
|
|
198
208
|
try {
|
|
199
209
|
if (parsed.service === 'box') {
|
|
200
210
|
if (!boxId || !boxToken) throw new Error('unreadable cloud box session')
|
|
211
|
+
if (
|
|
212
|
+
parsed.appFontsWireSha256 !== undefined &&
|
|
213
|
+
typeof parsed.appFontsWireSha256 !== 'string'
|
|
214
|
+
)
|
|
215
|
+
throw new Error('unreadable app-font wire digest')
|
|
201
216
|
return createCloudBoxSession({
|
|
202
217
|
boxId,
|
|
203
218
|
boxToken,
|
|
@@ -205,6 +220,7 @@ export function parseCloudSession(encoded: string): CloudSession {
|
|
|
205
220
|
platform,
|
|
206
221
|
device,
|
|
207
222
|
artifact,
|
|
223
|
+
appFontsWireSha256: parsed.appFontsWireSha256,
|
|
208
224
|
})
|
|
209
225
|
}
|
|
210
226
|
const simId = parsed.simId
|
package/cli/commands/assert.ts
CHANGED
|
@@ -162,11 +162,14 @@ interface VerbResult {
|
|
|
162
162
|
reason?: string
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
async function runVerbOnce(
|
|
165
|
+
async function runVerbOnce(
|
|
166
|
+
verbArgs: string[],
|
|
167
|
+
globalArgs: string[],
|
|
168
|
+
): Promise<VerbResult> {
|
|
166
169
|
// re-invoke self through the real CLI entry, so every code path the user's
|
|
167
170
|
// verb would take, assert takes too.
|
|
168
171
|
const { executable, prefixArgs } = rnxSelfInvocation()
|
|
169
|
-
const args = [...prefixArgs, ...verbArgs, '--json']
|
|
172
|
+
const args = [...prefixArgs, ...globalArgs, ...verbArgs, '--json']
|
|
170
173
|
|
|
171
174
|
return new Promise<VerbResult>((resolve) => {
|
|
172
175
|
const proc = spawn(executable, args, {
|
|
@@ -402,7 +405,10 @@ function runJq(expr: string, payload: unknown): Promise<string> {
|
|
|
402
405
|
})
|
|
403
406
|
}
|
|
404
407
|
|
|
405
|
-
export async function runAssert(
|
|
408
|
+
export async function runAssert(
|
|
409
|
+
rawArgs: string[],
|
|
410
|
+
target: { port?: number; sim?: string } = {},
|
|
411
|
+
): Promise<void> {
|
|
406
412
|
if (rawArgs[0] === '--help' || rawArgs[0] === '-h' || rawArgs.length === 0) {
|
|
407
413
|
printHelp()
|
|
408
414
|
peachExit(rawArgs.length === 0 ? 2 : 0)
|
|
@@ -430,6 +436,13 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
|
|
|
430
436
|
const effectivePredicates =
|
|
431
437
|
predicates.length > 0 ? predicates : [{ kind: '--exists', args: [] }]
|
|
432
438
|
|
|
439
|
+
// the child is a fresh CLI process: without the caller's explicit port and
|
|
440
|
+
// sim it falls back to the saved target.
|
|
441
|
+
const globalArgs = [
|
|
442
|
+
...(target.port != null ? ['--port', String(target.port)] : []),
|
|
443
|
+
...(target.sim ? ['--sim', target.sim] : []),
|
|
444
|
+
]
|
|
445
|
+
|
|
433
446
|
const deadline = withinMs > 0 ? Date.now() + withinMs : 0
|
|
434
447
|
let attempts = 0
|
|
435
448
|
let lastFailReason = ''
|
|
@@ -437,7 +450,7 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
|
|
|
437
450
|
|
|
438
451
|
while (true) {
|
|
439
452
|
attempts++
|
|
440
|
-
const verbResult = await runVerbOnce(verbArgs)
|
|
453
|
+
const verbResult = await runVerbOnce(verbArgs, globalArgs)
|
|
441
454
|
if (!verbResult.ok) {
|
|
442
455
|
lastVerbSkipReason = verbResult.reason || 'verb failed'
|
|
443
456
|
if (Date.now() < deadline) {
|
package/cli/commands/box.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// name on the command line, else the one `peach box use` remembered. see
|
|
11
11
|
// boxTarget below.
|
|
12
12
|
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
13
14
|
import fs from 'node:fs'
|
|
14
15
|
import path from 'node:path'
|
|
15
16
|
import { createInterface } from 'node:readline'
|
|
@@ -536,7 +537,13 @@ export async function createPrebuiltBundleBox(options: {
|
|
|
536
537
|
const nativeRenderer = options.nativeProjectDir
|
|
537
538
|
? readNativeProjectRenderer(options.nativeProjectDir, platform)
|
|
538
539
|
: undefined
|
|
539
|
-
|
|
540
|
+
const appFonts = options.nativeProjectDir
|
|
541
|
+
? readNativeLinkedAppFontsWire({
|
|
542
|
+
projectDir: options.nativeProjectDir,
|
|
543
|
+
platform,
|
|
544
|
+
})
|
|
545
|
+
: ''
|
|
546
|
+
const box = await cloudBoxClient(options).create({
|
|
540
547
|
name: options.name,
|
|
541
548
|
stack: prebuiltBundleBoxTemplate.stack,
|
|
542
549
|
template: BOX_TEMPLATES.resolve({ name: prebuiltBundleBoxTemplate.name }).provenance,
|
|
@@ -544,16 +551,17 @@ export async function createPrebuiltBundleBox(options: {
|
|
|
544
551
|
options.bundle,
|
|
545
552
|
options.device,
|
|
546
553
|
options.initialUrl ?? null,
|
|
547
|
-
|
|
548
|
-
? readNativeLinkedAppFontsWire({
|
|
549
|
-
projectDir: options.nativeProjectDir,
|
|
550
|
-
platform,
|
|
551
|
-
})
|
|
552
|
-
: '',
|
|
554
|
+
appFonts,
|
|
553
555
|
nativeRenderer,
|
|
554
556
|
),
|
|
555
557
|
...(options.secrets ? { secrets: options.secrets } : {}),
|
|
556
558
|
})
|
|
559
|
+
return {
|
|
560
|
+
box,
|
|
561
|
+
appFontsWireSha256: box.existing
|
|
562
|
+
? undefined
|
|
563
|
+
: createHash('sha256').update(appFonts).digest('hex'),
|
|
564
|
+
}
|
|
557
565
|
}
|
|
558
566
|
|
|
559
567
|
// a plain .env file: `NAME=value` lines, an optional `export `, optional
|
|
@@ -698,7 +706,7 @@ async function runCloudBoxCreate(
|
|
|
698
706
|
}
|
|
699
707
|
let session: BoxShellSession | null = null
|
|
700
708
|
try {
|
|
701
|
-
const
|
|
709
|
+
const created =
|
|
702
710
|
'bundle' in source
|
|
703
711
|
? await createPrebuiltBundleBox({
|
|
704
712
|
...connection,
|
|
@@ -708,21 +716,27 @@ async function runCloudBoxCreate(
|
|
|
708
716
|
name,
|
|
709
717
|
...(secrets ? { secrets } : {}),
|
|
710
718
|
})
|
|
711
|
-
:
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
719
|
+
: {
|
|
720
|
+
box: await cloudBoxClient(connection).create({
|
|
721
|
+
name,
|
|
722
|
+
stack: template.stack,
|
|
723
|
+
template: template.provenance,
|
|
724
|
+
source: source.checkout,
|
|
725
|
+
...(secrets ? { secrets } : {}),
|
|
726
|
+
onProgress:
|
|
727
|
+
opts.verbose && !json
|
|
728
|
+
? (seeded, total) => console.log(` ...seeded ${seeded}/${total} files`)
|
|
729
|
+
: undefined,
|
|
730
|
+
}),
|
|
731
|
+
}
|
|
732
|
+
const { box } = created
|
|
722
733
|
if (json) {
|
|
723
734
|
console.log(
|
|
724
735
|
JSON.stringify({
|
|
725
736
|
...box.description,
|
|
737
|
+
...('appFontsWireSha256' in created
|
|
738
|
+
? { appFontsWireSha256: created.appFontsWireSha256 }
|
|
739
|
+
: {}),
|
|
726
740
|
...(box.accessToken ? { accessToken: box.accessToken } : {}),
|
|
727
741
|
}),
|
|
728
742
|
)
|