@buoy-gg/highlight-updates 7.0.41 → 7.0.43

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 (108) hide show
  1. package/README.md +11 -71
  2. package/lib/commonjs/highlight-updates/HighlightUpdatesOverlay.js +1 -281
  3. package/lib/commonjs/highlight-updates/components/HighlightFilterView.js +1 -1474
  4. package/lib/commonjs/highlight-updates/components/HighlightUpdatesModal.js +1 -641
  5. package/lib/commonjs/highlight-updates/components/IdentifierBadge.js +1 -273
  6. package/lib/commonjs/highlight-updates/components/IsolatedRenderList.js +1 -188
  7. package/lib/commonjs/highlight-updates/components/ModalHeaderContent.js +1 -293
  8. package/lib/commonjs/highlight-updates/components/RenderCauseBadge.js +1 -508
  9. package/lib/commonjs/highlight-updates/components/RenderDetailView.js +1 -846
  10. package/lib/commonjs/highlight-updates/components/RenderHistoryViewer.js +1 -915
  11. package/lib/commonjs/highlight-updates/components/RenderListItem.js +1 -235
  12. package/lib/commonjs/highlight-updates/components/RendersCopySettingsView.js +1 -469
  13. package/lib/commonjs/highlight-updates/components/StatsDisplay.js +1 -73
  14. package/lib/commonjs/highlight-updates/types/copySettings.js +1 -107
  15. package/lib/commonjs/highlight-updates/utils/CommitProfiler.js +1 -455
  16. package/lib/commonjs/highlight-updates/utils/HighlightUpdatesController.js +1 -1929
  17. package/lib/commonjs/highlight-updates/utils/PerformanceLogger.js +26 -359
  18. package/lib/commonjs/highlight-updates/utils/ProfilerInterceptor.js +1 -371
  19. package/lib/commonjs/highlight-updates/utils/RenderCauseDetector.js +5 -1840
  20. package/lib/commonjs/highlight-updates/utils/RenderTracker.js +1 -952
  21. package/lib/commonjs/highlight-updates/utils/ViewTypeMapper.js +1 -264
  22. package/lib/commonjs/highlight-updates/utils/copySettingsStorage.js +1 -32
  23. package/lib/commonjs/highlight-updates/utils/devtoolsComponentNames.js +1 -188
  24. package/lib/commonjs/highlight-updates/utils/namedComposite.js +1 -41
  25. package/lib/commonjs/highlight-updates/utils/ownStateChanged.js +1 -103
  26. package/lib/commonjs/highlight-updates/utils/rawEventEmitter.js +1 -50
  27. package/lib/commonjs/highlight-updates/utils/rawEventEmitter.web.js +1 -12
  28. package/lib/commonjs/highlight-updates/utils/renderExportFormatter.js +1 -58
  29. package/lib/commonjs/highlight-updates/utils/rendersExportFormatter.js +1 -485
  30. package/lib/commonjs/highlight-updates/utils/screenElements.js +1 -1269
  31. package/lib/commonjs/highlight-updates/utils/tapMatch.js +1 -83
  32. package/lib/commonjs/highlight-updates/utils/testIDs.js +1 -68
  33. package/lib/commonjs/highlight-updates/utils/touchCapture.js +1 -277
  34. package/lib/commonjs/index.js +1 -429
  35. package/lib/commonjs/preset.js +1 -278
  36. package/lib/commonjs/sync/highlightUpdatesSyncAdapter.js +1 -235
  37. package/lib/module/highlight-updates/HighlightUpdatesOverlay.js +1 -274
  38. package/lib/module/highlight-updates/components/HighlightFilterView.js +1 -1468
  39. package/lib/module/highlight-updates/components/HighlightUpdatesModal.js +1 -634
  40. package/lib/module/highlight-updates/components/IdentifierBadge.js +1 -265
  41. package/lib/module/highlight-updates/components/IsolatedRenderList.js +1 -183
  42. package/lib/module/highlight-updates/components/ModalHeaderContent.js +1 -288
  43. package/lib/module/highlight-updates/components/RenderCauseBadge.js +1 -499
  44. package/lib/module/highlight-updates/components/RenderDetailView.js +1 -840
  45. package/lib/module/highlight-updates/components/RenderHistoryViewer.js +1 -908
  46. package/lib/module/highlight-updates/components/RenderListItem.js +1 -230
  47. package/lib/module/highlight-updates/components/RendersCopySettingsView.js +1 -465
  48. package/lib/module/highlight-updates/components/StatsDisplay.js +1 -70
  49. package/lib/module/highlight-updates/types/copySettings.js +1 -102
  50. package/lib/module/highlight-updates/utils/CommitProfiler.js +1 -451
  51. package/lib/module/highlight-updates/utils/HighlightUpdatesController.js +1 -1925
  52. package/lib/module/highlight-updates/utils/PerformanceLogger.js +26 -353
  53. package/lib/module/highlight-updates/utils/ProfilerInterceptor.js +1 -358
  54. package/lib/module/highlight-updates/utils/RenderCauseDetector.js +5 -1830
  55. package/lib/module/highlight-updates/utils/RenderTracker.js +1 -949
  56. package/lib/module/highlight-updates/utils/ViewTypeMapper.js +1 -255
  57. package/lib/module/highlight-updates/utils/copySettingsStorage.js +1 -26
  58. package/lib/module/highlight-updates/utils/devtoolsComponentNames.js +1 -178
  59. package/lib/module/highlight-updates/utils/namedComposite.js +1 -36
  60. package/lib/module/highlight-updates/utils/ownStateChanged.js +1 -98
  61. package/lib/module/highlight-updates/utils/rawEventEmitter.js +1 -46
  62. package/lib/module/highlight-updates/utils/rawEventEmitter.web.js +1 -3
  63. package/lib/module/highlight-updates/utils/renderExportFormatter.js +1 -54
  64. package/lib/module/highlight-updates/utils/rendersExportFormatter.js +1 -478
  65. package/lib/module/highlight-updates/utils/screenElements.js +1 -1263
  66. package/lib/module/highlight-updates/utils/tapMatch.js +1 -76
  67. package/lib/module/highlight-updates/utils/testIDs.js +1 -60
  68. package/lib/module/highlight-updates/utils/touchCapture.js +1 -270
  69. package/lib/module/index.js +1 -130
  70. package/lib/module/preset.js +1 -272
  71. package/lib/module/sync/highlightUpdatesSyncAdapter.js +1 -230
  72. package/lib/web/index.mjs +28 -29
  73. package/package.json +7 -11
  74. package/lib/typescript/highlight-updates/HighlightUpdatesOverlay.d.ts.map +0 -1
  75. package/lib/typescript/highlight-updates/components/HighlightFilterView.d.ts.map +0 -1
  76. package/lib/typescript/highlight-updates/components/HighlightUpdatesModal.d.ts.map +0 -1
  77. package/lib/typescript/highlight-updates/components/IdentifierBadge.d.ts.map +0 -1
  78. package/lib/typescript/highlight-updates/components/IsolatedRenderList.d.ts.map +0 -1
  79. package/lib/typescript/highlight-updates/components/ModalHeaderContent.d.ts.map +0 -1
  80. package/lib/typescript/highlight-updates/components/RenderCauseBadge.d.ts.map +0 -1
  81. package/lib/typescript/highlight-updates/components/RenderDetailView.d.ts.map +0 -1
  82. package/lib/typescript/highlight-updates/components/RenderHistoryViewer.d.ts.map +0 -1
  83. package/lib/typescript/highlight-updates/components/RenderListItem.d.ts.map +0 -1
  84. package/lib/typescript/highlight-updates/components/RendersCopySettingsView.d.ts.map +0 -1
  85. package/lib/typescript/highlight-updates/components/StatsDisplay.d.ts.map +0 -1
  86. package/lib/typescript/highlight-updates/types/copySettings.d.ts.map +0 -1
  87. package/lib/typescript/highlight-updates/utils/CommitProfiler.d.ts.map +0 -1
  88. package/lib/typescript/highlight-updates/utils/HighlightUpdatesController.d.ts.map +0 -1
  89. package/lib/typescript/highlight-updates/utils/PerformanceLogger.d.ts.map +0 -1
  90. package/lib/typescript/highlight-updates/utils/ProfilerInterceptor.d.ts.map +0 -1
  91. package/lib/typescript/highlight-updates/utils/RenderCauseDetector.d.ts.map +0 -1
  92. package/lib/typescript/highlight-updates/utils/RenderTracker.d.ts.map +0 -1
  93. package/lib/typescript/highlight-updates/utils/ViewTypeMapper.d.ts.map +0 -1
  94. package/lib/typescript/highlight-updates/utils/copySettingsStorage.d.ts.map +0 -1
  95. package/lib/typescript/highlight-updates/utils/devtoolsComponentNames.d.ts.map +0 -1
  96. package/lib/typescript/highlight-updates/utils/namedComposite.d.ts.map +0 -1
  97. package/lib/typescript/highlight-updates/utils/ownStateChanged.d.ts.map +0 -1
  98. package/lib/typescript/highlight-updates/utils/rawEventEmitter.d.ts.map +0 -1
  99. package/lib/typescript/highlight-updates/utils/rawEventEmitter.web.d.ts.map +0 -1
  100. package/lib/typescript/highlight-updates/utils/renderExportFormatter.d.ts.map +0 -1
  101. package/lib/typescript/highlight-updates/utils/rendersExportFormatter.d.ts.map +0 -1
  102. package/lib/typescript/highlight-updates/utils/screenElements.d.ts.map +0 -1
  103. package/lib/typescript/highlight-updates/utils/tapMatch.d.ts.map +0 -1
  104. package/lib/typescript/highlight-updates/utils/testIDs.d.ts.map +0 -1
  105. package/lib/typescript/highlight-updates/utils/touchCapture.d.ts.map +0 -1
  106. package/lib/typescript/index.d.ts.map +0 -1
  107. package/lib/typescript/preset.d.ts.map +0 -1
  108. package/lib/typescript/sync/highlightUpdatesSyncAdapter.d.ts.map +0 -1
