From 8bbee987ffaaf1757786c42b4bbf930e2dee22c0 Mon Sep 17 00:00:00 2001 From: SinachPat Date: Thu, 7 May 2026 08:36:20 +0100 Subject: [PATCH] fix: resolve false 'static page' detection and missing route discovery on CLI-proxied apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/components/canvas/LiveArtboard.tsx | 6 +- packages/live-sdk/src/hook.ts | 11 ++- packages/renderer/src/fiber-hook.ts | 73 ++++++++++++++++++- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/packages/app/src/components/canvas/LiveArtboard.tsx b/packages/app/src/components/canvas/LiveArtboard.tsx index b3e7ce7..b071710 100644 --- a/packages/app/src/components/canvas/LiveArtboard.tsx +++ b/packages/app/src/components/canvas/LiveArtboard.tsx @@ -124,11 +124,13 @@ export function LiveArtboard({ pendingStylesFetchRef.current = selectedComponentId; } 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); staticTimerRef.current = setTimeout(() => { if (!hasReactRef.current) onStaticPageDetected?.(); - }, 4000); + }, 8000); break; case 'FIBER_TREE_UPDATE': // Mark that this iframe contains a live React app. diff --git a/packages/live-sdk/src/hook.ts b/packages/live-sdk/src/hook.ts index 88c3356..8b64444 100644 --- a/packages/live-sdk/src/hook.ts +++ b/packages/live-sdk/src/hook.ts @@ -442,9 +442,18 @@ function installFiberHook(): void { // Current route first addRoute(window.location.pathname, document.title || undefined); - // Scan real elements + // Scan real 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('a[href]').forEach((a) => { 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); if (url.origin !== window.location.origin) return; addRoute(url.pathname, a.textContent ?? undefined); diff --git a/packages/renderer/src/fiber-hook.ts b/packages/renderer/src/fiber-hook.ts index 7354cfb..05dc618 100644 --- a/packages/renderer/src/fiber-hook.ts +++ b/packages/renderer/src/fiber-hook.ts @@ -868,14 +868,25 @@ export function buildProxyFiberHookScript(): string { } catch (e) { /* Next.js not present */ } })(); - // ③ same-origin links from the rendered DOM + // ③ 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]'); for (var i = 0; i < anchors.length; i++) { 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); 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); } catch (e) { /* skip malformed hrefs */ } } @@ -899,12 +910,68 @@ export function buildProxyFiberHookScript(): string { 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) ──────── var rootFontSizePx = parseFloat( window.getComputedStyle(document.documentElement).getPropertyValue('font-size') || '16' ) || 16; 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); + // 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) window.addEventListener('popstate', function() { setTimeout(discoverRoutes, 100); }); })();`;