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