@replohq/sandbox-runtime 1.8.0 → 1.9.0

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.
@@ -0,0 +1 @@
1
+ export declare function PageCaptureResponder(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { startPageCaptureResponder } from "./startPageCaptureResponder";
4
+ function startResponder() {
5
+ return startPageCaptureResponder(window);
6
+ }
7
+ function PageCaptureResponder() {
8
+ return /* @__PURE__ */ jsx("template", { ref: startResponder });
9
+ }
10
+ export {
11
+ PageCaptureResponder
12
+ };
@@ -0,0 +1 @@
1
+ export { base64UrlToBytes, bytesToBase64Url, constantTimeEqual, getHmacSha256Hex, randomBase64Url, randomUUID, sha256Hex } from '../../../chunk-LIPH4O4F.mjs';
@@ -1,4 +1,4 @@
1
- import { randomUUID } from '../../../chunk-SGKQ5HGT.mjs';
1
+ import { randomUUID } from '../../../chunk-LIPH4O4F.mjs';
2
2
  import { isNumber_default } from '../../../chunk-FLYEIRFK.mjs';
3
3
  import '../../../chunk-G3S6ZNPF.mjs';
4
4
 
@@ -0,0 +1,34 @@
1
+ /** Site Builder asks the cross-origin preview iframe to draw its own first screen; the site's runtime replies with a JPEG. */
2
+ export declare const PAGE_CAPTURE_REQUEST = "replo:page-capture:request";
3
+ export declare const PAGE_CAPTURE_RESPONSE = "replo:page-capture:response";
4
+ export interface PageCaptureRequestMessage {
5
+ type: typeof PAGE_CAPTURE_REQUEST;
6
+ requestId: string;
7
+ width: number;
8
+ height: number;
9
+ }
10
+ export type PageCaptureResponseMessage = {
11
+ type: typeof PAGE_CAPTURE_RESPONSE;
12
+ requestId: string;
13
+ jpeg: ArrayBuffer;
14
+ ms: number;
15
+ } | {
16
+ type: typeof PAGE_CAPTURE_RESPONSE;
17
+ requestId: string;
18
+ jpeg: null;
19
+ error: string;
20
+ };
21
+ export declare function isPageCaptureRequestMessage(value: unknown): value is PageCaptureRequestMessage;
22
+ export declare class PageCaptureError extends Error {
23
+ }
24
+ /** Parent side: ask `frame` to capture itself and resolve with its JPEG bytes. */
25
+ export declare function requestPageCapture({ frame, targetOrigin, width, height, timeoutMs, }: {
26
+ frame: Window;
27
+ targetOrigin: string;
28
+ width: number;
29
+ height: number;
30
+ timeoutMs?: number;
31
+ }): Promise<{
32
+ jpeg: ArrayBuffer;
33
+ ms: number;
34
+ }>;
@@ -0,0 +1,52 @@
1
+ import { randomUUID } from '../../../chunk-LIPH4O4F.mjs';
2
+
3
+ var PAGE_CAPTURE_REQUEST = "replo:page-capture:request";
4
+ var PAGE_CAPTURE_RESPONSE = "replo:page-capture:response";
5
+ function isPageCaptureRequestMessage(value) {
6
+ return typeof value === "object" && value !== null && "type" in value && value.type === PAGE_CAPTURE_REQUEST && "requestId" in value && typeof value.requestId === "string" && "width" in value && typeof value.width === "number" && "height" in value && typeof value.height === "number";
7
+ }
8
+ function isPageCaptureResponseMessage(value, requestId) {
9
+ return typeof value === "object" && value !== null && "type" in value && value.type === PAGE_CAPTURE_RESPONSE && "requestId" in value && value.requestId === requestId;
10
+ }
11
+ var PageCaptureError = class extends Error {
12
+ };
13
+ function requestPageCapture({
14
+ frame,
15
+ targetOrigin,
16
+ width,
17
+ height,
18
+ timeoutMs = 15e3
19
+ }) {
20
+ const requestId = randomUUID();
21
+ return new Promise((resolve, reject) => {
22
+ const onMessage = (event) => {
23
+ if (event.source !== frame || !isPageCaptureResponseMessage(event.data, requestId)) {
24
+ return;
25
+ }
26
+ cleanup();
27
+ if (event.data.jpeg) {
28
+ resolve({ jpeg: event.data.jpeg, ms: event.data.ms });
29
+ } else {
30
+ reject(new PageCaptureError(event.data.error));
31
+ }
32
+ };
33
+ const timeout = setTimeout(() => {
34
+ cleanup();
35
+ reject(new PageCaptureError("capture timed out"));
36
+ }, timeoutMs);
37
+ const cleanup = () => {
38
+ clearTimeout(timeout);
39
+ window.removeEventListener("message", onMessage);
40
+ };
41
+ window.addEventListener("message", onMessage);
42
+ const request = {
43
+ type: PAGE_CAPTURE_REQUEST,
44
+ requestId,
45
+ width,
46
+ height
47
+ };
48
+ frame.postMessage(request, targetOrigin);
49
+ });
50
+ }
51
+
52
+ export { PAGE_CAPTURE_REQUEST, PAGE_CAPTURE_RESPONSE, PageCaptureError, isPageCaptureRequestMessage, requestPageCapture };
@@ -1,4 +1,4 @@
1
- import { randomUUID } from '../../chunk-SGKQ5HGT.mjs';
1
+ import { randomUUID } from '../../chunk-LIPH4O4F.mjs';
2
2
  import { isNullish } from '../../chunk-JUKCUPQ6.mjs';
3
3
  import '../../chunk-FLYEIRFK.mjs';
4
4
  import '../../chunk-TESFJQID.mjs';
@@ -389,9 +389,9 @@ export declare const selectModeFormatCommandSchema: z.ZodEnum<{
389
389
  * Renaming or adding a variant here is a TS error in all three.
390
390
  */
391
391
  export declare const selectModeSelectionTypeSchema: z.ZodEnum<{
392
- element: "element";
393
- area: "area";
394
392
  drag: "drag";
393
+ area: "area";
394
+ element: "element";
395
395
  none: "none";
396
396
  }>;
397
397
  /**
@@ -342,9 +342,9 @@ export declare const websiteBuilderToPreviewIframeMethodConfig: {
342
342
  setSelectionType: {
343
343
  inputSchema: z.ZodObject<{
344
344
  selectionType: z.ZodEnum<{
345
- element: "element";
346
- area: "area";
347
345
  drag: "drag";
346
+ area: "area";
347
+ element: "element";
348
348
  none: "none";
349
349
  }>;
350
350
  selectors: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -862,9 +862,9 @@ export declare const selectModeParentMessageSchema: z.ZodDiscriminatedUnion<[z.Z
862
862
  method: z.ZodLiteral<"setSelectionType">;
863
863
  data: z.ZodObject<{
864
864
  selectionType: z.ZodEnum<{
865
- element: "element";
866
- area: "area";
867
865
  drag: "drag";
866
+ area: "area";
867
+ element: "element";
868
868
  none: "none";
869
869
  }>;
870
870
  selectors: z.ZodOptional<z.ZodArray<z.ZodString>>;
package/buildMetadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sandbox-runtime",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "branch": "HEAD",
5
- "commit": "6ad933540e110b76d214b8700e7a356a551c869a",
6
- "builtAt": "2026-09-24T18:57:19.451Z"
5
+ "commit": "455eef2903f4cc6a8f457d5ec77be1e1dc95aad9",
6
+ "builtAt": "2026-09-25T00:24:57.880Z"
7
7
  };
@@ -0,0 +1,11 @@
1
+ export declare function parseYouTubeId(src: string): string | null;
2
+ export declare function parseVimeoId(src: string): string | null;
3
+ /**
4
+ * Draws the page's first screen (from the document top, regardless of scroll)
5
+ * into an exact width×height JPEG. Videos and YouTube/Vimeo embeds, which the
6
+ * page cannot read directly, are swapped for their current frame or thumbnail.
7
+ */
8
+ export declare function capturePage({ width, height, }: {
9
+ width: number;
10
+ height: number;
11
+ }): Promise<Blob>;
package/capturePage.js ADDED
@@ -0,0 +1,261 @@
1
+ import { snapdom } from "@zumer/snapdom";
2
+ import { PageCaptureError } from "./_vendor/replo-utils/lib/pageCapture.mjs";
3
+ import { z } from "zod";
4
+ const JPEG_QUALITY = 0.8;
5
+ const VIDEO_FRAME_QUALITY = 0.85;
6
+ const VIDEO_LOAD_TIMEOUT_MS = 4e3;
7
+ const VIDEO_SEEK_TIMEOUT_MS = 1500;
8
+ const PLACEHOLDER_BACKGROUND = "#d9d9d6";
9
+ const IFRAME_PLACEHOLDER_MARKER = "repeating-linear-gradient(45deg";
10
+ const vimeoOembedSchema = z.object({ thumbnail_url: z.string() });
11
+ function parseYouTubeId(src) {
12
+ const match = src.match(
13
+ /(?:youtube(?:-nocookie)?\.com\/embed\/|youtu\.be\/)([\w-]{11})/
14
+ );
15
+ return match?.[1] ?? null;
16
+ }
17
+ function parseVimeoId(src) {
18
+ const match = src.match(/player\.vimeo\.com\/video\/(\d+)/);
19
+ return match?.[1] ?? null;
20
+ }
21
+ function canReadIframeDocument(iframe) {
22
+ try {
23
+ return Boolean(iframe.contentDocument ?? iframe.contentWindow?.document);
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ async function fetchAsDataUrl(url) {
29
+ const blob = await (await fetch(url)).blob();
30
+ return await new Promise((resolve, reject) => {
31
+ const reader = new FileReader();
32
+ reader.addEventListener("load", () => {
33
+ resolve(String(reader.result));
34
+ });
35
+ reader.addEventListener("error", () => {
36
+ reject(reader.error ?? new PageCaptureError("image read failed"));
37
+ });
38
+ reader.readAsDataURL(blob);
39
+ });
40
+ }
41
+ function isCanvasReadable(source) {
42
+ try {
43
+ const canvas = document.createElement("canvas");
44
+ canvas.width = 1;
45
+ canvas.height = 1;
46
+ canvas.getContext("2d")?.drawImage(source, 0, 0, 1, 1);
47
+ canvas.toDataURL();
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function videoFrameDataUrl(video) {
54
+ const canvas = document.createElement("canvas");
55
+ canvas.width = video.videoWidth;
56
+ canvas.height = video.videoHeight;
57
+ const context = canvas.getContext("2d");
58
+ if (!context) {
59
+ return null;
60
+ }
61
+ context.drawImage(video, 0, 0);
62
+ return canvas.toDataURL("image/jpeg", VIDEO_FRAME_QUALITY);
63
+ }
64
+ function isOnScreen({
65
+ element,
66
+ height
67
+ }) {
68
+ const rect = element.getBoundingClientRect();
69
+ return rect.bottom > 0 && rect.top < height && rect.width > 0;
70
+ }
71
+ async function readVideoFrame({
72
+ video,
73
+ height
74
+ }) {
75
+ if (!isOnScreen({ element: video, height })) {
76
+ return null;
77
+ }
78
+ if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && isCanvasReadable(video)) {
79
+ return videoFrameDataUrl(video);
80
+ }
81
+ const copy = document.createElement("video");
82
+ copy.crossOrigin = "anonymous";
83
+ copy.muted = true;
84
+ copy.preload = "auto";
85
+ try {
86
+ copy.src = video.currentSrc || video.src;
87
+ await new Promise((resolve, reject) => {
88
+ copy.addEventListener("loadeddata", () => {
89
+ resolve();
90
+ });
91
+ copy.addEventListener("error", () => {
92
+ reject(new PageCaptureError("video load failed"));
93
+ });
94
+ setTimeout(() => {
95
+ reject(new PageCaptureError("video load timed out"));
96
+ }, VIDEO_LOAD_TIMEOUT_MS);
97
+ });
98
+ if (video.currentTime > 0.1) {
99
+ copy.currentTime = video.currentTime;
100
+ await new Promise((resolve) => {
101
+ copy.addEventListener("seeked", () => {
102
+ resolve();
103
+ });
104
+ setTimeout(resolve, VIDEO_SEEK_TIMEOUT_MS);
105
+ });
106
+ }
107
+ return isCanvasReadable(copy) ? videoFrameDataUrl(copy) : null;
108
+ } catch {
109
+ return null;
110
+ } finally {
111
+ copy.removeAttribute("src");
112
+ copy.load();
113
+ }
114
+ }
115
+ async function readIframeThumbnail(iframe) {
116
+ const src = iframe.src;
117
+ let url = null;
118
+ const youTubeId = parseYouTubeId(src);
119
+ if (youTubeId) {
120
+ url = `https://i.ytimg.com/vi/${youTubeId}/hqdefault.jpg`;
121
+ }
122
+ const vimeoId = parseVimeoId(src);
123
+ if (vimeoId) {
124
+ try {
125
+ const response = await fetch(
126
+ `https://vimeo.com/api/oembed.json?url=https://vimeo.com/${vimeoId}&width=1280`
127
+ );
128
+ const body = vimeoOembedSchema.safeParse(await response.json());
129
+ url = body.success ? body.data.thumbnail_url : null;
130
+ } catch {
131
+ url = null;
132
+ }
133
+ }
134
+ if (!url) {
135
+ return null;
136
+ }
137
+ return fetchAsDataUrl(url).catch(() => null);
138
+ }
139
+ function createStandInsPlugin({
140
+ objectFitByFrame,
141
+ iframeThumbnails
142
+ }) {
143
+ return {
144
+ name: "replo-page-capture-stand-ins",
145
+ afterClone(context) {
146
+ const clone = context.clone;
147
+ if (!clone) {
148
+ return;
149
+ }
150
+ for (const image of clone.querySelectorAll("img")) {
151
+ const objectFit = objectFitByFrame.get(image.getAttribute("src") ?? "");
152
+ if (objectFit) {
153
+ image.style.objectFit = objectFit;
154
+ }
155
+ }
156
+ const placeholders = Array.from(
157
+ clone.querySelectorAll("div")
158
+ ).filter((element) => {
159
+ return (element.getAttribute("style") ?? "").includes(
160
+ IFRAME_PLACEHOLDER_MARKER
161
+ );
162
+ });
163
+ placeholders.forEach((placeholder, index) => {
164
+ const thumbnail = iframeThumbnails[index];
165
+ placeholder.style.backgroundImage = thumbnail ? `url("${thumbnail}")` : "none";
166
+ placeholder.style.backgroundColor = PLACEHOLDER_BACKGROUND;
167
+ placeholder.style.backgroundSize = "cover";
168
+ placeholder.style.backgroundPosition = "center";
169
+ });
170
+ }
171
+ };
172
+ }
173
+ function pageBackgroundColor() {
174
+ for (const element of [document.body, document.documentElement]) {
175
+ const color = getComputedStyle(element).backgroundColor;
176
+ if (color && color !== "transparent" && color !== "rgba(0, 0, 0, 0)") {
177
+ return color;
178
+ }
179
+ }
180
+ return "#fff";
181
+ }
182
+ function canvasToJpeg(canvas) {
183
+ return new Promise((resolve, reject) => {
184
+ canvas.toBlob(
185
+ (blob) => {
186
+ if (blob) {
187
+ resolve(blob);
188
+ } else {
189
+ reject(new PageCaptureError("jpeg encode failed"));
190
+ }
191
+ },
192
+ "image/jpeg",
193
+ JPEG_QUALITY
194
+ );
195
+ });
196
+ }
197
+ async function capturePage({
198
+ width,
199
+ height
200
+ }) {
201
+ const videos = Array.from(document.querySelectorAll("video"));
202
+ const iframes = Array.from(document.querySelectorAll("iframe")).filter(
203
+ (iframe) => !canReadIframeDocument(iframe)
204
+ );
205
+ const [videoFrames, iframeThumbnails] = await Promise.all([
206
+ Promise.all(
207
+ videos.map((video) => {
208
+ return readVideoFrame({ video, height });
209
+ })
210
+ ),
211
+ Promise.all(iframes.map(readIframeThumbnail))
212
+ ]);
213
+ const originalPosters = [];
214
+ const objectFitByFrame = /* @__PURE__ */ new Map();
215
+ videos.forEach((video, index) => {
216
+ const frame = videoFrames[index];
217
+ if (!frame) {
218
+ return;
219
+ }
220
+ originalPosters.push({ video, poster: video.getAttribute("poster") });
221
+ video.setAttribute("poster", frame);
222
+ objectFitByFrame.set(frame, getComputedStyle(video).objectFit);
223
+ });
224
+ try {
225
+ const raw = await snapdom.toRaw(document.body, {
226
+ scale: 1,
227
+ embedFonts: true,
228
+ plugins: [createStandInsPlugin({ objectFitByFrame, iframeThumbnails })]
229
+ });
230
+ const image = new Image();
231
+ image.src = raw;
232
+ await image.decode();
233
+ await new Promise((resolve) => {
234
+ setTimeout(resolve, 100);
235
+ });
236
+ const canvas = document.createElement("canvas");
237
+ canvas.width = width;
238
+ canvas.height = height;
239
+ const context = canvas.getContext("2d");
240
+ if (!context) {
241
+ throw new PageCaptureError("canvas 2d context unavailable");
242
+ }
243
+ context.fillStyle = pageBackgroundColor();
244
+ context.fillRect(0, 0, width, height);
245
+ context.drawImage(image, 0, 0);
246
+ return await canvasToJpeg(canvas);
247
+ } finally {
248
+ for (const { video, poster } of originalPosters) {
249
+ if (poster === null) {
250
+ video.removeAttribute("poster");
251
+ } else {
252
+ video.setAttribute("poster", poster);
253
+ }
254
+ }
255
+ }
256
+ }
257
+ export {
258
+ capturePage,
259
+ parseVimeoId,
260
+ parseYouTubeId
261
+ };
@@ -0,0 +1,67 @@
1
+ import { v4 } from 'uuid';
2
+
3
+ function bytesToBase64Url(bytes) {
4
+ let binary = "";
5
+ for (const byte of bytes) {
6
+ binary += String.fromCharCode(byte);
7
+ }
8
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
9
+ }
10
+ function base64UrlToBytes(input) {
11
+ const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
12
+ const padded = normalized.padEnd(
13
+ normalized.length + (4 - normalized.length % 4) % 4,
14
+ "="
15
+ );
16
+ const binary = atob(padded);
17
+ const bytes = new Uint8Array(binary.length);
18
+ for (let i = 0; i < binary.length; i += 1) {
19
+ bytes[i] = binary.charCodeAt(i);
20
+ }
21
+ return bytes;
22
+ }
23
+ function randomBase64Url(byteLength) {
24
+ return bytesToBase64Url(crypto.getRandomValues(new Uint8Array(byteLength)));
25
+ }
26
+ function randomUUID() {
27
+ return v4();
28
+ }
29
+ async function sha256Hex(input) {
30
+ const buffer = typeof input === "string" ? new TextEncoder().encode(input) : input;
31
+ const hash = await crypto.subtle.digest("SHA-256", buffer);
32
+ return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
33
+ }
34
+ function constantTimeEqual({
35
+ left,
36
+ right
37
+ }) {
38
+ if (left.length !== right.length) {
39
+ return false;
40
+ }
41
+ let difference = 0;
42
+ for (let index = 0; index < left.length; index += 1) {
43
+ difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
44
+ }
45
+ return difference === 0;
46
+ }
47
+ async function getHmacSha256Hex({
48
+ rawBody,
49
+ signingKey
50
+ }) {
51
+ const encoder = new TextEncoder();
52
+ const key = await crypto.subtle.importKey(
53
+ "raw",
54
+ encoder.encode(signingKey),
55
+ { name: "HMAC", hash: "SHA-256" },
56
+ false,
57
+ ["sign"]
58
+ );
59
+ const signature = await crypto.subtle.sign(
60
+ "HMAC",
61
+ key,
62
+ encoder.encode(rawBody)
63
+ );
64
+ return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
65
+ }
66
+
67
+ export { base64UrlToBytes, bytesToBase64Url, constantTimeEqual, getHmacSha256Hex, randomBase64Url, randomUUID, sha256Hex };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sandbox-runtime",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.8.0",
6
+ "version": "1.9.0",
7
7
  "branch": "HEAD",
8
- "commit": "6ad933540e110b76d214b8700e7a356a551c869a",
9
- "builtAt": "2026-09-24T18:57:19.451Z"
8
+ "commit": "455eef2903f4cc6a8f457d5ec77be1e1dc95aad9",
9
+ "builtAt": "2026-09-25T00:24:57.880Z"
10
10
  },
11
11
  "description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -40,6 +40,7 @@
40
40
  "@tiptap/extension-text": "^2.25.0",
41
41
  "@tiptap/extension-underline": "^2.25.0",
42
42
  "@tiptap/pm": "^2.25.0",
43
+ "@zumer/snapdom": "^3.0.0",
43
44
  "uuid": "^11.1.0",
44
45
  "zod": "^4.3.6",
45
46
  "zustand": "^4.5.7"
@@ -8,6 +8,7 @@ import { DocumentReadyBeacon } from "./DocumentReadyBeacon";
8
8
  import LinkInterceptor from "./LinkInterceptor";
9
9
  import { NavigationReceiver } from "./NavigationReceiver";
10
10
  import { NextErrorReporter } from "./NextErrorReporter";
11
+ import { PageCaptureResponder } from "./PageCaptureResponder";
11
12
  import PostHogRelay from "./posthog-relay";
12
13
  import { EditorSelection } from "./select-mode/EditorSelection";
13
14
  function ViewportSnapshotResponder() {
@@ -24,6 +25,7 @@ function SandboxRuntime({ children }) {
24
25
  /* @__PURE__ */ jsx(NavigationReceiver, {}),
25
26
  /* @__PURE__ */ jsx(PostHogRelay, {}),
26
27
  /* @__PURE__ */ jsx(ViewportSnapshotResponder, {}),
28
+ /* @__PURE__ */ jsx(PageCaptureResponder, {}),
27
29
  /* @__PURE__ */ jsx(EditorSelection, {}),
28
30
  /* @__PURE__ */ jsx(AnnotationLayerMount, {}),
29
31
  /* @__PURE__ */ jsx(DesignLibraryPreview, {}),
@@ -0,0 +1,32 @@
1
+ import type { PageCaptureResponseMessage } from "./_vendor/replo-utils/lib/pageCapture";
2
+ interface PageCaptureMessageListener {
3
+ (event: {
4
+ data: unknown;
5
+ source: unknown;
6
+ origin: string;
7
+ }): void;
8
+ }
9
+ /** The slice of `window` the responder touches, so specs can pass a fake. */
10
+ export interface PageCaptureWindow {
11
+ readonly innerWidth: number;
12
+ readonly parent: {
13
+ postMessage(message: PageCaptureResponseMessage, targetOrigin: string, transfer: Transferable[]): void;
14
+ };
15
+ readonly document: {
16
+ readonly fonts?: {
17
+ readonly ready: Promise<unknown>;
18
+ };
19
+ };
20
+ requestIdleCallback?(callback: () => void, options: {
21
+ timeout: number;
22
+ }): number;
23
+ addEventListener(type: "message", listener: PageCaptureMessageListener): void;
24
+ removeEventListener(type: "message", listener: PageCaptureMessageListener): void;
25
+ }
26
+ /**
27
+ * Answers Site Builder's page-capture requests with a JPEG of this page's
28
+ * first screen. Requests run one at a time so concurrent captures never
29
+ * fight over the page's temporary video posters.
30
+ */
31
+ export declare function startPageCaptureResponder(targetWindow: PageCaptureWindow): () => void;
32
+ export {};
@@ -0,0 +1,87 @@
1
+ import {
2
+ isPageCaptureRequestMessage,
3
+ PAGE_CAPTURE_RESPONSE
4
+ } from "./_vendor/replo-utils/lib/pageCapture.mjs";
5
+ import { isTrustedViewportSnapshotRequester } from "./_vendor/replo-utils/lib/viewportSnapshot.mjs";
6
+ const IDLE_TIMEOUT_MS = 1e3;
7
+ function waitForIdle(targetWindow) {
8
+ return new Promise((resolve) => {
9
+ if (targetWindow.requestIdleCallback) {
10
+ targetWindow.requestIdleCallback(
11
+ () => {
12
+ resolve();
13
+ },
14
+ { timeout: IDLE_TIMEOUT_MS }
15
+ );
16
+ } else {
17
+ setTimeout(resolve, 0);
18
+ }
19
+ });
20
+ }
21
+ async function respondToRequest({
22
+ targetWindow,
23
+ request
24
+ }) {
25
+ const failure = (error) => {
26
+ return {
27
+ response: {
28
+ type: PAGE_CAPTURE_RESPONSE,
29
+ requestId: request.requestId,
30
+ jpeg: null,
31
+ error
32
+ },
33
+ transfer: []
34
+ };
35
+ };
36
+ if (targetWindow.innerWidth !== request.width) {
37
+ return failure("layout-width-mismatch");
38
+ }
39
+ try {
40
+ const startedAt = performance.now();
41
+ await waitForIdle(targetWindow);
42
+ await targetWindow.document.fonts?.ready;
43
+ const { capturePage } = await import("./capturePage");
44
+ const blob = await capturePage({
45
+ width: request.width,
46
+ height: request.height
47
+ });
48
+ const jpeg = await blob.arrayBuffer();
49
+ return {
50
+ response: {
51
+ type: PAGE_CAPTURE_RESPONSE,
52
+ requestId: request.requestId,
53
+ jpeg,
54
+ ms: Math.round(performance.now() - startedAt)
55
+ },
56
+ transfer: [jpeg]
57
+ };
58
+ } catch (error) {
59
+ return failure(error instanceof Error ? error.message : String(error));
60
+ }
61
+ }
62
+ function startPageCaptureResponder(targetWindow) {
63
+ let queue = Promise.resolve();
64
+ const onMessage = (event) => {
65
+ if (event.source !== targetWindow.parent || !isPageCaptureRequestMessage(event.data) || !isTrustedViewportSnapshotRequester(event.origin)) {
66
+ return;
67
+ }
68
+ const request = event.data;
69
+ const requesterOrigin = event.origin;
70
+ queue = queue.then(async () => {
71
+ const { response, transfer } = await respondToRequest({
72
+ targetWindow,
73
+ request
74
+ });
75
+ targetWindow.parent.postMessage(response, requesterOrigin, transfer);
76
+ }).catch((error) => {
77
+ console.warn("[replo] page capture reply failed", error);
78
+ });
79
+ };
80
+ targetWindow.addEventListener("message", onMessage);
81
+ return () => {
82
+ targetWindow.removeEventListener("message", onMessage);
83
+ };
84
+ }
85
+ export {
86
+ startPageCaptureResponder
87
+ };
@@ -1,7 +0,0 @@
1
- import { v4 } from 'uuid';
2
-
3
- function randomUUID() {
4
- return v4();
5
- }
6
-
7
- export { randomUUID };