hypha-debugger 0.2.6 → 0.2.7
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/hypha-debugger.js +76 -18
- package/dist/hypha-debugger.min.js +2 -2
- package/dist/hypha-debugger.mjs +76 -18
- package/dist/hypha-debugger.mjs.map +1 -1
- package/package.json +1 -1
package/dist/hypha-debugger.mjs
CHANGED
|
@@ -2060,12 +2060,16 @@ async function takeScreenshot(selector, format, quality, max_width, max_height,
|
|
|
2060
2060
|
// to the viewport dimensions.
|
|
2061
2061
|
const viewportW = window.innerWidth;
|
|
2062
2062
|
const viewportH = window.innerHeight;
|
|
2063
|
+
// 1x1 transparent PNG — used as placeholder for images that fail
|
|
2064
|
+
// to load (CORS-blocked, 404, etc.) so html-to-image doesn't reject.
|
|
2065
|
+
const TRANSPARENT_PIXEL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=";
|
|
2063
2066
|
const captureOptions = {
|
|
2064
2067
|
quality: qual,
|
|
2065
2068
|
pixelRatio: 1, // always capture at 1x — we'll resize after
|
|
2066
2069
|
cacheBust: true,
|
|
2067
2070
|
skipAutoScale: true,
|
|
2068
2071
|
skipFonts: true, // CORS-blocked stylesheets can hang font inlining
|
|
2072
|
+
imagePlaceholder: TRANSPARENT_PIXEL, // fallback for broken images
|
|
2069
2073
|
filter: (el) => {
|
|
2070
2074
|
// Exclude the debugger overlay and cursor from screenshots
|
|
2071
2075
|
return (el.id !== "hypha-debugger-host" &&
|
|
@@ -2078,21 +2082,37 @@ async function takeScreenshot(selector, format, quality, max_width, max_height,
|
|
|
2078
2082
|
captureOptions.width = viewportW;
|
|
2079
2083
|
captureOptions.height = viewportH;
|
|
2080
2084
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
// Hard timeout: pages with cross-origin resources can make
|
|
2085
|
-
// html-to-image wait indefinitely on blocked fetches.
|
|
2086
|
-
const timeoutMs = 15000;
|
|
2087
|
-
dataUrl = await Promise.race([
|
|
2085
|
+
const runCapture = async (opts, timeoutMs = 15000) => {
|
|
2086
|
+
const capturePromise = fmt === "jpeg" ? toJpeg(node, opts) : toPng(node, opts);
|
|
2087
|
+
return Promise.race([
|
|
2088
2088
|
capturePromise,
|
|
2089
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`Screenshot capture timed out after ${timeoutMs}ms
|
|
2089
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Screenshot capture timed out after ${timeoutMs}ms`)), timeoutMs)),
|
|
2090
2090
|
]);
|
|
2091
|
+
};
|
|
2092
|
+
let dataUrl;
|
|
2093
|
+
try {
|
|
2094
|
+
dataUrl = await runCapture(captureOptions);
|
|
2091
2095
|
}
|
|
2092
2096
|
catch (captureErr) {
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2097
|
+
// Fallback: retry without images (filter them out). Some pages have
|
|
2098
|
+
// images that html-to-image can't inline even with imagePlaceholder.
|
|
2099
|
+
try {
|
|
2100
|
+
const noImagesOpts = {
|
|
2101
|
+
...captureOptions,
|
|
2102
|
+
filter: (el) => {
|
|
2103
|
+
if (!captureOptions.filter(el))
|
|
2104
|
+
return false;
|
|
2105
|
+
const tag = el.tagName?.toLowerCase();
|
|
2106
|
+
return tag !== "img" && tag !== "picture" && tag !== "video";
|
|
2107
|
+
},
|
|
2108
|
+
};
|
|
2109
|
+
dataUrl = await runCapture(noImagesOpts, 10000);
|
|
2110
|
+
}
|
|
2111
|
+
catch (retryErr) {
|
|
2112
|
+
return {
|
|
2113
|
+
error: `Capture failed: ${errorMessage(captureErr)} (retry without images also failed: ${errorMessage(retryErr)})`,
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2096
2116
|
}
|
|
2097
2117
|
// Resize down to fit within (maxW × maxH) and re-encode. If resize
|
|
2098
2118
|
// fails (e.g. data URL too large to load back into an Image), fall
|
|
@@ -2617,6 +2637,29 @@ function getFiberFromDOM(node) {
|
|
|
2617
2637
|
k.startsWith("__reactInternalInstance$"));
|
|
2618
2638
|
return fiberKey ? node[fiberKey] : null;
|
|
2619
2639
|
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Find the topmost DOM element that has a React fiber attached.
|
|
2642
|
+
* React mounts on various nodes — #root is common but not universal
|
|
2643
|
+
* (#app, #__next, body, etc.). We scan until we find one.
|
|
2644
|
+
*/
|
|
2645
|
+
function findReactRoot() {
|
|
2646
|
+
// Try common selectors first (fast path)
|
|
2647
|
+
const common = ["#root", "#app", "#__next", "[data-reactroot]", "main", "body"];
|
|
2648
|
+
for (const sel of common) {
|
|
2649
|
+
const el = document.querySelector(sel);
|
|
2650
|
+
if (el && getFiberFromDOM(el))
|
|
2651
|
+
return el;
|
|
2652
|
+
}
|
|
2653
|
+
// Slow path: walk the tree, find first element with a fiber
|
|
2654
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null);
|
|
2655
|
+
let node = walker.currentNode;
|
|
2656
|
+
while (node) {
|
|
2657
|
+
if (node instanceof Element && getFiberFromDOM(node))
|
|
2658
|
+
return node;
|
|
2659
|
+
node = walker.nextNode();
|
|
2660
|
+
}
|
|
2661
|
+
return null;
|
|
2662
|
+
}
|
|
2620
2663
|
function getComponentName(fiber) {
|
|
2621
2664
|
const { type } = fiber;
|
|
2622
2665
|
if (!type)
|
|
@@ -2727,16 +2770,30 @@ function fiberToInfo(fiber, depth, maxDepth) {
|
|
|
2727
2770
|
return info;
|
|
2728
2771
|
}
|
|
2729
2772
|
function getReactTree(selector, max_depth) {
|
|
2730
|
-
selector = selector ?? "#root";
|
|
2731
2773
|
const maxDepth = max_depth ?? 5;
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2774
|
+
let rootEl;
|
|
2775
|
+
let usedSelector;
|
|
2776
|
+
if (selector) {
|
|
2777
|
+
rootEl = document.querySelector(selector);
|
|
2778
|
+
usedSelector = selector;
|
|
2779
|
+
if (!rootEl) {
|
|
2780
|
+
return { error: `No element found for selector: ${selector}` };
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
else {
|
|
2784
|
+
// Auto-detect: scan for any element with a React fiber attached
|
|
2785
|
+
rootEl = findReactRoot();
|
|
2786
|
+
usedSelector = rootEl?.tagName.toLowerCase() + (rootEl?.id ? "#" + rootEl.id : "") || "(auto)";
|
|
2787
|
+
if (!rootEl) {
|
|
2788
|
+
return {
|
|
2789
|
+
error: "No React root found on this page. Tried #root, #app, #__next, [data-reactroot], main, body — none had React fibers. This page may not be a React app, or React may not have rendered yet. Pass an explicit `selector` if you know the mount point.",
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2735
2792
|
}
|
|
2736
2793
|
const fiber = getFiberFromDOM(rootEl);
|
|
2737
2794
|
if (!fiber) {
|
|
2738
2795
|
return {
|
|
2739
|
-
error: `No React fiber found on element "${
|
|
2796
|
+
error: `No React fiber found on element "${usedSelector}". Is this a React app?`,
|
|
2740
2797
|
};
|
|
2741
2798
|
}
|
|
2742
2799
|
// Walk up to find the root component fiber (skip HostRoot)
|
|
@@ -2756,13 +2813,14 @@ function getReactTree(selector, max_depth) {
|
|
|
2756
2813
|
}
|
|
2757
2814
|
getReactTree.__schema__ = {
|
|
2758
2815
|
name: "getReactTree",
|
|
2759
|
-
description: "Inspect the React component tree
|
|
2816
|
+
description: "Inspect the React component tree. Returns component names, props, state (including hooks), and children hierarchy. " +
|
|
2817
|
+
"When no selector is provided, auto-detects the React root by scanning common mount points (#root, #app, #__next, [data-reactroot], main, body) and then walking the DOM for any element with a React fiber attached.",
|
|
2760
2818
|
parameters: {
|
|
2761
2819
|
type: "object",
|
|
2762
2820
|
properties: {
|
|
2763
2821
|
selector: {
|
|
2764
2822
|
type: "string",
|
|
2765
|
-
description:
|
|
2823
|
+
description: "CSS selector of the React root element. Omit for auto-detection.",
|
|
2766
2824
|
},
|
|
2767
2825
|
max_depth: {
|
|
2768
2826
|
type: "number",
|