rnxsim 0.0.0 → 0.1.312
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 +21 -0
- package/README.md +512 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +39 -0
- package/detox/expectations.ts +537 -0
- package/detox/gestures.ts +480 -0
- package/detox/index.ts +2118 -0
- package/detox/jest-preset.cjs +51 -0
- package/detox/jest-setup-after-env.cjs +18 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation.ts +43 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-cli/bin.js +15 -0
- package/dist-cli/chunks/agent-NXVDDCFG.js +48 -0
- package/dist-cli/chunks/agent-wrapper-DXJCFKL3.js +17 -0
- package/dist-cli/chunks/app-fonts-W5AJZQJF.js +4 -0
- package/dist-cli/chunks/assert-3LOJEUDT.js +49 -0
- package/dist-cli/chunks/auth-MFTYYPNX.js +12 -0
- package/dist-cli/chunks/beta-GT3NN67B.js +4 -0
- package/dist-cli/chunks/camera-6EAK7CUL.js +22 -0
- package/dist-cli/chunks/chunk-274UD5FD.js +4 -0
- package/dist-cli/chunks/chunk-2SA2VTGP.js +5 -0
- package/dist-cli/chunks/chunk-33FKQH54.js +5 -0
- package/dist-cli/chunks/chunk-37I5AIS7.js +3 -0
- package/dist-cli/chunks/chunk-3D57IBPC.js +4 -0
- package/dist-cli/chunks/chunk-4BXCZHAU.js +4 -0
- package/dist-cli/chunks/chunk-5OX7ANRA.js +5 -0
- package/dist-cli/chunks/chunk-66NF2KNG.js +9 -0
- package/dist-cli/chunks/chunk-6UKJB2VC.js +125 -0
- package/dist-cli/chunks/chunk-7INBNA3U.js +26 -0
- package/dist-cli/chunks/chunk-7VWOSTD5.js +5 -0
- package/dist-cli/chunks/chunk-AY47WKS5.js +23 -0
- package/dist-cli/chunks/chunk-B7KMNHX7.js +4 -0
- package/dist-cli/chunks/chunk-BKNOLP3J.js +4 -0
- package/dist-cli/chunks/chunk-BRIZOR4K.js +28 -0
- package/dist-cli/chunks/chunk-DM6KUAOZ.js +4 -0
- package/dist-cli/chunks/chunk-EUAVBTKU.js +4 -0
- package/dist-cli/chunks/chunk-FTC4M26Q.js +4 -0
- package/dist-cli/chunks/chunk-FWYYCUAZ.js +4 -0
- package/dist-cli/chunks/chunk-G74YIEVS.js +7 -0
- package/dist-cli/chunks/chunk-GZDHEONW.js +507 -0
- package/dist-cli/chunks/chunk-HG7ORJMT.js +5 -0
- package/dist-cli/chunks/chunk-HMDOLCKG.js +15 -0
- package/dist-cli/chunks/chunk-JG6S4RSH.js +4 -0
- package/dist-cli/chunks/chunk-KR5BGMBZ.js +4 -0
- package/dist-cli/chunks/chunk-KZW5WBGU.js +983 -0
- package/dist-cli/chunks/chunk-L24Z3CRS.js +30 -0
- package/dist-cli/chunks/chunk-LDQ7HVCW.js +4 -0
- package/dist-cli/chunks/chunk-MENV7VLD.js +4 -0
- package/dist-cli/chunks/chunk-MNGQ42QE.js +568 -0
- package/dist-cli/chunks/chunk-N6A3SXMR.js +4 -0
- package/dist-cli/chunks/chunk-NLEMQ4PY.js +6 -0
- package/dist-cli/chunks/chunk-NPVGMH2N.js +101 -0
- package/dist-cli/chunks/chunk-OR3XTXEB.js +107 -0
- package/dist-cli/chunks/chunk-PEPUGJVI.js +4 -0
- package/dist-cli/chunks/chunk-Q3RGLERJ.js +142 -0
- package/dist-cli/chunks/chunk-SI3BUTQR.js +4 -0
- package/dist-cli/chunks/chunk-T4FT6CM7.js +4 -0
- package/dist-cli/chunks/chunk-TBMVBI26.js +7 -0
- package/dist-cli/chunks/chunk-TCWQJILS.js +5 -0
- package/dist-cli/chunks/chunk-TGQNQ6XT.js +766 -0
- package/dist-cli/chunks/chunk-TYE3WBSR.js +5 -0
- package/dist-cli/chunks/chunk-U4PN6JOT.js +6 -0
- package/dist-cli/chunks/chunk-VUFRB347.js +4 -0
- package/dist-cli/chunks/chunk-VZBLFMYC.js +10 -0
- package/dist-cli/chunks/chunk-WBT32R6N.js +25 -0
- package/dist-cli/chunks/chunk-XRGEEPJ5.js +38 -0
- package/dist-cli/chunks/chunk-Y2FSZSO7.js +71 -0
- package/dist-cli/chunks/chunk-YRV7HR3P.js +74 -0
- package/dist-cli/chunks/chunk-YZRFF4YA.js +14 -0
- package/dist-cli/chunks/chunk-ZMZTHU7V.js +4 -0
- package/dist-cli/chunks/cleanup-H3GWQB63.js +25 -0
- package/dist-cli/chunks/cli-version-3QPAJ3LC.js +4 -0
- package/dist-cli/chunks/compat-5WG2XJV7.js +20 -0
- package/dist-cli/chunks/config-ANMBVVLL.js +13 -0
- package/dist-cli/chunks/control-NSGNQFJG.js +4 -0
- package/dist-cli/chunks/daemon-JHF4BJ3U.js +4 -0
- package/dist-cli/chunks/debug-CHV2O2KF.js +200 -0
- package/dist-cli/chunks/desktop-FIPIK4CH.js +31 -0
- package/dist-cli/chunks/detox-CVUJP7K6.js +52 -0
- package/dist-cli/chunks/device-6WCUM4W6.js +18 -0
- package/dist-cli/chunks/diagnose-I6EMWEII.js +43 -0
- package/dist-cli/chunks/disk-cleanup-P27CYFTN.js +4 -0
- package/dist-cli/chunks/drivers-HQ6AKZT6.js +4 -0
- package/dist-cli/chunks/film-UOQFFYNF.js +74 -0
- package/dist-cli/chunks/flow-N27MRRNL.js +4 -0
- package/dist-cli/chunks/help-SF6FEGEE.js +4 -0
- package/dist-cli/chunks/hidden-runtime-alias-NJYFSJKZ.js +4 -0
- package/dist-cli/chunks/home-paths-XOV44JEF.js +4 -0
- package/dist-cli/chunks/inspect-CWFRFQTY.js +4 -0
- package/dist-cli/chunks/install-desktop-KLQDNXZQ.js +4 -0
- package/dist-cli/chunks/login-BPFQENCX.js +28 -0
- package/dist-cli/chunks/logout-5A45ZGY7.js +4 -0
- package/dist-cli/chunks/maestro-E4CSDMLM.js +104 -0
- package/dist-cli/chunks/maestro-generate-CKMOJQX2.js +36 -0
- package/dist-cli/chunks/mode-SZYNMIQF.js +26 -0
- package/dist-cli/chunks/optional-demo-registry-FRH4SFVG.js +4 -0
- package/dist-cli/chunks/perf-VXU2XJAX.js +48 -0
- package/dist-cli/chunks/permissions-IRIAOVND.js +13 -0
- package/dist-cli/chunks/record-RY7FPLLH.js +72 -0
- package/dist-cli/chunks/report-issue-KNWXOE53.js +29 -0
- package/dist-cli/chunks/runtime-AR3GJTGK.js +4 -0
- package/dist-cli/chunks/screenshot-command-QLALR6GY.js +159 -0
- package/dist-cli/chunks/screenshot-layers-OQFMFVRL.js +20 -0
- package/dist-cli/chunks/screenshots-capture-6TSRJFGB.js +42 -0
- package/dist-cli/chunks/serve-F54J2NQO.js +44 -0
- package/dist-cli/chunks/setup-7V6UKX4U.js +32 -0
- package/dist-cli/chunks/skills-QQBOSETH.js +50 -0
- package/dist-cli/chunks/storage-TAPMKK5O.js +37 -0
- package/dist-cli/chunks/store-7NX3S43H.js +4 -0
- package/dist-cli/chunks/telemetry-E7BUK6RA.js +4 -0
- package/dist-cli/chunks/timeline-IAM564NA.js +24 -0
- package/dist-cli/chunks/upgrade-CD55QXIX.js +7 -0
- package/dist-cli/chunks/upload-GZ6LIMYM.js +4 -0
- package/dist-cli/chunks/version-3DYJ5AS3.js +6 -0
- package/dist-cli/chunks/web-6NW6YLPI.js +4 -0
- package/dist-cli/chunks/what-happened-CZZDOOVG.js +25 -0
- package/dist-lib/agent-daemon-client.cjs +468 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +725 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +195 -0
- package/dist-lib/backend-origin.cjs +70 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +2863 -0
- package/dist-lib/detox/jest-preset.cjs +51 -0
- package/dist-lib/dev-bundle-resolution.cjs +223 -0
- package/dist-lib/home-paths.cjs +421 -0
- package/dist-lib/host/bridge-host.cjs +5600 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/websocket-proxy.cjs +214 -0
- package/dist-lib/index.cjs +513 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/menu.cjs +620 -0
- package/dist-lib/menu.mjs +581 -0
- package/dist-lib/metro.cjs +297 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
- package/dist-lib/sdk.cjs +2333 -0
- package/dist-lib/sdk.mjs +2240 -0
- package/dist-lib/skills.cjs +23911 -0
- package/dist-lib/vite.cjs +358 -0
- package/package.json +189 -18
- package/scripts/dev-server-scanner.ts +941 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/scripts/postinstall.cjs +3 -0
- package/scripts/preuninstall.cjs +118 -0
- package/skills/contrast/SKILL.md +171 -0
- package/skills/rnx-debug/SKILL.md +425 -0
- package/skills/rnx-perf/SKILL.md +295 -0
- package/skills/rnx-setup/SKILL.md +160 -0
- package/skills/rnx-test/SKILL.md +292 -0
- package/skills/rnx-visual/SKILL.md +122 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +573 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +204 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +171 -0
- package/src/browser-cache.ts +16 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +32 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +134 -0
- package/src/dev-bundle-resolution.ts +236 -0
- package/src/disk-cleanup.ts +466 -0
- package/src/home-paths.ts +552 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2626 -0
- package/src/host/fetch-proxy-handler.ts +397 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/websocket-proxy.ts +210 -0
- package/src/index.ts +12 -705
- package/src/jump-to-source-babel.ts +135 -0
- package/src/menu.ts +734 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +96 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +92 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +228 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +454 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +7 -0
- package/src/settings-bundle.ts +418 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +190 -0
- package/src/vite-plugin.ts +1750 -0
- package/src/worklets-babel.ts +134 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LEGACY_BOTTOM_BEZEL,
|
|
3
|
+
LEGACY_FRAME_RADIUS,
|
|
4
|
+
LEGACY_HOME_BUTTON_RING,
|
|
5
|
+
LEGACY_HOME_BUTTON_SIZE,
|
|
6
|
+
LEGACY_SIDE_BEZEL,
|
|
7
|
+
LEGACY_TOP_BEZEL,
|
|
8
|
+
SIDE_BUTTON_RESERVE,
|
|
9
|
+
} from 'sootsim-engine/ios/chrome-metrics'
|
|
10
|
+
import {
|
|
11
|
+
getDevice,
|
|
12
|
+
resolveDeviceFrameGeometry,
|
|
13
|
+
type DeviceModel,
|
|
14
|
+
} from 'sootsim-engine/settings'
|
|
15
|
+
import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
|
|
16
|
+
|
|
17
|
+
const METALLIC_RING_SHADOW =
|
|
18
|
+
'inset 0 0 0 0.5px #000, inset 0 0 0 2px #757575, inset 0 0 0 5px #212121'
|
|
19
|
+
const FRAME_OUTLINE_BLEED = 1
|
|
20
|
+
|
|
21
|
+
type FrameButtonLayout = {
|
|
22
|
+
side: 'left' | 'right'
|
|
23
|
+
top: number
|
|
24
|
+
height: number
|
|
25
|
+
width: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type LegacyHomeButtonLayout = {
|
|
29
|
+
top: number
|
|
30
|
+
left: number
|
|
31
|
+
size: number
|
|
32
|
+
ring: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FramedScreenshotLayout {
|
|
36
|
+
model: DeviceModel
|
|
37
|
+
renderScale: number
|
|
38
|
+
outerWidth: number
|
|
39
|
+
outerHeight: number
|
|
40
|
+
logicalOuterWidth: number
|
|
41
|
+
logicalOuterHeight: number
|
|
42
|
+
logicalFrameWidth: number
|
|
43
|
+
logicalFrameHeight: number
|
|
44
|
+
logicalFrameLeft: number
|
|
45
|
+
logicalFrameTop: number
|
|
46
|
+
logicalScreenWidth: number
|
|
47
|
+
logicalScreenHeight: number
|
|
48
|
+
logicalScreenLeft: number
|
|
49
|
+
logicalScreenTop: number
|
|
50
|
+
logicalScreenRadius: number
|
|
51
|
+
logicalFrameRadius: number
|
|
52
|
+
frameBackground: string
|
|
53
|
+
frameOutline: string
|
|
54
|
+
metallicRingShadow: string | null
|
|
55
|
+
buttons: FrameButtonLayout[]
|
|
56
|
+
legacyHomeButton: LegacyHomeButtonLayout | null
|
|
57
|
+
showHomeIndicator: boolean
|
|
58
|
+
logicalHomeIndicatorStripHeight: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Exact shell device frame layout for still exports.
|
|
62
|
+
// Mirrors DeviceFrame's bezel/button geometry while intentionally excluding:
|
|
63
|
+
// - the electron simulator top bar
|
|
64
|
+
// - the browser rail / gutter
|
|
65
|
+
// The screen bitmap itself still comes from the raw bridge screenshot.
|
|
66
|
+
export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotLayout {
|
|
67
|
+
const spec = getDevice(model)
|
|
68
|
+
const renderScale = spec.scale
|
|
69
|
+
// legacy = home-button iPhone (SE). the canvas workspace also has no island /
|
|
70
|
+
// home indicator / corner radius, but no status bar, so gate on statusBar > 0
|
|
71
|
+
// to keep it out of the SE shell (mirrors DeviceFrame.isLegacyHardware).
|
|
72
|
+
const isLegacyHardware =
|
|
73
|
+
spec.statusBarHeight > 0 &&
|
|
74
|
+
!spec.dynamicIsland &&
|
|
75
|
+
spec.homeIndicatorHeight === 0 &&
|
|
76
|
+
spec.cornerRadius === 0
|
|
77
|
+
const logicalScreenWidth = spec.width
|
|
78
|
+
const logicalScreenHeight = spec.height
|
|
79
|
+
const modernGeometry = resolveDeviceFrameGeometry(spec)
|
|
80
|
+
const logicalFrameWidth = isLegacyHardware
|
|
81
|
+
? spec.width + LEGACY_SIDE_BEZEL * 2
|
|
82
|
+
: modernGeometry.frameWidth
|
|
83
|
+
const logicalFrameHeight = isLegacyHardware
|
|
84
|
+
? spec.height + LEGACY_TOP_BEZEL + LEGACY_BOTTOM_BEZEL
|
|
85
|
+
: modernGeometry.frameHeight
|
|
86
|
+
const logicalFrameLeft = SIDE_BUTTON_RESERVE
|
|
87
|
+
const logicalFrameTop = FRAME_OUTLINE_BLEED
|
|
88
|
+
const logicalOuterWidth = logicalFrameWidth + logicalFrameLeft * 2
|
|
89
|
+
const logicalOuterHeight = logicalFrameHeight + FRAME_OUTLINE_BLEED * 2
|
|
90
|
+
const logicalScreenLeft =
|
|
91
|
+
logicalFrameLeft + (isLegacyHardware ? LEGACY_SIDE_BEZEL : modernGeometry.screenLeft)
|
|
92
|
+
const logicalScreenTop = isLegacyHardware ? LEGACY_TOP_BEZEL : modernGeometry.screenTop
|
|
93
|
+
const logicalFrameRadius = isLegacyHardware
|
|
94
|
+
? LEGACY_FRAME_RADIUS
|
|
95
|
+
: modernGeometry.frameRadius
|
|
96
|
+
const buttons: FrameButtonLayout[] = [
|
|
97
|
+
{
|
|
98
|
+
side: spec.hardwareButtons.lock.side,
|
|
99
|
+
top: spec.hardwareButtons.lock.top,
|
|
100
|
+
height: spec.hardwareButtons.lock.height,
|
|
101
|
+
width: spec.hardwareButtons.width,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
side: spec.hardwareButtons.ringToggle.side,
|
|
105
|
+
top: spec.hardwareButtons.ringToggle.top,
|
|
106
|
+
height: spec.hardwareButtons.ringToggle.height,
|
|
107
|
+
width: spec.hardwareButtons.width,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
side: spec.hardwareButtons.volumeUp.side,
|
|
111
|
+
top: spec.hardwareButtons.volumeUp.top,
|
|
112
|
+
height: spec.hardwareButtons.volumeUp.height,
|
|
113
|
+
width: spec.hardwareButtons.width,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
side: spec.hardwareButtons.volumeDown.side,
|
|
117
|
+
top: spec.hardwareButtons.volumeDown.top,
|
|
118
|
+
height: spec.hardwareButtons.volumeDown.height,
|
|
119
|
+
width: spec.hardwareButtons.width,
|
|
120
|
+
},
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
model,
|
|
125
|
+
renderScale,
|
|
126
|
+
outerWidth: Math.round(logicalOuterWidth * renderScale),
|
|
127
|
+
outerHeight: Math.round(logicalOuterHeight * renderScale),
|
|
128
|
+
logicalOuterWidth,
|
|
129
|
+
logicalOuterHeight,
|
|
130
|
+
logicalFrameWidth,
|
|
131
|
+
logicalFrameHeight,
|
|
132
|
+
logicalFrameLeft,
|
|
133
|
+
logicalFrameTop,
|
|
134
|
+
logicalScreenWidth,
|
|
135
|
+
logicalScreenHeight,
|
|
136
|
+
logicalScreenLeft,
|
|
137
|
+
logicalScreenTop,
|
|
138
|
+
logicalScreenRadius: isLegacyHardware ? 0 : modernGeometry.screenRadius,
|
|
139
|
+
logicalFrameRadius,
|
|
140
|
+
frameBackground: isLegacyHardware
|
|
141
|
+
? 'linear-gradient(180deg, #1b1c20 0%, #0f1012 42%, #050608 100%)'
|
|
142
|
+
: '#000000',
|
|
143
|
+
frameOutline: isLegacyHardware ? '0 0 0 1px #1f2125' : '0 0 0 1px #333',
|
|
144
|
+
metallicRingShadow: isLegacyHardware ? null : METALLIC_RING_SHADOW,
|
|
145
|
+
buttons,
|
|
146
|
+
legacyHomeButton: isLegacyHardware
|
|
147
|
+
? {
|
|
148
|
+
top:
|
|
149
|
+
LEGACY_TOP_BEZEL +
|
|
150
|
+
spec.height +
|
|
151
|
+
(LEGACY_BOTTOM_BEZEL - LEGACY_HOME_BUTTON_SIZE) / 2,
|
|
152
|
+
left: spec.width / 2 - LEGACY_HOME_BUTTON_SIZE / 2 + LEGACY_SIDE_BEZEL,
|
|
153
|
+
size: LEGACY_HOME_BUTTON_SIZE,
|
|
154
|
+
ring: LEGACY_HOME_BUTTON_RING,
|
|
155
|
+
}
|
|
156
|
+
: null,
|
|
157
|
+
showHomeIndicator: spec.homeIndicatorHeight > 0,
|
|
158
|
+
logicalHomeIndicatorStripHeight: spec.homeIndicatorHeight,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderSideButtonHtml(button: FrameButtonLayout): string {
|
|
163
|
+
const isRight = button.side === 'right'
|
|
164
|
+
const clip = isRight ? 'inset(-3px -3px -3px 0)' : 'inset(-3px 0 -3px -3px)'
|
|
165
|
+
return `
|
|
166
|
+
<div
|
|
167
|
+
aria-hidden="true"
|
|
168
|
+
style="
|
|
169
|
+
position:absolute;
|
|
170
|
+
${isRight ? `right:-${button.width}px;` : `left:-${button.width}px;`}
|
|
171
|
+
top:${button.top}px;
|
|
172
|
+
width:${button.width}px;
|
|
173
|
+
height:${button.height}px;
|
|
174
|
+
background-color:#212121;
|
|
175
|
+
border-top-left-radius:${isRight ? 0 : 1.5}px;
|
|
176
|
+
border-bottom-left-radius:${isRight ? 0 : 1.5}px;
|
|
177
|
+
border-top-right-radius:${isRight ? 1.5 : 0}px;
|
|
178
|
+
border-bottom-right-radius:${isRight ? 1.5 : 0}px;
|
|
179
|
+
box-shadow:
|
|
180
|
+
inset 0 0.5px 0 #616161,
|
|
181
|
+
inset 0 -0.5px 0 #3F3F3F,
|
|
182
|
+
${isRight ? 'inset -0.5px 0 0 #3F3F3F,' : 'inset 0.5px 0 0 #3F3F3F,'}
|
|
183
|
+
0 0 0 0.5px #676767,
|
|
184
|
+
0 0 0 1.5px #000;
|
|
185
|
+
clip-path:${clip};
|
|
186
|
+
pointer-events:none;
|
|
187
|
+
z-index:2;
|
|
188
|
+
"
|
|
189
|
+
></div>
|
|
190
|
+
`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderFrameHtml(
|
|
194
|
+
layout: FramedScreenshotLayout,
|
|
195
|
+
rawImageDataUrl: string,
|
|
196
|
+
): string {
|
|
197
|
+
const buttonsHtml = layout.buttons.map(renderSideButtonHtml).join('')
|
|
198
|
+
|
|
199
|
+
const ringHtml = layout.metallicRingShadow
|
|
200
|
+
? `
|
|
201
|
+
<div
|
|
202
|
+
aria-hidden="true"
|
|
203
|
+
style="
|
|
204
|
+
position:absolute;
|
|
205
|
+
inset:0;
|
|
206
|
+
border-radius:${layout.logicalFrameRadius}px;
|
|
207
|
+
box-shadow:${layout.metallicRingShadow};
|
|
208
|
+
pointer-events:none;
|
|
209
|
+
z-index:10;
|
|
210
|
+
"
|
|
211
|
+
></div>
|
|
212
|
+
`
|
|
213
|
+
: ''
|
|
214
|
+
|
|
215
|
+
const legacyHomeButtonHtml = layout.legacyHomeButton
|
|
216
|
+
? `
|
|
217
|
+
<div
|
|
218
|
+
aria-hidden="true"
|
|
219
|
+
style="
|
|
220
|
+
position:absolute;
|
|
221
|
+
top:${layout.legacyHomeButton.top}px;
|
|
222
|
+
left:${layout.legacyHomeButton.left}px;
|
|
223
|
+
width:${layout.legacyHomeButton.size}px;
|
|
224
|
+
height:${layout.legacyHomeButton.size}px;
|
|
225
|
+
border-radius:50%;
|
|
226
|
+
background:
|
|
227
|
+
radial-gradient(circle at 35% 32%, rgba(62,64,68,0.35) 0%, rgba(17,18,20,0.98) 72%, rgba(8,9,11,1) 100%);
|
|
228
|
+
box-shadow:
|
|
229
|
+
inset 0 0 0 1px rgba(255,255,255,0.06),
|
|
230
|
+
inset 0 0 0 ${layout.legacyHomeButton.ring}px rgba(184,192,204,0.45),
|
|
231
|
+
0 0 0 1px rgba(0,0,0,0.5);
|
|
232
|
+
z-index:3;
|
|
233
|
+
pointer-events:none;
|
|
234
|
+
display:flex;
|
|
235
|
+
align-items:center;
|
|
236
|
+
justify-content:center;
|
|
237
|
+
"
|
|
238
|
+
>
|
|
239
|
+
<div
|
|
240
|
+
style="
|
|
241
|
+
width:16px;
|
|
242
|
+
height:16px;
|
|
243
|
+
border-radius:4px;
|
|
244
|
+
box-shadow:inset 0 0 0 1.5px rgba(188,194,202,0.5);
|
|
245
|
+
"
|
|
246
|
+
></div>
|
|
247
|
+
</div>
|
|
248
|
+
`
|
|
249
|
+
: ''
|
|
250
|
+
|
|
251
|
+
const homeIndicatorHtml = layout.showHomeIndicator
|
|
252
|
+
? `
|
|
253
|
+
<div
|
|
254
|
+
aria-hidden="true"
|
|
255
|
+
style="
|
|
256
|
+
position:absolute;
|
|
257
|
+
left:0;
|
|
258
|
+
right:0;
|
|
259
|
+
bottom:4px;
|
|
260
|
+
height:${layout.logicalHomeIndicatorStripHeight}px;
|
|
261
|
+
display:flex;
|
|
262
|
+
align-items:center;
|
|
263
|
+
justify-content:center;
|
|
264
|
+
pointer-events:none;
|
|
265
|
+
z-index:10;
|
|
266
|
+
"
|
|
267
|
+
>
|
|
268
|
+
<div
|
|
269
|
+
style="
|
|
270
|
+
width:134px;
|
|
271
|
+
height:5px;
|
|
272
|
+
border-radius:2.5px;
|
|
273
|
+
background-color:rgb(150, 150, 150);
|
|
274
|
+
opacity:0.5;
|
|
275
|
+
"
|
|
276
|
+
></div>
|
|
277
|
+
</div>
|
|
278
|
+
`
|
|
279
|
+
: ''
|
|
280
|
+
|
|
281
|
+
return `<!doctype html>
|
|
282
|
+
<html>
|
|
283
|
+
<head>
|
|
284
|
+
<meta charset="utf-8" />
|
|
285
|
+
<style>
|
|
286
|
+
html, body {
|
|
287
|
+
margin: 0;
|
|
288
|
+
width: ${layout.outerWidth}px;
|
|
289
|
+
height: ${layout.outerHeight}px;
|
|
290
|
+
background: transparent;
|
|
291
|
+
}
|
|
292
|
+
body {
|
|
293
|
+
overflow: hidden;
|
|
294
|
+
}
|
|
295
|
+
img {
|
|
296
|
+
display: block;
|
|
297
|
+
width: 100%;
|
|
298
|
+
height: 100%;
|
|
299
|
+
}
|
|
300
|
+
</style>
|
|
301
|
+
</head>
|
|
302
|
+
<body>
|
|
303
|
+
<div
|
|
304
|
+
id="frame-export-root"
|
|
305
|
+
style="
|
|
306
|
+
position:relative;
|
|
307
|
+
width:${layout.outerWidth}px;
|
|
308
|
+
height:${layout.outerHeight}px;
|
|
309
|
+
overflow:hidden;
|
|
310
|
+
background:transparent;
|
|
311
|
+
"
|
|
312
|
+
>
|
|
313
|
+
<div
|
|
314
|
+
id="frame-scale-layer"
|
|
315
|
+
style="
|
|
316
|
+
position:absolute;
|
|
317
|
+
top:0;
|
|
318
|
+
left:0;
|
|
319
|
+
width:${layout.logicalOuterWidth}px;
|
|
320
|
+
height:${layout.logicalOuterHeight}px;
|
|
321
|
+
transform:scale(${layout.renderScale});
|
|
322
|
+
transform-origin:top left;
|
|
323
|
+
overflow:visible;
|
|
324
|
+
"
|
|
325
|
+
>
|
|
326
|
+
<div
|
|
327
|
+
id="frame"
|
|
328
|
+
style="
|
|
329
|
+
position:absolute;
|
|
330
|
+
top:${layout.logicalFrameTop}px;
|
|
331
|
+
left:${layout.logicalFrameLeft}px;
|
|
332
|
+
width:${layout.logicalFrameWidth}px;
|
|
333
|
+
height:${layout.logicalFrameHeight}px;
|
|
334
|
+
border-radius:${layout.logicalFrameRadius}px;
|
|
335
|
+
box-sizing:border-box;
|
|
336
|
+
overflow:visible;
|
|
337
|
+
background:${layout.frameBackground};
|
|
338
|
+
box-shadow:${layout.frameOutline};
|
|
339
|
+
"
|
|
340
|
+
>
|
|
341
|
+
${buttonsHtml}
|
|
342
|
+
${ringHtml}
|
|
343
|
+
${legacyHomeButtonHtml}
|
|
344
|
+
<div
|
|
345
|
+
id="screen"
|
|
346
|
+
style="
|
|
347
|
+
position:absolute;
|
|
348
|
+
top:${layout.logicalScreenTop}px;
|
|
349
|
+
left:${layout.logicalScreenLeft - layout.logicalFrameLeft}px;
|
|
350
|
+
width:${layout.logicalScreenWidth}px;
|
|
351
|
+
height:${layout.logicalScreenHeight}px;
|
|
352
|
+
border-radius:${layout.logicalScreenRadius}px;
|
|
353
|
+
overflow:hidden;
|
|
354
|
+
background:#000;
|
|
355
|
+
"
|
|
356
|
+
>
|
|
357
|
+
<img src="${rawImageDataUrl}" alt="" />
|
|
358
|
+
${homeIndicatorHtml}
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
</body>
|
|
364
|
+
</html>`
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function composeFramedScreenshot(
|
|
368
|
+
rawPng: Buffer,
|
|
369
|
+
model: DeviceModel,
|
|
370
|
+
): Promise<Buffer> {
|
|
371
|
+
const { chromium } = await import('playwright')
|
|
372
|
+
const browser = await launchReapedChrome((options) => chromium.launch(options), {
|
|
373
|
+
headless: true,
|
|
374
|
+
})
|
|
375
|
+
try {
|
|
376
|
+
const composer = await createFramedScreenshotComposer(browser)
|
|
377
|
+
try {
|
|
378
|
+
return await composer.compose(rawPng, model)
|
|
379
|
+
} finally {
|
|
380
|
+
await composer.close()
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
await browser.close()
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
type BrowserLike = {
|
|
388
|
+
newContext: (opts: {
|
|
389
|
+
viewport: { width: number; height: number }
|
|
390
|
+
deviceScaleFactor: number
|
|
391
|
+
}) => Promise<{
|
|
392
|
+
newPage: () => Promise<{
|
|
393
|
+
setContent: (html: string, opts: { waitUntil: 'load' }) => Promise<void>
|
|
394
|
+
waitForTimeout: (ms: number) => Promise<void>
|
|
395
|
+
screenshot: (opts: {
|
|
396
|
+
type: 'png'
|
|
397
|
+
clip: { x: number; y: number; width: number; height: number }
|
|
398
|
+
omitBackground: true
|
|
399
|
+
}) => Promise<Buffer>
|
|
400
|
+
}>
|
|
401
|
+
close: () => Promise<void>
|
|
402
|
+
}>
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
type CachedFramePage = {
|
|
406
|
+
layout: FramedScreenshotLayout
|
|
407
|
+
context: Awaited<ReturnType<BrowserLike['newContext']>>
|
|
408
|
+
page: Awaited<ReturnType<Awaited<ReturnType<BrowserLike['newContext']>>['newPage']>>
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export async function createFramedScreenshotComposer(browser: BrowserLike) {
|
|
412
|
+
const cache = new Map<DeviceModel, CachedFramePage>()
|
|
413
|
+
|
|
414
|
+
async function getCachedPage(model: DeviceModel): Promise<CachedFramePage> {
|
|
415
|
+
const existing = cache.get(model)
|
|
416
|
+
if (existing) return existing
|
|
417
|
+
const layout = getFramedScreenshotLayout(model)
|
|
418
|
+
const context = await browser.newContext({
|
|
419
|
+
viewport: { width: layout.outerWidth, height: layout.outerHeight },
|
|
420
|
+
deviceScaleFactor: 1,
|
|
421
|
+
})
|
|
422
|
+
const page = await context.newPage()
|
|
423
|
+
const created = { layout, context, page }
|
|
424
|
+
cache.set(model, created)
|
|
425
|
+
return created
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
async compose(rawPng: Buffer, model: DeviceModel): Promise<Buffer> {
|
|
430
|
+
const cached = await getCachedPage(model)
|
|
431
|
+
const rawImageDataUrl = `data:image/png;base64,${rawPng.toString('base64')}`
|
|
432
|
+
await cached.page.setContent(renderFrameHtml(cached.layout, rawImageDataUrl), {
|
|
433
|
+
waitUntil: 'load',
|
|
434
|
+
})
|
|
435
|
+
await cached.page.waitForTimeout(20)
|
|
436
|
+
return (await cached.page.screenshot({
|
|
437
|
+
type: 'png',
|
|
438
|
+
clip: {
|
|
439
|
+
x: 0,
|
|
440
|
+
y: 0,
|
|
441
|
+
width: cached.layout.outerWidth,
|
|
442
|
+
height: cached.layout.outerHeight,
|
|
443
|
+
},
|
|
444
|
+
omitBackground: true,
|
|
445
|
+
})) as Buffer
|
|
446
|
+
},
|
|
447
|
+
async close(): Promise<void> {
|
|
448
|
+
for (const entry of cache.values()) {
|
|
449
|
+
await entry.context.close()
|
|
450
|
+
}
|
|
451
|
+
cache.clear()
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
+
import { tmpdir } from 'os'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
|
|
5
|
+
import { runFlowPlayback } from '../../cli/commands/flow'
|
|
6
|
+
import {
|
|
7
|
+
composeMarketingScreenshots,
|
|
8
|
+
type MarketingBrowserLike,
|
|
9
|
+
type ComposeResult,
|
|
10
|
+
type ComposeSlideSource,
|
|
11
|
+
} from './compose'
|
|
12
|
+
import { createFramedScreenshotComposer } from './frame-compose'
|
|
13
|
+
import { applyOrgDeckToPlan, type OrgDeckFrame } from './org-deck'
|
|
14
|
+
import { loadScreenshotsPlan, type NormalizedScreenshotsPlan } from './schema'
|
|
15
|
+
|
|
16
|
+
export interface RunScreenshotsOverrides {
|
|
17
|
+
appTarget?: string | null
|
|
18
|
+
deviceModel?: string | null
|
|
19
|
+
simId?: string | null
|
|
20
|
+
captureOnly?: boolean
|
|
21
|
+
composeOnly?: boolean
|
|
22
|
+
// copy/colors pulled from the team's org deck to overlay onto the plan's
|
|
23
|
+
// compose slides (sootsim screenshot appstore --org … --repo …).
|
|
24
|
+
orgDeckFrames?: OrgDeckFrame[] | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CaptureResult {
|
|
28
|
+
manifestPath: string
|
|
29
|
+
rawDir: string
|
|
30
|
+
framedDir: string
|
|
31
|
+
rawFiles: string[]
|
|
32
|
+
framedFiles: string[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ScreenshotsRunResult {
|
|
36
|
+
plan: NormalizedScreenshotsPlan
|
|
37
|
+
capture: CaptureResult
|
|
38
|
+
compose: ComposeResult | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveCapturePathMode(plan: NormalizedScreenshotsPlan): 'plan' | 'flow' {
|
|
42
|
+
if (plan.capture.pathMode === 'plan' || plan.capture.pathMode === 'flow') {
|
|
43
|
+
return plan.capture.pathMode
|
|
44
|
+
}
|
|
45
|
+
return plan.capture.fromDir ? 'flow' : 'plan'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildCaptureFlowArgs(
|
|
49
|
+
plan: NormalizedScreenshotsPlan,
|
|
50
|
+
overrides: RunScreenshotsOverrides,
|
|
51
|
+
): string[] {
|
|
52
|
+
if (!plan.capture.flowPath) {
|
|
53
|
+
throw new Error('capture flow path is required to build flow args')
|
|
54
|
+
}
|
|
55
|
+
const args = [plan.capture.flowPath]
|
|
56
|
+
const pathMode = resolveCapturePathMode(plan)
|
|
57
|
+
args.push('--screenshots', plan.capture.rawDir)
|
|
58
|
+
if (pathMode === 'flow') {
|
|
59
|
+
args.push('--screenshot-paths', 'flow')
|
|
60
|
+
}
|
|
61
|
+
const appTarget = overrides.appTarget ?? plan.appTarget
|
|
62
|
+
const deviceModel = overrides.deviceModel ?? plan.deviceModel
|
|
63
|
+
const simId = overrides.simId ?? plan.capture.simId
|
|
64
|
+
if (appTarget) args.push('--url', appTarget)
|
|
65
|
+
if (deviceModel) args.push('--device', deviceModel)
|
|
66
|
+
if (simId) args.push('--sim', simId)
|
|
67
|
+
if (plan.capture.openInNewSim) args.push('--new')
|
|
68
|
+
return args
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveSlideRawPath(
|
|
72
|
+
plan: NormalizedScreenshotsPlan,
|
|
73
|
+
screenshot: string,
|
|
74
|
+
): string {
|
|
75
|
+
return path.isAbsolute(screenshot)
|
|
76
|
+
? screenshot
|
|
77
|
+
: path.join(plan.capture.rawDir, screenshot)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveCaptureManifestPath(plan: NormalizedScreenshotsPlan): string {
|
|
81
|
+
return path.join(plan.capture.outDir, 'manifest.json')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function runCaptureStage(
|
|
85
|
+
plan: NormalizedScreenshotsPlan,
|
|
86
|
+
overrides: RunScreenshotsOverrides,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
if (!plan.capture.flowPath) return
|
|
89
|
+
mkdirSync(plan.capture.rawDir, { recursive: true })
|
|
90
|
+
const args = buildCaptureFlowArgs(plan, overrides)
|
|
91
|
+
const exitCode = await runFlowPlayback(args)
|
|
92
|
+
if (exitCode !== 0) {
|
|
93
|
+
throw new Error(`flow capture failed with exit code ${exitCode}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function buildFramedSources(
|
|
98
|
+
plan: NormalizedScreenshotsPlan,
|
|
99
|
+
browserOverride?: MarketingBrowserLike,
|
|
100
|
+
): Promise<Map<string, string>> {
|
|
101
|
+
const sources = new Map<string, string>()
|
|
102
|
+
mkdirSync(plan.capture.framedDir, { recursive: true })
|
|
103
|
+
const browser =
|
|
104
|
+
browserOverride ??
|
|
105
|
+
(await (async () => {
|
|
106
|
+
const { chromium } = await import('playwright')
|
|
107
|
+
return launchReapedChrome((options) => chromium.launch(options), {
|
|
108
|
+
headless: true,
|
|
109
|
+
})
|
|
110
|
+
})())
|
|
111
|
+
try {
|
|
112
|
+
const composer = await createFramedScreenshotComposer(browser)
|
|
113
|
+
try {
|
|
114
|
+
for (const slide of plan.compose.slides) {
|
|
115
|
+
const rawPath = resolveSlideRawPath(plan, slide.screenshot)
|
|
116
|
+
const rawBuffer = readFileSync(rawPath)
|
|
117
|
+
const framedBuffer = await composer.compose(rawBuffer, plan.compose.frame.style)
|
|
118
|
+
const framedPath = path.join(plan.capture.framedDir, `${slide.assetKey}.png`)
|
|
119
|
+
writeFileSync(framedPath, framedBuffer)
|
|
120
|
+
sources.set(slide.id, framedPath)
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
await composer.close()
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
if (!browserOverride && typeof browser.close === 'function') {
|
|
127
|
+
await browser.close()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return sources
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function writeCaptureManifest(
|
|
134
|
+
plan: NormalizedScreenshotsPlan,
|
|
135
|
+
framedSources: Map<string, string>,
|
|
136
|
+
effectiveDeviceModel: string | null,
|
|
137
|
+
): CaptureResult {
|
|
138
|
+
const rawFiles: string[] = []
|
|
139
|
+
const framedFiles: string[] = []
|
|
140
|
+
mkdirSync(plan.capture.outDir, { recursive: true })
|
|
141
|
+
const manifestPath = resolveCaptureManifestPath(plan)
|
|
142
|
+
const slides = plan.compose.slides.map((slide) => {
|
|
143
|
+
const rawPath = resolveSlideRawPath(plan, slide.screenshot)
|
|
144
|
+
const framedPath = framedSources.get(slide.id) ?? null
|
|
145
|
+
rawFiles.push(rawPath)
|
|
146
|
+
if (framedPath) framedFiles.push(framedPath)
|
|
147
|
+
return {
|
|
148
|
+
id: slide.id,
|
|
149
|
+
screenshot: slide.screenshot,
|
|
150
|
+
rawPath,
|
|
151
|
+
framedPath,
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
writeFileSync(
|
|
155
|
+
manifestPath,
|
|
156
|
+
JSON.stringify(
|
|
157
|
+
{
|
|
158
|
+
generatedAt: new Date().toISOString(),
|
|
159
|
+
deviceModel: effectiveDeviceModel,
|
|
160
|
+
mode: plan.capture.mode,
|
|
161
|
+
rawDir: plan.capture.rawDir,
|
|
162
|
+
framedDir: plan.capture.framedDir,
|
|
163
|
+
slides,
|
|
164
|
+
},
|
|
165
|
+
null,
|
|
166
|
+
2,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
return {
|
|
170
|
+
manifestPath,
|
|
171
|
+
rawDir: plan.capture.rawDir,
|
|
172
|
+
framedDir: plan.capture.framedDir,
|
|
173
|
+
rawFiles,
|
|
174
|
+
framedFiles,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolveComposeSources(
|
|
179
|
+
plan: NormalizedScreenshotsPlan,
|
|
180
|
+
framedSources: Map<string, string>,
|
|
181
|
+
): ComposeSlideSource[] {
|
|
182
|
+
return plan.compose.slides.map((slide) => ({
|
|
183
|
+
slide,
|
|
184
|
+
imagePath:
|
|
185
|
+
plan.compose.frame.show === true
|
|
186
|
+
? (framedSources.get(slide.id) ?? resolveSlideRawPath(plan, slide.screenshot))
|
|
187
|
+
: resolveSlideRawPath(plan, slide.screenshot),
|
|
188
|
+
}))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function runScreenshotsPlan(
|
|
192
|
+
planPath: string,
|
|
193
|
+
overrides: RunScreenshotsOverrides = {},
|
|
194
|
+
): Promise<ScreenshotsRunResult> {
|
|
195
|
+
const plan = loadScreenshotsPlan(planPath)
|
|
196
|
+
if (overrides.orgDeckFrames && overrides.orgDeckFrames.length > 0) {
|
|
197
|
+
applyOrgDeckToPlan(plan, overrides.orgDeckFrames)
|
|
198
|
+
}
|
|
199
|
+
const effectiveDeviceModel = overrides.deviceModel ?? plan.deviceModel
|
|
200
|
+
if (!overrides.composeOnly) {
|
|
201
|
+
await runCaptureStage(plan, overrides)
|
|
202
|
+
}
|
|
203
|
+
const needsFramedSources =
|
|
204
|
+
plan.capture.mode !== 'raw' ||
|
|
205
|
+
(!overrides.captureOnly && plan.compose.frame.show === true)
|
|
206
|
+
const needsSharedBrowser = needsFramedSources || !overrides.captureOnly
|
|
207
|
+
const sharedBrowser = needsSharedBrowser
|
|
208
|
+
? await (async () => {
|
|
209
|
+
const { chromium } = await import('playwright')
|
|
210
|
+
return launchReapedChrome((options) => chromium.launch(options), {
|
|
211
|
+
headless: true,
|
|
212
|
+
})
|
|
213
|
+
})()
|
|
214
|
+
: null
|
|
215
|
+
try {
|
|
216
|
+
const framedSources = needsFramedSources
|
|
217
|
+
? await buildFramedSources(plan, sharedBrowser ?? undefined)
|
|
218
|
+
: new Map()
|
|
219
|
+
const capture = writeCaptureManifest(plan, framedSources, effectiveDeviceModel)
|
|
220
|
+
|
|
221
|
+
if (overrides.captureOnly) {
|
|
222
|
+
return { plan, capture, compose: null }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const composeSources = resolveComposeSources(plan, framedSources)
|
|
226
|
+
const compose = await composeMarketingScreenshots(
|
|
227
|
+
plan.compose,
|
|
228
|
+
composeSources,
|
|
229
|
+
sharedBrowser ?? undefined,
|
|
230
|
+
)
|
|
231
|
+
return { plan, capture, compose }
|
|
232
|
+
} finally {
|
|
233
|
+
if (sharedBrowser && typeof sharedBrowser.close === 'function') {
|
|
234
|
+
await sharedBrowser.close()
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function runScreenshotsPlanFromExistingRaw(
|
|
240
|
+
planPath: string,
|
|
241
|
+
rawDir: string,
|
|
242
|
+
): Promise<ScreenshotsRunResult> {
|
|
243
|
+
const plan = loadScreenshotsPlan(planPath)
|
|
244
|
+
plan.capture.rawDir = rawDir
|
|
245
|
+
plan.capture.flowPath = null
|
|
246
|
+
plan.capture.fromDir = rawDir
|
|
247
|
+
const framedDir = path.join(tmpdir(), 'sootsim-screenshots-framed')
|
|
248
|
+
plan.capture.framedDir = framedDir
|
|
249
|
+
const framedSources = await buildFramedSources(plan)
|
|
250
|
+
const capture = writeCaptureManifest(plan, framedSources, plan.deviceModel)
|
|
251
|
+
const compose = await composeMarketingScreenshots(
|
|
252
|
+
plan.compose,
|
|
253
|
+
resolveComposeSources(plan, framedSources),
|
|
254
|
+
)
|
|
255
|
+
return { plan, capture, compose }
|
|
256
|
+
}
|