updated stuff
This commit is contained in:
@@ -179,57 +179,75 @@ export function buildProxyFiberHookScript(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── React DevTools global hook ────────────────────────────────────────────
|
// ── React DevTools global hook ────────────────────────────────────────────
|
||||||
// Two-layer bulletproof strategy:
|
// Strategy: PATCH the hook that window.__REACT_DEVTOOLS_GLOBAL_HOOK__ already
|
||||||
|
// points to, rather than creating a new object.
|
||||||
//
|
//
|
||||||
// LAYER 1 — Lock the global: replace window.__REACT_DEVTOOLS_GLOBAL_HOOK__
|
// WHY patching beats replacing:
|
||||||
// with our fresh hook, then make the property non-writable so neither the
|
// The Chrome DevTools extension injects at document_start (before HTML
|
||||||
// Chrome DevTools extension (which runs at document_start and may
|
// parsing) and often locks window.__REACT_DEVTOOLS_GLOBAL_HOOK__ as
|
||||||
// re-initialize) nor any webpack module can swap it out again.
|
// { configurable: false, writable: false }. Any attempt to replace the
|
||||||
|
// global property silently no-ops. We'd end up with getter/setter on a
|
||||||
|
// fresh hook object that React and React Refresh never see.
|
||||||
//
|
//
|
||||||
// LAYER 2 — Lock inject via getter/setter: define hook.inject as an accessor
|
// WHAT we patch:
|
||||||
// property. GET always returns a working implementation (_omInjectCurrent).
|
// 1. Supplement missing methods (renderers, checkDCE, onScheduleFiberRoot …)
|
||||||
// SET intercepts React Refresh's attempt to wrap inject and re-wraps the
|
// so every method React 19 and React Refresh might call is present.
|
||||||
// wrapper with a try-catch fallback, so even if React Refresh captured
|
// 2. Redefine hook.inject as a getter/setter accessor:
|
||||||
// oldInject=undefined from an earlier stub and its wrapper throws at call
|
// GET → always returns _omInjectCurrent (never undefined, never throws).
|
||||||
// time, we catch the error and fall back to our own ID allocation.
|
// SET → when React Refresh does hook.inject = wrapper(oldInject),
|
||||||
// This handles EVERY timing scenario without relying on script order.
|
// our setter wraps the wrapper in try/catch so that even a
|
||||||
|
// broken wrapper (where oldInject was captured as undefined by an
|
||||||
|
// earlier DevTools stub) falls back to our own ID allocation.
|
||||||
|
// 3. isDisabled must be false — React Refresh bails out if truthy.
|
||||||
|
|
||||||
var _existingHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
// Get (or lazily create) the global hook.
|
||||||
var _prevCommit = (_existingHook && typeof _existingHook.onCommitFiberRoot === 'function')
|
var hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||||
? _existingHook.onCommitFiberRoot : null;
|
if (!hook) {
|
||||||
|
hook = {};
|
||||||
|
try {
|
||||||
|
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
||||||
|
} catch(e) { /* locked — nothing we can do; hook is still a valid object */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save any pre-existing onCommitFiberRoot (e.g. from DevTools extension).
|
||||||
|
var _prevCommit = typeof hook.onCommitFiberRoot === 'function'
|
||||||
|
? hook.onCommitFiberRoot : null;
|
||||||
|
|
||||||
|
// Supplement missing properties (safe to assign even on a locked global —
|
||||||
|
// the global itself is locked, not the hook object's properties).
|
||||||
|
if (!hook.renderers) hook.renderers = new Map();
|
||||||
|
if (!hook.supportsFiber) hook.supportsFiber = true;
|
||||||
|
if (hook.isDisabled === undefined) hook.isDisabled = false;
|
||||||
|
if (!hook.checkDCE) hook.checkDCE = function() {};
|
||||||
|
if (!hook.onScheduleFiberRoot) hook.onScheduleFiberRoot = function() {};
|
||||||
|
if (!hook.onCommitFiberUnmount) hook.onCommitFiberUnmount = function() {};
|
||||||
|
if (!hook.onPostCommitFiberRoot) hook.onPostCommitFiberRoot = function() {};
|
||||||
|
if (!hook.onCommitFiberRoot) hook.onCommitFiberRoot = null; // set below
|
||||||
|
|
||||||
|
// ── Protect inject with a getter/setter accessor ──────────────────────────
|
||||||
|
// Our getter always returns _omInjectCurrent — a function that never throws.
|
||||||
|
// React Refresh does:
|
||||||
|
// var oldInject = hook.inject; // reads our getter → our function
|
||||||
|
// hook.inject = function(r) { // triggers our setter
|
||||||
|
// var id = oldInject.apply(…); // calls our function (not undefined)
|
||||||
|
// };
|
||||||
|
// Even if React Refresh ran BEFORE our script and already set hook.inject to
|
||||||
|
// a broken wrapper (where oldInject===undefined), our setter replaces it with
|
||||||
|
// a try-catch-guarded version that falls back on error.
|
||||||
|
|
||||||
var _omNextId = 0;
|
var _omNextId = 0;
|
||||||
|
|
||||||
// Base inject implementation — always safe, never throws.
|
|
||||||
var _omInjectCurrent = function(renderer) {
|
var _omInjectCurrent = function(renderer) {
|
||||||
try {
|
try {
|
||||||
var id = ++_omNextId;
|
var id = ++_omNextId;
|
||||||
hook.renderers.set(id, renderer);
|
hook.renderers.set(id, renderer);
|
||||||
console.log(OM_TAG, 'React registered via inject(), rendererId=' + id);
|
console.log(OM_TAG, 'inject() called, rendererId=' + id);
|
||||||
return id;
|
return id;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
return ++_omNextId;
|
return ++_omNextId;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var hook = {
|
try {
|
||||||
renderers: new Map(),
|
|
||||||
supportsFiber: true,
|
|
||||||
isDisabled: false, // React Refresh checks this; must be false
|
|
||||||
checkDCE: function() {},
|
|
||||||
onScheduleFiberRoot: function() {},
|
|
||||||
onCommitFiberUnmount: function() {},
|
|
||||||
onPostCommitFiberRoot: function() {},
|
|
||||||
onCommitFiberRoot: null, // assigned below
|
|
||||||
// inject is NOT in the literal — it is defined as a getter/setter below.
|
|
||||||
};
|
|
||||||
|
|
||||||
// Layer 2: protect inject with an accessor property.
|
|
||||||
// React Refresh does: var oldInject = hook.inject; hook.inject = wrapper(oldInject);
|
|
||||||
// Our getter ensures oldInject is NEVER undefined regardless of timing.
|
|
||||||
// Our setter wraps whatever React Refresh installs with a try-catch so that
|
|
||||||
// even a broken wrapper (where oldInject was captured as undefined from an
|
|
||||||
// earlier DevTools stub) falls back gracefully.
|
|
||||||
Object.defineProperty(hook, 'inject', {
|
Object.defineProperty(hook, 'inject', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
@@ -241,8 +259,7 @@ export function buildProxyFiberHookScript(): string {
|
|||||||
try {
|
try {
|
||||||
return wrapped.apply(this, arguments);
|
return wrapped.apply(this, arguments);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// React Refresh's wrapper tried to call an undefined oldInject.
|
// React Refresh's wrapper tried to call an undefined oldInject — fall back.
|
||||||
// Fall back to direct ID allocation.
|
|
||||||
console.log(OM_TAG, 'inject() fallback after wrapper error, id=' + (_omNextId + 1));
|
console.log(OM_TAG, 'inject() fallback after wrapper error, id=' + (_omNextId + 1));
|
||||||
var id = ++_omNextId;
|
var id = ++_omNextId;
|
||||||
hook.renderers.set(id, renderer);
|
hook.renderers.set(id, renderer);
|
||||||
@@ -251,19 +268,11 @@ export function buildProxyFiberHookScript(): string {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Layer 1: lock the global so nothing can replace our hook after this point.
|
|
||||||
try {
|
|
||||||
Object.defineProperty(window, '__REACT_DEVTOOLS_GLOBAL_HOOK__', {
|
|
||||||
configurable: false,
|
|
||||||
enumerable: true,
|
|
||||||
writable: false,
|
|
||||||
value: hook,
|
|
||||||
});
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
// Property is already non-configurable (DevTools extension locked it first).
|
// inject property itself is non-configurable — overwrite directly.
|
||||||
// Simple assignment is a best-effort fallback.
|
// This is a best-effort last resort; the try-catch in the function body
|
||||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
// at least prevents React from seeing an exception from hook.inject().
|
||||||
|
hook.inject = _omInjectCurrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
hook.onCommitFiberRoot = function(rendererId, root, priorityLevel, didError) {
|
hook.onCommitFiberRoot = function(rendererId, root, priorityLevel, didError) {
|
||||||
@@ -1001,73 +1010,81 @@ 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 ─────────────────────────────────────────────
|
// ── DOM-driven fiber capture ──────────────────────────────────────────────
|
||||||
// onCommitFiberRoot only fires on FUTURE commits. If React already completed
|
// React annotates every host DOM element with __reactFiber$xxx and every
|
||||||
// its first render (hydration) before our hook script was evaluated, we miss
|
// root container (including document for App Router's hydrateRoot(document))
|
||||||
// the initial tree entirely and the 4-second static-page timer fires falsely.
|
// with __reactContainer$xxx. We walk these annotations up to the HostRoot
|
||||||
|
// and serialize the tree, exactly as onCommitFiberRoot would.
|
||||||
//
|
//
|
||||||
// Fix: scan common React root containers for __reactFiber$ DOM annotations
|
// This is our PRIMARY mechanism — the React DevTools hook (with all its
|
||||||
// that React writes onto every host element. Walk the found fiber up to the
|
// browser-extension and React-Refresh timing hazards) is treated as a
|
||||||
// HostRoot (fiber.return chain) and serialize it exactly as onCommitFiberRoot
|
// secondary signal. Even if the hook integration is completely broken,
|
||||||
// does. Two attempts cover the two race modes:
|
// polling + MutationObserver will eventually find the tree.
|
||||||
// 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.
|
|
||||||
// Priority order covers the most common React root containers:
|
|
||||||
// - Next.js Pages Router: #__next
|
|
||||||
// - CRA / Vite: #root
|
|
||||||
// - Generic: #app
|
|
||||||
// - Next.js App Router: <html> or <body> (hydrateRoot on document)
|
|
||||||
// - Fallback DOM scan: first annotated element anywhere in <body>
|
|
||||||
var candidates = [
|
|
||||||
document.getElementById('__next'),
|
|
||||||
document.getElementById('root'),
|
|
||||||
document.getElementById('app'),
|
|
||||||
document.documentElement, // <html> — App Router hydrateRoot target
|
|
||||||
document.body,
|
|
||||||
];
|
|
||||||
|
|
||||||
function getFiber(el) {
|
function getFiberFromEl(el) {
|
||||||
if (!el) return null;
|
if (!el) return null;
|
||||||
var keys = Object.keys(el);
|
var keys;
|
||||||
|
try { keys = Object.keys(el); } catch(e) { return null; }
|
||||||
|
var fiberKey = null, containerKey = null;
|
||||||
for (var ki = 0; ki < keys.length; ki++) {
|
for (var ki = 0; ki < keys.length; ki++) {
|
||||||
if (keys[ki].indexOf('__reactFiber$') === 0) return el[keys[ki]];
|
var k = keys[ki];
|
||||||
|
if (!fiberKey && k.indexOf('__reactFiber$') === 0) fiberKey = k;
|
||||||
|
else if (!containerKey && k.indexOf('__reactContainer$') === 0) containerKey = k;
|
||||||
|
}
|
||||||
|
if (fiberKey) return el[fiberKey]; // direct fiber reference
|
||||||
|
if (containerKey) { // FiberRoot — use .current
|
||||||
|
var fr = el[containerKey];
|
||||||
|
return fr && fr.current ? fr.current : null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true if a fiber tree was found and posted, false otherwise.
|
||||||
|
function captureExistingTree() {
|
||||||
|
// Priority candidates: most common React root containers.
|
||||||
|
var candidates = [
|
||||||
|
document.getElementById('__next'), // Next.js Pages Router
|
||||||
|
document.getElementById('root'), // CRA / Vite
|
||||||
|
document.getElementById('app'), // Generic
|
||||||
|
document, // Next.js App Router: hydrateRoot(document)
|
||||||
|
document.documentElement, // <html>
|
||||||
|
document.body, // <body>
|
||||||
|
];
|
||||||
|
|
||||||
var fiber = null;
|
var fiber = null;
|
||||||
for (var ci = 0; ci < candidates.length; ci++) {
|
for (var ci = 0; ci < candidates.length && !fiber; ci++) {
|
||||||
fiber = getFiber(candidates[ci]);
|
fiber = getFiberFromEl(candidates[ci]);
|
||||||
if (fiber) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last resort: walk the DOM looking for any annotated element.
|
// Fallback: scan every element in <html> for annotations. This catches
|
||||||
|
// any framework whose root container we don't recognise.
|
||||||
if (!fiber) {
|
if (!fiber) {
|
||||||
var allEls = document.body ? document.body.querySelectorAll('*') : [];
|
var root = document.documentElement || document.body;
|
||||||
|
if (root) {
|
||||||
|
var allEls = root.querySelectorAll('*');
|
||||||
for (var di = 0; di < allEls.length && !fiber; di++) {
|
for (var di = 0; di < allEls.length && !fiber; di++) {
|
||||||
fiber = getFiber(allEls[di]);
|
fiber = getFiberFromEl(allEls[di]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fiber) {
|
if (!fiber) return false;
|
||||||
console.log(OM_TAG, 'captureExistingTree: no __reactFiber$ found anywhere in DOM');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk up to the HostRoot (the sentinel fiber React builds the tree from).
|
// Walk up to the HostRoot (the sentinel fiber React builds the tree from).
|
||||||
var f = fiber;
|
var f = fiber;
|
||||||
while (f.return) f = f.return;
|
while (f.return) f = f.return;
|
||||||
|
|
||||||
console.log(OM_TAG, 'captureExistingTree: found fiber, walking to root, posting tree');
|
|
||||||
// Rebuild maps and serialize — identical to what onCommitFiberRoot does.
|
// Rebuild maps and serialize — identical to what onCommitFiberRoot does.
|
||||||
nodeMap = {};
|
nodeMap = {};
|
||||||
fiberMap = new WeakMap();
|
fiberMap = new WeakMap();
|
||||||
var tree = serializeFiber(f, '');
|
var tree = serializeFiber(f, '');
|
||||||
|
if (!tree) return false;
|
||||||
|
|
||||||
|
console.log(OM_TAG, 'captureExistingTree: posted tree, root=' + tree.name);
|
||||||
reapplyOverrides();
|
reapplyOverrides();
|
||||||
post({ type: 'FIBER_TREE_UPDATE', root: tree });
|
post({ type: 'FIBER_TREE_UPDATE', root: tree });
|
||||||
if (selectedNodeId) updateHighlight();
|
if (selectedNodeId) updateHighlight();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Ready signal (includes root font size for rem→px normalisation) ────────
|
// ── Ready signal (includes root font size for rem→px normalisation) ────────
|
||||||
@@ -1075,14 +1092,63 @@ export function buildProxyFiberHookScript(): string {
|
|||||||
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).
|
// ── Robust tree discovery: polling + MutationObserver + periodic refresh ──
|
||||||
captureExistingTree();
|
// Don't trust the hook integration. Even if onCommitFiberRoot fires, we want
|
||||||
|
// a fallback for the case where it doesn't (broken DevTools/React Refresh).
|
||||||
|
//
|
||||||
|
// • Initial backoff schedule: 0, 50, 200, 500, 1s, 2s, 4s, 8s, 12s, 16s
|
||||||
|
// Stops as soon as a tree is found.
|
||||||
|
// • MutationObserver on <html>: any DOM change triggers one re-attempt.
|
||||||
|
// • Once found, periodic 3 s rescans pick up future updates that the hook
|
||||||
|
// might have missed.
|
||||||
|
var _treeFound = false;
|
||||||
|
var _rescanInterval = null;
|
||||||
|
|
||||||
|
function tryCapture(label) {
|
||||||
|
if (captureExistingTree()) {
|
||||||
|
if (!_treeFound) {
|
||||||
|
_treeFound = true;
|
||||||
|
console.log(OM_TAG, 'tree found via ' + label);
|
||||||
|
// Set up periodic rescans so future React commits are reflected even
|
||||||
|
// if onCommitFiberRoot is silenced by hook breakage.
|
||||||
|
if (!_rescanInterval) {
|
||||||
|
_rescanInterval = setInterval(function() { captureExistingTree(); }, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule discoverRoutes after the first capture attempts settle.
|
||||||
setTimeout(discoverRoutes, 800);
|
setTimeout(discoverRoutes, 800);
|
||||||
// Attempt 2: safety-net capture 2 s later for deferred hydration / Suspense.
|
|
||||||
setTimeout(captureExistingTree, 2000);
|
// Initial polling schedule — geometric backoff up to 16 s.
|
||||||
// Re-discover on SPA navigation (Next.js App Router fires popstate on push)
|
tryCapture('immediate');
|
||||||
window.addEventListener('popstate', function() { setTimeout(discoverRoutes, 100); });
|
[50, 200, 500, 1000, 2000, 4000, 8000, 12000, 16000].forEach(function(ms) {
|
||||||
|
setTimeout(function() { if (!_treeFound) tryCapture('poll-' + ms); }, ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
// MutationObserver: trip the moment React first writes to the DOM.
|
||||||
|
try {
|
||||||
|
var _mo = new MutationObserver(function() {
|
||||||
|
if (_treeFound) { _mo.disconnect(); return; }
|
||||||
|
tryCapture('mutation');
|
||||||
|
});
|
||||||
|
_mo.observe(document.documentElement || document.body, {
|
||||||
|
childList: true, subtree: true, attributes: false,
|
||||||
|
});
|
||||||
|
// Disconnect if we still haven't found anything after 30s — at that point
|
||||||
|
// the page is genuinely static or React is too broken to render.
|
||||||
|
setTimeout(function() { try { _mo.disconnect(); } catch(e) {} }, 30000);
|
||||||
|
} catch(e) { /* MutationObserver not available */ }
|
||||||
|
|
||||||
|
// Re-discover on SPA navigation (Next.js App Router fires popstate on push).
|
||||||
|
window.addEventListener('popstate', function() {
|
||||||
|
setTimeout(discoverRoutes, 100);
|
||||||
|
setTimeout(function() { captureExistingTree(); }, 200);
|
||||||
|
});
|
||||||
})();`;
|
})();`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user