@botim/mp-debug-sdk 0.3.1 → 0.4.1

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/dist/index.d.cts CHANGED
@@ -45,9 +45,11 @@ interface BuiltinHostHooks {
45
45
  /**
46
46
  * Capture a screenshot. Two return shapes are accepted:
47
47
  * - a raw base64 string → assumed PNG (legacy form)
48
- * - `{ data, format }` → format is one of 'png-base64' | 'jpeg-base64'
49
- * so the admin viewer can pick the right MIME for the data URL.
50
- * The SDK enforces a 1 MB base64 cap regardless.
48
+ * - `{ data, format }` → format is one of:
49
+ * 'png-base64' | 'jpeg-base64' raw bitmap, viewer renders as <img>
50
+ * 'rrweb-snapshot' — `data` is JSON the viewer rebuilds
51
+ * 'html-snapshot' — legacy `data` JSON: { html, … }
52
+ * The SDK enforces a 1 MB base64/JSON cap regardless.
51
53
  */
52
54
  screenshot?: () => ScreenshotResult | Promise<ScreenshotResult>;
53
55
  }
@@ -55,10 +57,17 @@ type ScreenshotResult = string | {
55
57
  data: string;
56
58
  format?: 'png-base64' | 'jpeg-base64'
57
59
  /**
58
- * Self-contained HTML+CSS snapshot `data` is a JSON string with
59
- * the shape `{ html, viewport, url, capturedAt }`. Admin viewers
60
- * render `html` inside a sandboxed `<iframe srcdoc>` so the result
61
- * looks pixel-identical to the live page without any pixel encoding.
60
+ * rrweb-snapshot tree, JSON-stringified. `data` parses to:
61
+ * { snapshot, viewport: { w, h }, url, capturedAt }
62
+ * The admin viewer calls `rrwebSnapshot.rebuild(snapshot, …)` into
63
+ * an iframe to re-materialize the page with full shadow DOM /
64
+ * adoptedStyleSheets / cross-origin CSS fidelity.
65
+ */
66
+ | 'rrweb-snapshot'
67
+ /**
68
+ * Legacy self-contained HTML snapshot (pre-0.4). `data` JSON shape
69
+ * is `{ html, viewport, url, capturedAt }`. Kept on the wire for
70
+ * back-compat; admin viewers still know how to render it.
62
71
  */
63
72
  | 'html-snapshot';
64
73
  };
package/dist/index.d.ts CHANGED
@@ -45,9 +45,11 @@ interface BuiltinHostHooks {
45
45
  /**
46
46
  * Capture a screenshot. Two return shapes are accepted:
47
47
  * - a raw base64 string → assumed PNG (legacy form)
48
- * - `{ data, format }` → format is one of 'png-base64' | 'jpeg-base64'
49
- * so the admin viewer can pick the right MIME for the data URL.
50
- * The SDK enforces a 1 MB base64 cap regardless.
48
+ * - `{ data, format }` → format is one of:
49
+ * 'png-base64' | 'jpeg-base64' raw bitmap, viewer renders as <img>
50
+ * 'rrweb-snapshot' — `data` is JSON the viewer rebuilds
51
+ * 'html-snapshot' — legacy `data` JSON: { html, … }
52
+ * The SDK enforces a 1 MB base64/JSON cap regardless.
51
53
  */
52
54
  screenshot?: () => ScreenshotResult | Promise<ScreenshotResult>;
53
55
  }
