@buoy-gg/highlight-updates 7.0.36 → 7.0.38

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