From 60287ff252c50d59eb70b03da24f00433c509d86 Mon Sep 17 00:00:00 2001 From: Pavel Ivanov Date: Sun, 2 Feb 2025 03:46:59 +0200 Subject: [PATCH] improve component tree --- packages/scan/src/core/instrumentation.ts | 1 + packages/scan/src/new-outlines/index.ts | 4 +- .../src/web/components/inspector/index.tsx | 2 + .../components/inspector/overlay/index.tsx | 1 + .../src/web/components/inspector/states.ts | 121 ++- .../src/web/components/inspector/utils.ts | 59 +- .../widget/components-tree/breadcrumb.tsx | 2 +- .../widget/components-tree/index.tsx | 776 ++++++++++-------- .../widget/components-tree/state.ts | 11 +- .../src/web/components/widget/fps-meter.tsx | 2 +- .../scan/src/web/components/widget/header.tsx | 38 +- .../web/components/widget/resize-handle.tsx | 20 +- .../web/components/widget/toolbar/arrows.tsx | 328 -------- packages/scan/src/web/utils/helpers.ts | 81 +- packages/scan/src/web/utils/pin.ts | 57 +- 15 files changed, 630 insertions(+), 873 deletions(-) delete mode 100644 packages/scan/src/web/components/widget/toolbar/arrows.tsx diff --git a/packages/scan/src/core/instrumentation.ts b/packages/scan/src/core/instrumentation.ts index fe302ec2..8399d166 100644 --- a/packages/scan/src/core/instrumentation.ts +++ b/packages/scan/src/core/instrumentation.ts @@ -521,6 +521,7 @@ export const createInstrumentation = ( ), ); } + const { selfTime } = getTimings(fiber); const fps = getFPS(); diff --git a/packages/scan/src/new-outlines/index.ts b/packages/scan/src/new-outlines/index.ts index 966e4e9e..6715f117 100644 --- a/packages/scan/src/new-outlines/index.ts +++ b/packages/scan/src/new-outlines/index.ts @@ -472,9 +472,7 @@ export const initReactScanInstrumentation = () => { } if (Store.inspectState.value.kind === 'focused') { - if (isCompositeFiber(fiber)) { - inspectorUpdateSignal.value = Date.now(); - } + inspectorUpdateSignal.value = Date.now(); } ReactScanInternals.options.value.onRender?.(fiber, renders); diff --git a/packages/scan/src/web/components/inspector/index.tsx b/packages/scan/src/web/components/inspector/index.tsx index 9de99a06..909b64b4 100644 --- a/packages/scan/src/web/components/inspector/index.tsx +++ b/packages/scan/src/web/components/inspector/index.tsx @@ -111,6 +111,7 @@ export const Inspector = constant(() => { const { parentCompositeFiber } = getCompositeFiberFromElement( state.focusedDomElement, + state.fiber ); @@ -135,6 +136,7 @@ export const Inspector = constant(() => { const { parentCompositeFiber } = getCompositeFiberFromElement( inspectState.focusedDomElement, + inspectState.fiber ); if (!parentCompositeFiber) { diff --git a/packages/scan/src/web/components/inspector/overlay/index.tsx b/packages/scan/src/web/components/inspector/overlay/index.tsx index 9bb164b7..eab7b06d 100644 --- a/packages/scan/src/web/components/inspector/overlay/index.tsx +++ b/packages/scan/src/web/components/inspector/overlay/index.tsx @@ -11,6 +11,7 @@ import { import { signalIsSettingsOpen } from '~web/state'; import { cn, throttle } from '~web/utils/helpers'; import { lerp } from '~web/utils/lerp'; +import { getFiberPath } from '~web/utils/pin'; import { timelineState } from '../states'; type DrawKind = 'locked' | 'inspecting'; diff --git a/packages/scan/src/web/components/inspector/states.ts b/packages/scan/src/web/components/inspector/states.ts index 1ad42945..ffb2fb91 100644 --- a/packages/scan/src/web/components/inspector/states.ts +++ b/packages/scan/src/web/components/inspector/states.ts @@ -23,33 +23,39 @@ export interface TimelineUpdate { } export interface TimelineState { - updates: TimelineUpdate[]; - currentIndex: number; - playbackSpeed: 1 | 2 | 4; + updates: Array; + currentFiber: Fiber | null; totalUpdates: number; - isVisible: boolean; windowOffset: number; + currentIndex: number; isViewingHistory: boolean; latestFiber: Fiber | null; + isVisible: boolean; + playbackSpeed: 1 | 2 | 4; } export const TIMELINE_MAX_UPDATES = 1000; -const timelineStateDefault: TimelineState = { +export const timelineStateDefault: TimelineState = { updates: [], - currentIndex: 0, - playbackSpeed: 1, + currentFiber: null, totalUpdates: 0, - isVisible: false, windowOffset: 0, + currentIndex: 0, isViewingHistory: false, latestFiber: null, + isVisible: false, + playbackSpeed: 1, }; export const timelineState = signal(timelineStateDefault); export const inspectorUpdateSignal = signal(0); +// Add batching storage +let pendingUpdates: Array<{ update: TimelineUpdate; fiber: Fiber | null }> = []; +let batchTimeout: ReturnType | null = null; + export const timelineActions = { showTimeline: () => { timelineState.value = { @@ -82,49 +88,76 @@ export const timelineActions = { }, addUpdate: (update: TimelineUpdate, latestFiber: Fiber | null) => { - const { updates, totalUpdates, currentIndex, isViewingHistory } = - timelineState.value; - - const newUpdates = [...updates]; - const newTotalUpdates = totalUpdates + 1; - - if (newUpdates.length >= TIMELINE_MAX_UPDATES) { - newUpdates.shift(); - } - - newUpdates.push(update); - - const newWindowOffset = Math.max(0, newTotalUpdates - TIMELINE_MAX_UPDATES); + // Collect the update + pendingUpdates.push({ update, fiber: latestFiber }); + + if (!batchTimeout) { + // If no batch is scheduled, schedule one + batchTimeout = setTimeout(() => { + // Process all collected updates + const batchedUpdates = pendingUpdates; + pendingUpdates = []; + batchTimeout = null; + + // Apply all updates in the batch + const { updates, totalUpdates, currentIndex, isViewingHistory } = + timelineState.value; + const newUpdates = [...updates]; + let newTotalUpdates = totalUpdates; + + // Process each update in the batch + for (const { update } of batchedUpdates) { + if (newUpdates.length >= TIMELINE_MAX_UPDATES) { + newUpdates.shift(); + } + newUpdates.push(update); + newTotalUpdates++; + } - let newCurrentIndex: number; - if (isViewingHistory) { - if (currentIndex === totalUpdates - 1) { - newCurrentIndex = newUpdates.length - 1; - } else if (currentIndex === 0) { - newCurrentIndex = 0; - } else { - if (newWindowOffset === 0) { - newCurrentIndex = currentIndex; + const newWindowOffset = Math.max( + 0, + newTotalUpdates - TIMELINE_MAX_UPDATES, + ); + + let newCurrentIndex: number; + if (isViewingHistory) { + if (currentIndex === totalUpdates - 1) { + newCurrentIndex = newUpdates.length - 1; + } else if (currentIndex === 0) { + newCurrentIndex = 0; + } else { + if (newWindowOffset === 0) { + newCurrentIndex = currentIndex; + } else { + newCurrentIndex = currentIndex - 1; + } + } } else { - newCurrentIndex = currentIndex - 1; + newCurrentIndex = newUpdates.length - 1; } - } - } else { - newCurrentIndex = newUpdates.length - 1; - } - timelineState.value = { - ...timelineState.value, - latestFiber, - updates: newUpdates, - totalUpdates: newTotalUpdates, - windowOffset: newWindowOffset, - currentIndex: newCurrentIndex, - isViewingHistory, - }; + // Get the latest fiber from the last update in batch + const lastUpdate = batchedUpdates[batchedUpdates.length - 1]; + + timelineState.value = { + ...timelineState.value, + latestFiber: lastUpdate.fiber, + updates: newUpdates, + totalUpdates: newTotalUpdates, + windowOffset: newWindowOffset, + currentIndex: newCurrentIndex, + isViewingHistory, + }; + }, 100); + } }, reset: () => { + if (batchTimeout) { + clearTimeout(batchTimeout); + batchTimeout = null; + } + pendingUpdates = []; timelineState.value = timelineStateDefault; }, }; diff --git a/packages/scan/src/web/components/inspector/utils.ts b/packages/scan/src/web/components/inspector/utils.ts index d68f5f2e..5faf9475 100644 --- a/packages/scan/src/web/components/inspector/utils.ts +++ b/packages/scan/src/web/components/inspector/utils.ts @@ -130,21 +130,22 @@ export const getNearestFiberFromElement = ( } }; -export const getParentCompositeFiber = (fiber: Fiber) => { - let curr: Fiber | null = fiber; - let prevHost = null; +export const getParentCompositeFiber = ( + fiber: Fiber, +): readonly [Fiber, Fiber | null] | null => { + let current: Fiber | null = fiber; + let prevHost: Fiber | null = null; - while (curr) { - if (isCompositeFiber(curr)) { - return [curr, prevHost] as const; - } - if (isHostFiber(curr)) { - prevHost = curr; - } - curr = curr.return; + while (current) { + if (isCompositeFiber(current)) return [current, prevHost] as const; + if (isHostFiber(current) && !prevHost) prevHost = current; + current = current.return; } + + return null; }; + const isFiberInTree = (fiber: Fiber, root: Fiber): boolean => { { // const root= fiberRootCache.get(fiber) || (fiber.alternate && fiberRootCache.get(fiber.alternate) ) @@ -221,28 +222,28 @@ export const getCompositeComponentFromElement = (element: Element) => { }; }; -export const getCompositeFiberFromElement = (element: Element) => { - const associatedFiber = getNearestFiberFromElement(element); +export const getCompositeFiberFromElement = ( + element: Element, + knownFiber?: Fiber, +) => { + if (!element.isConnected) return {}; - if (!associatedFiber) return {}; - const currentAssociatedFiber = isCurrentTree(associatedFiber) - ? associatedFiber - : (associatedFiber.alternate ?? associatedFiber); - const stateNode = getFirstStateNode(currentAssociatedFiber); - if (!stateNode) return {}; + let fiber = knownFiber ?? getNearestFiberFromElement(element); + if (!fiber) return {}; - const anotherRes = getParentCompositeFiber(currentAssociatedFiber); - if (!anotherRes) { - return {}; - } - let [parentCompositeFiber] = anotherRes; - parentCompositeFiber = - (isCurrentTree(parentCompositeFiber) - ? parentCompositeFiber - : parentCompositeFiber.alternate) ?? parentCompositeFiber; + // Get the current associated fiber + fiber = isCurrentTree(fiber) ? fiber : (fiber.alternate ?? fiber); + + if (!getFirstStateNode(fiber)) return {}; + + // Fetch the parent composite fiber efficiently + const parentCompositeFiber = getParentCompositeFiber(fiber)?.[0]; + if (!parentCompositeFiber) return {}; return { - parentCompositeFiber, + parentCompositeFiber: isCurrentTree(parentCompositeFiber) + ? parentCompositeFiber + : (parentCompositeFiber.alternate ?? parentCompositeFiber), }; }; diff --git a/packages/scan/src/web/components/widget/components-tree/breadcrumb.tsx b/packages/scan/src/web/components/widget/components-tree/breadcrumb.tsx index 74267904..fe54acda 100644 --- a/packages/scan/src/web/components/widget/components-tree/breadcrumb.tsx +++ b/packages/scan/src/web/components/widget/components-tree/breadcrumb.tsx @@ -111,7 +111,7 @@ export const Breadcrumb = ({ selectedElement }: { selectedElement: HTMLElement | + + + )} - - )} - - - )} + ) + : !!flattenedNodes.length && ( + + {flattenedNodes.length} + + ) + } @@ -963,7 +1023,7 @@ export const ComponentsTree = () => { diff --git a/packages/scan/src/web/components/widget/components-tree/state.ts b/packages/scan/src/web/components/widget/components-tree/state.ts index d952abce..9a4b4d18 100644 --- a/packages/scan/src/web/components/widget/components-tree/state.ts +++ b/packages/scan/src/web/components/widget/components-tree/state.ts @@ -16,15 +16,6 @@ export interface FlattenedNode extends TreeNode { fiber: Fiber; } -export const SEARCH_PREFIX_LENGTH = 3; - -export interface SearchIndex { - prefixMap: Map>; - nodeMap: Map; - labelMap: Map; - PREFIX_LENGTH: number; -} - export const searchState = signal<{ query: string; matches: FlattenedNode[]; @@ -42,4 +33,4 @@ export interface TreeItem { fiber: Fiber; } -export const signalSkipTreeUpdate = signal(false); +export const signalSkipTreeUpdate = signal(false); diff --git a/packages/scan/src/web/components/widget/fps-meter.tsx b/packages/scan/src/web/components/widget/fps-meter.tsx index eaaf6985..e94532e1 100644 --- a/packages/scan/src/web/components/widget/fps-meter.tsx +++ b/packages/scan/src/web/components/widget/fps-meter.tsx @@ -34,7 +34,7 @@ export const FpsMeter = () => { > diff --git a/packages/scan/src/web/components/widget/header.tsx b/packages/scan/src/web/components/widget/header.tsx index 95985134..8f7d1081 100644 --- a/packages/scan/src/web/components/widget/header.tsx +++ b/packages/scan/src/web/components/widget/header.tsx @@ -164,25 +164,31 @@ const HeaderInspect = () => { className="flex items-center gap-x-1" > {name ?? 'Unknown'} - + { !!firstWrapperType && ( - + + {firstWrapperType.type} + + {firstWrapperType.compiler && ( + )} - > - {firstWrapperType.type} - {firstWrapperType.compiler && '✦'} - + ) } diff --git a/packages/scan/src/web/components/widget/resize-handle.tsx b/packages/scan/src/web/components/widget/resize-handle.tsx index 1abecf82..5b572376 100644 --- a/packages/scan/src/web/components/widget/resize-handle.tsx +++ b/packages/scan/src/web/components/widget/resize-handle.tsx @@ -284,12 +284,26 @@ export const ResizeHandle = ({ position }: ResizeHandleProps) => { position: newPosition, }; + // Adjust components tree width when widget is resized + const maxTreeWidth = Math.floor(newWidth - (MIN_SIZE.width / 2)); + const currentTreeWidth = signalWidget.value.componentsTree.width; + const defaultWidth = Math.floor(newWidth * 0.3); // Use 30% of window width as default + + const newTreeWidth = isCurrentFullWidth + ? MIN_CONTAINER_WIDTH + : (position === 'left' || position === 'right') && !isCurrentFullWidth + ? Math.min(maxTreeWidth, Math.max(MIN_CONTAINER_WIDTH, defaultWidth)) + : Math.min(maxTreeWidth, Math.max(MIN_CONTAINER_WIDTH, currentTreeWidth)); + requestAnimationFrame(() => { signalWidget.value = { corner: newCorner, dimensions: newDimensions, lastDimensions: dimensions, - componentsTree: signalWidget.value.componentsTree, + componentsTree: { + ...signalWidget.value.componentsTree, + width: newTreeWidth, + }, }; containerStyle.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; @@ -302,6 +316,10 @@ export const ResizeHandle = ({ position }: ResizeHandleProps) => { corner: newCorner, dimensions: newDimensions, lastDimensions: dimensions, + componentsTree: { + ...signalWidget.value.componentsTree, + width: newTreeWidth, + }, }); }, []); diff --git a/packages/scan/src/web/components/widget/toolbar/arrows.tsx b/packages/scan/src/web/components/widget/toolbar/arrows.tsx deleted file mode 100644 index df2302a5..00000000 --- a/packages/scan/src/web/components/widget/toolbar/arrows.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import { getFiberId } from 'bippy'; -import type { Fiber } from 'bippy'; -import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; -import { Store } from '~core/index'; -import { Icon } from '~web/components/icon'; -import { - type InspectableElement, - getCompositeComponentFromElement, - getInspectableElements, -} from '~web/components/inspector/utils'; -import { useDelayedValue } from '~web/hooks/use-mount-delay'; -import { cn } from '~web/utils/helpers'; -import { constant } from '~web/utils/preact/constant'; -import { getBatchedRectMap } from '../../../../new-outlines'; - -interface FocusedState { - kind: 'focused'; - focusedDomElement: Element; - fiber: Fiber; -} - -type InspectingState = { - kind: 'inspecting'; - hoveredDomElement: Element | null; -}; - -type InspectOffState = { - kind: 'inspect-off'; -}; - -type UninitializedState = { - kind: 'uninitialized'; -}; - -type States = - | FocusedState - | InspectingState - | InspectOffState - | UninitializedState; - -export function isFocusedState(state: States): state is FocusedState { - return state.kind === 'focused'; -} - -export const Arrows = constant(() => { - const refButtonPrevious = useRef(null); - const refButtonNext = useRef(null); - const refAllElements = useRef>([]); - const refCurrentFiberId = useRef(null); - - const [shouldRender, setShouldRender] = useState(false); - const isMounted = useDelayedValue(shouldRender, 0, 1000); - - const findNextElement = useCallback( - async (currentElement: Element, direction: 'next' | 'previous') => { - const currentIndex = refAllElements.current.findIndex( - (item) => item.element === currentElement, - ); - if (currentIndex === -1) { - return null; - } - - const startIndex = currentIndex + (direction === 'next' ? 1 : -1); - - const elements = refAllElements.current; - const totalElements = elements.length; - - const BATCH_SIZE = 500; - - // this looks very convoluted, but we need to efficiently find the next - // selectable fiber, and because there may be hundreds of components that are not - // in viewport, or have 0 width/height in the neighboring indexes we need to search in large - // batches to avoid latency when the user clicks the next button - - if (direction === 'next') { - for ( - let batchStart = startIndex; - batchStart < totalElements; - batchStart += BATCH_SIZE - ) { - const batchEnd = Math.min(batchStart + BATCH_SIZE, totalElements); - const batchElements = elements - .slice(batchStart, batchEnd) - .map((item) => item.element); - - if (batchElements.length === 0) continue; - - const allEntries: IntersectionObserverEntry[] = []; - for await (const entries of getBatchedRectMap(batchElements)) { - allEntries.push(...entries); - } - - const entryMap = new Map( - allEntries.map((entry) => [entry.target, entry]), - ); - - for (let i = 0; i < batchElements.length; i++) { - const element = batchElements[i]; - if (!element) continue; - - const { parentCompositeFiber } = - getCompositeComponentFromElement(element); - if (!parentCompositeFiber) continue; - - const nextFiberId = getFiberId(parentCompositeFiber); - if (nextFiberId === refCurrentFiberId.current) continue; - - const entry = entryMap.get(element); - if ( - !entry || - !entry.isIntersecting || - entry.intersectionRect.width <= 0 || - entry.intersectionRect.height <= 0 - ) - continue; - - const rect = element.getBoundingClientRect(); - const isVisible = - rect.top < window.innerHeight && - rect.bottom > 0 && - rect.left < window.innerWidth && - rect.right > 0; - - if (!isVisible) continue; - - refCurrentFiberId.current = nextFiberId; - return element; - } - } - } else { - // For previous direction, process from current index to start - for (let batchEnd = startIndex; batchEnd >= 0; batchEnd -= BATCH_SIZE) { - const batchStart = Math.max(batchEnd - BATCH_SIZE + 1, 0); - const batchElements = elements - .slice(batchStart, batchEnd + 1) - .map((item) => item.element) - .reverse(); - - if (batchElements.length === 0) continue; - - const allEntries: IntersectionObserverEntry[] = []; - for await (const entries of getBatchedRectMap(batchElements)) { - allEntries.push(...entries); - } - - const entryMap = new Map( - allEntries.map((entry) => [entry.target, entry]), - ); - - for (let i = 0; i < batchElements.length; i++) { - const element = batchElements[i]; - if (!element) continue; - - const { parentCompositeFiber } = - getCompositeComponentFromElement(element); - if (!parentCompositeFiber) continue; - - const nextFiberId = getFiberId(parentCompositeFiber); - if (nextFiberId === refCurrentFiberId.current) continue; - - const entry = entryMap.get(element); - if ( - !entry || - !entry.isIntersecting || - entry.intersectionRect.width <= 0 || - entry.intersectionRect.height <= 0 - ) - continue; - - const rect = element.getBoundingClientRect(); - const isVisible = - rect.top < window.innerHeight && - rect.bottom > 0 && - rect.left < window.innerWidth && - rect.right > 0; - - if (!isVisible) continue; - - refCurrentFiberId.current = nextFiberId; - return element; - } - } - } - - return null; - }, - [], - ); - - const handleClick = async (direction: 'previous' | 'next') => { - const state = Store.inspectState.value; - if (state.kind !== 'focused') return; - - const nextElement = await findNextElement(state.focusedDomElement, direction); - if (!nextElement) return; - - const { parentCompositeFiber } = getCompositeComponentFromElement(nextElement); - if (!parentCompositeFiber) return; - - Store.inspectState.value = { - kind: 'focused', - focusedDomElement: nextElement, - fiber: parentCompositeFiber, - }; - }; - - useEffect(() => { - const unsubscribe = Store.inspectState.subscribe(async (state) => { - if ( - state.kind === 'focused' && - refButtonPrevious.current && - refButtonNext.current - ) { - refAllElements.current = getInspectableElements(); - - const fiber = state.fiber; - if (fiber) { - const currentFiberId = getFiberId(fiber); - refCurrentFiberId.current = currentFiberId; - } - - const hasPrevious = !!(await findNextElement( - state.focusedDomElement, - 'previous', - )); - if (!refButtonPrevious.current || !refButtonNext.current) { - // handle the ref unmounting before the awaited code (us) - return; - } - refButtonPrevious.current.disabled = !hasPrevious; - refButtonPrevious.current.classList.toggle('opacity-50', !hasPrevious); - refButtonPrevious.current.classList.toggle( - 'cursor-not-allowed', - !hasPrevious, - ); - - const hasNext = !!(await findNextElement( - state.focusedDomElement, - 'next', - )); - refButtonNext.current.disabled = !hasNext; - refButtonNext.current.classList.toggle('opacity-50', !hasNext); - refButtonNext.current.classList.toggle('cursor-not-allowed', !hasNext); - - setShouldRender(true); - } - - if ( - state.kind === 'inspecting' && - refButtonPrevious.current && - refButtonNext.current - ) { - refButtonPrevious.current.disabled = true; - refButtonPrevious.current.classList.toggle('opacity-50', true); - refButtonPrevious.current.classList.toggle('cursor-not-allowed', true); - refButtonNext.current.disabled = true; - refButtonNext.current.classList.toggle('opacity-50', true); - refButtonNext.current.classList.toggle('cursor-not-allowed', true); - setShouldRender(true); - } - - if (state.kind === 'inspect-off') { - refAllElements.current = []; - refCurrentFiberId.current = null; - setShouldRender(false); - } - - if (state.kind === 'uninitialized') { - - Store.inspectState.value = { - kind: 'inspect-off', - }; - } - }); - - return () => { - unsubscribe(); - }; - }, [findNextElement]); - - return ( -
- - -
- ); -}); diff --git a/packages/scan/src/web/utils/helpers.ts b/packages/scan/src/web/utils/helpers.ts index 51289407..d3f05f6a 100644 --- a/packages/scan/src/web/utils/helpers.ts +++ b/packages/scan/src/web/utils/helpers.ts @@ -5,6 +5,7 @@ import { SimpleMemoComponentTag, SuspenseComponentTag, getDisplayName, + hasMemoCache, } from 'bippy'; import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; @@ -111,68 +112,33 @@ export const getExtendedDisplayName = (fiber: Fiber): ExtendedDisplayName => { }; } - const { tag, type } = fiber; - + const { tag, type, elementType } = fiber; let name = getDisplayName(type); const wrappers: Array = []; - - // Check for wrapped components like Foo(Bar(Component)) - // Match the outermost wrapper first, then work inwards - while (name && /^(\w+)\((.*)\)$/.test(name)) { - const wrapper = name.match(/^(\w+)\(/)?.[1]; - if (wrapper) { - wrappers.unshift(wrapper); // Add to start of array to maintain order - name = name.slice(wrapper.length + 1, -1); // Remove wrapper and parentheses - } - } - const wrapperTypes: Array = []; - // Process wrappers in order they appear in the displayName - for (const wrapper of wrappers) { - if (wrapper.toLowerCase().includes('memo')) { - wrapperTypes.push({ - type: 'memo', - title: 'Memoized component that skips re-renders if props are the same', - compiler: false, - }); - } else if (wrapper.toLowerCase().includes('forwardref')) { - wrapperTypes.push({ - type: 'forwardRef', - title: - 'Component that can forward refs to DOM elements or other components', - }); - } - } - - // Check for React compiler auto-memoization - if ( - typeof type === 'function' && - type !== null && - '_automaticMemoized' in type - ) { - wrapperTypes.push({ - type: 'memo', - title: 'This component has been auto-memoized by the React Compiler', - compiler: true, - }); - } - - // Add wrappers based on fiber tags if ( - (tag === SimpleMemoComponentTag || tag === MemoComponentTag) && - !wrapperTypes.some((w) => w.type === 'memo' && !w.compiler) + hasMemoCache(fiber) || + tag === SimpleMemoComponentTag || + tag === MemoComponentTag || + (type as { $$typeof?: symbol })?.$$typeof === Symbol.for('react.memo') || + (elementType as { $$typeof?: symbol })?.$$typeof === + Symbol.for('react.memo') ) { + const compiler = hasMemoCache(fiber); wrapperTypes.push({ type: 'memo', - title: 'Memoized component that skips re-renders if props are the same', - compiler: false, + title: compiler + ? 'This component has been auto-memoized by the React Compiler.' + : 'Memoized component that skips re-renders if props are the same', + compiler, }); } if ( - tag === ForwardRefTag && - !wrapperTypes.some((w) => w.type === 'forwardRef') + tag === ForwardRefTag || + (type as { $$typeof?: symbol })?.$$typeof === + Symbol.for('react.forward_ref') ) { wrapperTypes.push({ type: 'forwardRef', @@ -202,6 +168,21 @@ export const getExtendedDisplayName = (fiber: Fiber): ExtendedDisplayName => { }); } + if (typeof name === 'string') { + const wrapperRegex = /^(\w+)\((.*)\)$/; + let currentName = name; + while (wrapperRegex.test(currentName)) { + const match = currentName.match(wrapperRegex); + if (match?.[1] && match?.[2]) { + wrappers.unshift(match[1]); + currentName = match[2]; + } else { + break; + } + } + name = currentName; + } + return { name: name || 'Unknown', wrappers, diff --git a/packages/scan/src/web/utils/pin.ts b/packages/scan/src/web/utils/pin.ts index 1169a86a..53754442 100644 --- a/packages/scan/src/web/utils/pin.ts +++ b/packages/scan/src/web/utils/pin.ts @@ -14,6 +14,29 @@ export interface FiberMetadata { const metadata = readLocalStorage('react-scann-pinned'); +export const getFiberPath = (fiber: Fiber): string => { + const pathSegments: string[] = []; + let currentFiber: Fiber | null = fiber; + + while (currentFiber) { + const elementType = currentFiber.elementType; + const name = + typeof elementType === 'function' + ? elementType.displayName || elementType.name + : typeof elementType === 'string' + ? elementType + : 'Unknown'; + + const index = + currentFiber.index !== undefined ? `[${currentFiber.index}]` : ''; + pathSegments.unshift(`${name}${index}`); + + currentFiber = currentFiber.return ?? null; + } + + return pathSegments.join('::'); +}; + export const getFiberMetadata = (fiber: Fiber): FiberMetadata | null => { if (!fiber || !fiber.elementType) return null; @@ -35,19 +58,7 @@ export const getFiberMetadata = (fiber: Fiber): FiberMetadata | null => { parentFiber = parentFiber.return; } - const pathSegments: string[] = []; - let currentFiber: Fiber | null = fiber; - - while (currentFiber) { - if (currentFiber.elementType?.name) { - const index = - currentFiber.index !== undefined ? `[${currentFiber.index}]` : ''; - pathSegments.unshift(`${currentFiber.elementType.name}${index}`); - } - currentFiber = currentFiber.return ?? null; - } - - const path = pathSegments.join('::'); + const path = getFiberPath(fiber); const propKeys = fiber.pendingProps ? Object.keys(fiber.pendingProps).filter((key) => key !== 'children') @@ -56,24 +67,6 @@ export const getFiberMetadata = (fiber: Fiber): FiberMetadata | null => { return { componentName, parent, position, sibling, path, propKeys }; }; -const reconstructPath = (fiber: Fiber): string => { - const pathSegments: string[] = []; - let currentFiber = fiber; - - while (currentFiber) { - if (currentFiber.elementType?.name) { - const index = - currentFiber.index !== undefined ? `[${currentFiber.index}]` : ''; - pathSegments.unshift(`${currentFiber.elementType.name}${index}`); - } - const nextFiber = currentFiber.return; - if (!nextFiber) break; - currentFiber = nextFiber; - } - - return pathSegments.join('::'); -}; - const checkFiberMatch = (fiber: Fiber | undefined): boolean => { if (!fiber || !fiber.elementType || !metadata?.componentName) return false; @@ -93,7 +86,7 @@ const checkFiberMatch = (fiber: Fiber | undefined): boolean => { if (parent !== metadata.parent) return false; if (fiber.index !== metadata.position) return false; - const fiberPath = reconstructPath(fiber); + const fiberPath = getFiberPath(fiber); return fiberPath === metadata.path; };