@@ -55,10 +57,17 @@ type ScreenshotResult = string | {
55
57
  data: string;
56
58
  format?: 'png-base64' | 'jpeg-base64'
57
59
  /**
58
- * Self-contained HTML+CSS snapshot `data` is a JSON string with
59
- * the shape `{ html, viewport, url, capturedAt }`. Admin viewers
60
- * render `html` inside a sandboxed `<iframe srcdoc>` so the result
61
- * looks pixel-identical to the live page without any pixel encoding.
60
+ * rrweb-snapshot tree, JSON-stringified. `data` parses to:
61
+ * { snapshot, viewport: { w, h }, url, capturedAt }
62
+ * The admin viewer calls `rrwebSnapshot.rebuild(snapshot, …)` into
63
+ * an iframe to re-materialize the page with full shadow DOM /
64
+ * adoptedStyleSheets / cross-origin CSS fidelity.
65
+ */
66
+ | 'rrweb-snapshot'
67
+ /**
68
+ * Legacy self-contained HTML snapshot (pre-0.4). `data` JSON shape
69
+ * is `{ html, viewport, url, capturedAt }`. Kept on the wire for
70
+ * back-compat; admin viewers still know how to render it.
62
71
  */
63
72
  | 'html-snapshot';
64
73
  };
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { snapshot } from 'rrweb-snapshot';
2
+
1
3
  // src/types.ts
2
4
  var SCHEMA_VERSION = 2;
3
5
 
@@ -751,8 +753,6 @@ var CommandRegistry = class {
751
753
  }
752
754
  }
753
755
  };
754
-
755
- // src/commands/builtins.ts
756
756
  var MAX_DUMP_BYTES = 64 * 1024;
757
757
  var MAX_SCREENSHOT_BYTES = 1024 * 1024;
758
758
  async function defaultDomScreenshot() {
@@ -761,163 +761,107 @@ async function defaultDomScreenshot() {
761
761
  "[@botim/debug-sdk] default screenshot requires a DOM. Provide builtins.screenshot for non-browser runtimes (e.g. native bridge)."
762
762
  );
763
763
  }
