Key Inspector: add a YouBar+ diagnostic to find out why forceRerender doesn't work

This commit is contained in:
everestmcarthur 2026-08-01 20:00:47 -04:00
parent 8b1eb71674
commit 7050778072
2 changed files with 157 additions and 0 deletions

View file

@ -0,0 +1,132 @@
import { findByProps } from "@vendetta/metro";
import { rawFindByTypeName } from "@shared/lib/rawFind";
declare const window: any;
const WORK_TAGS: Record<number, string> = {
0: "FunctionComponent", 1: "ClassComponent", 3: "HostRoot", 5: "HostComponent",
6: "HostText", 7: "Fragment", 8: "Mode", 9: "ContextConsumer", 10: "ContextProvider",
11: "ForwardRef", 13: "SuspenseComponent", 14: "MemoComponent", 15: "SimpleMemoComponent"
};
function tagName(tag: number): string {
return WORK_TAGS[tag] ?? `tag ${tag}`;
}
/**
* Diagnoses exactly why forceRerender isn't making YouBar+'s buttons show up live, instead of
* guessing again from decompiled source with no device to check against. Every step is
* independently reported so a single failed assumption doesn't hide whether everything after it
* would've worked - e.g. "component not found" vs "found, but no fiber roots" vs "fiber found, but
* zero class-component ancestors" are very different bugs with very different fixes.
*/
export function runYouBarDiagnostics(): string {
const lines: string[] = [`YouBar+ diagnostics - ${new Date().toISOString()}`, ""];
// Step 1: is the component itself findable right now?
const target = rawFindByTypeName("YouBarNotificationsButton");
lines.push(`1. rawFindByTypeName("YouBarNotificationsButton") -> ${target ? "FOUND" : "NOT FOUND"}`);
if (target) {
lines.push(` typeof target = ${typeof target}`);
lines.push(` has .type (inner render fn) = ${typeof target?.type === "function"}`);
lines.push(` has .compare = ${"compare" in (target ?? {})}`);
lines.push(` $$typeof = ${String(target?.$$typeof ?? "none")}`);
}
lines.push("");
// Step 2: does the devtools hook exist at all, and does it know about any renderers?
const hook = window?.__REACT_DEVTOOLS_GLOBAL_HOOK__;
lines.push(`2. window.__REACT_DEVTOOLS_GLOBAL_HOOK__ -> ${hook ? "present" : "MISSING"}`);
if (hook) {
lines.push(` typeof getFiberRoots = ${typeof hook.getFiberRoots}`);
lines.push(` renderers = ${hook.renderers ? `Map(${hook.renderers.size ?? "?"})` : "MISSING"}`);
}
lines.push("");
// Step 3: for each renderer, how many fiber roots does it report, and can we find our target
// fiber by walking them?
let foundFiber: any = null;
if (hook?.getFiberRoots && hook?.renderers?.keys && target) {
let rendererCount = 0;
for (const rendererID of hook.renderers.keys()) {
rendererCount++;
let roots: Set<any> | undefined;
try {
roots = hook.getFiberRoots(rendererID);
} catch (e) {
lines.push(`3. renderer ${rendererID}: getFiberRoots threw: ${e}`);
continue;
}
lines.push(`3. renderer ${rendererID}: ${roots ? roots.size : 0} fiber root(s)`);
if (roots) {
for (const root of roots) {
if (!foundFiber) foundFiber = findFiberByType(root.current, target);
}
}
}
if (rendererCount === 0) lines.push("3. hook.renderers is empty - React never registered with it");
} else {
lines.push("3. skipped (no hook, no renderers, or component not found in step 1)");
}
lines.push(` target fiber found by tree walk -> ${foundFiber ? "YES" : "NO"}`);
lines.push("");
// Step 4: if we found the fiber, walk every ancestor and report its type - this is the actual
// question forceRerender's bump() depends on: does a class component exist anywhere above us?
if (foundFiber) {
lines.push("4. Ancestor chain from target fiber up to root:");
let node = foundFiber;
let depth = 0;
let sawClassComponent = false;
while (node && depth < 60) {
const hasForceUpdate = typeof node.stateNode?.forceUpdate === "function";
if (hasForceUpdate) sawClassComponent = true;
lines.push(` [${depth}] ${tagName(node.tag)}${hasForceUpdate ? " <- has forceUpdate()" : ""}`);
node = node.return;
depth++;
}
lines.push(` class-component ancestor with forceUpdate found -> ${sawClassComponent ? "YES" : "NO"}`);
} else {
lines.push("4. skipped (no fiber found in step 3)");
}
lines.push("");
// Step 5: the fallback mechanism - can we force an update via a genuine, public navigation
// no-op instead of walking fiber internals at all?
const navModule = findByProps("getRootNavigationRef");
lines.push(`5. findByProps("getRootNavigationRef") -> ${navModule ? "FOUND" : "NOT FOUND"}`);
if (navModule) {
try {
const navRef = navModule.getRootNavigationRef?.();
lines.push(` getRootNavigationRef() -> ${navRef ? "returned a ref" : "null/undefined"}`);
if (navRef) {
lines.push(` typeof isReady = ${typeof navRef.isReady}`);
lines.push(` isReady() = ${(() => { try { return navRef.isReady?.(); } catch (e) { return `threw: ${e}`; } })()}`);
lines.push(` typeof getCurrentRoute = ${typeof navRef.getCurrentRoute}`);
const route = (() => { try { return navRef.getCurrentRoute?.(); } catch { return undefined; } })();
lines.push(` getCurrentRoute() -> ${route ? JSON.stringify({ name: route.name, params: route.params }) : "none"}`);
lines.push(` typeof navigate = ${typeof navRef.navigate}`);
lines.push(` typeof setParams = ${typeof navRef.setParams}`);
}
} catch (e) {
lines.push(` threw: ${e}`);
}
}
return lines.join("\n");
}
/** Same shape as forceRerender.ts's own walk - kept identical on purpose so this reports the truth. */
function findFiberByType(root: any, target: any): any {
const stack = root ? [root] : [];
let guard = 0;
while (stack.length) {
if (++guard > 200_000) return null;
const fiber = stack.pop();
if (!fiber) continue;
if (fiber.type === target || fiber.elementType === target) return fiber;
if (fiber.child) stack.push(fiber.child);
if (fiber.sibling) stack.push(fiber.sibling);
}
return null;
}

