@replohq/sandbox-runtime 1.7.13 → 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.
Files changed (33) hide show
  1. package/PageCaptureResponder.d.ts +1 -0
  2. package/PageCaptureResponder.js +12 -0
  3. package/_vendor/replo-utils/lib/crypto.mjs +1 -0
  4. package/_vendor/replo-utils/lib/math.mjs +1 -1
  5. package/_vendor/replo-utils/lib/pageCapture.d.ts +34 -0
  6. package/_vendor/replo-utils/lib/pageCapture.mjs +52 -0
  7. package/_vendor/schemas/errors.d.ts +1 -0
  8. package/_vendor/schemas/generated/selectMode.d.ts +25 -0
  9. package/_vendor/schemas/iframeRpcChannel.mjs +1 -1
  10. package/_vendor/schemas/selectMode.d.ts +101 -2
  11. package/_vendor/schemas/selectMode.mjs +1 -1
  12. package/_vendor/schemas/selectModeIframeRpc.d.ts +148 -4
  13. package/_vendor/schemas/selectModeIframeRpc.mjs +1 -1
  14. package/_vendor/schemas/selectModeImage.d.ts +9 -0
  15. package/_vendor/schemas/selectModeImage.mjs +24 -0
  16. package/buildMetadata.js +3 -3
  17. package/capturePage.d.ts +11 -0
  18. package/capturePage.js +261 -0
  19. package/chunk-LIPH4O4F.mjs +67 -0
  20. package/{chunk-4T7WSMC3.mjs → chunk-XWNBKE63.mjs} +25 -1
  21. package/package.json +5 -4
  22. package/sandbox-runtime.js +2 -0
  23. package/select-mode/descriptor/elementDescriptor.js +2 -0
  24. package/select-mode/hooks/useOptimisticOverrides.js +63 -10
  25. package/select-mode/hooks/useSelectionInteractions.js +1 -10
  26. package/select-mode/iframeRpcChannel.d.ts +42 -0
  27. package/select-mode/imageOverridePreview.d.ts +6 -0
  28. package/select-mode/imageOverridePreview.js +77 -0
  29. package/select-mode/imageSources.d.ts +3 -0
  30. package/select-mode/imageSources.js +68 -0
  31. package/startPageCaptureResponder.d.ts +32 -0
  32. package/startPageCaptureResponder.js +87 -0
  33. package/chunk-SGKQ5HGT.mjs +0 -7
