@buoy-gg/highlight-updates 4.0.2 → 6.0.0

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.
@@ -37,6 +37,7 @@ var _RenderTracker = require("./RenderTracker");
37
37
  var _PerformanceLogger = require("./PerformanceLogger");
38
38
  var _RenderCauseDetector = require("./RenderCauseDetector");
39
39
  var _sharedUi = require("@buoy-gg/shared-ui");
40
+ var _devtoolsComponentNames = require("./devtoolsComponentNames");
40
41
  // State
41
42
  let globalEnabled = false; // User-controlled toggle for visual highlights
42
43
  // When true, tracking + measurement + storage all run as normal, but the
@@ -118,6 +119,10 @@ function getNativeTag(instance) {
118
119
  if (inst.canonical) {
119
120
  if (inst.canonical.__nativeTag != null) return inst.canonical.__nativeTag;
120
121
  if (inst.canonical._nativeTag != null) return inst.canonical._nativeTag;
122
+ // Modern Fabric instance handles: { node, canonical: { nativeTag, ... } }.
123
+ // Without this shape ~99% of traced nodes were dropped on New
124
+ // Architecture — highlights only appeared for tap-shaped legacy nodes.
125
+ if (inst.canonical.nativeTag != null) return inst.canonical.nativeTag;
121
126
  }
122
127
  return null;
123
128
  }
