1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910import type {11 Thenable,12 ReactComponentInfo,13 ReactDebugInfo,14 ReactAsyncInfo,15 ReactIOInfo,16 ReactStackTrace,17 ReactCallSite,18 Wakeable,19} from 'shared/ReactTypes';2021import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';2223import {24 ComponentFilterDisplayName,25 ComponentFilterElementType,26 ComponentFilterHOC,27 ComponentFilterLocation,28 ComponentFilterEnvironmentName,29 ComponentFilterActivitySlice,30 ElementTypeClass,31 ElementTypeContext,32 ElementTypeFunction,33 ElementTypeForwardRef,34 ElementTypeHostComponent,35 ElementTypeMemo,36 ElementTypeOtherOrUnknown,37 ElementTypeProfiler,38 ElementTypeRoot,39 ElementTypeSuspense,40 ElementTypeSuspenseList,41 ElementTypeTracingMarker,42 ElementTypeViewTransition,43 ElementTypeActivity,44 ElementTypeVirtual,45 StrictMode,46 ActivityHiddenMode,47 ActivityVisibleMode,48} from 'react-devtools-shared/src/frontend/types';49import {50 deletePathInObject,51 getInObject,52 getUID,53 renamePathInObject,54 setInObject,55 utfEncodeString,56} from 'react-devtools-shared/src/utils';57import {58 formatConsoleArgumentsToSingleString,59 formatDurationToMicrosecondsGranularity,60 gte,61 serializeToString,62} from 'react-devtools-shared/src/backend/utils';63import {64 extractLocationFromComponentStack,65 extractLocationFromOwnerStack,66 parseStackTrace,67} from 'react-devtools-shared/src/backend/utils/parseStackTrace';68import {69 cleanForBridge,70 copyWithDelete,71 copyWithRename,72 copyWithSet,73 getEffectDurations,74} from '../utils';75import {76 __DEBUG__,77 PROFILING_FLAG_BASIC_SUPPORT,78 PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,79 TREE_OPERATION_ADD,80 TREE_OPERATION_REMOVE,81 TREE_OPERATION_REORDER_CHILDREN,82 TREE_OPERATION_SET_SUBTREE_MODE,83 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,84 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,85 TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,86 SUSPENSE_TREE_OPERATION_ADD,87 SUSPENSE_TREE_OPERATION_REMOVE,88 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,89 SUSPENSE_TREE_OPERATION_RESIZE,90 SUSPENSE_TREE_OPERATION_SUSPENDERS,91 UNKNOWN_SUSPENDERS_NONE,92 UNKNOWN_SUSPENDERS_REASON_PRODUCTION,93 UNKNOWN_SUSPENDERS_REASON_OLD_VERSION,94 UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE,95} from '../../constants';96import {inspectHooksOfFiber} from 'react-debug-tools';97import {98 CONCURRENT_MODE_NUMBER,99 CONCURRENT_MODE_SYMBOL_STRING,100 DEPRECATED_ASYNC_MODE_SYMBOL_STRING,101 PROVIDER_NUMBER,102 PROVIDER_SYMBOL_STRING,103 CONTEXT_NUMBER,104 CONTEXT_SYMBOL_STRING,105 CONSUMER_SYMBOL_STRING,106 STRICT_MODE_NUMBER,107 STRICT_MODE_SYMBOL_STRING,108 PROFILER_NUMBER,109 PROFILER_SYMBOL_STRING,110 LAZY_SYMBOL_STRING,111 REACT_OPTIMISTIC_KEY,112} from '../shared/ReactSymbols';113import {enableStyleXFeatures} from 'react-devtools-feature-flags';114115import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs';116117import {getIODescription} from 'shared/ReactIODescription';118119import {120 getPublicInstance,121 getNativeTag,122 getCurrentTime,123} from 'react-devtools-shared/src/backend/DevToolsNativeHost';124import {125 isError,126 rootSupportsProfiling,127 isErrorBoundary,128 getSecondaryEnvironmentName,129 areEqualRects,130} from './shared/DevToolsFiberInspection';131import {132 didFiberRender,133 getContextChanged,134 getChangedHooksIndices,135 getChangedKeys,136} from './shared/DevToolsFiberChangeDetection';137import {getInternalReactConstants} from './shared/DevToolsFiberInternalReactConstants';138import {139 ioExistsInSuspenseAncestor,140 getAwaitInSuspendedByFromIO,141 getVirtualEndTime,142} from './shared/DevToolsFiberSuspense';143144import {145 getStackByFiberInDevAndProd,146 getOwnerStackByFiberInDev,147 supportsOwnerStacks,148 supportsConsoleTasks,149} from './DevToolsFiberComponentStack';150151import {getStyleXData} from '../StyleX/utils';152153import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';154import type {155 ChangeDescription,156 CommitDataBackend,157 DevToolsHook,158 InspectedElement,159 InspectedElementPayload,160 InstanceAndStyle,161 HostInstance,162 PathFrame,163 PathMatch,164 ProfilingDataBackend,165 ProfilingDataForRootBackend,166 ReactRenderer,167 Rect,168 RendererInterface,169 SerializedElement,170 SerializedAsyncInfo,171 ProfilingSettings,172} from '../types';173import type {174 ComponentFilter,175 ActivitySliceFilter,176 ElementType,177 Plugins,178} from 'react-devtools-shared/src/frontend/types';179import type {ReactFunctionLocation} from 'shared/ReactTypes';180import type {181 FiberInstance,182 FilteredFiberInstance,183 VirtualInstance,184 DevToolsInstance,185 SuspenseNode,186} from './shared/DevToolsFiberTypes';187import {188 FIBER_INSTANCE,189 VIRTUAL_INSTANCE,190 FILTERED_FIBER_INSTANCE,191} from './shared/DevToolsFiberTypes';192import {getDispatcherRef} from '../shared/DevToolsReactDispatcher';193import {getSourceLocationByFiber} from './DevToolsFiberComponentStack';194import {formatOwnerStack} from '../shared/DevToolsOwnerStack';195196function createFiberInstance(fiber: Fiber): FiberInstance {197 return {198 kind: FIBER_INSTANCE,199 id: getUID(),200 parent: null,201 firstChild: null,202 nextSibling: null,203 source: null,204 logCount: 0,205 treeBaseDuration: 0,206 suspendedBy: null,207 suspenseNode: null,208 data: fiber,209 };210}211212// This is used to represent a filtered Fiber but still lets us find its host instance.213function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {214 return {215 kind: FILTERED_FIBER_INSTANCE,216 id: 0,217 parent: null,218 firstChild: null,219 nextSibling: null,220 source: null,221 logCount: 0,222 treeBaseDuration: 0,223 suspendedBy: null,224 suspenseNode: null,225 data: fiber,226 } as any;227}228229function createVirtualInstance(230 debugEntry: ReactComponentInfo,231): VirtualInstance {232 return {233 kind: VIRTUAL_INSTANCE,234 id: getUID(),235 parent: null,236 firstChild: null,237 nextSibling: null,238 source: null,239 logCount: 0,240 treeBaseDuration: 0,241 suspendedBy: null,242 suspenseNode: null,243 data: debugEntry,244 };245}246247// Update flags need to be propagated up until the caller that put the corresponding248// node on the stack.249// If you push a new node, you need to handle ShouldResetChildren when you pop it.250// If you push a new Suspense node, you need to handle ShouldResetSuspenseChildren when you pop it.251type UpdateFlags = number;252const NoUpdate = /* */ 0b000;253const ShouldResetChildren = /* */ 0b001;254const ShouldResetSuspenseChildren = /* */ 0b010;255const ShouldResetParentSuspenseChildren = /* */ 0b100;256257function createSuspenseNode(258 instance: FiberInstance | FilteredFiberInstance,259): SuspenseNode {260 return (instance.suspenseNode = {261 instance: instance,262 parent: null,263 firstChild: null,264 nextSibling: null,265 rects: null,266 suspendedBy: new Map(),267 environments: new Map(),268 endTime: 0,269 hasUniqueSuspenders: false,270 hasUnknownSuspenders: false,271 });272}273274// All environment names we've seen so far. This lets us create a list of filters to apply.275// This should ideally include env of filtered Components too so that you can add those as276// filters at the same time as removing some other filter.277const knownEnvironmentNames: Set<string> = new Set();278279// Map of FiberRoot to their root FiberInstance.280const rootToFiberInstanceMap: Map<FiberRoot, FiberInstance> = new Map();281282// Map of id to FiberInstance or VirtualInstance.283// This Map is used to e.g. get the display name for a Fiber or schedule an update,284// operations that should be the same whether the current and work-in-progress Fiber is used.285const idToDevToolsInstanceMap: Map<286 FiberInstance['id'] | VirtualInstance['id'],287 FiberInstance | VirtualInstance,288> = new Map();289290let focusedActivityID: null | FiberInstance['id'] = null;291let focusedActivity: null | Fiber = null;292293const idToSuspenseNodeMap: Map<FiberInstance['id'], SuspenseNode> = new Map();294295// Map of canonical HostInstances to the nearest parent DevToolsInstance.296const publicInstanceToDevToolsInstanceMap: Map<HostInstance, DevToolsInstance> =297 new Map();298// Map of resource DOM nodes to all the nearest DevToolsInstances that depend on it.299const hostResourceToDevToolsInstanceMap: Map<300 HostInstance,301 Set<DevToolsInstance>,302> = new Map();303304function aquireHostInstance(305 nearestInstance: DevToolsInstance,306 hostInstance: HostInstance,307): void {308 const publicInstance = getPublicInstance(hostInstance);309 publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance);310}311312function releaseHostInstance(313 nearestInstance: DevToolsInstance,314 hostInstance: HostInstance,315): void {316 const publicInstance = getPublicInstance(hostInstance);317 if (318 publicInstanceToDevToolsInstanceMap.get(publicInstance) === nearestInstance319 ) {320 publicInstanceToDevToolsInstanceMap.delete(publicInstance);321 }322}323324function aquireHostResource(325 nearestInstance: DevToolsInstance,326 resource: ?{instance?: HostInstance},327): void {328 const hostInstance = resource && resource.instance;329 if (hostInstance) {330 const publicInstance = getPublicInstance(hostInstance);331 let resourceInstances =332 hostResourceToDevToolsInstanceMap.get(publicInstance);333 if (resourceInstances === undefined) {334 resourceInstances = new Set();335 hostResourceToDevToolsInstanceMap.set(publicInstance, resourceInstances);336 // Store the first match in the main map for quick access when selecting DOM node.337 publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance);338 }339 resourceInstances.add(nearestInstance);340 }341}342343function releaseHostResource(344 nearestInstance: DevToolsInstance,345 resource: ?{instance?: HostInstance},346): void {347 const hostInstance = resource && resource.instance;348 if (hostInstance) {349 const publicInstance = getPublicInstance(hostInstance);350 const resourceInstances =351 hostResourceToDevToolsInstanceMap.get(publicInstance);352 if (resourceInstances !== undefined) {353 resourceInstances.delete(nearestInstance);354 if (resourceInstances.size === 0) {355 hostResourceToDevToolsInstanceMap.delete(publicInstance);356 publicInstanceToDevToolsInstanceMap.delete(publicInstance);357 } else if (358 publicInstanceToDevToolsInstanceMap.get(publicInstance) ===359 nearestInstance360 ) {361 // This was the first one. Store the next first one in the main map for easy access.362 // eslint-disable-next-line no-for-of-loops/no-for-of-loops363 for (const firstInstance of resourceInstances) {364 publicInstanceToDevToolsInstanceMap.set(365 publicInstance,366 firstInstance,367 );368 break;369 }370 }371 }372 }373}374375export function attach(376 hook: DevToolsHook,377 rendererID: number,378 renderer: ReactRenderer,379 global: Object,380 shouldStartProfilingNow: boolean,381 profilingSettings: ProfilingSettings,382 componentFiltersOrComponentFiltersPromise:383 | Array<ComponentFilter>384 | Promise<Array<ComponentFilter>>,385): RendererInterface {386 // Newer versions of the reconciler package also specific reconciler version.387 // If that version number is present, use it.388 // Third party renderer versions may not match the reconciler version,389 // and the latter is what's important in terms of tags and symbols.390 const version = renderer.reconcilerVersion || renderer.version;391392 const {393 getDisplayNameForFiber,394 getTypeSymbol,395 ReactPriorityLevels,396 ReactTypeOfWork,397 StrictModeBits,398 SuspenseyImagesMode,399 } = getInternalReactConstants(version);400 const {401 ActivityComponent,402 ClassComponent,403 DehydratedSuspenseComponent,404 ForwardRef,405 Fragment,406 FunctionComponent,407 HostRoot,408 HostHoistable,409 HostSingleton,410 HostPortal,411 HostComponent,412 HostText,413 IncompleteClassComponent,414 IncompleteFunctionComponent,415 IndeterminateComponent,416 LegacyHiddenComponent,417 MemoComponent,418 OffscreenComponent,419 SimpleMemoComponent,420 SuspenseComponent,421 SuspenseListComponent,422 TracingMarkerComponent,423 Throw,424 ViewTransitionComponent,425 } = ReactTypeOfWork;426 const {427 ImmediatePriority,428 UserBlockingPriority,429 NormalPriority,430 LowPriority,431 IdlePriority,432 NoPriority,433 } = ReactPriorityLevels;434435 const {436 overrideHookState,437 overrideHookStateDeletePath,438 overrideHookStateRenamePath,439 overrideProps,440 overridePropsDeletePath,441 overridePropsRenamePath,442 scheduleRefresh,443 setErrorHandler,444 setSuspenseHandler,445 scheduleUpdate,446 scheduleRetry,447 getCurrentFiber,448 } = renderer;449 const supportsTogglingError =450 typeof setErrorHandler === 'function' &&451 typeof scheduleUpdate === 'function';452 const supportsTogglingSuspense =453 typeof setSuspenseHandler === 'function' &&454 typeof scheduleUpdate === 'function';455 const supportsPerformanceTracks = gte(version, '19.2.0');456457 if (typeof scheduleRefresh === 'function') {458 // When Fast Refresh updates a component, the frontend may need to purge cached information.459 // For example, ASTs cached for the component (for named hooks) may no longer be valid.460 // Send a signal to the frontend to purge this cached information.461 // The "fastRefreshScheduled" dispatched is global (not Fiber or even Renderer specific).462 // This is less effecient since it means the front-end will need to purge the entire cache,463 // but this is probably an okay trade off in order to reduce coupling between the DevTools and Fast Refresh.464 renderer.scheduleRefresh = (...args) => {465 try {466 hook.emit('fastRefreshScheduled');467 } finally {468 return scheduleRefresh(...args);469 }470 };471 }472473 type ComponentLogs = {474 errors: Map<string, number>,475 errorsCount: number,476 warnings: Map<string, number>,477 warningsCount: number,478 };479 // Tracks Errors/Warnings logs added to a Fiber. They are added before the commit and get480 // picked up a FiberInstance. This keeps it around as long as the Fiber is alive which481 // lets the Fiber get reparented/remounted and still observe the previous errors/warnings.482 // Unless we explicitly clear the logs from a Fiber.483 const fiberToComponentLogsMap: WeakMap<Fiber, ComponentLogs> = new WeakMap();484 // Tracks whether we've performed a commit since the last log. This is used to know485 // whether we received any new logs between the commit and post commit phases. I.e.486 // if any passive effects called console.warn / console.error.487 let needsToFlushComponentLogs = false;488489 function bruteForceFlushErrorsAndWarnings(root: FiberInstance) {490 // Refresh error/warning count for all mounted unfiltered Fibers.491 let hasChanges = false;492 // eslint-disable-next-line no-for-of-loops/no-for-of-loops493 for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {494 if (devtoolsInstance.kind === FIBER_INSTANCE) {495 const fiber = devtoolsInstance.data;496 const componentLogsEntry = fiberToComponentLogsMap.get(fiber);497 const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);498 if (changed) {499 hasChanges = true;500 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);501 }502 } else {503 // Virtual Instances cannot log in passive effects and so never appear here.504 }505 }506 if (hasChanges) {507 flushPendingEvents(root);508 }509 }510511 function clearErrorsAndWarnings() {512 // Note, this only clears logs for Fibers that have instances. If they're filtered513 // and then mount, the logs are there. Ensuring we only clear what you've seen.514 // If we wanted to clear the whole set, we'd replace fiberToComponentLogsMap with a515 // new WeakMap. It's unclear whether we should clear componentInfoToComponentLogsMap516 // since it's shared by other renderers but presumably it would.517518 // eslint-disable-next-line no-for-of-loops/no-for-of-loops519 for (const devtoolsInstance of idToDevToolsInstanceMap.values()) {520 if (devtoolsInstance.kind === FIBER_INSTANCE) {521 const fiber = devtoolsInstance.data;522 fiberToComponentLogsMap.delete(fiber);523 if (fiber.alternate) {524 fiberToComponentLogsMap.delete(fiber.alternate);525 }526 } else {527 componentInfoToComponentLogsMap.delete(devtoolsInstance.data);528 }529 const changed = recordConsoleLogs(devtoolsInstance, undefined);530 if (changed) {531 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);532 }533 }534 flushPendingEvents(null);535 }536537 function clearConsoleLogsHelper(instanceID: number, type: 'error' | 'warn') {538 const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID);539 if (devtoolsInstance !== undefined) {540 let componentLogsEntry;541 if (devtoolsInstance.kind === FIBER_INSTANCE) {542 const fiber = devtoolsInstance.data;543 componentLogsEntry = fiberToComponentLogsMap.get(fiber);544545 if (componentLogsEntry === undefined && fiber.alternate !== null) {546 componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);547 }548 } else {549 const componentInfo = devtoolsInstance.data;550 componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo);551 }552 if (componentLogsEntry !== undefined) {553 if (type === 'error') {554 componentLogsEntry.errors.clear();555 componentLogsEntry.errorsCount = 0;556 } else {557 componentLogsEntry.warnings.clear();558 componentLogsEntry.warningsCount = 0;559 }560 const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry);561 if (changed) {562 flushPendingEvents(null);563 updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id);564 }565 }566 }567 }568569 function clearErrorsForElementID(instanceID: number) {570 clearConsoleLogsHelper(instanceID, 'error');571 }572573 function clearWarningsForElementID(instanceID: number) {574 clearConsoleLogsHelper(instanceID, 'warn');575 }576577 function updateMostRecentlyInspectedElementIfNecessary(578 fiberID: number,579 ): void {580 if (581 mostRecentlyInspectedElement !== null &&582 mostRecentlyInspectedElement.id === fiberID583 ) {584 hasElementUpdatedSinceLastInspected = true;585 }586 }587588 function getComponentStack(589 topFrame: Error,590 ): null | {enableOwnerStacks: boolean, componentStack: string} {591 if (getCurrentFiber == null) {592 // Expected this to be part of the renderer. Ignore.593 return null;594 }595 const current = getCurrentFiber();596 if (current === null) {597 // Outside of our render scope.598 return null;599 }600601 if (supportsConsoleTasks(current)) {602 // This will be handled natively by console.createTask. No need for603 // DevTools to add it.604 return null;605 }606607 const dispatcherRef = getDispatcherRef(renderer);608 if (dispatcherRef === undefined) {609 return null;610 }611612 const enableOwnerStacks = supportsOwnerStacks(current);613 let componentStack = '';614 if (enableOwnerStacks) {615 // Prefix the owner stack with the current stack. I.e. what called616 // console.error. While this will also be part of the native stack,617 // it is hidden and not presented alongside this argument so we print618 // them all together.619 const topStackFrames = formatOwnerStack(topFrame);620 if (topStackFrames) {621 componentStack += '\n' + topStackFrames;622 }623 componentStack += getOwnerStackByFiberInDev(624 ReactTypeOfWork,625 current,626 dispatcherRef,627 );628 } else {629 componentStack = getStackByFiberInDevAndProd(630 ReactTypeOfWork,631 current,632 dispatcherRef,633 );634 }635 return {enableOwnerStacks, componentStack};636 }637638 // Called when an error or warning is logged during render, commit, or passive (including unmount functions).639 function onErrorOrWarning(640 type: 'error' | 'warn',641 args: $ReadOnlyArray<any>,642 ): void {643 if (getCurrentFiber == null) {644 // Expected this to be part of the renderer. Ignore.645 return;646 }647 const fiber = getCurrentFiber();648 if (fiber === null) {649 // Outside of our render scope.650 return;651 }652 if (type === 'error') {653 // if this is an error simulated by us to trigger error boundary, ignore654 if (655 forceErrorForFibers.get(fiber) === true ||656 (fiber.alternate !== null &&657 forceErrorForFibers.get(fiber.alternate) === true)658 ) {659 return;660 }661 }662663 // We can't really use this message as a unique key, since we can't distinguish664 // different objects in this implementation. We have to delegate displaying of the objects665 // to the environment, the browser console, for example, so this is why this should be kept666 // as an array of arguments, instead of the plain string.667 // [Warning: %o, {...}] and [Warning: %o, {...}] will be considered as the same message,668 // even if objects are different669 const message = formatConsoleArgumentsToSingleString(...args);670671 // Track the warning/error for later.672 let componentLogsEntry = fiberToComponentLogsMap.get(fiber);673 if (componentLogsEntry === undefined && fiber.alternate !== null) {674 componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);675 if (componentLogsEntry !== undefined) {676 // Use the same set for both Fibers.677 fiberToComponentLogsMap.set(fiber, componentLogsEntry);678 }679 }680 if (componentLogsEntry === undefined) {681 componentLogsEntry = {682 errors: new Map(),683 errorsCount: 0 as number,684 warnings: new Map(),685 warningsCount: 0 as number,686 };687 fiberToComponentLogsMap.set(fiber, componentLogsEntry);688 }689690 const messageMap =691 type === 'error'692 ? componentLogsEntry.errors693 : componentLogsEntry.warnings;694 const count = messageMap.get(message) || 0;695 messageMap.set(message, count + 1);696 if (type === 'error') {697 componentLogsEntry.errorsCount++;698 } else {699 componentLogsEntry.warningsCount++;700 }701702 // The changes will be flushed later when we commit.703704 // If the log happened in a passive effect, then this happens after we've705 // already committed the new tree so the change won't show up until we rerender706 // that component again. We need to visit a Component with passive effects in707 // handlePostCommitFiberRoot again to ensure that we flush the changes after passive.708 needsToFlushComponentLogs = true;709 }710711 function debug(712 name: string,713 instance: DevToolsInstance,714 parentInstance: null | DevToolsInstance,715 extraString: string = '',716 ): void {717 // $FlowFixMe[constant-condition]718 if (__DEBUG__) {719 const displayName =720 instance.kind === VIRTUAL_INSTANCE721 ? instance.data.name || 'null'722 : instance.data.tag +723 ':' +724 (getDisplayNameForFiber(instance.data) || 'null');725726 const maybeID =727 instance.kind === FILTERED_FIBER_INSTANCE ? '<no id>' : instance.id;728729 const parentDisplayName =730 parentInstance === null731 ? ''732 : parentInstance.kind === VIRTUAL_INSTANCE733 ? parentInstance.data.name || 'null'734 : parentInstance.data.tag +735 ':' +736 (getDisplayNameForFiber(parentInstance.data) || 'null');737738 const maybeParentID =739 parentInstance === null ||740 parentInstance.kind === FILTERED_FIBER_INSTANCE741 ? '<no id>'742 : parentInstance.id;743744 console.groupCollapsed(745 `[renderer] %c${name} %c${displayName} (${maybeID}) %c${746 parentInstance ? `${parentDisplayName} (${maybeParentID})` : ''747 } %c${extraString}`,748 'color: red; font-weight: bold;',749 'color: blue;',750 'color: purple;',751 'color: black;',752 );753 console.log(new Error().stack.split('\n').slice(1).join('\n'));754 console.groupEnd();755 }756 }757758 // eslint-disable-next-line no-unused-vars759 function debugTree(instance: DevToolsInstance, indent: number = 0) {760 // $FlowFixMe[constant-condition]761 if (__DEBUG__) {762 const name =763 (instance.kind !== VIRTUAL_INSTANCE764 ? getDisplayNameForFiber(instance.data)765 : instance.data.name) || '';766 console.log(767 ' '.repeat(indent) +768 '- ' +769 (instance.kind === FILTERED_FIBER_INSTANCE ? 0 : instance.id) +770 ' (' +771 name +772 ')',773 'parent',774 instance.parent === null775 ? ' '776 : instance.parent.kind === FILTERED_FIBER_INSTANCE777 ? 0778 : instance.parent.id,779 'next',780 instance.nextSibling === null ? ' ' : instance.nextSibling.id,781 );782 let child = instance.firstChild;783 while (child !== null) {784 debugTree(child, indent + 1);785 child = child.nextSibling;786 }787 }788 }789790 // Configurable Components tree filters.791 const hideElementsWithDisplayNames: Set<RegExp> = new Set();792 const hideElementsWithPaths: Set<RegExp> = new Set();793 const hideElementsWithTypes: Set<ElementType> = new Set();794 const hideElementsWithEnvs: Set<string> = new Set();795 let isInFocusedActivity: boolean = true;796797 // Highlight updates798 let traceUpdatesEnabled: boolean = false;799 const traceUpdatesForNodes: Set<HostInstance> = new Set();800801 function applyComponentFilters(802 componentFilters: Array<ComponentFilter>,803 nextActivitySlice: null | Fiber,804 ) {805 hideElementsWithTypes.clear();806 hideElementsWithDisplayNames.clear();807 hideElementsWithPaths.clear();808 hideElementsWithEnvs.clear();809 const previousFocusedActivityID = focusedActivityID;810 focusedActivityID = null;811 focusedActivity = null;812 // Consider everything in the slice by default813 isInFocusedActivity = true;814815 componentFilters.forEach(componentFilter => {816 if (!componentFilter.isEnabled) {817 return;818 }819820 switch (componentFilter.type) {821 case ComponentFilterDisplayName:822 if (componentFilter.isValid && componentFilter.value !== '') {823 hideElementsWithDisplayNames.add(824 new RegExp(componentFilter.value, 'i'),825 );826 }827 break;828 case ComponentFilterElementType:829 hideElementsWithTypes.add(componentFilter.value);830 break;831 case ComponentFilterLocation:832 if (componentFilter.isValid && componentFilter.value !== '') {833 hideElementsWithPaths.add(new RegExp(componentFilter.value, 'i'));834 }835 break;836 case ComponentFilterHOC:837 hideElementsWithDisplayNames.add(new RegExp('\\('));838 break;839 case ComponentFilterEnvironmentName:840 hideElementsWithEnvs.add(componentFilter.value);841 break;842 case ComponentFilterActivitySlice:843 if (844 nextActivitySlice !== null &&845 nextActivitySlice.tag === ActivityComponent846 ) {847 focusedActivity = nextActivitySlice;848 isInFocusedActivity = false;849 if (componentFilter.rendererID !== rendererID) {850 // We filtered an Activity from another renderer.851 // We need to restore the instance ID since we won't be mounting it852 // in this renderer.853 focusedActivityID = previousFocusedActivityID;854 }855 } else {856 // We're not filtering by activity slice after all.857 // Don't mark the filter as disabled here.858 // Otherwise updateComponentFilters() will think no enabled filter was changed.859 }860 break;861 default:862 console.warn(863 `Invalid component filter type "${componentFilter.type}"`,864 );865 break;866 }867 });868 }869870 if (Array.isArray(componentFiltersOrComponentFiltersPromise)) {871 applyComponentFilters(componentFiltersOrComponentFiltersPromise, null);872 } else {873 componentFiltersOrComponentFiltersPromise.then(componentFilters => {874 applyComponentFilters(componentFilters, null);875 });876 }877878 // If necessary, we can revisit optimizing this operation.879 // For example, we could add a new recursive unmount tree operation.880 // The unmount operations are already significantly smaller than mount operations though.881 // This is something to keep in mind for later.882 function updateComponentFilters(componentFilters: Array<ComponentFilter>) {883 if (isProfiling) {884 // Re-mounting a tree while profiling is in progress might break a lot of assumptions.885 // If necessary, we could support this- but it doesn't seem like a necessary use case.886 // Supporting change of filters while profiling would require a refactor887 // to flush after each root instead of at the end.888 throw Error('Cannot modify filter preferences while profiling');889 }890891 const previousForcedFallbacks =892 forceFallbackForFibers.size > 0 ? new Set(forceFallbackForFibers) : null;893 const previousForcedErrors =894 forceErrorForFibers.size > 0 ? new Map(forceErrorForFibers) : null;895896 // The ID will be based on the old tree. We need to find the Fiber based on897 // that ID before we unmount everything. We set the activity slice ID once898 // we mount it again.899 let nextFocusedActivity: null | Fiber = null;900 let focusedActivityFilter: null | ActivitySliceFilter = null;901 for (let i = 0; i < componentFilters.length; i++) {902 const filter = componentFilters[i];903 if (filter.type === ComponentFilterActivitySlice && filter.isEnabled) {904 focusedActivityFilter = filter;905 const instance = idToDevToolsInstanceMap.get(filter.activityID);906 if (instance !== undefined && instance.kind === FIBER_INSTANCE) {907 nextFocusedActivity = instance.data;908 }909 }910 }911912 // Recursively unmount all roots.913 hook.getFiberRoots(rendererID).forEach(root => {914 const rootInstance = rootToFiberInstanceMap.get(root);915 if (rootInstance === undefined) {916 throw new Error(917 'Expected the root instance to already exist when applying filters',918 );919 }920 currentRoot = rootInstance;921 unmountInstanceRecursively(rootInstance);922 rootToFiberInstanceMap.delete(root);923 currentRoot = null as any;924 });925926 if (927 nextFocusedActivity !== focusedActivity &&928 (focusedActivityFilter === null ||929 focusedActivityFilter.rendererID === rendererID)930 ) {931 // When we find the applied instance during mount we will send the actual ID.932 // Otherwise 0 will indicate that we unfocused the activity slice.933 pushOperation(TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE);934 pushOperation(0);935 }936 applyComponentFilters(componentFilters, nextFocusedActivity);937938 // Reset pseudo counters so that new path selections will be persisted.939 rootDisplayNameCounter.clear();940941 // We just cleared all the forced states. Schedule updates on the affected Fibers942 // so that we get their initial states again according to the new filters.943 if (typeof scheduleUpdate === 'function') {944 if (previousForcedFallbacks !== null) {945 // eslint-disable-next-line no-for-of-loops/no-for-of-loops946 for (const fiber of previousForcedFallbacks) {947 if (typeof scheduleRetry === 'function') {948 scheduleRetry(fiber);949 } else {950 scheduleUpdate(fiber);951 }952 }953 }954 if (955 previousForcedErrors !== null &&956 typeof setErrorHandler === 'function'957 ) {958 // Unlike for Suspense, disabling the forced error state requires setting959 // the status to false first. `shouldErrorFiberAccordingToMap` will clear960 // the Fibers later.961 setErrorHandler(shouldErrorFiberAccordingToMap);962 // eslint-disable-next-line no-for-of-loops/no-for-of-loops963 for (const [fiber, shouldError] of previousForcedErrors) {964 forceErrorForFibers.set(fiber, false);965 if (shouldError) {966 if (typeof scheduleRetry === 'function') {967 scheduleRetry(fiber);968 } else {969 scheduleUpdate(fiber);970 }971 }972 }973 }974 }975976 // Recursively re-mount all roots with new filter criteria applied.977 hook.getFiberRoots(rendererID).forEach(root => {978 const current = root.current;979 const newRoot = createFiberInstance(current);980 rootToFiberInstanceMap.set(root, newRoot);981 idToDevToolsInstanceMap.set(newRoot.id, newRoot);982983 // Before the traversals, remember to start tracking984 // our path in case we have selection to restore.985 if (trackedPath !== null) {986 mightBeOnTrackedPath = true;987 }988989 currentRoot = newRoot;990 setRootPseudoKey(currentRoot.id, root.current);991 mountFiberRecursively(root.current, false);992 currentRoot = null as any;993 });994995 // We need to write back the new ID for the focused Fiber.996 // Otherwise subsequent filter applications will try to focus based on the old ID.997 // This is also relevant to filter across renderers.998 if (focusedActivityFilter !== null && focusedActivityID !== null) {999 focusedActivityFilter.activityID = focusedActivityID;1000 }10011002 // We're not profiling so it's safe to flush without a specific root.1003 flushPendingEvents(null);10041005 needsToFlushComponentLogs = false;1006 }10071008 function getEnvironmentNames(): Array<string> {1009 return Array.from(knownEnvironmentNames);1010 }10111012 function isFiberHydrated(fiber: Fiber): boolean {1013 if (OffscreenComponent === -1) {1014 throw new Error('not implemented for legacy suspense');1015 }1016 switch (fiber.tag) {1017 case HostRoot:1018 const rootState = fiber.memoizedState;1019 return !rootState.isDehydrated;1020 case SuspenseComponent:1021 const suspenseState = fiber.memoizedState;1022 return suspenseState === null || suspenseState.dehydrated === null;1023 default:1024 throw new Error('not implemented for work tag ' + fiber.tag);1025 }1026 }10271028 function shouldFilterVirtual(1029 data: ReactComponentInfo,1030 secondaryEnv: null | string,1031 ): boolean {1032 if (!isInFocusedActivity) {1033 return true;1034 }10351036 // For purposes of filtering Server Components are always Function Components.1037 // Environment will be used to filter Server vs Client.1038 // Technically they can be forwardRef and memo too but those filters will go away1039 // as those become just plain user space function components like any HoC.1040 if (hideElementsWithTypes.has(ElementTypeFunction)) {1041 return true;1042 }10431044 if (hideElementsWithDisplayNames.size > 0) {1045 const displayName = data.name;1046 if (displayName != null) {1047 // eslint-disable-next-line no-for-of-loops/no-for-of-loops1048 for (const displayNameRegExp of hideElementsWithDisplayNames) {1049 if (displayNameRegExp.test(displayName)) {1050 return true;1051 }1052 }1053 }1054 }10551056 if (1057 (data.env == null || hideElementsWithEnvs.has(data.env)) &&1058 (secondaryEnv === null || hideElementsWithEnvs.has(secondaryEnv))1059 ) {1060 // If a Component has two environments, you have to filter both for it not to appear.1061 return true;1062 }10631064 return false;1065 }10661067 // NOTICE Keep in sync with get*ForFiber methods1068 function shouldFilterFiber(fiber: Fiber): boolean {1069 const {tag, type, key} = fiber;10701071 // It is never valid to filter the root element.1072 if (tag !== HostRoot && !isInFocusedActivity) {1073 return true;1074 }10751076 switch (tag) {1077 case DehydratedSuspenseComponent:1078 // TODO: ideally we would show dehydrated Suspense immediately.1079 // However, it has some special behavior (like disconnecting1080 // an alternate and turning into real Suspense) which breaks DevTools.1081 // For now, ignore it, and only show it once it gets hydrated.1082 // https://github.com/bvaughn/react-devtools-experimental/issues/1971083 return true;1084 case HostPortal:1085 case HostText:1086 case LegacyHiddenComponent:1087 case OffscreenComponent:1088 case Throw:1089 return true;1090 case HostRoot:1091 // It is never valid to filter the root element.1092 return false;1093 case Fragment:1094 return key === null;1095 default:1096 const typeSymbol = getTypeSymbol(type);10971098 switch (typeSymbol) {1099 case CONCURRENT_MODE_NUMBER:1100 case CONCURRENT_MODE_SYMBOL_STRING:1101 case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:1102 case STRICT_MODE_NUMBER:1103 case STRICT_MODE_SYMBOL_STRING:1104 return true;1105 default:1106 break;1107 }1108 }11091110 const elementType = getElementTypeForFiber(fiber);1111 if (hideElementsWithTypes.has(elementType)) {1112 return true;1113 }11141115 if (hideElementsWithDisplayNames.size > 0) {1116 const displayName = getDisplayNameForFiber(fiber);1117 if (displayName != null) {1118 // eslint-disable-next-line no-for-of-loops/no-for-of-loops1119 for (const displayNameRegExp of hideElementsWithDisplayNames) {1120 if (displayNameRegExp.test(displayName)) {1121 return true;1122 }1123 }1124 }1125 }11261127 if (hideElementsWithEnvs.has('Client')) {1128 // If we're filtering out the Client environment we should filter out all1129 // "Client Components". Technically that also includes the built-ins but1130 // since that doesn't actually include any additional code loading it's1131 // useful to not filter out the built-ins. Those can be filtered separately.1132 // There's no other way to filter out just Function components on the Client.1133 // Therefore, this only filters Class and Function components.1134 switch (tag) {1135 case ClassComponent:1136 case IncompleteClassComponent:1137 case IncompleteFunctionComponent:1138 case FunctionComponent:1139 case IndeterminateComponent:1140 case ForwardRef:1141 case MemoComponent:1142 case SimpleMemoComponent:1143 return true;1144 }1145 }11461147 /* DISABLED: https://github.com/react/react/pull/284171148 if (hideElementsWithPaths.size > 0) {1149 const source = getSourceForFiber(fiber);11501151 if (source != null) {1152 const {fileName} = source;1153 // eslint-disable-next-line no-for-of-loops/no-for-of-loops1154 for (const pathRegExp of hideElementsWithPaths) {1155 if (pathRegExp.test(fileName)) {1156 return true;1157 }1158 }1159 }1160 }1161 */11621163 return false;1164 }11651166 // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods1167 function getElementTypeForFiber(fiber: Fiber): ElementType {1168 const {type, tag} = fiber;11691170 switch (tag) {1171 case ActivityComponent:1172 return ElementTypeActivity;1173 case ClassComponent:1174 case IncompleteClassComponent:1175 return ElementTypeClass;1176 case IncompleteFunctionComponent:1177 case FunctionComponent:1178 case IndeterminateComponent:1179 return ElementTypeFunction;1180 case ForwardRef:1181 return ElementTypeForwardRef;1182 case HostRoot:1183 return ElementTypeRoot;1184 case HostComponent:1185 case HostHoistable:1186 case HostSingleton:1187 return ElementTypeHostComponent;1188 case HostPortal:1189 case HostText:1190 case Fragment:1191 return ElementTypeOtherOrUnknown;1192 case MemoComponent:1193 case SimpleMemoComponent:1194 return ElementTypeMemo;1195 case SuspenseComponent:1196 return ElementTypeSuspense;1197 case SuspenseListComponent:1198 return ElementTypeSuspenseList;1199 case TracingMarkerComponent:1200 return ElementTypeTracingMarker;1201 case ViewTransitionComponent:1202 return ElementTypeViewTransition;1203 default:1204 const typeSymbol = getTypeSymbol(type);12051206 switch (typeSymbol) {1207 case CONCURRENT_MODE_NUMBER:1208 case CONCURRENT_MODE_SYMBOL_STRING:1209 case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:1210 return ElementTypeOtherOrUnknown;1211 case PROVIDER_NUMBER:1212 case PROVIDER_SYMBOL_STRING:1213 return ElementTypeContext;1214 case CONTEXT_NUMBER:1215 case CONTEXT_SYMBOL_STRING:1216 return ElementTypeContext;1217 case STRICT_MODE_NUMBER:1218 case STRICT_MODE_SYMBOL_STRING:1219 return ElementTypeOtherOrUnknown;1220 case PROFILER_NUMBER:1221 case PROFILER_SYMBOL_STRING:1222 return ElementTypeProfiler;1223 default:1224 return ElementTypeOtherOrUnknown;1225 }1226 }1227 }12281229 // When a mount or update is in progress, this value tracks the root that is being operated on.1230 let currentRoot: FiberInstance = null as any;12311232 // Removes a Fiber (and its alternate) from the Maps used to track their id.1233 // This method should always be called when a Fiber is unmounting.1234 function untrackFiber(nearestInstance: DevToolsInstance, fiber: Fiber) {1235 if (forceErrorForFibers.size > 0) {1236 forceErrorForFibers.delete(fiber);1237 if (fiber.alternate) {1238 forceErrorForFibers.delete(fiber.alternate);1239 }1240 if (forceErrorForFibers.size === 0 && setErrorHandler != null) {1241 setErrorHandler(shouldErrorFiberAlwaysNull);1242 }1243 }12441245 if (forceFallbackForFibers.size > 0) {1246 forceFallbackForFibers.delete(fiber);1247 if (fiber.alternate) {1248 forceFallbackForFibers.delete(fiber.alternate);1249 }1250 if (forceFallbackForFibers.size === 0 && setSuspenseHandler != null) {1251 setSuspenseHandler(shouldSuspendFiberAlwaysFalse);1252 }1253 }12541255 // TODO: Consider using a WeakMap instead. The only thing where that doesn't work1256 // is React Native Paper which tracks tags but that support is eventually going away1257 // and can use the old findFiberByHostInstance strategy.12581259 if (fiber.tag === HostHoistable) {1260 releaseHostResource(nearestInstance, fiber.memoizedState);1261 } else if (1262 fiber.tag === HostComponent ||1263 fiber.tag === HostText ||1264 fiber.tag === HostSingleton1265 ) {1266 releaseHostInstance(nearestInstance, fiber.stateNode);1267 }12681269 // Recursively clean up any filtered Fibers below this one as well since1270 // we won't recordUnmount on those.1271 for (let child = fiber.child; child !== null; child = child.sibling) {1272 if (shouldFilterFiber(child)) {1273 untrackFiber(nearestInstance, child);1274 }1275 }1276 }12771278 function getChangeDescription(1279 prevFiber: Fiber | null,1280 nextFiber: Fiber,1281 ): ChangeDescription | null {1282 switch (nextFiber.tag) {1283 case ClassComponent:1284 if (prevFiber === null) {1285 return {1286 context: null,1287 didHooksChange: false,1288 isFirstMount: true,1289 props: null,1290 state: null,1291 };1292 } else {1293 const data: ChangeDescription = {1294 context: getContextChanged(prevFiber, nextFiber),1295 didHooksChange: false,1296 isFirstMount: false,1297 props: getChangedKeys(1298 prevFiber.memoizedProps,1299 nextFiber.memoizedProps,1300 ),1301 state: getChangedKeys(1302 prevFiber.memoizedState,1303 nextFiber.memoizedState,1304 ),1305 };1306 return data;1307 }1308 case IncompleteFunctionComponent:1309 case FunctionComponent:1310 case IndeterminateComponent:1311 case ForwardRef:1312 case MemoComponent:1313 case SimpleMemoComponent:1314 if (prevFiber === null) {1315 return {1316 context: null,1317 didHooksChange: false,1318 isFirstMount: true,1319 props: null,1320 state: null,1321 };1322 } else {1323 const prevHooks = inspectHooks(prevFiber);1324 const nextHooks = inspectHooks(nextFiber);1325 const indices = getChangedHooksIndices(prevHooks, nextHooks);1326 const data: ChangeDescription = {1327 context: getContextChanged(prevFiber, nextFiber),1328 didHooksChange: indices !== null && indices.length > 0,1329 isFirstMount: false,1330 props: getChangedKeys(1331 prevFiber.memoizedProps,1332 nextFiber.memoizedProps,1333 ),1334 state: null,1335 hooks: indices,1336 };1337 // Only traverse the hooks list once, depending on what info we're returning.1338 return data;1339 }1340 default:1341 return null;1342 }1343 }13441345 type OperationsArray = Array<number>;13461347 type StringTableEntry = {1348 encodedString: Array<number>,1349 id: number,1350 };13511352 const pendingOperations: OperationsArray = [];1353 const pendingRealUnmountedIDs: Array<FiberInstance['id']> = [];1354 const pendingRealUnmountedSuspenseIDs: Array<FiberInstance['id']> = [];1355 const pendingSuspenderChanges: Set<FiberInstance['id']> = new Set();1356 let pendingOperationsQueue: Array<OperationsArray> | null = [];1357 const pendingStringTable: Map<string, StringTableEntry> = new Map();1358 let pendingStringTableLength: number = 0;13591360 function pushOperation(op: number): void {1361 if (__DEV__) {1362 if (!Number.isInteger(op)) {1363 console.error(1364 'pushOperation() was called but the value is not an integer.',1365 op,1366 );1367 }1368 }1369 pendingOperations.push(op);1370 }13711372 function shouldBailoutWithPendingOperations() {1373 if (isProfiling) {1374 if (1375 currentCommitProfilingMetadata != null &&1376 currentCommitProfilingMetadata.durations.length > 01377 ) {1378 return false;1379 }1380 }13811382 return (1383 pendingOperations.length === 0 &&1384 pendingRealUnmountedIDs.length === 0 &&1385 pendingRealUnmountedSuspenseIDs.length === 0 &&1386 pendingSuspenderChanges.size === 01387 );1388 }13891390 function flushOrQueueOperations(operations: OperationsArray): void {1391 if (shouldBailoutWithPendingOperations()) {1392 return;1393 }13941395 if (pendingOperationsQueue !== null) {1396 pendingOperationsQueue.push(operations);1397 } else {1398 hook.emit('operations', operations);1399 }1400 }14011402 function recordConsoleLogs(1403 instance: FiberInstance | VirtualInstance,1404 componentLogsEntry: void | ComponentLogs,1405 ): boolean {1406 if (componentLogsEntry === undefined) {1407 if (instance.logCount === 0) {1408 // Nothing has changed.1409 return false;1410 }1411 // Reset to zero.1412 instance.logCount = 0;1413 pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);1414 pushOperation(instance.id);1415 pushOperation(0);1416 pushOperation(0);1417 return true;1418 } else {1419 const totalCount =1420 componentLogsEntry.errorsCount + componentLogsEntry.warningsCount;1421 if (instance.logCount === totalCount) {1422 // Nothing has changed.1423 return false;1424 }1425 // Update counts.1426 instance.logCount = totalCount;1427 pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);1428 pushOperation(instance.id);1429 pushOperation(componentLogsEntry.errorsCount);1430 pushOperation(componentLogsEntry.warningsCount);1431 return true;1432 }1433 }14341435 /**1436 * Allowed to flush pending events without a specific root when:1437 * - pending operations don't record tree mutations e.g. TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS1438 * - not profiling (the commit tree builder requires the root of the mutations)1439 */1440 function flushPendingEvents(root: FiberInstance | null): void {1441 if (shouldBailoutWithPendingOperations()) {1442 // If we aren't profiling, we can just bail out here.1443 // No use sending an empty update over the bridge.1444 //1445 // The Profiler stores metadata for each commit and reconstructs the app tree per commit using:1446 // (1) an initial tree snapshot and1447 // (2) the operations array for each commit1448 // Because of this, it's important that the operations and metadata arrays align,1449 // So it's important not to omit even empty operations while profiling is active.1450 return;1451 }14521453 const numUnmountIDs = pendingRealUnmountedIDs.length;1454 const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length;1455 const numSuspenderChanges = pendingSuspenderChanges.size;14561457 const operations = new Array<number>(1458 // Identify which renderer this update is coming from.1459 2 + // [rendererID, rootFiberID]1460 // How big is the string table?1461 1 + // [stringTableLength]1462 // Then goes the actual string table.1463 pendingStringTableLength +1464 // All unmounts of Suspense boundaries are batched in a single message.1465 // [TREE_OPERATION_REMOVE_SUSPENSE, removedSuspenseIDLength, ...ids]1466 (numUnmountSuspenseIDs > 0 ? 2 + numUnmountSuspenseIDs : 0) +1467 // All unmounts are batched in a single message.1468 // [TREE_OPERATION_REMOVE, removedIDLength, ...ids]1469 (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +1470 // Regular operations1471 pendingOperations.length +1472 // All suspender changes are batched in a single message.1473 // [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, endTime, isSuspended]]1474 (numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 4 : 0),1475 );14761477 // Identify which renderer this update is coming from.1478 // This enables roots to be mapped to renderers,1479 // Which in turn enables fiber props, states, and hooks to be inspected.1480 let i = 0;1481 operations[i++] = rendererID;1482 if (root === null) {1483 operations[i++] = -1;1484 } else {1485 operations[i++] = root.id;1486 }14871488 // Now fill in the string table.1489 // [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...]1490 operations[i++] = pendingStringTableLength;1491 pendingStringTable.forEach((entry, stringKey) => {1492 const encodedString = entry.encodedString;14931494 // Don't use the string length.1495 // It won't work for multibyte characters (like emoji).1496 const length = encodedString.length;14971498 operations[i++] = length;1499 for (let j = 0; j < length; j++) {1500 operations[i + j] = encodedString[j];1501 }15021503 i += length;1504 });15051506 if (numUnmountSuspenseIDs > 0) {1507 // All unmounts of Suspense boundaries are batched in a single message.1508 operations[i++] = SUSPENSE_TREE_OPERATION_REMOVE;1509 // The first number is how many unmounted IDs we're gonna send.1510 operations[i++] = numUnmountSuspenseIDs;1511 // Fill in the real unmounts in the reverse order.1512 // They were inserted parents-first by React, but we want children-first.1513 // So we traverse our array backwards.1514 for (let j = 0; j < pendingRealUnmountedSuspenseIDs.length; j++) {1515 operations[i++] = pendingRealUnmountedSuspenseIDs[j];1516 }1517 }15181519 if (numUnmountIDs > 0) {1520 // All unmounts except roots are batched in a single message.1521 operations[i++] = TREE_OPERATION_REMOVE;1522 // The first number is how many unmounted IDs we're gonna send.1523 operations[i++] = numUnmountIDs;1524 // Fill in the real unmounts in the reverse order.1525 // They were inserted parents-first by React, but we want children-first.1526 // So we traverse our array backwards.1527 for (let j = 0; j < pendingRealUnmountedIDs.length; j++) {1528 operations[i++] = pendingRealUnmountedIDs[j];1529 }1530 }15311532 // Fill in pending operations.1533 for (let j = 0; j < pendingOperations.length; j++) {1534 operations[i + j] = pendingOperations[j];1535 }1536 i += pendingOperations.length;15371538 // Suspender changes might affect newly mounted nodes that we already recorded1539 // in pending operations.1540 if (numSuspenderChanges > 0) {1541 operations[i++] = SUSPENSE_TREE_OPERATION_SUSPENDERS;1542 operations[i++] = numSuspenderChanges;1543 pendingSuspenderChanges.forEach(fiberIdWithChanges => {1544 const suspense = idToSuspenseNodeMap.get(fiberIdWithChanges);1545 if (suspense === undefined) {1546 // Probably forgot to cleanup pendingSuspenderChanges when this node was removed.1547 throw new Error(1548 `Could not send suspender changes for "${fiberIdWithChanges}" since the Fiber no longer exists.`,1549 );1550 }1551 operations[i++] = fiberIdWithChanges;1552 operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0;1553 operations[i++] = Math.round(suspense.endTime * 1000);1554 const instance = suspense.instance;1555 const isSuspended =1556 // TODO: Track if other SuspenseNode like SuspenseList rows are suspended.1557 (instance.kind === FIBER_INSTANCE ||1558 instance.kind === FILTERED_FIBER_INSTANCE) &&1559 instance.data.tag === SuspenseComponent &&1560 instance.data.memoizedState !== null;1561 operations[i++] = isSuspended ? 1 : 0;1562 operations[i++] = suspense.environments.size;1563 suspense.environments.forEach((count, env) => {1564 operations[i++] = getStringID(env);1565 });1566 });1567 }15681569 // Let the frontend know about tree operations.1570 flushOrQueueOperations(operations);15711572 // Reset all of the pending state now that we've told the frontend about it.1573 pendingOperations.length = 0;1574 pendingRealUnmountedIDs.length = 0;1575 pendingRealUnmountedSuspenseIDs.length = 0;1576 pendingSuspenderChanges.clear();1577 pendingStringTable.clear();1578 pendingStringTableLength = 0;1579 }15801581 function measureHostInstance(instance: HostInstance): null | Array<Rect> {1582 // Feature detect measurement capabilities of this environment.1583 // TODO: Consider making this capability injected by the ReactRenderer.1584 if (typeof instance !== 'object' || instance === null) {1585 return null;1586 }1587 if (1588 typeof instance.getClientRects === 'function' ||1589 instance.nodeType === 31590 ) {1591 // DOM1592 const doc = instance.ownerDocument;1593 if (instance === doc.documentElement) {1594 // This is the document element. The size of this element is not actually1595 // what determines the whole scrollable area of the screen. Because any1596 // thing that overflows the document will also contribute to the scrollable.1597 // This is unlike overflow: scroll which clips those.1598 // Therefore, we use the scrollable size for this rect instead.1599 return [1600 {1601 x: 0,1602 y: 0,1603 width: instance.scrollWidth,1604 height: instance.scrollHeight,1605 },1606 ];1607 }1608 const result: Array<Rect> = [];1609 const win = doc && doc.defaultView;1610 const scrollX = win ? win.scrollX : 0;1611 const scrollY = win ? win.scrollY : 0;1612 let rects;1613 if (instance.nodeType === 3) {1614 // Text nodes cannot be measured directly but we can measure a Range.1615 if (typeof doc.createRange !== 'function') {1616 return null;1617 }1618 const range = doc.createRange();1619 if (typeof range.getClientRects !== 'function') {1620 return null;1621 }1622 range.selectNodeContents(instance);1623 rects = range.getClientRects();1624 } else {1625 rects = instance.getClientRects();1626 }1627 for (let i = 0; i < rects.length; i++) {1628 const rect = rects[i];1629 result.push({1630 x: rect.x + scrollX,1631 y: rect.y + scrollY,1632 width: rect.width,1633 height: rect.height,1634 });1635 }1636 return result;1637 }1638 if (instance.canonical) {1639 // Native1640 const publicInstance = instance.canonical.publicInstance;1641 if (!publicInstance) {1642 // The publicInstance may not have been initialized yet if there was no ref on this node.1643 // We can't initialize it from any existing Hook but we could fallback to this async form:1644 // renderer.extraDevToolsConfig.getInspectorDataForInstance(instance).hierarchy[last].getInspectorData().measure(callback)1645 return null;1646 }1647 if (typeof publicInstance.getBoundingClientRect === 'function') {1648 // enableAccessToHostTreeInFabric / ReadOnlyElement1649 return [publicInstance.getBoundingClientRect()];1650 }1651 if (typeof publicInstance.unstable_getBoundingClientRect === 'function') {1652 // ReactFabricHostComponent1653 return [publicInstance.unstable_getBoundingClientRect()];1654 }1655 }1656 return null;1657 }16581659 function measureInstance(instance: DevToolsInstance): null | Array<Rect> {1660 // Synchronously return the client rects of the Host instances directly inside this Instance.1661 const hostInstances = findAllCurrentHostInstances(instance);1662 let result: null | Array<Rect> = null;1663 for (let i = 0; i < hostInstances.length; i++) {1664 const childResult = measureHostInstance(hostInstances[i]);1665 if (childResult !== null) {1666 if (result === null) {1667 result = childResult;1668 } else {1669 result = result.concat(childResult);1670 }1671 }1672 }1673 return result;1674 }16751676 function getStringID(string: string | null): number {1677 if (string === null) {1678 return 0;1679 }1680 const existingEntry = pendingStringTable.get(string);1681 if (existingEntry !== undefined) {1682 return existingEntry.id;1683 }16841685 const id = pendingStringTable.size + 1;1686 const encodedString = utfEncodeString(string);16871688 pendingStringTable.set(string, {1689 encodedString,1690 id,1691 });16921693 // The string table total length needs to account both for the string length,1694 // and for the array item that contains the length itself.1695 //1696 // Don't use string length for this table.1697 // It won't work for multibyte characters (like emoji).1698 pendingStringTableLength += encodedString.length + 1;16991700 return id;1701 }17021703 let isInDisconnectedSubtree = false;17041705 function recordMount(1706 fiber: Fiber,1707 parentInstance: DevToolsInstance | null,1708 ): FiberInstance {1709 const isRoot = fiber.tag === HostRoot;1710 let fiberInstance;1711 if (isRoot) {1712 const entry = rootToFiberInstanceMap.get(fiber.stateNode);1713 if (entry === undefined) {1714 throw new Error('The root should have been registered at this point');1715 }1716 fiberInstance = entry;1717 } else {1718 fiberInstance = createFiberInstance(fiber);1719 }1720 idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance);17211722 // $FlowFixMe[constant-condition]1723 if (__DEBUG__) {1724 debug('recordMount()', fiberInstance, parentInstance);1725 }17261727 recordReconnect(fiberInstance, parentInstance);1728 return fiberInstance;1729 }17301731 function recordReconnect(1732 fiberInstance: FiberInstance,1733 parentInstance: DevToolsInstance | null,1734 ): void {1735 if (isInDisconnectedSubtree) {1736 // We're disconnected. We'll reconnect a hidden mount after the parent reappears.1737 return;1738 }1739 const id = fiberInstance.id;1740 const fiber = fiberInstance.data;17411742 const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');17431744 const isRoot = fiber.tag === HostRoot;17451746 if (isRoot) {1747 const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner');17481749 // Adding a new field here would require a bridge protocol version bump (a backwads breaking change).1750 // Instead let's re-purpose a pre-existing field to carry more information.1751 let profilingFlags = 0;1752 if (isProfilingSupported) {1753 profilingFlags = PROFILING_FLAG_BASIC_SUPPORT;1754 if (supportsPerformanceTracks) {1755 profilingFlags |= PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT;1756 }1757 }17581759 // Set supportsStrictMode to false for production renderer builds1760 const isProductionBuildOfRenderer = renderer.bundleType === 0;17611762 pushOperation(TREE_OPERATION_ADD);1763 pushOperation(id);1764 pushOperation(ElementTypeRoot);1765 pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0);1766 pushOperation(profilingFlags);1767 pushOperation(1768 !isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0,1769 );1770 pushOperation(hasOwnerMetadata ? 1 : 0);17711772 if (isProfiling) {1773 if (displayNamesByRootID !== null) {1774 displayNamesByRootID.set(id, getDisplayNameForRoot(fiber));1775 }1776 }1777 } else {1778 const suspenseNode = fiberInstance.suspenseNode;1779 if (suspenseNode !== null && fiber.memoizedState === null) {1780 // We're reconnecting an unsuspended Suspense. Measure to see if anything changed.1781 const prevRects = suspenseNode.rects;1782 const nextRects = measureInstance(fiberInstance);1783 if (!areEqualRects(prevRects, nextRects)) {1784 suspenseNode.rects = nextRects;1785 recordSuspenseResize(suspenseNode);1786 }1787 }17881789 const {key} = fiber;1790 const displayName = getDisplayNameForFiber(fiber);1791 const elementType = getElementTypeForFiber(fiber);17921793 // Finding the owner instance might require traversing the whole parent path which1794 // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we1795 // need it but we have some synchronous operations in the front end like Alt+Left1796 // which selects the owner immediately. Typically most owners are only a few parents1797 // away so maybe it's not so bad.1798 const debugOwner = getUnfilteredOwner(fiber);1799 const ownerInstance = findNearestOwnerInstance(1800 parentInstance,1801 debugOwner,1802 );1803 if (1804 ownerInstance !== null &&1805 debugOwner === fiber._debugOwner &&1806 fiber._debugStack != null &&1807 ownerInstance.source === null1808 ) {1809 // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on1810 // the debugStack will be a stack frame inside the ownerInstance's source.1811 ownerInstance.source = fiber._debugStack;1812 }18131814 let unfilteredParent = parentInstance;1815 while (1816 unfilteredParent !== null &&1817 unfilteredParent.kind === FILTERED_FIBER_INSTANCE1818 ) {1819 unfilteredParent = unfilteredParent.parent;1820 }18211822 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;1823 const parentID = unfilteredParent === null ? 0 : unfilteredParent.id;18241825 const displayNameStringID = getStringID(displayName);18261827 // This check is a guard to handle a React element that has been modified1828 // in such a way as to bypass the default stringification of the "key" property.1829 const keyString =1830 key === null1831 ? null1832 : key === REACT_OPTIMISTIC_KEY1833 ? 'React.optimisticKey'1834 : String(key);1835 const keyStringID = getStringID(keyString);18361837 const nameProp =1838 fiber.tag === SuspenseComponent1839 ? fiber.memoizedProps.name1840 : fiber.tag === ActivityComponent1841 ? fiber.memoizedProps.name1842 : null;1843 const namePropString = nameProp == null ? null : String(nameProp);1844 const namePropStringID = getStringID(namePropString);18451846 pushOperation(TREE_OPERATION_ADD);1847 pushOperation(id);1848 pushOperation(elementType);1849 pushOperation(parentID);1850 pushOperation(ownerID);1851 pushOperation(displayNameStringID);1852 pushOperation(keyStringID);1853 pushOperation(namePropStringID);18541855 // If this subtree has a new mode, let the frontend know.1856 if ((fiber.mode & StrictModeBits) !== 0) {1857 let parentFiber = null;1858 let parentFiberInstance = parentInstance;1859 while (parentFiberInstance !== null) {1860 if (parentFiberInstance.kind === FIBER_INSTANCE) {1861 parentFiber = parentFiberInstance.data;1862 break;1863 }1864 parentFiberInstance = parentFiberInstance.parent;1865 }1866 if (parentFiber === null || (parentFiber.mode & StrictModeBits) === 0) {1867 pushOperation(TREE_OPERATION_SET_SUBTREE_MODE);1868 pushOperation(id);1869 pushOperation(StrictMode);1870 }1871 }18721873 // If this is an Activity component, check if it's hidden.1874 if (fiber.tag === ActivityComponent) {1875 const offscreenChild = fiber.child;1876 if (1877 offscreenChild !== null &&1878 offscreenChild.tag === OffscreenComponent &&1879 offscreenChild.memoizedState !== null1880 ) {1881 pushOperation(TREE_OPERATION_SET_SUBTREE_MODE);1882 pushOperation(id);1883 pushOperation(ActivityHiddenMode);1884 }1885 }1886 }18871888 let componentLogsEntry = fiberToComponentLogsMap.get(fiber);1889 if (componentLogsEntry === undefined && fiber.alternate !== null) {1890 componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate);1891 }1892 recordConsoleLogs(fiberInstance, componentLogsEntry);18931894 if (isProfilingSupported) {1895 recordProfilingDurations(fiberInstance, null);1896 }1897 }18981899 function recordVirtualMount(1900 instance: VirtualInstance,1901 parentInstance: DevToolsInstance | null,1902 secondaryEnv: null | string,1903 ): void {1904 const id = instance.id;19051906 idToDevToolsInstanceMap.set(id, instance);19071908 recordVirtualReconnect(instance, parentInstance, secondaryEnv);1909 }19101911 function recordVirtualReconnect(1912 instance: VirtualInstance,1913 parentInstance: DevToolsInstance | null,1914 secondaryEnv: null | string,1915 ): void {1916 if (isInDisconnectedSubtree) {1917 // We're disconnected. We'll reconnect a hidden mount after the parent reappears.1918 return;1919 }1920 const componentInfo = instance.data;19211922 const key =1923 typeof componentInfo.key === 'string' ? componentInfo.key : null;1924 const env = componentInfo.env;1925 let displayName = componentInfo.name || '';1926 if (typeof env === 'string') {1927 // We model environment as an HoC name for now.1928 if (secondaryEnv !== null) {1929 displayName = secondaryEnv + '(' + displayName + ')';1930 }1931 displayName = env + '(' + displayName + ')';1932 }1933 const elementType = ElementTypeVirtual;19341935 // Finding the owner instance might require traversing the whole parent path which1936 // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we1937 // need it but we have some synchronous operations in the front end like Alt+Left1938 // which selects the owner immediately. Typically most owners are only a few parents1939 // away so maybe it's not so bad.1940 const debugOwner = getUnfilteredOwner(componentInfo);1941 const ownerInstance = findNearestOwnerInstance(parentInstance, debugOwner);1942 if (1943 ownerInstance !== null &&1944 debugOwner === componentInfo.owner &&1945 componentInfo.debugStack != null &&1946 ownerInstance.source === null1947 ) {1948 // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on1949 // the debugStack will be a stack frame inside the ownerInstance's source.1950 ownerInstance.source = componentInfo.debugStack;1951 }19521953 let unfilteredParent = parentInstance;1954 while (1955 unfilteredParent !== null &&1956 unfilteredParent.kind === FILTERED_FIBER_INSTANCE1957 ) {1958 unfilteredParent = unfilteredParent.parent;1959 }19601961 const ownerID = ownerInstance === null ? 0 : ownerInstance.id;1962 const parentID = unfilteredParent === null ? 0 : unfilteredParent.id;19631964 const displayNameStringID = getStringID(displayName);19651966 // This check is a guard to handle a React element that has been modified1967 // in such a way as to bypass the default stringification of the "key" property.1968 const keyString = key === null ? null : String(key);1969 const keyStringID = getStringID(keyString);1970 const namePropStringID = getStringID(null);19711972 const id = instance.id;19731974 pushOperation(TREE_OPERATION_ADD);1975 pushOperation(id);1976 pushOperation(elementType);1977 pushOperation(parentID);1978 pushOperation(ownerID);1979 pushOperation(displayNameStringID);1980 pushOperation(keyStringID);1981 pushOperation(namePropStringID);19821983 const componentLogsEntry =1984 componentInfoToComponentLogsMap.get(componentInfo);1985 recordConsoleLogs(instance, componentLogsEntry);1986 }19871988 function recordSuspenseMount(1989 suspenseInstance: SuspenseNode,1990 parentSuspenseInstance: SuspenseNode | null,1991 ): void {1992 const fiberInstance = suspenseInstance.instance;1993 if (fiberInstance.kind === FILTERED_FIBER_INSTANCE) {1994 throw new Error('Cannot record a mount for a filtered Fiber instance.');1995 }1996 const fiberID = fiberInstance.id;19971998 let unfilteredParent = parentSuspenseInstance;1999 while (2000 unfilteredParent !== null &&
Findings
✓ No findings reported for this file.