@buoy-gg/highlight-updates 2.1.12 → 2.1.13

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