@buoy-gg/highlight-updates 2.1.10 → 2.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/lib/commonjs/highlight-updates/HighlightUpdatesOverlay.js +285 -1
  2. package/lib/commonjs/highlight-updates/components/HighlightFilterView.js +1371 -1
  3. package/lib/commonjs/highlight-updates/components/HighlightUpdatesModal.js +591 -1
  4. package/lib/commonjs/highlight-updates/components/IdentifierBadge.js +267 -1
  5. package/lib/commonjs/highlight-updates/components/IsolatedRenderList.js +178 -1
  6. package/lib/commonjs/highlight-updates/components/ModalHeaderContent.js +303 -1
  7. package/lib/commonjs/highlight-updates/components/RenderCauseBadge.js +500 -1
  8. package/lib/commonjs/highlight-updates/components/RenderDetailView.js +830 -1
  9. package/lib/commonjs/highlight-updates/components/RenderHistoryViewer.js +894 -1
  10. package/lib/commonjs/highlight-updates/components/RenderListItem.js +220 -1
  11. package/lib/commonjs/highlight-updates/components/StatsDisplay.js +70 -1
  12. package/lib/commonjs/highlight-updates/components/index.js +97 -1
  13. package/lib/commonjs/highlight-updates/utils/HighlightUpdatesController.js +1435 -1
  14. package/lib/commonjs/highlight-updates/utils/PerformanceLogger.js +359 -1
  15. package/lib/commonjs/highlight-updates/utils/ProfilerInterceptor.js +371 -1
  16. package/lib/commonjs/highlight-updates/utils/RenderCauseDetector.js +1828 -1
  17. package/lib/commonjs/highlight-updates/utils/RenderTracker.js +903 -1
  18. package/lib/commonjs/highlight-updates/utils/ViewTypeMapper.js +264 -1
  19. package/lib/commonjs/highlight-updates/utils/renderExportFormatter.js +58 -1
  20. package/lib/commonjs/index.js +311 -1
  21. package/lib/commonjs/preset.js +278 -1
  22. package/lib/module/highlight-updates/HighlightUpdatesOverlay.js +278 -1
  23. package/lib/module/highlight-updates/components/HighlightFilterView.js +1365 -1
  24. package/lib/module/highlight-updates/components/HighlightUpdatesModal.js +585 -1
  25. package/lib/module/highlight-updates/components/IdentifierBadge.js +259 -1
  26. package/lib/module/highlight-updates/components/IsolatedRenderList.js +174 -1
  27. package/lib/module/highlight-updates/components/ModalHeaderContent.js +298 -1
  28. package/lib/module/highlight-updates/components/RenderCauseBadge.js +491 -1
  29. package/lib/module/highlight-updates/components/RenderDetailView.js +826 -1
  30. package/lib/module/highlight-updates/components/RenderHistoryViewer.js +888 -1
  31. package/lib/module/highlight-updates/components/RenderListItem.js +215 -1
  32. package/lib/module/highlight-updates/components/StatsDisplay.js +67 -1
  33. package/lib/module/highlight-updates/components/index.js +16 -1
  34. package/lib/module/highlight-updates/utils/HighlightUpdatesController.js +1431 -1
  35. package/lib/module/highlight-updates/utils/PerformanceLogger.js +353 -1
  36. package/lib/module/highlight-updates/utils/ProfilerInterceptor.js +358 -1
  37. package/lib/module/highlight-updates/utils/RenderCauseDetector.js +1818 -1
  38. package/lib/module/highlight-updates/utils/RenderTracker.js +900 -1
  39. package/lib/module/highlight-updates/utils/ViewTypeMapper.js +255 -1
  40. package/lib/module/highlight-updates/utils/renderExportFormatter.js +54 -1
  41. package/lib/module/index.js +71 -1
  42. package/lib/module/preset.js +272 -1
  43. package/lib/typescript/highlight-updates/HighlightUpdatesOverlay.d.ts.map +1 -0
  44. package/lib/typescript/highlight-updates/components/HighlightFilterView.d.ts.map +1 -0
  45. package/lib/typescript/highlight-updates/components/HighlightUpdatesModal.d.ts.map +1 -0
  46. package/lib/typescript/highlight-updates/components/IdentifierBadge.d.ts.map +1 -0
  47. package/lib/typescript/highlight-updates/components/IsolatedRenderList.d.ts.map +1 -0
  48. package/lib/typescript/highlight-updates/components/ModalHeaderContent.d.ts.map +1 -0
  49. package/lib/typescript/highlight-updates/components/RenderCauseBadge.d.ts.map +1 -0
  50. package/lib/typescript/highlight-updates/components/RenderDetailView.d.ts.map +1 -0
  51. package/lib/typescript/highlight-updates/components/RenderHistoryViewer.d.ts.map +1 -0
  52. package/lib/typescript/highlight-updates/components/RenderListItem.d.ts.map +1 -0
  53. package/lib/typescript/highlight-updates/components/StatsDisplay.d.ts.map +1 -0
  54. package/lib/typescript/highlight-updates/components/index.d.ts.map +1 -0
  55. package/lib/typescript/highlight-updates/utils/HighlightUpdatesController.d.ts.map +1 -0
  56. package/lib/typescript/highlight-updates/utils/PerformanceLogger.d.ts.map +1 -0
  57. package/lib/typescript/highlight-updates/utils/ProfilerInterceptor.d.ts.map +1 -0
  58. package/lib/typescript/highlight-updates/utils/RenderCauseDetector.d.ts.map +1 -0
  59. package/lib/typescript/highlight-updates/utils/RenderTracker.d.ts.map +1 -0
  60. package/lib/typescript/highlight-updates/utils/ViewTypeMapper.d.ts.map +1 -0
  61. package/lib/typescript/highlight-updates/utils/renderExportFormatter.d.ts.map +1 -0
  62. package/lib/typescript/index.d.ts.map +1 -0
  63. package/lib/typescript/preset.d.ts.map +1 -0
  64. package/package.json +7 -7
