updated stuff

This commit is contained in:
SinachPat
2026-05-10 00:45:55 +01:00
parent c53db55209
commit 4b7f14564a
+182 -116
View File
@@ -179,91 +179,100 @@ 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 = {
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', {
configurable: true,
enumerable: true,
get: function() { return _omInjectCurrent; },
set: function(fn) {
if (typeof fn !== 'function') return;
var wrapped = fn;
_omInjectCurrent = function(renderer) {
try {
return wrapped.apply(this, arguments);
} catch(e) {
// React Refresh's wrapper tried to call an undefined oldInject.
// Fall back to direct ID allocation.
console.log(OM_TAG, 'inject() fallback after wrapper error, id=' + (_omNextId + 1));
var id = ++_omNextId;
hook.renderers.set(id, renderer);
return id;
}
};
},
});
// Layer 1: lock the global so nothing can replace our hook after this point.
try { try {
Object.defineProperty(window, '__REACT_DEVTOOLS_GLOBAL_HOOK__', { Object.defineProperty(hook, 'inject', {
configurable: false, configurable: true,
enumerable: true, enumerable: true,
writable: false, get: function() { return _omInjectCurrent; },
value: hook, set: function(fn) {
if (typeof fn !== 'function') return;
var wrapped = fn;
_omInjectCurrent = function(renderer) {
try {
return wrapped.apply(this, arguments);
} catch(e) {
// React Refresh's wrapper tried to call an undefined oldInject — fall back.
console.log(OM_TAG, 'inject() fallback after wrapper error, id=' + (_omNextId + 1));
var id = ++_omNextId;
hook.renderers.set(id, renderer);
return id;
}
};
},
}); });
} 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 getFiberFromEl(el) {
if (!el) return null;
var keys;
try { keys = Object.keys(el); } catch(e) { return null; }
var fiberKey = null, containerKey = null;
for (var ki = 0; ki < keys.length; 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;
}
// Returns true if a fiber tree was found and posted, false otherwise.
function captureExistingTree() { function captureExistingTree() {
// Find any DOM element that React has annotated with a fiber reference. // Priority candidates: most common React root containers.
// 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 = [ var candidates = [
document.getElementById('__next'), document.getElementById('__next'), // Next.js Pages Router
document.getElementById('root'), document.getElementById('root'), // CRA / Vite
document.getElementById('app'), document.getElementById('app'), // Generic
document.documentElement, // <html> — App Router hydrateRoot target document, // Next.js App Router: hydrateRoot(document)
document.body, document.documentElement, // <html>
document.body, // <body>
]; ];
function getFiber(el) {
if (!el) return null;
var keys = Object.keys(el);
for (var ki = 0; ki < keys.length; ki++) {
if (keys[ki].indexOf('__reactFiber$') === 0) return el[keys[ki]];
}
return null;
}
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;
for (var di = 0; di < allEls.length && !fiber; di++) { if (root) {
fiber = getFiber(allEls[di]); var allEls = root.querySelectorAll('*');
for (var di = 0; di < allEls.length && !fiber; 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);
});
})();`; })();`;
} }