fix: resolve false 'static page' detection and missing route discovery on CLI-proxied apps
Three related fixes for the canvas live-render panel: 1. captureExistingTree() in fiber-hook — retroactively walks the already- mounted React fiber tree via __reactFiber$ DOM annotations. Called immediately on READY (handles post-hydration race) and again at 2 s (handles deferred hydration / Suspense). Prevents the 'Static HTML page' false positive on React apps that mounted before the hook script ran. 2. Static-page timer bumped from 4 s → 8 s in LiveArtboard to give the 2-second captureExistingTree safety-net enough headroom before the no-React verdict fires. 3. Route discovery same-origin fix applied to both renderer/fiber-hook.ts and live-sdk/hook.ts — root-relative hrefs (starting with '/') are now accepted regardless of origin so CLI-proxied pages (where links still point to the original domain) expose their navigation routes correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3f1fd802bf
commit
8bbee987ff
@@ -124,11 +124,13 @@ export function LiveArtboard({
|
|||||||
pendingStylesFetchRef.current = selectedComponentId;
|
pendingStylesFetchRef.current = selectedComponentId;
|
||||||
}
|
}
|
||||||
onReady?.();
|
onReady?.();
|
||||||
// Start a 4-second timer: if React never commits, this is a static page.
|
// Start an 8-second timer: if React never commits (including the
|
||||||
|
// 2-second retroactive captureExistingTree safety net), this is
|
||||||
|
// genuinely a static page.
|
||||||
if (staticTimerRef.current) clearTimeout(staticTimerRef.current);
|
if (staticTimerRef.current) clearTimeout(staticTimerRef.current);
|
||||||
staticTimerRef.current = setTimeout(() => {
|
staticTimerRef.current = setTimeout(() => {
|
||||||
if (!hasReactRef.current) onStaticPageDetected?.();
|
if (!hasReactRef.current) onStaticPageDetected?.();
|
||||||
}, 4000);
|
}, 8000);
|
||||||
break;
|
break;
|
||||||
case 'FIBER_TREE_UPDATE':
|
case 'FIBER_TREE_UPDATE':
|
||||||
// Mark that this iframe contains a live React app.
|
// Mark that this iframe contains a live React app.
|
||||||
|
|||||||
@@ -442,9 +442,18 @@ function installFiberHook(): void {
|
|||||||
// Current route first
|
// Current route first
|
||||||
addRoute(window.location.pathname, document.title || undefined);
|
addRoute(window.location.pathname, document.title || undefined);
|
||||||
|
|
||||||
// Scan real <a> elements
|
// Scan real <a> elements — accept root-relative paths regardless of origin
|
||||||
|
// so that CLI-proxied pages (where links still point to the original domain)
|
||||||
|
// are handled correctly alongside direct same-origin connections.
|
||||||
document.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((a) => {
|
document.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((a) => {
|
||||||
try {
|
try {
|
||||||
|
const rawHref = (a.getAttribute('href') ?? '').trim();
|
||||||
|
// Root-relative paths are always valid routes.
|
||||||
|
if (rawHref.startsWith('/')) {
|
||||||
|
addRoute(rawHref, a.textContent ?? undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Absolute URLs — only add if same-origin (direct connection).
|
||||||
const url = new URL(a.href, window.location.href);
|
const url = new URL(a.href, window.location.href);
|
||||||
if (url.origin !== window.location.origin) return;
|
if (url.origin !== window.location.origin) return;
|
||||||
addRoute(url.pathname, a.textContent ?? undefined);
|
addRoute(url.pathname, a.textContent ?? undefined);
|
||||||
|
|||||||
@@ -868,14 +868,25 @@ export function buildProxyFiberHookScript(): string {
|
|||||||
} catch (e) { /* Next.js not present */ }
|
} catch (e) { /* Next.js not present */ }
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ③ <a href> same-origin links from the rendered DOM
|
// ③ <a href> links — relative paths AND same-origin absolute URLs.
|
||||||
|
// When running through the CLI proxy, the page's links still point to the
|
||||||
|
// original domain (e.g. intraining.com), not the proxy (localhost:4170).
|
||||||
|
// A strict same-origin check would reject all of them. Instead, accept
|
||||||
|
// any href that is already a root-relative path ("/courses"), plus
|
||||||
|
// same-origin absolute URLs for direct (non-proxied) connections.
|
||||||
var anchors = document.querySelectorAll('a[href]');
|
var anchors = document.querySelectorAll('a[href]');
|
||||||
for (var i = 0; i < anchors.length; i++) {
|
for (var i = 0; i < anchors.length; i++) {
|
||||||
try {
|
try {
|
||||||
|
var rawHref = (anchors[i].getAttribute('href') || '').trim();
|
||||||
|
// Root-relative paths — always valid routes regardless of origin
|
||||||
|
if (rawHref.charAt(0) === '/') {
|
||||||
|
addRoute(rawHref, (anchors[i].textContent || '').trim() || undefined);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Absolute URLs — only add if same-origin (direct connection)
|
||||||
var url = new URL(anchors[i].href, window.location.href);
|
var url = new URL(anchors[i].href, window.location.href);
|
||||||
if (url.origin !== window.location.origin) continue;
|
if (url.origin !== window.location.origin) continue;
|
||||||
// Skip hash-only links
|
if (!url.pathname || (url.hash && !url.pathname)) continue;
|
||||||
if (!url.pathname || url.hash && !url.pathname) continue;
|
|
||||||
addRoute(url.pathname, (anchors[i].textContent || '').trim() || undefined);
|
addRoute(url.pathname, (anchors[i].textContent || '').trim() || undefined);
|
||||||
} catch (e) { /* skip malformed hrefs */ }
|
} catch (e) { /* skip malformed hrefs */ }
|
||||||
}
|
}
|
||||||
@@ -899,12 +910,68 @@ export function buildProxyFiberHookScript(): string {
|
|||||||
if (routes.length > 0) post({ type: 'ROUTES_DISCOVERED', routes: routes });
|
if (routes.length > 0) post({ type: 'ROUTES_DISCOVERED', routes: routes });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Retroactive fiber capture ─────────────────────────────────────────────
|
||||||
|
// onCommitFiberRoot only fires on FUTURE commits. If React already completed
|
||||||
|
// its first render (hydration) before our hook script was evaluated, we miss
|
||||||
|
// the initial tree entirely and the 4-second static-page timer fires falsely.
|
||||||
|
//
|
||||||
|
// Fix: scan common React root containers for __reactFiber$ DOM annotations
|
||||||
|
// that React writes onto every host element. Walk the found fiber up to the
|
||||||
|
// HostRoot (fiber.return chain) and serialize it exactly as onCommitFiberRoot
|
||||||
|
// does. Two attempts cover the two race modes:
|
||||||
|
// Attempt 1 (immediate) – hook ran after hydration; DOM is populated now.
|
||||||
|
// Attempt 2 (2 s delay) – hook ran before hydration or Suspense deferred.
|
||||||
|
function captureExistingTree() {
|
||||||
|
// Find any DOM element that React has annotated with a fiber reference.
|
||||||
|
var candidates = [
|
||||||
|
document.getElementById('__next'),
|
||||||
|
document.getElementById('root'),
|
||||||
|
document.getElementById('app'),
|
||||||
|
document.body,
|
||||||
|
];
|
||||||
|
|
||||||
|
var fiber = null;
|
||||||
|
for (var ci = 0; ci < candidates.length; ci++) {
|
||||||
|
var el = candidates[ci];
|
||||||
|
if (!el) continue;
|
||||||
|
var keys = Object.keys(el);
|
||||||
|
for (var ki = 0; ki < keys.length; ki++) {
|
||||||
|
if (keys[ki].indexOf('__reactFiber$') === 0) {
|
||||||
|
fiber = el[keys[ki]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fiber) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fiber) return; // React not yet mounted on any known container.
|
||||||
|
|
||||||
|
// Walk up to the HostRoot (the sentinel fiber React builds the tree from).
|
||||||
|
var f = fiber;
|
||||||
|
while (f.return) f = f.return;
|
||||||
|
|
||||||
|
// Rebuild maps and serialize — identical to what onCommitFiberRoot does.
|
||||||
|
nodeMap = {};
|
||||||
|
fiberMap = new WeakMap();
|
||||||
|
var tree = serializeFiber(f, '');
|
||||||
|
if (!tree) return; // Nothing serializable yet.
|
||||||
|
|
||||||
|
reapplyOverrides();
|
||||||
|
post({ type: 'FIBER_TREE_UPDATE', root: tree });
|
||||||
|
if (selectedNodeId) updateHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Ready signal (includes root font size for rem→px normalisation) ────────
|
// ── Ready signal (includes root font size for rem→px normalisation) ────────
|
||||||
var rootFontSizePx = parseFloat(
|
var rootFontSizePx = parseFloat(
|
||||||
window.getComputedStyle(document.documentElement).getPropertyValue('font-size') || '16'
|
window.getComputedStyle(document.documentElement).getPropertyValue('font-size') || '16'
|
||||||
) || 16;
|
) || 16;
|
||||||
post({ type: 'READY', rootFontSizePx: rootFontSizePx });
|
post({ type: 'READY', rootFontSizePx: rootFontSizePx });
|
||||||
|
// Attempt 1: capture already-mounted React tree immediately (handles the
|
||||||
|
// common case where hydration completed before the hook script ran).
|
||||||
|
captureExistingTree();
|
||||||
setTimeout(discoverRoutes, 800);
|
setTimeout(discoverRoutes, 800);
|
||||||
|
// Attempt 2: safety-net capture 2 s later for deferred hydration / Suspense.
|
||||||
|
setTimeout(captureExistingTree, 2000);
|
||||||
// Re-discover on SPA navigation (Next.js App Router fires popstate on push)
|
// Re-discover on SPA navigation (Next.js App Router fires popstate on push)
|
||||||
window.addEventListener('popstate', function() { setTimeout(discoverRoutes, 100); });
|
window.addEventListener('popstate', function() { setTimeout(discoverRoutes, 100); });
|
||||||
})();`;
|
})();`;
|
||||||
|
|||||||
Reference in New Issue
Block a user