@@ -324,48 +329,12 @@ function isOurOverlayTag(nativeTag) {
324
329
  // 5. Cached property access to avoid repeated traversal
325
330
  // ============================================================================
326
331
 
327
- // Components that MUST always be excluded to prevent infinite loops.
328
- // Includes the Highlight Updates overlay AND the Events tool (which subscribes
329
- // to render callbacks via RenderTracker.onRenderEvent - tracking its renders
330
- // would create an infinite callback loop).
331
- const HIGHLIGHT_OVERLAY_COMPONENTS = new Set([
332
- // Highlight Updates overlay components
333
- "HighlightUpdatesOverlay", "HighlightUpdatesModal", "HighlightFilterView", "RenderDetailView", "RenderListItem", "RenderListItemInner", "RenderHistoryViewer", "RenderHistoryFooter", "RenderCauseBadge", "TwoLevelCauseBadge", "EnhancedCauseDisplay", "IsolatedRenderList", "IsolatedRenderListInner", "StatsDisplay", "StatsDisplayInner", "CurrentStateView", "EventStepperFooter",
334
- // Events tool components - subscribes to onRenderEvent, tracking these causes infinite loops
335
- "EventsModal", "UnifiedEventList", "UnifiedEventItem", "UnifiedEventDetail", "UnifiedEventFilters", "UnifiedEventViewer", "ReactQueryEventDetail", "EventsCopySettingsView"]);
332
+ // Component-name sets/prefixes live in devtoolsComponentNames.ts (shared with
333
+ // CommitProfiler). Native-ID sets stay here only the node walk uses them.
336
334
 
337
335
  // Native IDs that MUST always be excluded (highlight overlay)
338
336
  const HIGHLIGHT_OVERLAY_NATIVE_IDS = new Set(["highlight-updates-overlay", "__rn_buoy__highlight-modal"]);
339
337
 
340
- // Prefixes for always-excluded components (highlight overlay + events tool)
341
- const HIGHLIGHT_OVERLAY_PREFIXES = ["HighlightUpdates", "RenderList", "RenderDetail", "RenderHistory", "RenderCause", "EventStepper",
342
- // Events tool - subscribes to onRenderEvent, must always be excluded
343
- "UnifiedEvent", "EventsModal", "EventsCopy"];
344
-
345
- // Other dev tools components - can be toggled via excludeDevTools setting
346
- const OTHER_DEV_TOOLS_COMPONENTS = new Set([
347
- // React Buoy devtools components
348
- "JsModalComponent", "JsModal", "TypePicker", "PatternInput", "PatternChip", "DetectedItemsSection", "DetectedCategoryBadge", "IdentifierBadge", "CategoryBadge", "AppRenderer", "AppOverlay", "FloatingTools", "DialDevTools", "DevToolsVisibilityProvider", "AppHostProvider", "MinimizedToolsProvider",
349
- // Shared UI components used in modals
350
- "ModalHeader", "TabSelector", "SectionHeader", "DraggableHeader", "WindowControls",
351
- // Shared UI data viewer components (JSON diff viewers, etc.)
352
- "TreeDiffViewer", "DataViewer", "SplitDiffViewer", "VirtualizedDataExplorer", "DiffSummary", "TypeLegend", "IndentGuides", "IndentGuidesOverlay", "CyberpunkInput", "DiffView", "AnswerCard", "DetailsSection", "DetailRow", "QuickActionsSection", "FilterOptionCard",
353
- // Modal header sub-components
354
- "SearchSection", "SearchSectionInner", "HeaderActions", "HeaderActionsInner", "MainListHeader", "FilterViewHeader", "DetailViewHeader",
355
- // Floating menu components (minimized tools, dial, etc.)
356
- "FloatingMenu", "FloatingDevTools", "MinimizedToolsStack", "MinimizedToolsContext", "GlitchToolButton", "ExpandablePopover", "CollapsedPeek", "ExpandedWrapper", "DialIcon", "DialMenu", "DialIconItem", "OnboardingTooltip", "MenuLauncherIcon", "DevToolsSettingsModal", "SettingsModal",
357
- // Image Overlay components
358
- "ImageOverlayModal", "ImageOverlayStandalone", "ToolCard", "ToolIcon", "GripVerticalIcon", "UserStatus", "Divider",
359
- // Shared UI icon components (Chevrons, etc.)
360
- "ChevronDown", "ChevronUp", "ChevronLeft", "ChevronRight", "ChevronDownIcon", "ChevronUpIcon", "ChevronLeftIcon", "ChevronRightIcon",
361
- // React Native LogBox components (shown on reload/errors)
362
- "LogBox", "LogBoxLog", "LogBoxLogNotification", "LogBoxNotificationContainer", "_LogBoxNotificationContainer", "LogBoxInspector", "LogBoxInspectorContainer", "LogBoxInspectorHeader", "LogBoxInspectorBody", "LogBoxInspectorFooter", "LogBoxInspectorMessageHeader", "LogBoxInspectorStackFrame", "LogBoxInspectorSection", "LogBoxButton", "LogBoxMessage"]);
363
-
364
- // Other dev tools prefixes - can be toggled via excludeDevTools setting
365
- const OTHER_DEV_TOOLS_PREFIXES = ["JsModal", "TreeDiff", "DataViewer", "DiffView", "DiffSummary", "Virtualized",
366
- // Floating menu prefixes
367
- "Floating", "Minimized", "Dial", "Expandable", "Chevron", "Glitch", "Settings", "Onboarding", "DevTools", "ImageOverlay"];
368
-
369
338
  // Other dev tools native IDs - can be toggled
370
339
  const OTHER_DEV_TOOLS_NATIVE_IDS = new Set(["jsmodal-root", "image-overlay-standalone",
371
340
  // LogBox native IDs
@@ -524,8 +493,15 @@ function isOurOverlayNode(stateNode) {
524
493
  // Get nativeTag for caching
525
494
  const nativeTag = getNativeTag(stateNode) || getNativeTag(node?.canonical?.publicInstance);
526
495
 
527
- // Check if we should exclude all dev tools or just highlight overlay
528
- const excludeDevTools = _RenderTracker.RenderTracker.getSettings().excludeDevTools;
496
+ // Check if we should exclude all dev tools or just highlight overlay.
497
+ // Bench "profile our own tools" mode (the example app's /tool-bench page)
498
+ // forces this OFF so live highlight boxes flash on our tool UI too —
499
+ // except the perf-monitor HUD (checked separately in the walk below),
500
+ // whose 250ms sample-tick churn would flash constantly. Cache reads and
501
+ // writes are already gated on excludeDevTools, so bench mode bypasses
502
+ // the cache and can't poison it.
503
+ const benchMode = (0, _devtoolsComponentNames.isCapturingBuoyToolRenders)();
504
+ const excludeDevTools = !benchMode && _RenderTracker.RenderTracker.getSettings().excludeDevTools;
529
505
 
530
506
  // Check cache first (O(1)) - but only for the current excludeDevTools setting
531
507
  // When excludeDevTools is false, skip cache to re-evaluate all nodes
@@ -558,12 +534,12 @@ function isOurOverlayNode(stateNode) {
558
534
  const name = getComponentName(current);
559
535
  if (name) {
560
536
  // Always check highlight overlay components (must always be excluded)
561
- if (HIGHLIGHT_OVERLAY_COMPONENTS.has(name)) {
537
+ if (_devtoolsComponentNames.HIGHLIGHT_OVERLAY_COMPONENTS.has(name)) {
562
538
  result = true;
563
539
  break;
564
540
  }
565
541
  // Check highlight overlay prefixes
566
- for (const prefix of HIGHLIGHT_OVERLAY_PREFIXES) {
542
+ for (const prefix of _devtoolsComponentNames.HIGHLIGHT_OVERLAY_PREFIXES) {
567
543
  if (name.startsWith(prefix)) {
568
544
  result = true;
569
545
  break;
@@ -571,14 +547,20 @@ function isOurOverlayNode(stateNode) {
571
547
  }
572
548
  if (result) break;
573
549
 
550
+ // Bench mode: the perf-monitor HUD still never flashes.
551
+ if (benchMode && (0, _devtoolsComponentNames.isPerfMonitorComponentName)(name)) {
552
+ result = true;
553
+ break;
554
+ }
555
+
574
556
  // Only check other dev tools if excludeDevTools is enabled
575
557
  if (excludeDevTools) {
576
- if (OTHER_DEV_TOOLS_COMPONENTS.has(name)) {
558
+ if (_devtoolsComponentNames.OTHER_DEV_TOOLS_COMPONENTS.has(name)) {
577
559
  result = true;
578
560
  break;
579
561
  }
580
562
  // Check other dev tools prefixes
581
- for (const prefix of OTHER_DEV_TOOLS_PREFIXES) {
563
+ for (const prefix of _devtoolsComponentNames.OTHER_DEV_TOOLS_PREFIXES) {
582
564
  if (name.startsWith(prefix)) {
583
565
  result = true;
584
566
  break;
@@ -780,14 +762,33 @@ function handleTraceUpdates(nodes) {
780
762
  // Use batch size from RenderTracker settings (default: 150)
781
763
  // Note: batchSize already retrieved above for perfTimer
782
764
  perfTimer.markMeasurementStart();
783
- const measurePromises = nodesToDraw.slice(0, batchSize).map(({
765
+ // Measure up to batchSize: coalesced commits can exceed one batch and
766
+ // the old hard slice silently never highlighted the same tail components.
767
+ const measurePromises = nodesToDraw.slice(0, batchSize * 4).map(({
784
768
  node: stateNode,
785
769
  color,
786
770
  count
787
771
  }) => new Promise(resolve => {
772
+ // RN silently never fires measure callbacks for detached nodes;
773
+ // without this timeout one dead node hangs the whole batch's
774
+ // Promise.all (no boxes, no tracking, counts still climb).
775
+ let settled = false;
776
+ const settle = value => {
777
+ if (settled) return;
778
+ settled = true;
779
+ clearTimeout(timeoutId);
780
+ resolve(value);
781
+ };
782
+ const timeoutId = setTimeout(() => settle({
783
+ rect: null,
784
+ stateNode,
785
+ color,
786
+ count
787
+ }), 500);
788
788
  const publicInstance = getPublicInstance(stateNode);
789
- if (!publicInstance) {
790
- resolve({
789
+ const nativeTag = getNativeTag(stateNode) || getNativeTag(publicInstance);
790
+ if (nativeTag == null) {
791
+ settle({
791
792
  rect: null,
792
793
  stateNode,
793
794
  color,
@@ -795,44 +796,62 @@ function handleTraceUpdates(nodes) {
795
796
  });
796
797
  return;
797
798
  }
798
- const nativeTag = getNativeTag(stateNode) || getNativeTag(publicInstance);
799
- if (nativeTag == null) {
800
- resolve({
801
- rect: null,
799
+ const onMeasured = (x, y, width, height, pageX, pageY) => {
800
+ if (pageX == null || pageY == null || width == null || height == null) {
801
+ settle({
802
+ rect: null,
803
+ stateNode,
804
+ color,
805
+ count
806
+ });
807
+ return;
808
+ }
809
+ settle({
810
+ rect: {
811
+ id: nativeTag,
812
+ x: pageX,
813
+ y: pageY,
814
+ width,
815
+ height,
816
+ color,
817
+ count
818
+ },
802
819
  stateNode,
803
820
  color,
804
821
  count
805
822
  });
806
- return;
807
- }
808
- try {
809
- publicInstance.measure((x, y, width, height, pageX, pageY) => {
810
- if (pageX == null || pageY == null || width == null || height == null) {
811
- resolve({
823
+ };
824
+ if (!publicInstance) {
825
+ // New Architecture: canonical.publicInstance is often lazily
826
+ // ABSENT on traceUpdates wrappers measure the wrapper's `node`
827
+ // (the Fabric shadow node) directly through FabricUIManager.
828
+ const fabricNode = stateNode.node;
829
+ const fabricUI = globalThis.nativeFabricUIManager;
830
+ if (fabricNode && fabricUI?.measure) {
831
+ try {
832
+ fabricUI.measure(fabricNode, onMeasured);
833
+ } catch (error) {
834
+ settle({
812
835
  rect: null,
813
836
  stateNode,
814
837
  color,
815
838
  count
816
839
  });
817
- return;
818
840
  }
819
- resolve({
820
- rect: {
821
- id: nativeTag,
822
- x: pageX,
823
- y: pageY,
824
- width,
825
- height,
826
- color,
827
- count
828
- },
829
- stateNode,
830
- color,
831
- count
832
- });
841
+ return;
842
+ }
843
+ settle({
844
+ rect: null,
845
+ stateNode,
846
+ color,
847
+ count
833
848
  });
849
+ return;
850
+ }
851
+ try {
852
+ publicInstance.measure(onMeasured);
834
853
  } catch (error) {
835
- resolve({
854
+ settle({
836
855
  rect: null,
837
856
  stateNode,
838
857
  color,
@@ -847,119 +866,126 @@ function handleTraceUpdates(nodes) {
847
866
  // Mark measurement phase complete
848
867
  perfTimer.markMeasurementComplete(validResults.length, results.length - validResults.length);
849
868
 
850
- // Keep a live handle to each measured node so the desktop's "capture this
851
- // component" flow can re-measure (and scroll to) it on demand later.
852
- // Defensive: never let registration break the measurement/tracking batch.
853
- try {
854
- for (const r of validResults) {
855
- if (r.rect) registerTrackedNode(r.rect.id, r.stateNode);
856
- }
857
- } catch (e) {
858
- debugLog("registerTrackedNode batch failed", e);
869
+ // DRAW FIRST. With the render list open, tracking a big burst
870
+ // (store writes + cause detection + list row rebuilds) used to run
871
+ // BEFORE the highlight callback, so boxes painted ~0.7s later
872
+ // (user-timed: 1.3s list-closed vs 2s+ list-open, 2026-07-08).
873
+ // Boxes are the product — paint them, then bookkeep.
874
+ if (rects.length > 0 && highlightCallback && globalEnabled && !highlightsSuppressed) {
875
+ highlightCallback(rects);
859
876
  }
860
877
 
861
- // Track renders - behavior differs based on what's enabled:
862
- // - globalEnabled (visual tool): full tracking with storage and list updates
863
- // - backgroundTrackingEnabled only (Events tool): emit events only, no storage
864
- const settings = _RenderTracker.RenderTracker.getSettings();
865
- const trackCauses = settings.trackRenderCauses && settings.showRenderCount;
866
- let batchNativeTags = null;
867
- if (trackCauses) {
868
- batchNativeTags = new Set();
869
- for (const {
870
- rect
871
- } of validResults) {
872
- if (rect) {
873
- batchNativeTags.add(rect.id);
878
+ // Tracking/list work runs in a macrotask so React commits the boxes
879
+ // before this heavier pass starts.
880
+ setTimeout(() => {
881
+ // Keep a live handle to each measured node so the desktop's "capture this
882
+ // component" flow can re-measure (and scroll to) it on demand later.
883
+ // Defensive: never let registration break the measurement/tracking batch.
884
+ try {
885
+ for (const r of validResults) {
886
+ if (r.rect) registerTrackedNode(r.rect.id, r.stateNode);
874
887
  }
888
+ } catch (e) {
889
+ debugLog("registerTrackedNode batch failed", e);
875
890
  }
876
- }
877
- if (globalEnabled) {
878
- // Visual tool is on - full tracking with storage and list notifications
879
- _RenderTracker.RenderTracker.startBatch();
880
- for (const {
881
- rect,
882
- stateNode,
883
- color,
884
- count
885
- } of validResults) {
886
- if (rect) {
887
- const componentInfo = extractComponentInfo(stateNode);
888
- let renderCause;
889
- if (trackCauses && batchNativeTags) {
890
- const node = stateNode;
891
- const fiber = node?.canonical?.internalInstanceHandle;
892
- renderCause = (0, _RenderCauseDetector.detectRenderCause)(rect.id, fiber, batchNativeTags, settings.debugLogLevel);
891
+
892
+ // Track renders - behavior differs based on what's enabled:
893
+ // - globalEnabled (visual tool): full tracking with storage and list updates
894
+ // - backgroundTrackingEnabled only (Events tool): emit events only, no storage
895
+ const settings = _RenderTracker.RenderTracker.getSettings();
896
+ const trackCauses = settings.trackRenderCauses && settings.showRenderCount;
897
+ let batchNativeTags = null;
898
+ if (trackCauses) {
899
+ batchNativeTags = new Set();
900
+ for (const {
901
+ rect
902
+ } of validResults) {
903
+ if (rect) {
904
+ batchNativeTags.add(rect.id);
893
905
  }
894
- _RenderTracker.RenderTracker.trackRender({
895
- nativeTag: rect.id,
896
- viewType: componentInfo.viewType,
897
- testID: componentInfo.testID,
898
- nativeID: componentInfo.nativeID,
899
- accessibilityLabel: componentInfo.accessibilityLabel,
900
- componentName: componentInfo.componentName,
901
- measurements: {
902
- x: rect.x,
903
- y: rect.y,
904
- width: rect.width,
905
- height: rect.height
906
- },
907
- color,
908
- count,
909
- renderCause
910
- });
911
906
  }
912
907
  }
913
- _RenderTracker.RenderTracker.endBatch();
914
- } else {
915
- // Only background tracking (Events tool) - emit events without storage
916
- for (const {
917
- rect,
918
- stateNode,
919
- color,
920
- count
921
- } of validResults) {
922
- if (rect) {
923
- const componentInfo = extractComponentInfo(stateNode);
924
- let renderCause;
925
- if (trackCauses && batchNativeTags) {
926
- const node = stateNode;
927
- const fiber = node?.canonical?.internalInstanceHandle;
928
- renderCause = (0, _RenderCauseDetector.detectRenderCause)(rect.id, fiber, batchNativeTags, settings.debugLogLevel);
908
+ if (globalEnabled) {
909
+ // Visual tool is on - full tracking with storage and list notifications
910
+ _RenderTracker.RenderTracker.startBatch();
911
+ for (const {
912
+ rect,
913
+ stateNode,
914
+ color,
915
+ count
916
+ } of validResults) {
917
+ if (rect) {
918
+ const componentInfo = extractComponentInfo(stateNode);
919
+ let renderCause;
920
+ if (trackCauses && batchNativeTags) {
921
+ const node = stateNode;
922
+ const fiber = node?.canonical?.internalInstanceHandle;
923
+ renderCause = (0, _RenderCauseDetector.detectRenderCause)(rect.id, fiber, batchNativeTags, settings.debugLogLevel);
924
+ }
925
+ _RenderTracker.RenderTracker.trackRender({
926
+ nativeTag: rect.id,
927
+ viewType: componentInfo.viewType,
928
+ testID: componentInfo.testID,
929
+ nativeID: componentInfo.nativeID,
930
+ accessibilityLabel: componentInfo.accessibilityLabel,
931
+ componentName: componentInfo.componentName,
932
+ measurements: {
933
+ x: rect.x,
934
+ y: rect.y,
935
+ width: rect.width,
936
+ height: rect.height
937
+ },
938
+ color,
939
+ count,
940
+ renderCause
941
+ });
942
+ }
943
+ }
944
+ _RenderTracker.RenderTracker.endBatch();
945
+ } else {
946
+ // Only background tracking (Events tool) - emit events without storage
947
+ for (const {
948
+ rect,
949
+ stateNode,
950
+ color,
951
+ count
952
+ } of validResults) {
953
+ if (rect) {
954
+ const componentInfo = extractComponentInfo(stateNode);
955
+ let renderCause;
956
+ if (trackCauses && batchNativeTags) {
957
+ const node = stateNode;
958
+ const fiber = node?.canonical?.internalInstanceHandle;
959
+ renderCause = (0, _RenderCauseDetector.detectRenderCause)(rect.id, fiber, batchNativeTags, settings.debugLogLevel);
960
+ }
961
+ _RenderTracker.RenderTracker.emitRenderEvent({
962
+ nativeTag: rect.id,
963
+ viewType: componentInfo.viewType,
964
+ testID: componentInfo.testID,
965
+ nativeID: componentInfo.nativeID,
966
+ accessibilityLabel: componentInfo.accessibilityLabel,
967
+ componentName: componentInfo.componentName,
968
+ measurements: {
969
+ x: rect.x,
970
+ y: rect.y,
971
+ width: rect.width,
972
+ height: rect.height
973
+ },
974
+ color,
975
+ count,
976
+ renderCause
977
+ });
929
978
  }
930
- _RenderTracker.RenderTracker.emitRenderEvent({
931
- nativeTag: rect.id,
932
- viewType: componentInfo.viewType,
933
- testID: componentInfo.testID,
934
- nativeID: componentInfo.nativeID,
935
- accessibilityLabel: componentInfo.accessibilityLabel,
936
- componentName: componentInfo.componentName,
937
- measurements: {
938
- x: rect.x,
939
- y: rect.y,
940
- width: rect.width,
941
- height: rect.height
942
- },
943
- color,
944
- count,
945
- renderCause
946
- });
947
979
  }
948
980
  }
949
- }
950
981
 
951
- // Mark tracking phase complete
952
- perfTimer.markTrackingComplete();
953
-
954
- // Only show visual highlights when the visual tool is enabled (not just
955
- // background tracking) AND not running in silent mode (screenshot tool).
956
- if (rects.length > 0 && highlightCallback && globalEnabled && !highlightsSuppressed) {
957
- highlightCallback(rects);
958
- }
982
+ // Mark tracking phase complete
983
+ perfTimer.markTrackingComplete();
959
984
 
960
- // Mark callback phase complete and finish timing
961
- perfTimer.markCallbackComplete();
962
- perfTimer.finish();
985
+ // Mark callback phase complete and finish timing
986
+ perfTimer.markCallbackComplete();
987
+ perfTimer.finish();
988
+ }, 0);
963
989
  }).catch(error => {
964
990
  console.error("[HighlightUpdates] Error in measurement pipeline:", error);
965
991
  perfTimer.finish(); // Still log timing even on error
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.OTHER_DEV_TOOLS_PREFIXES = exports.OTHER_DEV_TOOLS_COMPONENTS = exports.HIGHLIGHT_OVERLAY_PREFIXES = exports.HIGHLIGHT_OVERLAY_COMPONENTS = void 0;
7
+ exports.isCapturingBuoyToolRenders = isCapturingBuoyToolRenders;
8
+ exports.isDevToolsComponentName = isDevToolsComponentName;
9
+ exports.isPerfMonitorComponentName = isPerfMonitorComponentName;
10
+ exports.setCaptureBuoyToolRenders = setCaptureBuoyToolRenders;
11
+ // ============================================================================
12
+ // Buoy devtools component-name sets, shared by HighlightUpdatesController's
13
+ // per-node exclusion walk and CommitProfiler's subtree pruning.
14
+ //
15
+ // Extracted from HighlightUpdatesController.ts verbatim — keep the split:
16
+ // HIGHLIGHT_* sets are ALWAYS excluded (tracking them causes infinite render
17
+ // loops); OTHER_DEV_TOOLS_* sets are toggleable via the excludeDevTools
18
+ // setting in the highlight flow (CommitProfiler always excludes both).
19
+ // ============================================================================
20
+
21
+ // Components that MUST always be excluded to prevent infinite loops.
22
+ // Includes the Highlight Updates overlay AND the Events tool (which subscribes
23
+ // to render callbacks via RenderTracker.onRenderEvent - tracking its renders
24
+ // would create an infinite callback loop).
25
+ const HIGHLIGHT_OVERLAY_COMPONENTS = exports.HIGHLIGHT_OVERLAY_COMPONENTS = new Set([
26
+ // Highlight Updates overlay components
27
+ "HighlightUpdatesOverlay", "HighlightUpdatesModal", "HighlightFilterView", "RenderDetailView", "RenderListItem", "RenderListItemInner", "RenderHistoryViewer", "RenderHistoryFooter", "RenderCauseBadge", "TwoLevelCauseBadge", "EnhancedCauseDisplay", "IsolatedRenderList", "IsolatedRenderListInner", "StatsDisplay", "StatsDisplayInner", "CurrentStateView", "EventStepperFooter",
28
+ // Events tool components - subscribes to onRenderEvent, tracking these causes infinite loops
29
+ "EventsModal", "UnifiedEventList", "UnifiedEventItem", "UnifiedEventDetail", "UnifiedEventFilters", "UnifiedEventViewer", "ReactQueryEventDetail", "EventsCopySettingsView"]);
30
+
31
+ // Prefixes for always-excluded components (highlight overlay + events tool)
32
+ const HIGHLIGHT_OVERLAY_PREFIXES = exports.HIGHLIGHT_OVERLAY_PREFIXES = ["HighlightUpdates", "RenderList", "RenderDetail", "RenderHistory", "RenderCause", "EventStepper",
33
+ // Events tool - subscribes to onRenderEvent, must always be excluded
34
+ "UnifiedEvent", "EventsModal", "EventsCopy"];
35
+
36
+ // Other dev tools components - can be toggled via excludeDevTools setting
37
+ const OTHER_DEV_TOOLS_COMPONENTS = exports.OTHER_DEV_TOOLS_COMPONENTS = new Set([
38
+ // React Buoy devtools components
39
+ "JsModalComponent", "JsModal", "TypePicker", "PatternInput", "PatternChip", "DetectedItemsSection", "DetectedCategoryBadge", "IdentifierBadge", "CategoryBadge", "AppRenderer", "AppOverlay", "FloatingTools", "DialDevTools", "DevToolsVisibilityProvider", "AppHostProvider", "MinimizedToolsProvider",
40
+ // Shared UI components used in modals
41
+ "ModalHeader", "TabSelector", "SectionHeader", "DraggableHeader", "WindowControls",
42
+ // Shared UI data viewer components (JSON diff viewers, etc.)
43
+ "TreeDiffViewer", "DataViewer", "SplitDiffViewer", "VirtualizedDataExplorer", "DiffSummary", "TypeLegend", "IndentGuides", "IndentGuidesOverlay", "CyberpunkInput", "DiffView", "AnswerCard", "DetailsSection", "DetailRow", "QuickActionsSection", "FilterOptionCard",
44
+ // Modal header sub-components
45
+ "SearchSection", "SearchSectionInner", "HeaderActions", "HeaderActionsInner", "MainListHeader", "FilterViewHeader", "DetailViewHeader",
46
+ // Floating menu components (minimized tools, dial, etc.)
47
+ "FloatingMenu", "FloatingDevTools", "MinimizedToolsStack", "MinimizedToolsContext", "GlitchToolButton", "ExpandablePopover", "CollapsedPeek", "ExpandedWrapper", "DialIcon", "DialMenu", "DialIconItem", "OnboardingTooltip", "MenuLauncherIcon", "DevToolsSettingsModal", "SettingsModal",
48
+ // Image Overlay components
49
+ "ImageOverlayModal", "ImageOverlayStandalone", "ToolCard", "ToolIcon", "GripVerticalIcon", "UserStatus", "Divider",
50
+ // Shared UI icon components (Chevrons, etc.)
51
+ "ChevronDown", "ChevronUp", "ChevronLeft", "ChevronRight", "ChevronDownIcon", "ChevronUpIcon", "ChevronLeftIcon", "ChevronRightIcon",
52
+ // React Native LogBox components (shown on reload/errors)
53
+ "LogBox", "LogBoxLog", "LogBoxLogNotification", "LogBoxNotificationContainer", "_LogBoxNotificationContainer", "LogBoxInspector", "LogBoxInspectorContainer", "LogBoxInspectorHeader", "LogBoxInspectorBody", "LogBoxInspectorFooter", "LogBoxInspectorMessageHeader", "LogBoxInspectorStackFrame", "LogBoxInspectorSection", "LogBoxButton", "LogBoxMessage",
54
+ // Perf-monitor components. The HUD re-renders on every 250ms sample tick
55
+ // during a benchmark recording, so leaving these in would dominate any
56
+ // render-capture result. NOTE: no blanket "Perf" prefix — user benchmark
57
+ // screens are commonly named Perf* (e.g. PerfBenchHeavy in the example app).
58
+ "PerfMonitorOverlay", "PerfMonitorModal", "PerfMonitorRoot", "PerfHudInline", "PerfDiagnostics", "MetricSparkline", "JsFallbackNotice", "AutomationConfigView", "AutomationProgressOverlay", "AutomationResumer", "SaveBenchmarkPrompt", "SavedCasesModal", "BatchBarReport", "BatchReportView",
59
+ // Perf HUD INNER components. The live-highlight exclusion walk climbs at
60
+ // most 30 fiber levels from a flagged host view — the HUD's metric
61
+ // TextInputs sit under deep TextInput/Animated wrappers, so the walk can
62
+ // miss the PerfMonitorOverlay root; near names are what it actually hits.
63
+ "PerfHudInlineInner", "PerfMonitorOverlayInner", "MiniMetric", "MetricRow", "MetricNumber", "MetricSparklineInner", "RecColumn", "PillBody", "FullBody", "TargetLine", "PagesSection", "ProbesSection"]);
64
+
65
+ // Other dev tools prefixes - can be toggled via excludeDevTools setting
66
+ const OTHER_DEV_TOOLS_PREFIXES = exports.OTHER_DEV_TOOLS_PREFIXES = ["JsModal", "TreeDiff", "DataViewer", "DiffView", "DiffSummary", "Virtualized",
67
+ // Floating menu prefixes
68
+ "Floating", "Minimized", "Dial", "Expandable", "Chevron", "Glitch", "Settings", "Onboarding", "DevTools", "ImageOverlay",
69
+ // Perf-monitor prefixes (safe: user code is Perf*, buoy code is
70
+ // PerfMonitor* / PerfHud*)
71
+ "PerfMonitor", "PerfHud"];
72
+
73
+ // ── Bench "profile our own tools" mode ──────────────────────────────────
74
+ // When enabled (by a dedicated benchmark screen, e.g. the example app's
75
+ // /tool-bench page), render capture INCLUDES Buoy's tool UI so we can
76
+ // benchmark our own devtools. Two groups stay excluded regardless:
77
+ // - highlight overlay + events tool (tracking them = infinite render loop)
78
+ // - perf-monitor UI (the HUD re-renders on every 250ms sample tick while
79
+ // recording, so its renders would dominate any capture result)
80
+ // Only consulted by `isDevToolsComponentName` (the CommitProfiler path) —
81
+ // the highlight flow's own excludeDevTools walk reads the raw sets above
82
+ // and is deliberately unaffected.
83
+ let captureBuoyToolRenders = false;
84
+ function setCaptureBuoyToolRenders(enabled) {
85
+ captureBuoyToolRenders = enabled;
86
+ }
87
+ function isCapturingBuoyToolRenders() {
88
+ return captureBuoyToolRenders;
89
+ }
90
+
91
+ // Perf-monitor components without the "PerfMonitor" prefix (which is
92
+ // matched separately). Mirrors the perf entries in OTHER_DEV_TOOLS_*.
93
+ const PERF_MONITOR_ALWAYS_EXCLUDED = new Set(["PerfHudInline", "PerfHudInlineInner", "PerfDiagnostics", "MetricSparkline", "MetricSparklineInner", "JsFallbackNotice", "AutomationConfigView", "AutomationProgressOverlay", "AutomationResumer", "SaveBenchmarkPrompt", "SavedCasesModal", "BatchBarReport", "BatchReportView",
94
+ // HUD inner components — churn every 250ms sample tick while recording.
95
+ "MiniMetric", "MetricRow", "MetricNumber", "RecColumn", "PillBody", "FullBody", "TargetLine", "PagesSection", "ProbesSection"]);
96
+
97
+ /**
98
+ * True when a name belongs to the perf-monitor UI. These stay excluded
99
+ * even in bench "profile our own tools" mode — the HUD re-renders on
100
+ * every 250ms sample tick while recording, so both capture results and
101
+ * live highlight boxes would be dominated by it.
102
+ */
103
+ function isPerfMonitorComponentName(name) {
104
+ return name.startsWith("PerfMonitor") || name.startsWith("PerfHud") || PERF_MONITOR_ALWAYS_EXCLUDED.has(name);
105
+ }
106
+
107
+ /**
108
+ * True when a component display name belongs to Buoy's own devtools UI (or
109
+ * RN's LogBox). Sets are checked first (O(1)), then prefixes.
110
+ */
111
+ function isDevToolsComponentName(name) {
112
+ // Always excluded — tracking these causes infinite render loops.
113
+ if (HIGHLIGHT_OVERLAY_COMPONENTS.has(name)) return true;
114
+ for (let i = 0; i < HIGHLIGHT_OVERLAY_PREFIXES.length; i++) {
115
+ if (name.startsWith(HIGHLIGHT_OVERLAY_PREFIXES[i])) return true;
116
+ }
117
+ if (captureBuoyToolRenders) {
118
+ // Benchmarking our own tools: capture everything except the
119
+ // perf-monitor UI (its 250ms HUD churn would drown the result).
120
+ return isPerfMonitorComponentName(name);
121
+ }
122
+ if (OTHER_DEV_TOOLS_COMPONENTS.has(name)) return true;
123
+ for (let i = 0; i < OTHER_DEV_TOOLS_PREFIXES.length; i++) {
124
+ if (name.startsWith(OTHER_DEV_TOOLS_PREFIXES[i])) return true;
125
+ }
126
+ return false;
127
+ }