View file

@ -7,6 +7,7 @@ import NoteBox from "@shared/ui/NoteBox";
import PrimaryButton from "@shared/ui/PrimaryButton";
import { runAllChecks, formatReport } from "../lib/checks";
import { dumpVendettaApiTree } from "../lib/apiTree";
import { runYouBarDiagnostics } from "../lib/youBarDiagnostics";
const { View, Text } = ReactNative;
const { FormSection, FormInput } = Forms;
@ -35,6 +36,29 @@ function FullScanSection() {
);
}
function YouBarDiagnosticsSection() {
return (
<FormSection title="YouBar+ diagnostics">
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<PrimaryButton
label="Run YouBar+ diagnostics & copy report"
onPress={() => {
const report = runYouBarDiagnostics();
clipboard.setString(report);
showToast("Diagnostics copied - paste into chat", undefined);
}}
/>
<Text style={{ marginTop: 8, fontSize: 12.5, opacity: 0.7 }}>
Checks whether YouBarNotificationsButton can be found live, whether the React
DevTools fiber-root registry forceRerender depends on is actually populated, and
whether a class-component ancestor or the navigation ref fallback exists at all -
run this right after toggling YouBar+ on/off to see exactly which step is failing.
</Text>
</View>
</FormSection>
);
}
function ApiTreeSection() {
return (
<FormSection title="Full Vendetta API tree">
@ -129,6 +153,7 @@ export default function Settings() {
return (
<SettingsScaffold>
<FullScanSection />
<YouBarDiagnosticsSection />
<ApiTreeSection />
<ManualSearchSection />
<NoteBox>