peachsim 0.1.1406 → 0.1.1463
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -4
- package/README.md +22 -2
- package/cli/app-fonts.ts +50 -44
- package/cli/bin.ts +5 -4
- package/cli/bridge-diagnostics.ts +5 -0
- package/cli/bridge-flow-runner.ts +21 -13
- package/cli/cloud-client.ts +4 -3
- package/cli/commands/control.ts +155 -123
- package/cli/commands/daemon-mac-app.ts +3 -3
- package/cli/commands/daemon.ts +2 -2
- package/cli/commands/do-chain.ts +9 -2
- package/cli/commands/electron.ts +4 -1
- package/cli/commands/inspect/core.ts +1 -1
- package/cli/commands/inspect/settle.ts +19 -7
- package/cli/commands/inspect/settling.ts +92 -56
- package/cli/commands/inspect/wait-idle.ts +21 -9
- package/cli/commands/inspect/wait-ready.ts +22 -8
- package/cli/commands/inspect.ts +101 -134
- package/cli/commands/setup.ts +26 -16
- package/cli/commands/upgrade.ts +53 -9
- package/cli/desktop-companion.ts +1 -1
- package/cli/drivers/playwright-sim-host.ts +22 -13
- package/cli/drivers/playwright.ts +3 -3
- package/cli/main.ts +14 -5
- package/cli/outbound-endpoints.ts +1 -9
- package/detox/index.ts +6 -1
- 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 +3 -3
- package/dist-lib/bridge-contract.mjs +3 -3
- package/dist-lib/bundle-channel.cjs +322 -274
- 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 +322 -45
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +69 -33
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +3650 -3528
- 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 +327 -123
- 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 +10 -5
- 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 +49 -47
- 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 +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- 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 +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +23 -17
- package/dist-lib/sdk.cjs +99 -61
- package/dist-lib/sdk.mjs +97 -61
- package/dist-lib/skills.cjs +2959 -2780
- package/dist-lib/swift-compile.cjs +26 -12
- package/dist-lib/swift-compile.mjs +26 -12
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +2 -2
- package/dist-lib/swift-submit.mjs +2 -2
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +66 -31
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/kotlin/AppSources/android/view/Window.kt +2 -0
- package/kotlin/AppSources/dev/peach/app/PeachApp.kt +1 -1
- package/kotlin/ComposeSources/PeachComposeHost.kt +35 -9
- package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +2 -2
- package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +9 -6
- package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +4 -4
- package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +2 -2
- package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +22 -8
- package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +0 -3
- package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +10 -9
- package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +6 -5
- package/kotlin/fixtures/import-drawing/Drawing.d.kt.ts +7 -0
- package/kotlin/fixtures/import-drawing/Drawing.kt +101 -0
- package/kotlin/fixtures/import-drawing/ImportDrawing.native.tsx +18 -0
- package/kotlin/fixtures/import-drawing/README.md +34 -0
- package/kotlin/fixtures/import-drawing/app/_layout.tsx +4 -0
- package/kotlin/fixtures/import-drawing/app/index+spa.tsx +1 -0
- package/kotlin/fixtures/import-drawing/package.json +23 -0
- package/kotlin/fixtures/import-drawing/tsconfig.json +20 -0
- package/kotlin/fixtures/import-drawing/vite.config.ts +20 -0
- package/kotlin/fixtures/import-scroll/ImportNested.native.tsx +10 -0
- package/kotlin/fixtures/import-scroll/Nested.d.kt.ts +4 -0
- package/kotlin/fixtures/import-scroll/README.md +15 -0
- package/kotlin/fixtures/import-scroll/app/nested+spa.tsx +1 -0
- package/kotlin/fixtures/import-values/ImportValues.native.tsx +26 -0
- package/kotlin/fixtures/import-values/README.md +6 -1
- package/kotlin/fixtures/import-values/Values.d.kt.ts +4 -0
- package/kotlin/fixtures/import-values/Values.kt +18 -0
- package/kotlin/tools/expo-types.main.kts +17 -5
- package/package.json +2 -1
- package/scripts/dev-server-scanner.ts +33 -23
- package/src/assetCatalogContract.ts +7 -0
- package/src/bridge-contract.ts +21 -3
- package/src/connect.ts +18 -5
- package/src/desktop-host-delivery.ts +33 -3
- package/src/expo-swift-host.ts +6 -3
- package/src/host/bridge-host.ts +2 -0
- package/src/host/fetch-proxy-transport.ts +8 -0
- package/src/kotlin-host.ts +11 -5
- package/src/native-firebase-config.ts +95 -54
- package/src/process-output.ts +16 -0
- package/src/sdk.ts +6 -1
- package/src/sim-client.ts +6 -1
- package/src/swift-compile.ts +36 -15
- package/src/swift-distribution.lock.json +8 -8
- package/src/swift-standin-emit.ts +10 -1
- package/src/swift-standins.ts +21 -7
- package/src/swift-submit.ts +6 -1
- /package/kotlin/fixtures/{kotlin-app-variants → import-scroll}/Nested.kt +0 -0
|
@@ -76,13 +76,25 @@ for (path in args) {
|
|
|
76
76
|
type.typeArgumentsAsTypes.singleOrNull()?.text ?: ""
|
|
77
77
|
}
|
|
78
78
|
if (composeView != null && !klass.hasModifier(KtTokens.ABSTRACT_KEYWORD)) {
|
|
79
|
-
views += "{\"name\":${quote(name)},\"hidden\":$isHidden,\"props\":${quote(composeView)}}"
|
|
79
|
+
views += "{\"name\":${quote(name)},\"hidden\":$isHidden,\"props\":${quote(composeView)},\"appContext\":true}"
|
|
80
80
|
return
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
val isAndroidView = !klass.hasModifier(KtTokens.ABSTRACT_KEYWORD) && klass.superTypeListEntries.any {
|
|
83
|
+
val ref = it.typeReference?.text?.substringAfterLast('.')
|
|
84
|
+
ref == "ExpoView" || ref == "View" || ref == "FrameLayout" || ref == "LinearLayout" || ref == "ViewGroup"
|
|
85
|
+
}
|
|
86
|
+
if (isAndroidView) {
|
|
87
|
+
// native selects exact public constructor signatures, preferring Context/AppContext.
|
|
88
|
+
val constructors = listOfNotNull(klass.primaryConstructor) + klass.secondaryConstructors
|
|
89
|
+
val signatures = constructors.filter { !hidden(it) }.map { ctor ->
|
|
90
|
+
ctor.valueParameters.map { it.typeReference?.text?.substringAfterLast('.') }
|
|
91
|
+
}
|
|
92
|
+
val takesAppContext = when {
|
|
93
|
+
listOf("Context", "AppContext") in signatures -> true
|
|
94
|
+
listOf("Context") in signatures -> false
|
|
95
|
+
else -> return
|
|
96
|
+
}
|
|
97
|
+
views += "{\"name\":${quote(name)},\"hidden\":$isHidden,\"props\":\"\",\"appContext\":$takesAppContext}"
|
|
86
98
|
return
|
|
87
99
|
}
|
|
88
100
|
if (klass.superTypeListEntries.any { it.typeReference?.text?.substringAfterLast('.') == "ComposeProps" }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peachsim",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1463",
|
|
4
4
|
"description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
|
|
5
5
|
"author": "Tamagui LLC",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -370,6 +370,7 @@
|
|
|
370
370
|
"pngjs": "catalog:",
|
|
371
371
|
"rollup": "4.62.2",
|
|
372
372
|
"systemjs": "6.15.1",
|
|
373
|
+
"tsconfck": "3.1.6",
|
|
373
374
|
"typescript": "7.1.0-dev.20261003.1",
|
|
374
375
|
"vite": "8.3.3",
|
|
375
376
|
"yaml": "^2.8.3"
|
|
@@ -861,38 +861,48 @@ export async function probePort(
|
|
|
861
861
|
// going through scanDevServers. wait for the optional in-repo registry here
|
|
862
862
|
// too, or a cold CLI process can build the known app's bundle url before its
|
|
863
863
|
// runtime config is available.
|
|
864
|
-
await loadOptionalDemoApps()
|
|
864
|
+
const demoApps = await loadOptionalDemoApps()
|
|
865
865
|
const probeHostname = await tcpPing(port)
|
|
866
866
|
if (!probeHostname) return null
|
|
867
867
|
|
|
868
|
+
// the owner lists a scanned server's project, and its cwd picks an in-repo
|
|
869
|
+
// demo app's runtime config. a direct probe on a published install has
|
|
870
|
+
// neither use, so it skips the lookups, which shell out to lsof on macOS.
|
|
871
|
+
// when they run, they overlap the http probes, which cost about as much.
|
|
868
872
|
const suppliedPid = listeningProcess?.pid
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
873
|
+
const ownerPromise = (async (): Promise<Pick<DiscoveredServer, 'cwd' | 'pid'>> => {
|
|
874
|
+
if (!listeningProcess && demoApps.length === 0) return {}
|
|
875
|
+
const ownerPid =
|
|
876
|
+
suppliedPid && suppliedPid > 0 ? suppliedPid : await resolveListeningPid(port)
|
|
877
|
+
const ownerCwd = ownerPid ? await resolveProcessCwd(ownerPid) : null
|
|
878
|
+
return {
|
|
879
|
+
...(ownerPid ? { pid: ownerPid } : {}),
|
|
880
|
+
...(ownerCwd ? { cwd: ownerCwd } : {}),
|
|
881
|
+
}
|
|
882
|
+
})()
|
|
876
883
|
|
|
877
884
|
const onePath = `/node_modules/one/metro-entry.bundle?platform=ios&dev=true`
|
|
878
885
|
|
|
879
886
|
// wave 1 — safe parallel probes. none of these mutate metro state.
|
|
880
|
-
const [sootsimRes, statusRes, manifestRes, expoRes, swiftRes, oneScanRes] =
|
|
887
|
+
const [[sootsimRes, statusRes, manifestRes, expoRes, swiftRes, oneScanRes], owner] =
|
|
881
888
|
await Promise.all([
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
889
|
+
Promise.all([
|
|
890
|
+
knownNonPatched.has(port)
|
|
891
|
+
? Promise.resolve(null)
|
|
892
|
+
: httpGet(probeHostname, port, '/__peachsim/'),
|
|
893
|
+
httpGet(probeHostname, port, '/status'),
|
|
894
|
+
knownOne.has(port)
|
|
895
|
+
? Promise.resolve(null)
|
|
896
|
+
: httpGet(probeHostname, port, '/', 'GET', MANIFEST_TIMEOUT_MS, {
|
|
897
|
+
'expo-platform': 'ios',
|
|
898
|
+
}),
|
|
899
|
+
knownNonExpo.has(port)
|
|
900
|
+
? Promise.resolve(null)
|
|
901
|
+
: httpGet(probeHostname, port, '/_expo/status'),
|
|
902
|
+
httpGet(probeHostname, port, SWIFT_DISCOVERY_PATH),
|
|
903
|
+
httpGet(probeHostname, port, '/__server-scan', 'GET', MANIFEST_TIMEOUT_MS),
|
|
904
|
+
]),
|
|
905
|
+
ownerPromise,
|
|
896
906
|
])
|
|
897
907
|
|
|
898
908
|
// the One plugin advertises its exact native entry, including replacement
|
|
@@ -47,6 +47,13 @@ export type SwiftImageResources = Record<string, Record<string, SwiftImageVarian
|
|
|
47
47
|
// an app catalog color resolves by appearance or names a system color.
|
|
48
48
|
export type AppCatalogColor = { light: string; dark: string } | { system: string }
|
|
49
49
|
|
|
50
|
+
// the premultiplied sRGB rendition SwiftUI passes to a shader image argument.
|
|
51
|
+
export interface SfSymbolShaderImage {
|
|
52
|
+
readonly width: number
|
|
53
|
+
readonly height: number
|
|
54
|
+
readonly pixels: Uint8Array
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
// UIImage's ordered monochrome layers and unrounded content bearings.
|
|
51
58
|
export interface SfSymbolImageDrawing {
|
|
52
59
|
referencePointSize: number
|
package/src/bridge-contract.ts
CHANGED
|
@@ -351,8 +351,12 @@ export interface SemanticReadyProbe {
|
|
|
351
351
|
flag: unknown
|
|
352
352
|
at: number
|
|
353
353
|
nodes: number
|
|
354
|
+
// ms the counted tree had held its shape when the count was read; 0 when
|
|
355
|
+
// the reader cannot tell
|
|
356
|
+
treeQuietMs: number
|
|
354
357
|
targets: number
|
|
355
358
|
liveFrameActive: boolean
|
|
359
|
+
paintedAppContent: boolean
|
|
356
360
|
browserSurfaceCount: number
|
|
357
361
|
liveFrameChannels: number
|
|
358
362
|
liveFramePublishes: number
|
|
@@ -378,9 +382,15 @@ export const HEADLESS_TENANT_FETCH_HUNG_MARKER = 'headless tenant fetch hung'
|
|
|
378
382
|
|
|
379
383
|
export function readyProbeHasContent(
|
|
380
384
|
probe: Pick<SemanticReadyProbe, 'nodes' | 'targets'> &
|
|
381
|
-
Partial<
|
|
385
|
+
Partial<
|
|
386
|
+
Pick<
|
|
387
|
+
SemanticReadyProbe,
|
|
388
|
+
'liveFrameActive' | 'browserSurfaceCount' | 'paintedAppContent'
|
|
389
|
+
>
|
|
390
|
+
>,
|
|
382
391
|
): boolean {
|
|
383
392
|
return (
|
|
393
|
+
probe.paintedAppContent === true ||
|
|
384
394
|
(probe.browserSurfaceCount ?? 0) > 0 ||
|
|
385
395
|
probe.liveFrameActive ||
|
|
386
396
|
probe.targets > 0 ||
|
|
@@ -390,10 +400,18 @@ export function readyProbeHasContent(
|
|
|
390
400
|
|
|
391
401
|
export function readyProbeHasTargetContent(
|
|
392
402
|
probe: Pick<SemanticReadyProbe, 'targets'> &
|
|
393
|
-
Partial<
|
|
403
|
+
Partial<
|
|
404
|
+
Pick<
|
|
405
|
+
SemanticReadyProbe,
|
|
406
|
+
'liveFrameActive' | 'browserSurfaceCount' | 'paintedAppContent'
|
|
407
|
+
>
|
|
408
|
+
>,
|
|
394
409
|
): boolean {
|
|
395
410
|
return (
|
|
396
|
-
|
|
411
|
+
probe.paintedAppContent === true ||
|
|
412
|
+
(probe.browserSurfaceCount ?? 0) > 0 ||
|
|
413
|
+
probe.liveFrameActive ||
|
|
414
|
+
probe.targets > 0
|
|
397
415
|
)
|
|
398
416
|
}
|
|
399
417
|
|
package/src/connect.ts
CHANGED
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
resolveCloudSessionForParsed,
|
|
10
10
|
} from '../cli/cloud-client'
|
|
11
11
|
import { readCurrentSimId } from '../cli/current-sim'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
createBridge,
|
|
14
|
+
createBridgeFromParsed,
|
|
15
|
+
resolveBridgeWorld,
|
|
16
|
+
type BridgeSimInfo,
|
|
17
|
+
type ParsedBridgeCliArgs,
|
|
18
|
+
} from '../cli/ws-bridge'
|
|
13
19
|
import { SimClient, SimConnectError } from './sim-client'
|
|
14
20
|
|
|
15
21
|
export interface ConnectOptions {
|
|
@@ -72,15 +78,22 @@ export async function connect(options: ConnectOptions = {}): Promise<SimClient>
|
|
|
72
78
|
})
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
const port = options.port ?? resolveBridgeWorld().bridgePort
|
|
76
81
|
const commandTimeoutMs = options.timeoutMs ?? 15_000
|
|
82
|
+
const parsed = {
|
|
83
|
+
...target,
|
|
84
|
+
wsPort: options.port ?? resolveBridgeWorld().bridgePort,
|
|
85
|
+
explicitPort: options.port !== undefined,
|
|
86
|
+
commandTimeoutMs,
|
|
87
|
+
positional: [],
|
|
88
|
+
} satisfies ParsedBridgeCliArgs
|
|
77
89
|
|
|
78
90
|
// discovery socket: list sims and pick the target. closed before the pinned
|
|
79
91
|
// client socket is opened so a failed connect never leaks a connection.
|
|
80
|
-
const discovery =
|
|
92
|
+
const discovery = createBridgeFromParsed(parsed)
|
|
81
93
|
let picked: BridgeSimInfo
|
|
82
94
|
try {
|
|
83
|
-
|
|
95
|
+
const sims = await discovery.listSims()
|
|
96
|
+
picked = pickSim(sims, options.sim, parsed.wsPort)
|
|
84
97
|
} catch (error) {
|
|
85
98
|
discovery.close()
|
|
86
99
|
if (error instanceof SimConnectError) throw error
|
|
@@ -91,7 +104,7 @@ export async function connect(options: ConnectOptions = {}): Promise<SimClient>
|
|
|
91
104
|
|
|
92
105
|
// pinned socket: every command targets exactly the picked sim, immune to the
|
|
93
106
|
// CLI's saved-pin re-resolution.
|
|
94
|
-
const bridge = createBridge(
|
|
107
|
+
const bridge = createBridge(parsed.wsPort, {
|
|
95
108
|
commandTimeoutMs,
|
|
96
109
|
cliLabel: 'sdk',
|
|
97
110
|
simId: picked.id,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// peach's binding of the shared runtime-delivery machinery for the native
|
|
2
2
|
// desktop host. the host releases on its own (packages/peach-desktop's Native
|
|
3
|
-
// Desktop Release workflow) and
|
|
3
|
+
// Desktop Release workflow) and explicit installs download and verify each version
|
|
4
4
|
// into ~/.peach/desktop/versions/<version>/, exactly as `peach runtime install` manages
|
|
5
5
|
// the engine runtime. on macOS the active version is at /Applications/Peach.app,
|
|
6
6
|
// where Spotlight finds it and the CLI, the fleet installer and the app's own
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// its active version under ~/.peach and bundles node and software vulkan.
|
|
10
10
|
|
|
11
11
|
import { spawnSync } from 'node:child_process'
|
|
12
|
+
import { createHash } from 'node:crypto'
|
|
12
13
|
import fs from 'node:fs'
|
|
13
14
|
import os from 'node:os'
|
|
14
15
|
import path from 'node:path'
|
|
@@ -185,6 +186,32 @@ export function activeDesktopHostApp(): string | null {
|
|
|
185
186
|
return process.platform === 'darwin' && placedHostVersion() ? MAC_HOST_APP : null
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/** protocol 2 binds port zero before publishing the host origin and evaluates
|
|
190
|
+
* in its realms over node ipc for the process that started it. the receipt
|
|
191
|
+
* travels inside the checksummed archive and signed mac bundle. bind its
|
|
192
|
+
* declaration to the entry bytes so a receipt cannot bless another source. */
|
|
193
|
+
export function hostEntrySupportsAutomaticPort(hostEntry: string): boolean {
|
|
194
|
+
try {
|
|
195
|
+
const contract: unknown = JSON.parse(
|
|
196
|
+
fs.readFileSync(
|
|
197
|
+
path.join(path.dirname(hostEntry), 'headless-contract.json'),
|
|
198
|
+
'utf8',
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
return (
|
|
202
|
+
typeof contract === 'object' &&
|
|
203
|
+
contract !== null &&
|
|
204
|
+
'protocol' in contract &&
|
|
205
|
+
contract.protocol === 2 &&
|
|
206
|
+
'entrySha256' in contract &&
|
|
207
|
+
contract.entrySha256 ===
|
|
208
|
+
createHash('sha256').update(fs.readFileSync(hostEntry)).digest('hex')
|
|
209
|
+
)
|
|
210
|
+
} catch {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
188
215
|
/** the installed standalone host command; the driver appends its connection arguments. */
|
|
189
216
|
export function headlessHostCommand(): string[] | null {
|
|
190
217
|
const platform = desktopHostPlatform()
|
|
@@ -196,13 +223,16 @@ export function headlessHostCommand(): string[] | null {
|
|
|
196
223
|
if (!app) return null
|
|
197
224
|
const launcher = path.join(app, 'Contents/Frameworks/node')
|
|
198
225
|
const host = path.join(app, 'Contents/Resources/node/host.mjs')
|
|
199
|
-
// releases before the bundled launcher have no headless host
|
|
200
|
-
|
|
226
|
+
// releases before the bundled launcher have no headless host, and releases
|
|
227
|
+
// before the automatic-port bind cannot serve `--port 0`; use the browser
|
|
228
|
+
if (!fs.existsSync(launcher) || !hostEntrySupportsAutomaticPort(host)) return null
|
|
201
229
|
return [launcher, '--experimental-vm-modules', host]
|
|
202
230
|
}
|
|
203
231
|
const version = placedHostVersion()
|
|
204
232
|
if (!version) return null
|
|
205
233
|
const dir = desktopHostDir(version)
|
|
234
|
+
// a placed host that predates the automatic-port bind upgrades like a missing one
|
|
235
|
+
if (!hostEntrySupportsAutomaticPort(path.join(dir, 'node/host.mjs'))) return null
|
|
206
236
|
return [
|
|
207
237
|
path.join(dir, 'bin/node'),
|
|
208
238
|
'--experimental-vm-modules',
|
package/src/expo-swift-host.ts
CHANGED
|
@@ -353,14 +353,17 @@ export async function compileExpoSwiftModules(
|
|
|
353
353
|
!(error instanceof SwiftMissingModulesError)
|
|
354
354
|
)
|
|
355
355
|
throw error
|
|
356
|
+
// a source error or a module peach does not provide holds until the
|
|
357
|
+
// stamp changes; retrying either relinks every namespace and rewrites
|
|
358
|
+
// the linked package, so no reload would ever reuse its artifact.
|
|
359
|
+
const missing = error instanceof SwiftMissingModulesError
|
|
356
360
|
if (
|
|
357
|
-
sourceErrors.length &&
|
|
358
|
-
sourceErrors.length === errors.length &&
|
|
361
|
+
((sourceErrors.length && sourceErrors.length === errors.length) || missing) &&
|
|
359
362
|
sourceStamp(packagePath, module.root) === stamps.get(unit)
|
|
360
363
|
) {
|
|
361
364
|
failedCompilations.set(packagePath, {
|
|
362
365
|
stamp: stamps.get(unit)!,
|
|
363
|
-
diagnostic: sourceErrors.join('\n'),
|
|
366
|
+
diagnostic: missing ? diagnostic : sourceErrors.join('\n'),
|
|
364
367
|
})
|
|
365
368
|
}
|
|
366
369
|
failures.set(
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
SOOTSIM_BRIDGE_SIM_CLOSE_REASON,
|
|
17
17
|
} from '../bridge-constants.ts'
|
|
18
18
|
import { getCliVersion } from '../cli-version.ts'
|
|
19
|
+
import { activeDesktopHostApp } from '../desktop-host-delivery.ts'
|
|
19
20
|
import { handleExpoSwiftRequest } from '../expo-swift-host.ts'
|
|
20
21
|
import {
|
|
21
22
|
activeRuntimeDir as getActiveRuntimeDir,
|
|
@@ -361,6 +362,7 @@ export function injectSharedConfigIntoHtml(
|
|
|
361
362
|
`<script>window.__sootsimSharedConfig=${payload};` +
|
|
362
363
|
bridgePortTag +
|
|
363
364
|
contrastOriginTag +
|
|
365
|
+
`window.__peachDesktopHostInstalled=${activeDesktopHostApp() !== null};` +
|
|
364
366
|
`window.__sootsimCliVersion=${JSON.stringify(getCliVersion())};</script>`
|
|
365
367
|
const html = data.toString('utf8')
|
|
366
368
|
if (html.includes('<head>')) return html.replace('<head>', `<head>${tag}`)
|
|
@@ -82,10 +82,18 @@ async function createIdentity(): Promise<Omit<FetchTransportIdentity, 'port'>> {
|
|
|
82
82
|
export async function startFetchProxyTransport(identityPath: string) {
|
|
83
83
|
const identity = readIdentity(identityPath) ?? { ...(await createIdentity()), port: 0 }
|
|
84
84
|
const sessions = new Set<ServerHttp2Session>()
|
|
85
|
+
// every open stream holds its queued response data against maxSessionMemory
|
|
86
|
+
// (megabytes), a few hundred kilobytes once a body outgrows the peer's
|
|
87
|
+
// window. past that cap bun answers new streams with GOAWAY
|
|
88
|
+
// ENHANCE_YOUR_CALM, failing every request on the session, or leaves them
|
|
89
|
+
// unanswered. the default 10 MB covers about 40 such streams, while the
|
|
90
|
+
// native host's undici adopts the advertised 256 as its own limit, so the cap
|
|
91
|
+
// covers a megabyte for each stream it admits.
|
|
85
92
|
const server = createSecureServer(
|
|
86
93
|
{
|
|
87
94
|
cert: identity.cert,
|
|
88
95
|
key: identity.key,
|
|
96
|
+
maxSessionMemory: 256,
|
|
89
97
|
settings: { maxConcurrentStreams: 256 },
|
|
90
98
|
},
|
|
91
99
|
(req, res) => {
|
package/src/kotlin-host.ts
CHANGED
|
@@ -343,7 +343,7 @@ interface ExpoTypesFile {
|
|
|
343
343
|
hiddenParameter: boolean
|
|
344
344
|
}[]
|
|
345
345
|
// Expo views; compose views also name their props class
|
|
346
|
-
views: { name: string; hidden: boolean; props: string }[]
|
|
346
|
+
views: { name: string; hidden: boolean; props: string; appContext: boolean }[]
|
|
347
347
|
// ComposeProps classes with their public member properties
|
|
348
348
|
props: { name: string; hidden: boolean; states: string[] }[]
|
|
349
349
|
}
|
|
@@ -433,10 +433,16 @@ function readExpoTypes(output: string): ExpoTypesFile[] {
|
|
|
433
433
|
!isRecord(view) ||
|
|
434
434
|
typeof view.name !== 'string' ||
|
|
435
435
|
typeof view.hidden !== 'boolean' ||
|
|
436
|
-
typeof view.props !== 'string'
|
|
436
|
+
typeof view.props !== 'string' ||
|
|
437
|
+
typeof view.appContext !== 'boolean'
|
|
437
438
|
)
|
|
438
439
|
throw new Error('expo-types printed an invalid view')
|
|
439
|
-
return {
|
|
440
|
+
return {
|
|
441
|
+
name: view.name,
|
|
442
|
+
hidden: view.hidden,
|
|
443
|
+
props: view.props,
|
|
444
|
+
appContext: view.appContext,
|
|
445
|
+
}
|
|
440
446
|
}),
|
|
441
447
|
props: row.props.map((entry: unknown) => {
|
|
442
448
|
if (
|
|
@@ -463,7 +469,7 @@ function scanExpoTypes(
|
|
|
463
469
|
sources: string[],
|
|
464
470
|
): Promise<ExpoTypesFile[]> {
|
|
465
471
|
const candidates = sources.filter((file) =>
|
|
466
|
-
/\bRecord\b|\benum\s+class\b|\bExpoComposeView\b|\bExpoView\b|\bComposeProps\b/.test(
|
|
472
|
+
/\bRecord\b|\benum\s+class\b|\bExpoComposeView\b|\bExpoView\b|\bComposeProps\b|:\s*(?:[A-Za-z0-9_.]+\.)?(?:View|FrameLayout|LinearLayout|ViewGroup)\b/.test(
|
|
467
473
|
fs.readFileSync(file, 'utf8'),
|
|
468
474
|
),
|
|
469
475
|
)
|
|
@@ -560,7 +566,7 @@ function expoTypeSources(files: ExpoTypesFile[]): {
|
|
|
560
566
|
.filter((view) => !view.hidden)
|
|
561
567
|
.map(
|
|
562
568
|
(view) =>
|
|
563
|
-
` dev.peach.expo.PeachExpoTypes.view(${view.name}::class, ${view.props ? `${view.props}::class` : 'null'}) { context, appContext -> ${view.name}(context, appContext) }`,
|
|
569
|
+
` dev.peach.expo.PeachExpoTypes.view(${view.name}::class, ${view.props ? `${view.props}::class` : 'null'}) { context, appContext -> ${view.name}(${view.appContext ? 'context, appContext' : 'context'}) }`,
|
|
564
570
|
),
|
|
565
571
|
...file.props
|
|
566
572
|
.filter((entry) => !entry.hidden)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// read native default initialization inputs before the guest Firebase SDK starts.
|
|
2
2
|
// expose only FirebaseOptions, never a partner-specific extra configuration.
|
|
3
|
-
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
4
4
|
import fs from 'node:fs'
|
|
5
5
|
import os from 'node:os'
|
|
6
6
|
import path from 'node:path'
|
|
@@ -8,13 +8,18 @@ import { isRecord } from './is-record'
|
|
|
8
8
|
import { parsePlistSource } from './plist'
|
|
9
9
|
import { listXcodeAppTargets, readXcodeAppTarget } from './xcode-project'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
// expo's synchronous ESM/TypeScript loader requires Node's module evaluator.
|
|
12
|
+
// a private result file keeps authored console output off the configuration wire.
|
|
13
|
+
const EXPO_CONFIG_SCRIPT = `const { createRequire } = require('node:module');
|
|
14
|
+
const { writeFileSync } = require('node:fs');
|
|
15
|
+
const { join } = require('node:path');
|
|
16
|
+
const root = process.argv[1];
|
|
17
|
+
const { getConfig } = createRequire(join(root, 'package.json'))('@expo/config');
|
|
18
|
+
const result = JSON.stringify(getConfig(root, { skipSDKVersionRequirement: true }).exp);
|
|
19
|
+
if (Buffer.byteLength(result) > 1024 * 1024) throw new Error('Expo native configuration exceeds 1 MiB.');
|
|
20
|
+
writeFileSync(process.argv[2], result, { mode: 0o600 });`
|
|
21
|
+
|
|
22
|
+
function hasExpoConfig(projectRoot: string) {
|
|
18
23
|
const appJsonPath = path.join(projectRoot, 'app.json')
|
|
19
24
|
const appJson: unknown = fs.existsSync(appJsonPath)
|
|
20
25
|
? JSON.parse(fs.readFileSync(appJsonPath, 'utf8'))
|
|
@@ -25,53 +30,89 @@ export function readNativeFirebaseConfig(
|
|
|
25
30
|
[appJson.ios, appJson.android].some(
|
|
26
31
|
(value) => isRecord(value) && typeof value.googleServicesFile === 'string',
|
|
27
32
|
))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'app.config.ts',
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
33
|
+
return (
|
|
34
|
+
staticExpo ||
|
|
35
|
+
['app.config.js', 'app.config.ts', 'app.config.cjs', 'app.config.mjs'].some((name) =>
|
|
36
|
+
fs.existsSync(path.join(projectRoot, name)),
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function expoConfigArgs(projectRoot: string, resultPath: string) {
|
|
42
|
+
return [
|
|
43
|
+
'--input-type=commonjs',
|
|
44
|
+
'-e',
|
|
45
|
+
EXPO_CONFIG_SCRIPT,
|
|
46
|
+
path.resolve(projectRoot),
|
|
47
|
+
resultPath,
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readExpoConfigResult(resultPath: string, status: number | null, stderr: string) {
|
|
52
|
+
if (status !== 0)
|
|
53
|
+
throw new Error(stderr || 'Expo native configuration evaluation failed.')
|
|
54
|
+
const resolved: unknown = JSON.parse(fs.readFileSync(resultPath, 'utf8'))
|
|
55
|
+
if (!isRecord(resolved)) throw new Error('Expo native configuration is unavailable.')
|
|
56
|
+
return resolved
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resultDirectory = () => fs.mkdtempSync(path.join(os.tmpdir(), 'peach-expo-config-'))
|
|
60
|
+
|
|
61
|
+
function readExpoConfig(projectRoot: string): Record<string, unknown> {
|
|
62
|
+
if (!hasExpoConfig(projectRoot)) return {}
|
|
63
|
+
const directory = resultDirectory()
|
|
64
|
+
const resultPath = path.join(directory, 'config.json')
|
|
65
|
+
try {
|
|
66
|
+
const child = spawnSync('node', expoConfigArgs(projectRoot, resultPath), {
|
|
67
|
+
cwd: projectRoot,
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
70
|
+
timeout: 30_000,
|
|
71
|
+
maxBuffer: 1024 * 1024,
|
|
72
|
+
})
|
|
73
|
+
if (child.error) throw child.error
|
|
74
|
+
return readExpoConfigResult(resultPath, child.status, child.stderr)
|
|
75
|
+
} finally {
|
|
76
|
+
fs.rmSync(directory, { recursive: true, force: true })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// the same evaluation without blocking, so a caller can overlap it with other
|
|
81
|
+
// startup work and hand the result to readNativeFirebaseConfig.
|
|
82
|
+
export async function readExpoConfigAsync(
|
|
83
|
+
projectRoot: string,
|
|
84
|
+
): Promise<Record<string, unknown>> {
|
|
85
|
+
if (!hasExpoConfig(projectRoot)) return {}
|
|
86
|
+
const directory = resultDirectory()
|
|
87
|
+
const resultPath = path.join(directory, 'config.json')
|
|
88
|
+
try {
|
|
89
|
+
const child = spawn('node', expoConfigArgs(projectRoot, resultPath), {
|
|
90
|
+
cwd: projectRoot,
|
|
91
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
92
|
+
timeout: 30_000,
|
|
93
|
+
})
|
|
94
|
+
let stderr = ''
|
|
95
|
+
child.stderr.setEncoding('utf8')
|
|
96
|
+
child.stderr.on('data', (chunk: string) => {
|
|
97
|
+
stderr += chunk
|
|
98
|
+
})
|
|
99
|
+
const status = await new Promise<number | null>((resolve, reject) => {
|
|
100
|
+
child.once('error', reject)
|
|
101
|
+
child.once('close', resolve)
|
|
102
|
+
})
|
|
103
|
+
return readExpoConfigResult(resultPath, status, stderr)
|
|
104
|
+
} finally {
|
|
105
|
+
fs.rmSync(directory, { recursive: true, force: true })
|
|
74
106
|
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function readNativeFirebaseConfig(
|
|
110
|
+
projectRoot: string,
|
|
111
|
+
platforms: readonly string[] = ['ios', 'android'],
|
|
112
|
+
iosTarget?: string,
|
|
113
|
+
config: Record<string, unknown> = readExpoConfig(projectRoot),
|
|
114
|
+
) {
|
|
115
|
+
const result: Record<string, unknown> = {}
|
|
75
116
|
const ios = config.ios
|
|
76
117
|
if (
|
|
77
118
|
platforms.includes('ios') &&
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// wait for both pipes before forcing an exit with live handles.
|
|
2
|
+
// console writers must use these streams rather than a separate runtime sink.
|
|
3
|
+
export async function exitWithFlushedOutput(code: number): Promise<never> {
|
|
4
|
+
await Promise.all(
|
|
5
|
+
[process.stdout, process.stderr].map(
|
|
6
|
+
(stream) =>
|
|
7
|
+
new Promise<void>((resolve, reject) => {
|
|
8
|
+
stream.end((error?: Error | null) => {
|
|
9
|
+
if (error) reject(error)
|
|
10
|
+
else resolve()
|
|
11
|
+
})
|
|
12
|
+
}),
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
process.exit(code)
|
|
16
|
+
}
|
package/src/sdk.ts
CHANGED
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
export * from './sim-client'
|
|
9
9
|
export * from '../cli/commands/inspect/core'
|
|
10
10
|
export * from '../cli/commands/inspect/actions'
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
IdleObservationError,
|
|
13
|
+
RuntimeReadinessUnsupportedError,
|
|
14
|
+
waitForSootsimIdle,
|
|
15
|
+
waitReadyAndSettle,
|
|
16
|
+
} from '../cli/commands/inspect/settling'
|
|
12
17
|
export type {
|
|
13
18
|
SettledWaitReady,
|
|
14
19
|
WaitForSootsimIdleOptions,
|
package/src/sim-client.ts
CHANGED
|
@@ -358,7 +358,12 @@ export class SimClient implements WsBridge, InspectBridge {
|
|
|
358
358
|
return status
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
/**
|
|
361
|
+
/**
|
|
362
|
+
* wait for layout/animation/transition settle after an interaction.
|
|
363
|
+
* a still-busy guest returns settled false with its own elapsed time.
|
|
364
|
+
* a failed idle read throws IdleObservationError with the host elapsed
|
|
365
|
+
* time and the original failure.
|
|
366
|
+
*/
|
|
362
367
|
settle(
|
|
363
368
|
opts: { maxMs?: number; strict?: boolean } = {},
|
|
364
369
|
): Promise<WaitForSootsimIdleResult> {
|