@@ -1,1929 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.default = void 0;
7
- var _utils = require("@buoy-gg/shared-ui/utils");
8
- var _ProfilerInterceptor = require("./ProfilerInterceptor");
9
- var _reactNative = require("react-native");
10
- var _RenderTracker = require("./RenderTracker");
11
- var _screenElements = require("./screenElements");
12
- var _tapMatch = require("./tapMatch");
13
- var _PerformanceLogger = require("./PerformanceLogger");
14
- var _RenderCauseDetector = require("./RenderCauseDetector");
15
- var _sharedUi = require("@buoy-gg/shared-ui");
16
- var _devtoolsComponentNames = require("./devtoolsComponentNames");
17
- /**
18
- * Highlight Updates Controller
19
- *
20
- * Standalone implementation that replicates React DevTools' "Highlight updates
21
- * when components render" feature WITHOUT requiring DevTools to be connected.
22
- *
23
- * Architecture:
24
- * This controller directly enables the DevTools backend's traceUpdates feature
25
- * by calling setTraceUpdatesEnabled(true) on each renderer interface. This is
26
- * the same function that DevTools' frontend calls when you check the
27
- * "Highlight updates when components render" checkbox.
28
- *
29
- * Key insight: The rendererInterfaces are available on the global hook even
30
- * without DevTools frontend connected. We just need to enable tracing directly.
31
- *
32
- * Flow:
33
- * 1. User enables highlights via toggle()
34
- * 2. We call rendererInterface.setTraceUpdatesEnabled(true) on all renderers
35
- * 3. DevTools backend now tracks renders and emits 'traceUpdates' events
36
- * 4. We subscribe to 'traceUpdates' via hook.sub()
37
- * 5. When components re-render, we receive the Set of host stateNodes
38
- * 6. We measure each node and render colored border highlights
39
- *
40
- * This gives us 100% accuracy with DevTools behavior because we're using the
41
- * exact same detection code - we're just enabling it programmatically.
42
- */
43
-
44
- "use strict";
45
- // State
46
- let globalEnabled = false; // User-controlled toggle for visual highlights
47
- // When true, tracking + measurement + storage all run as normal, but the
48
- // visual highlight boxes are NOT drawn. Lets the desktop "screenshot this
49
- // component" flow get the render history + measurements with no on-device
50
- // overlay. Independent of globalEnabled; reset whenever the visual tool is
51
- // enabled (the Renders tab always wants its boxes).
52
- let highlightsSuppressed = false;
53
- let backgroundTrackingEnabled = false; // Auto-enabled when event callbacks exist (Events tool)
54
- let initialized = false;
55
- let hook = null;
56
- let highlightCallback = null;
57
- let badgePressCallback = null;
58
- let traceUpdatesUnsubscribe = null;
59
- let isProcessing = false;
60
-
61
- // Freeze frame state
62
- let isFrozen = false;
63
- const freezeListeners = new Set();
64
- const stateListeners = new Set();
65
-
66
- // Track render counts per node (for color assignment like DevTools)
67
- // Map from nativeTag to render count - nativeTag is stable across re-renders
68
- const nodeRenderCounts = new Map();
69
-
70
- // Color palette for highlights (same as DevTools)
71
- // From: react-devtools-core/dist/backend.js line 6361
72
- const COLORS = ["#37afa9", "#63b19e", "#80b393", "#97b488", "#abb67d", "#beb771", "#cfb965", "#dfba57", "#efbb49", "#febc38"];
73
-
74
- // Set to true for verbose debugging of our implementation
75
- // Disabled for now to focus on profiler logs only
76
- const DEBUG = false;
77
- function debugLog(message, data) {
78
- if (!DEBUG) return;
79
- if (data !== undefined) {
80
- console.log(`[HighlightUpdates] ${message}`, data);
81
- } else {
82
- console.log(`[HighlightUpdates] ${message}`);
83
- }
84
- }
85
-
86
- /**
87
- * Get the public instance from a stateNode (the node passed in traceUpdates)
88
- */
89
- function getPublicInstance(stateNode) {
90
- const dom = (0, _utils.getDOMPublicInstance)(stateNode);
91
- if (dom) return dom;
92
- if (!stateNode) return null;
93
- const node = stateNode;
94
-
95
- // Fabric: stateNode.canonical.publicInstance
96
- if (node.canonical?.publicInstance) {
97
- return node.canonical.publicInstance;
98
- }
99
-
100
- // Legacy Fabric: stateNode.canonical with measure
101
- if (node.canonical && typeof node.canonical.measure === "function") {
102
- return node.canonical;
103
- }
104
-
105
- // Legacy renderer: stateNode has measure directly
106
- if (typeof node.measure === "function") {
107
- return node;
108
- }
109
- return null;
110
- }
111
-
112
- /**
113
- * Get native tag from public instance or stateNode
114
- */
115
- function getNativeTag(instance) {
116
- const dom = (0, _utils.getDOMNodeHandle)(instance);
117
- if (dom !== null) return dom;
118
- if (instance == null) return null;
119
- const inst = instance;
120
-
121
- // Try direct properties
122
- if (inst.__nativeTag != null) return inst.__nativeTag;
123
- if (inst._nativeTag != null) return inst._nativeTag;
124
- if (inst.nativeTag != null) return inst.nativeTag;
125
-
126
- // Try canonical
127
- if (inst.canonical) {
128
- if (inst.canonical.__nativeTag != null) return inst.canonical.__nativeTag;
129
- if (inst.canonical._nativeTag != null) return inst.canonical._nativeTag;
130
- // Modern Fabric instance handles: { node, canonical: { nativeTag, ... } }.
131
- // Without this shape ~99% of traced nodes were dropped on New
132
- // Architecture — highlights only appeared for tap-shaped legacy nodes.
133
- if (inst.canonical.nativeTag != null) return inst.canonical.nativeTag;
134
- }
135
- return null;
136
- }
137
-
138
- /**
139
- * Extract component info from a stateNode for RenderTracker
140
- */
141
- function extractComponentInfo(stateNode) {
142
- const node = stateNode;
143
- const info = {
144
- viewType: "Unknown"
145
- };
146
-
147
- // Get viewType from viewConfig
148
- if (node?.canonical?.viewConfig?.uiViewClassName) {
149
- info.viewType = node.canonical.viewConfig.uiViewClassName;
150
- } else if (node?.viewConfig?.uiViewClassName) {
151
- info.viewType = node.viewConfig.uiViewClassName;
152
- }
153
-
154
- // Try to get props from various locations
155
- // Fabric: canonical.internalInstanceHandle, Paper: _internalInstanceHandle, RN CLI DEV: _internalFiberInstanceHandleDEV
156
- const fiber = node?.canonical?.internalInstanceHandle || node?._internalInstanceHandle || node?._internalFiberInstanceHandleDEV || (0, _utils.getDOMFiber)(node);
157
- const currentProps = node?.canonical?.currentProps;
158
- const pendingProps = node?.canonical?.pendingProps;
159
- const fiberPendingProps = fiber?.pendingProps;
160
- const fiberMemoizedProps = fiber?.memoizedProps;
161
-
162
- // Extract testID
163
- info.testID = currentProps?.testID || pendingProps?.testID || fiberPendingProps?.testID || fiberMemoizedProps?.testID || undefined;
164
-
165
- // Extract nativeID
166
- info.nativeID = currentProps?.nativeID || pendingProps?.nativeID || fiberPendingProps?.nativeID || fiberMemoizedProps?.nativeID || undefined;
167
-
168
- // Extract accessibilityLabel
169
- info.accessibilityLabel = currentProps?.accessibilityLabel || pendingProps?.accessibilityLabel || fiberPendingProps?.accessibilityLabel || fiberMemoizedProps?.accessibilityLabel || undefined;
170
-
171
- // Extract componentName - use getOwningComponentName to get the React component
172
- // that rendered this host component
173
- info.componentName = getOwningComponentName(fiber) || undefined;
174
- return info;
175
- }
176
-
177
- /**
178
- * Describe a node for logging (extensive version for debugging)
179
- */
180
- function describeNodeForLog(stateNode) {
181
- const node = stateNode;
182
- const publicInstance = getPublicInstance(stateNode);
183
- const nativeTag = getNativeTag(stateNode) || getNativeTag(publicInstance);
184
- const info = {
185
- nativeTag: nativeTag ?? "unknown",
186
- type: node?.canonical ? "Fabric" : "Legacy",
187
- hasMeasure: typeof publicInstance?.measure === "function"
188
- };
189
-
190
- // Extract as much info as possible from the stateNode
191
- if (node) {
192
- // Direct properties on stateNode
193
- info.stateNodeKeys = Object.keys(node).slice(0, 20);
194
-
195
- // Check the 'node' property (might have useful info)
196
- if (node.node) {
197
- info.nodeKeys = Object.keys(node.node).slice(0, 20);
198
- }
199
-
200
- // Check for viewConfig (contains component type info)
201
- if (node.viewConfig) {
202
- info.viewConfig = {
203
- uiViewClassName: node.viewConfig.uiViewClassName,
204
- validAttributes: node.viewConfig.validAttributes ? Object.keys(node.viewConfig.validAttributes).slice(0, 10) : undefined
205
- };
206
- }
207
-
208
- // Check canonical structure (Fabric)
209
- if (node.canonical) {
210
- const canonical = node.canonical;
211
- info.canonicalKeys = Object.keys(canonical).slice(0, 20);
212
- if (canonical.viewConfig) {
213
- info.canonicalViewConfig = {
214
- uiViewClassName: canonical.viewConfig.uiViewClassName
215
- };
216
- }
217
- if (canonical.nativeTag != null) {
218
- info.canonicalNativeTag = canonical.nativeTag;
219
- }
220
-
221
- // Check internalInstanceHandle (THE FIBER!)
222
- if (canonical.internalInstanceHandle) {
223
- const fiber = canonical.internalInstanceHandle;
224
- info.fiberKeys = Object.keys(fiber).slice(0, 30);
225
-
226
- // Component type info
227
- if (fiber.type) {
228
- info.fiberType = typeof fiber.type === "function" ? fiber.type.name || fiber.type.displayName || "function" : fiber.type;
229
- }
230
- if (fiber.elementType) {
231
- info.fiberElementType = typeof fiber.elementType === "function" ? fiber.elementType.name || fiber.elementType.displayName || "function" : fiber.elementType;
232
- }
233
-
234
- // Debug info on fiber
235
- if (fiber._debugOwner) {
236
- const owner = fiber._debugOwner;
237
- info.fiberDebugOwner = owner.type?.name || owner.type?.displayName || owner.elementType?.name || "unknown";
238
- }
239
- if (fiber._debugSource) {
240
- info.fiberDebugSource = fiber._debugSource;
241
- }
242
-
243
- // Fiber tag (tells us what kind of fiber it is)
244
- if (fiber.tag != null) {
245
- info.fiberTag = fiber.tag;
246
- }
247
-
248
- // pendingProps on fiber might have testID/nativeID
249
- if (fiber.pendingProps) {
250
- if (fiber.pendingProps.nativeID) info.fiberPendingNativeID = fiber.pendingProps.nativeID;
251
- if (fiber.pendingProps.testID) info.fiberPendingTestID = fiber.pendingProps.testID;
252
- }
253
- if (fiber.memoizedProps) {
254
- if (fiber.memoizedProps.nativeID) info.fiberMemoizedNativeID = fiber.memoizedProps.nativeID;
255
- if (fiber.memoizedProps.testID) info.fiberMemoizedTestID = fiber.memoizedProps.testID;
256
- }
257
- }
258
-
259
- // Check publicInstance
260
- if (canonical.publicInstance) {
261
- const pub = canonical.publicInstance;
262
- info.publicInstanceKeys = Object.keys(pub).slice(0, 20);
263
-
264
- // Look for nativeID
265
- if (pub.nativeID != null) info.nativeID = pub.nativeID;
266
- if (pub._nativeID != null) info._nativeID = pub._nativeID;
267
-
268
- // Look for props
269
- if (pub.props) {
270
- info.publicInstanceProps = Object.keys(pub.props).slice(0, 15);
271
- if (pub.props.nativeID) info.propsNativeID = pub.props.nativeID;
272
- if (pub.props.testID) info.propsTestID = pub.props.testID;
273
- if (pub.props.accessibilityLabel) info.accessibilityLabel = pub.props.accessibilityLabel;
274
- }
275
- }
276
-
277
- // Check for currentProps - log actual VALUES
278
- if (canonical.currentProps) {
279
- info.currentPropsKeys = Object.keys(canonical.currentProps).slice(0, 15);
280
- // Log actual values of identifying props
281
- if (canonical.currentProps.nativeID != null) info.currentPropsNativeID = canonical.currentProps.nativeID;
282
- if (canonical.currentProps.testID != null) info.currentPropsTestID = canonical.currentProps.testID;
283
- if (canonical.currentProps.accessibilityLabel != null) info.currentPropsAccessLabel = canonical.currentProps.accessibilityLabel;
284
- }
285
-
286
- // Check for pendingProps
287
- if (canonical.pendingProps) {
288
- info.pendingPropsKeys = Object.keys(canonical.pendingProps).slice(0, 15);
289
- if (canonical.pendingProps.nativeID != null) info.pendingPropsNativeID = canonical.pendingProps.nativeID;
290
- if (canonical.pendingProps.testID != null) info.pendingPropsTestID = canonical.pendingProps.testID;
291
- }
292
- }
293
-
294
- // Check for fiber reference directly on node
295
- if (node._debugOwner) {
296
- const owner = node._debugOwner;
297
- info.debugOwnerType = owner.type?.name || owner.type?.displayName || typeof owner.type;
298
- }
299
- if (node._debugSource) {
300
- info.debugSource = node._debugSource;
301
- }
302
- }
303
- return info;
304
- }
305
-
306
- // Set to true to enable debug logging
307
- const DEBUG_LOGGING = false;
308
-
309
- // Lock to prevent infinite loops when rendering our overlay
310
- let renderingLock = false;
311
- let renderingLockTimeout = null;
312
-
313
- // How long to ignore new events after starting a render (ms)
314
- // Should match HIGHLIGHT_DURATION in the overlay to prevent false triggers
315
- const RENDER_LOCK_DURATION = 350;
316
-
317
- // Track nativeTags of components we're currently highlighting
318
- // This helps us identify (and skip) our own overlay Views if they re-render
319
- const renderedOverlayTags = new Set();
320
-
321
- /**
322
- * Check if a nativeTag belongs to our overlay (was rendered by us previously)
323
- */
324
- function isOurOverlayTag(nativeTag) {
325
- if (nativeTag == null) return false;
326
- return renderedOverlayTags.has(nativeTag);
327
- }
328
-
329
- // ============================================================================
330
- // OPTIMIZED DEV TOOLS DETECTION
331
- // ============================================================================
332
- // Performance optimizations applied:
333
- // 1. Result caching by nativeTag (same tag = same result)
334
- // 2. Set-based O(1) lookups instead of multiple string comparisons
335
- // 3. Component name check FIRST (appears early in tree, ~depth 5-8)
336
- // 4. Early exit on first match
337
- // 5. Cached property access to avoid repeated traversal
338
- // ============================================================================
339
-
340
- // Component-name sets/prefixes live in devtoolsComponentNames.ts (shared with
341
- // CommitProfiler). Native-ID sets stay here — only the node walk uses them.
342
-
343
- // Native IDs that MUST always be excluded (highlight overlay)
344
- const HIGHLIGHT_OVERLAY_NATIVE_IDS = new Set(["highlight-updates-overlay", "__rn_buoy__highlight-modal"]);
345
-
346
- // Other dev tools native IDs - can be toggled
347
- const OTHER_DEV_TOOLS_NATIVE_IDS = new Set(["jsmodal-root", "image-overlay-standalone",
348
- // LogBox native IDs
349
- "logbox_inspector", "logbox"]);
350
-
351
- // Cache: nativeTag -> isDevTools (avoid re-walking tree for same node)
352
- const devToolsNodeCache = new Map();
353
- const CACHE_MAX_SIZE = 500;
354
-
355
- /**
356
- * Clear the devtools node cache.
357
- * Call this when the excludeDevTools setting changes to re-evaluate all nodes.
358
- */
359
- function clearDevToolsCache() {
360
- devToolsNodeCache.clear();
361
- }
362
-
363
- /**
364
- * Check if a nativeID belongs to highlight overlay (always excluded)
365
- */
366
- function isHighlightOverlayNativeID(nativeID) {
367
- if (!nativeID) return false;
368
- // Direct Set lookup (O(1))
369
- if (HIGHLIGHT_OVERLAY_NATIVE_IDS.has(nativeID)) return true;
370
- // Prefix check for highlight overlay
371
- if (nativeID.startsWith("__highlight_")) return true;
372
- return false;
373
- }
374
-
375
- /**
376
- * Check if a nativeID belongs to other dev tools (optionally excluded)
377
- */
378
- function isOtherDevToolsNativeID(nativeID) {
379
- if (!nativeID) return false;
380
- // Direct Set lookup (O(1))
381
- if (OTHER_DEV_TOOLS_NATIVE_IDS.has(nativeID)) return true;
382
- // Prefix checks for other devtools nativeIDs
383
- const firstChar = nativeID.charCodeAt(0);
384
- if (firstChar === 95) {
385
- // '_' = 95
386
- if (nativeID.startsWith("__rn_buoy__")) return true;
387
- }
388
- // Check for LogBox nativeIDs (start with 'l')
389
- if (firstChar === 108 && nativeID.startsWith("logbox")) {
390
- // 'l' = 108
391
- return true;
392
- }
393
- return false;
394
- }
395
-
396
- /**
397
- * Get component name from fiber (cached property access pattern)
398
- * For host components (RCTView, RCTText), returns the native type.
399
- * Use getOwningComponentName to get the React component that rendered it.
400
- */
401
- function getComponentName(fiber) {
402
- if (!fiber) return null;
403
- // Most common: type.name
404
- const type = fiber.type;
405
- if (type) {
406
- if (typeof type === 'string') return type;
407
- if (type.name) return type.name;
408
- if (type.displayName) return type.displayName;
409
- }
410
- // Fallback: elementType
411
- const elementType = fiber.elementType;
412
- if (elementType) {
413
- if (elementType.name) return elementType.name;
414
- if (elementType.displayName) return elementType.displayName;
415
- }
416
- return null;
417
- }
418
-
419
- // Common React Native internal component names to skip when finding user components
420
- const INTERNAL_COMPONENT_NAMES = new Set([
421
- // React Native core
422
- 'View', 'Text', 'TextImpl', 'Image', 'ScrollView', 'FlatList', 'SectionList', 'TouchableOpacity', 'TouchableHighlight', 'TouchableWithoutFeedback', 'Pressable', 'TextInput', 'Switch', 'ActivityIndicator', 'Modal', 'StatusBar', 'KeyboardAvoidingView',
423
- // Animated components
424
- 'AnimatedComponent', 'AnimatedComponentWrapper',
425
- // React Navigation / Screens
426
- 'ScreenContainer', 'ScreenStack', 'Screen', 'ScreenContentWrapper',
427
- // SVG components
428
- 'Svg', 'G', 'Path', 'Rect', 'Circle', 'Line', 'Polygon', 'Polyline', 'Ellipse', 'Text as SVGText', 'TSpan', 'TextPath', 'Use', 'Symbol', 'Defs', 'ClipPath', 'LinearGradient', 'RadialGradient', 'Stop', 'Mask', 'Pattern', 'Image as SVGImage',
429
- // SafeArea
430
- 'SafeAreaProvider', 'SafeAreaView', 'SafeAreaListener',
431
- // Gesture Handler
432
- 'GestureHandlerRootView', 'GestureDetector',
433
- // Reanimated
434
- 'ReanimatedView', 'ReanimatedText', 'ReanimatedImage', 'ReanimatedScrollView',
435
- // Common wrapper names
436
- 'Fragment', 'Suspense', 'Provider', 'Consumer', 'Context', 'ForwardRef']);
437
-
438
- /**
439
- * Check if a component name is an internal/wrapper component that should be skipped
440
- */
441
- function isInternalComponent(name) {
442
- if (!name) return true;
443
- if (INTERNAL_COMPONENT_NAMES.has(name)) return true;
444
- // Skip anonymous components and common patterns
445
- if (name === 'Unknown' || name === 'Component') return true;
446
- // Skip Animated.* wrappers
447
- if (name.startsWith('Animated')) return true;
448
- return false;
449
- }
450
-
451
- /**
452
- * Get the owning React component name for a host fiber.
453
- * Walks up the fiber tree to find the first user-defined component,
454
- * skipping React Native internal components.
455
- */
456
- function getOwningComponentName(fiber) {
457
- if (!fiber) return null;
458
-
459
- // Walk up the fiber tree looking for a user-defined component
460
- let current = fiber._debugOwner || fiber.return;
461
- let depth = 0;
462
- let firstNonHostName = null;
463
- while (current && depth < 30) {
464
- const name = getComponentName(current);
465
-
466
- // Skip host components (their type is a string like "RCTView")
467
- if (name && typeof current.type !== 'string') {
468
- // Remember the first non-host component as fallback
469
- if (!firstNonHostName) {
470
- firstNonHostName = name;
471
- }
472
- // Return this component if it's not an internal wrapper
473
- if (!isInternalComponent(name)) {
474
- return name;
475
- }
476
- }
477
- current = current.return;
478
- depth++;
479
- }
480
-
481
- // Fallback to first non-host component found (even if internal)
482
- return firstNonHostName;
483
- }
484
-
485
- /**
486
- * Check if a stateNode belongs to our dev tools.
487
- *
488
- * OPTIMIZED: Uses caching, O(1) Set lookups, and early exit patterns.
489
- * Walks up fiber tree checking component names (fast) before nativeIDs.
490
- *
491
- * When excludeDevTools setting is false, only the Highlight Updates overlay
492
- * is excluded (to prevent infinite loops). Other dev tools will be highlighted.
493
- */
494
- function isOurOverlayNode(stateNode) {
495
- const node = stateNode;
496
- // Fabric: fiber is at canonical.internalInstanceHandle
497
- // Paper/Legacy: fiber is at _internalInstanceHandle directly on stateNode
498
- // RN CLI DEV: fiber is at _internalFiberInstanceHandleDEV
499
- const fiber = node?.canonical?.internalInstanceHandle || node?._internalInstanceHandle || node?._internalFiberInstanceHandleDEV || (0, _utils.getDOMFiber)(node);
500
-
501
- // Get nativeTag for caching
502
- const nativeTag = getNativeTag(stateNode) || getNativeTag(node?.canonical?.publicInstance);
503
-
504
- // Check if we should exclude all dev tools or just highlight overlay.
505
- // Bench "profile our own tools" mode (the example app's /tool-bench page)
506
- // forces this OFF so live highlight boxes flash on our tool UI too —
507
- // except the perf-monitor HUD (checked separately in the walk below),
508
- // whose 250ms sample-tick churn would flash constantly. Cache reads and
509
- // writes are already gated on excludeDevTools, so bench mode bypasses
510
- // the cache and can't poison it.
511
- const benchMode = (0, _devtoolsComponentNames.isCapturingBuoyToolRenders)();
512
- const excludeDevTools = !benchMode && _RenderTracker.RenderTracker.getSettings().excludeDevTools;
513
- if (excludeDevTools && (0, _utils.isDOMElement)(stateNode) && stateNode.closest('[data-buoy-root]')) return true;
514
-
515
- // Check cache first (O(1)) - but only for the current excludeDevTools setting
516
- // When excludeDevTools is false, skip cache to re-evaluate all nodes
517
- if (nativeTag != null && excludeDevTools) {
518
- const cached = devToolsNodeCache.get(nativeTag);
519
- if (cached !== undefined) {
520
- return cached;
521
- }
522
- }
523
- let result = false;
524
-
525
- // Fast path: check direct nativeID
526
- const directNativeID = fiber?.pendingProps?.nativeID || fiber?.memoizedProps?.nativeID || node?.canonical?.currentProps?.nativeID || null;
527
-
528
- // Always check highlight overlay nativeIDs first (must always be excluded)
529
- if (isHighlightOverlayNativeID(directNativeID)) {
530
- result = true;
531
- } else if (excludeDevTools && isOtherDevToolsNativeID(directNativeID)) {
532
- // Only check other dev tools if excludeDevTools is enabled
533
- result = true;
534
- }
535
- if (!result && fiber) {
536
- // Walk up fiber tree - check component names FIRST (they appear early)
537
- let current = fiber;
538
- let depth = 0;
539
-
540
- // Reduced max depth - component names appear by depth 15 typically
541
- while (current && depth < 30) {
542
- // Component name check FIRST (faster, appears earlier in tree)
543
- const name = getComponentName(current);
544
- if (name) {
545
- // Always check highlight overlay components (must always be excluded)
546
- if (_devtoolsComponentNames.HIGHLIGHT_OVERLAY_COMPONENTS.has(name)) {
547
- result = true;
548
- break;
549
- }
550
- // Check highlight overlay prefixes
551
- for (const prefix of _devtoolsComponentNames.HIGHLIGHT_OVERLAY_PREFIXES) {
552
- if (name.startsWith(prefix)) {
553
- result = true;
554
- break;
555
- }
556
- }
557
- if (result) break;
558
-
559
- // Bench mode: the perf-monitor HUD still never flashes.
560
- if (benchMode && (0, _devtoolsComponentNames.isPerfMonitorComponentName)(name)) {
561
- result = true;
562
- break;
563
- }
564
-
565
- // Only check other dev tools if excludeDevTools is enabled
566
- if (excludeDevTools) {
567
- // Two sets, same meaning: COMPONENTS is the original list, EXACT
568
- // holds the names that used to be matched by over-broad prefixes
569
- // (the "Virtualized" that hid every FlatList, the "Dial" that hid
570
- // every Dialog). Both paths must consult both, or Buoy's own chrome
571
- // starts flashing highlight boxes in one of them and not the other.
572
- if (_devtoolsComponentNames.OTHER_DEV_TOOLS_COMPONENTS.has(name) || _devtoolsComponentNames.OTHER_DEV_TOOLS_EXACT.has(name)) {
573
- result = true;
574
- break;
575
- }
576
- // Check other dev tools prefixes
577
- for (const prefix of _devtoolsComponentNames.OTHER_DEV_TOOLS_PREFIXES) {
578
- if (name.startsWith(prefix)) {
579
- result = true;
580
- break;
581
- }
582
- }
583
- if (result) break;
584
- }
585
- }
586
-
587
- // NativeID check (only if component name didn't match)
588
- const nativeID = current.pendingProps?.nativeID || current.memoizedProps?.nativeID;
589
- if (isHighlightOverlayNativeID(nativeID)) {
590
- result = true;
591
- break;
592
- }
593
- if (excludeDevTools && isOtherDevToolsNativeID(nativeID)) {
594
- result = true;
595
- break;
596
- }
597
- current = current.return;
598
- depth++;
599
- }
600
- }
601
-
602
- // Only cache positive results when excludeDevTools is true (default mode)
603
- // Don't cache when excludeDevTools is false to avoid complex cache invalidation
604
- if (result && nativeTag != null && excludeDevTools) {
605
- if (devToolsNodeCache.size >= CACHE_MAX_SIZE) {
606
- // Clear oldest entries (simple strategy - clear half)
607
- const entries = Array.from(devToolsNodeCache.keys());
608
- for (let i = 0; i < CACHE_MAX_SIZE / 2; i++) {
609
- devToolsNodeCache.delete(entries[i]);
610
- }
611
- }
612
- devToolsNodeCache.set(nativeTag, result);
613
- }
614
- return result;
615
- }
616
-
617
- /**
618
- * Get color based on render count (same algorithm as DevTools)
619
- * More renders = warmer color (cyan → yellow)
620
- */
621
- function getColorForRenderCount(count) {
622
- // Clamp to color array bounds
623
- const index = Math.min(count - 1, COLORS.length - 1);
624
- return COLORS[Math.max(0, index)];
625
- }
626
-
627
- /**
628
- * Handle traceUpdates event from DevTools backend
629
- * This is called with a Set of stateNodes that should be highlighted
630
- *
631
- * Strategy to prevent infinite loops:
632
- * 1. Use a rendering lock while rendering highlights
633
- * 2. Use an isProcessing flag to prevent concurrent processing
634
- * 3. If Chrome DevTools agent is available, use native overlay (faster, no React)
635
- * 4. Otherwise, use our React overlay with locks to prevent cascading renders
636
- */
637
- function handleTraceUpdates(nodes) {
638
- if (nodes.size === 0) {
639
- return;
640
- }
641
-
642
- // Skip if neither visual toggle nor background tracking is enabled
643
- if (!globalEnabled && !backgroundTrackingEnabled) {
644
- return;
645
- }
646
-
647
- // Skip processing when paused - don't count renders or show highlights
648
- const trackerState = _RenderTracker.RenderTracker.getState();
649
- if (trackerState.isPaused) {
650
- return;
651
- }
652
-
653
- // Mark event received for end-to-end timing
654
- if (_PerformanceLogger.PerformanceLogger.isEnabled()) {
655
- (0, _PerformanceLogger.markEventReceived)();
656
- }
657
-
658
- // Start performance timing
659
- const batchSize = _RenderTracker.RenderTracker.getBatchSize();
660
- const perfTimer = _PerformanceLogger.PerformanceLogger.startBatch(nodes.size, batchSize);
661
-
662
- // NOTE: Lock-based skipping disabled - relying on nativeID filtering instead
663
- // If nativeID filtering doesn't work, re-enable this:
664
- //
665
- // if (renderingLock || isProcessing) {
666
- // if (DEBUG_LOGGING) {
667
- // console.log(`[HighlightUpdates] SKIPPED - lock:${renderingLock} processing:${isProcessing}`);
668
- // }
669
- // return;
670
- // }
671
- // isProcessing = true;
672
-
673
- // Process nodes: track render counts and assign colors
674
- // Filter out our own overlay nodes to prevent infinite loop
675
- // Also apply user-defined filters to both overlay AND tracking
676
- const nodesToDraw = [];
677
- let skippedOverlayCount = 0;
678
- let skippedByFilterCount = 0;
679
- const hasActiveFilters = _RenderTracker.RenderTracker.hasActiveFilters();
680
- for (const stateNode of nodes) {
681
- if (stateNode && typeof stateNode === "object") {
682
- try {
683
- // Skip our own overlay nodes (identified by nativeID)
684
- if (isOurOverlayNode(stateNode)) {
685
- skippedOverlayCount++;
686
- continue;
687
- }
688
-
689
- // Get nativeTag for tracking render counts
690
- const publicInstance = getPublicInstance(stateNode);
691
- const nativeTag = getNativeTag(stateNode) || getNativeTag(publicInstance);
692
- if (nativeTag == null) {
693
- continue;
694
- }
695
-
696
- // Extract component info for filtering
697
- const componentInfo = extractComponentInfo(stateNode);
698
-
699
- // Apply user-defined filters - skip components that don't pass filters
700
- // This affects BOTH the overlay highlights AND the render list
701
- if (hasActiveFilters && !_RenderTracker.RenderTracker.passesFilters(componentInfo)) {
702
- skippedByFilterCount++;
703
- continue;
704
- }
705
-
706
- // Check if render counting is enabled
707
- const showRenderCount = _RenderTracker.RenderTracker.getSettings().showRenderCount;
708
- let newCount;
709
- let color;
710
- if (showRenderCount) {
711
- // Get current render count and increment (using nativeTag as stable key)
712
- const currentCount = nodeRenderCounts.get(nativeTag) || 0;
713
- newCount = currentCount + 1;
714
- nodeRenderCounts.set(nativeTag, newCount);
715
- // Assign color based on render count
716
- color = getColorForRenderCount(newCount);
717
- } else {
718
- // Skip counting - use fixed color and count of 0
719
- newCount = 0;
720
- color = COLORS[0]; // Use first color (cyan)
721
- }
722
- nodesToDraw.push({
723
- node: stateNode,
724
- color,
725
- count: newCount
726
- });
727
- } catch {
728
- // Skip nodes from custom renderers (e.g. React Native Skia) that have
729
- // non-standard stateNode structures
730
- }
731
- }
732
- }
733
-
734
- // Mark filtering phase complete (includes both overlay nodes and user-filtered nodes)
735
- perfTimer.markFilteringComplete(skippedOverlayCount + skippedByFilterCount, nodesToDraw.length);
736
-
737
- // Only log if there are real updates (not just overlay re-renders)
738
- if (nodesToDraw.length === 0) {
739
- // All nodes were overlay nodes - silently skip
740
- perfTimer.finish(); // Still log the batch even if empty
741
- return;
742
- }
743
-
744
- // Log actual component updates
745
- if (DEBUG_LOGGING) {
746
- console.log(`[HighlightUpdates] ${nodesToDraw.length} component(s) re-rendered`);
747
- }
748
-
749
- // NOTE: We prefer our React overlay for standalone operation.
750
- // The native overlay only works when Chrome DevTools profiler is actively enabled.
751
- // Uncomment below to use native overlay when available:
752
- //
753
- // const agent = hook?.reactDevtoolsAgent;
754
- // if (agent?.emit) {
755
- // if (DEBUG_LOGGING) {
756
- // console.log(`[HighlightUpdates] Using native overlay (agent available)`);
757
- // }
758
- // agent.emit("drawTraceUpdates", nodesToDraw);
759
- // isProcessing = false;
760
- // return;
761
- // }
762
-
763
- // Check if we should process - either we have a visual callback OR event callbacks
764
- const hasEventCallbacks = _RenderTracker.RenderTracker.getRenderEventCallbackCount() > 0;
765
- if (!highlightCallback && !hasEventCallbacks) {
766
- if (DEBUG_LOGGING) {
767
- console.log(`[HighlightUpdates] No highlightCallback and no event callbacks - skipping render`);
768
- }
769
- isProcessing = false;
770
- return;
771
- }
772
-
773
- // NOTE: Lock disabled - relying on nativeID filtering instead
774
-
775
- // Measure each node and call the highlight callback
776
- // Use batch size from RenderTracker settings (default: 150)
777
- // Note: batchSize already retrieved above for perfTimer
778
- perfTimer.markMeasurementStart();
779
- // Measure up to 4× batchSize: coalesced commits can exceed one batch and
780
- // the old hard slice silently never highlighted the same tail components.
781
- const measurePromises = nodesToDraw.slice(0, batchSize * 4).map(({
782
- node: stateNode,
783
- color,
784
- count
785
- }) => new Promise(resolve => {
786
- // RN silently never fires measure callbacks for detached nodes;
787
- // without this timeout one dead node hangs the whole batch's
788
- // Promise.all (no boxes, no tracking, counts still climb).
789
- let settled = false;
790
- const settle = value => {
791
- if (settled) return;
792
- settled = true;
793
- clearTimeout(timeoutId);
794
- resolve(value);
795
- };
796
- const timeoutId = setTimeout(() => settle({
797
- rect: null,
798
- stateNode,
799
- color,
800
- count
801
- }), 500);
802
- const publicInstance = getPublicInstance(stateNode);
803
- const nativeTag = getNativeTag(stateNode) || getNativeTag(publicInstance);
804
- if (nativeTag == null) {
805
- settle({
806
- rect: null,
807
- stateNode,
808
- color,
809
- count
810
- });
811
- return;
812
- }
813
- const onMeasured = (x, y, width, height, pageX, pageY) => {
814
- if (pageX == null || pageY == null || width == null || height == null) {
815
- settle({
816
- rect: null,
817
- stateNode,
818
- color,
819
- count
820
- });
821
- return;
822
- }
823
- settle({
824
- rect: {
825
- id: nativeTag,
826
- x: pageX,
827
- y: pageY,
828
- width,
829
- height,
830
- color,
831
- count
832
- },
833
- stateNode,
834
- color,
835
- count
836
- });
837
- };
838
- if (!publicInstance) {
839
- // New Architecture: canonical.publicInstance is often lazily
840
- // ABSENT on traceUpdates wrappers — measure the wrapper's `node`
841
- // (the Fabric shadow node) directly through FabricUIManager.
842
- const fabricNode = stateNode.node;
843
- const fabricUI = globalThis.nativeFabricUIManager;
844
- if (fabricNode && fabricUI?.measure) {
845
- try {
846
- fabricUI.measure(fabricNode, onMeasured);
847
- } catch (error) {
848
- settle({
849
- rect: null,
850
- stateNode,
851
- color,
852
- count
853
- });
854
- }
855
- return;
856
- }
857
- settle({
858
- rect: null,
859
- stateNode,
860
- color,
861
- count
862
- });
863
- return;
864
- }
865
- try {
866
- publicInstance.measure(onMeasured);
867
- } catch (error) {
868
- settle({
869
- rect: null,
870
- stateNode,
871
- color,
872
- count
873
- });
874
- }
875
- }));
876
- Promise.all(measurePromises).then(results => {
877
- const validResults = results.filter(r => r.rect !== null);
878
- const rects = validResults.map(r => r.rect);
879
-
880
- // Mark measurement phase complete
881
- perfTimer.markMeasurementComplete(validResults.length, results.length - validResults.length);
882
-
883
- // DRAW FIRST. With the render list open, tracking a big burst
884
- // (store writes + cause detection + list row rebuilds) used to run
885
- // BEFORE the highlight callback, so boxes painted ~0.7s later
886
- // (user-timed: 1.3s list-closed vs 2s+ list-open, 2026-07-08).
887
- // Boxes are the product — paint them, then bookkeep.
888
- if (rects.length > 0 && highlightCallback && globalEnabled && !highlightsSuppressed) {
889
- highlightCallback(rects);
890
- }
891
-
892
- // Tracking/list work runs in a macrotask so React commits the boxes
893
- // before this heavier pass starts.
894
- setTimeout(() => {
895
- // Keep a live handle to each measured node so the desktop's "capture this
896
- // component" flow can re-measure (and scroll to) it on demand later.
897
- // Defensive: never let registration break the measurement/tracking batch.
898
- try {
899
- for (const r of validResults) {
900
- if (r.rect) registerTrackedNode(r.rect.id, r.stateNode);
901
- }
902
- } catch (e) {
903
- debugLog("registerTrackedNode batch failed", e);
904
- }
905
-
906
- // Track renders - behavior differs based on what's enabled:
907
- // - globalEnabled (visual tool): full tracking with storage and list updates
908
- // - backgroundTrackingEnabled only (Events tool): emit events only, no storage
909
- const settings = _RenderTracker.RenderTracker.getSettings();
910
- const trackCauses = settings.trackRenderCauses && settings.showRenderCount;
911
- let batchNativeTags = null;
912
- if (trackCauses) {
913
- batchNativeTags = new Set();
914
- for (const {
915
- rect
916
- } of validResults) {
917
- if (rect) {
918
- batchNativeTags.add(rect.id);
919
- }
920
- }
921
- }
922
- if (globalEnabled) {
923
- // Visual tool is on - full tracking with storage and list notifications
924
- _RenderTracker.RenderTracker.startBatch();
925
- for (const {
926
- rect,
927
- stateNode,
928
- color,
929
- count
930
- } of validResults) {
931
- if (rect) {
932
- const componentInfo = extractComponentInfo(stateNode);
933
- let renderCause;
934
- if (trackCauses && batchNativeTags) {
935
- const node = stateNode;
936
- const fiber = getFiberFromNode(node);
937
- renderCause = (0, _RenderCauseDetector.detectRenderCause)(rect.id, fiber, batchNativeTags, settings.debugLogLevel);
938
- }
939
- _RenderTracker.RenderTracker.trackRender({
940
- nativeTag: rect.id,
941
- viewType: componentInfo.viewType,
942
- testID: componentInfo.testID,
943
- nativeID: componentInfo.nativeID,
944
- accessibilityLabel: componentInfo.accessibilityLabel,
945
- componentName: componentInfo.componentName,
946
- measurements: {
947
- x: rect.x,
948
- y: rect.y,
949
- width: rect.width,
950
- height: rect.height
951
- },
952
- color,
953
- count,
954
- renderCause
955
- });
956
- }
957
- }
958
- _RenderTracker.RenderTracker.endBatch();
959
- } else {
960
- // Only background tracking (Events tool) - emit events without storage
961
- for (const {
962
- rect,
963
- stateNode,
964
- color,
965
- count
966
- } of validResults) {
967
- if (rect) {
968
- const componentInfo = extractComponentInfo(stateNode);
969
- let renderCause;
970
- if (trackCauses && batchNativeTags) {
971
- const node = stateNode;
972
- const fiber = getFiberFromNode(node);
973
- renderCause = (0, _RenderCauseDetector.detectRenderCause)(rect.id, fiber, batchNativeTags, settings.debugLogLevel);
974
- }
975
- _RenderTracker.RenderTracker.emitRenderEvent({
976
- nativeTag: rect.id,
977
- viewType: componentInfo.viewType,
978
- testID: componentInfo.testID,
979
- nativeID: componentInfo.nativeID,
980
- accessibilityLabel: componentInfo.accessibilityLabel,
981
- componentName: componentInfo.componentName,
982
- measurements: {
983
- x: rect.x,
984
- y: rect.y,
985
- width: rect.width,
986
- height: rect.height
987
- },
988
- color,
989
- count,
990
- renderCause
991
- });
992
- }
993
- }
994
- }
995
-
996
- // Mark tracking phase complete
997
- perfTimer.markTrackingComplete();
998
-
999
- // Mark callback phase complete and finish timing
1000
- perfTimer.markCallbackComplete();
1001
- perfTimer.finish();
1002
- }, 0);
1003
- }).catch(error => {
1004
- console.error("[HighlightUpdates] Error in measurement pipeline:", error);
1005
- perfTimer.finish(); // Still log timing even on error
1006
- });
1007
- }
1008
-
1009
- /* ================================================================== *
1010
- * Component locator — powers the desktop "screenshot this component"
1011
- * flow. The dashboard sends a query (testID / nativeID / name / nativeTag);
1012
- * we resolve it to a live native node, best-effort scroll it into view,
1013
- * re-measure its on-screen rectangle, and return that (plus the pixel scale
1014
- * and screen size) so the desktop can crop the simulator screenshot to it.
1015
- * ================================================================== */
1016
-
1017
- // nativeTag → live node, weakly held (when WeakRef exists in the runtime) so
1018
- // unmounted views can still be GC'd. WeakRef isn't in this package's TS lib, so
1019
- // reach it through globalThis rather than the (untyped) global.
1020
- const WeakRefCtor = globalThis.WeakRef;
1021
- const nodeRegistry = new Map();
1022
- function registerTrackedNode(nativeTag, stateNode) {
1023
- // WeakRef throws if the target isn't an object, so guard hard — this runs in
1024
- // the measure pipeline and must never break tracking.
1025
- if (nativeTag == null || stateNode == null || typeof stateNode !== "object") {
1026
- return;
1027
- }
1028
- try {
1029
- nodeRegistry.set(nativeTag, WeakRefCtor ? new WeakRefCtor(stateNode) : {
1030
- deref: () => stateNode
1031
- });
1032
- } catch {
1033
- nodeRegistry.set(nativeTag, {
1034
- deref: () => stateNode
1035
- });
1036
- }
1037
- }
1038
- function derefNode(nativeTag) {
1039
- const ref = nodeRegistry.get(nativeTag);
1040
- const node = ref?.deref();
1041
- if (!node) nodeRegistry.delete(nativeTag);
1042
- return node ?? null;
1043
- }
1044
-
1045
- /** Promisified absolute measure → on-screen rect in points (or null). */
1046
- function measureNode(publicInstance) {
1047
- return new Promise(resolve => {
1048
- try {
1049
- publicInstance.measure((_x, _y, width, height, pageX, pageY) => {
1050
- if (pageX == null || pageY == null || width == null || height == null) {
1051
- resolve(null);
1052
- } else {
1053
- resolve({
1054
- x: pageX,
1055
- y: pageY,
1056
- width,
1057
- height
1058
- });
1059
- }
1060
- });
1061
- } catch {
1062
- resolve(null);
1063
- }
1064
- });
1065
- }
1066
- function getFiberFromNode(stateNode) {
1067
- const node = stateNode;
1068
- return node?.canonical?.internalInstanceHandle || node?._internalInstanceHandle || node?._internalFiberInstanceHandleDEV || (0, _utils.getDOMFiber)(node) || null;
1069
- }
1070
-
1071
- /**
1072
- * Walk up the fiber tree from a node to find the nearest ScrollView-like
1073
- * instance (anything exposing scrollTo + an inner content node).
1074
- */
1075
- function findScrollAncestor(stateNode) {
1076
- let fiber = getFiberFromNode(stateNode);
1077
- let guard = 0;
1078
- while (fiber && guard++ < 200) {
1079
- const inst = fiber.stateNode;
1080
- if (inst && typeof inst.scrollTo === "function") {
1081
- // getInnerViewNode()/getScrollableNode() return a numeric node handle —
1082
- // only valid for the legacy UIManager.measureLayout fallback below.
1083
- // Fabric's ref.measureLayout requires an actual host-component ref
1084
- // (getNativeScrollRef()); passing it a number trips a dev warning
1085
- // ("ref.measureLayout must be called with a ref to a native component")
1086
- // and silently resolves neither callback.
1087
- const nativeRef = typeof inst.getNativeScrollRef === "function" && inst.getNativeScrollRef() || null;
1088
- const innerNode = typeof inst.getInnerViewNode === "function" && inst.getInnerViewNode() || typeof inst.getScrollableNode === "function" && inst.getScrollableNode() || inst;
1089
- return {
1090
- instance: inst,
1091
- nativeRef,
1092
- innerNode
1093
- };
1094
- }
1095
- fiber = fiber.return;
1096
- }
1097
- return null;
1098
- }
1099
-
1100
- /**
1101
- * Best-effort scroll of an ancestor ScrollView so the target sits near the top
1102
- * of its viewport. Returns true only if a scroll was actually issued. Never
1103
- * throws — if anything is missing (no ScrollView, measureLayout unsupported)
1104
- * it quietly returns false and the caller measures wherever the node is.
1105
- */
1106
- async function scrollDescendantIntoView(stateNode, publicInstance, margin) {
1107
- try {
1108
- const ancestor = findScrollAncestor(stateNode);
1109
- if (!ancestor) return false;
1110
-
1111
- // Position of the target WITHIN the scroll content — independent of the
1112
- // current scroll offset (the standard RN keyboard-aware-scroll technique).
1113
- // Try the Fabric path (ref.measureLayout against the inner view instance)
1114
- // first, then fall back to the legacy UIManager.measureLayout with tags.
1115
- const inst = publicInstance;
1116
- let top = null;
1117
-
1118
- // On some architectures measureLayout invokes NEITHER callback, which would
1119
- // hang the await forever — always race it against a short timeout.
1120
- const withTimeout = run => new Promise(resolve => {
1121
- let done = false;
1122
- const settle = v => {
1123
- if (done) return;
1124
- done = true;
1125
- resolve(v);
1126
- };
1127
- const timer = setTimeout(() => settle(null), 600);
1128
- try {
1129
- run(v => {
1130
- clearTimeout(timer);
1131
- settle(v);
1132
- });
1133
- } catch {
1134
- clearTimeout(timer);
1135
- settle(null);
1136
- }
1137
- });
1138
- if (ancestor.nativeRef && typeof inst.measureLayout === "function") {
1139
- top = await withTimeout(resolve => inst.measureLayout(ancestor.nativeRef, (_left, t) => resolve(t), () => resolve(null)));
1140
- }
1141
- if (top == null) {
1142
- const targetHandle = (0, _reactNative.findNodeHandle)(publicInstance);
1143
- const innerHandle = typeof ancestor.innerNode === "number" ? ancestor.innerNode : (0, _reactNative.findNodeHandle)(ancestor.innerNode);
1144
- if (targetHandle != null && innerHandle != null) {
1145
- top = await withTimeout(resolve => _reactNative.UIManager.measureLayout(targetHandle, innerHandle, () => resolve(null), (_left, t) => resolve(t)));
1146
- }
1147
- }
1148
- if (top == null) return false;
1149
- ancestor.instance.scrollTo({
1150
- y: Math.max(0, top - margin),
1151
- animated: false
1152
- });
1153
- // Let the scroll + layout settle before the caller re-measures.
1154
- await new Promise(r => setTimeout(r, 80));
1155
- return true;
1156
- } catch {
1157
- return false;
1158
- }
1159
- }
1160
- function matchScore(r, q) {
1161
- const fields = [r.testID, r.nativeID, r.accessibilityLabel, r.componentName, r.viewType, String(r.nativeTag)];
1162
- let best = 0;
1163
- for (const f of fields) {
1164
- if (!f) continue;
1165
- const lower = f.toLowerCase();
1166
- if (lower === q) return 3; // exact wins outright
1167
- if (lower.startsWith(q)) best = Math.max(best, 2);else if (lower.includes(q)) best = Math.max(best, 1);
1168
- }
1169
- return best;
1170
- }
1171
-
1172
- /**
1173
- * Resolve a component query to a fresh on-screen rectangle. See
1174
- * LocateComponentResult for the shape. Always resolves (never rejects).
1175
- */
1176
- async function locateComponent(params) {
1177
- const scrollIntoView = params.scrollIntoView !== false;
1178
- const margin = typeof params.margin === "number" ? params.margin : 12;
1179
- const renders = _RenderTracker.RenderTracker.getRenders();
1180
- const locateFromScreen = async () => {
1181
- const described = await (0, _screenElements.describeScreen)();
1182
- const q = (params.query ?? "").trim().toLowerCase();
1183
- let best;
1184
- for (const el of described.elements) {
1185
- if (params.nativeTag != null && el.nativeTag === params.nativeTag) {
1186
- best = {
1187
- el,
1188
- score: 1000
1189
- };
1190
- break;
1191
- }
1192
- const score = (0, _tapMatch.scoreTapMatch)({
1193
- nativeTag: el.nativeTag,
1194
- testID: el.testID,
1195
- label: el.label,
1196
- text: el.text,
1197
- name: el.name,
1198
- tap: el.tap
1199
- }, {
1200
- query: q || undefined,
1201
- nativeTag: params.nativeTag
1202
- });
1203
- if (score <= 0) continue;
1204
- if (!best || score > best.score) best = {
1205
- el,
1206
- score
1207
- };
1208
- }
1209
- if (!best?.el.frame) {
1210
- return {
1211
- matched: false,
1212
- reason: "no-match",
1213
- candidates: []
1214
- };
1215
- }
1216
- const win = described.screen;
1217
- const f = best.el.frame;
1218
- return {
1219
- matched: true,
1220
- live: true,
1221
- rect: {
1222
- x: f.x * win.width,
1223
- y: f.y * win.height,
1224
- width: f.width * win.width,
1225
- height: f.height * win.height
1226
- },
1227
- scale: _reactNative.PixelRatio.get(),
1228
- screen: {
1229
- width: win.width,
1230
- height: win.height
1231
- },
1232
- nativeTag: best.el.nativeTag ?? undefined,
1233
- componentName: best.el.name,
1234
- testID: best.el.testID
1235
- };
1236
- };
1237
- if (renders.length === 0) return locateFromScreen();
1238
-
1239
- // Rank candidates (skip ranking when an explicit nativeTag was given).
1240
- const q = (params.query ?? "").trim().toLowerCase();
1241
- const ranked = q ? renders.map(r => ({
1242
- r,
1243
- score: matchScore(r, q)
1244
- })).filter(x => x.score > 0).sort((a, b) => b.score - a.score || b.r.lastRenderTime - a.r.lastRenderTime) : [];
1245
- const candidates = ranked.slice(0, 8).map(({
1246
- r
1247
- }) => ({
1248
- nativeTag: r.nativeTag,
1249
- componentName: r.componentName,
1250
- testID: r.testID,
1251
- nativeID: r.nativeID,
1252
- viewType: r.viewType,
1253
- renderCount: r.renderCount
1254
- }));
1255
- const targetTag = params.nativeTag ?? (ranked.length > 0 ? ranked[0].r.nativeTag : undefined);
1256
- if (targetTag == null) {
1257
- const located = await locateFromScreen();
1258
- return located.matched ? located : {
1259
- matched: false,
1260
- reason: "no-match",
1261
- candidates
1262
- };
1263
- }
1264
- const tracked = renders.find(r => r.nativeTag === targetTag);
1265
-
1266
- // Try a live re-measure (and scroll) using the registered node.
1267
- let rect = null;
1268
- let live = false;
1269
- let scrolled = false;
1270
- const node = derefNode(targetTag);
1271
- if (node) {
1272
- const publicInstance = getPublicInstance(node);
1273
- if (publicInstance) {
1274
- if (scrollIntoView) {
1275
- scrolled = await scrollDescendantIntoView(node, publicInstance, margin);
1276
- }
1277
- rect = await measureNode(publicInstance);
1278
- live = !!rect;
1279
- }
1280
- }
1281
-
1282
- // Fall back to the last synced measurement if the live path failed.
1283
- if (!rect && tracked?.measurements) {
1284
- rect = {
1285
- ...tracked.measurements
1286
- };
1287
- }
1288
- if (!rect) {
1289
- return {
1290
- matched: false,
1291
- reason: "no-measurement",
1292
- nativeTag: targetTag,
1293
- candidates
1294
- };
1295
- }
1296
- const scale = _reactNative.PixelRatio.get();
1297
- const win = _reactNative.Dimensions.get("window");
1298
- const inView = rect.x >= -1 && rect.y >= -1 && rect.x + rect.width <= win.width + 1 && rect.y + rect.height <= win.height + 1;
1299
- return {
1300
- matched: true,
1301
- live,
1302
- scrolled,
1303
- inView,
1304
- rect,
1305
- scale,
1306
- screen: {
1307
- width: win.width,
1308
- height: win.height
1309
- },
1310
- nativeTag: targetTag,
1311
- componentName: tracked?.componentName,
1312
- testID: tracked?.testID,
1313
- candidates
1314
- };
1315
- }
1316
-
1317
- /**
1318
- * Enable or disable trace updates on all renderer interfaces.
1319
- * This is the key function that makes standalone tracing work!
1320
- *
1321
- * The rendererInterfaces map contains the renderer backend for each React root.
1322
- * Each renderer has a setTraceUpdatesEnabled function that controls whether
1323
- * it emits 'traceUpdates' events when components re-render.
1324
- *
1325
- * See: react-devtools-core/dist/backend.js line 15487-15489
1326
- */
1327
- function setTraceUpdatesOnRenderers(enabled) {
1328
- if (!hook?.rendererInterfaces) {
1329
- debugLog("No rendererInterfaces available");
1330
- return;
1331
- }
1332
- let count = 0;
1333
- hook.rendererInterfaces.forEach((rendererInterface, rendererId) => {
1334
- if (typeof rendererInterface.setTraceUpdatesEnabled === "function") {
1335
- try {
1336
- rendererInterface.setTraceUpdatesEnabled(enabled);
1337
- count++;
1338
- debugLog(`Renderer ${rendererId}: setTraceUpdatesEnabled(${enabled})`);
1339
- } catch (error) {
1340
- debugLog(`Renderer ${rendererId}: error setting trace updates`, error);
1341
- }
1342
- } else {
1343
- debugLog(`Renderer ${rendererId}: no setTraceUpdatesEnabled method`);
1344
- }
1345
- });
1346
- debugLog(`Set trace updates ${enabled ? "enabled" : "disabled"} on ${count} renderer(s)`);
1347
-
1348
- // Also try the agent if available (in case DevTools is connected)
1349
- if (hook.reactDevtoolsAgent?.setTraceUpdatesEnabled) {
1350
- try {
1351
- hook.reactDevtoolsAgent.setTraceUpdatesEnabled(enabled);
1352
- debugLog("Also set trace updates on agent");
1353
- } catch (error) {
1354
- debugLog("Error setting trace updates on agent", error);
1355
- }
1356
- }
1357
- }
1358
-
1359
- /**
1360
- * Subscribe to the traceUpdates event from the hook
1361
- */
1362
- function subscribeToTraceUpdates() {
1363
- if (!hook) return;
1364
- if (traceUpdatesUnsubscribe) return; // Already subscribed
1365
-
1366
- debugLog("Subscribing to traceUpdates event");
1367
-
1368
- // The hook uses EventEmitter pattern with 'sub' method
1369
- // See: react-devtools-core/dist/backend.js line 17526
1370
- // hook.sub('traceUpdates', agent.onTraceUpdates)
1371
- if (typeof hook.sub === "function") {
1372
- traceUpdatesUnsubscribe = hook.sub("traceUpdates", handleTraceUpdates);
1373
- debugLog("Subscribed using hook.sub()");
1374
- } else {
1375
- debugLog("hook.sub not available, traceUpdates may not work");
1376
- }
1377
-
1378
- // CRITICAL: Enable trace updates on all renderers
1379
- // Without this, the renderers won't emit traceUpdates events
1380
- setTraceUpdatesOnRenderers(true);
1381
- }
1382
-
1383
- /**
1384
- * Unsubscribe from the traceUpdates event
1385
- */
1386
- function unsubscribeFromTraceUpdates() {
1387
- // Disable trace updates on renderers first
1388
- setTraceUpdatesOnRenderers(false);
1389
- if (traceUpdatesUnsubscribe) {
1390
- traceUpdatesUnsubscribe();
1391
- traceUpdatesUnsubscribe = null;
1392
- debugLog("Unsubscribed from traceUpdates event");
1393
- }
1394
- }
1395
-
1396
- /**
1397
- * Set the callback for rendering highlights
1398
- */
1399
- function setHighlightCallback(callback) {
1400
- highlightCallback = callback;
1401
- debugLog(`Highlight callback ${callback ? "set" : "cleared"}`);
1402
- }
1403
-
1404
- /**
1405
- * Set the callback for when a badge is pressed.
1406
- * This is used to navigate to the detail view when a user taps a render count badge.
1407
- */
1408
- function setBadgePressCallback(callback) {
1409
- badgePressCallback = callback;
1410
- debugLog(`Badge press callback ${callback ? "set" : "cleared"}`);
1411
- }
1412
-
1413
- /**
1414
- * Get the current badge press callback.
1415
- * Used by the overlay to check if badge presses should be handled.
1416
- */
1417
- function getBadgePressCallback() {
1418
- return badgePressCallback;
1419
- }
1420
-
1421
- /**
1422
- * Handle a badge press event.
1423
- * Calls the registered callback with the nativeTag of the component.
1424
- */
1425
- function handleBadgePress(nativeTag) {
1426
- if (badgePressCallback) {
1427
- badgePressCallback(nativeTag);
1428
- }
1429
- }
1430
-
1431
- // Spotlight highlight state - for showing which component is being viewed in detail
1432
-
1433
- let spotlightCallback = null;
1434
- let currentSpotlightTag = null;
1435
-
1436
- /**
1437
- * Set the callback for spotlight highlighting.
1438
- * Used by the overlay to show a special highlight for the component being viewed.
1439
- */
1440
- function setSpotlightCallback(callback) {
1441
- spotlightCallback = callback;
1442
- // If there's an existing spotlight, notify the new callback
1443
- if (callback && currentSpotlightTag !== null) {
1444
- callback(currentSpotlightTag);
1445
- }
1446
- }
1447
-
1448
- /**
1449
- * Set a spotlight highlight on a specific component.
1450
- * This is used when viewing a component's detail to show which one it is.
1451
- */
1452
- function setSpotlight(nativeTag) {
1453
- if (remoteHandler) {
1454
- remoteHandler("setSpotlight", {
1455
- nativeTag
1456
- });
1457
- return;
1458
- }
1459
- currentSpotlightTag = nativeTag;
1460
- if (spotlightCallback) {
1461
- spotlightCallback(nativeTag);
1462
- }
1463
- }
1464
-
1465
- // ============================================================================
1466
- // REMOTE MIRROR MODE
1467
- // ============================================================================
1468
-
1469
- let remoteHandler = null;
1470
-
1471
- /**
1472
- * Remote mirror mode (desktop dashboard): while a handler is set, mutations
1473
- * (toggle/setEnabled/toggleFreeze/clearRenderCounts/setSpotlight) forward
1474
- * (method, params) to the synced device instead of touching the local React
1475
- * internals — the resulting state comes back via replaceRemoteState(). Pass
1476
- * null to restore local behavior.
1477
- */
1478
- function setRemoteHandler(handler) {
1479
- remoteHandler = handler;
1480
- }
1481
-
1482
- /**
1483
- * Mirror the synced device's enabled/frozen flags into the local listeners
1484
- * (so the modal's toggle buttons reflect the device). Never patches React
1485
- * internals — it only updates the flags the UI reads.
1486
- */
1487
- function replaceRemoteState(state) {
1488
- if (globalEnabled !== state.enabled) {
1489
- globalEnabled = state.enabled;
1490
- notifyStateListeners();
1491
- }
1492
- if (isFrozen !== state.frozen) {
1493
- isFrozen = state.frozen;
1494
- notifyFreezeListeners();
1495
- }
1496
- }
1497
-
1498
- /**
1499
- * Get the current spotlight nativeTag.
1500
- */
1501
- function getSpotlight() {
1502
- return currentSpotlightTag;
1503
- }
1504
- function notifyStateListeners() {
1505
- stateListeners.forEach(listener => {
1506
- try {
1507
- listener(globalEnabled);
1508
- } catch (error) {
1509
- console.error("[HighlightUpdates] Error in state listener:", error);
1510
- }
1511
- });
1512
- }
1513
- function subscribe(listener) {
1514
- stateListeners.add(listener);
1515
- listener(globalEnabled);
1516
- return () => {
1517
- stateListeners.delete(listener);
1518
- };
1519
- }
1520
- function initialize() {
1521
- if (typeof __DEV__ !== "undefined" && !__DEV__) {
1522
- debugLog("Only available in development builds");
1523
- return false;
1524
- }
1525
-
1526
- // Remote mirror mode: initialization (DevTools hook, fiber walking)
1527
- // belongs to the device — pretend success so callers proceed to the
1528
- // forwarded toggle.
1529
- if (remoteHandler) {
1530
- return true;
1531
- }
1532
- if (initialized) {
1533
- return true;
1534
- }
1535
- hook = window?.__REACT_DEVTOOLS_GLOBAL_HOOK__ || null;
1536
- if (!hook) {
1537
- debugLog("React DevTools hook not found");
1538
- return false;
1539
- }
1540
- debugLog("Hook found");
1541
- debugLog(`Hook has sub: ${typeof hook.sub === "function"}`);
1542
- debugLog(`Hook has on: ${typeof hook.on === "function"}`);
1543
- debugLog(`Hook has emit: ${typeof hook.emit === "function"}`);
1544
-
1545
- // Install profiler interceptor to capture what DevTools detects
1546
- // This allows us to compare profiler data with our detection
1547
- (0, _ProfilerInterceptor.installProfilerInterceptor)();
1548
-
1549
- // Comparison callback available for future use when we re-enable our implementation
1550
- // setComparisonCallback((profilerNodes: Set<unknown>) => {
1551
- // debugLog(`[COMPARISON] Profiler detected ${profilerNodes.size} nodes`);
1552
- // });
1553
-
1554
- if (hook.rendererInterfaces && hook.rendererInterfaces.size > 0) {
1555
- initialized = true;
1556
- debugLog(`Initialized with ${hook.rendererInterfaces.size} renderer(s)`);
1557
- exposeGlobally();
1558
- return true;
1559
- }
1560
-
1561
- // Wait for renderer to be available
1562
- const checkInterval = setInterval(() => {
1563
- if (hook?.rendererInterfaces && hook.rendererInterfaces.size > 0) {
1564
- clearInterval(checkInterval);
1565
- initialized = true;
1566
- debugLog(`Initialized with ${hook.rendererInterfaces.size} renderer(s) (delayed)`);
1567
- exposeGlobally();
1568
- }
1569
- }, 100);
1570
- setTimeout(() => clearInterval(checkInterval), 10000);
1571
- return false;
1572
- }
1573
- function exposeGlobally() {
1574
- if (typeof window !== "undefined") {
1575
- window.__HIGHLIGHT_UPDATES_CONTROLLER__ = {
1576
- enable,
1577
- disable,
1578
- toggle,
1579
- isEnabled,
1580
- setEnabled,
1581
- subscribe,
1582
- initialize,
1583
- destroy,
1584
- isInitialized: () => initialized,
1585
- setHighlightCallback
1586
- };
1587
- }
1588
- }
1589
- function enable() {
1590
- if (typeof __DEV__ !== "undefined" && !__DEV__) return;
1591
- if (!initialized) {
1592
- initialize();
1593
- }
1594
-
1595
- // Enabling the VISUAL tool always means boxes should show — clear any
1596
- // suppression left over from a silent (screenshot-tool) session.
1597
- if (highlightsSuppressed) {
1598
- highlightsSuppressed = false;
1599
- }
1600
- if (globalEnabled) {
1601
- return;
1602
- }
1603
- debugLog("Enabling highlights");
1604
-
1605
- // Only subscribe if not already subscribed via background tracking
1606
- if (!backgroundTrackingEnabled) {
1607
- subscribeToTraceUpdates();
1608
- }
1609
-
1610
- // Start tracking renders
1611
- _RenderTracker.RenderTracker.start();
1612
-
1613
- // Note: profiler logging disabled for normal operation
1614
- // enableProfilerLogging();
1615
-
1616
- globalEnabled = true;
1617
- notifyStateListeners();
1618
- (0, _sharedUi.notifySubscriberCountChange)('render');
1619
- }
1620
- function disable() {
1621
- if (typeof __DEV__ !== "undefined" && !__DEV__) return;
1622
- if (!globalEnabled) {
1623
- return;
1624
- }
1625
- debugLog("Disabling highlights");
1626
-
1627
- // Clear any pending locks/state
1628
- renderingLock = false;
1629
- isProcessing = false;
1630
- if (renderingLockTimeout) {
1631
- clearTimeout(renderingLockTimeout);
1632
- renderingLockTimeout = null;
1633
- }
1634
- renderedOverlayTags.clear();
1635
- devToolsNodeCache.clear(); // Clear detection cache
1636
- (0, _RenderCauseDetector.clearRenderCauseState)(); // Clear render cause state to prevent memory leaks
1637
-
1638
- // Freeze is a view state on a LIVE overlay, and the overlay is going away:
1639
- // the boxes it was holding open were just cleared with the tags above. Left
1640
- // latched it outlived the thing it froze — switch tracking off and back on
1641
- // and the button still read "Unfreeze frame" on a tool with nothing frozen,
1642
- // while new highlights (correctly) refused to fade.
1643
- if (isFrozen) {
1644
- isFrozen = false;
1645
- notifyFreezeListeners();
1646
- }
1647
-
1648
- // Only unsubscribe if background tracking doesn't need it
1649
- if (!backgroundTrackingEnabled) {
1650
- unsubscribeFromTraceUpdates();
1651
- }
1652
-
1653
- // Stop tracking renders
1654
- _RenderTracker.RenderTracker.stop();
1655
-
1656
- // Note: profiler logging disabled for normal operation
1657
- // disableProfilerLogging();
1658
-
1659
- globalEnabled = false;
1660
- notifyStateListeners();
1661
- (0, _sharedUi.notifySubscriberCountChange)('render');
1662
- }
1663
-
1664
- /**
1665
- * Enable background render tracking (for Events tool).
1666
- * This enables render interception WITHOUT affecting the visual toggle state.
1667
- * The visual highlight toggle remains independent.
1668
- */
1669
- function enableBackgroundTracking() {
1670
- if (typeof __DEV__ !== "undefined" && !__DEV__) return;
1671
- if (backgroundTrackingEnabled) {
1672
- return;
1673
- }
1674
- if (!initialized) {
1675
- initialize();
1676
- }
1677
- debugLog("Enabling background tracking");
1678
-
1679
- // Only subscribe if not already subscribed via main toggle
1680
- if (!globalEnabled) {
1681
- subscribeToTraceUpdates();
1682
- }
1683
- backgroundTrackingEnabled = true;
1684
- }
1685
-
1686
- /**
1687
- * Disable background render tracking (for Events tool).
1688
- * Only actually unsubscribes if the main toggle is also off.
1689
- */
1690
- function disableBackgroundTracking() {
1691
- if (typeof __DEV__ !== "undefined" && !__DEV__) return;
1692
- if (!backgroundTrackingEnabled) {
1693
- return;
1694
- }
1695
- debugLog("Disabling background tracking");
1696
- backgroundTrackingEnabled = false;
1697
-
1698
- // Only unsubscribe if main toggle is also off
1699
- if (!globalEnabled) {
1700
- unsubscribeFromTraceUpdates();
1701
- }
1702
- }
1703
-
1704
- /**
1705
- * Check if background tracking is enabled (for debugging)
1706
- */
1707
- function isBackgroundTrackingEnabled() {
1708
- return backgroundTrackingEnabled;
1709
- }
1710
- function toggle() {
1711
- if (typeof __DEV__ !== "undefined" && !__DEV__) return;
1712
- if (remoteHandler) {
1713
- remoteHandler("toggle");
1714
- return;
1715
- }
1716
- if (globalEnabled) {
1717
- disable();
1718
- } else {
1719
- enable();
1720
- }
1721
- }
1722
-
1723
- /**
1724
- * Clear all render counts and tracked data
1725
- */
1726
- function clearRenderCounts() {
1727
- if (remoteHandler) {
1728
- remoteHandler("clearRenderCounts");
1729
- return;
1730
- }
1731
- nodeRenderCounts.clear();
1732
- _RenderTracker.RenderTracker.clear();
1733
- debugLog("Cleared render counts");
1734
- }
1735
- function isEnabled() {
1736
- return globalEnabled;
1737
- }
1738
- function setEnabled(enabled) {
1739
- if (remoteHandler) {
1740
- remoteHandler("setEnabled", {
1741
- enabled
1742
- });
1743
- return;
1744
- }
1745
- if (enabled) enable();else disable();
1746
- }
1747
-
1748
- /**
1749
- * Suppress (or restore) the visual highlight boxes without affecting tracking.
1750
- * When suppressing, any boxes currently on screen are cleared immediately.
1751
- */
1752
- function setHighlightsSuppressed(suppressed) {
1753
- if (highlightsSuppressed === suppressed) return;
1754
- highlightsSuppressed = suppressed;
1755
- if (suppressed && highlightCallback) {
1756
- // Wipe whatever is currently drawn so nothing lingers.
1757
- highlightCallback([]);
1758
- }
1759
- }
1760
- function getHighlightsSuppressed() {
1761
- return highlightsSuppressed;
1762
- }
1763
-
1764
- /**
1765
- * Silent tracking — used by the desktop "screenshot this component" flow.
1766
- * Turns the render tracker on (so the render list + measurements populate and
1767
- * sync) while keeping the visual highlight boxes hidden. Disabling restores
1768
- * normal state. On the desktop mirror this forwards to the device.
1769
- */
1770
- function setSilentTracking(enabled) {
1771
- if (remoteHandler) {
1772
- remoteHandler("setSilentTracking", {
1773
- enabled
1774
- });
1775
- return;
1776
- }
1777
- if (enabled) {
1778
- enable(); // clears suppression as part of a visual enable...
1779
- setHighlightsSuppressed(true); // ...so re-suppress right after.
1780
- } else {
1781
- setHighlightsSuppressed(false);
1782
- disable();
1783
- }
1784
- }
1785
- function isInitialized() {
1786
- return initialized;
1787
- }
1788
-
1789
- // ============================================================================
1790
- // FREEZE FRAME MODE
1791
- // ============================================================================
1792
-
1793
- /**
1794
- * Notify all freeze state listeners of a change
1795
- */
1796
- function notifyFreezeListeners() {
1797
- freezeListeners.forEach(listener => {
1798
- try {
1799
- listener(isFrozen);
1800
- } catch (error) {
1801
- console.error("[HighlightUpdates] Error in freeze listener:", error);
1802
- }
1803
- });
1804
- }
1805
-
1806
- /**
1807
- * Subscribe to freeze state changes
1808
- */
1809
- function subscribeToFreeze(listener) {
1810
- freezeListeners.add(listener);
1811
- listener(isFrozen);
1812
- return () => {
1813
- freezeListeners.delete(listener);
1814
- };
1815
- }
1816
-
1817
- /**
1818
- * Freeze highlights - prevents highlights from fading away.
1819
- * New renders are still captured but old highlights don't clear.
1820
- */
1821
- function freeze() {
1822
- if (typeof __DEV__ !== "undefined" && !__DEV__) return;
1823
- if (isFrozen) return;
1824
- isFrozen = true;
1825
- debugLog("Freeze mode enabled");
1826
- notifyFreezeListeners();
1827
- }
1828
-
1829
- /**
1830
- * Unfreeze highlights - resumes normal cleanup behavior.
1831
- * Clears all current highlights when unfreezing.
1832
- */
1833
- function unfreeze() {
1834
- if (typeof __DEV__ !== "undefined" && !__DEV__) return;
1835
- if (!isFrozen) return;
1836
- isFrozen = false;
1837
- debugLog("Freeze mode disabled");
1838
-
1839
- // Clear all current highlights when unfreezing
1840
- if (highlightCallback) {
1841
- highlightCallback([]);
1842
- }
1843
- notifyFreezeListeners();
1844
- }
1845
-
1846
- /**
1847
- * Toggle freeze state
1848
- */
1849
- function toggleFreeze() {
1850
- if (remoteHandler) {
1851
- remoteHandler("toggleFreeze");
1852
- return;
1853
- }
1854
- if (isFrozen) {
1855
- unfreeze();
1856
- } else {
1857
- freeze();
1858
- }
1859
- }
1860
-
1861
- /**
1862
- * Check if freeze mode is active
1863
- */
1864
- function getFrozen() {
1865
- return isFrozen;
1866
- }
1867
- function destroy() {
1868
- if (!initialized) return;
1869
- unsubscribeFromTraceUpdates();
1870
-
1871
- // Uninstall profiler interceptor
1872
- (0, _ProfilerInterceptor.uninstallProfilerInterceptor)();
1873
- (0, _ProfilerInterceptor.setComparisonCallback)(null);
1874
-
1875
- // Reset freeze state
1876
- isFrozen = false;
1877
- freezeListeners.clear();
1878
- globalEnabled = false;
1879
- hook = null;
1880
- highlightCallback = null;
1881
- initialized = false;
1882
- if (typeof window !== "undefined") {
1883
- delete window.__HIGHLIGHT_UPDATES_CONTROLLER__;
1884
- }
1885
- debugLog("Destroyed");
1886
- }
1887
- const HighlightUpdatesController = {
1888
- subscribe,
1889
- enable,
1890
- disable,
1891
- toggle,
1892
- isEnabled,
1893
- setEnabled,
1894
- initialize,
1895
- destroy,
1896
- isInitialized,
1897
- setHighlightCallback,
1898
- clearRenderCounts,
1899
- // Freeze frame mode
1900
- freeze,
1901
- unfreeze,
1902
- toggleFreeze,
1903
- getFrozen,
1904
- subscribeToFreeze,
1905
- // Badge press callback for "Click Overlay Badge → Jump to Detail"
1906
- setBadgePressCallback,
1907
- getBadgePressCallback,
1908
- handleBadgePress,
1909
- // Spotlight highlight for detail view
1910
- setSpotlightCallback,
1911
- setSpotlight,
1912
- getSpotlight,
1913
- // Cache management for excludeDevTools toggle
1914
- clearDevToolsCache,
1915
- // Background tracking for Events tool (separate from visual toggle)
1916
- enableBackgroundTracking,
1917
- disableBackgroundTracking,
1918
- isBackgroundTrackingEnabled,
1919
- // Remote mirror mode (desktop dashboard)
1920
- setRemoteHandler,
1921
- replaceRemoteState,
1922
- // Component locator (desktop "screenshot this component" flow)
1923
- locateComponent,
1924
- // Silent tracking (track + measure without drawing boxes)
1925
- setHighlightsSuppressed,
1926
- getHighlightsSuppressed,
1927
- setSilentTracking
1928
- };
1929
- var _default = exports.default = HighlightUpdatesController;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;var _utils=require("@buoy-gg/shared-ui/utils");var _ProfilerInterceptor=require("./ProfilerInterceptor");var _reactNative=require("react-native");var _RenderTracker=require("./RenderTracker");var _screenElements=require("./screenElements");var _tapMatch=require("./tapMatch");var _PerformanceLogger=require("./PerformanceLogger");var _RenderCauseDetector=require("./RenderCauseDetector");var _sharedUi=require("@buoy-gg/shared-ui");var _devtoolsComponentNames=require("./devtoolsComponentNames");"use strict";let globalEnabled=false;let highlightsSuppressed=false;let backgroundTrackingEnabled=false;let initialized=false;let hook=null;let highlightCallback=null;let badgePressCallback=null;let traceUpdatesUnsubscribe=null;let isProcessing=false;let isFrozen=false;const freezeListeners=new Set;const stateListeners=new Set;const nodeRenderCounts=new Map;const COLORS=["#37afa9","#63b19e","#80b393","#97b488","#abb67d","#beb771","#cfb965","#dfba57","#efbb49","#febc38"];const DEBUG=false;function debugLog(e,n){if(!DEBUG)return;if(n!==void 0){console.log(`[HighlightUpdates] ${e}`,n)}else{console.log(`[HighlightUpdates] ${e}`)}}function getPublicInstance(e){const n=(0,_utils.getDOMPublicInstance)(e);if(n)return n;if(!e)return null;const i=e;if(i.canonical?.publicInstance){return i.canonical.publicInstance}if(i.canonical&&typeof i.canonical.measure==="function"){return i.canonical}if(typeof i.measure==="function"){return i}return null}function getNativeTag(e){const n=(0,_utils.getDOMNodeHandle)(e);if(n!==null)return n;if(e==null)return null;const i=e;if(i.__nativeTag!=null)return i.__nativeTag;if(i._nativeTag!=null)return i._nativeTag;if(i.nativeTag!=null)return i.nativeTag;if(i.canonical){if(i.canonical.__nativeTag!=null)return i.canonical.__nativeTag;if(i.canonical._nativeTag!=null)return i.canonical._nativeTag;if(i.canonical.nativeTag!=null)return i.canonical.nativeTag}return null}function extractComponentInfo(e){const n=e;const i={viewType:"Unknown"};if(n?.canonical?.viewConfig?.uiViewClassName){i.viewType=n.canonical.viewConfig.uiViewClassName}else if(n?.viewConfig?.uiViewClassName){i.viewType=n.viewConfig.uiViewClassName}const o=n?.canonical?.internalInstanceHandle||n?._internalInstanceHandle||n?._internalFiberInstanceHandleDEV||(0,_utils.getDOMFiber)(n);const r=n?.canonical?.currentProps;const a=n?.canonical?.pendingProps;const t=o?.pendingProps;const u=o?.memoizedProps;i.testID=r?.testID||a?.testID||t?.testID||u?.testID||void 0;i.nativeID=r?.nativeID||a?.nativeID||t?.nativeID||u?.nativeID||void 0;i.accessibilityLabel=r?.accessibilityLabel||a?.accessibilityLabel||t?.accessibilityLabel||u?.accessibilityLabel||void 0;i.componentName=getOwningComponentName(o)||void 0;return i}function describeNodeForLog(e){const n=e;const i=getPublicInstance(e);const o=getNativeTag(e)||getNativeTag(i);const r={nativeTag:o??"unknown",type:n?.canonical?"Fabric":"Legacy",hasMeasure:typeof i?.measure==="function"};if(n){r.stateNodeKeys=Object.keys(n).slice(0,20);if(n.node){r.nodeKeys=Object.keys(n.node).slice(0,20)}if(n.viewConfig){r.viewConfig={uiViewClassName:n.viewConfig.uiViewClassName,validAttributes:n.viewConfig.validAttributes?Object.keys(n.viewConfig.validAttributes).slice(0,10):void 0}}if(n.canonical){const a=n.canonical;r.canonicalKeys=Object.keys(a).slice(0,20);if(a.viewConfig){r.canonicalViewConfig={uiViewClassName:a.viewConfig.uiViewClassName}}if(a.nativeTag!=null){r.canonicalNativeTag=a.nativeTag}if(a.internalInstanceHandle){const t=a.internalInstanceHandle;r.fiberKeys=Object.keys(t).slice(0,30);if(t.type){r.fiberType=typeof t.type==="function"?t.type.name||t.type.displayName||"function":t.type}if(t.elementType){r.fiberElementType=typeof t.elementType==="function"?t.elementType.name||t.elementType.displayName||"function":t.elementType}if(t._debugOwner){const u=t._debugOwner;r.fiberDebugOwner=u.type?.name||u.type?.displayName||u.elementType?.name||"unknown"}if(t._debugSource){r.fiberDebugSource=t._debugSource}if(t.tag!=null){r.fiberTag=t.tag}if(t.pendingProps){if(t.pendingProps.nativeID)r.fiberPendingNativeID=t.pendingProps.nativeID;if(t.pendingProps.testID)r.fiberPendingTestID=t.pendingProps.testID}if(t.memoizedProps){if(t.memoizedProps.nativeID)r.fiberMemoizedNativeID=t.memoizedProps.nativeID;if(t.memoizedProps.testID)r.fiberMemoizedTestID=t.memoizedProps.testID}}if(a.publicInstance){const t=a.publicInstance;r.publicInstanceKeys=Object.keys(t).slice(0,20);if(t.nativeID!=null)r.nativeID=t.nativeID;if(t._nativeID!=null)r._nativeID=t._nativeID;if(t.props){r.publicInstanceProps=Object.keys(t.props).slice(0,15);if(t.props.nativeID)r.propsNativeID=t.props.nativeID;if(t.props.testID)r.propsTestID=t.props.testID;if(t.props.accessibilityLabel)r.accessibilityLabel=t.props.accessibilityLabel}}if(a.currentProps){r.currentPropsKeys=Object.keys(a.currentProps).slice(0,15);if(a.currentProps.nativeID!=null)r.currentPropsNativeID=a.currentProps.nativeID;if(a.currentProps.testID!=null)r.currentPropsTestID=a.currentProps.testID;if(a.currentProps.accessibilityLabel!=null)r.currentPropsAccessLabel=a.currentProps.accessibilityLabel}if(a.pendingProps){r.pendingPropsKeys=Object.keys(a.pendingProps).slice(0,15);if(a.pendingProps.nativeID!=null)r.pendingPropsNativeID=a.pendingProps.nativeID;if(a.pendingProps.testID!=null)r.pendingPropsTestID=a.pendingProps.testID}}if(n._debugOwner){const a=n._debugOwner;r.debugOwnerType=a.type?.name||a.type?.displayName||typeof a.type}if(n._debugSource){r.debugSource=n._debugSource}}return r}const DEBUG_LOGGING=false;let renderingLock=false;let renderingLockTimeout=null;const RENDER_LOCK_DURATION=350;const renderedOverlayTags=new Set;function isOurOverlayTag(e){if(e==null)return false;return renderedOverlayTags.has(e)}const HIGHLIGHT_OVERLAY_NATIVE_IDS=new Set(["highlight-updates-overlay","__rn_buoy__highlight-modal"]);const OTHER_DEV_TOOLS_NATIVE_IDS=new Set(["jsmodal-root","image-overlay-standalone","logbox_inspector","logbox"]);const devToolsNodeCache=new Map;const CACHE_MAX_SIZE=500;function clearDevToolsCache(){devToolsNodeCache.clear()}function isHighlightOverlayNativeID(e){if(!e)return false;if(HIGHLIGHT_OVERLAY_NATIVE_IDS.has(e))return true;if(e.startsWith("__highlight_"))return true;return false}function isOtherDevToolsNativeID(e){if(!e)return false;if(OTHER_DEV_TOOLS_NATIVE_IDS.has(e))return true;const n=e.charCodeAt(0);if(n===95){if(e.startsWith("__rn_buoy__"))return true}if(n===108&&e.startsWith("logbox")){return true}return false}function getComponentName(e){if(!e)return null;const n=e.type;if(n){if(typeof n==="string")return n;if(n.name)return n.name;if(n.displayName)return n.displayName}const i=e.elementType;if(i){if(i.name)return i.name;if(i.displayName)return i.displayName}return null}const INTERNAL_COMPONENT_NAMES=new Set(["View","Text","TextImpl","Image","ScrollView","FlatList","SectionList","TouchableOpacity","TouchableHighlight","TouchableWithoutFeedback","Pressable","TextInput","Switch","ActivityIndicator","Modal","StatusBar","KeyboardAvoidingView","AnimatedComponent","AnimatedComponentWrapper","ScreenContainer","ScreenStack","Screen","ScreenContentWrapper","Svg","G","Path","Rect","Circle","Line","Polygon","Polyline","Ellipse","Text as SVGText","TSpan","TextPath","Use","Symbol","Defs","ClipPath","LinearGradient","RadialGradient","Stop","Mask","Pattern","Image as SVGImage","SafeAreaProvider","SafeAreaView","SafeAreaListener","GestureHandlerRootView","GestureDetector","ReanimatedView","ReanimatedText","ReanimatedImage","ReanimatedScrollView","Fragment","Suspense","Provider","Consumer","Context","ForwardRef"]);function isInternalComponent(e){if(!e)return true;if(INTERNAL_COMPONENT_NAMES.has(e))return true;if(e==="Unknown"||e==="Component")return true;if(e.startsWith("Animated"))return true;return false}function getOwningComponentName(e){if(!e)return null;let n=e._debugOwner||e.return;let i=0;let o=null;while(n&&i<30){const r=getComponentName(n);if(r&&typeof n.type!=="string"){if(!o){o=r}if(!isInternalComponent(r)){return r}}n=n.return;i++}return o}function isOurOverlayNode(e){const n=e;const i=n?.canonical?.internalInstanceHandle||n?._internalInstanceHandle||n?._internalFiberInstanceHandleDEV||(0,_utils.getDOMFiber)(n);const o=getNativeTag(e)||getNativeTag(n?.canonical?.publicInstance);const r=(0,_devtoolsComponentNames.isCapturingBuoyToolRenders)();const a=!r&&_RenderTracker.RenderTracker.getSettings().excludeDevTools;if(a&&(0,_utils.isDOMElement)(e)&&e.closest("[data-buoy-root]"))return true;if(o!=null&&a){const g=devToolsNodeCache.get(o);if(g!==void 0){return g}}let t=false;const u=i?.pendingProps?.nativeID||i?.memoizedProps?.nativeID||n?.canonical?.currentProps?.nativeID||null;if(isHighlightOverlayNativeID(u)){t=true}else if(a&&isOtherDevToolsNativeID(u)){t=true}if(!t&&i){let g=i;let m=0;while(g&&m<30){const s=getComponentName(g);if(s){if(_devtoolsComponentNames.HIGHLIGHT_OVERLAY_COMPONENTS.has(s)){t=true;break}for(const d of _devtoolsComponentNames.HIGHLIGHT_OVERLAY_PREFIXES){if(s.startsWith(d)){t=true;break}}if(t)break;if(r&&(0,_devtoolsComponentNames.isPerfMonitorComponentName)(s)){t=true;break}if(a){if(_devtoolsComponentNames.OTHER_DEV_TOOLS_COMPONENTS.has(s)||_devtoolsComponentNames.OTHER_DEV_TOOLS_EXACT.has(s)){t=true;break}for(const d of _devtoolsComponentNames.OTHER_DEV_TOOLS_PREFIXES){if(s.startsWith(d)){t=true;break}}if(t)break}}const f=g.pendingProps?.nativeID||g.memoizedProps?.nativeID;if(isHighlightOverlayNativeID(f)){t=true;break}if(a&&isOtherDevToolsNativeID(f)){t=true;break}g=g.return;m++}}if(t&&o!=null&&a){if(devToolsNodeCache.size>=CACHE_MAX_SIZE){const g=Array.from(devToolsNodeCache.keys());for(let m=0;m<CACHE_MAX_SIZE/2;m++){devToolsNodeCache.delete(g[m])}}devToolsNodeCache.set(o,t)}return t}function getColorForRenderCount(e){const n=Math.min(e-1,COLORS.length-1);return COLORS[Math.max(0,n)]}function handleTraceUpdates(e){if(e.size===0){return}if(!globalEnabled&&!backgroundTrackingEnabled){return}const n=_RenderTracker.RenderTracker.getState();if(n.isPaused){return}if(_PerformanceLogger.PerformanceLogger.isEnabled()){(0,_PerformanceLogger.markEventReceived)()}const i=_RenderTracker.RenderTracker.getBatchSize();const o=_PerformanceLogger.PerformanceLogger.startBatch(e.size,i);const r=[];let a=0;let t=0;const u=_RenderTracker.RenderTracker.hasActiveFilters();for(const s of e){if(s&&typeof s==="object"){try{if(isOurOverlayNode(s)){a++;continue}const f=getPublicInstance(s);const d=getNativeTag(s)||getNativeTag(f);if(d==null){continue}const y=extractComponentInfo(s);if(u&&!_RenderTracker.RenderTracker.passesFilters(y)){t++;continue}const I=_RenderTracker.RenderTracker.getSettings().showRenderCount;let p;let c;if(I){const l=nodeRenderCounts.get(d)||0;p=l+1;nodeRenderCounts.set(d,p);c=getColorForRenderCount(p)}else{p=0;c=COLORS[0]}r.push({node:s,color:c,count:p})}catch{}}}o.markFilteringComplete(a+t,r.length);if(r.length===0){o.finish();return}if(DEBUG_LOGGING){console.log(`[HighlightUpdates] ${r.length} component(s) re-rendered`)}const g=_RenderTracker.RenderTracker.getRenderEventCallbackCount()>0;if(!highlightCallback&&!g){if(DEBUG_LOGGING){console.log(`[HighlightUpdates] No highlightCallback and no event callbacks - skipping render`)}isProcessing=false;return}o.markMeasurementStart();const m=r.slice(0,i*4).map(({node:s,color:f,count:d})=>new Promise(y=>{let I=false;const p=b=>{if(I)return;I=true;clearTimeout(c);y(b)};const c=setTimeout(()=>p({rect:null,stateNode:s,color:f,count:d}),500);const l=getPublicInstance(s);const v=getNativeTag(s)||getNativeTag(l);if(v==null){p({rect:null,stateNode:s,color:f,count:d});return}const _=(b,T,h,D,w,C)=>{if(w==null||C==null||h==null||D==null){p({rect:null,stateNode:s,color:f,count:d});return}p({rect:{id:v,x:w,y:C,width:h,height:D,color:f,count:d},stateNode:s,color:f,count:d})};if(!l){const b=s.node;const T=globalThis.nativeFabricUIManager;if(b&&T?.measure){try{T.measure(b,_)}catch(h){p({rect:null,stateNode:s,color:f,count:d})}return}p({rect:null,stateNode:s,color:f,count:d});return}try{l.measure(_)}catch(b){p({rect:null,stateNode:s,color:f,count:d})}}));Promise.all(m).then(s=>{const f=s.filter(y=>y.rect!==null);const d=f.map(y=>y.rect);o.markMeasurementComplete(f.length,s.length-f.length);if(d.length>0&&highlightCallback&&globalEnabled&&!highlightsSuppressed){highlightCallback(d)}setTimeout(()=>{try{for(const c of f){if(c.rect)registerTrackedNode(c.rect.id,c.stateNode)}}catch(c){debugLog("registerTrackedNode batch failed",c)}const y=_RenderTracker.RenderTracker.getSettings();const I=y.trackRenderCauses&&y.showRenderCount;let p=null;if(I){p=new Set;for(const{rect:c}of f){if(c){p.add(c.id)}}}if(globalEnabled){_RenderTracker.RenderTracker.startBatch();for(const{rect:c,stateNode:l,color:v,count:_}of f){if(c){const b=extractComponentInfo(l);let T;if(I&&p){const h=l;const D=getFiberFromNode(h);T=(0,_RenderCauseDetector.detectRenderCause)(c.id,D,p,y.debugLogLevel)}_RenderTracker.RenderTracker.trackRender({nativeTag:c.id,viewType:b.viewType,testID:b.testID,nativeID:b.nativeID,accessibilityLabel:b.accessibilityLabel,componentName:b.componentName,measurements:{x:c.x,y:c.y,width:c.width,height:c.height},color:v,count:_,renderCause:T})}}_RenderTracker.RenderTracker.endBatch()}else{for(const{rect:c,stateNode:l,color:v,count:_}of f){if(c){const b=extractComponentInfo(l);let T;if(I&&p){const h=l;const D=getFiberFromNode(h);T=(0,_RenderCauseDetector.detectRenderCause)(c.id,D,p,y.debugLogLevel)}_RenderTracker.RenderTracker.emitRenderEvent({nativeTag:c.id,viewType:b.viewType,testID:b.testID,nativeID:b.nativeID,accessibilityLabel:b.accessibilityLabel,componentName:b.componentName,measurements:{x:c.x,y:c.y,width:c.width,height:c.height},color:v,count:_,renderCause:T})}}}o.markTrackingComplete();o.markCallbackComplete();o.finish()},0)}).catch(s=>{console.error("[HighlightUpdates] Error in measurement pipeline:",s);o.finish()})}const WeakRefCtor=globalThis.WeakRef;const nodeRegistry=new Map;function registerTrackedNode(e,n){if(e==null||n==null||typeof n!=="object"){return}try{nodeRegistry.set(e,WeakRefCtor?new WeakRefCtor(n):{deref:()=>n})}catch{nodeRegistry.set(e,{deref:()=>n})}}function derefNode(e){const n=nodeRegistry.get(e);const i=n?.deref();if(!i)nodeRegistry.delete(e);return i??null}function measureNode(e){return new Promise(n=>{try{e.measure((i,o,r,a,t,u)=>{if(t==null||u==null||r==null||a==null){n(null)}else{n({x:t,y:u,width:r,height:a})}})}catch{n(null)}})}function getFiberFromNode(e){const n=e;return n?.canonical?.internalInstanceHandle||n?._internalInstanceHandle||n?._internalFiberInstanceHandleDEV||(0,_utils.getDOMFiber)(n)||null}function findScrollAncestor(e){let n=getFiberFromNode(e);let i=0;while(n&&i++<200){const o=n.stateNode;if(o&&typeof o.scrollTo==="function"){const r=typeof o.getNativeScrollRef==="function"&&o.getNativeScrollRef()||null;const a=typeof o.getInnerViewNode==="function"&&o.getInnerViewNode()||typeof o.getScrollableNode==="function"&&o.getScrollableNode()||o;return{instance:o,nativeRef:r,innerNode:a}}n=n.return}return null}async function scrollDescendantIntoView(e,n,i){try{const o=findScrollAncestor(e);if(!o)return false;const r=n;let a=null;const t=u=>new Promise(g=>{let m=false;const s=d=>{if(m)return;m=true;g(d)};const f=setTimeout(()=>s(null),600);try{u(d=>{clearTimeout(f);s(d)})}catch{clearTimeout(f);s(null)}});if(o.nativeRef&&typeof r.measureLayout==="function"){a=await t(u=>r.measureLayout(o.nativeRef,(g,m)=>u(m),()=>u(null)))}if(a==null){const u=(0,_reactNative.findNodeHandle)(n);const g=typeof o.innerNode==="number"?o.innerNode:(0,_reactNative.findNodeHandle)(o.innerNode);if(u!=null&&g!=null){a=await t(m=>_reactNative.UIManager.measureLayout(u,g,()=>m(null),(s,f)=>m(f)))}}if(a==null)return false;o.instance.scrollTo({y:Math.max(0,a-i),animated:false});await new Promise(u=>setTimeout(u,80));return true}catch{return false}}function matchScore(e,n){const i=[e.testID,e.nativeID,e.accessibilityLabel,e.componentName,e.viewType,String(e.nativeTag)];let o=0;for(const r of i){if(!r)continue;const a=r.toLowerCase();if(a===n)return 3;if(a.startsWith(n))o=Math.max(o,2);else if(a.includes(n))o=Math.max(o,1)}return o}async function locateComponent(e){const n=e.scrollIntoView!==false;const i=typeof e.margin==="number"?e.margin:12;const o=_RenderTracker.RenderTracker.getRenders();const r=async()=>{const l=await(0,_screenElements.describeScreen)();const v=(e.query??"").trim().toLowerCase();let _;for(const h of l.elements){if(e.nativeTag!=null&&h.nativeTag===e.nativeTag){_={el:h,score:1e3};break}const D=(0,_tapMatch.scoreTapMatch)({nativeTag:h.nativeTag,testID:h.testID,label:h.label,text:h.text,name:h.name,tap:h.tap},{query:v||void 0,nativeTag:e.nativeTag});if(D<=0)continue;if(!_||D>_.score)_={el:h,score:D}}if(!_?.el.frame){return{matched:false,reason:"no-match",candidates:[]}}const b=l.screen;const T=_.el.frame;return{matched:true,live:true,rect:{x:T.x*b.width,y:T.y*b.height,width:T.width*b.width,height:T.height*b.height},scale:_reactNative.PixelRatio.get(),screen:{width:b.width,height:b.height},nativeTag:_.el.nativeTag??void 0,componentName:_.el.name,testID:_.el.testID}};if(o.length===0)return r();const a=(e.query??"").trim().toLowerCase();const t=a?o.map(l=>({r:l,score:matchScore(l,a)})).filter(l=>l.score>0).sort((l,v)=>v.score-l.score||v.r.lastRenderTime-l.r.lastRenderTime):[];const u=t.slice(0,8).map(({r:l})=>({nativeTag:l.nativeTag,componentName:l.componentName,testID:l.testID,nativeID:l.nativeID,viewType:l.viewType,renderCount:l.renderCount}));const g=e.nativeTag??(t.length>0?t[0].r.nativeTag:void 0);if(g==null){const l=await r();return l.matched?l:{matched:false,reason:"no-match",candidates:u}}const m=o.find(l=>l.nativeTag===g);let s=null;let f=false;let d=false;const y=derefNode(g);if(y){const l=getPublicInstance(y);if(l){if(n){d=await scrollDescendantIntoView(y,l,i)}s=await measureNode(l);f=!!s}}if(!s&&m?.measurements){s={...m.measurements}}if(!s){return{matched:false,reason:"no-measurement",nativeTag:g,candidates:u}}const I=_reactNative.PixelRatio.get();const p=_reactNative.Dimensions.get("window");const c=s.x>=-1&&s.y>=-1&&s.x+s.width<=p.width+1&&s.y+s.height<=p.height+1;return{matched:true,live:f,scrolled:d,inView:c,rect:s,scale:I,screen:{width:p.width,height:p.height},nativeTag:g,componentName:m?.componentName,testID:m?.testID,candidates:u}}function setTraceUpdatesOnRenderers(e){if(!hook?.rendererInterfaces){debugLog("No rendererInterfaces available");return}let n=0;hook.rendererInterfaces.forEach((i,o)=>{if(typeof i.setTraceUpdatesEnabled==="function"){try{i.setTraceUpdatesEnabled(e);n++;debugLog(`Renderer ${o}: setTraceUpdatesEnabled(${e})`)}catch(r){debugLog(`Renderer ${o}: error setting trace updates`,r)}}else{debugLog(`Renderer ${o}: no setTraceUpdatesEnabled method`)}});debugLog(`Set trace updates ${e?"enabled":"disabled"} on ${n} renderer(s)`);if(hook.reactDevtoolsAgent?.setTraceUpdatesEnabled){try{hook.reactDevtoolsAgent.setTraceUpdatesEnabled(e);debugLog("Also set trace updates on agent")}catch(i){debugLog("Error setting trace updates on agent",i)}}}function subscribeToTraceUpdates(){if(!hook)return;if(traceUpdatesUnsubscribe)return;debugLog("Subscribing to traceUpdates event");if(typeof hook.sub==="function"){traceUpdatesUnsubscribe=hook.sub("traceUpdates",handleTraceUpdates);debugLog("Subscribed using hook.sub()")}else{debugLog("hook.sub not available, traceUpdates may not work")}setTraceUpdatesOnRenderers(true)}function unsubscribeFromTraceUpdates(){setTraceUpdatesOnRenderers(false);if(traceUpdatesUnsubscribe){traceUpdatesUnsubscribe();traceUpdatesUnsubscribe=null;debugLog("Unsubscribed from traceUpdates event")}}function setHighlightCallback(e){highlightCallback=e;debugLog(`Highlight callback ${e?"set":"cleared"}`)}function setBadgePressCallback(e){badgePressCallback=e;debugLog(`Badge press callback ${e?"set":"cleared"}`)}function getBadgePressCallback(){return badgePressCallback}function handleBadgePress(e){if(badgePressCallback){badgePressCallback(e)}}let spotlightCallback=null;let currentSpotlightTag=null;function setSpotlightCallback(e){spotlightCallback=e;if(e&&currentSpotlightTag!==null){e(currentSpotlightTag)}}function setSpotlight(e){if(remoteHandler){remoteHandler("setSpotlight",{nativeTag:e});return}currentSpotlightTag=e;if(spotlightCallback){spotlightCallback(e)}}let remoteHandler=null;function setRemoteHandler(e){remoteHandler=e}function replaceRemoteState(e){if(globalEnabled!==e.enabled){globalEnabled=e.enabled;notifyStateListeners()}if(isFrozen!==e.frozen){isFrozen=e.frozen;notifyFreezeListeners()}}function getSpotlight(){return currentSpotlightTag}function notifyStateListeners(){stateListeners.forEach(e=>{try{e(globalEnabled)}catch(n){console.error("[HighlightUpdates] Error in state listener:",n)}})}function subscribe(e){stateListeners.add(e);e(globalEnabled);return()=>{stateListeners.delete(e)}}function initialize(){if(typeof __DEV__!=="undefined"&&!__DEV__){debugLog("Only available in development builds");return false}if(remoteHandler){return true}if(initialized){return true}hook=window?.__REACT_DEVTOOLS_GLOBAL_HOOK__||null;if(!hook){debugLog("React DevTools hook not found");return false}debugLog("Hook found");debugLog(`Hook has sub: ${typeof hook.sub==="function"}`);debugLog(`Hook has on: ${typeof hook.on==="function"}`);debugLog(`Hook has emit: ${typeof hook.emit==="function"}`);(0,_ProfilerInterceptor.installProfilerInterceptor)();if(hook.rendererInterfaces&&hook.rendererInterfaces.size>0){initialized=true;debugLog(`Initialized with ${hook.rendererInterfaces.size} renderer(s)`);exposeGlobally();return true}const e=setInterval(()=>{if(hook?.rendererInterfaces&&hook.rendererInterfaces.size>0){clearInterval(e);initialized=true;debugLog(`Initialized with ${hook.rendererInterfaces.size} renderer(s) (delayed)`);exposeGlobally()}},100);setTimeout(()=>clearInterval(e),1e4);return false}function exposeGlobally(){if(typeof window!=="undefined"){window.__HIGHLIGHT_UPDATES_CONTROLLER__={enable,disable,toggle,isEnabled,setEnabled,subscribe,initialize,destroy,isInitialized:()=>initialized,setHighlightCallback}}}function enable(){if(typeof __DEV__!=="undefined"&&!__DEV__)return;if(!initialized){initialize()}if(highlightsSuppressed){highlightsSuppressed=false}if(globalEnabled){return}debugLog("Enabling highlights");if(!backgroundTrackingEnabled){subscribeToTraceUpdates()}_RenderTracker.RenderTracker.start();globalEnabled=true;notifyStateListeners();(0,_sharedUi.notifySubscriberCountChange)("render")}function disable(){if(typeof __DEV__!=="undefined"&&!__DEV__)return;if(!globalEnabled){return}debugLog("Disabling highlights");renderingLock=false;isProcessing=false;if(renderingLockTimeout){clearTimeout(renderingLockTimeout);renderingLockTimeout=null}renderedOverlayTags.clear();devToolsNodeCache.clear();(0,_RenderCauseDetector.clearRenderCauseState)();if(isFrozen){isFrozen=false;notifyFreezeListeners()}if(!backgroundTrackingEnabled){unsubscribeFromTraceUpdates()}_RenderTracker.RenderTracker.stop();globalEnabled=false;notifyStateListeners();(0,_sharedUi.notifySubscriberCountChange)("render")}function enableBackgroundTracking(){if(typeof __DEV__!=="undefined"&&!__DEV__)return;if(backgroundTrackingEnabled){return}if(!initialized){initialize()}debugLog("Enabling background tracking");if(!globalEnabled){subscribeToTraceUpdates()}backgroundTrackingEnabled=true}function disableBackgroundTracking(){if(typeof __DEV__!=="undefined"&&!__DEV__)return;if(!backgroundTrackingEnabled){return}debugLog("Disabling background tracking");backgroundTrackingEnabled=false;if(!globalEnabled){unsubscribeFromTraceUpdates()}}function isBackgroundTrackingEnabled(){return backgroundTrackingEnabled}function toggle(){if(typeof __DEV__!=="undefined"&&!__DEV__)return;if(remoteHandler){remoteHandler("toggle");return}if(globalEnabled){disable()}else{enable()}}function clearRenderCounts(){if(remoteHandler){remoteHandler("clearRenderCounts");return}nodeRenderCounts.clear();_RenderTracker.RenderTracker.clear();debugLog("Cleared render counts")}function isEnabled(){return globalEnabled}function setEnabled(e){if(remoteHandler){remoteHandler("setEnabled",{enabled:e});return}if(e)enable();else disable()}function setHighlightsSuppressed(e){if(highlightsSuppressed===e)return;highlightsSuppressed=e;if(e&&highlightCallback){highlightCallback([])}}function getHighlightsSuppressed(){return highlightsSuppressed}function setSilentTracking(e){if(remoteHandler){remoteHandler("setSilentTracking",{enabled:e});return}if(e){enable();setHighlightsSuppressed(true)}else{setHighlightsSuppressed(false);disable()}}function isInitialized(){return initialized}function notifyFreezeListeners(){freezeListeners.forEach(e=>{try{e(isFrozen)}catch(n){console.error("[HighlightUpdates] Error in freeze listener:",n)}})}function subscribeToFreeze(e){freezeListeners.add(e);e(isFrozen);return()=>{freezeListeners.delete(e)}}function freeze(){if(typeof __DEV__!=="undefined"&&!__DEV__)return;if(isFrozen)return;isFrozen=true;debugLog("Freeze mode enabled");notifyFreezeListeners()}function unfreeze(){if(typeof __DEV__!=="undefined"&&!__DEV__)return;if(!isFrozen)return;isFrozen=false;debugLog("Freeze mode disabled");if(highlightCallback){highlightCallback([])}notifyFreezeListeners()}function toggleFreeze(){if(remoteHandler){remoteHandler("toggleFreeze");return}if(isFrozen){unfreeze()}else{freeze()}}function getFrozen(){return isFrozen}function destroy(){if(!initialized)return;unsubscribeFromTraceUpdates();(0,_ProfilerInterceptor.uninstallProfilerInterceptor)();(0,_ProfilerInterceptor.setComparisonCallback)(null);isFrozen=false;freezeListeners.clear();globalEnabled=false;hook=null;highlightCallback=null;initialized=false;if(typeof window!=="undefined"){delete window.__HIGHLIGHT_UPDATES_CONTROLLER__}debugLog("Destroyed")}const HighlightUpdatesController={subscribe,enable,disable,toggle,isEnabled,setEnabled,initialize,destroy,isInitialized,setHighlightCallback,clearRenderCounts,freeze,unfreeze,toggleFreeze,getFrozen,subscribeToFreeze,setBadgePressCallback,getBadgePressCallback,handleBadgePress,setSpotlightCallback,setSpotlight,getSpotlight,clearDevToolsCache,enableBackgroundTracking,disableBackgroundTracking,isBackgroundTrackingEnabled,setRemoteHandler,replaceRemoteState,locateComponent,setHighlightsSuppressed,getHighlightsSuppressed,setSilentTracking};var _default=exports.default=HighlightUpdatesController;