@@ -1 +1,1828 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.capturePropsSnapshot=capturePropsSnapshot,exports.captureStateSnapshot=captureStateSnapshot,exports.clearRenderCauseState=clearRenderCauseState,exports.detectRenderCause=detectRenderCause,exports.getRenderCauseStats=getRenderCauseStats,exports.removeRenderCauseState=removeRenderCauseState,exports.safeCloneForHistory=safeCloneForHistory;const COMPONENT_PROP_CONFIGS={RCTText:{alwaysTrack:["children"],skip:[],description:"Text component - children is the displayed content"},RCTVirtualText:{alwaysTrack:["children"],skip:[],description:"Virtual Text (nested) - children is the displayed content"},RCTView:{alwaysTrack:[],skip:["children"],description:"View container - children are React elements"},RCTImageView:{alwaysTrack:["source"],skip:["children"],description:"Image - source contains the image URL/require"},RCTTextInput:{alwaysTrack:["value","defaultValue"],skip:["children"],description:"TextInput - value is the input content"},RCTSwitch:{alwaysTrack:["value"],skip:["children"],description:"Switch - value is the toggle state"},default:{alwaysTrack:[],skip:["children"],description:"Unknown component type"}};function getComponentPropConfig(e){return e&&COMPONENT_PROP_CONFIGS[e]||COMPONENT_PROP_CONFIGS.default}const componentFiberStates=new WeakMap;function getComponentFiberPrevState(e){if(!e)return;let o=componentFiberStates.get(e);return o||(e.alternate&&(o=componentFiberStates.get(e.alternate),o)?o:void 0)}function setComponentFiberState(e,o){e&&(componentFiberStates.set(e,o),e.alternate&&componentFiberStates.set(e.alternate,o))}const previousStates=new Map,MAX_STORED_STATES=500,MAX_CHANGED_KEYS=10,MAX_HOOK_DEPTH=50,MAX_PARENT_DEPTH=50,INTERNAL_COMPONENT_NAMES=new Set(["View","Text","TextImpl","Image","ScrollView","FlatList","SectionList","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","Pressable","TextInput","Switch","ActivityIndicator","Modal","StatusBar","KeyboardAvoidingView","AnimatedComponent","AnimatedComponentWrapper","Fragment","Suspense","Provider","Consumer","Context","ForwardRef","Unknown","Component"]);function isInternalComponentName(e){return!e||!!INTERNAL_COMPONENT_NAMES.has(e)||!!e.startsWith("Animated")}function getOwningComponentFiber(e){if(!e)return null;let o=e._debugOwner||e.return,t=0,n=null;for(;o&&t<30;){const e=o.tag;if((0===e||1===e||11===e||15===e)&&(n||(n=o),!isInternalComponentName(getComponentNameFromFiber(o))))return o;o=o.return,t++}return n}function getComponentNameFromFiber(e){if(!e)return null;const o=e.type;if(o){if("string"==typeof o)return o;if(o.name)return o.name;if(o.displayName)return o.displayName}return null}function detectHookType(e){if(!e||"object"!=typeof e)return"unknown";if(null!==e.queue&&void 0!==e.queue)return"useState";const o=e.memoizedState;if(null!==o&&"object"==typeof o&&!Array.isArray(o)&&"current"in o&&1===Object.keys(o).length)return"useRef";if(Array.isArray(o)&&2===o.length){const[,e]=o;if(Array.isArray(e)||null===e)return"function"==typeof o[0]?"useCallback":"useMemo"}return null!==o&&"object"==typeof o&&!Array.isArray(o)&&("tag"in o||"create"in o||"destroy"in o)?"useEffect":"unknown"}function extractHookValue(e,o){const t=e?.memoizedState;switch(o){case"useState":case"useReducer":default:return t;case"useRef":return t?.current;case"useMemo":case"useCallback":return Array.isArray(t)?t[0]:t;case"useEffect":return"[effect]"}}function extractHookStates(e){if(!e?.memoizedState)return null;const o=e.memoizedState;if("object"!=typeof o||null===o)return null;if(!("next"in o)&&!("queue"in o)&&!("memoizedState"in o))return null;const t=[];let n=o,r=0;for(;null!==n&&r<MAX_HOOK_DEPTH;){const e=detectHookType(n),o=extractHookValue(n,e);t.push({index:r,type:e,value:o,rawState:n.memoizedState}),n=n.next,r++}return t.length>0?t:null}function compareHookStates(e,o){if(!e||!o)return null;const t=[],n=Math.max(e.length,o.length);for(let r=0;r<n;r++){const n=e[r],a=o[r];if(n||!a)if(!n||a){if(n&&a){if(n.rawState===a.rawState)continue;if("useEffect"===a.type)continue;if("useMemo"===a.type||"useCallback"===a.type)continue;if("useState"===a.type||"useReducer"===a.type||"useRef"===a.type){const e=formatHookValue(n.value,n.type),o=formatHookValue(a.value,a.type);t.push({index:r,type:a.type,previousValue:e,currentValue:o,description:`${e} → ${o}`})}}}else t.push({index:r,type:n.type,previousValue:formatHookValue(n.value,n.type),description:`Hook[${r}] removed`});else t.push({index:r,type:a.type,currentValue:formatHookValue(a.value,a.type),description:`Hook[${r}] added`})}return t.length>0?t:null}function formatHookValue(e,o){return formatDisplayValue(e)}function formatDisplayValue(e){return null===e?null:void 0!==e?"boolean"==typeof e||"number"==typeof e?e:"string"==typeof e?e.length>30?e.slice(0,30)+"...":e:"function"==typeof e?`[Function: ${e.name||"anonymous"}]`:Array.isArray(e)?`[Array: ${e.length} items]`:"object"==typeof e?`{Object: ${Object.keys(e).length} keys}`:String(e):void 0}function detectComponentCause(e){if(!e)return{cause:"unknown",hookChanges:null};let o=e,t=e.alternate;const n=getComponentFiberPrevState(e);if(n&&t){const r=extractHookStates(e),a=extractHookStates(t),s=n.extractedHooks;if(r&&a&&s&&s.length>0){const n=s.find(e=>"useState"===e.type);if(n){const s=r.find(e=>e.index===n.index),i=a.find(e=>e.index===n.index);if(s&&i){const r=s.value===n.value,a=i.value===n.value;r&&!a&&(o=t,t=e)}}}}const r=extractHookStates(o);let a=null,s=null,i=null;return t?(a=t.memoizedProps,s=t.memoizedState,i=extractHookStates(t)):n&&(a=n.memoizedProps,s=n.memoizedState,i=n.extractedHooks),setComponentFiberState(o,{memoizedProps:o.memoizedProps,memoizedState:o.memoizedState,extractedHooks:r}),null===a?{cause:"mount",hookChanges:null}:shallowEqual(a,o.memoizedProps)?s!==o.memoizedState?{cause:"state",hookChanges:compareHookStates(i,r)}:{cause:"parent",hookChanges:null}:{cause:"props",hookChanges:null}}function shallowEqual(e,o){if(e===o)return!0;if(!e||!o)return!1;if("object"!=typeof e||"object"!=typeof o)return!1;const t=Object.keys(e),n=Object.keys(o);if(t.length!==n.length)return!1;for(const n of t)if(e[n]!==o[n])return!1;return!0}function safeStringify(e,o=3){const t=new WeakSet;return function e(n,r){if(r>o)return"[MAX_DEPTH]";if(null===n)return null;if(void 0===n)return;if("function"==typeof n)return`[Function: ${n.name||"anonymous"}]`;if("symbol"==typeof n)return`[Symbol: ${n.toString()}]`;if("object"!=typeof n)return n;if(t.has(n))return"[Circular]";if(t.add(n),Array.isArray(n))return n.slice(0,10).map(o=>e(o,r+1));const a={},s=Object.keys(n).slice(0,20);for(const o of s)try{a[o]=e(n[o],r+1)}catch{a[o]="[Error accessing]"}return a}(e,0)}function logRawFiberData(e,o,t,n,r,a){const s=getComponentNameFromFiber(t)||"Unknown";if(console.log("\n========================================"),console.log("[RN-BUOY DEBUG] RENDER EVENT"),console.log("========================================"),console.log(`Native Tag: ${e}`),console.log(`Component Name: ${s}`),console.log(`Is First Render: ${!n}`),console.log(`Component Cause Detected: ${a}`),console.log("----------------------------------------"),console.log("\n--- NATIVE FIBER (Host Component) ---"),console.log("fiber.type:",o?.type),console.log("fiber.tag:",o?.tag),console.log("fiber.memoizedProps (CURRENT):",safeStringify(o?.memoizedProps)),console.log("fiber.memoizedState:",safeStringify(o?.memoizedState)),n&&(console.log("PREVIOUS memoizedProps:",safeStringify(n.memoizedProps)),console.log("PREVIOUS memoizedState:",safeStringify(n.memoizedState))),console.log("\n--- COMPONENT FIBER (React Component) ---"),t){console.log("componentFiber.type:",t?.type?.name||t?.type),console.log("componentFiber.tag:",t?.tag),console.log("componentFiber.memoizedProps:",safeStringify(t?.memoizedProps)),console.log("componentFiber.memoizedState:",safeStringify(t?.memoizedState)),console.log("\n--- HOOKS STATE (linked list walk) ---");let e=t?.memoizedState,o=0;for(;e&&o<10;)console.log(`Hook[${o}]:`,safeStringify({memoizedState:e.memoizedState,baseState:e.baseState,queue:e.queue?"[Queue object]":null})),e=e.next,o++;console.log("\n--- EXTRACTED HOOKS (Phase 3) ---");const n=extractHookStates(t);if(n)for(const e of n)console.log(`Hook[${e.index}] (${e.type}):`,formatDisplayValue(e.value));else console.log("(No hooks extracted)")}else console.log("(No component fiber found)");console.log("\n--- CHILDREN/TEXT CONTENT ---");const i=o?.memoizedProps?.children;console.log("children type:",typeof i),console.log("children value:",safeStringify(i)),console.log("\n========================================\n")}function getTagName(e){return void 0===e?"undefined":{0:"FunctionComponent",1:"ClassComponent",2:"IndeterminateComponent",3:"HostRoot",4:"HostPortal",5:"HostComponent",6:"HostText",7:"Fragment",8:"Mode",9:"ContextConsumer",10:"ContextProvider",11:"ForwardRef",12:"Profiler",13:"SuspenseComponent",14:"MemoComponent",15:"SimpleMemoComponent"}[e]||`Unknown(${e})`}function logMinimal(e,o){if(o.hookChanges&&0!==o.hookChanges.length)for(const t of o.hookChanges)console.log(`[${e||"Unknown"}] ${t.type}[${t.index}]: ${t.previousValue} → ${t.currentValue}`)}function logVerbose(e,o,t,n,r){const a=getComponentNameFromFiber(t)||"Unknown",s="string"==typeof o?.type?o.type:"Unknown",{cause:i,hookChanges:l}=n;if(console.log(`[RENDER] ${a} (${s}:${e}) | Cause: ${i.toUpperCase()}`+(r&&r.length>0?` | Props: [${r.join(", ")}]`:"")),l&&l.length>0)for(const e of l)console.log(` └─ ${e.type}[${e.index}]: ${e.previousValue} → ${e.currentValue}`)}function logComprehensiveRenderData(e,o,t,n,r,a,s){const i=getComponentNameFromFiber(t)||"Unknown",l="string"==typeof o?.type?o.type:"Unknown";console.log("\n[RN-BUOY RENDER DEBUG] ═══════════════════════════════════════"),console.log(`Render #${s} for ${l} (nativeTag: ${e})`),console.log(`Timestamp: ${(new Date).toISOString()}`),console.log("═══════════════════════════════════════════════════════════════\n"),console.log(`NATIVE FIBER (${l}):`),console.log(` type: "${o?.type}"`),console.log(` tag: ${o?.tag} (${getTagName(o?.tag)})`);const c=o?.alternate,u=c?"alternate":n?"storage":"none";console.log(` Previous values source: ${u}`);const m=o?.memoizedProps?.children,p=c?.memoizedProps?.children,f=void 0!==p?p:n?.memoizedProps?.children,g=void 0!==f&&m!==f;console.log(` memoizedProps.children: ${formatDisplayValue(m)}${g?` (was: ${formatDisplayValue(f)})`:void 0!==f?" (unchanged)":" (first render)"}`);const d=o?.memoizedProps?.style,y=c?.memoizedProps?.style,S=void 0!==y?y:n?.memoizedProps?.style,h=void 0!==S&&d!==S;if(d&&(console.log(` memoizedProps.style: ${JSON.stringify(safeStringify(d,2))}${h?" (REFERENCE CHANGED)":""}`),h&&S)){const e=deepEqual(d,S);console.log(` └─ Values actually changed: ${!e}`)}console.log(` All memoizedProps keys: [${Object.keys(o?.memoizedProps||{}).join(", ")}]`);const C=o?.memoizedProps?.testID,b=o?.memoizedProps?.nativeID,k=o?.memoizedProps?.accessibilityLabel;if(C&&console.log(` testID: "${C}"`),b&&console.log(` nativeID: "${b}"`),k&&console.log(` accessibilityLabel: "${k}"`),console.log(" alternate: "+(c?"YES":"NO")),c&&console.log(` alternate.memoizedProps.children: ${formatDisplayValue(p)}`),console.log(" Tree structure:"),console.log(` return (parent): ${o?.return?getTagName(o.return.tag):"null"}`),console.log(` child: ${o?.child?getTagName(o.child.tag):"null"}`),console.log(` sibling: ${o?.sibling?getTagName(o.sibling.tag):"null"}`),console.log(""),console.log(`COMPONENT FIBER (${i}):`),t){if(console.log(` name: "${i}"`),console.log(` type: ${typeof t.type} (${t.type?.name||t.type?.displayName||"anonymous"})`),console.log(` tag: ${t.tag} (${getTagName(t.tag)})`),console.log(` memoizedProps: ${JSON.stringify(safeStringify(t.memoizedProps,2))}`),console.log(" alternate: "+(t.alternate?"YES":"NO")),t.alternate){console.log(` ALTERNATE memoizedProps: ${JSON.stringify(safeStringify(t.alternate.memoizedProps,2))}`);const e=!shallowEqual(t.alternate.memoizedProps,t.memoizedProps);console.log(" Props changed (vs alternate): "+(e?"YES":"NO")),console.log(" ALTERNATE memoizedState: "+(t.alternate.memoizedState===t.memoizedState?"SAME":"DIFFERENT"))}const e=getComponentFiberPrevState(t);if(e){console.log(` WeakMap PREVIOUS memoizedProps: ${JSON.stringify(safeStringify(e.memoizedProps,2))}`);const o=!shallowEqual(e.memoizedProps,t.memoizedProps);console.log(" Props changed (vs WeakMap): "+(o?"YES":"NO"))}else console.log(" WeakMap PREVIOUS state: (not found - first render or WeakMap cleared)");if(t._debugOwner){const e=getComponentNameFromFiber(t._debugOwner);console.log(` _debugOwner: "${e}"`);let o=t._debugOwner,n=1;for(;o&&n<5;){const e=getComponentNameFromFiber(o);console.log(` └─[${n}] ${e||"unknown"} (tag: ${o.tag})`),o=o._debugOwner,n++}}let n=0,r=o._debugOwner||o.return;for(;r&&r!==t&&n<30;)r=r.return,n++;console.log(` Depth from native fiber: ${n}`)}else console.log(" (No component fiber found - could not walk up tree)");if(console.log(""),console.log("HOOKS:"),t?.memoizedState){const e=extractHookStates(t),o=t.alternate?extractHookStates(t.alternate):null,n=getComponentFiberPrevState(t),r=o||n?.extractedHooks,a=o?"alternate":n?"WeakMap":"none";e&&e.length>0?(console.log(` Total hooks: ${e.length}`),console.log(` Previous values source: ${a}`),e.forEach((e,o)=>{const t=r?.[o],n=!!t&&t.rawState!==e.rawState,a=t?formatDisplayValue(t.value):"N/A",s=n?" ← CHANGED":"";console.log(` [${o}] ${e.type}: ${formatDisplayValue(e.value)}${t?` (was: ${a})`:" (first render)"}${s}`)})):(console.log(" (Could not extract hooks - memoizedState structure not recognized)"),console.log(" Raw memoizedState type: "+typeof t.memoizedState),console.log(` Has 'next' property: ${"next"in(t.memoizedState||{})}`),console.log(` Has 'queue' property: ${"queue"in(t.memoizedState||{})}`))}else console.log(" (No memoizedState - class component, no hooks, or not a function component)");if(console.log(""),console.log("RAW HOOKS DATA:"),t?.memoizedState&&"object"==typeof t.memoizedState){let e=t.memoizedState,o=0;for(;e&&o<10;)console.log(` Hook[${o}]:`),console.log(` memoizedState: ${safeStringify(e.memoizedState,2)}`),console.log(` baseState: ${safeStringify(e.baseState,2)}`),console.log(" queue: "+(e.queue?"[Queue present]":"null")),console.log(" next: "+(e.next?"[Next hook]":"null")),e=e.next,o++;0===o&&console.log(" (memoizedState is not a hooks linked list)")}console.log(""),console.log("DETECTION RESULTS:"),console.log(` Component Cause: ${r.cause.toUpperCase()}`),r.hookChanges&&r.hookChanges.length>0&&(console.log(" Hook Changes Detected:"),r.hookChanges.forEach(e=>{console.log(` [${e.index}] ${e.type}: ${e.previousValue} → ${e.currentValue}`),e.description&&console.log(` ${e.description}`)}));const N="string"==typeof o?.type?o.type:void 0,T=getChangedKeys(n?.memoizedProps,o?.memoizedProps,N);T&&T.length>0?console.log(` Native Props Changed: [${T.join(", ")}]`):n&&console.log(" Native Props Changed: (none detected)"),console.log(""),console.log("BATCH CONTEXT:"),console.log(` Batch size: ${a.size}`),console.log(` All tags in batch: [${Array.from(a).slice(0,20).join(", ")}${a.size>20?"...":""}]`);const E=getParentNativeTag(o),z=null!==E&&a.has(E);if(console.log(` Parent nativeTag: ${E??"not found"}`),console.log(" Parent in batch: "+(z?"YES":"NO")),a.size>1){console.log(" Components in same batch:");let e=o?.return,t=0;for(;e&&t<10;){const o=getNativeTagFromStateNode(e.stateNode);if(null!==o&&a.has(o)){const n=getComponentNameFromFiber(e)||e.type;console.log(` [depth ${t}] ${n} (tag: ${o})`)}e=e.return,t++}}console.log("\n═══════════════════════════════════════════════════════════════\n")}function detectRenderCause(e,o,t,n="off"){const r=Date.now();if(!o)return{type:"unknown",timestamp:r};let a=o,s=o.alternate;const i=previousStates.get(e);if(i&&s){const e=i.memoizedProps?.children,t=o.memoizedProps?.children,r=s.memoizedProps?.children,l=o.memoizedProps===i.memoizedProps||void 0!==e&&t===e,c=s.memoizedProps===i.memoizedProps||void 0!==e&&r===e;l&&!c&&(a=s,s=o,"all"===n&&console.log("[RenderCause] Detected fiber swap - using alternate as current"))}let l=null,c=null;s?(l=s.memoizedProps,c=s.memoizedState):i&&(l=i.memoizedProps,c=i.memoizedState);const u=null!==l?{memoizedProps:l,memoizedState:c,timestamp:r}:void 0,m={memoizedProps:a.memoizedProps,memoizedState:a.memoizedState,timestamp:r};updateStoredState(e,m);const p=getOwningComponentFiber(a),f=getComponentNameFromFiber(p)||void 0,g=getParentComponentName(a,f)||void 0,d=detectComponentCause(p),y=d.cause,S=d.hookChanges;if(!u){if("off"!==n)if("minimal"===n)console.log(`[${f||"Unknown"}] MOUNT`);else if("verbose"===n){const o="string"==typeof a?.type?a.type:"Unknown";console.log(`[RENDER] ${f||"Unknown"} (${o}:${e}) | Cause: MOUNT`)}else"all"===n&&logComprehensiveRenderData(e,a,p,u,d,t,previousStates.has(e)?previousStates.size:1);return{type:"mount",timestamp:r,componentCause:"mount",componentName:f}}const h="string"==typeof a.type?a.type:void 0,C=getChangedKeys(u.memoizedProps,m.memoizedProps,h);if("off"!==n&&("minimal"===n?logMinimal(f,d):"verbose"===n?logVerbose(e,a,p,d,C):"all"===n&&logComprehensiveRenderData(e,a,p,u,d,t,previousStates.has(e)?previousStates.size:1)),C&&C.length>0)return{type:"props",changedKeys:C.slice(0,MAX_CHANGED_KEYS),timestamp:r,componentCause:y,componentName:f,parentComponentName:g,hookChanges:S||void 0};const b=detectHookChanges(u.memoizedState,m.memoizedState);if(b&&b.length>0)return{type:"hooks",hookIndices:b,timestamp:r,componentCause:y,componentName:f,parentComponentName:g,hookChanges:S||void 0};const k=getParentNativeTag(a);return k&&t.has(k)?{type:"parent",timestamp:r,componentCause:y,componentName:f,parentComponentName:g,hookChanges:S||void 0}:"state"===y?{type:"state",timestamp:r,componentCause:y,componentName:f,parentComponentName:g,hookChanges:S||void 0}:"props"===y?{type:"props",timestamp:r,componentCause:y,componentName:f,parentComponentName:g}:"parent"===y?{type:"parent",timestamp:r,componentCause:y,componentName:f,parentComponentName:g}:{type:"unknown",timestamp:r,componentCause:y,componentName:f,parentComponentName:g}}function getChangedKeys(e,o,t){if(e===o)return null;if(!e||!o)return null;if("object"!=typeof e||"object"!=typeof o)return null;if(Array.isArray(e)||Array.isArray(o))return null;const n=getComponentPropConfig(t),r=new Set([...Object.keys(e),...Object.keys(o)]),a=[];for(const t of r)if(!t.startsWith("__")&&!n.skip.includes(t)){if("children"===t){if(!n.alwaysTrack.includes("children"))continue;const r=e[t],s=o[t];r!==s&&(isPrimitive(r)&&isPrimitive(s)?a.push(`children: ${formatValue(r)} → ${formatValue(s)}`):a.push("children (content)"));continue}if(e[t]!==o[t]){const n=e[t],r=o[t];"style"===t?deepEqual(n,r)?a.push(`${t} (ref only)`):a.push(t):"function"==typeof n&&"function"==typeof r?a.push(`${t} (fn ref)`):a.push(t)}}return a.length>0?a:null}function isPlainObject(e){if(null===e||"object"!=typeof e)return!1;if(Array.isArray(e))return!1;const o=Object.getPrototypeOf(e);return o===Object.prototype||null===o}function deepEqual(e,o,t=0){if(t>5)return!1;if(e===o)return!0;if(typeof e!=typeof o)return!1;if("object"!=typeof e||null===e||null===o)return e===o;if(Array.isArray(e)!==Array.isArray(o))return!1;if(Array.isArray(e)){if(e.length!==o.length)return!1;for(let n=0;n<e.length;n++)if(!deepEqual(e[n],o[n],t+1))return!1;return!0}const n=Object.keys(e),r=Object.keys(o);if(n.length!==r.length)return!1;for(const a of n){if(!r.includes(a))return!1;if(!deepEqual(e[a],o[a],t+1))return!1}return!0}function isPrimitive(e){return null==e||"string"==typeof e||"number"==typeof e||"boolean"==typeof e}function formatValue(e){return null===e?"null":void 0===e?"undefined":"string"==typeof e?e.length>20?`"${e.slice(0,20)}..."`:`"${e}"`:"number"==typeof e||"boolean"==typeof e?String(e):"[object]"}function detectHookChanges(e,o){if(e===o)return null;if(null==e||null==o)return null;if("object"!=typeof e||"object"!=typeof o)return null;const t=[];let n=e,r=o,a=0;for(;null!==r&&a<MAX_HOOK_DEPTH;)(null===n||didHookChange(n,r))&&t.push(a),r=r?.next??null,n=n?.next??null,a++;return t.length>0?t:null}function didHookChange(e,o){return e!==o&&(e.memoizedState!==o.memoizedState||void 0!==e.baseState&&e.baseState!==o.baseState)}function getParentNativeTag(e){let o=e?.return,t=0;for(;o&&t<50;){if(o.stateNode){const e=getNativeTagFromStateNode(o.stateNode);if(null!=e)return e}o=o.return,t++}return null}function getParentComponentName(e,o){if(!e)return null;const t=getOwningComponentFiber(e);if(!t)return null;let n=t.return,r=0;for(;n&&r<50;){const e=n.tag;if(0===e||1===e||11===e||15===e){const e=getComponentNameFromFiber(n);if(e&&!isInternalComponentName(e)&&e!==o)return e}n=n.return,r++}return null}function getNativeTagFromStateNode(e){return e?"number"==typeof e.__nativeTag?e.__nativeTag:"number"==typeof e._nativeTag?e._nativeTag:"number"==typeof e.canonical?.__nativeTag?e.canonical.__nativeTag:"number"==typeof e.canonical?.publicInstance?.__nativeTag?e.canonical.publicInstance.__nativeTag:null:null}function updateStoredState(e,o){if(previousStates.size>=MAX_STORED_STATES){const e=Array.from(previousStates.entries());e.sort((e,o)=>e[1].timestamp-o[1].timestamp);const o=Math.floor(MAX_STORED_STATES/4);for(let t=0;t<o;t++)previousStates.delete(e[t][0])}previousStates.set(e,o)}function clearRenderCauseState(){previousStates.clear()}function removeRenderCauseState(e){previousStates.delete(e)}function getRenderCauseStats(){return{storedStates:previousStates.size,maxStates:MAX_STORED_STATES}}const MAX_CLONE_DEPTH=5,MAX_STRING_LENGTH=200,MAX_ARRAY_ITEMS=20,MAX_OBJECT_KEYS=30;function safeCloneForHistory(e,o=0,t=new WeakSet){if(null==e)return e;if("boolean"==typeof e||"number"==typeof e)return e;if("string"==typeof e)return e.length>MAX_STRING_LENGTH?e.slice(0,MAX_STRING_LENGTH)+"...":e;if("function"==typeof e)return`[Function: ${e.name||"anonymous"}]`;if("symbol"==typeof e)return`[Symbol: ${e.description||""}]`;if(o>=MAX_CLONE_DEPTH)return Array.isArray(e)?`[Array: ${e.length} items]`:"object"==typeof e?`[Object: ${Object.keys(e).length} keys]`:"[...]";if("object"==typeof e){if(t.has(e))return"[Circular]";t.add(e)}if(Array.isArray(e)){const n=e.slice(0,MAX_ARRAY_ITEMS).map(e=>safeCloneForHistory(e,o+1,t));return e.length>MAX_ARRAY_ITEMS&&n.push(`[...${e.length-MAX_ARRAY_ITEMS} more]`),n}if(e instanceof Date)return e.toISOString();if(e instanceof Error)return`[Error: ${e.message}]`;if(e instanceof RegExp)return e.toString();if(e instanceof Map){const n={__type:"Map",__size:e.size};let r=0;for(const[a,s]of e){if(r>=MAX_OBJECT_KEYS){n[`...${e.size-r} more`]=!0;break}n["string"==typeof a?a:String(a)]=safeCloneForHistory(s,o+1,t),r++}return n}if(e instanceof Set)return{__type:"Set",__size:e.size,values:Array.from(e).slice(0,MAX_ARRAY_ITEMS).map(e=>safeCloneForHistory(e,o+1,t))};if("object"==typeof e){const n={},r=Object.keys(e),a=r.slice(0,MAX_OBJECT_KEYS);for(const r of a)if(!r.startsWith("__")&&"children"!==r)try{n[r]=safeCloneForHistory(e[r],o+1,t)}catch{n[r]="[Error accessing property]"}return r.length>MAX_OBJECT_KEYS&&(n[`...${r.length-MAX_OBJECT_KEYS} more keys`]=!0),n}return"[Unknown type]"}function capturePropsSnapshot(e){if(e?.memoizedProps)return safeCloneForHistory(e.memoizedProps)}function captureStateSnapshot(e){if(!e?.memoizedState)return;const o=e.memoizedState;if("object"==typeof o&&null!==o&&"next"in o){const e=[];let t=o,n=0;for(;t&&n<MAX_HOOK_DEPTH;)e.push({index:n,memoizedState:safeCloneForHistory(t.memoizedState)}),t=t.next,n++;return{__type:"Hooks",hooks:e}}return safeCloneForHistory(o)}
1
+ /**
2
+ * RenderCauseDetector
3
+ *
4
+ * Detects WHY a component rendered by comparing current fiber state
5
+ * to previously stored state. Supports detection of:
6
+ * - First mount
7
+ * - Props changes (with changed key names)
8
+ * - Hooks/state changes (with hook indices)
9
+ * - Parent re-renders
10
+ *
11
+ * TWO-LEVEL CAUSATION:
12
+ * 1. Native level: Why did the native view's props change? (style, accessibilityState, etc.)
13
+ * 2. Component level: Why did the owning React component re-render? (props, state, parent)
14
+ *
15
+ * This gives the full picture:
16
+ * "StepperValueDisplay re-rendered because PARENT, which caused native Text to update PROPS [style]"
17
+ *
18
+ * COMPONENT-SPECIFIC DETECTION:
19
+ * Different native components have different important props:
20
+ * - RCTText: `children` IS the text content - always track it
21
+ * - RCTView: `children` is React elements - skip it
22
+ * - RCTImageView: `source` is the image URL - track it
23
+ *
24
+ * See: RENDER_CAUSE_TEXT_COMPONENT_PLAN.md for full documentation
25
+ */
26
+
27
+ "use strict";
28
+
29
+ Object.defineProperty(exports, "__esModule", {
30
+ value: true
31
+ });
32
+ exports.capturePropsSnapshot = capturePropsSnapshot;
33
+ exports.captureStateSnapshot = captureStateSnapshot;
34
+ exports.clearRenderCauseState = clearRenderCauseState;
35
+ exports.detectRenderCause = detectRenderCause;
36
+ exports.getRenderCauseStats = getRenderCauseStats;
37
+ exports.removeRenderCauseState = removeRenderCauseState;
38
+ exports.safeCloneForHistory = safeCloneForHistory;
39
+ // ============================================================================
40
+ // TYPE DEFINITIONS
41
+ // Based on React Native source: packages/react-native/Libraries/Renderer/
42
+ // ============================================================================
43
+
44
+ /**
45
+ * React Fiber Work Tags
46
+ * Identifies the type of fiber node in the tree
47
+ *
48
+ * Source: ReactWorkTags.js in React source
49
+ */
50
+
51
+ // IncompleteFunctionComponent
52
+
53
+ /**
54
+ * React Native Host Component Types
55
+ * Native view types that we can detect and handle specifically
56
+ *
57
+ * Source: packages/react-native/Libraries/Components/
58
+ */
59
+
60
+ // Other native components
61
+
62
+ /**
63
+ * Component-specific prop detection configuration
64
+ * Defines which props are meaningful for each native component type
65
+ */
66
+
67
+ /**
68
+ * Detected hook type for categorization
69
+ * Determines what kind of React hook we're looking at
70
+ */
71
+
72
+ /**
73
+ * Extracted hook state with type information
74
+ * Represents a single hook's current state for comparison
75
+ */
76
+
77
+ /**
78
+ * Configuration for each native component type
79
+ * Tells us which props are meaningful to track for render cause detection
80
+ */
81
+ const COMPONENT_PROP_CONFIGS = {
82
+ // Text components - children IS the actual text content
83
+ RCTText: {
84
+ alwaysTrack: ["children"],
85
+ // The displayed text/number
86
+ skip: [],
87
+ description: "Text component - children is the displayed content"
88
+ },
89
+ RCTVirtualText: {
90
+ alwaysTrack: ["children"],
91
+ // Nested text content
92
+ skip: [],
93
+ description: "Virtual Text (nested) - children is the displayed content"
94
+ },
95
+ // View - children is React elements, not meaningful for diff
96
+ RCTView: {
97
+ alwaysTrack: [],
98
+ skip: ["children"],
99
+ // React elements, not useful to track
100
+ description: "View container - children are React elements"
101
+ },
102
+ // Image - source is the important prop
103
+ RCTImageView: {
104
+ alwaysTrack: ["source"],
105
+ skip: ["children"],
106
+ description: "Image - source contains the image URL/require"
107
+ },
108
+ // TextInput - value is important
109
+ RCTTextInput: {
110
+ alwaysTrack: ["value", "defaultValue"],
111
+ skip: ["children"],
112
+ description: "TextInput - value is the input content"
113
+ },
114
+ // Switch - value is the on/off state
115
+ RCTSwitch: {
116
+ alwaysTrack: ["value"],
117
+ skip: ["children"],
118
+ description: "Switch - value is the toggle state"
119
+ },
120
+ // Default for unknown components
121
+ default: {
122
+ alwaysTrack: [],
123
+ skip: ["children"],
124
+ // Skip children by default
125
+ description: "Unknown component type"
126
+ }
127
+ };
128
+
129
+ /**
130
+ * Get the prop configuration for a native component type
131
+ */
132
+ function getComponentPropConfig(fiberType) {
133
+ if (!fiberType) return COMPONENT_PROP_CONFIGS.default;
134
+ return COMPONENT_PROP_CONFIGS[fiberType] || COMPONENT_PROP_CONFIGS.default;
135
+ }
136
+
137
+ /**
138
+ * Stored fiber state for comparison between renders
139
+ */
140
+
141
+ // ============================================================================
142
+ // COMPONENT FIBER STATE STORAGE
143
+ // ============================================================================
144
+
145
+ /**
146
+ * Stored component fiber state for comparison between renders.
147
+ * Now includes extracted hook states for Phase 3 hook value tracking.
148
+ */
149
+
150
+ /**
151
+ * Storage for previous component fiber states
152
+ *
153
+ * IMPORTANT: React uses double-buffering with "current" and "work-in-progress" fibers.
154
+ * The fiber reference changes between renders as React swaps the trees.
155
+ * We need to check both the fiber AND its alternate when looking up state.
156
+ *
157
+ * WeakMap is used to automatically clean up when fibers are garbage collected.
158
+ */
159
+ const componentFiberStates = new WeakMap();
160
+
161
+ /**
162
+ * Get previous state for a component fiber, checking both current and alternate.
163
+ *
164
+ * React's reconciler maintains two fiber trees:
165
+ * - "current" - the tree currently rendered to screen
166
+ * - "workInProgress" - the tree being built for the next render
167
+ *
168
+ * These are linked via the `alternate` property. When a render completes,
169
+ * they swap roles. So the fiber we see this render may be the alternate
170
+ * of what we saw last render.
171
+ *
172
+ * See: packages/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js
173
+ */
174
+ function getComponentFiberPrevState(fiber) {
175
+ if (!fiber) return undefined;
176
+
177
+ // First, try the fiber itself
178
+ let prev = componentFiberStates.get(fiber);
179
+ if (prev) return prev;
180
+
181
+ // If not found, try its alternate (the other side of double-buffering)
182
+ if (fiber.alternate) {
183
+ prev = componentFiberStates.get(fiber.alternate);
184
+ if (prev) return prev;
185
+ }
186
+ return undefined;
187
+ }
188
+
189
+ /**
190
+ * Store state for a component fiber.
191
+ * Stores on both the fiber and its alternate to ensure we find it next render.
192
+ */
193
+ function setComponentFiberState(fiber, state) {
194
+ if (!fiber) return;
195
+
196
+ // Store on the current fiber
197
+ componentFiberStates.set(fiber, state);
198
+
199
+ // Also store on alternate if it exists, so we find it regardless of which
200
+ // side of double-buffering we see next render
201
+ if (fiber.alternate) {
202
+ componentFiberStates.set(fiber.alternate, state);
203
+ }
204
+ }
205
+
206
+ // Storage for previous fiber states
207
+ const previousStates = new Map();
208
+
209
+ // Configuration
210
+ const MAX_STORED_STATES = 500;
211
+ const MAX_CHANGED_KEYS = 10;
212
+ const MAX_HOOK_DEPTH = 50;
213
+ const MAX_PARENT_DEPTH = 50;
214
+
215
+ // React Native internal components to skip when finding user components
216
+ const INTERNAL_COMPONENT_NAMES = new Set([
217
+ // React Native core primitives
218
+ 'View', 'Text', 'TextImpl', 'Image', 'ScrollView', 'FlatList', 'SectionList', 'TouchableOpacity', 'TouchableHighlight', 'TouchableWithoutFeedback', 'Pressable', 'TextInput', 'Switch', 'ActivityIndicator', 'Modal', 'StatusBar', 'KeyboardAvoidingView',
219
+ // Animated components
220
+ 'AnimatedComponent', 'AnimatedComponentWrapper',
221
+ // React internals
222
+ 'Fragment', 'Suspense', 'Provider', 'Consumer', 'Context', 'ForwardRef',
223
+ // Common wrapper names
224
+ 'Unknown', 'Component']);
225
+
226
+ /**
227
+ * Check if a component name is an internal React Native component
228
+ */
229
+ function isInternalComponentName(name) {
230
+ if (!name) return true;
231
+ if (INTERNAL_COMPONENT_NAMES.has(name)) return true;
232
+ // Skip Animated.* wrappers
233
+ if (name.startsWith('Animated')) return true;
234
+ return false;
235
+ }
236
+
237
+ /**
238
+ * Get the owning React component fiber for a host fiber.
239
+ * Walks up the fiber tree to find the first USER-DEFINED component,
240
+ * skipping React Native internal components like View, Text, etc.
241
+ */
242
+ function getOwningComponentFiber(fiber) {
243
+ if (!fiber) return null;
244
+
245
+ // Walk up to find the component that owns this host element
246
+ let current = fiber._debugOwner || fiber.return;
247
+ let depth = 0;
248
+ let firstComponentFiber = null; // Fallback if no user component found
249
+
250
+ while (current && depth < 30) {
251
+ // Function components have tag 0 (FunctionComponent) or 11 (ForwardRef)
252
+ // Class components have tag 1 (ClassComponent)
253
+ // Host components have tag 5 (HostComponent) - skip these
254
+ const tag = current.tag;
255
+
256
+ // Tags: 0=FunctionComponent, 1=ClassComponent, 11=ForwardRef, 15=SimpleMemoComponent
257
+ if (tag === 0 || tag === 1 || tag === 11 || tag === 15) {
258
+ const name = getComponentNameFromFiber(current);
259
+
260
+ // Remember first component as fallback
261
+ if (!firstComponentFiber) {
262
+ firstComponentFiber = current;
263
+ }
264
+
265
+ // Return if this is a user-defined component (not internal)
266
+ if (!isInternalComponentName(name)) {
267
+ return current;
268
+ }
269
+ }
270
+ current = current.return;
271
+ depth++;
272
+ }
273
+
274
+ // Return first component found as fallback (even if internal)
275
+ return firstComponentFiber;
276
+ }
277
+
278
+ /**
279
+ * Get component name from a fiber
280
+ */
281
+ function getComponentNameFromFiber(fiber) {
282
+ if (!fiber) return null;
283
+ const type = fiber.type;
284
+ if (type) {
285
+ if (typeof type === 'string') return type;
286
+ if (type.name) return type.name;
287
+ if (type.displayName) return type.displayName;
288
+ }
289
+ return null;
290
+ }
291
+
292
+ // ============================================================================
293
+ // HOOK STATE EXTRACTION (Phase 3)
294
+ // ============================================================================
295
+
296
+ /**
297
+ * Detect the type of a hook based on its structure.
298
+ *
299
+ * React hooks have different shapes:
300
+ * - useState/useReducer: Has a `queue` property with dispatch function
301
+ * - useRef: memoizedState is { current: value }
302
+ * - useMemo/useCallback: memoizedState is [value, deps] array
303
+ * - useEffect/useLayoutEffect: memoizedState is an effect object with tag
304
+ *
305
+ * See: packages/react-native/Libraries/Renderer/implementations/ReactNativeRenderer-dev.js
306
+ */
307
+ function detectHookType(hookState) {
308
+ if (!hookState || typeof hookState !== 'object') {
309
+ return 'unknown';
310
+ }
311
+
312
+ // useState/useReducer: has a queue with dispatch function
313
+ // This is the most reliable indicator
314
+ if (hookState.queue !== null && hookState.queue !== undefined) {
315
+ // Could be useState or useReducer - both have queue
316
+ // useReducer typically has a more complex reducer, but we can't easily distinguish
317
+ return 'useState';
318
+ }
319
+ const memoizedState = hookState.memoizedState;
320
+
321
+ // useRef: memoizedState is an object with only { current: value }
322
+ if (memoizedState !== null && typeof memoizedState === 'object' && !Array.isArray(memoizedState) && 'current' in memoizedState && Object.keys(memoizedState).length === 1) {
323
+ return 'useRef';
324
+ }
325
+
326
+ // useMemo/useCallback: memoizedState is [value, deps] array
327
+ // The deps is an array used for dependency comparison
328
+ if (Array.isArray(memoizedState) && memoizedState.length === 2) {
329
+ const [, deps] = memoizedState;
330
+ // If second element is an array (deps), it's likely useMemo/useCallback
331
+ if (Array.isArray(deps) || deps === null) {
332
+ // useCallback stores a function, useMemo stores any value
333
+ if (typeof memoizedState[0] === 'function') {
334
+ return 'useCallback';
335
+ }
336
+ return 'useMemo';
337
+ }
338
+ }
339
+
340
+ // useEffect/useLayoutEffect: memoizedState has effect-specific properties
341
+ if (memoizedState !== null && typeof memoizedState === 'object' && !Array.isArray(memoizedState) && ('tag' in memoizedState || 'create' in memoizedState || 'destroy' in memoizedState)) {
342
+ return 'useEffect';
343
+ }
344
+ return 'unknown';
345
+ }
346
+
347
+ /**
348
+ * Extract the actual value from a hook's memoizedState.
349
+ * Different hooks store values differently.
350
+ */
351
+ function extractHookValue(hookState, hookType) {
352
+ const memoizedState = hookState?.memoizedState;
353
+ switch (hookType) {
354
+ case 'useState':
355
+ case 'useReducer':
356
+ // useState/useReducer: memoizedState IS the value directly
357
+ return memoizedState;
358
+ case 'useRef':
359
+ // useRef: memoizedState is { current: value }
360
+ return memoizedState?.current;
361
+ case 'useMemo':
362
+ case 'useCallback':
363
+ // useMemo/useCallback: memoizedState is [value, deps]
364
+ return Array.isArray(memoizedState) ? memoizedState[0] : memoizedState;
365
+ case 'useEffect':
366
+ // useEffect: we don't really have a "value" to show
367
+ return '[effect]';
368
+ default:
369
+ return memoizedState;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Extract all hook states from a fiber's memoizedState linked list.
375
+ *
376
+ * For function components, fiber.memoizedState is a linked list where each
377
+ * node represents one hook call. The nodes are linked via the `next` property.
378
+ *
379
+ * This function walks the list and extracts:
380
+ * - The hook's index (position in the list)
381
+ * - The detected hook type (useState, useRef, useMemo, etc.)
382
+ * - The actual value stored in the hook
383
+ *
384
+ * @param fiber - The component fiber (must be a function component)
385
+ * @returns Array of extracted hook states, or null if no hooks
386
+ */
387
+ function extractHookStates(fiber) {
388
+ if (!fiber?.memoizedState) return null;
389
+
390
+ // Check if this looks like a hooks linked list (has 'next' property)
391
+ // Class components have different memoizedState structure
392
+ const firstHook = fiber.memoizedState;
393
+ if (typeof firstHook !== 'object' || firstHook === null) {
394
+ return null;
395
+ }
396
+
397
+ // If it doesn't have 'next', it might be a class component state object
398
+ // or a single primitive value
399
+ if (!('next' in firstHook) && !('queue' in firstHook) && !('memoizedState' in firstHook)) {
400
+ return null;
401
+ }
402
+ const states = [];
403
+ let hookState = firstHook;
404
+ let index = 0;
405
+
406
+ // Walk the linked list, with safety limit
407
+ while (hookState !== null && index < MAX_HOOK_DEPTH) {
408
+ const hookType = detectHookType(hookState);
409
+ const value = extractHookValue(hookState, hookType);
410
+ states.push({
411
+ index,
412
+ type: hookType,
413
+ value,
414
+ rawState: hookState.memoizedState
415
+ });
416
+ hookState = hookState.next;
417
+ index++;
418
+ }
419
+ return states.length > 0 ? states : null;
420
+ }
421
+
422
+ /**
423
+ * Compare two sets of hook states and find what changed.
424
+ *
425
+ * Returns an array of HookStateChange objects describing each change
426
+ * with meaningful before/after values.
427
+ *
428
+ * @param prevStates - Previous hook states (from last render)
429
+ * @param currentStates - Current hook states
430
+ * @returns Array of changes, or null if no changes detected
431
+ */
432
+ function compareHookStates(prevStates, currentStates) {
433
+ // Can't compare if either is missing
434
+ if (!prevStates || !currentStates) return null;
435
+ const changes = [];
436
+
437
+ // Compare each hook by index
438
+ const maxLength = Math.max(prevStates.length, currentStates.length);
439
+ for (let i = 0; i < maxLength; i++) {
440
+ const prev = prevStates[i];
441
+ const curr = currentStates[i];
442
+
443
+ // New hook added (shouldn't normally happen, but handle it)
444
+ if (!prev && curr) {
445
+ changes.push({
446
+ index: i,
447
+ type: curr.type,
448
+ currentValue: formatHookValue(curr.value, curr.type),
449
+ description: `Hook[${i}] added`
450
+ });
451
+ continue;
452
+ }
453
+
454
+ // Hook removed (shouldn't normally happen)
455
+ if (prev && !curr) {
456
+ changes.push({
457
+ index: i,
458
+ type: prev.type,
459
+ previousValue: formatHookValue(prev.value, prev.type),
460
+ description: `Hook[${i}] removed`
461
+ });
462
+ continue;
463
+ }
464
+
465
+ // Both exist - compare values
466
+ if (prev && curr) {
467
+ // Skip if raw state reference is the same (no change)
468
+ if (prev.rawState === curr.rawState) {
469
+ continue;
470
+ }
471
+
472
+ // For effects, skip (they have complex internal state)
473
+ if (curr.type === 'useEffect') {
474
+ continue;
475
+ }
476
+
477
+ // For useMemo/useCallback, the value changing means deps changed
478
+ // Skip these as they're not typically interesting for debugging
479
+ if (curr.type === 'useMemo' || curr.type === 'useCallback') {
480
+ continue;
481
+ }
482
+
483
+ // Only report useState/useReducer/useRef changes (most useful)
484
+ if (curr.type === 'useState' || curr.type === 'useReducer' || curr.type === 'useRef') {
485
+ const prevFormatted = formatHookValue(prev.value, prev.type);
486
+ const currFormatted = formatHookValue(curr.value, curr.type);
487
+ changes.push({
488
+ index: i,
489
+ type: curr.type,
490
+ previousValue: prevFormatted,
491
+ currentValue: currFormatted,
492
+ description: `${prevFormatted} → ${currFormatted}`
493
+ });
494
+ }
495
+ }
496
+ }
497
+ return changes.length > 0 ? changes : null;
498
+ }
499
+
500
+ /**
501
+ * Format a hook value for display.
502
+ * Handles different value types appropriately.
503
+ */
504
+ function formatHookValue(value, hookType) {
505
+ // For useRef, the value is already unwrapped
506
+ if (hookType === 'useRef') {
507
+ return formatDisplayValue(value);
508
+ }
509
+
510
+ // For useState/useReducer, value is the state directly
511
+ return formatDisplayValue(value);
512
+ }
513
+
514
+ /**
515
+ * Format any value for display, handling special cases.
516
+ */
517
+ function formatDisplayValue(value) {
518
+ if (value === null) return null;
519
+ if (value === undefined) return undefined;
520
+ if (typeof value === 'boolean') return value;
521
+ if (typeof value === 'number') return value;
522
+ if (typeof value === 'string') {
523
+ // Truncate long strings
524
+ return value.length > 30 ? value.slice(0, 30) + '...' : value;
525
+ }
526
+ if (typeof value === 'function') {
527
+ return `[Function: ${value.name || 'anonymous'}]`;
528
+ }
529
+ if (Array.isArray(value)) {
530
+ return `[Array: ${value.length} items]`;
531
+ }
532
+ if (typeof value === 'object') {
533
+ // For objects, just show type hint
534
+ const keys = Object.keys(value);
535
+ return `{Object: ${keys.length} keys}`;
536
+ }
537
+ return String(value);
538
+ }
539
+
540
+ // ============================================================================
541
+ // COMPONENT CAUSE DETECTION
542
+ // ============================================================================
543
+
544
+ /**
545
+ * Result of component cause detection.
546
+ * Includes the cause type and detailed hook state changes (Phase 3).
547
+ */
548
+
549
+ /**
550
+ * Detect why a React component re-rendered (component-level cause).
551
+ * This is different from native-level cause - it tells you WHY the
552
+ * React component function was called, not what native props changed.
553
+ *
554
+ * DOUBLE-BUFFERING HANDLING:
555
+ * React maintains two fiber trees (current/workInProgress) that swap each render.
556
+ * We use getComponentFiberPrevState() to check both the fiber and its alternate
557
+ * when looking up previous state, ensuring we don't falsely detect "mount".
558
+ *
559
+ * SWAP DETECTION:
560
+ * After React commits, fibers swap roles. The fiber we receive might be the "old"
561
+ * fiber (pre-update values) with its alternate being the "new" fiber (post-update).
562
+ * We detect this by comparing against our stored previous state and swap if needed.
563
+ *
564
+ * DETECTION LOGIC:
565
+ * 1. If no previous state found → "mount" (first render)
566
+ * 2. If memoizedProps changed → "props" (parent passed different props)
567
+ * 3. If memoizedState changed → "state" (useState/useReducer updated)
568
+ * 4. Otherwise → "parent" (parent re-rendered, no changes to this component)
569
+ *
570
+ * PHASE 3 ENHANCEMENT:
571
+ * When state changes are detected, we also extract the actual hook values
572
+ * and compare them to provide meaningful before/after information.
573
+ */
574
+ function detectComponentCause(componentFiber) {
575
+ if (!componentFiber) return {
576
+ cause: "unknown",
577
+ hookChanges: null
578
+ };
579
+
580
+ // STRATEGY: Handle React's double-buffering swap
581
+ // The fiber we receive might be the OLD fiber (with previous values) where
582
+ // its alternate contains the NEW values. We need to detect and correct this.
583
+ let currentFiber = componentFiber;
584
+ let alternateFiber = componentFiber.alternate;
585
+
586
+ // Check if we need to swap fiber and alternate using WeakMap stored state
587
+ const storedPrev = getComponentFiberPrevState(componentFiber);
588
+ if (storedPrev && alternateFiber) {
589
+ // Strategy: Compare first useState hook values to determine which is current
590
+ // The fiber whose state matches stored is the OLD fiber; swap it!
591
+ const fiberHooks = extractHookStates(componentFiber);
592
+ const altHooks = extractHookStates(alternateFiber);
593
+ const storedHooks = storedPrev.extractedHooks;
594
+ if (fiberHooks && altHooks && storedHooks && storedHooks.length > 0) {
595
+ // Find first useState hook for comparison
596
+ const storedStateHook = storedHooks.find(h => h.type === 'useState');
597
+ if (storedStateHook) {
598
+ const fiberStateHook = fiberHooks.find(h => h.index === storedStateHook.index);
599
+ const altStateHook = altHooks.find(h => h.index === storedStateHook.index);
600
+ if (fiberStateHook && altStateHook) {
601
+ const fiberMatchesStored = fiberStateHook.value === storedStateHook.value;
602
+ const altMatchesStored = altStateHook.value === storedStateHook.value;
603
+
604
+ // If fiber matches stored but alternate doesn't, fiber is OLD - swap!
605
+ if (fiberMatchesStored && !altMatchesStored) {
606
+ currentFiber = alternateFiber;
607
+ alternateFiber = componentFiber;
608
+ }
609
+ }
610
+ }
611
+ }
612
+ }
613
+
614
+ // Extract hook states from the CORRECT current fiber
615
+ const currentHooks = extractHookStates(currentFiber);
616
+
617
+ // Get previous state from alternate (which is now correctly the OLD fiber)
618
+ let prevMemoizedProps = null;
619
+ let prevMemoizedState = null;
620
+ let prevExtractedHooks = null;
621
+ if (alternateFiber) {
622
+ // Alternate fiber available - use it as previous state
623
+ prevMemoizedProps = alternateFiber.memoizedProps;
624
+ prevMemoizedState = alternateFiber.memoizedState;
625
+ prevExtractedHooks = extractHookStates(alternateFiber);
626
+ } else {
627
+ // Fall back to WeakMap storage (first render won't have alternate)
628
+ if (storedPrev) {
629
+ prevMemoizedProps = storedPrev.memoizedProps;
630
+ prevMemoizedState = storedPrev.memoizedState;
631
+ prevExtractedHooks = storedPrev.extractedHooks;
632
+ }
633
+ }
634
+
635
+ // Store current state for next comparison (on both fiber and alternate)
636
+ // This is critical for swap detection on next render
637
+ setComponentFiberState(currentFiber, {
638
+ memoizedProps: currentFiber.memoizedProps,
639
+ memoizedState: currentFiber.memoizedState,
640
+ extractedHooks: currentHooks
641
+ });
642
+
643
+ // First render - no previous state found (neither alternate nor WeakMap)
644
+ if (prevMemoizedProps === null) {
645
+ return {
646
+ cause: "mount",
647
+ hookChanges: null
648
+ };
649
+ }
650
+
651
+ // Check if props changed (shallow comparison)
652
+ // This means the parent passed different props to this component
653
+ const propsChanged = !shallowEqual(prevMemoizedProps, currentFiber.memoizedProps);
654
+ if (propsChanged) {
655
+ return {
656
+ cause: "props",
657
+ hookChanges: null
658
+ };
659
+ }
660
+
661
+ // Check if state changed (for hooks, memoizedState is a linked list)
662
+ // This means useState/useReducer triggered a re-render
663
+ const stateChanged = prevMemoizedState !== currentFiber.memoizedState;
664
+ if (stateChanged) {
665
+ // Phase 3: Compare hook states to get actual value changes
666
+ const hookChanges = compareHookStates(prevExtractedHooks, currentHooks);
667
+ return {
668
+ cause: "state",
669
+ hookChanges
670
+ };
671
+ }
672
+
673
+ // If neither props nor state changed, it's a parent re-render cascade
674
+ // This component re-rendered because its parent did, not because of its own changes
675
+ return {
676
+ cause: "parent",
677
+ hookChanges: null
678
+ };
679
+ }
680
+
681
+ /**
682
+ * Shallow equality check for props objects
683
+ */
684
+ function shallowEqual(objA, objB) {
685
+ if (objA === objB) return true;
686
+ if (!objA || !objB) return false;
687
+ if (typeof objA !== 'object' || typeof objB !== 'object') return false;
688
+ const keysA = Object.keys(objA);
689
+ const keysB = Object.keys(objB);
690
+ if (keysA.length !== keysB.length) return false;
691
+ for (const key of keysA) {
692
+ if (objA[key] !== objB[key]) return false;
693
+ }
694
+ return true;
695
+ }
696
+
697
+ /**
698
+ * Safe JSON stringify that handles circular references and functions
699
+ */
700
+ function safeStringify(obj, maxDepth = 3) {
701
+ const seen = new WeakSet();
702
+ function stringify(value, depth) {
703
+ if (depth > maxDepth) return "[MAX_DEPTH]";
704
+ if (value === null) return null;
705
+ if (value === undefined) return undefined;
706
+ if (typeof value === "function") return `[Function: ${value.name || "anonymous"}]`;
707
+ if (typeof value === "symbol") return `[Symbol: ${value.toString()}]`;
708
+ if (typeof value !== "object") return value;
709
+ if (seen.has(value)) return "[Circular]";
710
+ seen.add(value);
711
+ if (Array.isArray(value)) {
712
+ return value.slice(0, 10).map(v => stringify(v, depth + 1));
713
+ }
714
+ const result = {};
715
+ const keys = Object.keys(value).slice(0, 20); // Limit keys
716
+ for (const key of keys) {
717
+ try {
718
+ result[key] = stringify(value[key], depth + 1);
719
+ } catch {
720
+ result[key] = "[Error accessing]";
721
+ }
722
+ }
723
+ return result;
724
+ }
725
+ return stringify(obj, 0);
726
+ }
727
+
728
+ /**
729
+ * Log raw fiber debug data to console
730
+ */
731
+ function logRawFiberData(nativeTag, fiber, owningComponentFiber, prev, current, componentCause) {
732
+ const componentName = getComponentNameFromFiber(owningComponentFiber) || "Unknown";
733
+ console.log("\n========================================");
734
+ console.log("[RN-BUOY DEBUG] RENDER EVENT");
735
+ console.log("========================================");
736
+ console.log(`Native Tag: ${nativeTag}`);
737
+ console.log(`Component Name: ${componentName}`);
738
+ console.log(`Is First Render: ${!prev}`);
739
+ console.log(`Component Cause Detected: ${componentCause}`);
740
+ console.log("----------------------------------------");
741
+
742
+ // Native fiber (host component) data
743
+ console.log("\n--- NATIVE FIBER (Host Component) ---");
744
+ console.log("fiber.type:", fiber?.type);
745
+ console.log("fiber.tag:", fiber?.tag);
746
+ console.log("fiber.memoizedProps (CURRENT):", safeStringify(fiber?.memoizedProps));
747
+ console.log("fiber.memoizedState:", safeStringify(fiber?.memoizedState));
748
+ if (prev) {
749
+ console.log("PREVIOUS memoizedProps:", safeStringify(prev.memoizedProps));
750
+ console.log("PREVIOUS memoizedState:", safeStringify(prev.memoizedState));
751
+ }
752
+
753
+ // Component fiber (React component) data
754
+ console.log("\n--- COMPONENT FIBER (React Component) ---");
755
+ if (owningComponentFiber) {
756
+ console.log("componentFiber.type:", owningComponentFiber?.type?.name || owningComponentFiber?.type);
757
+ console.log("componentFiber.tag:", owningComponentFiber?.tag);
758
+ console.log("componentFiber.memoizedProps:", safeStringify(owningComponentFiber?.memoizedProps));
759
+ console.log("componentFiber.memoizedState:", safeStringify(owningComponentFiber?.memoizedState));
760
+
761
+ // Try to extract hook state (memoizedState is a linked list for function components)
762
+ console.log("\n--- HOOKS STATE (linked list walk) ---");
763
+ let hookState = owningComponentFiber?.memoizedState;
764
+ let hookIndex = 0;
765
+ while (hookState && hookIndex < 10) {
766
+ console.log(`Hook[${hookIndex}]:`, safeStringify({
767
+ memoizedState: hookState.memoizedState,
768
+ baseState: hookState.baseState,
769
+ queue: hookState.queue ? "[Queue object]" : null
770
+ }));
771
+ hookState = hookState.next;
772
+ hookIndex++;
773
+ }
774
+
775
+ // Phase 3: Show extracted hook states with type detection
776
+ console.log("\n--- EXTRACTED HOOKS (Phase 3) ---");
777
+ const extractedHooks = extractHookStates(owningComponentFiber);
778
+ if (extractedHooks) {
779
+ for (const hook of extractedHooks) {
780
+ console.log(`Hook[${hook.index}] (${hook.type}):`, formatDisplayValue(hook.value));
781
+ }
782
+ } else {
783
+ console.log("(No hooks extracted)");
784
+ }
785
+ } else {
786
+ console.log("(No component fiber found)");
787
+ }
788
+
789
+ // Children/text content
790
+ console.log("\n--- CHILDREN/TEXT CONTENT ---");
791
+ const children = fiber?.memoizedProps?.children;
792
+ console.log("children type:", typeof children);
793
+ console.log("children value:", safeStringify(children));
794
+ console.log("\n========================================\n");
795
+ }
796
+
797
+ /**
798
+ * Get human-readable name for a fiber work tag
799
+ */
800
+ function getTagName(tag) {
801
+ if (tag === undefined) return 'undefined';
802
+ const tags = {
803
+ 0: 'FunctionComponent',
804
+ 1: 'ClassComponent',
805
+ 2: 'IndeterminateComponent',
806
+ 3: 'HostRoot',
807
+ 4: 'HostPortal',
808
+ 5: 'HostComponent',
809
+ 6: 'HostText',
810
+ 7: 'Fragment',
811
+ 8: 'Mode',
812
+ 9: 'ContextConsumer',
813
+ 10: 'ContextProvider',
814
+ 11: 'ForwardRef',
815
+ 12: 'Profiler',
816
+ 13: 'SuspenseComponent',
817
+ 14: 'MemoComponent',
818
+ 15: 'SimpleMemoComponent'
819
+ };
820
+ return tags[tag] || `Unknown(${tag})`;
821
+ }
822
+
823
+ // ============================================================================
824
+ // DEBUG LOGGING FUNCTIONS (by level)
825
+ // ============================================================================
826
+
827
+ /**
828
+ * MINIMAL logging - Only hook/state value changes
829
+ * Shows just the essential info: "useState: 3334 → 3335"
830
+ *
831
+ * This is the most concise view for debugging state changes.
832
+ */
833
+ function logMinimal(componentName, componentCauseResult) {
834
+ // Only log if there are actual hook changes
835
+ if (!componentCauseResult.hookChanges || componentCauseResult.hookChanges.length === 0) {
836
+ return;
837
+ }
838
+ for (const change of componentCauseResult.hookChanges) {
839
+ console.log(`[${componentName || 'Unknown'}] ${change.type}[${change.index}]: ${change.previousValue} → ${change.currentValue}`);
840
+ }
841
+ }
842
+
843
+ /**
844
+ * VERBOSE logging - Component info + cause + value changes
845
+ * Shows component context with the changes.
846
+ */
847
+ function logVerbose(nativeTag, fiber, owningComponentFiber, componentCauseResult, changedNativeProps) {
848
+ const componentName = getComponentNameFromFiber(owningComponentFiber) || "Unknown";
849
+ const nativeType = typeof fiber?.type === "string" ? fiber.type : "Unknown";
850
+ const {
851
+ cause,
852
+ hookChanges
853
+ } = componentCauseResult;
854
+
855
+ // Single-line summary
856
+ console.log(`[RENDER] ${componentName} (${nativeType}:${nativeTag}) | Cause: ${cause.toUpperCase()}` + (changedNativeProps && changedNativeProps.length > 0 ? ` | Props: [${changedNativeProps.join(', ')}]` : ''));
857
+
858
+ // Hook changes on separate lines (if any)
859
+ if (hookChanges && hookChanges.length > 0) {
860
+ for (const change of hookChanges) {
861
+ console.log(` └─ ${change.type}[${change.index}]: ${change.previousValue} → ${change.currentValue}`);
862
+ }
863
+ }
864
+ }
865
+
866
+ /**
867
+ * Comprehensive render debug logging (level: "all").
868
+ * Captures EVERYTHING available from the React fiber for analysis.
869
+ *
870
+ * This function is called when debugLogLevel is "all".
871
+ * Use this to understand exactly what data is available for render cause detection.
872
+ */
873
+ function logComprehensiveRenderData(nativeTag, fiber, owningComponentFiber, prev, componentCauseResult, batchNativeTags, renderCount) {
874
+ const componentName = getComponentNameFromFiber(owningComponentFiber) || "Unknown";
875
+ const nativeType = typeof fiber?.type === "string" ? fiber.type : "Unknown";
876
+ console.log(`\n[RN-BUOY RENDER DEBUG] ═══════════════════════════════════════`);
877
+ console.log(`Render #${renderCount} for ${nativeType} (nativeTag: ${nativeTag})`);
878
+ console.log(`Timestamp: ${new Date().toISOString()}`);
879
+ console.log(`═══════════════════════════════════════════════════════════════\n`);
880
+
881
+ // === NATIVE FIBER (Host Component) ===
882
+ console.log(`NATIVE FIBER (${nativeType}):`);
883
+ console.log(` type: "${fiber?.type}"`);
884
+ console.log(` tag: ${fiber?.tag} (${getTagName(fiber?.tag)})`);
885
+
886
+ // Use ALTERNATE for previous values (most reliable!)
887
+ const altFiber = fiber?.alternate;
888
+ const prevSource = altFiber ? 'alternate' : prev ? 'storage' : 'none';
889
+ console.log(` Previous values source: ${prevSource}`);
890
+
891
+ // Current and previous children - USE ALTERNATE!
892
+ const currChildren = fiber?.memoizedProps?.children;
893
+ const altChildren = altFiber?.memoizedProps?.children;
894
+ const prevChildren = altChildren !== undefined ? altChildren : prev?.memoizedProps?.children;
895
+ const childrenChanged = prevChildren !== undefined ? currChildren !== prevChildren : false;
896
+ console.log(` memoizedProps.children: ${formatDisplayValue(currChildren)}${childrenChanged ? ` (was: ${formatDisplayValue(prevChildren)})` : prevChildren !== undefined ? ' (unchanged)' : ' (first render)'}`);
897
+
898
+ // Style comparison - USE ALTERNATE!
899
+ const currStyle = fiber?.memoizedProps?.style;
900
+ const altStyle = altFiber?.memoizedProps?.style;
901
+ const prevStyle = altStyle !== undefined ? altStyle : prev?.memoizedProps?.style;
902
+ const styleChanged = prevStyle !== undefined ? currStyle !== prevStyle : false;
903
+ if (currStyle) {
904
+ console.log(` memoizedProps.style: ${JSON.stringify(safeStringify(currStyle, 2))}${styleChanged ? ' (REFERENCE CHANGED)' : ''}`);
905
+ if (styleChanged && prevStyle) {
906
+ const valuesEqual = deepEqual(currStyle, prevStyle);
907
+ console.log(` └─ Values actually changed: ${!valuesEqual}`);
908
+ }
909
+ }
910
+
911
+ // All other props
912
+ console.log(` All memoizedProps keys: [${Object.keys(fiber?.memoizedProps || {}).join(', ')}]`);
913
+
914
+ // Identifying props
915
+ const testID = fiber?.memoizedProps?.testID;
916
+ const nativeID = fiber?.memoizedProps?.nativeID;
917
+ const accessibilityLabel = fiber?.memoizedProps?.accessibilityLabel;
918
+ if (testID) console.log(` testID: "${testID}"`);
919
+ if (nativeID) console.log(` nativeID: "${nativeID}"`);
920
+ if (accessibilityLabel) console.log(` accessibilityLabel: "${accessibilityLabel}"`);
921
+
922
+ // Alternate fiber info
923
+ console.log(` alternate: ${altFiber ? 'YES' : 'NO'}`);
924
+ if (altFiber) {
925
+ console.log(` alternate.memoizedProps.children: ${formatDisplayValue(altChildren)}`);
926
+ }
927
+
928
+ // Fiber tree structure
929
+ console.log(` Tree structure:`);
930
+ console.log(` return (parent): ${fiber?.return ? getTagName(fiber.return.tag) : 'null'}`);
931
+ console.log(` child: ${fiber?.child ? getTagName(fiber.child.tag) : 'null'}`);
932
+ console.log(` sibling: ${fiber?.sibling ? getTagName(fiber.sibling.tag) : 'null'}`);
933
+ console.log('');
934
+
935
+ // === COMPONENT FIBER (React Component) ===
936
+ console.log(`COMPONENT FIBER (${componentName}):`);
937
+ if (owningComponentFiber) {
938
+ console.log(` name: "${componentName}"`);
939
+ console.log(` type: ${typeof owningComponentFiber.type} (${owningComponentFiber.type?.name || owningComponentFiber.type?.displayName || 'anonymous'})`);
940
+ console.log(` tag: ${owningComponentFiber.tag} (${getTagName(owningComponentFiber.tag)})`);
941
+
942
+ // Component props
943
+ console.log(` memoizedProps: ${JSON.stringify(safeStringify(owningComponentFiber.memoizedProps, 2))}`);
944
+
945
+ // Check ALTERNATE fiber (React's built-in previous state!)
946
+ console.log(` alternate: ${owningComponentFiber.alternate ? 'YES' : 'NO'}`);
947
+ if (owningComponentFiber.alternate) {
948
+ console.log(` ALTERNATE memoizedProps: ${JSON.stringify(safeStringify(owningComponentFiber.alternate.memoizedProps, 2))}`);
949
+ const altPropsChanged = !shallowEqual(owningComponentFiber.alternate.memoizedProps, owningComponentFiber.memoizedProps);
950
+ console.log(` Props changed (vs alternate): ${altPropsChanged ? 'YES' : 'NO'}`);
951
+
952
+ // Check state via alternate
953
+ console.log(` ALTERNATE memoizedState: ${owningComponentFiber.alternate.memoizedState === owningComponentFiber.memoizedState ? 'SAME' : 'DIFFERENT'}`);
954
+ }
955
+
956
+ // Previous component state (from WeakMap - for comparison)
957
+ const compPrev = getComponentFiberPrevState(owningComponentFiber);
958
+ if (compPrev) {
959
+ console.log(` WeakMap PREVIOUS memoizedProps: ${JSON.stringify(safeStringify(compPrev.memoizedProps, 2))}`);
960
+ const propsChanged = !shallowEqual(compPrev.memoizedProps, owningComponentFiber.memoizedProps);
961
+ console.log(` Props changed (vs WeakMap): ${propsChanged ? 'YES' : 'NO'}`);
962
+ } else {
963
+ console.log(` WeakMap PREVIOUS state: (not found - first render or WeakMap cleared)`);
964
+ }
965
+
966
+ // Debug owner chain
967
+ if (owningComponentFiber._debugOwner) {
968
+ const ownerName = getComponentNameFromFiber(owningComponentFiber._debugOwner);
969
+ console.log(` _debugOwner: "${ownerName}"`);
970
+
971
+ // Walk up owner chain
972
+ let owner = owningComponentFiber._debugOwner;
973
+ let depth = 1;
974
+ while (owner && depth < 5) {
975
+ const name = getComponentNameFromFiber(owner);
976
+ console.log(` └─[${depth}] ${name || 'unknown'} (tag: ${owner.tag})`);
977
+ owner = owner._debugOwner;
978
+ depth++;
979
+ }
980
+ }
981
+
982
+ // How far we walked to find this component
983
+ let walkDepth = 0;
984
+ let walker = fiber._debugOwner || fiber.return;
985
+ while (walker && walker !== owningComponentFiber && walkDepth < 30) {
986
+ walker = walker.return;
987
+ walkDepth++;
988
+ }
989
+ console.log(` Depth from native fiber: ${walkDepth}`);
990
+ } else {
991
+ console.log(` (No component fiber found - could not walk up tree)`);
992
+ }
993
+ console.log('');
994
+
995
+ // === HOOKS (For Function Components) ===
996
+ console.log(`HOOKS:`);
997
+ if (owningComponentFiber?.memoizedState) {
998
+ const hooks = extractHookStates(owningComponentFiber);
999
+
1000
+ // Try to get previous hooks from ALTERNATE fiber first (more reliable!)
1001
+ const alternateHooks = owningComponentFiber.alternate ? extractHookStates(owningComponentFiber.alternate) : null;
1002
+
1003
+ // Fall back to WeakMap storage
1004
+ const compPrev = getComponentFiberPrevState(owningComponentFiber);
1005
+ const prevHooks = alternateHooks || compPrev?.extractedHooks;
1006
+ const prevSource = alternateHooks ? 'alternate' : compPrev ? 'WeakMap' : 'none';
1007
+ if (hooks && hooks.length > 0) {
1008
+ console.log(` Total hooks: ${hooks.length}`);
1009
+ console.log(` Previous values source: ${prevSource}`);
1010
+ hooks.forEach((hook, i) => {
1011
+ const prevHook = prevHooks?.[i];
1012
+ const changed = prevHook ? prevHook.rawState !== hook.rawState : false;
1013
+ const prevValue = prevHook ? formatDisplayValue(prevHook.value) : 'N/A';
1014
+ const marker = changed ? ' ← CHANGED' : '';
1015
+ console.log(` [${i}] ${hook.type}: ${formatDisplayValue(hook.value)}${prevHook ? ` (was: ${prevValue})` : ' (first render)'}${marker}`);
1016
+ });
1017
+ } else {
1018
+ console.log(` (Could not extract hooks - memoizedState structure not recognized)`);
1019
+ console.log(` Raw memoizedState type: ${typeof owningComponentFiber.memoizedState}`);
1020
+ console.log(` Has 'next' property: ${'next' in (owningComponentFiber.memoizedState || {})}`);
1021
+ console.log(` Has 'queue' property: ${'queue' in (owningComponentFiber.memoizedState || {})}`);
1022
+ }
1023
+ } else {
1024
+ console.log(` (No memoizedState - class component, no hooks, or not a function component)`);
1025
+ }
1026
+ console.log('');
1027
+
1028
+ // === RAW HOOKS DATA (for deep debugging) ===
1029
+ console.log(`RAW HOOKS DATA:`);
1030
+ if (owningComponentFiber?.memoizedState && typeof owningComponentFiber.memoizedState === 'object') {
1031
+ let hookState = owningComponentFiber.memoizedState;
1032
+ let hookIndex = 0;
1033
+ while (hookState && hookIndex < 10) {
1034
+ console.log(` Hook[${hookIndex}]:`);
1035
+ console.log(` memoizedState: ${safeStringify(hookState.memoizedState, 2)}`);
1036
+ console.log(` baseState: ${safeStringify(hookState.baseState, 2)}`);
1037
+ console.log(` queue: ${hookState.queue ? '[Queue present]' : 'null'}`);
1038
+ console.log(` next: ${hookState.next ? '[Next hook]' : 'null'}`);
1039
+ hookState = hookState.next;
1040
+ hookIndex++;
1041
+ }
1042
+ if (hookIndex === 0) {
1043
+ console.log(` (memoizedState is not a hooks linked list)`);
1044
+ }
1045
+ }
1046
+ console.log('');
1047
+
1048
+ // === DETECTION RESULTS ===
1049
+ console.log(`DETECTION RESULTS:`);
1050
+ console.log(` Component Cause: ${componentCauseResult.cause.toUpperCase()}`);
1051
+ if (componentCauseResult.hookChanges && componentCauseResult.hookChanges.length > 0) {
1052
+ console.log(` Hook Changes Detected:`);
1053
+ componentCauseResult.hookChanges.forEach(change => {
1054
+ console.log(` [${change.index}] ${change.type}: ${change.previousValue} → ${change.currentValue}`);
1055
+ if (change.description) {
1056
+ console.log(` ${change.description}`);
1057
+ }
1058
+ });
1059
+ }
1060
+
1061
+ // What our prop detection would find
1062
+ const nativeType2 = typeof fiber?.type === "string" ? fiber.type : undefined;
1063
+ const changedProps = getChangedKeys(prev?.memoizedProps, fiber?.memoizedProps, nativeType2);
1064
+ if (changedProps && changedProps.length > 0) {
1065
+ console.log(` Native Props Changed: [${changedProps.join(', ')}]`);
1066
+ } else if (prev) {
1067
+ console.log(` Native Props Changed: (none detected)`);
1068
+ }
1069
+ console.log('');
1070
+
1071
+ // === BATCH CONTEXT ===
1072
+ console.log(`BATCH CONTEXT:`);
1073
+ console.log(` Batch size: ${batchNativeTags.size}`);
1074
+ console.log(` All tags in batch: [${Array.from(batchNativeTags).slice(0, 20).join(', ')}${batchNativeTags.size > 20 ? '...' : ''}]`);
1075
+ const parentTag = getParentNativeTag(fiber);
1076
+ const parentInBatch = parentTag !== null && batchNativeTags.has(parentTag);
1077
+ console.log(` Parent nativeTag: ${parentTag ?? 'not found'}`);
1078
+ console.log(` Parent in batch: ${parentInBatch ? 'YES' : 'NO'}`);
1079
+
1080
+ // Walk up to find parent components in batch
1081
+ if (batchNativeTags.size > 1) {
1082
+ console.log(` Components in same batch:`);
1083
+ let parent = fiber?.return;
1084
+ let depth = 0;
1085
+ while (parent && depth < 10) {
1086
+ const parentNT = getNativeTagFromStateNode(parent.stateNode);
1087
+ if (parentNT !== null && batchNativeTags.has(parentNT)) {
1088
+ const parentName = getComponentNameFromFiber(parent) || parent.type;
1089
+ console.log(` [depth ${depth}] ${parentName} (tag: ${parentNT})`);
1090
+ }
1091
+ parent = parent.return;
1092
+ depth++;
1093
+ }
1094
+ }
1095
+ console.log(`\n═══════════════════════════════════════════════════════════════\n`);
1096
+ }
1097
+
1098
+ /**
1099
+ * Detect why a component rendered
1100
+ *
1101
+ * @param nativeTag - The native tag of the component
1102
+ * @param fiber - The React fiber object (host fiber)
1103
+ * @param batchNativeTags - Set of all nativeTags in this render batch (for parent detection)
1104
+ * @param debugLogLevel - Debug logging level: "off" | "minimal" | "verbose" | "all"
1105
+ * @returns RenderCause object describing why the component rendered
1106
+ */
1107
+ function detectRenderCause(nativeTag, fiber, batchNativeTags, debugLogLevel = "off") {
1108
+ const now = Date.now();
1109
+ if (!fiber) {
1110
+ return {
1111
+ type: "unknown",
1112
+ timestamp: now
1113
+ };
1114
+ }
1115
+
1116
+ // STRATEGY: React's double-buffering means fiber and fiber.alternate swap roles each render.
1117
+ // The `internalInstanceHandle` we receive may point to either the "current" (just committed)
1118
+ // or the "workInProgress" (about to be committed) fiber depending on timing.
1119
+ //
1120
+ // To reliably get current vs previous:
1121
+ // 1. Check our stored previous state by nativeTag (we store AFTER each detection)
1122
+ // 2. The current fiber's memoizedProps should be DIFFERENT from stored previous
1123
+ // 3. If they're the same, we're looking at the alternate (need to swap)
1124
+ //
1125
+ // Detection: If fiber.memoizedProps === storedPrev.memoizedProps, we got the wrong fiber!
1126
+
1127
+ let currentFiber = fiber;
1128
+ let alternateFiber = fiber.alternate;
1129
+
1130
+ // Check if we need to swap fiber and alternate
1131
+ // This happens due to React's double-buffering - we might get the "old" fiber
1132
+ const storedPrev = previousStates.get(nativeTag);
1133
+ if (storedPrev && alternateFiber) {
1134
+ // Detection strategy: Compare children values to determine which fiber is "current"
1135
+ // The fiber with the DIFFERENT children value from stored is the NEW (current) fiber
1136
+ // The fiber with the SAME children value as stored is the OLD (alternate) fiber
1137
+ const storedChildren = storedPrev.memoizedProps?.children;
1138
+ const fiberChildren = fiber.memoizedProps?.children;
1139
+ const altChildren = alternateFiber.memoizedProps?.children;
1140
+
1141
+ // Check by reference first (fastest), then by value for primitives
1142
+ const fiberMatchesStored = fiber.memoizedProps === storedPrev.memoizedProps || storedChildren !== undefined && fiberChildren === storedChildren;
1143
+ const altMatchesStored = alternateFiber.memoizedProps === storedPrev.memoizedProps || storedChildren !== undefined && altChildren === storedChildren;
1144
+
1145
+ // If fiber matches stored but alternate doesn't, fiber is OLD - swap!
1146
+ if (fiberMatchesStored && !altMatchesStored) {
1147
+ currentFiber = alternateFiber;
1148
+ alternateFiber = fiber;
1149
+ if (debugLogLevel === "all") {
1150
+ console.log(`[RenderCause] Detected fiber swap - using alternate as current`);
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ // Get previous state from alternate fiber first, fall back to our Map
1156
+ let prevMemoizedProps = null;
1157
+ let prevMemoizedState = null;
1158
+ if (alternateFiber) {
1159
+ // Alternate fiber available - use it directly (most reliable!)
1160
+ prevMemoizedProps = alternateFiber.memoizedProps;
1161
+ prevMemoizedState = alternateFiber.memoizedState;
1162
+ } else {
1163
+ // Fall back to our Map storage (first render won't have alternate)
1164
+ if (storedPrev) {
1165
+ prevMemoizedProps = storedPrev.memoizedProps;
1166
+ prevMemoizedState = storedPrev.memoizedState;
1167
+ }
1168
+ }
1169
+
1170
+ // Build prev object for compatibility with existing code
1171
+ const prev = prevMemoizedProps !== null ? {
1172
+ memoizedProps: prevMemoizedProps,
1173
+ memoizedState: prevMemoizedState,
1174
+ timestamp: now
1175
+ } : undefined;
1176
+ const current = {
1177
+ memoizedProps: currentFiber.memoizedProps,
1178
+ memoizedState: currentFiber.memoizedState,
1179
+ timestamp: now
1180
+ };
1181
+
1182
+ // Store current state for next comparison (as fallback for edge cases)
1183
+ updateStoredState(nativeTag, current);
1184
+
1185
+ // Get the owning React component for two-level causation
1186
+ // Use currentFiber to ensure we walk up from the correct fiber
1187
+ const owningComponentFiber = getOwningComponentFiber(currentFiber);
1188
+ const componentName = getComponentNameFromFiber(owningComponentFiber) || undefined;
1189
+
1190
+ // Get parent component name for cascade visualization
1191
+ const parentComponentName = getParentComponentName(currentFiber, componentName) || undefined;
1192
+
1193
+ // Phase 3: detectComponentCause now returns both cause and hook changes
1194
+ const componentCauseResult = detectComponentCause(owningComponentFiber);
1195
+ const componentCause = componentCauseResult.cause;
1196
+ const componentHookChanges = componentCauseResult.hookChanges;
1197
+
1198
+ // First mount detection - no alternate fiber AND no stored state
1199
+ if (!prev) {
1200
+ // Log mount event if logging is enabled
1201
+ if (debugLogLevel !== "off") {
1202
+ if (debugLogLevel === "minimal") {
1203
+ console.log(`[${componentName || 'Unknown'}] MOUNT`);
1204
+ } else if (debugLogLevel === "verbose") {
1205
+ const nativeType = typeof currentFiber?.type === "string" ? currentFiber.type : "Unknown";
1206
+ console.log(`[RENDER] ${componentName || 'Unknown'} (${nativeType}:${nativeTag}) | Cause: MOUNT`);
1207
+ } else if (debugLogLevel === "all") {
1208
+ const renderCount = previousStates.has(nativeTag) ? previousStates.size : 1;
1209
+ logComprehensiveRenderData(nativeTag, currentFiber, owningComponentFiber, prev, componentCauseResult, batchNativeTags, renderCount);
1210
+ }
1211
+ }
1212
+ return {
1213
+ type: "mount",
1214
+ timestamp: now,
1215
+ componentCause: "mount",
1216
+ // Override - if native is mount, component is too
1217
+ componentName
1218
+ };
1219
+ }
1220
+
1221
+ // Get the native component type for component-specific prop handling
1222
+ // currentFiber.type is the native component name (e.g., "RCTText", "RCTView")
1223
+ const nativeType = typeof currentFiber.type === "string" ? currentFiber.type : undefined;
1224
+
1225
+ // Props change detection (native view props)
1226
+ // Pass nativeType so we can handle component-specific props (e.g., children for Text)
1227
+ const changedProps = getChangedKeys(prev.memoizedProps, current.memoizedProps, nativeType);
1228
+
1229
+ // Debug logging based on level (for non-mount renders)
1230
+ if (debugLogLevel !== "off") {
1231
+ if (debugLogLevel === "minimal") {
1232
+ // Only log if there are hook changes
1233
+ logMinimal(componentName, componentCauseResult);
1234
+ } else if (debugLogLevel === "verbose") {
1235
+ logVerbose(nativeTag, currentFiber, owningComponentFiber, componentCauseResult, changedProps);
1236
+ } else if (debugLogLevel === "all") {
1237
+ const renderCount = previousStates.has(nativeTag) ? previousStates.size : 1;
1238
+ logComprehensiveRenderData(nativeTag, currentFiber, owningComponentFiber, prev, componentCauseResult, batchNativeTags, renderCount);
1239
+ }
1240
+ }
1241
+ if (changedProps && changedProps.length > 0) {
1242
+ return {
1243
+ type: "props",
1244
+ changedKeys: changedProps.slice(0, MAX_CHANGED_KEYS),
1245
+ timestamp: now,
1246
+ componentCause,
1247
+ componentName,
1248
+ parentComponentName,
1249
+ // Phase 3: Include hook changes when component cause is state
1250
+ hookChanges: componentHookChanges || undefined
1251
+ };
1252
+ }
1253
+
1254
+ // Hooks/State change detection (host fiber's hooks - usually none for host components)
1255
+ const nativeHookChanges = detectHookChanges(prev.memoizedState, current.memoizedState);
1256
+ if (nativeHookChanges && nativeHookChanges.length > 0) {
1257
+ return {
1258
+ type: "hooks",
1259
+ hookIndices: nativeHookChanges,
1260
+ timestamp: now,
1261
+ componentCause,
1262
+ componentName,
1263
+ parentComponentName,
1264
+ // Phase 3: Include hook changes when component cause is state
1265
+ hookChanges: componentHookChanges || undefined
1266
+ };
1267
+ }
1268
+
1269
+ // Parent re-rendered detection
1270
+ // Check if parent fiber's nativeTag is also in this batch
1271
+ const parentNativeTag = getParentNativeTag(currentFiber);
1272
+ if (parentNativeTag && batchNativeTags.has(parentNativeTag)) {
1273
+ return {
1274
+ type: "parent",
1275
+ timestamp: now,
1276
+ componentCause,
1277
+ componentName,
1278
+ parentComponentName,
1279
+ hookChanges: componentHookChanges || undefined
1280
+ };
1281
+ }
1282
+
1283
+ // If we couldn't detect native-level cause, fall back to component cause
1284
+ // This handles cases where the component re-rendered due to state but
1285
+ // the native view props didn't change (or changed in undetectable ways)
1286
+ if (componentCause === "state") {
1287
+ return {
1288
+ type: "state",
1289
+ timestamp: now,
1290
+ componentCause,
1291
+ componentName,
1292
+ parentComponentName,
1293
+ // Phase 3: Include hook changes for state-caused renders
1294
+ hookChanges: componentHookChanges || undefined
1295
+ };
1296
+ }
1297
+ if (componentCause === "props") {
1298
+ return {
1299
+ type: "props",
1300
+ timestamp: now,
1301
+ componentCause,
1302
+ componentName,
1303
+ parentComponentName
1304
+ };
1305
+ }
1306
+ if (componentCause === "parent") {
1307
+ return {
1308
+ type: "parent",
1309
+ timestamp: now,
1310
+ componentCause,
1311
+ componentName,
1312
+ parentComponentName
1313
+ };
1314
+ }
1315
+ return {
1316
+ type: "unknown",
1317
+ timestamp: now,
1318
+ componentCause,
1319
+ componentName,
1320
+ parentComponentName
1321
+ };
1322
+ }
1323
+
1324
+ /**
1325
+ * Shallow compare object keys to find changes
1326
+ * Returns array of changed key names
1327
+ *
1328
+ * @param prev - Previous props object
1329
+ * @param next - Current props object
1330
+ * @param fiberType - Native component type (e.g., "RCTText", "RCTView")
1331
+ * Used to determine which props are meaningful to track
1332
+ *
1333
+ * COMPONENT-SPECIFIC HANDLING:
1334
+ * - RCTText/RCTVirtualText: `children` IS the text content, always track it
1335
+ * - RCTView: `children` is React elements, skip it
1336
+ * - RCTImageView: `source` is important, track it
1337
+ *
1338
+ * See: COMPONENT_PROP_CONFIGS for full configuration
1339
+ */
1340
+ function getChangedKeys(prev, next, fiberType) {
1341
+ // Same reference = no changes
1342
+ if (prev === next) return null;
1343
+
1344
+ // Handle null/undefined cases
1345
+ if (!prev || !next) return null;
1346
+ if (typeof prev !== "object" || typeof next !== "object") return null;
1347
+
1348
+ // Handle arrays (props shouldn't be arrays, but just in case)
1349
+ if (Array.isArray(prev) || Array.isArray(next)) return null;
1350
+
1351
+ // Get component-specific prop configuration
1352
+ const propConfig = getComponentPropConfig(fiberType);
1353
+ const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);
1354
+ const changed = [];
1355
+ for (const key of allKeys) {
1356
+ // Skip internal React props
1357
+ if (key.startsWith("__")) continue;
1358
+
1359
+ // Check if this key should be skipped for this component type
1360
+ if (propConfig.skip.includes(key)) continue;
1361
+
1362
+ // For `children` prop, use component-specific rules
1363
+ if (key === "children") {
1364
+ // Only track children if component config says to (e.g., RCTText)
1365
+ if (!propConfig.alwaysTrack.includes("children")) {
1366
+ continue; // Skip children for most components
1367
+ }
1368
+
1369
+ // For Text components, compare children values
1370
+ // Children can be string, number, or array of mixed content
1371
+ const prevChildren = prev[key];
1372
+ const nextChildren = next[key];
1373
+ if (prevChildren !== nextChildren) {
1374
+ // Format the change nicely for display
1375
+ if (isPrimitive(prevChildren) && isPrimitive(nextChildren)) {
1376
+ // Show the actual value change: "children: 6 → 4"
1377
+ changed.push(`children: ${formatValue(prevChildren)} → ${formatValue(nextChildren)}`);
1378
+ } else {
1379
+ // Complex children (arrays, objects) - just note it changed
1380
+ changed.push("children (content)");
1381
+ }
1382
+ }
1383
+ continue;
1384
+ }
1385
+
1386
+ // Standard shallow comparison for other props
1387
+ if (prev[key] !== next[key]) {
1388
+ // Phase 4: For object props like style, check if it's a reference-only change
1389
+ const prevVal = prev[key];
1390
+ const nextVal = next[key];
1391
+ if (key === "style") {
1392
+ // Style can be an object, array, or array with falsy values
1393
+ // Deep compare to see if values actually changed
1394
+ if (deepEqual(prevVal, nextVal)) {
1395
+ // Reference changed but values are the same - mark as ref-only
1396
+ changed.push(`${key} (ref only)`);
1397
+ } else {
1398
+ // Values actually changed
1399
+ changed.push(key);
1400
+ }
1401
+ } else if (typeof prevVal === "function" && typeof nextVal === "function") {
1402
+ // Functions are often recreated - note this for potential useCallback suggestion
1403
+ changed.push(`${key} (fn ref)`);
1404
+ } else {
1405
+ changed.push(key);
1406
+ }
1407
+ }
1408
+ }
1409
+ return changed.length > 0 ? changed : null;
1410
+ }
1411
+
1412
+ /**
1413
+ * Check if a value is a plain object (not array, null, or class instance)
1414
+ */
1415
+ function isPlainObject(value) {
1416
+ if (value === null || typeof value !== "object") return false;
1417
+ if (Array.isArray(value)) return false;
1418
+ // Check if it's a plain object (not a class instance)
1419
+ const proto = Object.getPrototypeOf(value);
1420
+ return proto === Object.prototype || proto === null;
1421
+ }
1422
+
1423
+ /**
1424
+ * Deep equality check for objects (used for style comparison)
1425
+ * Returns true if objects have the same values
1426
+ */
1427
+ function deepEqual(obj1, obj2, depth = 0) {
1428
+ // Prevent infinite recursion
1429
+ if (depth > 5) return false;
1430
+
1431
+ // Same reference
1432
+ if (obj1 === obj2) return true;
1433
+
1434
+ // Type check
1435
+ if (typeof obj1 !== typeof obj2) return false;
1436
+
1437
+ // Primitives
1438
+ if (typeof obj1 !== "object" || obj1 === null || obj2 === null) {
1439
+ return obj1 === obj2;
1440
+ }
1441
+
1442
+ // Arrays
1443
+ if (Array.isArray(obj1) !== Array.isArray(obj2)) return false;
1444
+ if (Array.isArray(obj1)) {
1445
+ if (obj1.length !== obj2.length) return false;
1446
+ for (let i = 0; i < obj1.length; i++) {
1447
+ if (!deepEqual(obj1[i], obj2[i], depth + 1)) return false;
1448
+ }
1449
+ return true;
1450
+ }
1451
+
1452
+ // Objects
1453
+ const keys1 = Object.keys(obj1);
1454
+ const keys2 = Object.keys(obj2);
1455
+ if (keys1.length !== keys2.length) return false;
1456
+ for (const key of keys1) {
1457
+ if (!keys2.includes(key)) return false;
1458
+ if (!deepEqual(obj1[key], obj2[key], depth + 1)) return false;
1459
+ }
1460
+ return true;
1461
+ }
1462
+
1463
+ /**
1464
+ * Check if a value is a primitive (string, number, boolean, null, undefined)
1465
+ */
1466
+ function isPrimitive(value) {
1467
+ return value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
1468
+ }
1469
+
1470
+ /**
1471
+ * Format a value for display in change messages
1472
+ */
1473
+ function formatValue(value) {
1474
+ if (value === null) return "null";
1475
+ if (value === undefined) return "undefined";
1476
+ if (typeof value === "string") {
1477
+ // Truncate long strings
1478
+ return value.length > 20 ? `"${value.slice(0, 20)}..."` : `"${value}"`;
1479
+ }
1480
+ if (typeof value === "number") return String(value);
1481
+ if (typeof value === "boolean") return String(value);
1482
+ return "[object]";
1483
+ }
1484
+
1485
+ /**
1486
+ * Detect which hooks changed by walking the linked list
1487
+ * Returns array of hook indices that changed
1488
+ */
1489
+ function detectHookChanges(prevState, nextState) {
1490
+ // Same reference = no changes
1491
+ if (prevState === nextState) return null;
1492
+
1493
+ // If either is null/undefined, can't compare
1494
+ if (prevState == null || nextState == null) return null;
1495
+
1496
+ // Check if this looks like a hooks linked list
1497
+ // Hooks have a 'next' property forming a linked list
1498
+ if (typeof prevState !== "object" || typeof nextState !== "object") {
1499
+ return null;
1500
+ }
1501
+ const changes = [];
1502
+ let prevHook = prevState;
1503
+ let nextHook = nextState;
1504
+ let index = 0;
1505
+
1506
+ // Walk the hooks linked list
1507
+ while (nextHook !== null && index < MAX_HOOK_DEPTH) {
1508
+ if (prevHook === null) {
1509
+ // New hook added (shouldn't happen normally, but handle it)
1510
+ changes.push(index);
1511
+ } else if (didHookChange(prevHook, nextHook)) {
1512
+ changes.push(index);
1513
+ }
1514
+
1515
+ // Move to next hook in list
1516
+ nextHook = nextHook?.next ?? null;
1517
+ prevHook = prevHook?.next ?? null;
1518
+ index++;
1519
+ }
1520
+ return changes.length > 0 ? changes : null;
1521
+ }
1522
+
1523
+ /**
1524
+ * Check if a single hook changed
1525
+ */
1526
+ function didHookChange(prev, next) {
1527
+ if (prev === next) return false;
1528
+
1529
+ // Check memoizedState (useState, useReducer, useMemo, useCallback, useRef)
1530
+ if (prev.memoizedState !== next.memoizedState) {
1531
+ return true;
1532
+ }
1533
+
1534
+ // Check baseState (useReducer specific)
1535
+ if (prev.baseState !== undefined && prev.baseState !== next.baseState) {
1536
+ return true;
1537
+ }
1538
+ return false;
1539
+ }
1540
+
1541
+ /**
1542
+ * Get the nativeTag of the nearest parent host component
1543
+ */
1544
+ function getParentNativeTag(fiber) {
1545
+ let parent = fiber?.return;
1546
+ let depth = 0;
1547
+ while (parent && depth < MAX_PARENT_DEPTH) {
1548
+ // Check if this is a host component with a stateNode
1549
+ if (parent.stateNode) {
1550
+ const tag = getNativeTagFromStateNode(parent.stateNode);
1551
+ if (tag != null) return tag;
1552
+ }
1553
+ parent = parent.return;
1554
+ depth++;
1555
+ }
1556
+ return null;
1557
+ }
1558
+
1559
+ /**
1560
+ * Get the parent React component name for cascade visualization.
1561
+ * Walks up the fiber tree to find the nearest user-defined component
1562
+ * that is DIFFERENT from the current component.
1563
+ */
1564
+ function getParentComponentName(fiber, currentComponentName) {
1565
+ if (!fiber) return null;
1566
+
1567
+ // Start from the owning component's parent
1568
+ const owningFiber = getOwningComponentFiber(fiber);
1569
+ if (!owningFiber) return null;
1570
+ let parent = owningFiber.return;
1571
+ let depth = 0;
1572
+ while (parent && depth < MAX_PARENT_DEPTH) {
1573
+ const tag = parent.tag;
1574
+
1575
+ // Tags: 0=FunctionComponent, 1=ClassComponent, 11=ForwardRef, 15=SimpleMemoComponent
1576
+ if (tag === 0 || tag === 1 || tag === 11 || tag === 15) {
1577
+ const name = getComponentNameFromFiber(parent);
1578
+
1579
+ // Skip internal React components and same-name components
1580
+ if (name && !isInternalComponentName(name) && name !== currentComponentName) {
1581
+ return name;
1582
+ }
1583
+ }
1584
+ parent = parent.return;
1585
+ depth++;
1586
+ }
1587
+ return null;
1588
+ }
1589
+
1590
+ /**
1591
+ * Extract nativeTag from various stateNode formats
1592
+ */
1593
+ function getNativeTagFromStateNode(stateNode) {
1594
+ if (!stateNode) return null;
1595
+
1596
+ // Direct __nativeTag (Fabric)
1597
+ if (typeof stateNode.__nativeTag === "number") {
1598
+ return stateNode.__nativeTag;
1599
+ }
1600
+
1601
+ // Direct _nativeTag (Legacy/Paper)
1602
+ if (typeof stateNode._nativeTag === "number") {
1603
+ return stateNode._nativeTag;
1604
+ }
1605
+
1606
+ // Fabric canonical path
1607
+ if (typeof stateNode.canonical?.__nativeTag === "number") {
1608
+ return stateNode.canonical.__nativeTag;
1609
+ }
1610
+
1611
+ // Public instance path
1612
+ if (typeof stateNode.canonical?.publicInstance?.__nativeTag === "number") {
1613
+ return stateNode.canonical.publicInstance.__nativeTag;
1614
+ }
1615
+ return null;
1616
+ }
1617
+
1618
+ /**
1619
+ * Update stored state with automatic cleanup when limit is reached
1620
+ */
1621
+ function updateStoredState(nativeTag, state) {
1622
+ // Enforce max limit - remove oldest 25% when full
1623
+ if (previousStates.size >= MAX_STORED_STATES) {
1624
+ const entries = Array.from(previousStates.entries());
1625
+ // Sort by timestamp (oldest first)
1626
+ entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
1627
+
1628
+ // Remove oldest 25%
1629
+ const removeCount = Math.floor(MAX_STORED_STATES / 4);
1630
+ for (let i = 0; i < removeCount; i++) {
1631
+ previousStates.delete(entries[i][0]);
1632
+ }
1633
+ }
1634
+ previousStates.set(nativeTag, state);
1635
+ }
1636
+
1637
+ /**
1638
+ * Clear all stored states
1639
+ * Call this when tracking is disabled to prevent memory leaks
1640
+ */
1641
+ function clearRenderCauseState() {
1642
+ previousStates.clear();
1643
+ }
1644
+
1645
+ /**
1646
+ * Remove a specific component from state storage
1647
+ * Useful when a component is unmounted
1648
+ */
1649
+ function removeRenderCauseState(nativeTag) {
1650
+ previousStates.delete(nativeTag);
1651
+ }
1652
+
1653
+ /**
1654
+ * Get storage stats for debugging/display
1655
+ */
1656
+ function getRenderCauseStats() {
1657
+ return {
1658
+ storedStates: previousStates.size,
1659
+ maxStates: MAX_STORED_STATES
1660
+ };
1661
+ }
1662
+
1663
+ // === Props/State Snapshot Capture for History ===
1664
+
1665
+ // Max depth for cloning nested objects
1666
+ const MAX_CLONE_DEPTH = 5;
1667
+ // Max string length for values
1668
+ const MAX_STRING_LENGTH = 200;
1669
+ // Max array items to include
1670
+ const MAX_ARRAY_ITEMS = 20;
1671
+ // Max object keys to include
1672
+ const MAX_OBJECT_KEYS = 30;
1673
+
1674
+ /**
1675
+ * Safely clone props/state for history storage.
1676
+ * Handles circular references, functions, and large objects.
1677
+ */
1678
+ function safeCloneForHistory(value, depth = 0, seen = new WeakSet()) {
1679
+ // Handle primitives
1680
+ if (value === null || value === undefined) return value;
1681
+ if (typeof value === "boolean" || typeof value === "number") return value;
1682
+
1683
+ // Handle strings (truncate long ones)
1684
+ if (typeof value === "string") {
1685
+ return value.length > MAX_STRING_LENGTH ? value.slice(0, MAX_STRING_LENGTH) + "..." : value;
1686
+ }
1687
+
1688
+ // Handle functions - show name or placeholder
1689
+ if (typeof value === "function") {
1690
+ return `[Function: ${value.name || "anonymous"}]`;
1691
+ }
1692
+
1693
+ // Handle symbols
1694
+ if (typeof value === "symbol") {
1695
+ return `[Symbol: ${value.description || ""}]`;
1696
+ }
1697
+
1698
+ // Stop at max depth
1699
+ if (depth >= MAX_CLONE_DEPTH) {
1700
+ if (Array.isArray(value)) return `[Array: ${value.length} items]`;
1701
+ if (typeof value === "object") {
1702
+ const keys = Object.keys(value);
1703
+ return `[Object: ${keys.length} keys]`;
1704
+ }
1705
+ return "[...]";
1706
+ }
1707
+
1708
+ // Handle circular references
1709
+ if (typeof value === "object") {
1710
+ if (seen.has(value)) {
1711
+ return "[Circular]";
1712
+ }
1713
+ seen.add(value);
1714
+ }
1715
+
1716
+ // Handle arrays
1717
+ if (Array.isArray(value)) {
1718
+ const cloned = value.slice(0, MAX_ARRAY_ITEMS).map(item => safeCloneForHistory(item, depth + 1, seen));
1719
+ if (value.length > MAX_ARRAY_ITEMS) {
1720
+ cloned.push(`[...${value.length - MAX_ARRAY_ITEMS} more]`);
1721
+ }
1722
+ return cloned;
1723
+ }
1724
+
1725
+ // Handle Date
1726
+ if (value instanceof Date) {
1727
+ return value.toISOString();
1728
+ }
1729
+
1730
+ // Handle Error
1731
+ if (value instanceof Error) {
1732
+ return `[Error: ${value.message}]`;
1733
+ }
1734
+
1735
+ // Handle RegExp
1736
+ if (value instanceof RegExp) {
1737
+ return value.toString();
1738
+ }
1739
+
1740
+ // Handle Map
1741
+ if (value instanceof Map) {
1742
+ const obj = {
1743
+ __type: "Map",
1744
+ __size: value.size
1745
+ };
1746
+ let count = 0;
1747
+ for (const [k, v] of value) {
1748
+ if (count >= MAX_OBJECT_KEYS) {
1749
+ obj[`...${value.size - count} more`] = true;
1750
+ break;
1751
+ }
1752
+ const key = typeof k === "string" ? k : String(k);
1753
+ obj[key] = safeCloneForHistory(v, depth + 1, seen);
1754
+ count++;
1755
+ }
1756
+ return obj;
1757
+ }
1758
+
1759
+ // Handle Set
1760
+ if (value instanceof Set) {
1761
+ return {
1762
+ __type: "Set",
1763
+ __size: value.size,
1764
+ values: Array.from(value).slice(0, MAX_ARRAY_ITEMS).map(item => safeCloneForHistory(item, depth + 1, seen))
1765
+ };
1766
+ }
1767
+
1768
+ // Handle plain objects
1769
+ if (typeof value === "object") {
1770
+ const cloned = {};
1771
+ const keys = Object.keys(value);
1772
+ const keysToClone = keys.slice(0, MAX_OBJECT_KEYS);
1773
+ for (const key of keysToClone) {
1774
+ // Skip internal React props
1775
+ if (key.startsWith("__") || key === "children") continue;
1776
+ try {
1777
+ cloned[key] = safeCloneForHistory(value[key], depth + 1, seen);
1778
+ } catch {
1779
+ cloned[key] = "[Error accessing property]";
1780
+ }
1781
+ }
1782
+ if (keys.length > MAX_OBJECT_KEYS) {
1783
+ cloned[`...${keys.length - MAX_OBJECT_KEYS} more keys`] = true;
1784
+ }
1785
+ return cloned;
1786
+ }
1787
+ return "[Unknown type]";
1788
+ }
1789
+
1790
+ /**
1791
+ * Capture props snapshot from a fiber for history storage
1792
+ */
1793
+ function capturePropsSnapshot(fiber) {
1794
+ if (!fiber?.memoizedProps) return undefined;
1795
+ return safeCloneForHistory(fiber.memoizedProps);
1796
+ }
1797
+
1798
+ /**
1799
+ * Capture state snapshot from a fiber for history storage
1800
+ * For function components, this walks the hooks linked list
1801
+ */
1802
+ function captureStateSnapshot(fiber) {
1803
+ if (!fiber?.memoizedState) return undefined;
1804
+ const state = fiber.memoizedState;
1805
+
1806
+ // Check if it's a hooks linked list (has 'next' property)
1807
+ if (typeof state === "object" && state !== null && "next" in state) {
1808
+ // It's a hooks list - extract hook values
1809
+ const hooks = [];
1810
+ let current = state;
1811
+ let index = 0;
1812
+ while (current && index < MAX_HOOK_DEPTH) {
1813
+ hooks.push({
1814
+ index,
1815
+ memoizedState: safeCloneForHistory(current.memoizedState)
1816
+ });
1817
+ current = current.next;
1818
+ index++;
1819
+ }
1820
+ return {
1821
+ __type: "Hooks",
1822
+ hooks
1823
+ };
1824
+ }
1825
+
1826
+ // It's a regular state object (class component or simple state)
1827
+ return safeCloneForHistory(state);
1828
+ }