764
+ const injected = injectAdoptedStyleSheets();
765
+ let tree;
764
766
  try {
765
- return await captureViaSvgForeignObject();
766
- } catch (err) {
767
- try {
768
- console.warn(
769
- "[@botim/debug-sdk] SVG/canvas screenshot failed, falling back to HTML snapshot:",
770
- err instanceof Error ? err.message : err
771
- );
772
- } catch {
773
- }
774
- return await captureViaHtmlSnapshot();
775
- }
776
- }
777
- var VOID_ELEMENTS = /* @__PURE__ */ new Set([
778
- "area",
779
- "base",
780
- "br",
781
- "col",
782
- "embed",
783
- "hr",
784
- "img",
785
- "input",
786
- "link",
787
- "meta",
788
- "source",
789
- "track",
790
- "wbr"
791
- ]);
792
- var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "noscript", "template"]);
793
- var DEVTOOL_OVERLAY_PREFIXES = ["vite-", "astro-dev-", "next-route-"];
794
- function isDevtoolOverlay(tag) {
795
- return DEVTOOL_OVERLAY_PREFIXES.some((p) => tag.startsWith(p));
796
- }
797
- function escapeText(s) {
798
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
799
- }
800
- function escapeAttr(s) {
801
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
802
- }
803
- function serializeComposed(node, opts, depth) {
804
- if (depth > 256) return "";
805
- if (node.nodeType === Node.TEXT_NODE) {
806
- return escapeText(node.textContent ?? "");
807
- }
808
- if (node.nodeType === Node.COMMENT_NODE) return "";
809
- if (node.nodeType !== Node.ELEMENT_NODE) return "";
810
- const el = node;
811
- const tag = el.tagName.toLowerCase();
812
- if (SKIP_TAGS.has(tag)) return "";
813
- if (isDevtoolOverlay(tag)) return "";
814
- if (tag === "slot") {
815
- const slot = el;
816
- let assigned = [];
817
- try {
818
- assigned = slot.assignedNodes({ flatten: true });
819
- } catch {
767
+ tree = snapshot(document, {
768
+ inlineStylesheet: true,
769
+ inlineImages: true,
770
+ recordCanvas: true,
771
+ // Deliberately NOT slimming. SlimDOM drops <link rel="preload">,
772
+ // hidden form metadata, and other "noise" that's actually relevant
773
+ // when reproducing a layout bug.
774
+ slimDOM: false,
775
+ // Don't mask anything by default — debug-relay already runs a
776
+ // top-level redactor on console payloads, and on-screen text is the
777
+ // whole point of capturing a screenshot. Hosts that need PII masking
778
+ // can wire their own builtins.screenshot using rrweb-snapshot's
779
+ // `maskTextSelector` / `maskInputOptions`.
780
+ maskAllInputs: false
781
+ });
782
+ } finally {
783
+ for (const node of injected) {
784
+ try {
785
+ node.remove();
786
+ } catch {
787
+ }
820
788
  }
821
- const source = assigned.length > 0 ? assigned : Array.from(el.childNodes);
822
- return source.map((c) => serializeComposed(c, opts, depth + 1)).join("");
823
789
  }
824
- if (tag === "link" && (el.getAttribute("rel") || "").toLowerCase() === "stylesheet") {
825
- return "";
790
+ if (!tree) {
791
+ throw new Error("[@botim/debug-sdk] rrweb-snapshot returned null tree");
826
792
  }
827
- if (tag === "style") {
828
- return `<style>${el.textContent ?? ""}</style>`;
829
- }
830
- if (tag === "img" && opts.stripCrossOriginImages) {
831
- const src = el.getAttribute("src") || "";
832
- if (/^(https?:|\/\/)/i.test(src)) {
793
+ const payload = {
794
+ snapshot: tree,
795
+ viewport: {
796
+ w: window.innerWidth || document.documentElement.clientWidth || 0,
797
+ h: window.innerHeight || document.documentElement.clientHeight || 0
798
+ },
799
+ url: location.href,
800
+ capturedAt: Date.now()
801
+ };
802
+ return {
803
+ data: JSON.stringify(payload),
804
+ format: "rrweb-snapshot"
805
+ };
806
+ }
807
+ function injectAdoptedStyleSheets() {
808
+ const injected = [];
809
+ const collect = (sheets) => {
810
+ if (!sheets || sheets.length === 0) return "";
811
+ const chunks = [];
812
+ for (const sheet of sheets) {
833
813
  try {
834
- const u = new URL(src, location.href);
835
- if (u.origin !== location.origin) return "";
814
+ const rules = sheet.cssRules;
815
+ for (const rule of Array.from(rules)) chunks.push(rule.cssText);
836
816
  } catch {
837
- return "";
838
817
  }
839
818
  }
840
- }
841
- if (tag === "canvas") return `<canvas></canvas>`;
842
- const attrs = Array.from(el.attributes).filter((a) => !a.name.startsWith("on")).map((a) => ` ${a.name}="${escapeAttr(a.value)}"`).join("");
843
- if (VOID_ELEMENTS.has(tag)) return `<${tag}${attrs}>`;
844
- const childSource = el.shadowRoot ?? el;
845
- const inner = Array.from(childSource.childNodes).map((c) => serializeComposed(c, opts, depth + 1)).join("");
846
- return `<${tag}${attrs}>${inner}</${tag}>`;
847
- }
848
- function readInlineableStyles() {
849
- const chunks = [];
850
- for (const sheet of Array.from(document.styleSheets)) {
851
- try {
852
- const rules = sheet.cssRules;
853
- for (const rule of Array.from(rules)) chunks.push(rule.cssText);
854
- } catch {
819
+ return chunks.join("\n");
820
+ };
821
+ const inject = (parent, css) => {
822
+ if (!css) return;
823
+ const ownerDoc = parent instanceof Document ? parent : parent.ownerDocument;
824
+ if (!ownerDoc) return;
825
+ const style = ownerDoc.createElement("style");
826
+ style.setAttribute("data-botim-adopted", "1");
827
+ style.textContent = css;
828
+ const target = parent instanceof Document ? parent.head ?? parent.documentElement ?? parent.body : parent;
829
+ if (target) {
830
+ target.insertBefore(style, target.firstChild);
831
+ injected.push(style);
855
832
  }
856
- }
857
- return chunks.join("\n");
858
- }
859
- async function captureViaSvgForeignObject() {
860
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
861
- const w = Math.max(1, Math.min(window.innerWidth || document.documentElement.clientWidth || 1024, 2400));
862
- const h = Math.max(1, Math.min(document.documentElement.scrollHeight, 4e3));
863
- const inlinedCss = readInlineableStyles();
864
- const bodyHtml = serializeComposed(document.body, { stripCrossOriginImages: true }, 0);
865
- const htmlStr = `<html xmlns="http://www.w3.org/1999/xhtml"><body>${bodyHtml}</body></html>`;
866
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}"><foreignObject width="100%" height="100%">` + (inlinedCss ? `<style xmlns="http://www.w3.org/1999/xhtml">${escapeForXml(inlinedCss)}</style>` : "") + htmlStr + `</foreignObject></svg>`;
867
- const blobUrl = URL.createObjectURL(
868
- new Blob([svg], { type: "image/svg+xml;charset=utf-8" })
869
- );
833
+ };
870
834
  try {
871
- const img = await loadImage(blobUrl);
872
- const canvas = document.createElement("canvas");
873
- canvas.width = w * dpr;
874
- canvas.height = h * dpr;
875
- const ctx = canvas.getContext("2d");
876
- if (!ctx) throw new Error("canvas 2D context unavailable");
877
- const pageBg = getComputedStyle(document.body).backgroundColor || "#ffffff";
878
- ctx.fillStyle = pageBg.startsWith("rgba(0, 0, 0, 0)") ? "#ffffff" : pageBg;
879
- ctx.fillRect(0, 0, canvas.width, canvas.height);
880
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
881
- const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
882
- return { data: dataUrl.split(",")[1], format: "jpeg-base64" };
883
- } finally {
884
- URL.revokeObjectURL(blobUrl);
835
+ inject(
836
+ document,
837
+ collect(document.adoptedStyleSheets)
838
+ );
839
+ } catch {
885
840
  }
886
- }
887
- function loadImage(src) {
888
- return new Promise((resolve, reject) => {
889
- const img = new Image();
890
- img.crossOrigin = "anonymous";
891
- img.onload = () => resolve(img);
892
- img.onerror = () => reject(new Error("failed to render snapshot SVG"));
893
- img.src = src;
894
- });
895
- }
896
- function escapeForXml(s) {
897
- return s.replace(/]]>/g, "]]&gt;").replace(/<\//g, "&lt;/");
898
- }
899
- async function captureViaHtmlSnapshot() {
900
- const inlinedCss = readInlineableStyles();
901
- const bodyHtml = serializeComposed(document.body, { stripCrossOriginImages: false }, 0);
902
- const html = `<!DOCTYPE html>
903
- <html lang="en">
904
- <head>
905
- <meta charset="utf-8">
906
- <meta name="viewport" content="width=device-width,initial-scale=1">
907
- <base href="${escapeAttr(location.href)}">
908
- ` + (inlinedCss ? `<style data-botim-snapshot="1">${inlinedCss}</style>
909
- ` : "") + `</head>
910
- <body>${bodyHtml}</body>
911
- </html>`;
912
- return {
913
- data: JSON.stringify({
914
- html,
915
- viewport: { w: window.innerWidth, h: window.innerHeight },
916
- url: location.href,
917
- capturedAt: Date.now()
918
- }),
919
- format: "html-snapshot"
841
+ const walkRoot = (root) => {
842
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
843
+ let n = walker.currentNode;
844
+ while (n) {
845
+ const el = n;
846
+ const sr = el.shadowRoot;
847
+ if (sr) {
848
+ try {
849
+ inject(
850
+ sr,
851
+ collect(sr.adoptedStyleSheets)
852
+ );
853
+ } catch {
854
+ }
855
+ walkRoot(sr);
856
+ }
857
+ n = walker.nextNode();
858
+ }
920
859
  };
860
+ try {
861
+ walkRoot(document);
862
+ } catch {
863
+ }
864
+ return injected;
921
865
  }
922
866
  function registerBuiltins(registry, hooks = {}) {
923
867
  registry.register("ping", ping);