From 76dddd1d574b2d60299453cccc2589139bb22b51 Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Acosta Date: Tue, 13 May 2025 16:05:41 -0700 Subject: [PATCH] Port complete --- .../packages/react-mcp-server/src/index.ts | 2 +- .../src/tools/componentTree.ts | 59 +- .../src/utils/reactDevTools.ts | 399 ---- .../reactDevTools/extractComponentTree.ts | 310 +++ .../src/utils/reactDevTools/reactDevTools.ts | 1785 +++++++++++++++++ .../reactDevTools/reactDevToolsConstants.ts | 22 + .../reactDevTools/reactDevToolsSymbols.ts | 32 + .../utils/reactDevTools/reactDevToolsTypes.ts | 305 +++ .../utils/reactDevTools/reactDevToolsUtils.ts | 608 ++++++ 9 files changed, 3078 insertions(+), 444 deletions(-) delete mode 100644 compiler/packages/react-mcp-server/src/utils/reactDevTools.ts create mode 100644 compiler/packages/react-mcp-server/src/utils/reactDevTools/extractComponentTree.ts create mode 100644 compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevTools.ts create mode 100644 compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsConstants.ts create mode 100644 compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsSymbols.ts create mode 100644 compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsTypes.ts create mode 100644 compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsUtils.ts diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index 4f3ed755ae..443e72811b 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -369,7 +369,7 @@ server.tool( text: z.string(), }, async ({text}) => { - const componentTree = await parseReactComponentTree(text); + const componentTree = await parseReactComponentTree(); return { content: [ diff --git a/compiler/packages/react-mcp-server/src/tools/componentTree.ts b/compiler/packages/react-mcp-server/src/tools/componentTree.ts index 0dc2089d4f..7f6e5090fd 100644 --- a/compiler/packages/react-mcp-server/src/tools/componentTree.ts +++ b/compiler/packages/react-mcp-server/src/tools/componentTree.ts @@ -1,13 +1,6 @@ -import * as babel from '@babel/core'; import puppeteer from 'puppeteer'; -import {readFileSync} from 'fs'; -import * as path from 'path'; -// @ts-ignore -import * as babelPresetTypescript from '@babel/preset-typescript'; -// @ts-ignore -import * as babelPresetEnv from '@babel/preset-env'; -// @ts-ignore -import * as babelPresetReact from '@babel/preset-react'; +import extractComponentTreeFromDevTools from '../utils/reactDevTools/extractComponentTree'; +// import {generateComponentTree} from '../utils/reactDevTools/reactDevTools'; function delay(time: number) { return new Promise(resolve => { @@ -15,7 +8,7 @@ function delay(time: number) { }); } -export async function parseReactComponentTree(code: string): Promise { +export async function parseReactComponentTree(): Promise { const browser = await puppeteer.connect({ browserURL: 'http://127.0.0.1:9222', defaultViewport: null, @@ -27,15 +20,23 @@ export async function parseReactComponentTree(code: string): Promise { for (const page of pages) { const url = await page.url(); - if (url.startsWith('https://react.dev')) { + if (url.startsWith('http://localhost:3000')) { localhostPage = page; break; } } if (localhostPage) { - const devtoolsHook = await localhostPage.evaluate(getReactComponentTree); - console.log(devtoolsHook); + const devtoolsHook = await localhostPage.evaluate( + () => (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__, + ); + + try { + } catch (error) { + console.error(error); + } + + extractComponentTreeFromDevTools(devtoolsHook); return new Promise(resolve => resolve(JSON.stringify(devtoolsHook))); } else { @@ -43,36 +44,6 @@ export async function parseReactComponentTree(code: string): Promise { } } -function getReactComponentTree() { - // Check if the React DevTools hook is available - const hook: any = (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; - if (!hook) { - console.error( - 'React DevTools hook is not available. Make sure React DevTools extension is installed.', - ); - return null; - } - - // Get the first renderer from the DevTools hook - const renderers: any = Array.from(hook.renderers.values()); - - // return renderers; - - if (renderers.length === 0) { - console.error('No React renderers found.'); - return null; - } - - const rootFiber = Array.from( - (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.getFiberRoots(1), - )[0]; - - //ROOT FIBER - if (!rootFiber) { - return 'error'; - } -} - -parseReactComponentTree('') +parseReactComponentTree() .then(result => console.log(result)) .catch(error => console.error(error)); diff --git a/compiler/packages/react-mcp-server/src/utils/reactDevTools.ts b/compiler/packages/react-mcp-server/src/utils/reactDevTools.ts deleted file mode 100644 index 4eb533ecd4..0000000000 --- a/compiler/packages/react-mcp-server/src/utils/reactDevTools.ts +++ /dev/null @@ -1,399 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -// Define ReactComponentInfo type directly to avoid import issues -type ReactComponentInfo = { - name: string; - [key: string]: any; -}; - -// Constants for instance types -const FIBER_INSTANCE = 0; -const FILTERED_FIBER_INSTANCE = 2; -const VIRTUAL_INSTANCE = 1; - -// Only keeping the constants needed for tree traversal - -// Fiber tags - only keeping the ones used in getDisplayNameForFiber -const FunctionComponent = 0; -const ClassComponent = 1; -const IndeterminateComponent = 2; -const HostRoot = 3; -const HostPortal = 4; -const HostComponent = 5; -const HostText = 6; -const Fragment = 7; -const ForwardRef = 11; -const Profiler = 12; -const SuspenseComponent = 13; -const MemoComponent = 14; -const SimpleMemoComponent = 15; -const LazyComponent = 16; -const IncompleteClassComponent = 17; -const SuspenseListComponent = 19; -const ScopeComponent = 21; -const OffscreenComponent = 22; -const LegacyHiddenComponent = 23; -const CacheComponent = 24; -const TracingMarkerComponent = 25; -const HostHoistable = 26; -const HostSingleton = 27; -const IncompleteFunctionComponent = 28; -const Throw = 29; -const ViewTransitionComponent = 30; -const ActivityComponent = 31; - -// Symbol constants -const CONCURRENT_MODE_NUMBER = 0xeacf; -const CONCURRENT_MODE_SYMBOL_STRING = 'Symbol(react.concurrent_mode)'; -const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = 'Symbol(react.async_mode)'; -const PROVIDER_NUMBER = 0xeacd; -const PROVIDER_SYMBOL_STRING = 'Symbol(react.provider)'; -const CONTEXT_NUMBER = 0xeace; -const CONTEXT_SYMBOL_STRING = 'Symbol(react.context)'; -const SERVER_CONTEXT_SYMBOL_STRING = 'Symbol(react.server_context)'; -const CONSUMER_SYMBOL_STRING = 'Symbol(react.consumer)'; -const STRICT_MODE_NUMBER = 0xeacc; -const STRICT_MODE_SYMBOL_STRING = 'Symbol(react.strict_mode)'; -const PROFILER_NUMBER = 0xead2; -const PROFILER_SYMBOL_STRING = 'Symbol(react.profiler)'; -const SCOPE_NUMBER = 0xead7; -const SCOPE_SYMBOL_STRING = 'Symbol(react.scope)'; -// Define a string key for REACT_MEMO_CACHE_SENTINEL to fix type issues -const REACT_MEMO_CACHE_SENTINEL = 'REACT_MEMO_CACHE_SENTINEL'; - -// Type definitions -type Fiber = { - tag: number; - elementType: any; - type: any; - stateNode: any; - memoizedProps: any; - memoizedState: any; - updateQueue: any; - key: string | null; - _debugID?: number; - _debugOwner?: any; - _debugStack?: any; - child: Fiber | null; - sibling: Fiber | null; -}; - -type Source = { - fileName: string; - lineNumber: number; - columnNumber?: number; -}; - -type FiberInstance = { - kind: 0; - id: number; - parent: null | DevToolsInstance; - firstChild: null | DevToolsInstance; - nextSibling: null | DevToolsInstance; - source: null | string | Error | Source; - logCount: number; - treeBaseDuration: number; - data: Fiber; -}; - -type FilteredFiberInstance = { - kind: 2; - id: number; - parent: null | DevToolsInstance; - firstChild: null | DevToolsInstance; - nextSibling: null | DevToolsInstance; - source: null | string | Error | Source; - logCount: number; - treeBaseDuration: number; - data: Fiber; -}; - -type VirtualInstance = { - kind: 1; - id: number; - parent: null | DevToolsInstance; - firstChild: null | DevToolsInstance; - nextSibling: null | DevToolsInstance; - source: null | string | Error | Source; - logCount: number; - treeBaseDuration: number; - data: ReactComponentInfo; -}; - -type DevToolsInstance = FiberInstance | FilteredFiberInstance | VirtualInstance; - -// Helper functions -function getUID(): number { - return Math.floor(Math.random() * 1000000); -} - -function getTypeSymbol(type: any): symbol | number | string | null { - const symbolOrNumber = - typeof type === 'object' && type !== null ? type.$$typeof : null; - return symbolOrNumber; -} - -function resolveFiberType(type: any): any { - return type.type || type; -} - -function getDisplayName(type: any): string | null { - if (type == null) { - return null; - } - - let displayName = null; - if (typeof type.displayName === 'string') { - displayName = type.displayName; - } else if (typeof type.name === 'string' && type.name !== '') { - displayName = type.name; - } - - return displayName || 'Anonymous'; -} - -function getWrappedDisplayName( - outerType: any, - innerType: any, - wrapperName: string, - fallbackName: string, -): string { - const displayName = getDisplayName(innerType); - return displayName ? `${wrapperName}(${displayName})` : fallbackName; -} - -// Instance creation functions -function createFiberInstance(fiber: Fiber): FiberInstance { - return { - kind: FIBER_INSTANCE, - id: getUID(), - parent: null, - firstChild: null, - nextSibling: null, - source: null, - logCount: 0, - treeBaseDuration: 0, - data: fiber, - }; -} - -function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance { - return { - kind: FILTERED_FIBER_INSTANCE, - id: 0, - parent: null, - firstChild: null, - nextSibling: null, - source: null, - logCount: 0, - treeBaseDuration: 0, - data: fiber, - }; -} - -function createVirtualInstance( - debugEntry: ReactComponentInfo, -): VirtualInstance { - return { - kind: VIRTUAL_INSTANCE, - id: getUID(), - parent: null, - firstChild: null, - nextSibling: null, - source: null, - logCount: 0, - treeBaseDuration: 0, - data: debugEntry, - }; -} - -// Main functions for tree traversal -function getDisplayNameForFiber( - fiber: Fiber, - shouldSkipForgetCheck: boolean = false, -): string | null { - const {elementType, type, tag} = fiber; - - let resolvedType = type; - if (typeof type === 'object' && type !== null) { - resolvedType = resolveFiberType(type); - } - - let resolvedContext: any = null; - if ( - !shouldSkipForgetCheck && - (fiber.updateQueue?.memoCache != null || - (Array.isArray(fiber.memoizedState?.memoizedState) && - fiber.memoizedState.memoizedState[0]?.[REACT_MEMO_CACHE_SENTINEL]) || - fiber.memoizedState?.memoizedState?.[REACT_MEMO_CACHE_SENTINEL]) - ) { - const displayNameWithoutForgetWrapper = getDisplayNameForFiber(fiber, true); - if (displayNameWithoutForgetWrapper == null) { - return null; - } - - return `Forget(${displayNameWithoutForgetWrapper})`; - } - - switch (tag) { - case ActivityComponent: - return 'Activity'; - case CacheComponent: - return 'Cache'; - case ClassComponent: - case IncompleteClassComponent: - case IncompleteFunctionComponent: - case FunctionComponent: - case IndeterminateComponent: - return getDisplayName(resolvedType); - case ForwardRef: - return getWrappedDisplayName( - elementType, - resolvedType, - 'ForwardRef', - 'Anonymous', - ); - case HostRoot: - const fiberRoot = fiber.stateNode; - if (fiberRoot != null && fiberRoot._debugRootType !== null) { - return fiberRoot._debugRootType; - } - return null; - case HostComponent: - case HostSingleton: - case HostHoistable: - return type; - case HostPortal: - case HostText: - return null; - case Fragment: - return 'Fragment'; - case LazyComponent: - return 'Lazy'; - case MemoComponent: - case SimpleMemoComponent: - return getWrappedDisplayName( - elementType, - resolvedType, - 'Memo', - 'Anonymous', - ); - case SuspenseComponent: - return 'Suspense'; - case LegacyHiddenComponent: - return 'LegacyHidden'; - case OffscreenComponent: - return 'Offscreen'; - case ScopeComponent: - return 'Scope'; - case SuspenseListComponent: - return 'SuspenseList'; - case Profiler: - return 'Profiler'; - case TracingMarkerComponent: - return 'TracingMarker'; - case ViewTransitionComponent: - return 'ViewTransition'; - case Throw: - return 'Error'; - default: - const typeSymbol = getTypeSymbol(type); - - switch (typeSymbol) { - case CONCURRENT_MODE_NUMBER: - case CONCURRENT_MODE_SYMBOL_STRING: - case DEPRECATED_ASYNC_MODE_SYMBOL_STRING: - return null; - case PROVIDER_NUMBER: - case PROVIDER_SYMBOL_STRING: - resolvedContext = fiber.type._context || fiber.type.context; - return `${resolvedContext.displayName || 'Context'}.Provider`; - case CONTEXT_NUMBER: - case CONTEXT_SYMBOL_STRING: - case SERVER_CONTEXT_SYMBOL_STRING: - if ( - fiber.type._context === undefined && - fiber.type.Provider === fiber.type - ) { - resolvedContext = fiber.type; - return `${resolvedContext.displayName || 'Context'}.Provider`; - } - resolvedContext = fiber.type._context || fiber.type; - return `${resolvedContext.displayName || 'Context'}.Consumer`; - case CONSUMER_SYMBOL_STRING: - resolvedContext = fiber.type._context; - return `${resolvedContext.displayName || 'Context'}.Consumer`; - case STRICT_MODE_NUMBER: - case STRICT_MODE_SYMBOL_STRING: - return null; - case PROFILER_NUMBER: - case PROFILER_SYMBOL_STRING: - return `Profiler(${fiber.memoizedProps.id})`; - case SCOPE_NUMBER: - case SCOPE_SYMBOL_STRING: - return 'Scope'; - default: - return null; - } - } -} - -function debugTree(instance: DevToolsInstance, indent: number = 0) { - const name = - (instance.kind !== VIRTUAL_INSTANCE - ? getDisplayNameForFiber(instance.data) - : instance.data.name) || ''; - console.log( - ' '.repeat(indent) + - '- ' + - (instance.kind === FILTERED_FIBER_INSTANCE ? 0 : instance.id) + - ' (' + - name + - ')', - 'parent', - instance.parent === null - ? ' ' - : instance.parent.kind === FILTERED_FIBER_INSTANCE - ? 0 - : instance.parent.id, - 'next', - instance.nextSibling === null ? ' ' : instance.nextSibling.id, - ); - let child = instance.firstChild; - while (child !== null) { - debugTree(child, indent + 1); - child = child.nextSibling; - } -} - -// Function to insert a child into the tree -function insertChild(child: DevToolsInstance) { - if (child.parent !== null) { - throw new Error('Child already has a parent'); - } - - child.parent = null; - child.nextSibling = null; - - return child; -} - -// Export the functions needed for tree traversal -export { - debugTree, - getDisplayNameForFiber, - createFiberInstance, - createFilteredFiberInstance, - createVirtualInstance, - insertChild, - FIBER_INSTANCE, - FILTERED_FIBER_INSTANCE, - VIRTUAL_INSTANCE, -}; diff --git a/compiler/packages/react-mcp-server/src/utils/reactDevTools/extractComponentTree.ts b/compiler/packages/react-mcp-server/src/utils/reactDevTools/extractComponentTree.ts new file mode 100644 index 0000000000..4c6d83d575 --- /dev/null +++ b/compiler/packages/react-mcp-server/src/utils/reactDevTools/extractComponentTree.ts @@ -0,0 +1,310 @@ +import {generateComponentTree} from './reactDevTools'; +import {Fiber} from './reactDevToolsTypes'; + +/** + * Interface representing a node in the component tree + */ +export interface ComponentTreeNode { + id: number; + name: string; + type: string | number; + key: string | null; + children: ComponentTreeNode[]; + props?: Record | undefined; + state?: Record | undefined; + fiber?: Fiber | undefined; +} + +/** + * Extracts the component tree from the React DevTools implementation + * @param renderer The React renderer + * @param fiber The root fiber to start from + * @returns The component tree as a structured object + */ +export function extractComponentTree( + renderer: any, + fiber: Fiber, +): ComponentTreeNode | null { + const result = generateComponentTree(renderer, fiber, false); + + const idToDevToolsInstanceMap = (result as any).idToDevToolsInstanceMap; + const currentRoot = (result as any).currentRoot; + + if (!currentRoot || !idToDevToolsInstanceMap) { + console.error( + 'Failed to extract component tree - internal structures not available', + ); + return null; + } + + return convertDevToolsInstanceToTreeNode(currentRoot); + + /** + * Helper function to convert a DevTools instance to our tree node format + */ + function convertDevToolsInstanceToTreeNode(instance: any): ComponentTreeNode { + // Extract name based on instance kind + let name = 'Unknown'; + let type: string | number = 'Unknown'; + let key: string | null = null; + let props: Record | undefined = undefined; + let state: Record | undefined = undefined; + let fiberData: Fiber | undefined = undefined; + + if (instance.kind === 0) { + // FIBER_INSTANCE + const fiber = instance.data; + fiberData = fiber; + + // @ts-ignore - Accessing private function + const getDisplayNameForFiber = (result as any).getDisplayNameForFiber; + if (typeof getDisplayNameForFiber === 'function') { + name = getDisplayNameForFiber(fiber) || 'Unknown'; + } else { + // Fallback to type name if available + name = + fiber.type?.displayName || + fiber.type?.name || + (typeof fiber.type === 'string' ? fiber.type : 'Unknown'); + } + + // Get element type + // @ts-ignore - Accessing private function + const getElementTypeForFiber = (result as any).getElementTypeForFiber; + if (typeof getElementTypeForFiber === 'function') { + type = getElementTypeForFiber(fiber); + } + + // Get key + key = fiber.key !== null ? String(fiber.key) : null; + + // Get props and state if available + if (fiber.memoizedProps) { + props = {...fiber.memoizedProps}; + } + + if (fiber.memoizedState) { + // For hooks, this might be complex, so we'll just indicate it exists + state = + typeof fiber.memoizedState === 'object' + ? {...fiber.memoizedState} + : {value: fiber.memoizedState}; + } + } else if (instance.kind === 1) { + // VIRTUAL_INSTANCE + name = instance.data.name || 'Virtual'; + type = 'Virtual'; + key = instance.data.key != null ? String(instance.data.key) : null; + + // Add environment if it exists + if (instance.data.env) { + props = {env: instance.data.env}; + } + } else if (instance.kind === 2) { + // FILTERED_FIBER_INSTANCE + name = 'Filtered'; + type = 'Filtered'; + } + + // Create the node + const node: ComponentTreeNode = { + id: instance.id, + name, + type, + key, + children: [], + props, + state: state, + fiber: fiberData, + }; + + // Add children recursively + let child = instance.firstChild; + while (child) { + node.children.push(convertDevToolsInstanceToTreeNode(child)); + child = child.nextSibling; + } + + return node; + } +} + +/** + * Formats the component tree as a string + * @param tree The component tree to format + * @param options Options for formatting + * @returns A formatted string representation of the component tree + */ +export function formatComponentTree( + tree: ComponentTreeNode | null, + options: { + maxDepth?: number; + showIds?: boolean; + showProps?: boolean; + showState?: boolean; + } = {}, +): string { + if (!tree) { + return 'No component tree available'; + } + + const { + maxDepth = Infinity, + showIds = true, + showProps = false, + showState = false, + } = options; + + const lines: string[] = ['React Component Tree:']; + + function formatNode(node: ComponentTreeNode, depth: number = 0) { + if (depth > maxDepth) return; + + const indent = ' '.repeat(depth); + let output = `${indent}${node.name}`; + + if (node.key) { + output += ` key="${node.key}"`; + } + + if (showIds) { + output += ` (id: ${node.id})`; + } + + lines.push(output); + + // Format props if requested + if (showProps && node.props) { + const propsStr = JSON.stringify(node.props, null, 2) + .split('\n') + .map(line => `${indent} ${line}`) + .join('\n'); + lines.push(`${indent} Props: ${propsStr}`); + } + + // Format state if requested + if (showState && node.state) { + const stateStr = JSON.stringify(node.state, null, 2) + .split('\n') + .map(line => `${indent} ${line}`) + .join('\n'); + lines.push(`${indent} State: ${stateStr}`); + } + + // Format children recursively + for (const child of node.children) { + formatNode(child, depth + 1); + } + } + + formatNode(tree); + return lines.join('\n'); +} + +/** + * Prints the component tree to the console + * @param tree The component tree to print + * @param options Options for printing + */ +export function printExtractedComponentTree( + tree: ComponentTreeNode | null, + options: { + maxDepth?: number; + showIds?: boolean; + showProps?: boolean; + showState?: boolean; + } = {}, +) { + const formattedTree = formatComponentTree(tree, options); + console.log(formattedTree); +} + +/** + * Helper function to extract and format the component tree in one step + * @param renderer The React renderer + * @param fiber The root fiber + * @param options Options for formatting + * @returns An object containing the tree and its formatted string representation + */ +export function extractAndFormatComponentTree( + renderer: any, + fiber: Fiber, + options: { + maxDepth?: number; + showIds?: boolean; + showProps?: boolean; + showState?: boolean; + } = {}, +) { + const tree = extractComponentTree(renderer, fiber); + const formattedTree = formatComponentTree(tree, options); + return { + tree, + formattedTree, + }; +} + +/** + * Helper function to extract and print the component tree in one step + * @param renderer The React renderer + * @param fiber The root fiber + * @param options Options for printing + * @returns The extracted component tree + */ +export function extractAndPrintComponentTree( + renderer: any, + fiber: Fiber, + options: { + maxDepth?: number; + showIds?: boolean; + showProps?: boolean; + showState?: boolean; + } = {}, +) { + const {tree, formattedTree} = extractAndFormatComponentTree( + renderer, + fiber, + options, + ); + console.log(formattedTree); + return tree; +} + +/** + * Helper function to extract the component tree from the React DevTools hook + * @param options Options for formatting + * @returns An object containing the tree and its formatted string representation, or null if extraction fails + */ +export default function extractComponentTreeFromDevTools(hook: any) { + if (!hook) { + console.error( + 'React DevTools hook is not available. Make sure React DevTools extension is installed.', + ); + return null; + } + + const renderers: any = Array.from(hook.renderers.values()); + if (renderers.length === 0) { + console.error('No React renderers found.'); + return null; + } + + const fiberRoots: any = Array.from( + hook.getFiberRoots(renderers[0].rendererID), + ); + if (fiberRoots.length === 0) { + console.error('No fiber roots found.'); + return null; + } + + const currentFiber = fiberRoots[0].current; + + const options: any = { + maxDepth: 3, + showIds: true, + showProps: true, + showState: true, + }; + + return extractAndFormatComponentTree(renderers[0], currentFiber, options); +} diff --git a/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevTools.ts b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevTools.ts new file mode 100644 index 0000000000..cd026dc155 --- /dev/null +++ b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevTools.ts @@ -0,0 +1,1785 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + ReactRenderer, + ChangeDescription, + SerializedElement, + PathFrame, + Source, + ReactComponentInfo, + ReactDebugInfo, + Fiber, + FiberRoot, + ElementType, + ElementTypeActivity, + ElementTypeClass, + ElementTypeContext, + ElementTypeForwardRef, + ElementTypeFunction, + ElementTypeHostComponent, + ElementTypeMemo, + ElementTypeOtherOrUnknown, + ElementTypeProfiler, + ElementTypeRoot, + ElementTypeSuspense, + ElementTypeSuspenseList, + ElementTypeTracingMarker, + ElementTypeViewTransition, + ElementTypeVirtual, + StrictMode, +} from './reactDevToolsTypes'; + +import { + __DEBUG__, + PROFILING_FLAG_BASIC_SUPPORT, + PROFILING_FLAG_TIMELINE_SUPPORT, + TREE_OPERATION_ADD, + TREE_OPERATION_SET_SUBTREE_MODE, + TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, + TREE_OPERATION_UPDATE_TREE_BASE_DURATION, +} from './reactDevToolsConstants'; + +import { + getInternalReactConstants, + getUID, + utfEncodeString, + componentInfoToComponentLogsMap, + is, + hasOwnProperty, +} from './reactDevToolsUtils'; +import { + CONCURRENT_MODE_NUMBER, + CONCURRENT_MODE_SYMBOL_STRING, + CONTEXT_NUMBER, + CONTEXT_SYMBOL_STRING, + DEPRECATED_ASYNC_MODE_SYMBOL_STRING, + PROFILER_NUMBER, + PROFILER_SYMBOL_STRING, + PROVIDER_NUMBER, + PROVIDER_SYMBOL_STRING, + STRICT_MODE_NUMBER, + STRICT_MODE_SYMBOL_STRING, +} from './reactDevToolsSymbols'; +import {injectProfilingHooks} from './reactDevToolsUtils'; + +// Kinds +const FIBER_INSTANCE = 0; +const VIRTUAL_INSTANCE = 1; +const FILTERED_FIBER_INSTANCE = 2; + +// This type represents a stateful instance of a Client Component i.e. a Fiber pair. +// These instances also let us track stateful DevTools meta data like id and warnings. +type FiberInstance = { + kind: 0; + id: number; + parent: null | DevToolsInstance; + firstChild: null | DevToolsInstance; + nextSibling: null | DevToolsInstance; + source: null | string | Error | Source; // source location of this component function, or owned child stack + logCount: number; // total number of errors/warnings last seen + treeBaseDuration: number; // the profiled time of the last render of this subtree + data: Fiber; // one of a Fiber pair +}; + +// This type represents a stateful instance of a Server Component or a Component +// that gets optimized away - e.g. call-through without creating a Fiber. +// It's basically a virtual Fiber. This is not a semantic concept in React. +// It only exists as a virtual concept to let the same Element in the DevTools +// persist. To be selectable separately from all ReactComponentInfo and overtime. +type VirtualInstance = { + kind: 1; + id: number; + parent: null | DevToolsInstance; + firstChild: null | DevToolsInstance; + nextSibling: null | DevToolsInstance; + source: null | string | Error | Source; // source location of this server component, or owned child stack + logCount: number; // total number of errors/warnings last seen + treeBaseDuration: number; // the profiled time of the last render of this subtree + // The latest info for this instance. This can be updated over time and the + // same info can appear in more than once ServerComponentInstance. + data: ReactComponentInfo; +}; + +type FilteredFiberInstance = { + kind: 2; + // We exclude id from the type to get errors if we try to access it. + // However it is still in the object to preserve hidden class. + // id: number, + parent: null | DevToolsInstance; + firstChild: null | DevToolsInstance; + nextSibling: null | DevToolsInstance; + source: null | string | Error | Source; // always null here. + logCount: number; // total number of errors/warnings last seen + treeBaseDuration: number; // the profiled time of the last render of this subtree + data: Fiber; // one of a Fiber pair +}; + +type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance; + +export function generateComponentTree( + renderer: ReactRenderer, + fiber: Fiber, + traceNearestHostComponentUpdate: boolean, +): { + idToDevToolsInstanceMap: Map; + currentRoot: FiberInstance; + rootToFiberInstanceMap: Map; + getDisplayNameForFiber?: (fiber: Fiber) => string | null; + getElementTypeForFiber?: (fiber: Fiber) => ElementType; +} { + // Newer versions of the reconciler package also specific reconciler version. + // If that version number is present, use it. + // Third party renderer versions may not match the reconciler version, + // and the latter is what's important in terms of tags and symbols. + const version = renderer.reconcilerVersion || renderer.version; + + // VARIABLES --------------------------------------------------------------- + // Running state of the remaining children from the previous version of this parent that + // we haven't yet added back. This should be reset anytime we change parent. + // Any remaining ones at the end will be deleted. + let remainingReconcilingChildren: null | DevToolsInstance = null; + // The previously placed child. + let previouslyReconciledSibling: null | DevToolsInstance = null; + // To save on stack allocation and ensure that they are updated as a pair, we also store + // the current parent here as well. + let reconcilingParent: null | DevToolsInstance = null; + + const { + getDisplayNameForFiber, + getTypeSymbol, + ReactPriorityLevels, + ReactTypeOfWork, + StrictModeBits, + } = getInternalReactConstants(version); + + const { + ActivityComponent, + CacheComponent, + ClassComponent, + ContextConsumer, + DehydratedSuspenseComponent, + ForwardRef, + Fragment, + FunctionComponent, + HostRoot, + HostHoistable, + HostSingleton, + HostPortal, + HostComponent, + HostText, + IncompleteClassComponent, + IncompleteFunctionComponent, + IndeterminateComponent, + LegacyHiddenComponent, + MemoComponent, + OffscreenComponent, + SimpleMemoComponent, + SuspenseComponent, + SuspenseListComponent, + TracingMarkerComponent, + Throw, + ViewTransitionComponent, + } = ReactTypeOfWork; + + type HostInstance = any; + + // Configurable Components tree filters. + const hideElementsWithDisplayNames: Set = new Set(); + const hideElementsWithPaths: Set = new Set(); + const hideElementsWithTypes: Set = new Set(); + const hideElementsWithEnvs: Set = new Set(); + + // Highlight updates + let traceUpdatesEnabled: boolean = false; + const traceUpdatesForNodes: Set = new Set(); + + const pendingOperations: OperationsArray = []; + const pendingRealUnmountedIDs: Array = []; + let pendingOperationsQueue: Array | null = []; + const pendingStringTable: Map = new Map(); + let pendingStringTableLength: number = 0; + let pendingUnmountedRootID: number | null = null; + + // Tracks Errors/Warnings logs added to a Fiber. They are added before the commit and get + // picked up a FiberInstance. This keeps it around as long as the Fiber is alive which + // lets the Fiber get reparented/remounted and still observe the previous errors/warnings. + // Unless we explicitly clear the logs from a Fiber. + const fiberToComponentLogsMap: WeakMap = new WeakMap(); + let isProfiling: boolean = false; + + let currentCommitProfilingMetadata: CommitProfilingData | null = null; + let recordChangeDescriptions: boolean = false; + + let currentRoot: FiberInstance = null as any; + + // Map of FiberRoot to their root FiberInstance. + const rootToFiberInstanceMap: Map = new Map(); + + // Map of id to FiberInstance or VirtualInstance. + // This Map is used to e.g. get the display name for a Fiber or schedule an update, + // operations that should be the same whether the current and work-in-progress Fiber is used. + const idToDevToolsInstanceMap: Map = + new Map(); + + let displayNamesByRootID: DisplayNamesByRootID | null = null; + + // Remember if we're trying to restore the selection after reload. + // In that case, we'll do some extra checks for matching mounts. + let trackedPath: Array | null = null; + let trackedPathMatchFiber: Fiber | null = null; // This is the deepest unfiltered match of a Fiber. + let trackedPathMatchInstance: FiberInstance | VirtualInstance | null = null; // This is the deepest matched filtered Instance. + let trackedPathMatchDepth = -1; + let mightBeOnTrackedPath = false; + + const rootPseudoKeys: Map = new Map(); + + // Map of canonical HostInstances to the nearest parent DevToolsInstance. + const publicInstanceToDevToolsInstanceMap: Map< + HostInstance, + DevToolsInstance + > = new Map(); + + // All environment names we've seen so far. This lets us create a list of filters to apply. + // This should ideally include env of filtered Components too so that you can add those as + // filters at the same time as removing some other filter. + const knownEnvironmentNames: Set = new Set(); + + // Map of resource DOM nodes to all the nearest DevToolsInstances that depend on it. + const hostResourceToDevToolsInstanceMap: Map< + HostInstance, + Set + > = new Map(); + // -------------------------------------------------------------------------- + // INLINE TYPES ------------------------------------------------------------- + type OperationsArray = Array; + + type StringTableEntry = { + encodedString: Array; + id: number; + }; + + type ComponentLogs = { + errors: Map; + errorsCount: number; + warnings: Map; + warningsCount: number; + }; + + type CommitProfilingData = { + changeDescriptions: Map | null; + commitTime: number; + durations: Array; + effectDuration: number | null; + maxActualDuration: number; + passiveEffectDuration: number | null; + priorityLevel: string | null; + updaters: Array | null; + }; + + type DisplayNamesByRootID = Map; + // -------------------------------------------------------------------------- + + function mountFiberRecursively( + fiber: Fiber, + traceNearestHostComponentUpdate: boolean, + ): void { + const shouldIncludeInTree = !shouldFilterFiber(fiber); + let newInstance = null; + if (shouldIncludeInTree) { + newInstance = recordMount(fiber, reconcilingParent); + insertChild(newInstance); + // if (__DEBUG__) { + // debug('mountFiberRecursively()', newInstance, reconcilingParent); + // } + } else if ( + reconcilingParent !== null && + reconcilingParent.kind === VIRTUAL_INSTANCE + ) { + // If the parent is a Virtual Instance and we filtered this Fiber we include a + // hidden node. + + if ( + reconcilingParent.data === fiber._debugOwner && + fiber._debugStack != null && + reconcilingParent.source === null + ) { + // The new Fiber is directly owned by the parent. Therefore somewhere on the + // debugStack will be a stack frame inside parent that we can use as its soruce. + reconcilingParent.source = fiber._debugStack; + } + + newInstance = createFilteredFiberInstance(fiber); + insertChild(newInstance); + } + + // If we have the tree selection from previous reload, try to match this Fiber. + // Also remember whether to do the same for siblings. + const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount( + fiber, + newInstance, + ); + + const stashedParent = reconcilingParent; + const stashedPrevious = previouslyReconciledSibling; + const stashedRemaining = remainingReconcilingChildren; + if (newInstance !== null) { + // Push a new DevTools instance parent while reconciling this subtree. + reconcilingParent = newInstance; + previouslyReconciledSibling = null; + remainingReconcilingChildren = null; + } + try { + if (traceUpdatesEnabled) { + if (traceNearestHostComponentUpdate) { + const elementType = getElementTypeForFiber(fiber); + // If an ancestor updated, we should mark the nearest host nodes for highlighting. + if (elementType === ElementTypeHostComponent) { + traceUpdatesForNodes.add(fiber.stateNode); + traceNearestHostComponentUpdate = false; + } + } + + // We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch, + // because we don't want to highlight every host node inside of a newly mounted subtree. + } + + if (fiber.tag === HostHoistable) { + const nearestInstance = reconcilingParent; + if (nearestInstance === null) { + throw new Error('Did not expect a host hoistable to be the root'); + } + aquireHostResource(nearestInstance, fiber.memoizedState); + } else if ( + fiber.tag === HostComponent || + fiber.tag === HostText || + fiber.tag === HostSingleton + ) { + const nearestInstance = reconcilingParent; + if (nearestInstance === null) { + throw new Error('Did not expect a host hoistable to be the root'); + } + aquireHostInstance(nearestInstance, fiber.stateNode); + } + + if (fiber.tag === SuspenseComponent) { + const isTimedOut = fiber.memoizedState !== null; + if (isTimedOut) { + // Special case: if Suspense mounts in a timed-out state, + // get the fallback child from the inner fragment and mount + // it as if it was our own child. Updates handle this too. + const primaryChildFragment = fiber.child; + const fallbackChildFragment = primaryChildFragment + ? primaryChildFragment.sibling + : null; + if (fallbackChildFragment) { + const fallbackChild = fallbackChildFragment.child; + if (fallbackChild !== null) { + updateTrackedPathStateBeforeMount(fallbackChildFragment, null); + mountChildrenRecursively( + fallbackChild, + traceNearestHostComponentUpdate, + ); + } + } + } else { + let primaryChild: Fiber | null = null; + const areSuspenseChildrenConditionallyWrapped = + OffscreenComponent === -1; + + if (areSuspenseChildrenConditionallyWrapped) { + primaryChild = fiber.child; + } else if (fiber.child !== null) { + primaryChild = fiber.child.child; + updateTrackedPathStateBeforeMount(fiber.child, null); + } + if (primaryChild !== null) { + mountChildrenRecursively( + primaryChild, + traceNearestHostComponentUpdate, + ); + } + } + } else { + if (fiber.child !== null) { + mountChildrenRecursively( + fiber.child, + traceNearestHostComponentUpdate, + ); + } + } + } finally { + if (newInstance !== null) { + reconcilingParent = stashedParent; + previouslyReconciledSibling = stashedPrevious; + remainingReconcilingChildren = stashedRemaining; + } + } + + // We're exiting this Fiber now, and entering its siblings. + // If we have selection to restore, we might need to re-activate tracking. + updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath); + } + + function aquireHostResource( + nearestInstance: DevToolsInstance, + resource: {instance?: HostInstance} | null | undefined, + ): void { + const hostInstance = resource && resource.instance; + if (hostInstance) { + const publicInstance = getPublicInstance(hostInstance); + let resourceInstances = + hostResourceToDevToolsInstanceMap.get(publicInstance); + if (resourceInstances === undefined) { + resourceInstances = new Set(); + hostResourceToDevToolsInstanceMap.set( + publicInstance, + resourceInstances, + ); + // Store the first match in the main map for quick access when selecting DOM node. + publicInstanceToDevToolsInstanceMap.set( + publicInstance, + nearestInstance, + ); + } + resourceInstances.add(nearestInstance); + } + } + + function mountChildrenRecursively( + firstChild: Fiber, + traceNearestHostComponentUpdate: boolean, + ): void { + mountVirtualChildrenRecursively( + firstChild, + null, + traceNearestHostComponentUpdate, + 0, // first level + ); + } + + function mountVirtualChildrenRecursively( + firstChild: Fiber, + lastChild: null | Fiber, // non-inclusive + traceNearestHostComponentUpdate: boolean, + virtualLevel: number, // the nth level of virtual instances + ): void { + // Iterate over siblings rather than recursing. + // This reduces the chance of stack overflow for wide trees (e.g. lists with many items). + let fiber: Fiber | null = firstChild; + let previousVirtualInstance: null | VirtualInstance = null; + let previousVirtualInstanceFirstFiber: Fiber = firstChild; + while (fiber !== null && fiber !== lastChild) { + let level = 0; + if (fiber._debugInfo) { + for (let i = 0; i < fiber._debugInfo.length; i++) { + const debugEntry: any = fiber._debugInfo[i]; + if (typeof debugEntry.name !== 'string') { + // Not a Component. Some other Debug Info. + continue; + } + // Scan up until the next Component to see if this component changed environment. + const componentInfo: any = debugEntry as any; + const secondaryEnv = getSecondaryEnvironmentName(fiber._debugInfo, i); + if (componentInfo.env != null) { + knownEnvironmentNames.add(componentInfo.env); + } + if (secondaryEnv !== null) { + knownEnvironmentNames.add(secondaryEnv); + } + if (shouldFilterVirtual(componentInfo, secondaryEnv)) { + // Skip. + continue; + } + if (level === virtualLevel) { + if ( + previousVirtualInstance === null || + // Consecutive children with the same debug entry as a parent gets + // treated as if they share the same virtual instance. + previousVirtualInstance.data !== debugEntry + ) { + if (previousVirtualInstance !== null) { + // Mount any previous children that should go into the previous parent. + mountVirtualInstanceRecursively( + previousVirtualInstance, + previousVirtualInstanceFirstFiber, + fiber, + traceNearestHostComponentUpdate, + virtualLevel, + ); + } + previousVirtualInstance = createVirtualInstance(componentInfo); + recordVirtualMount( + previousVirtualInstance, + reconcilingParent, + secondaryEnv, + ); + insertChild(previousVirtualInstance); + previousVirtualInstanceFirstFiber = fiber; + } + level++; + break; + } else { + level++; + } + } + } + if (level === virtualLevel) { + if (previousVirtualInstance !== null) { + // If we were working on a virtual instance and this is not a virtual + // instance, then we end the sequence and mount any previous children + // that should go into the previous virtual instance. + mountVirtualInstanceRecursively( + previousVirtualInstance, + previousVirtualInstanceFirstFiber, + fiber, + traceNearestHostComponentUpdate, + virtualLevel, + ); + previousVirtualInstance = null; + } + // We've reached the end of the virtual levels, but not beyond, + // and now continue with the regular fiber. + mountFiberRecursively(fiber, traceNearestHostComponentUpdate); + } + fiber = fiber.sibling; + } + if (previousVirtualInstance !== null) { + // Mount any previous children that should go into the previous parent. + mountVirtualInstanceRecursively( + previousVirtualInstance, + previousVirtualInstanceFirstFiber, + null, + traceNearestHostComponentUpdate, + virtualLevel, + ); + } + } + + function recordVirtualMount( + instance: VirtualInstance, + parentInstance: DevToolsInstance | null, + secondaryEnv: null | string, + ): void { + const id = instance.id; + + idToDevToolsInstanceMap.set(id, instance); + + const componentInfo: any = instance.data; + + const key = + typeof componentInfo.key === 'string' ? componentInfo.key : null; + const env = componentInfo.env; + let displayName = componentInfo.name || ''; + if (typeof env === 'string') { + // We model environment as an HoC name for now. + if (secondaryEnv !== null) { + displayName = secondaryEnv + '(' + displayName + ')'; + } + displayName = env + '(' + displayName + ')'; + } + const elementType = ElementTypeVirtual; + + // Finding the owner instance might require traversing the whole parent path which + // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we + // need it but we have some synchronous operations in the front end like Alt+Left + // which selects the owner immediately. Typically most owners are only a few parents + // away so maybe it's not so bad. + const debugOwner = getUnfilteredOwner(componentInfo); + const ownerInstance = findNearestOwnerInstance(parentInstance, debugOwner); + if ( + ownerInstance !== null && + debugOwner === componentInfo.owner && + componentInfo.debugStack != null && + ownerInstance.source === null + ) { + // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on + // the debugStack will be a stack frame inside the ownerInstance's source. + ownerInstance.source = componentInfo.debugStack; + } + const ownerID = ownerInstance === null ? 0 : ownerInstance.id; + const parentID = parentInstance + ? parentInstance.kind === FILTERED_FIBER_INSTANCE + ? // A Filtered Fiber Instance will always have a Virtual Instance as a parent. + (parentInstance.parent as VirtualInstance).id + : parentInstance.id + : 0; + + const displayNameStringID = getStringID(displayName); + + // This check is a guard to handle a React element that has been modified + // in such a way as to bypass the default stringification of the "key" property. + const keyString = key === null ? null : String(key); + const keyStringID = getStringID(keyString); + + pushOperation(TREE_OPERATION_ADD); + pushOperation(id); + pushOperation(elementType); + pushOperation(parentID); + pushOperation(ownerID); + pushOperation(displayNameStringID); + pushOperation(keyStringID); + + const componentLogsEntry = + componentInfoToComponentLogsMap.get(componentInfo); + recordConsoleLogs(instance, componentLogsEntry); + } + + function createVirtualInstance( + debugEntry: ReactComponentInfo, + ): VirtualInstance { + return { + kind: VIRTUAL_INSTANCE, + id: getUID(), + parent: null, + firstChild: null, + nextSibling: null, + source: null, + logCount: 0, + treeBaseDuration: 0, + data: debugEntry, + }; + } + + function mountVirtualInstanceRecursively( + virtualInstance: VirtualInstance, + firstChild: Fiber, + lastChild: null | Fiber, // non-inclusive + traceNearestHostComponentUpdate: boolean, + virtualLevel: number, // the nth level of virtual instances + ): void { + // If we have the tree selection from previous reload, try to match this Instance. + // Also remember whether to do the same for siblings. + const mightSiblingsBeOnTrackedPath = + updateVirtualTrackedPathStateBeforeMount( + virtualInstance, + reconcilingParent, + ); + + const stashedParent = reconcilingParent; + const stashedPrevious = previouslyReconciledSibling; + const stashedRemaining = remainingReconcilingChildren; + // Push a new DevTools instance parent while reconciling this subtree. + reconcilingParent = virtualInstance; + previouslyReconciledSibling = null; + remainingReconcilingChildren = null; + try { + mountVirtualChildrenRecursively( + firstChild, + lastChild, + traceNearestHostComponentUpdate, + virtualLevel + 1, + ); + // Must be called after all children have been appended. + recordVirtualProfilingDurations(virtualInstance); + } finally { + reconcilingParent = stashedParent; + previouslyReconciledSibling = stashedPrevious; + remainingReconcilingChildren = stashedRemaining; + updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath); + } + } + + function recordVirtualProfilingDurations(virtualInstance: VirtualInstance) { + const id = virtualInstance.id; + + let treeBaseDuration = 0; + // Add up the base duration of the child instances. The virtual base duration + // will be the same as children's duration since we don't take up any render + // time in the virtual instance. + for ( + let child = virtualInstance.firstChild; + child !== null; + child = child.nextSibling + ) { + treeBaseDuration += child.treeBaseDuration; + } + + if (isProfiling) { + const previousTreeBaseDuration = virtualInstance.treeBaseDuration; + if (treeBaseDuration !== previousTreeBaseDuration) { + // Tree base duration updates are included in the operations typed array. + // So we have to convert them from milliseconds to microseconds so we can send them as ints. + const convertedTreeBaseDuration = Math.floor( + (treeBaseDuration || 0) * 1000, + ); + pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION); + pushOperation(id); + pushOperation(convertedTreeBaseDuration); + } + } + + virtualInstance.treeBaseDuration = treeBaseDuration; + } + + function updateVirtualTrackedPathStateBeforeMount( + virtualInstance: VirtualInstance, + parentInstance: null | DevToolsInstance, + ): boolean { + if (trackedPath === null || !mightBeOnTrackedPath) { + // Fast path: there's nothing to track so do nothing and ignore siblings. + return false; + } + // Check if we've matched our nearest unfiltered parent so far. + if (trackedPathMatchInstance === parentInstance) { + const actualFrame = getVirtualPathFrame(virtualInstance); + // $FlowFixMe[incompatible-use] found when upgrading Flow + const expectedFrame = trackedPath[trackedPathMatchDepth + 1]; + if (expectedFrame === undefined) { + throw new Error('Expected to see a frame at the next depth.'); + } + if ( + actualFrame.index === expectedFrame.index && + actualFrame.key === expectedFrame.key && + actualFrame.displayName === expectedFrame.displayName + ) { + // We have our next match. + trackedPathMatchFiber = null; // Don't bother looking in Fibers anymore. We're deeper now. + trackedPathMatchInstance = virtualInstance; + trackedPathMatchDepth++; + // Are we out of frames to match? + // $FlowFixMe[incompatible-use] found when upgrading Flow + if (trackedPathMatchDepth === trackedPath.length - 1) { + // There's nothing that can possibly match afterwards. + // Don't check the children. + mightBeOnTrackedPath = false; + } else { + // Check the children, as they might reveal the next match. + mightBeOnTrackedPath = true; + } + // In either case, since we have a match, we don't need + // to check the siblings. They'll never match. + return false; + } + } + if (trackedPathMatchFiber !== null) { + // We're still looking for a Fiber which might be underneath this instance. + return true; + } + // This Instance's parent is on the path, but this Instance itself isn't. + // There's no need to check its children--they won't be on the path either. + mightBeOnTrackedPath = false; + // However, one of its siblings may be on the path so keep searching. + return true; + } + + function getVirtualPathFrame(virtualInstance: any): PathFrame { + return { + displayName: virtualInstance.data.name || '', + key: virtualInstance.data.key == null ? null : virtualInstance.data.key, + index: -1, // We use -1 to indicate that this is a virtual path frame. + }; + } + + function getSecondaryEnvironmentName( + debugInfo: ReactDebugInfo | null | undefined, + index: number, + ): null | string { + if (debugInfo != null) { + const componentInfo: any = debugInfo[index] as any; + for (let i = index + 1; i < debugInfo.length; i++) { + const debugEntry: any = debugInfo[i] as any; + if (typeof debugEntry.env === 'string') { + // If the next environment is different then this component was the boundary + // and it changed before entering the next component. So we assign this + // component a secondary environment. + return componentInfo.env !== debugEntry.env ? debugEntry.env : null; + } + } + } + return null; + } + + function updateTrackedPathStateAfterMount( + mightSiblingsBeOnTrackedPath: boolean, + ) { + // updateTrackedPathStateBeforeMount() told us whether to match siblings. + // Now that we're entering siblings, let's use that information. + mightBeOnTrackedPath = mightSiblingsBeOnTrackedPath; + } + + function aquireHostInstance( + nearestInstance: DevToolsInstance, + hostInstance: HostInstance, + ): void { + const publicInstance = getPublicInstance(hostInstance); + publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance); + } + + // Ideally, this should be injected from Reconciler config + function getPublicInstance(instance: any): HostInstance { + // Typically the PublicInstance and HostInstance is the same thing but not in Fabric. + // So we need to detect this and use that as the public instance. + + // React Native. Modern. Fabric. + if (instance !== null) { + if ( + typeof instance.canonical === 'object' && + instance.canonical !== null + ) { + if ( + typeof instance.canonical.publicInstance === 'object' && + instance.canonical.publicInstance !== null + ) { + return instance.canonical.publicInstance; + } + } + + // React Native. Legacy. Paper. + if (typeof instance._nativeTag === 'number') { + return instance._nativeTag; + } + } + + // React Web. Usually a DOM element. + return instance; + } + + function updateTrackedPathStateBeforeMount( + fiber: Fiber, + fiberInstance: null | FiberInstance | FilteredFiberInstance, + ): boolean { + if (trackedPath === null || !mightBeOnTrackedPath) { + // Fast path: there's nothing to track so do nothing and ignore siblings. + return false; + } + const returnFiber = fiber.return; + const returnAlternate = returnFiber !== null ? returnFiber.alternate : null; + // By now we know there's some selection to restore, and this is a new Fiber. + // Is this newly mounted Fiber a direct child of the current best match? + // (This will also be true for new roots if we haven't matched anything yet.) + if ( + trackedPathMatchFiber === returnFiber || + (trackedPathMatchFiber === returnAlternate && returnAlternate !== null) + ) { + // Is this the next Fiber we should select? Let's compare the frames. + const actualFrame = getPathFrame(fiber); + // $FlowFixMe[incompatible-use] found when upgrading Flow + const expectedFrame = trackedPath[trackedPathMatchDepth + 1]; + if (expectedFrame === undefined) { + throw new Error('Expected to see a frame at the next depth.'); + } + if ( + actualFrame.index === expectedFrame.index && + actualFrame.key === expectedFrame.key && + actualFrame.displayName === expectedFrame.displayName + ) { + // We have our next match. + trackedPathMatchFiber = fiber; + if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) { + trackedPathMatchInstance = fiberInstance; + } + trackedPathMatchDepth++; + // Are we out of frames to match? + // $FlowFixMe[incompatible-use] found when upgrading Flow + if (trackedPathMatchDepth === trackedPath.length - 1) { + // There's nothing that can possibly match afterwards. + // Don't check the children. + mightBeOnTrackedPath = false; + } else { + // Check the children, as they might reveal the next match. + mightBeOnTrackedPath = true; + } + // In either case, since we have a match, we don't need + // to check the siblings. They'll never match. + return false; + } + } + if (trackedPathMatchFiber === null && fiberInstance === null) { + // We're now looking for a Virtual Instance. It might be inside filtered Fibers + // so we keep looking below. + return true; + } + // This Fiber's parent is on the path, but this Fiber itself isn't. + // There's no need to check its children--they won't be on the path either. + mightBeOnTrackedPath = false; + // However, one of its siblings may be on the path so keep searching. + return true; + } + + function getPathFrame(fiber: Fiber): PathFrame { + const {key} = fiber; + let displayName = getDisplayNameForFiber(fiber); + const index = fiber.index; + switch (fiber.tag) { + case HostRoot: + // Roots don't have a real displayName, index, or key. + // Instead, we'll use the pseudo key (childDisplayName:indexWithThatName). + const rootInstance = rootToFiberInstanceMap.get(fiber.stateNode); + if (rootInstance === undefined) { + throw new Error( + 'Expected the root instance to exist when computing a path', + ); + } + const pseudoKey = rootPseudoKeys.get(rootInstance.id); + if (pseudoKey === undefined) { + throw new Error('Expected mounted root to have known pseudo key.'); + } + displayName = pseudoKey; + break; + case HostComponent: + displayName = fiber.type; + break; + default: + break; + } + return { + displayName, + key, + index, + }; + } + + function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance { + return { + kind: FILTERED_FIBER_INSTANCE, + id: 0, + parent: null, + firstChild: null, + nextSibling: null, + source: null, + logCount: 0, + treeBaseDuration: 0, + data: fiber, + } as any; + } + + function insertChild(instance: DevToolsInstance): void { + const parentInstance = reconcilingParent; + if (parentInstance === null) { + // This instance is at the root. + return; + } + // Place it in the parent. + instance.parent = parentInstance; + if (previouslyReconciledSibling === null) { + previouslyReconciledSibling = instance; + parentInstance.firstChild = instance; + } else { + previouslyReconciledSibling.nextSibling = instance; + previouslyReconciledSibling = instance; + } + instance.nextSibling = null; + } + + // NOTICE Keep in sync with get*ForFiber methods + function shouldFilterFiber(fiber: Fiber): boolean { + const {tag, type, key} = fiber; + + switch (tag) { + case DehydratedSuspenseComponent: + // TODO: ideally we would show dehydrated Suspense immediately. + // However, it has some special behavior (like disconnecting + // an alternate and turning into real Suspense) which breaks DevTools. + // For now, ignore it, and only show it once it gets hydrated. + // https://github.com/bvaughn/react-devtools-experimental/issues/197 + return true; + case HostPortal: + case HostText: + case LegacyHiddenComponent: + case OffscreenComponent: + case Throw: + return true; + case HostRoot: + // It is never valid to filter the root element. + return false; + case Fragment: + return key === null; + default: + const typeSymbol = getTypeSymbol(type); + + switch (typeSymbol) { + case CONCURRENT_MODE_NUMBER: + case CONCURRENT_MODE_SYMBOL_STRING: + case DEPRECATED_ASYNC_MODE_SYMBOL_STRING: + case STRICT_MODE_NUMBER: + case STRICT_MODE_SYMBOL_STRING: + return true; + default: + break; + } + } + + const elementType = getElementTypeForFiber(fiber); + if (hideElementsWithTypes.has(elementType)) { + return true; + } + + if (hideElementsWithDisplayNames.size > 0) { + const displayName = getDisplayNameForFiber(fiber); + if (displayName != null) { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const displayNameRegExp of hideElementsWithDisplayNames) { + if (displayNameRegExp.test(displayName)) { + return true; + } + } + } + } + + if (hideElementsWithEnvs.has('Client')) { + // If we're filtering out the Client environment we should filter out all + // "Client Components". Technically that also includes the built-ins but + // since that doesn't actually include any additional code loading it's + // useful to not filter out the built-ins. Those can be filtered separately. + // There's no other way to filter out just Function components on the Client. + // Therefore, this only filters Class and Function components. + switch (tag) { + case ClassComponent: + case IncompleteClassComponent: + case IncompleteFunctionComponent: + case FunctionComponent: + case IndeterminateComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: + return true; + } + } + + /* DISABLED: https://github.com/facebook/react/pull/28417 + if (hideElementsWithPaths.size > 0) { + const source = getSourceForFiber(fiber); + + if (source != null) { + const {fileName} = source; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const pathRegExp of hideElementsWithPaths) { + if (pathRegExp.test(fileName)) { + return true; + } + } + } + } + */ + + return false; + } + + function getElementTypeForFiber(fiber: Fiber): ElementType { + const {type, tag} = fiber; + + switch (tag) { + case ActivityComponent: + return ElementTypeActivity; + case ClassComponent: + case IncompleteClassComponent: + return ElementTypeClass; + case IncompleteFunctionComponent: + case FunctionComponent: + case IndeterminateComponent: + return ElementTypeFunction; + case ForwardRef: + return ElementTypeForwardRef; + case HostRoot: + return ElementTypeRoot; + case HostComponent: + case HostHoistable: + case HostSingleton: + return ElementTypeHostComponent; + case HostPortal: + case HostText: + case Fragment: + return ElementTypeOtherOrUnknown; + case MemoComponent: + case SimpleMemoComponent: + return ElementTypeMemo; + case SuspenseComponent: + return ElementTypeSuspense; + case SuspenseListComponent: + return ElementTypeSuspenseList; + case TracingMarkerComponent: + return ElementTypeTracingMarker; + case ViewTransitionComponent: + return ElementTypeViewTransition; + default: + const typeSymbol = getTypeSymbol(type); + + switch (typeSymbol) { + case CONCURRENT_MODE_NUMBER: + case CONCURRENT_MODE_SYMBOL_STRING: + case DEPRECATED_ASYNC_MODE_SYMBOL_STRING: + return ElementTypeOtherOrUnknown; + case PROVIDER_NUMBER: + case PROVIDER_SYMBOL_STRING: + return ElementTypeContext; + case CONTEXT_NUMBER: + case CONTEXT_SYMBOL_STRING: + return ElementTypeContext; + case STRICT_MODE_NUMBER: + case STRICT_MODE_SYMBOL_STRING: + return ElementTypeOtherOrUnknown; + case PROFILER_NUMBER: + case PROFILER_SYMBOL_STRING: + return ElementTypeProfiler; + default: + return ElementTypeOtherOrUnknown; + } + } + } + function recordMount( + fiber: Fiber, + parentInstance: DevToolsInstance | null, + ): FiberInstance { + const isRoot = fiber.tag === HostRoot; + let fiberInstance; + if (isRoot) { + const entry = rootToFiberInstanceMap.get(fiber.stateNode); + if (entry === undefined) { + throw new Error('The root should have been registered at this point'); + } + fiberInstance = entry; + } else { + fiberInstance = createFiberInstance(fiber); + } + idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance); + + const id = fiberInstance.id; + + const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration'); + + if (isRoot) { + const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner'); + + // Adding a new field here would require a bridge protocol version bump (a backwads breaking change). + // Instead let's re-purpose a pre-existing field to carry more information. + let profilingFlags = 0; + if (isProfilingSupported) { + profilingFlags = PROFILING_FLAG_BASIC_SUPPORT; + if (typeof injectProfilingHooks === 'function') { + profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT; + } + } + + // Set supportsStrictMode to false for production renderer builds + const isProductionBuildOfRenderer = renderer.bundleType === 0; + + pushOperation(TREE_OPERATION_ADD); + pushOperation(id); + pushOperation(ElementTypeRoot); + pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0); + pushOperation(profilingFlags); + pushOperation( + !isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0, + ); + pushOperation(hasOwnerMetadata ? 1 : 0); + + if (isProfiling) { + if (displayNamesByRootID !== null) { + displayNamesByRootID.set(id, getDisplayNameForRoot(fiber)); + } + } + } else { + const {key} = fiber; + const displayName = getDisplayNameForFiber(fiber); + const elementType = getElementTypeForFiber(fiber); + + // Finding the owner instance might require traversing the whole parent path which + // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we + // need it but we have some synchronous operations in the front end like Alt+Left + // which selects the owner immediately. Typically most owners are only a few parents + // away so maybe it's not so bad. + const debugOwner = getUnfilteredOwner(fiber); + const ownerInstance = findNearestOwnerInstance( + parentInstance, + debugOwner, + ); + if ( + ownerInstance !== null && + debugOwner === fiber._debugOwner && + fiber._debugStack != null && + ownerInstance.source === null + ) { + // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on + // the debugStack will be a stack frame inside the ownerInstance's source. + ownerInstance.source = fiber._debugStack; + } + const ownerID = ownerInstance === null ? 0 : ownerInstance.id; + const parentID = parentInstance + ? parentInstance.kind === FILTERED_FIBER_INSTANCE + ? // A Filtered Fiber Instance will always have a Virtual Instance as a parent. + (parentInstance.parent as VirtualInstance).id + : parentInstance.id + : 0; + + const displayNameStringID = getStringID(displayName); + + // This check is a guard to handle a React element that has been modified + // in such a way as to bypass the default stringification of the "key" property. + const keyString = key === null ? null : String(key); + const keyStringID = getStringID(keyString); + + pushOperation(TREE_OPERATION_ADD); + pushOperation(id); + pushOperation(elementType); + pushOperation(parentID); + pushOperation(ownerID); + pushOperation(displayNameStringID); + pushOperation(keyStringID); + + // If this subtree has a new mode, let the frontend know. + if ((fiber.mode & StrictModeBits) !== 0) { + let parentFiber = null; + let parentFiberInstance = parentInstance; + while (parentFiberInstance !== null) { + if (parentFiberInstance.kind === FIBER_INSTANCE) { + parentFiber = parentFiberInstance.data; + break; + } + parentFiberInstance = parentFiberInstance.parent; + } + if (parentFiber === null || (parentFiber.mode & StrictModeBits) === 0) { + pushOperation(TREE_OPERATION_SET_SUBTREE_MODE); + pushOperation(id); + pushOperation(StrictMode); + } + } + } + + let componentLogsEntry = fiberToComponentLogsMap.get(fiber); + if (componentLogsEntry === undefined && fiber.alternate !== null) { + componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); + } + recordConsoleLogs(fiberInstance, componentLogsEntry); + + if (isProfilingSupported) { + recordProfilingDurations(fiberInstance, null); + } + return fiberInstance; + } + + function findNearestOwnerInstance( + parentInstance: null | DevToolsInstance, + owner: void | null | ReactComponentInfo | Fiber, + ): null | FiberInstance | VirtualInstance { + if (owner == null) { + return null; + } + // Search the parent path for any instance that matches this kind of owner. + while (parentInstance !== null) { + if ( + parentInstance.data === owner || + // Typically both owner and instance.data would refer to the current version of a Fiber + // but it is possible for memoization to ignore the owner on the JSX. Then the new Fiber + // isn't propagated down as the new owner. In that case we might match the alternate + // instead. This is a bit hacky but the fastest check since type casting owner to a Fiber + // needs a duck type check anyway. + parentInstance.data === (owner as any).alternate + ) { + if (parentInstance.kind === FILTERED_FIBER_INSTANCE) { + return null; + } + return parentInstance; + } + parentInstance = parentInstance.parent; + } + // It is technically possible to create an element and render it in a different parent + // but this is a weird edge case and it is worth not having to scan the tree or keep + // a register for every fiber/component info. + return null; + } + + function getUnfilteredOwner( + owner: ReactComponentInfo | Fiber | null | void, + ): ReactComponentInfo | Fiber | null { + if (owner == null) { + return null; + } + if ('tag' in owner && typeof owner.tag === 'number') { + const ownerFiber: Fiber = owner as Fiber; // Refined + owner = ownerFiber._debugOwner; + } else { + const ownerInfo: any = owner as ReactComponentInfo; // Refined + owner = ownerInfo.owner; + } + while (owner) { + if ('tag' in owner && typeof owner.tag === 'number') { + const ownerFiber: Fiber = owner as any; // Refined + if (!shouldFilterFiber(ownerFiber)) { + return ownerFiber; + } + owner = ownerFiber._debugOwner; + } else { + const ownerInfo: any = owner as any; // Refined + if (!shouldFilterVirtual(ownerInfo, null)) { + return ownerInfo; + } + owner = ownerInfo.owner; + } + } + return null; + } + + function shouldFilterVirtual( + data: any, + secondaryEnv: null | string, + ): boolean { + // For purposes of filtering Server Components are always Function Components. + // Environment will be used to filter Server vs Client. + // Technically they can be forwardRef and memo too but those filters will go away + // as those become just plain user space function components like any HoC. + if (hideElementsWithTypes.has(ElementTypeFunction)) { + return true; + } + + if (hideElementsWithDisplayNames.size > 0) { + const displayName = data.name; + if (displayName != null) { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const displayNameRegExp of hideElementsWithDisplayNames) { + if (displayNameRegExp.test(displayName)) { + return true; + } + } + } + } + + if ( + (data.env == null || hideElementsWithEnvs.has(data.env)) && + (secondaryEnv === null || hideElementsWithEnvs.has(secondaryEnv)) + ) { + // If a Component has two environments, you have to filter both for it not to appear. + return true; + } + + return false; + } + + function getDisplayNameForRoot(fiber: Fiber): string { + let preferredDisplayName = null; + let fallbackDisplayName = null; + let child = fiber.child; + // Go at most three levels deep into direct children + // while searching for a child that has a displayName. + for (let i = 0; i < 3; i++) { + if (child === null) { + break; + } + const displayName = getDisplayNameForFiber(child); + if (displayName !== null) { + // Prefer display names that we get from user-defined components. + // We want to avoid using e.g. 'Suspense' unless we find nothing else. + if (typeof child.type === 'function') { + // There's a few user-defined tags, but we'll prefer the ones + // that are usually explicitly named (function or class components). + preferredDisplayName = displayName; + } else if (fallbackDisplayName === null) { + fallbackDisplayName = displayName; + } + } + if (preferredDisplayName !== null) { + break; + } + child = child.child; + } + return preferredDisplayName || fallbackDisplayName || 'Anonymous'; + } + + function createFiberInstance(fiber: Fiber): FiberInstance { + return { + kind: FIBER_INSTANCE, + id: getUID(), + parent: null, + firstChild: null, + nextSibling: null, + source: null, + logCount: 0, + treeBaseDuration: 0, + data: fiber, + }; + } + + function recordProfilingDurations( + fiberInstance: FiberInstance, + prevFiber: null | Fiber, + ) { + const id = fiberInstance.id; + const fiber = fiberInstance.data; + const {actualDuration, treeBaseDuration} = fiber; + + fiberInstance.treeBaseDuration = treeBaseDuration || 0; + + if (isProfiling) { + // It's important to update treeBaseDuration even if the current Fiber did not render, + // because it's possible that one of its descendants did. + if ( + prevFiber == null || + treeBaseDuration !== prevFiber.treeBaseDuration + ) { + // Tree base duration updates are included in the operations typed array. + // So we have to convert them from milliseconds to microseconds so we can send them as ints. + const convertedTreeBaseDuration = Math.floor( + (treeBaseDuration || 0) * 1000, + ); + pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION); + pushOperation(id); + pushOperation(convertedTreeBaseDuration); + } + + if (prevFiber == null || didFiberRender(prevFiber, fiber)) { + if (actualDuration != null) { + // The actual duration reported by React includes time spent working on children. + // This is useful information, but it's also useful to be able to exclude child durations. + // The frontend can't compute this, since the immediate children may have been filtered out. + // So we need to do this on the backend. + // Note that this calculated self duration is not the same thing as the base duration. + // The two are calculated differently (tree duration does not accumulate). + let selfDuration = actualDuration; + let child = fiber.child; + while (child !== null) { + selfDuration -= child.actualDuration || 0; + child = child.sibling; + } + + // If profiling is active, store durations for elements that were rendered during the commit. + // Note that we should do this for any fiber we performed work on, regardless of its actualDuration value. + // In some cases actualDuration might be 0 for fibers we worked on (particularly if we're using Date.now) + // In other cases (e.g. Memo) actualDuration might be greater than 0 even if we "bailed out". + const metadata = + currentCommitProfilingMetadata as CommitProfilingData; + metadata.durations.push(id, actualDuration, selfDuration); + metadata.maxActualDuration = Math.max( + metadata.maxActualDuration, + actualDuration, + ); + + if (recordChangeDescriptions) { + const changeDescription = getChangeDescription(prevFiber, fiber); + if (changeDescription !== null) { + if (metadata.changeDescriptions !== null) { + metadata.changeDescriptions.set(id, changeDescription); + } + } + } + } + } + + // If this Fiber was in the set of memoizedUpdaters we need to record + // it to be included in the description of the commit. + const fiberRoot: any = currentRoot.data.stateNode; + const updaters = fiberRoot.memoizedUpdaters; + if ( + updaters != null && + (updaters.has(fiber) || + // We check the alternate here because we're matching identity and + // prevFiber might be same as fiber. + (fiber.alternate !== null && updaters.has(fiber.alternate))) + ) { + const metadata = currentCommitProfilingMetadata as CommitProfilingData; + if (metadata.updaters === null) { + metadata.updaters = []; + } + metadata.updaters.push(instanceToSerializedElement(fiberInstance)); + } + } + } + + function instanceToSerializedElement( + instance: FiberInstance | VirtualInstance, + ): SerializedElement { + if (instance.kind === FIBER_INSTANCE) { + const fiber = instance.data; + return { + displayName: getDisplayNameForFiber(fiber) || 'Anonymous', + id: instance.id, + key: fiber.key, + type: getElementTypeForFiber(fiber), + }; + } else { + const componentInfo: any = instance.data; + return { + displayName: componentInfo.name || 'Anonymous', + id: instance.id, + key: componentInfo.key == null ? null : componentInfo.key, + type: ElementTypeVirtual, + }; + } + } + + function getChangeDescription( + prevFiber: Fiber | null, + nextFiber: Fiber, + ): ChangeDescription | null { + switch (nextFiber.tag) { + case ClassComponent: + if (prevFiber === null) { + return { + context: null, + didHooksChange: false, + isFirstMount: true, + props: null, + state: null, + }; + } else { + const data: ChangeDescription = { + context: getContextChanged(prevFiber, nextFiber), + didHooksChange: false, + isFirstMount: false, + props: getChangedKeys( + prevFiber.memoizedProps, + nextFiber.memoizedProps, + ), + state: getChangedKeys( + prevFiber.memoizedState, + nextFiber.memoizedState, + ), + }; + return data; + } + case IncompleteFunctionComponent: + case FunctionComponent: + case IndeterminateComponent: + case ForwardRef: + case MemoComponent: + case SimpleMemoComponent: + if (prevFiber === null) { + return { + context: null, + didHooksChange: false, + isFirstMount: true, + props: null, + state: null, + }; + } else { + const indices = getChangedHooksIndices( + prevFiber.memoizedState, + nextFiber.memoizedState, + ); + const data: ChangeDescription = { + context: getContextChanged(prevFiber, nextFiber), + didHooksChange: indices !== null && indices.length > 0, + isFirstMount: false, + props: getChangedKeys( + prevFiber.memoizedProps, + nextFiber.memoizedProps, + ), + state: null, + hooks: indices, + }; + // Only traverse the hooks list once, depending on what info we're returning. + return data; + } + default: + return null; + } + } + + function getChangedHooksIndices(prev: any, next: any): null | Array { + if (prev == null || next == null) { + return null; + } + + const indices = []; + let index = 0; + while (next !== null) { + if (didStatefulHookChange(prev, next)) { + indices.push(index); + } + next = next.next; + prev = prev.next; + index++; + } + + return indices; + } + + function isHookThatCanScheduleUpdate(hookObject: any) { + const queue = hookObject.queue; + if (!queue) { + return false; + } + + const boundHasOwnProperty = hasOwnProperty.bind(queue); + + // Detect the shape of useState() / useReducer() / useTransition() + // using the attributes that are unique to these hooks + // but also stable (e.g. not tied to current Lanes implementation) + // We don't check for dispatch property, because useTransition doesn't have it + if (boundHasOwnProperty('pending')) { + return true; + } + + // Detect useSyncExternalStore() + return ( + boundHasOwnProperty('value') && + boundHasOwnProperty('getSnapshot') && + typeof queue.getSnapshot === 'function' + ); + } + + function didStatefulHookChange(prev: any, next: any): boolean { + const prevMemoizedState = prev.memoizedState; + const nextMemoizedState = next.memoizedState; + + if (isHookThatCanScheduleUpdate(prev)) { + return prevMemoizedState !== nextMemoizedState; + } + + return false; + } + + function getChangedKeys(prev: any, next: any): null | Array { + if (prev == null || next == null) { + return null; + } + + const keys = new Set([...Object.keys(prev), ...Object.keys(next)]); + const changedKeys = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const key of keys) { + if (prev[key] !== next[key]) { + changedKeys.push(key); + } + } + + return changedKeys; + } + + function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean { + let prevContext = + prevFiber.dependencies && prevFiber.dependencies.firstContext; + let nextContext = + nextFiber.dependencies && nextFiber.dependencies.firstContext; + + while (prevContext && nextContext) { + // Note this only works for versions of React that support this key (e.v. 18+) + // For older versions, there's no good way to read the current context value after render has completed. + // This is because React maintains a stack of context values during render, + // but by the time DevTools is called, render has finished and the stack is empty. + if (prevContext.context !== nextContext.context) { + // If the order of context has changed, then the later context values might have + // changed too but the main reason it rerendered was earlier. Either an earlier + // context changed value but then we would have exited already. If we end up here + // it's because a state or props change caused the order of contexts used to change. + // So the main cause is not the contexts themselves. + return false; + } + if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) { + return true; + } + + prevContext = prevContext.next; + nextContext = nextContext.next; + } + return false; + } + + function didFiberRender(prevFiber: Fiber, nextFiber: Fiber): boolean { + switch (nextFiber.tag) { + case ClassComponent: + case FunctionComponent: + case ContextConsumer: + case MemoComponent: + case SimpleMemoComponent: + case ForwardRef: + // For types that execute user code, we check PerformedWork effect. + // We don't reflect bailouts (either referential or sCU) in DevTools. + // TODO: This flag is a leaked implementation detail. Once we start + // releasing DevTools in lockstep with React, we should import a + // function from the reconciler instead. + const PerformedWork = 0b000000000000000000000000001; + return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork; + // Note: ContextConsumer only gets PerformedWork effect in 16.3.3+ + // so it won't get highlighted with React 16.3.0 to 16.3.2. + default: + // For host components and other types, we compare inputs + // to determine whether something is an update. + return ( + prevFiber.memoizedProps !== nextFiber.memoizedProps || + prevFiber.memoizedState !== nextFiber.memoizedState || + prevFiber.ref !== nextFiber.ref + ); + } + } + + function getFiberFlags(fiber: Fiber): number { + // The name of this field changed from "effectTag" to "flags" + return fiber.flags !== undefined ? fiber.flags : (fiber as any).effectTag; + } + + function recordConsoleLogs( + instance: FiberInstance | VirtualInstance, + componentLogsEntry: void | ComponentLogs, + ): boolean { + if (componentLogsEntry === undefined) { + if (instance.logCount === 0) { + // Nothing has changed. + return false; + } + // Reset to zero. + instance.logCount = 0; + pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS); + pushOperation(instance.id); + pushOperation(0); + pushOperation(0); + return true; + } else { + const totalCount = + componentLogsEntry.errorsCount + componentLogsEntry.warningsCount; + if (instance.logCount === totalCount) { + // Nothing has changed. + return false; + } + // Update counts. + instance.logCount = totalCount; + pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS); + pushOperation(instance.id); + pushOperation(componentLogsEntry.errorsCount); + pushOperation(componentLogsEntry.warningsCount); + return true; + } + } + + function getStringID(string: string | null): number { + if (string === null) { + return 0; + } + const existingEntry = pendingStringTable.get(string); + if (existingEntry !== undefined) { + return existingEntry.id; + } + + const id = pendingStringTable.size + 1; + const encodedString = utfEncodeString(string); + + pendingStringTable.set(string, { + encodedString, + id, + }); + + // The string table total length needs to account both for the string length, + // and for the array item that contains the length itself. + // + // Don't use string length for this table. + // It won't work for multibyte characters (like emoji). + pendingStringTableLength += encodedString.length + 1; + + return id; + } + + function pushOperation(op: number): void { + // if (__DEV__) { + // if (!Number.isInteger(op)) { + // console.error( + // 'pushOperation() was called but the value is not an integer.', + // op, + // ); + // } + // } + pendingOperations.push(op); + } + + mountFiberRecursively(fiber, traceNearestHostComponentUpdate); + + return { + idToDevToolsInstanceMap, + currentRoot, + rootToFiberInstanceMap, + getDisplayNameForFiber, + getElementTypeForFiber, + }; +} diff --git a/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsConstants.ts b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsConstants.ts new file mode 100644 index 0000000000..33566a367a --- /dev/null +++ b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsConstants.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Flip this flag to true to enable verbose console debug logging. +export const __DEBUG__ = false; + +// Tree operation constants +export const TREE_OPERATION_ADD = 1; +export const TREE_OPERATION_REMOVE = 2; +export const TREE_OPERATION_REORDER_CHILDREN = 3; +export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4; +export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5; +export const TREE_OPERATION_REMOVE_ROOT = 6; +export const TREE_OPERATION_SET_SUBTREE_MODE = 7; + +// Profiling constants +export const PROFILING_FLAG_BASIC_SUPPORT = 0b01; +export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10; diff --git a/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsSymbols.ts b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsSymbols.ts new file mode 100644 index 0000000000..f4d5987c38 --- /dev/null +++ b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsSymbols.ts @@ -0,0 +1,32 @@ +export const MEMO_NUMBER = 0xead3; +export const MEMO_SYMBOL_STRING = 'Symbol(react.memo)'; +export const FORWARD_REF_NUMBER = 0xead0; +export const FORWARD_REF_SYMBOL_STRING = 'Symbol(react.forward_ref)'; + +export const REACT_MEMO_CACHE_SENTINEL: symbol = Symbol.for( + 'react.memo_cache_sentinel', +); + +export const CONCURRENT_MODE_NUMBER = 0xeacf; +export const CONCURRENT_MODE_SYMBOL_STRING = 'Symbol(react.concurrent_mode)'; + +export const CONTEXT_NUMBER = 0xeace; +export const CONTEXT_SYMBOL_STRING = 'Symbol(react.context)'; + +export const SERVER_CONTEXT_SYMBOL_STRING = 'Symbol(react.server_context)'; + +export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = 'Symbol(react.async_mode)'; + +export const PROVIDER_NUMBER = 0xeacd; +export const PROVIDER_SYMBOL_STRING = 'Symbol(react.provider)'; + +export const CONSUMER_SYMBOL_STRING = 'Symbol(react.consumer)'; + +export const STRICT_MODE_NUMBER = 0xeacc; +export const STRICT_MODE_SYMBOL_STRING = 'Symbol(react.strict_mode)'; + +export const PROFILER_NUMBER = 0xead2; +export const PROFILER_SYMBOL_STRING = 'Symbol(react.profiler)'; + +export const SCOPE_NUMBER = 0xead7; +export const SCOPE_SYMBOL_STRING = 'Symbol(react.scope)'; diff --git a/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsTypes.ts b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsTypes.ts new file mode 100644 index 0000000000..3d6d83ae73 --- /dev/null +++ b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsTypes.ts @@ -0,0 +1,305 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Types migrated from shared/ReactTypes.js +export type ReactCallSite = [ + string, // function name + string, // file name + number, // line number + number, // column number +]; + +export type ReactStackTrace = Array; + +export type ReactComponentInfo = { + name: string; + env?: string; + key?: null | string; + owner?: null | ReactComponentInfo; + stack?: null | ReactStackTrace; + props?: null | {[name: string]: any}; + // Stashed Data for the Specific Execution Environment. Not part of the transport protocol + debugStack?: null | Error; + debugTask?: null | any; +}; + +export type ReactDebugInfo = Array< + ReactComponentInfo | any // Simplified from the original which included other types +>; + +// Types migrated from react-reconciler/src/ReactInternalTypes.js +export type WorkTag = number; +export type Lanes = number; +export type Lane = number; +export type TypeOfMode = number; +export type Flags = number; + +// Migrated from react-devtools-shared/src/backend/types.js +export type WorkTagMap = { + CacheComponent: WorkTag; + ClassComponent: WorkTag; + ContextConsumer: WorkTag; + ContextProvider: WorkTag; + CoroutineComponent: WorkTag; + CoroutineHandlerPhase: WorkTag; + DehydratedSuspenseComponent: WorkTag; + ForwardRef: WorkTag; + Fragment: WorkTag; + FunctionComponent: WorkTag; + HostComponent: WorkTag; + HostPortal: WorkTag; + HostRoot: WorkTag; + HostHoistable: WorkTag; + HostSingleton: WorkTag; + HostText: WorkTag; + IncompleteClassComponent: WorkTag; + IncompleteFunctionComponent: WorkTag; + IndeterminateComponent: WorkTag; + LazyComponent: WorkTag; + LegacyHiddenComponent: WorkTag; + MemoComponent: WorkTag; + Mode: WorkTag; + OffscreenComponent: WorkTag; + Profiler: WorkTag; + ScopeComponent: WorkTag; + SimpleMemoComponent: WorkTag; + SuspenseComponent: WorkTag; + SuspenseListComponent: WorkTag; + TracingMarkerComponent: WorkTag; + YieldComponent: WorkTag; + Throw: WorkTag; + ViewTransitionComponent: WorkTag; + ActivityComponent: WorkTag; +}; + +// Dependencies for Fiber +export type Dependencies = { + lanes: Lanes; + firstContext: any | null; +}; + +// A simplified version of the Fiber type from react-reconciler/src/ReactInternalTypes.js +export type Fiber = { + // Tag identifying the type of fiber. + tag: WorkTag; + // Unique identifier of this child. + key: null | string; + // The value of element.type which is used to preserve the identity during + // reconciliation of this child. + elementType: any; + // The resolved function/class/ associated with this fiber. + type: any; + // The local state associated with this fiber. + stateNode: any; + // Remaining fields belong to Fiber + // The Fiber to return to after finishing processing this one. + return: Fiber | null; + // Singly Linked List Tree Structure. + child: Fiber | null; + sibling: Fiber | null; + index: number; + // The ref last used to attach this node. + ref: any; + refCleanup: null | (() => void); + // Input is the data coming into process this fiber. Arguments. Props. + pendingProps: any; + memoizedProps: any; + // A queue of state updates and callbacks. + updateQueue: any; + // The state used to create the output + memoizedState: any; + // Dependencies (contexts, events) for this fiber, if it has any + dependencies: Dependencies | null; + // Bitfield that describes properties about the fiber and its subtree. + mode: TypeOfMode; + // Effect + flags: Flags; + subtreeFlags: Flags; + deletions: Array | null; + lanes: Lanes; + childLanes: Lanes; + // This is a pooled version of a Fiber. Every fiber that gets updated will + // eventually have a pair. There are cases when we can clean up pairs to save + // memory if we need to. + alternate: Fiber | null; + // Time spent rendering this Fiber and its descendants for the current update. + actualDuration?: number; + // If the Fiber is currently active in the "render" phase, + // This marks the time at which the work began. + actualStartTime?: number; + // Duration of the most recent render time for this Fiber. + selfBaseDuration?: number; + // Sum of base times for all descendants of this Fiber. + treeBaseDuration?: number; + // DEV only fields + _debugInfo?: ReactDebugInfo | null; + _debugOwner?: ReactComponentInfo | Fiber | null; + _debugStack?: string | Error | null; + _debugTask?: any | null; + _debugNeedsRemount?: boolean; + _debugHookTypes?: Array | null; +}; + +// A simplified version of the FiberRoot type from react-reconciler/src/ReactInternalTypes.js +export type FiberRoot = { + // The type of root (legacy, batched, concurrent, etc.) + tag: number; + // Any additional information from the host associated with this root. + containerInfo: any; + // Used only by persistent updates. + pendingChildren: any; + // The currently active root fiber. This is the mutable root of the tree. + current: Fiber; + // A linked list of all roots that have pending work scheduled on them. + next: FiberRoot | null; + // Other fields omitted for simplicity +}; + +// Types migrated from react-devtools-shared/src/backend/types.js +export type BundleType = 0 | 1; // 0 = PROD, 1 = DEV + +export type HostInstance = any; + +export type Source = { + fileName: string; + lineNumber: number; + columnNumber?: number; +}; + +export type ReactRenderer = { + version: string; + rendererPackageName: string; + bundleType: BundleType; + // 16.0+ - To be removed in future versions. + findFiberByHostInstance?: (hostInstance: HostInstance) => any | null; + // 16.9+ + overrideHookState?: ( + fiber: any, + id: number, + path: Array, + value: any, + ) => void; + // 17+ + overrideHookStateDeletePath?: ( + fiber: any, + id: number, + path: Array, + ) => void; + // 17+ + overrideHookStateRenamePath?: ( + fiber: any, + id: number, + oldPath: Array, + newPath: Array, + ) => void; + // 16.7+ + overrideProps?: ( + fiber: any, + path: Array, + value: any, + ) => void; + // 17+ + overridePropsDeletePath?: (fiber: any, path: Array) => void; + // 17+ + overridePropsRenamePath?: ( + fiber: any, + oldPath: Array, + newPath: Array, + ) => void; + // 16.9+ + scheduleUpdate?: (fiber: any) => void; + setSuspenseHandler?: (shouldSuspend: (fiber: any) => boolean) => void; + // Only injected by React v16.8+ in order to support hooks inspection. + currentDispatcherRef?: any; + // Only injected by React v16.9+ in DEV mode. + // Enables DevTools to append owners-only component stack to error messages. + getCurrentFiber?: (() => any | null) | null; + // Only injected by React Flight Clients in DEV mode. + // Enables DevTools to append owners-only component stack to error messages from Server Components. + getCurrentComponentInfo?: () => any | null; + // 17.0.2+ + reconcilerVersion?: string; + // Uniquely identifies React DOM v15. + ComponentTree?: any; + // Present for React DOM v12 (possibly earlier) through v15. + Mount?: any; + // Only injected by React v17.0.3+ in DEV mode + setErrorHandler?: (shouldError: (fiber: any) => boolean | undefined) => void; + // Intentionally opaque type to avoid coupling DevTools to different Fast Refresh versions. + scheduleRefresh?: Function; + // 18.0+ + injectProfilingHooks?: (profilingHooks: any) => void; + getLaneLabelMap?: () => Map | null; +}; + +export type ChangeDescription = { + context: Array | boolean | null; + didHooksChange: boolean; + isFirstMount: boolean; + props: Array | null; + state: Array | null; + hooks?: Array | null; +}; + +export type PathFrame = { + key: string | null; + index: number; + displayName: string | null; +}; + +export type SerializedElement = { + displayName: string | null; + id: number; + key: number | string | null; + type: ElementType; +}; + +// WARNING +// The values below are referenced by ComponentFilters (which are saved via localStorage). +// Do not change them or it will break previously saved user customizations. +// If new element types are added, use new numbers rather than re-ordering existing ones. +// +// Changing these types is also a backwards breaking change for the standalone shell, +// since the frontend and backend must share the same values- +// and the backend is embedded in certain environments (like React Native). +export const ElementTypeClass = 1; +export const ElementTypeContext = 2; +export const ElementTypeFunction = 5; +export const ElementTypeForwardRef = 6; +export const ElementTypeHostComponent = 7; +export const ElementTypeMemo = 8; +export const ElementTypeOtherOrUnknown = 9; +export const ElementTypeProfiler = 10; +export const ElementTypeRoot = 11; +export const ElementTypeSuspense = 12; +export const ElementTypeSuspenseList = 13; +export const ElementTypeTracingMarker = 14; +export const ElementTypeVirtual = 15; +export const ElementTypeViewTransition = 16; +export const ElementTypeActivity = 17; + +// Different types of elements displayed in the Elements tree. +// These types may be used to visually distinguish types, +// or to enable/disable certain functionality. +export type ElementType = + | 1 + | 2 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17; + +export const StrictMode = 1; diff --git a/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsUtils.ts b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsUtils.ts new file mode 100644 index 0000000000..bbf43b1f23 --- /dev/null +++ b/compiler/packages/react-mcp-server/src/utils/reactDevTools/reactDevToolsUtils.ts @@ -0,0 +1,608 @@ +import {WorkTagMap} from './reactDevToolsTypes'; +import {compareVersions} from 'compare-versions'; +import {ReactComponentInfo} from './reactDevToolsTypes'; + +// Migrated from react-devtools-shared/src/backend/utils/index.js +export function gt(a: string = '', b: string = ''): boolean { + return compareVersions(a, b) === 1; +} + +export function gte(a: string = '', b: string = ''): boolean { + return compareVersions(a, b) > -1; +} + +// Migrated from react-devtools-shared/src/utils.js +const cachedDisplayNames: WeakMap = new WeakMap(); + +export function getWrappedDisplayName( + outerType: any, + innerType: any, + wrapperName: string, + fallbackName: string = 'Anonymous', +): string { + const displayName = outerType?.displayName; + return ( + displayName || `${wrapperName}(${getDisplayName(innerType, fallbackName)})` + ); +} + +export function getDisplayName( + type: any, + fallbackName: string = 'Anonymous', +): string { + const nameFromCache = cachedDisplayNames.get(type); + if (nameFromCache != null) { + return nameFromCache; + } + + let displayName = fallbackName; + + // The displayName property is not guaranteed to be a string. + // It's only safe to use for our purposes if it's a string. + if (typeof type.displayName === 'string') { + displayName = type.displayName; + } else if (typeof type.name === 'string' && type.name !== '') { + displayName = type.name; + } + + cachedDisplayNames.set(type, displayName); + return displayName; +} + +// Migrated from react-devtools-shared/src/backend/shared/DevToolsServerComponentLogs.js +type ComponentLogs = { + errors: Map; + errorsCount: number; + warnings: Map; + warningsCount: number; +}; + +// This keeps it around as long as the ComponentInfo is alive which +// lets the Fiber get reparented/remounted and still observe the previous errors/warnings. +// Unless we explicitly clear the logs from a Fiber. +export const componentInfoToComponentLogsMap: WeakMap< + ReactComponentInfo, + ComponentLogs +> = new WeakMap(); + +// Migrated from shared/objectIs.js +/** + * inlined Object.is polyfill to avoid requiring consumers ship their own + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is + */ +function isPolyfill(x: any, y: any) { + return ( + (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare + ); +} + +export const is: (x: any, y: any) => boolean = + typeof Object.is === 'function' ? Object.is : isPolyfill; + +// Migrated from shared/hasOwnProperty.js +export const hasOwnProperty = Object.prototype.hasOwnProperty; +import { + MEMO_NUMBER, + MEMO_SYMBOL_STRING, + FORWARD_REF_NUMBER, + FORWARD_REF_SYMBOL_STRING, + REACT_MEMO_CACHE_SENTINEL, + CONCURRENT_MODE_NUMBER, + CONCURRENT_MODE_SYMBOL_STRING, + DEPRECATED_ASYNC_MODE_SYMBOL_STRING, + PROVIDER_NUMBER, + PROVIDER_SYMBOL_STRING, + CONTEXT_NUMBER, + CONTEXT_SYMBOL_STRING, + SERVER_CONTEXT_SYMBOL_STRING, + CONSUMER_SYMBOL_STRING, + STRICT_MODE_NUMBER, + STRICT_MODE_SYMBOL_STRING, + PROFILER_NUMBER, + PROFILER_SYMBOL_STRING, + SCOPE_NUMBER, + SCOPE_SYMBOL_STRING, +} from './reactDevToolsSymbols'; +import {Fiber} from './reactDevToolsTypes'; + +// Migrated from react-devtools-shared/src/utils.js +let uidCounter: number = 0; + +export function getUID(): number { + return ++uidCounter; +} + +export function utfEncodeString(string: string): Array { + const encoded = []; + let i = 0; + let charCode; + while (i < string.length) { + charCode = string.charCodeAt(i); + // Handle multibyte unicode characters (like emoji). + if ((charCode & 0xf800) === 0xd800) { + encoded.push(surrogatePairToCodePoint(charCode, string.charCodeAt(++i))); + } else { + encoded.push(charCode); + } + ++i; + } + return encoded; +} + +function surrogatePairToCodePoint( + charCode1: number, + charCode2: number, +): number { + return ((charCode1 & 0x3ff) << 10) + (charCode2 & 0x3ff) + 0x10000; +} + +// Migrated from react-reconciler/src/ReactFiberDevToolsHook.js +export function injectProfilingHooks(profilingHooks: any): void { + // This is a simplified version of the function from ReactFiberDevToolsHook.js + // We're only implementing the bare minimum needed for the current use case +} + +type getDisplayNameForFiberType = (fiber: Fiber) => string | null; +type getTypeSymbolType = (type: any) => symbol | string | number; + +type ReactPriorityLevelsType = { + ImmediatePriority: number; + UserBlockingPriority: number; + NormalPriority: number; + LowPriority: number; + IdlePriority: number; + NoPriority: number; +}; + +export function getInternalReactConstants(version: string): { + getDisplayNameForFiber: getDisplayNameForFiberType; + getTypeSymbol: getTypeSymbolType; + ReactPriorityLevels: ReactPriorityLevelsType; + ReactTypeOfWork: WorkTagMap; + StrictModeBits: number; +} { + // ********************************************************** + // The section below is copied from files in React repo. + // Keep it in sync, and add version guards if it changes. + // + // Technically these priority levels are invalid for versions before 16.9, + // but 16.9 is the first version to report priority level to DevTools, + // so we can avoid checking for earlier versions and support pre-16.9 canary releases in the process. + let ReactPriorityLevels: ReactPriorityLevelsType = { + ImmediatePriority: 99, + UserBlockingPriority: 98, + NormalPriority: 97, + LowPriority: 96, + IdlePriority: 95, + NoPriority: 90, + }; + + if (gt(version, '17.0.2')) { + ReactPriorityLevels = { + ImmediatePriority: 1, + UserBlockingPriority: 2, + NormalPriority: 3, + LowPriority: 4, + IdlePriority: 5, + NoPriority: 0, + }; + } + + let StrictModeBits = 0; + if (gte(version, '18.0.0-alpha')) { + // 18+ + StrictModeBits = 0b011000; + } else if (gte(version, '16.9.0')) { + // 16.9 - 17 + StrictModeBits = 0b1; + } else if (gte(version, '16.3.0')) { + // 16.3 - 16.8 + StrictModeBits = 0b10; + } + + let ReactTypeOfWork: WorkTagMap = {} as WorkTagMap; + + // ********************************************************** + // The section below is copied from files in React repo. + // Keep it in sync, and add version guards if it changes. + // + // TODO Update the gt() check below to be gte() whichever the next version number is. + // Currently the version in Git is 17.0.2 (but that version has not been/may not end up being released). + if (gt(version, '17.0.1')) { + ReactTypeOfWork = { + CacheComponent: 24, // Experimental + ClassComponent: 1, + ContextConsumer: 9, + ContextProvider: 10, + CoroutineComponent: -1, // Removed + CoroutineHandlerPhase: -1, // Removed + DehydratedSuspenseComponent: 18, // Behind a flag + ForwardRef: 11, + Fragment: 7, + FunctionComponent: 0, + HostComponent: 5, + HostPortal: 4, + HostRoot: 3, + HostHoistable: 26, // In reality, 18.2+. But doesn't hurt to include it here + HostSingleton: 27, // Same as above + HostText: 6, + IncompleteClassComponent: 17, + IncompleteFunctionComponent: 28, + IndeterminateComponent: 2, // removed in 19.0.0 + LazyComponent: 16, + LegacyHiddenComponent: 23, + MemoComponent: 14, + Mode: 8, + OffscreenComponent: 22, // Experimental + Profiler: 12, + ScopeComponent: 21, // Experimental + SimpleMemoComponent: 15, + SuspenseComponent: 13, + SuspenseListComponent: 19, // Experimental + TracingMarkerComponent: 25, // Experimental - This is technically in 18 but we don't + // want to fork again so we're adding it here instead + YieldComponent: -1, // Removed + Throw: 29, + ViewTransitionComponent: 30, // Experimental + ActivityComponent: 31, + }; + } else if (gte(version, '17.0.0-alpha')) { + ReactTypeOfWork = { + CacheComponent: -1, // Doesn't exist yet + ClassComponent: 1, + ContextConsumer: 9, + ContextProvider: 10, + CoroutineComponent: -1, // Removed + CoroutineHandlerPhase: -1, // Removed + DehydratedSuspenseComponent: 18, // Behind a flag + ForwardRef: 11, + Fragment: 7, + FunctionComponent: 0, + HostComponent: 5, + HostPortal: 4, + HostRoot: 3, + HostHoistable: -1, // Doesn't exist yet + HostSingleton: -1, // Doesn't exist yet + HostText: 6, + IncompleteClassComponent: 17, + IncompleteFunctionComponent: -1, // Doesn't exist yet + IndeterminateComponent: 2, + LazyComponent: 16, + LegacyHiddenComponent: 24, + MemoComponent: 14, + Mode: 8, + OffscreenComponent: 23, // Experimental + Profiler: 12, + ScopeComponent: 21, // Experimental + SimpleMemoComponent: 15, + SuspenseComponent: 13, + SuspenseListComponent: 19, // Experimental + TracingMarkerComponent: -1, // Doesn't exist yet + YieldComponent: -1, // Removed + Throw: -1, // Doesn't exist yet + ViewTransitionComponent: -1, // Doesn't exist yet + ActivityComponent: -1, // Doesn't exist yet + }; + } else if (gte(version, '16.6.0-beta.0')) { + ReactTypeOfWork = { + CacheComponent: -1, // Doesn't exist yet + ClassComponent: 1, + ContextConsumer: 9, + ContextProvider: 10, + CoroutineComponent: -1, // Removed + CoroutineHandlerPhase: -1, // Removed + DehydratedSuspenseComponent: 18, // Behind a flag + ForwardRef: 11, + Fragment: 7, + FunctionComponent: 0, + HostComponent: 5, + HostPortal: 4, + HostRoot: 3, + HostHoistable: -1, // Doesn't exist yet + HostSingleton: -1, // Doesn't exist yet + HostText: 6, + IncompleteClassComponent: 17, + IncompleteFunctionComponent: -1, // Doesn't exist yet + IndeterminateComponent: 2, + LazyComponent: 16, + LegacyHiddenComponent: -1, + MemoComponent: 14, + Mode: 8, + OffscreenComponent: -1, // Experimental + Profiler: 12, + ScopeComponent: -1, // Experimental + SimpleMemoComponent: 15, + SuspenseComponent: 13, + SuspenseListComponent: 19, // Experimental + TracingMarkerComponent: -1, // Doesn't exist yet + YieldComponent: -1, // Removed + Throw: -1, // Doesn't exist yet + ViewTransitionComponent: -1, // Doesn't exist yet + ActivityComponent: -1, // Doesn't exist yet + }; + } else if (gte(version, '16.4.3-alpha')) { + ReactTypeOfWork = { + CacheComponent: -1, // Doesn't exist yet + ClassComponent: 2, + ContextConsumer: 11, + ContextProvider: 12, + CoroutineComponent: -1, // Removed + CoroutineHandlerPhase: -1, // Removed + DehydratedSuspenseComponent: -1, // Doesn't exist yet + ForwardRef: 13, + Fragment: 9, + FunctionComponent: 0, + HostComponent: 7, + HostPortal: 6, + HostRoot: 5, + HostHoistable: -1, // Doesn't exist yet + HostSingleton: -1, // Doesn't exist yet + HostText: 8, + IncompleteClassComponent: -1, // Doesn't exist yet + IncompleteFunctionComponent: -1, // Doesn't exist yet + IndeterminateComponent: 4, + LazyComponent: -1, // Doesn't exist yet + LegacyHiddenComponent: -1, + MemoComponent: -1, // Doesn't exist yet + Mode: 10, + OffscreenComponent: -1, // Experimental + Profiler: 15, + ScopeComponent: -1, // Experimental + SimpleMemoComponent: -1, // Doesn't exist yet + SuspenseComponent: 16, + SuspenseListComponent: -1, // Doesn't exist yet + TracingMarkerComponent: -1, // Doesn't exist yet + YieldComponent: -1, // Removed + Throw: -1, // Doesn't exist yet + ViewTransitionComponent: -1, // Doesn't exist yet + ActivityComponent: -1, // Doesn't exist yet + }; + } else { + ReactTypeOfWork = { + CacheComponent: -1, // Doesn't exist yet + ClassComponent: 2, + ContextConsumer: 12, + ContextProvider: 13, + CoroutineComponent: 7, + CoroutineHandlerPhase: 8, + DehydratedSuspenseComponent: -1, // Doesn't exist yet + ForwardRef: 14, + Fragment: 10, + FunctionComponent: 1, + HostComponent: 5, + HostPortal: 4, + HostRoot: 3, + HostHoistable: -1, // Doesn't exist yet + HostSingleton: -1, // Doesn't exist yet + HostText: 6, + IncompleteClassComponent: -1, // Doesn't exist yet + IncompleteFunctionComponent: -1, // Doesn't exist yet + IndeterminateComponent: 0, + LazyComponent: -1, // Doesn't exist yet + LegacyHiddenComponent: -1, + MemoComponent: -1, // Doesn't exist yet + Mode: 11, + OffscreenComponent: -1, // Experimental + Profiler: 15, + ScopeComponent: -1, // Experimental + SimpleMemoComponent: -1, // Doesn't exist yet + SuspenseComponent: 16, + SuspenseListComponent: -1, // Doesn't exist yet + TracingMarkerComponent: -1, // Doesn't exist yet + YieldComponent: 9, + Throw: -1, // Doesn't exist yet + ViewTransitionComponent: -1, // Doesn't exist yet + ActivityComponent: -1, // Doesn't exist yet + }; + } + // ********************************************************** + // End of copied code. + // ********************************************************** + + function getTypeSymbol(type: any): symbol | string | number { + const symbolOrNumber = + typeof type === 'object' && type !== null ? type.$$typeof : type; + + return typeof symbolOrNumber === 'symbol' + ? symbolOrNumber.toString() + : symbolOrNumber; + } + + const { + CacheComponent, + ClassComponent, + IncompleteClassComponent, + IncompleteFunctionComponent, + FunctionComponent, + IndeterminateComponent, + ForwardRef, + HostRoot, + HostHoistable, + HostSingleton, + HostComponent, + HostPortal, + HostText, + Fragment, + LazyComponent, + LegacyHiddenComponent, + MemoComponent, + OffscreenComponent, + Profiler, + ScopeComponent, + SimpleMemoComponent, + SuspenseComponent, + SuspenseListComponent, + TracingMarkerComponent, + Throw, + ViewTransitionComponent, + ActivityComponent, + } = ReactTypeOfWork; + + // TODO: any return type might be wrong + function resolveFiberType(type: any): any { + const typeSymbol = getTypeSymbol(type); + switch (typeSymbol) { + case MEMO_NUMBER: + case MEMO_SYMBOL_STRING: + // recursively resolving memo type in case of memo(forwardRef(Component)) + return resolveFiberType(type.type); + case FORWARD_REF_NUMBER: + case FORWARD_REF_SYMBOL_STRING: + return type.render; + default: + return type; + } + } + + // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods + function getDisplayNameForFiber( + fiber: Fiber, + shouldSkipForgetCheck: boolean = false, + ): string | null { + const {elementType, type, tag} = fiber; + + let resolvedType = type; + if (typeof type === 'object' && type !== null) { + resolvedType = resolveFiberType(type); + } + + let resolvedContext: any = null; + if ( + !shouldSkipForgetCheck && + // $FlowFixMe[incompatible-type] fiber.updateQueue is mixed + (fiber.updateQueue?.memoCache != null || + (Array.isArray(fiber.memoizedState?.memoizedState) && + fiber.memoizedState.memoizedState[0]?.[REACT_MEMO_CACHE_SENTINEL]) || + fiber.memoizedState?.memoizedState?.[REACT_MEMO_CACHE_SENTINEL]) + ) { + const displayNameWithoutForgetWrapper = getDisplayNameForFiber( + fiber, + true, + ); + if (displayNameWithoutForgetWrapper == null) { + return null; + } + + return `Forget(${displayNameWithoutForgetWrapper})`; + } + + switch (tag) { + case ActivityComponent: + return 'Activity'; + case CacheComponent: + return 'Cache'; + case ClassComponent: + case IncompleteClassComponent: + case IncompleteFunctionComponent: + case FunctionComponent: + case IndeterminateComponent: + return getDisplayName(resolvedType); + case ForwardRef: + return getWrappedDisplayName( + elementType, + resolvedType, + 'ForwardRef', + 'Anonymous', + ); + case HostRoot: + const fiberRoot = fiber.stateNode; + if (fiberRoot != null && fiberRoot._debugRootType !== null) { + return fiberRoot._debugRootType; + } + return null; + case HostComponent: + case HostSingleton: + case HostHoistable: + return type; + case HostPortal: + case HostText: + return null; + case Fragment: + return 'Fragment'; + case LazyComponent: + // This display name will not be user visible. + // Once a Lazy component loads its inner component, React replaces the tag and type. + // This display name will only show up in console logs when DevTools DEBUG mode is on. + return 'Lazy'; + case MemoComponent: + case SimpleMemoComponent: + // Display name in React does not use `Memo` as a wrapper but fallback name. + return getWrappedDisplayName( + elementType, + resolvedType, + 'Memo', + 'Anonymous', + ); + case LegacyHiddenComponent: + return 'LegacyHidden'; + case OffscreenComponent: + return 'Offscreen'; + case ScopeComponent: + return 'Scope'; + case SuspenseComponent: + return 'Suspense'; + case SuspenseListComponent: + return 'SuspenseList'; + case TracingMarkerComponent: + return 'TracingMarker'; + case ViewTransitionComponent: + return 'ViewTransition'; + case Throw: + return 'Throw'; + default: + const typeSymbol = getTypeSymbol(type); + + switch (typeSymbol) { + case CONCURRENT_MODE_NUMBER: + case CONCURRENT_MODE_SYMBOL_STRING: + case DEPRECATED_ASYNC_MODE_SYMBOL_STRING: + return 'ConcurrentMode'; + case PROVIDER_NUMBER: + case PROVIDER_SYMBOL_STRING: + // Grab the displayName from the type if it's available. + // If it doesn't have a displayName, we'll fall back to the generic "Context.Provider" name. + if (resolvedContext == null) { + resolvedContext = type._context || type; + } + return `${resolvedContext.displayName || 'Context'}.Provider`; + case CONTEXT_NUMBER: + case CONTEXT_SYMBOL_STRING: + case SERVER_CONTEXT_SYMBOL_STRING: + // Grab the displayName from the type if it's available. + // If it doesn't have a displayName, we'll fall back to the generic "Context.Consumer" name. + if (resolvedContext == null) { + resolvedContext = type._context || type; + } + return `${resolvedContext.displayName || 'Context'}.Consumer`; + case CONSUMER_SYMBOL_STRING: + // Grab the displayName from the type if it's available. + // If it doesn't have a displayName, we'll fall back to the generic "Context.Consumer" name. + if (resolvedContext == null) { + resolvedContext = type; + } + return `${resolvedContext.displayName || 'Context'}.Consumer`; + case STRICT_MODE_NUMBER: + case STRICT_MODE_SYMBOL_STRING: + return 'StrictMode'; + case PROFILER_NUMBER: + case PROFILER_SYMBOL_STRING: + return `Profiler`; + case SCOPE_NUMBER: + case SCOPE_SYMBOL_STRING: + return 'Scope'; + default: + // Unknown element type. + // This may mean a new element type that has not yet been added to DevTools. + return null; + } + } + } + + return { + getDisplayNameForFiber, + getTypeSymbol, + ReactPriorityLevels, + ReactTypeOfWork, + StrictModeBits, + }; +}