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