@@ -0,0 +1,77 @@
1
+ import { getImageEditSnapshot } from "../_vendor/schemas/selectModeImage.mjs";
2
+ import { getImageDescriptor, getPictureSources } from "./imageSources";
3
+ function createImagePreview(image) {
4
+ const picture = document.createElement("picture");
5
+ picture.hidden = true;
6
+ picture.dataset.reploImageResolver = "true";
7
+ const resolver = document.createElement("img");
8
+ resolver.alt = "";
9
+ const style = document.createElement("style");
10
+ const token = crypto.randomUUID();
11
+ image.dataset.reploImagePreview = token;
12
+ document.head.append(style);
13
+ let configuration = "";
14
+ let disposed = false;
15
+ function paint() {
16
+ if (disposed) return;
17
+ const url = resolver.currentSrc || resolver.src;
18
+ if (!url) return;
19
+ const rule = `[data-replo-image-preview="${token}"] { content: url(${JSON.stringify(url)}) !important; }`;
20
+ if (style.textContent !== rule) style.textContent = rule;
21
+ }
22
+ resolver.addEventListener("load", paint);
23
+ return {
24
+ render(config) {
25
+ const next = JSON.stringify(config);
26
+ if (configuration !== next) {
27
+ configuration = next;
28
+ const originals = getPictureSources(image);
29
+ const sources = config.sources.map((source, index) => {
30
+ const original = originals.find((candidate) => {
31
+ return candidate.media.trim() === source.media && candidate.type === source.type;
32
+ }) ?? originals[index];
33
+ const node = document.createElement("source");
34
+ node.media = source.media;
35
+ node.type = resolveMetadata(source.type, original?.type);
36
+ node.srcset = resolveMetadata(source.srcSet, original?.srcset);
37
+ node.sizes = resolveMetadata(source.sizes, original?.sizes);
38
+ return node;
39
+ });
40
+ resolver.setAttribute(
41
+ "src",
42
+ resolveMetadata(config.src, image.getAttribute("src"))
43
+ );
44
+ resolver.setAttribute(
45
+ "srcset",
46
+ resolveMetadata(config.srcSet, image.getAttribute("srcset"))
47
+ );
48
+ resolver.setAttribute(
49
+ "sizes",
50
+ resolveMetadata(config.sizes, image.sizes)
51
+ );
52
+ picture.replaceChildren(...sources, resolver);
53
+ if (!picture.isConnected) document.body.append(picture);
54
+ }
55
+ paint();
56
+ },
57
+ isPersisted(config) {
58
+ return JSON.stringify(
59
+ getImageEditSnapshot(getImageDescriptor(image), image.baseURI)
60
+ ) === JSON.stringify(getImageEditSnapshot(config, image.baseURI));
61
+ },
62
+ dispose() {
63
+ disposed = true;
64
+ resolver.removeEventListener("load", paint);
65
+ picture.remove();
66
+ style.remove();
67
+ if (image.dataset.reploImagePreview === token)
68
+ delete image.dataset.reploImagePreview;
69
+ }
70
+ };
71
+ }
72
+ function resolveMetadata(value, original) {
73
+ return value === "[image metadata omitted]" ? original ?? "" : value;
74
+ }
75
+ export {
76
+ createImagePreview
77
+ };
@@ -0,0 +1,3 @@
1
+ import type { SelectModeImage } from "../_vendor/schemas/generated/selectMode";
2
+ export declare function getPictureSources(image: HTMLImageElement): HTMLSourceElement[];
3
+ export declare function getImageDescriptor(element: Element): SelectModeImage | undefined;
@@ -0,0 +1,68 @@
1
+ function getPictureSources(image) {
2
+ const parent = image.parentElement;
3
+ if (parent?.tagName !== "PICTURE") {
4
+ return [];
5
+ }
6
+ return Array.from(parent.children).filter(
7
+ (child) => {
8
+ return child instanceof HTMLSourceElement && Boolean(
9
+ child.compareDocumentPosition(image) & Node.DOCUMENT_POSITION_FOLLOWING
10
+ );
11
+ }
12
+ );
13
+ }
14
+ const MAX_IMAGE_METADATA_LENGTH = 2048;
15
+ const MAX_PICTURE_SOURCES = 16;
16
+ function boundedImageMetadata(value) {
17
+ if (value.length > MAX_IMAGE_METADATA_LENGTH || /data:/i.test(value)) {
18
+ return "[image metadata omitted]";
19
+ }
20
+ return value;
21
+ }
22
+ function getPreviewUrl({
23
+ src,
24
+ srcSet,
25
+ baseURI
26
+ }) {
27
+ if (src.length > MAX_IMAGE_METADATA_LENGTH || srcSet.length > MAX_IMAGE_METADATA_LENGTH)
28
+ return null;
29
+ const candidate = src || srcSet.trim().split(/\s+/)[0]?.replace(/,+$/, "");
30
+ if (!candidate) {
31
+ return null;
32
+ }
33
+ try {
34
+ const url = new URL(candidate, baseURI);
35
+ return ["http:", "https:"].includes(url.protocol) ? url.href : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ function getImageDescriptor(element) {
41
+ if (!(element instanceof HTMLImageElement)) {
42
+ return void 0;
43
+ }
44
+ const src = element.getAttribute("src") ?? "";
45
+ const srcSet = element.getAttribute("srcset") ?? "";
46
+ return {
47
+ src: boundedImageMetadata(src),
48
+ srcSet: boundedImageMetadata(srcSet),
49
+ sizes: boundedImageMetadata(element.sizes),
50
+ currentSrc: boundedImageMetadata(element.currentSrc),
51
+ previewUrl: getPreviewUrl({ src, srcSet, baseURI: element.baseURI }),
52
+ sources: getPictureSources(element).filter((source) => source.media.length <= MAX_IMAGE_METADATA_LENGTH).slice(0, MAX_PICTURE_SOURCES).map((source) => ({
53
+ srcSet: boundedImageMetadata(source.srcset),
54
+ sizes: boundedImageMetadata(source.sizes),
55
+ media: source.media.trim(),
56
+ type: boundedImageMetadata(source.type),
57
+ previewUrl: getPreviewUrl({
58
+ src: "",
59
+ srcSet: source.srcset,
60
+ baseURI: source.baseURI
61
+ })
62
+ }))
63
+ };
64
+ }
65
+ export {
66
+ getImageDescriptor,
67
+ getPictureSources
68
+ };
@@ -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 };