peachsim 0.1.1625 → 0.1.1640
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +15 -2
- package/cli/app-config.ts +18 -6
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +284 -20
- package/cli/bridge-flow-runner.ts +66 -45
- package/cli/cloud-client.ts +60 -40
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +30 -189
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +91 -50
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +118 -48
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +14 -3
- package/cli/commands/upload.ts +93 -48
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +103 -45
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +3 -2
- package/cli/outbound-endpoints.ts +20 -6
- package/cli/pause-aware-timeout.ts +106 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +37 -19
- package/detox/attempt-events.cjs +66 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +269 -170
- package/detox/motion-change.cjs +206 -21
- package/detox/motion-change.d.cts +39 -2
- 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 +8 -7
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +13 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +11887 -7900
- 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 +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +1379 -725
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +9341 -5462
- package/dist-lib/expo-swift-metadata.cjs +11 -7
- package/dist-lib/expo-swift-metadata.mjs +11 -7
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +50204 -7258
- 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 +3541 -448
- 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 +2931 -104
- package/dist-lib/native-app-identity-project.cjs +289 -49
- package/dist-lib/native-app-identity-project.mjs +289 -49
- package/dist-lib/native-app-identity.cjs +77 -26
- package/dist-lib/native-app-identity.mjs +76 -26
- 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 +6 -2
- package/dist-lib/react-native-host-modules.mjs +6 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +72201 -26208
- package/dist-lib/swift-compile.cjs +1007 -140
- package/dist-lib/swift-compile.mjs +1049 -174
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +13 -5
- package/dist-lib/swift-submit.mjs +11 -5
- package/dist-lib/swift-toolchain.cjs +423 -33
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8628 -5268
- package/dist-lib/vite.cjs +2859 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/backend-origin.ts +13 -6
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +196 -49
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +17 -6
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +27 -6
- package/src/native-app-identity.ts +91 -4
- package/src/native-firebase-config.ts +209 -13
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +431 -0
- package/src/react-native-host-modules.ts +8 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +918 -109
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +24 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +180 -55
- package/src/swift-icu.ts +27 -13
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +821 -27
- package/src/swift-objc-standin.ts +148 -14
- package/src/swift-package-manifest.ts +60 -7
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +154 -81
- package/src/swift-standin-emit.ts +96 -15
- package/src/swift-standins.ts +530 -116
- package/src/swift-submit.ts +13 -5
- package/src/swift-toolchain.ts +500 -42
- package/src/vite-plugin.ts +12 -1
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1640 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -185,6 +185,8 @@ function enter(node, scope) {
|
|
|
185
185
|
const name = identifierName(id);
|
|
186
186
|
if (name !== void 0 && isNode(init))
|
|
187
187
|
declare(target, name, { kind: "value", init, scope });
|
|
188
|
+
else if (name !== void 0 && init === null)
|
|
189
|
+
declare(target, name, { kind: "uninitialized" });
|
|
188
190
|
else declareOpaque(target, patternNames(id));
|
|
189
191
|
}
|
|
190
192
|
return scope;
|
|
@@ -254,12 +256,18 @@ function collectInitData(root) {
|
|
|
254
256
|
immediate: /* @__PURE__ */ new Map(),
|
|
255
257
|
writes: [],
|
|
256
258
|
reassigned: /* @__PURE__ */ new Set(),
|
|
259
|
+
assigned: /* @__PURE__ */ new Map(),
|
|
257
260
|
free: /* @__PURE__ */ new Map()
|
|
258
261
|
};
|
|
259
262
|
visit(root, newScope(void 0, true), collection);
|
|
260
263
|
for (const { name, scope, value } of collection.writes) {
|
|
261
264
|
const binding = lookup(scope, name);
|
|
262
|
-
if (binding
|
|
265
|
+
if (binding?.kind === "uninitialized")
|
|
266
|
+
collection.assigned.set(
|
|
267
|
+
binding,
|
|
268
|
+
collection.assigned.has(binding) || !value ? null : { value, scope }
|
|
269
|
+
);
|
|
270
|
+
else if (binding) collection.reassigned.add(binding);
|
|
263
271
|
else
|
|
264
272
|
collection.free.set(
|
|
265
273
|
name,
|
|
@@ -320,6 +328,10 @@ function resolveObject(expression, scope, collection, depth) {
|
|
|
320
328
|
return free ? resolveObject(free.value, free.scope, collection, depth + 1) : void 0;
|
|
321
329
|
}
|
|
322
330
|
if (collection.reassigned.has(binding)) return void 0;
|
|
331
|
+
if (binding.kind === "uninitialized") {
|
|
332
|
+
const assigned = collection.assigned.get(binding);
|
|
333
|
+
return assigned ? resolveObject(assigned.value, assigned.scope, collection, depth + 1) : void 0;
|
|
334
|
+
}
|
|
323
335
|
if (binding.kind === "value")
|
|
324
336
|
return resolveObject(binding.init, binding.scope, collection, depth + 1);
|
|
325
337
|
if (binding.kind === "param") {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1640 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
|
|
3
3
|
// src/worklet-executable.ts
|
|
4
4
|
import { parse } from "acorn";
|
|
@@ -160,6 +160,8 @@ function enter(node, scope) {
|
|
|
160
160
|
const name = identifierName(id);
|
|
161
161
|
if (name !== void 0 && isNode(init))
|
|
162
162
|
declare(target, name, { kind: "value", init, scope });
|
|
163
|
+
else if (name !== void 0 && init === null)
|
|
164
|
+
declare(target, name, { kind: "uninitialized" });
|
|
163
165
|
else declareOpaque(target, patternNames(id));
|
|
164
166
|
}
|
|
165
167
|
return scope;
|
|
@@ -229,12 +231,18 @@ function collectInitData(root) {
|
|
|
229
231
|
immediate: /* @__PURE__ */ new Map(),
|
|
230
232
|
writes: [],
|
|
231
233
|
reassigned: /* @__PURE__ */ new Set(),
|
|
234
|
+
assigned: /* @__PURE__ */ new Map(),
|
|
232
235
|
free: /* @__PURE__ */ new Map()
|
|
233
236
|
};
|
|
234
237
|
visit(root, newScope(void 0, true), collection);
|
|
235
238
|
for (const { name, scope, value } of collection.writes) {
|
|
236
239
|
const binding = lookup(scope, name);
|
|
237
|
-
if (binding
|
|
240
|
+
if (binding?.kind === "uninitialized")
|
|
241
|
+
collection.assigned.set(
|
|
242
|
+
binding,
|
|
243
|
+
collection.assigned.has(binding) || !value ? null : { value, scope }
|
|
244
|
+
);
|
|
245
|
+
else if (binding) collection.reassigned.add(binding);
|
|
238
246
|
else
|
|
239
247
|
collection.free.set(
|
|
240
248
|
name,
|
|
@@ -295,6 +303,10 @@ function resolveObject(expression, scope, collection, depth) {
|
|
|
295
303
|
return free ? resolveObject(free.value, free.scope, collection, depth + 1) : void 0;
|
|
296
304
|
}
|
|
297
305
|
if (collection.reassigned.has(binding)) return void 0;
|
|
306
|
+
if (binding.kind === "uninitialized") {
|
|
307
|
+
const assigned = collection.assigned.get(binding);
|
|
308
|
+
return assigned ? resolveObject(assigned.value, assigned.scope, collection, depth + 1) : void 0;
|
|
309
|
+
}
|
|
298
310
|
if (binding.kind === "value")
|
|
299
311
|
return resolveObject(binding.init, binding.scope, collection, depth + 1);
|
|
300
312
|
if (binding.kind === "param") {
|
|
@@ -4,18 +4,21 @@ import androidx.compose.ui.graphics.Brush
|
|
|
4
4
|
import androidx.compose.ui.graphics.Color
|
|
5
5
|
import androidx.compose.ui.graphics.ImageBitmap
|
|
6
6
|
import androidx.compose.ui.graphics.SolidColor
|
|
7
|
-
import androidx.compose.ui.graphics.
|
|
7
|
+
import androidx.compose.ui.graphics.asComposeImageBitmap
|
|
8
8
|
import androidx.compose.ui.graphics.vector.ImageVector
|
|
9
9
|
import kotlin.io.encoding.Base64
|
|
10
10
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
|
11
|
+
import org.jetbrains.skia.Bitmap as SkiaBitmap
|
|
11
12
|
import org.jetbrains.skia.BlendMode
|
|
13
|
+
import org.jetbrains.skia.Canvas
|
|
14
|
+
import org.jetbrains.skia.Codec
|
|
15
|
+
import org.jetbrains.skia.Data
|
|
12
16
|
import org.jetbrains.skia.FilterMipmap
|
|
13
17
|
import org.jetbrains.skia.FilterMode
|
|
14
18
|
import org.jetbrains.skia.Image
|
|
15
19
|
import org.jetbrains.skia.MipmapMode
|
|
16
20
|
import org.jetbrains.skia.Paint
|
|
17
21
|
import org.jetbrains.skia.Rect
|
|
18
|
-
import org.jetbrains.skia.Surface
|
|
19
22
|
|
|
20
23
|
// one file a drawable resource resolves to under a density (ResTable_config's
|
|
21
24
|
// value, 0 for an unqualified directory) and a platform version: a vector the
|
|
@@ -67,28 +70,32 @@ private fun isBetter(a: PeachDrawable, b: PeachDrawable, requested: Int): Boolea
|
|
|
67
70
|
// bitmap drawn for a higher density than the display's is scaled down, its
|
|
68
71
|
// size rounded, through a linear filtered draw with the source blend; one
|
|
69
72
|
// drawn for a lower density, or nodpi, keeps its pixels, as from API 28 the
|
|
70
|
-
// upscale is left to draw time, which BitmapPainter does not do
|
|
73
|
+
// upscale is left to draw time, which BitmapPainter does not do. as
|
|
74
|
+
// ImageDecoder does, the codec decodes into a bitmap and the scale draws into
|
|
75
|
+
// another
|
|
71
76
|
@OptIn(ExperimentalEncodingApi::class)
|
|
72
77
|
internal fun peachDecodeBitmap(drawable: PeachDrawable.Bitmap, targetDensity: Int): ImageBitmap {
|
|
73
78
|
if (drawable.ninePatch)
|
|
74
79
|
throw UnsupportedOperationException("peach kotlin: the nine-patch drawable ${drawable.path} is not drawn yet")
|
|
75
|
-
val
|
|
80
|
+
val decoded = Codec.makeFromData(Data.makeFromBytes(Base64.decode(drawable.data))).readPixels()
|
|
76
81
|
val density = if (drawable.density == DENSITY_DEFAULT) DENSITY_MEDIUM else drawable.density
|
|
77
|
-
if (drawable.density == DENSITY_NONE || density <= targetDensity) return
|
|
82
|
+
if (drawable.density == DENSITY_NONE || density <= targetDensity) return decoded.asComposeImageBitmap()
|
|
78
83
|
val scale = targetDensity.toFloat() / density
|
|
79
|
-
val width = maxOf((
|
|
80
|
-
val height = maxOf((
|
|
81
|
-
val
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Rect.makeWH(
|
|
84
|
+
val width = maxOf((decoded.width * scale + 0.5f).toInt(), 1)
|
|
85
|
+
val height = maxOf((decoded.height * scale + 0.5f).toInt(), 1)
|
|
86
|
+
val scaled = SkiaBitmap().apply { allocN32Pixels(width, height) }
|
|
87
|
+
val canvas = Canvas(scaled)
|
|
88
|
+
canvas.scale(width.toFloat() / decoded.width, height.toFloat() / decoded.height)
|
|
89
|
+
canvas.drawImageRect(
|
|
90
|
+
Image.makeFromBitmap(decoded),
|
|
91
|
+
Rect.makeWH(decoded.width.toFloat(), decoded.height.toFloat()),
|
|
92
|
+
Rect.makeWH(decoded.width.toFloat(), decoded.height.toFloat()),
|
|
87
93
|
FilterMipmap(FilterMode.LINEAR, MipmapMode.NONE),
|
|
88
94
|
Paint().apply { blendMode = BlendMode.SRC },
|
|
89
95
|
false,
|
|
90
96
|
)
|
|
91
|
-
|
|
97
|
+
canvas.close()
|
|
98
|
+
return scaled.asComposeImageBitmap()
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
// ComplexColorCompat.willDraw: a transparent solid color draws nothing
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package androidx.annotation
|
|
2
|
+
|
|
3
|
+
// androidx.annotation's DrawableRes, which only its android artifact declares
|
|
4
|
+
@MustBeDocumented
|
|
5
|
+
@Retention(AnnotationRetention.BINARY)
|
|
6
|
+
@Target(
|
|
7
|
+
AnnotationTarget.FUNCTION,
|
|
8
|
+
AnnotationTarget.PROPERTY_GETTER,
|
|
9
|
+
AnnotationTarget.PROPERTY_SETTER,
|
|
10
|
+
AnnotationTarget.VALUE_PARAMETER,
|
|
11
|
+
AnnotationTarget.FIELD,
|
|
12
|
+
AnnotationTarget.LOCAL_VARIABLE,
|
|
13
|
+
)
|
|
14
|
+
annotation class DrawableRes
|
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '../src/backend-origin.ts'
|
|
26
26
|
import { applyPeachConfigToUrl } from '../src/config.ts'
|
|
27
27
|
import { fetchDevManifest } from '../src/dev-bundle-resolution'
|
|
28
|
+
import { parseExpoManifestBody } from '../src/expo-manifest-body'
|
|
28
29
|
import {
|
|
29
30
|
KOTLIN_DISCOVERY_PATH,
|
|
30
31
|
KOTLIN_GUEST_PATH,
|
|
@@ -969,7 +970,7 @@ function applyManifest(
|
|
|
969
970
|
): DiscoveredServer {
|
|
970
971
|
if (!manifestRes) return result
|
|
971
972
|
try {
|
|
972
|
-
const manifest =
|
|
973
|
+
const manifest = parseExpoManifestBody(manifestRes.body)
|
|
973
974
|
const client = manifest?.extra?.expoClient || manifest?.extra || {}
|
|
974
975
|
if (client.name) result.projectName = client.name
|
|
975
976
|
if (typeof client.primaryColor === 'string') result.primaryColor = client.primaryColor
|
|
@@ -310,9 +310,14 @@ attributes a missing frame to its hop:
|
|
|
310
310
|
- `delivery lag` — vsync postMessage receipt latency above the run's
|
|
311
311
|
best case; jitter here means the shell worker was descheduled or its
|
|
312
312
|
queue was busy.
|
|
313
|
-
- `
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
- `present interval` — intervals between admitted compositor presentations,
|
|
314
|
+
including native frames, while progress remains pending after the preceding
|
|
315
|
+
presentation (`motionPending` in `compositorCadence.frameSeries`). Settled
|
|
316
|
+
admitted frames end a sequence; deferred frames retain the pending gap.
|
|
317
|
+
`paintIntervals` reports the sample count, so zero samples do not prove
|
|
318
|
+
smoothness. Shell work and JavaScript flushes can park while native frames
|
|
319
|
+
continue presenting.
|
|
320
|
+
- `idle breaks` — host gaps >250ms, excluded from host interval statistics.
|
|
316
321
|
|
|
317
322
|
the compositor `compositorHostFrameClock` keeps its historical name and reports
|
|
318
323
|
display/timer ticks consumed by the compositor worker's resilient clock during
|
|
@@ -320,8 +325,9 @@ the acknowledged profile window. `published` is null because renderer-main no
|
|
|
320
325
|
longer publishes compositor frames. sparse consumed ticks with low production
|
|
321
326
|
time identifies worker scheduling starvation. many consumed ticks with sparse
|
|
322
327
|
paints can be legitimate dirty-gating; correlate the frame series before
|
|
323
|
-
treating it as a regression.
|
|
324
|
-
|
|
328
|
+
treating it as a regression. Host and shell delivery statistics come from the
|
|
329
|
+
shell worker; `cadence.paintInterval*` comes from compositor presentation times
|
|
330
|
+
during motion.
|
|
325
331
|
|
|
326
332
|
compare baseline vs contended captures of the same scripted window; also
|
|
327
333
|
compare **painted-frame counts**, the crudest and most robust signal.
|
|
@@ -42,7 +42,8 @@ debugging or `/peach-test` for writing tests.
|
|
|
42
42
|
daemon is down, the dev server port is wrong, or the runtime is missing.
|
|
43
43
|
walk the recovery checklist before reinstalling.
|
|
44
44
|
- **keep one sim for your whole session.** the first `peach open <port>`
|
|
45
|
-
launches
|
|
45
|
+
launches the simulator window: the native desktop app on Apple silicon macOS
|
|
46
|
+
and Linux x64, otherwise Chrome for Testing with an isolated profile. later opens reuse the
|
|
46
47
|
saved sim and navigate it in place. use `peach do reload` when the target
|
|
47
48
|
has not changed:
|
|
48
49
|
|
|
@@ -74,27 +75,23 @@ debugging or `/peach-test` for writing tests.
|
|
|
74
75
|
saved sim to reuse. verify the development server and Peach runtime, then
|
|
75
76
|
retry once after fixing the cause.
|
|
76
77
|
|
|
77
|
-
##
|
|
78
|
+
## the simulator window
|
|
78
79
|
|
|
79
|
-
on Apple silicon macOS and Linux x64,
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
on Apple silicon macOS and Linux x64, `peach open` shows the app in the native
|
|
81
|
+
desktop app, which is faster than the browser. setup installs it, and on a Mac
|
|
82
|
+
the first `peach open` installs it if it is missing. do not pass a driver flag
|
|
83
|
+
for the user's own viewing.
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
peach desktop install
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
desktop
|
|
90
|
-
`peach desktop install --native` on Linux installs the standalone headless host
|
|
91
|
-
instead of the desktop app. Intel Mac and Linux arm64 use the browser.
|
|
92
|
-
`peach open 8081 --driver playwright --headed` explicitly opens a browser window.
|
|
93
|
-
pass the selected driver when opening the app in the steps below; installing
|
|
94
|
-
the desktop app alone does not change the saved window preference.
|
|
85
|
+
the Linux desktop app needs a desktop session; its `.deb` installer uses
|
|
86
|
+
`sudo apt-get install`. `peach desktop install --native` on Linux installs the
|
|
87
|
+
standalone headless host instead of the desktop app. Intel Mac and Linux arm64
|
|
88
|
+
use the browser. `peach open 8081 --driver playwright --headed` explicitly opens
|
|
89
|
+
a browser window. a yellow "GPU disabled" item in the browser simulator's menu
|
|
90
|
+
bar means it is drawing without a GPU and will be slow; move the user to the
|
|
91
|
+
desktop app.
|
|
95
92
|
|
|
96
|
-
the
|
|
97
|
-
|
|
93
|
+
the background service below keeps the bridge ready for either window and is a
|
|
94
|
+
separate optional choice.
|
|
98
95
|
|
|
99
96
|
## first move
|
|
100
97
|
|
package/src/backend-origin.ts
CHANGED
|
@@ -98,12 +98,16 @@ export function isBrowserAssetProxyUrl(value: string): boolean {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// the PeachSim app's host; the backend serves every path on it
|
|
102
|
+
export const PEACHSIM_NATIVE_HOST = 'native.peachsim.com'
|
|
103
|
+
|
|
101
104
|
export function isContrastBackendHost(hostname: string): boolean {
|
|
102
105
|
const normalized = normalizeHostname(hostname)
|
|
103
106
|
return (
|
|
104
107
|
isLoopbackHost(normalized) ||
|
|
105
108
|
normalized === 'contrast.dev' ||
|
|
106
|
-
normalized.endsWith('.contrast.dev')
|
|
109
|
+
normalized.endsWith('.contrast.dev') ||
|
|
110
|
+
normalized === PEACHSIM_NATIVE_HOST
|
|
107
111
|
)
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -115,15 +119,18 @@ export function isContrastBackendOrigin(origin: string): boolean {
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
// a page's `?backend=` names another backend origin, judged by the same rule
|
|
123
|
+
export function resolveContrastBackendOrigin(
|
|
124
|
+
currentOrigin?: string | null,
|
|
125
|
+
search?: string | null,
|
|
126
|
+
): string {
|
|
127
|
+
const origin =
|
|
128
|
+
new URLSearchParams(search ?? '').get('backend')?.trim() || currentOrigin?.trim()
|
|
120
129
|
if (!origin) return DEFAULT_CONTRAST_BACKEND_ORIGIN
|
|
121
130
|
|
|
122
131
|
try {
|
|
123
132
|
const parsed = new URL(origin)
|
|
124
|
-
if (
|
|
125
|
-
return parsed.origin
|
|
126
|
-
}
|
|
133
|
+
if (isContrastBackendHost(parsed.hostname)) return parsed.origin
|
|
127
134
|
} catch {}
|
|
128
135
|
|
|
129
136
|
return DEFAULT_CONTRAST_BACKEND_ORIGIN
|
package/src/bridge-constants.ts
CHANGED
|
@@ -6,6 +6,17 @@ import { PEACH_SIMULATOR_PATH } from './simulator-route'
|
|
|
6
6
|
export const DEFAULT_SOOTSIM_BRIDGE_PORT = 7668
|
|
7
7
|
export const DEFAULT_SOOTSIM_BRIDGE_URL = `http://localhost:${DEFAULT_SOOTSIM_BRIDGE_PORT}/`
|
|
8
8
|
|
|
9
|
+
export function assertPeachPortEnvironment(env: Record<string, unknown>): void {
|
|
10
|
+
for (const [removed, replacement] of [
|
|
11
|
+
['SOOTSIM_SHELL_PORT', 'PEACH_SHELL_PORT'],
|
|
12
|
+
['SOOTSIM_BRIDGE_PORT', 'PEACH_BRIDGE_PORT'],
|
|
13
|
+
] satisfies [string, string][]) {
|
|
14
|
+
if (env[removed] !== undefined) {
|
|
15
|
+
throw new Error(`${removed} has been removed; unset it and use ${replacement}`)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
function parsePort(value: unknown): number | null {
|
|
10
21
|
if (typeof value === 'number') {
|
|
11
22
|
return Number.isInteger(value) && value > 0 ? value : null
|
package/src/bridge-contract.ts
CHANGED
|
@@ -385,39 +385,29 @@ export const SEMANTIC_READY_POLL_MS = 150
|
|
|
385
385
|
|
|
386
386
|
export const HEADLESS_TENANT_FETCH_HUNG_MARKER = 'headless tenant fetch hung'
|
|
387
387
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
)
|
|
388
|
+
type ReadyContentProbe = Pick<SemanticReadyProbe, 'nodes' | 'targets'> &
|
|
389
|
+
Partial<
|
|
390
|
+
Pick<
|
|
391
|
+
SemanticReadyProbe,
|
|
392
|
+
'liveFrameActive' | 'browserSurfaceCount' | 'paintedAppContent' | 'flag'
|
|
393
|
+
>
|
|
394
|
+
>
|
|
395
|
+
|
|
396
|
+
// the first content condition a probe satisfies. a probe without the ready
|
|
397
|
+
// flag counts only targeted content, never the node floor. empty when the
|
|
398
|
+
// probe has none.
|
|
399
|
+
export function readyProbeContentBasis(probe: ReadyContentProbe): string {
|
|
400
|
+
if (probe.paintedAppContent === true) return 'paintedAppContent'
|
|
401
|
+
if ((probe.browserSurfaceCount ?? 0) > 0) return 'browserSurface'
|
|
402
|
+
if (probe.liveFrameActive) return 'liveFrame'
|
|
403
|
+
if (probe.targets > 0) return 'targets'
|
|
404
|
+
if (probe.flag === true && probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR)
|
|
405
|
+
return 'nodeFloor'
|
|
406
|
+
return ''
|
|
404
407
|
}
|
|
405
408
|
|
|
406
|
-
export function
|
|
407
|
-
probe:
|
|
408
|
-
Partial<
|
|
409
|
-
Pick<
|
|
410
|
-
SemanticReadyProbe,
|
|
411
|
-
'liveFrameActive' | 'browserSurfaceCount' | 'paintedAppContent'
|
|
412
|
-
>
|
|
413
|
-
>,
|
|
414
|
-
): boolean {
|
|
415
|
-
return (
|
|
416
|
-
probe.paintedAppContent === true ||
|
|
417
|
-
(probe.browserSurfaceCount ?? 0) > 0 ||
|
|
418
|
-
probe.liveFrameActive ||
|
|
419
|
-
probe.targets > 0
|
|
420
|
-
)
|
|
409
|
+
export function readyProbeHasContent(probe: ReadyContentProbe): boolean {
|
|
410
|
+
return readyProbeContentBasis({ ...probe, flag: true }) !== ''
|
|
421
411
|
}
|
|
422
412
|
|
|
423
413
|
// one probe shows a ready app: it has said so with content behind it, or it
|
|
@@ -426,9 +416,7 @@ export function readyProbeHasTargetContent(
|
|
|
426
416
|
// already let the app settle judges a single probe with this alone.
|
|
427
417
|
export function readyProbeIsReady(probe: SemanticReadyProbe): boolean {
|
|
428
418
|
return (
|
|
429
|
-
(probe
|
|
430
|
-
? readyProbeHasContent(probe)
|
|
431
|
-
: readyProbeHasTargetContent(probe)) &&
|
|
419
|
+
readyProbeContentBasis(probe) !== '' &&
|
|
432
420
|
probe.externalReady !== false &&
|
|
433
421
|
!probe.externalError &&
|
|
434
422
|
!probe.loadingText
|
package/src/config.ts
CHANGED
|
@@ -76,6 +76,8 @@ export interface PeachConfig<Settings extends Record<string, any> = Record<strin
|
|
|
76
76
|
runtimeVersion?: string
|
|
77
77
|
// native host identity. omission preserves the Fabric default.
|
|
78
78
|
nativeRenderer?: NativeRenderer
|
|
79
|
+
// developer-owned browser token, restricted to the runtime's origin.
|
|
80
|
+
mapKit?: { token: string }
|
|
79
81
|
modules?: Record<string, ModuleResolution>
|
|
80
82
|
// native stubs keyed by the package they answer. the cli lowers these, with
|
|
81
83
|
// installed stub packages, into the wire slots below before anything runs.
|
|
@@ -95,6 +97,9 @@ export interface PeachConfig<Settings extends Record<string, any> = Record<strin
|
|
|
95
97
|
remap?: Record<string, string>
|
|
96
98
|
}
|
|
97
99
|
settings?: Partial<Settings>
|
|
100
|
+
// the properties a native host hands the root component at launch, the
|
|
101
|
+
// initialProperties of RCTRootView / runApplication's initialProps.
|
|
102
|
+
initialProps?: Record<string, unknown>
|
|
98
103
|
initialState?: {
|
|
99
104
|
authenticated?: boolean
|
|
100
105
|
locale?: string
|
|
@@ -131,6 +136,11 @@ export function isPeachConfig<Settings extends Record<string, any> = Record<stri
|
|
|
131
136
|
(value.nativeRenderer === undefined ||
|
|
132
137
|
value.nativeRenderer === 'paper' ||
|
|
133
138
|
value.nativeRenderer === 'fabric') &&
|
|
139
|
+
(value.mapKit === undefined ||
|
|
140
|
+
(isRecord(value.mapKit) &&
|
|
141
|
+
typeof value.mapKit.token === 'string' &&
|
|
142
|
+
value.mapKit.token.length > 0 &&
|
|
143
|
+
value.mapKit.token === value.mapKit.token.trim())) &&
|
|
134
144
|
(value.runtimeVersion === undefined ||
|
|
135
145
|
(typeof value.runtimeVersion === 'string' &&
|
|
136
146
|
value.runtimeVersion === value.runtimeVersion.trim() &&
|
|
@@ -144,6 +154,7 @@ export function isPeachConfig<Settings extends Record<string, any> = Record<stri
|
|
|
144
154
|
'env',
|
|
145
155
|
'network',
|
|
146
156
|
'settings',
|
|
157
|
+
'initialProps',
|
|
147
158
|
'initialState',
|
|
148
159
|
].every((key) => value[key] === undefined || isRecord(value[key])) &&
|
|
149
160
|
(value.packages === undefined ||
|
|
@@ -232,6 +243,7 @@ export function hasPeachConfig<
|
|
|
232
243
|
if (!config) return false
|
|
233
244
|
return (
|
|
234
245
|
config.nativeRenderer !== undefined ||
|
|
246
|
+
config.mapKit !== undefined ||
|
|
235
247
|
(typeof config.runtimeVersion === 'string' && config.runtimeVersion.length > 0) ||
|
|
236
248
|
hasOwnKeys(config.modules as Record<string, unknown> | undefined) ||
|
|
237
249
|
hasOwnKeys(config.turboModules as Record<string, unknown> | undefined) ||
|
|
@@ -240,6 +252,7 @@ export function hasPeachConfig<
|
|
|
240
252
|
hasOwnKeys(config.env as Record<string, unknown> | undefined) ||
|
|
241
253
|
hasOwnKeys(config.network?.remap as Record<string, unknown> | undefined) ||
|
|
242
254
|
hasOwnKeys(config.settings as Record<string, unknown> | undefined) ||
|
|
255
|
+
hasOwnKeys(config.initialProps) ||
|
|
243
256
|
hasOwnKeys(config.initialState as Record<string, unknown> | undefined)
|
|
244
257
|
)
|
|
245
258
|
}
|
|
@@ -257,6 +270,7 @@ export function mergePeachConfig<
|
|
|
257
270
|
return {
|
|
258
271
|
runtimeVersion: override.runtimeVersion ?? base.runtimeVersion,
|
|
259
272
|
nativeRenderer: override.nativeRenderer ?? base.nativeRenderer,
|
|
273
|
+
mapKit: override.mapKit ?? base.mapKit,
|
|
260
274
|
modules: { ...(base.modules || {}), ...(override.modules || {}) },
|
|
261
275
|
turboModules: { ...(base.turboModules || {}), ...(override.turboModules || {}) },
|
|
262
276
|
nativeModules: {
|
|
@@ -272,6 +286,7 @@ export function mergePeachConfig<
|
|
|
272
286
|
...(base.settings || {}),
|
|
273
287
|
...(override.settings || {}),
|
|
274
288
|
} as Partial<Settings>,
|
|
289
|
+
initialProps: { ...(base.initialProps || {}), ...(override.initialProps || {}) },
|
|
275
290
|
initialState: { ...(base.initialState || {}), ...(override.initialState || {}) },
|
|
276
291
|
}
|
|
277
292
|
}
|
|
@@ -229,9 +229,13 @@ export function headlessHostCommand(): string[] | null {
|
|
|
229
229
|
if (!fs.existsSync(launcher) || !hostEntrySupportsAutomaticPort(host)) return null
|
|
230
230
|
return [launcher, '--experimental-vm-modules', host]
|
|
231
231
|
}
|
|
232
|
-
const
|
|
232
|
+
const pin = desktopHostDelivery().resolvePin()
|
|
233
|
+
if (pin && !/^\d+\.\d+$/.test(pin))
|
|
234
|
+
throw new Error(`invalid native host version '${pin}' (expected <build>.<attempt>)`)
|
|
235
|
+
const version = pin || placedHostVersion()
|
|
233
236
|
if (!version) return null
|
|
234
237
|
const dir = desktopHostDir(version)
|
|
238
|
+
if (!fs.existsSync(path.join(dir, desktopHostExecutable()))) return null
|
|
235
239
|
// a placed host that predates the automatic-port bind upgrades like a missing one
|
|
236
240
|
if (!hostEntrySupportsAutomaticPort(path.join(dir, 'node/host.mjs'))) return null
|
|
237
241
|
return [
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
isLoopbackHost,
|
|
4
4
|
LOOPBACK_PROBE_HOSTNAMES,
|
|
5
5
|
} from './backend-origin'
|
|
6
|
+
import { parseExpoManifestBody } from './expo-manifest-body'
|
|
6
7
|
import {
|
|
7
8
|
alignLoopbackBundleUrlWithProbe,
|
|
8
9
|
METRO_BUNDLE_PATH,
|
|
@@ -223,7 +224,7 @@ async function probeExpoManifest(
|
|
|
223
224
|
|
|
224
225
|
let manifest: unknown
|
|
225
226
|
try {
|
|
226
|
-
manifest =
|
|
227
|
+
manifest = parseExpoManifestBody(body)
|
|
227
228
|
} catch {
|
|
228
229
|
if (answered.headers.get('content-type')?.includes('json'))
|
|
229
230
|
throw new Error(`Metro returned invalid manifest JSON at ${normalizedBaseUrl}/`)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// an Expo packager serves its manifest as JSON, or as the multipart form body
|
|
2
|
+
// Expo Updates defines (older CLIs always, whatever the request accepts) whose
|
|
3
|
+
// `manifest` part carries the same JSON.
|
|
4
|
+
export function parseExpoManifestBody(body: string) {
|
|
5
|
+
const opening = /^(--[^\r\n]+)\r?\n/.exec(body)
|
|
6
|
+
if (!opening) return JSON.parse(body)
|
|
7
|
+
for (const part of body.split(opening[1])) {
|
|
8
|
+
const headerEnd = part.search(/\r?\n\r?\n/)
|
|
9
|
+
if (headerEnd < 0 || !/\bname="manifest"/.test(part.slice(0, headerEnd))) continue
|
|
10
|
+
return JSON.parse(part.slice(headerEnd).trim())
|
|
11
|
+
}
|
|
12
|
+
throw new Error('Expo manifest response has no manifest part')
|
|
13
|
+
}
|