Remove debugging statements or use a logging library
console.log(
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 {copy} from 'clipboard-js';11import EventEmitter from '../events';12import {inspect} from 'util';13import {14 PROFILING_FLAG_BASIC_SUPPORT,15 PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT,16 TREE_OPERATION_ADD,17 TREE_OPERATION_REMOVE,18 TREE_OPERATION_REORDER_CHILDREN,19 TREE_OPERATION_SET_SUBTREE_MODE,20 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,21 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,22 TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,23 SUSPENSE_TREE_OPERATION_ADD,24 SUSPENSE_TREE_OPERATION_REMOVE,25 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,26 SUSPENSE_TREE_OPERATION_RESIZE,27 SUSPENSE_TREE_OPERATION_SUSPENDERS,28} from '../constants';29import {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 ElementTypeVirtual,43 ElementTypeViewTransition,44 ElementTypeActivity,45 ComponentFilterActivitySlice,46} from '../frontend/types';47import {48 getSavedComponentFilters,49 setSavedComponentFilters,50 shallowDiffers,51 utfDecodeStringWithRanges,52 parseElementDisplayNameFromBackend,53 unionOfTwoArrays,54} from '../utils';55import {localStorageGetItem, localStorageSetItem} from '../storage';56import {__DEBUG__} from '../constants';57import {printStore} from './utils';58import ProfilerStore from './ProfilerStore';59import {60 BRIDGE_PROTOCOL,61 currentBridgeProtocol,62} from 'react-devtools-shared/src/bridge';63import {64 StrictMode,65 ActivityHiddenMode,66 ActivityVisibleMode,67} from 'react-devtools-shared/src/frontend/types';68import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';6970import type {71 Element,72 ComponentFilter,73 ElementType,74 SuspenseNode,75 SuspenseTimelineStep,76 Rect,77} from 'react-devtools-shared/src/frontend/types';78import type {79 FrontendBridge,80 BridgeProtocol,81} from 'react-devtools-shared/src/bridge';82import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError';83import type {DevToolsHookSettings} from '../backend/types';8485import RBush from 'rbush';8687// Custom version which works with our Rect data structure.88class RectRBush extends RBush<Rect> {89 toBBox(rect: Rect): {90 minX: number,91 minY: number,92 maxX: number,93 maxY: number,94 } {95 return {96 minX: rect.x,97 minY: rect.y,98 maxX: rect.x + rect.width,99 maxY: rect.y + rect.height,100 };101 }102 compareMinX(a: Rect, b: Rect): number {103 return a.x - b.x;104 }105 compareMinY(a: Rect, b: Rect): number {106 return a.y - b.y;107 }108}109110const debug = (methodName: string, ...args: Array<string>) => {111 // $FlowFixMe[constant-condition]112 if (__DEBUG__) {113 console.log(114 `%cStore %c${methodName}`,115 'color: green; font-weight: bold;',116 'font-weight: bold;',117 ...args,118 );119 }120};121122const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =123 'React::DevTools::collapseNodesByDefault';124const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =125 'React::DevTools::recordChangeDescriptions';126127type ErrorAndWarningTuples = Array<{id: number, index: number}>;128129export type Config = {130 checkBridgeProtocolCompatibility?: boolean,131 isProfiling?: boolean,132 supportsInspectMatchingDOMElement?: boolean,133 supportsClickToInspect?: boolean,134 supportsReloadAndProfile?: boolean,135 supportsTraceUpdates?: boolean,136};137138const ADVANCED_PROFILING_NONE = 0;139const ADVANCED_PROFILING_PERFORMANCE_TRACKS = 2;140type AdvancedProfiling = 0 | 2;141142export type Capabilities = {143 supportsBasicProfiling: boolean,144 hasOwnerMetadata: boolean,145 supportsStrictMode: boolean,146 supportsAdvancedProfiling: AdvancedProfiling,147};148149function isNonZeroRect(rect: Rect) {150 return rect.width > 0 || rect.height > 0 || rect.x > 0 || rect.y > 0;151}152153function parseElementType(value: number): ElementType | null {154 // Cast before switching so Flow checks exhaustiveness while the default rejects unknown bridge values.155 const type = value as any as ElementType;156 switch (type) {157 case ElementTypeClass:158 case ElementTypeContext:159 case ElementTypeFunction:160 case ElementTypeForwardRef:161 case ElementTypeHostComponent:162 case ElementTypeMemo:163 case ElementTypeOtherOrUnknown:164 case ElementTypeProfiler:165 case ElementTypeRoot:166 case ElementTypeSuspense:167 case ElementTypeSuspenseList:168 case ElementTypeTracingMarker:169 case ElementTypeVirtual:170 case ElementTypeViewTransition:171 case ElementTypeActivity:172 return type;173 default:174 (type) as empty;175 return null;176 }177}178179/**180 * The store is the single source of truth for updates from the backend.181 * ContextProviders can subscribe to the Store for specific things they want to provide.182 */183export default class Store extends EventEmitter<{184 backendVersion: [],185 collapseNodesByDefault: [],186 componentFilters: [],187 error: [Error],188 hookSettings: [$ReadOnly<DevToolsHookSettings>],189 hostInstanceSelected: [Element['id'] | null],190 settingsUpdated: [$ReadOnly<DevToolsHookSettings>, Array<ComponentFilter>],191 mutated: [192 [193 Array<Element['id']>,194 Map<Element['id'], Element['id']>,195 Element['id'] | null,196 ],197 ],198 recordChangeDescriptions: [],199 roots: [],200 rootSupportsBasicProfiling: [],201 rootSupportsPerformanceTracks: [],202 suspenseTreeMutated: [[Map<SuspenseNode['id'], SuspenseNode['id']>]],203 supportsNativeStyleEditor: [],204 supportsReloadAndProfile: [],205 unsupportedBridgeProtocolDetected: [],206 unsupportedRendererVersionDetected: [],207}> {208 // If the backend version is new enough to report its (NPM) version, this is it.209 // This version may be displayed by the frontend for debugging purposes.210 _backendVersion: string | null = null;211212 _bridge: FrontendBridge;213214 // Computed whenever _errorsAndWarnings Map changes.215 _cachedComponentWithErrorCount: number = 0;216 _cachedComponentWithWarningCount: number = 0;217 _cachedErrorAndWarningTuples: ErrorAndWarningTuples | null = null;218219 // Should new nodes be collapsed by default when added to the tree?220 _collapseNodesByDefault: boolean = true;221222 _componentFilters: Array<ComponentFilter>;223224 // Map of ID to number of recorded error and warning message IDs.225 _errorsAndWarnings: Map<226 Element['id'],227 {errorCount: number, warningCount: number},228 > = new Map();229230 _focusedTransition: 0 | Element['id'] = 0;231232 // At least one of the injected renderers contains (DEV only) owner metadata.233 _hasOwnerMetadata: boolean = false;234235 // Map of ID to (mutable) Element.236 // Elements are mutated to avoid excessive cloning during tree updates.237 // The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage.238 _idToElement: Map<Element['id'], Element> = new Map();239240 _idToSuspense: Map<SuspenseNode['id'], SuspenseNode> = new Map();241242 // Should the React Native style editor panel be shown?243 _isNativeStyleEditorSupported: boolean = false;244245 _nativeStyleEditorValidAttributes: $ReadOnlyArray<string> | null = null;246247 // Older backends don't support an explicit bridge protocol,248 // so we should timeout eventually and show a downgrade message.249 _onBridgeProtocolTimeoutID: TimeoutID | null = null;250251 // Map of element (id) to the set of elements (ids) it owns.252 // This map enables getOwnersListForElement() to avoid traversing the entire tree.253 _ownersMap: Map<Element['id'], Set<Element['id']>> = new Map();254255 _profilerStore: ProfilerStore;256257 _recordChangeDescriptions: boolean = false;258259 // Incremented each time the store is mutated.260 // This enables a passive effect to detect a mutation between render and commit phase.261 _revision: number = 0;262 _revisionSuspense: number = 0;263264 // This Array must be treated as immutable!265 // Passive effects will check it for changes between render and mount.266 _roots: $ReadOnlyArray<Element['id']> = [];267268 _rootIDToCapabilities: Map<Element['id'], Capabilities> = new Map();269270 // Renderer ID is needed to support inspection fiber props, state, and hooks.271 _rootIDToRendererID: Map<Element['id'], number> = new Map();272273 // Stores all the SuspenseNode rects in an R-tree to make it fast to find overlaps.274 _rtree: RBush<Rect> = new RectRBush();275276 // These options may be initially set by a configuration option when constructing the Store.277 _supportsInspectMatchingDOMElement: boolean = false;278 _supportsClickToInspect: boolean = false;279 _supportsTraceUpdates: boolean = false;280281 _isReloadAndProfileFrontendSupported: boolean = false;282 _isReloadAndProfileBackendSupported: boolean = false;283284 // These options default to false but may be updated as roots are added and removed.285 _rootSupportsBasicProfiling: boolean = false;286 _rootSupportsPerformanceTracks: boolean = false;287288 _bridgeProtocol: BridgeProtocol | null = null;289 _unsupportedBridgeProtocolDetected: boolean = false;290 _unsupportedRendererVersionDetected: boolean = false;291292 // Total number of visible elements (within all roots).293 // Used for windowing purposes.294 _weightAcrossRoots: number = 0;295296 _shouldCheckBridgeProtocolCompatibility: boolean = false;297 _hookSettings: $ReadOnly<DevToolsHookSettings> | null = null;298 _shouldShowWarningsAndErrors: boolean = false;299300 // Only used in browser extension for synchronization with built-in Elements panel.301 _lastSelectedHostInstanceElementId: Element['id'] | null = null;302303 // Maximum recorded node depth during the lifetime of this Store.304 // Can only increase: not guaranteed to return maximal value for currently recorded elements.305 _maximumRecordedDepth = 0;306307 constructor(bridge: FrontendBridge, config?: Config) {308 super();309310 // $FlowFixMe[constant-condition]311 if (__DEBUG__) {312 debug('constructor', 'subscribing to Bridge');313 }314315 this._collapseNodesByDefault =316 localStorageGetItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) ===317 'true';318319 this._recordChangeDescriptions =320 localStorageGetItem(LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===321 'true';322323 this._componentFilters = getSavedComponentFilters();324325 let isProfiling = false;326 if (config != null) {327 isProfiling = config.isProfiling === true;328329 const {330 supportsInspectMatchingDOMElement,331 supportsClickToInspect,332 supportsReloadAndProfile,333 supportsTraceUpdates,334 checkBridgeProtocolCompatibility,335 } = config;336 if (supportsInspectMatchingDOMElement) {337 this._supportsInspectMatchingDOMElement = true;338 }339 if (supportsClickToInspect) {340 this._supportsClickToInspect = true;341 }342 if (supportsReloadAndProfile) {343 this._isReloadAndProfileFrontendSupported = true;344 }345 if (supportsTraceUpdates) {346 this._supportsTraceUpdates = true;347 }348 if (checkBridgeProtocolCompatibility) {349 this._shouldCheckBridgeProtocolCompatibility = true;350 }351 }352353 this._bridge = bridge;354 bridge.addListener('operations', this.onBridgeOperations);355 bridge.addListener('shutdown', this.onBridgeShutdown);356 bridge.addListener(357 'isReloadAndProfileSupportedByBackend',358 this.onBackendReloadAndProfileSupported,359 );360 bridge.addListener(361 'isNativeStyleEditorSupported',362 this.onBridgeNativeStyleEditorSupported,363 );364 bridge.addListener(365 'unsupportedRendererVersion',366 this.onBridgeUnsupportedRendererVersion,367 );368369 this._profilerStore = new ProfilerStore(bridge, this, isProfiling);370371 bridge.addListener('backendVersion', this.onBridgeBackendVersion);372 bridge.addListener('saveToClipboard', this.onSaveToClipboard);373 bridge.addListener('hookSettings', this.onHookSettings);374 bridge.addListener('backendInitialized', this.onBackendInitialized);375 bridge.addListener('selectElement', this.onHostInstanceSelected);376 }377378 // This is only used in tests to avoid memory leaks.379 assertExpectedRootMapSizes() {380 if (this.roots.length === 0) {381 // The only safe time to assert these maps are empty is when the store is empty.382 this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement');383 this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap');384 }385386 // These maps should always be the same size as the number of roots387 this.assertMapSizeMatchesRootCount(388 this._rootIDToCapabilities,389 '_rootIDToCapabilities',390 );391 this.assertMapSizeMatchesRootCount(392 this._rootIDToRendererID,393 '_rootIDToRendererID',394 );395 }396397 // This is only used in tests to avoid memory leaks.398 assertMapSizeMatchesRootCount<K, V>(map: Map<K, V>, mapName: string) {399 const expectedSize = this.roots.length;400 if (map.size !== expectedSize) {401 this._throwAndEmitError(402 Error(403 `Expected ${mapName} to contain ${expectedSize} items, but it contains ${404 map.size405 } items\n\n${inspect(map, {406 depth: 20,407 })}`,408 ),409 );410 }411 }412413 get backendVersion(): string | null {414 return this._backendVersion;415 }416417 get collapseNodesByDefault(): boolean {418 return this._collapseNodesByDefault;419 }420 set collapseNodesByDefault(value: boolean): void {421 this._collapseNodesByDefault = value;422423 localStorageSetItem(424 LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY,425 value ? 'true' : 'false',426 );427428 this.emit('collapseNodesByDefault');429 }430431 get componentFilters(): Array<ComponentFilter> {432 return this._componentFilters;433 }434 set componentFilters(value: Array<ComponentFilter>): void {435 if (this._profilerStore.isProfilingBasedOnUserInput) {436 // Re-mounting a tree while profiling is in progress might break a lot of assumptions.437 // If necessary, we could support this- but it doesn't seem like a necessary use case.438 this._throwAndEmitError(439 Error('Cannot modify filter preferences while profiling'),440 );441 }442443 // Filter updates are expensive to apply (since they impact the entire tree).444 // Let's determine if they've changed and avoid doing this work if they haven't.445 const prevEnabledComponentFilters = this._componentFilters.filter(446 filter => filter.isEnabled,447 );448 const nextEnabledComponentFilters = value.filter(449 filter => filter.isEnabled,450 );451 let haveEnabledFiltersChanged =452 prevEnabledComponentFilters.length !== nextEnabledComponentFilters.length;453 if (!haveEnabledFiltersChanged) {454 for (let i = 0; i < nextEnabledComponentFilters.length; i++) {455 const prevFilter = prevEnabledComponentFilters[i];456 const nextFilter = nextEnabledComponentFilters[i];457 if (shallowDiffers(prevFilter, nextFilter)) {458 haveEnabledFiltersChanged = true;459 break;460 }461 }462 }463464 this._componentFilters = value;465466 // Update persisted filter preferences467 setSavedComponentFilters(value);468 if (this._hookSettings === null) {469 // We changed filters before we got the hook settings.470 // Wait for hook settings before persisting component filters to not overwrite471 // persisted hook settings with defaults.472 // This exists purely as a type safety check; in practice the hook settings473 // should have arrived before any filter changes could be made.474 const onHookSettings = (settings: $ReadOnly<DevToolsHookSettings>) => {475 this._bridge.removeListener('hookSettings', onHookSettings);476 this.emit('settingsUpdated', settings, value);477 };478 this._bridge.addListener('hookSettings', onHookSettings);479 this._bridge.send('getHookSettings');480 } else {481 this.emit('settingsUpdated', this._hookSettings, value);482 }483484 // Notify the renderer that filter preferences have changed.485 // This is an expensive operation; it unmounts and remounts the entire tree,486 // so only do it if the set of enabled component filters has changed.487 if (haveEnabledFiltersChanged) {488 this._bridge.send('updateComponentFilters', value);489 }490491 this.emit('componentFilters');492 }493494 get bridgeProtocol(): BridgeProtocol | null {495 return this._bridgeProtocol;496 }497498 get componentWithErrorCount(): number {499 if (!this._shouldShowWarningsAndErrors) {500 return 0;501 }502503 return this._cachedComponentWithErrorCount;504 }505506 get componentWithWarningCount(): number {507 if (!this._shouldShowWarningsAndErrors) {508 return 0;509 }510511 return this._cachedComponentWithWarningCount;512 }513514 get displayingErrorsAndWarningsEnabled(): boolean {515 return this._shouldShowWarningsAndErrors;516 }517518 get hasOwnerMetadata(): boolean {519 return this._hasOwnerMetadata;520 }521522 get nativeStyleEditorValidAttributes(): $ReadOnlyArray<string> | null {523 return this._nativeStyleEditorValidAttributes;524 }525526 get numElements(): number {527 return this._weightAcrossRoots;528 }529530 get profilerStore(): ProfilerStore {531 return this._profilerStore;532 }533534 get recordChangeDescriptions(): boolean {535 return this._recordChangeDescriptions;536 }537 set recordChangeDescriptions(value: boolean): void {538 this._recordChangeDescriptions = value;539540 localStorageSetItem(541 LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,542 value ? 'true' : 'false',543 );544545 this.emit('recordChangeDescriptions');546 }547548 get revision(): number {549 return this._revision;550 }551 get revisionSuspense(): number {552 return this._revisionSuspense;553 }554555 get rootIDToRendererID(): Map<number, number> {556 return this._rootIDToRendererID;557 }558559 get roots(): $ReadOnlyArray<number> {560 return this._roots;561 }562563 // At least one of the currently mounted roots support the Legacy profiler.564 get rootSupportsBasicProfiling(): boolean {565 return this._rootSupportsBasicProfiling;566 }567568 // At least one of the currently mounted roots support performance tracks.569 get rootSupportsPerformanceTracks(): boolean {570 return this._rootSupportsPerformanceTracks;571 }572573 get supportsInspectMatchingDOMElement(): boolean {574 return this._supportsInspectMatchingDOMElement;575 }576577 get supportsClickToInspect(): boolean {578 return this._supportsClickToInspect;579 }580581 get supportsNativeStyleEditor(): boolean {582 return this._isNativeStyleEditorSupported;583 }584585 get supportsReloadAndProfile(): boolean {586 return (587 this._isReloadAndProfileFrontendSupported &&588 this._isReloadAndProfileBackendSupported589 );590 }591592 get supportsTraceUpdates(): boolean {593 return this._supportsTraceUpdates;594 }595596 get unsupportedBridgeProtocolDetected(): boolean {597 return this._unsupportedBridgeProtocolDetected;598 }599600 get unsupportedRendererVersionDetected(): boolean {601 return this._unsupportedRendererVersionDetected;602 }603604 get lastSelectedHostInstanceElementId(): Element['id'] | null {605 return this._lastSelectedHostInstanceElementId;606 }607608 containsElement(id: number): boolean {609 return this._idToElement.has(id);610 }611612 getElementAtIndex(index: number): Element | null {613 if (index < 0 || index >= this.numElements) {614 console.warn(615 `Invalid index ${index} specified; store contains ${this.numElements} items.`,616 );617618 return null;619 }620621 // Find which root this element is in...622 let root;623 let rootWeight = 0;624 for (let i = 0; i < this._roots.length; i++) {625 const rootID = this._roots[i];626 root = this._idToElement.get(rootID);627628 if (root === undefined) {629 // We should never reach this. This is a bug in the backend renderer.630 return this._throwAndEmitError(631 Error(632 `Couldn't find root with id "${rootID}": no matching node was found in the Store.`,633 ),634 );635 }636637 if (root.children.length === 0) {638 continue;639 }640641 if (rootWeight + root.weight > index) {642 break;643 } else {644 rootWeight += root.weight;645 }646 }647648 if (root === undefined) {649 return this._throwAndEmitError(650 Error(`Could not find an element at index "${index}" in the Store.`),651 );652 }653654 // Find the element in the tree using the weight of each node...655 // Skip over the root itself, because roots aren't visible in the Elements tree.656 let currentElement: Element = root;657 let currentWeight = rootWeight - 1;658659 while (index !== currentWeight) {660 const numChildren = currentElement.children.length;661 let didFindChild = false;662 for (let i = 0; i < numChildren; i++) {663 const childID = currentElement.children[i];664 const child = this._idToElement.get(childID);665666 if (child === undefined) {667 // We should never reach this. This is a bug in the backend renderer.668 return this._throwAndEmitError(669 Error(670 `Couldn't child element with id "${childID}": no matching node was found in the Store.`,671 ),672 );673 }674675 const childWeight = child.isCollapsed ? 1 : child.weight;676677 if (index <= currentWeight + childWeight) {678 currentWeight++;679 currentElement = child;680 didFindChild = true;681 break;682 } else {683 currentWeight += childWeight;684 }685 }686687 if (!didFindChild) {688 return this._throwAndEmitError(689 Error(690 `Could not find an element at index "${index}" because the Store tree weights are invalid.`,691 ),692 );693 }694 }695696 return currentElement;697 }698699 getElementIDAtIndex(index: number): number | null {700 const element = this.getElementAtIndex(index);701 return element === null ? null : element.id;702 }703704 getElementByID(id: number): Element | null {705 const element = this._idToElement.get(id);706 if (element === undefined) {707 console.warn(`No element found with id "${id}"`);708 return null;709 }710711 return element;712 }713714 _getElementByIDOrThrow(id: Element['id']): Element {715 const element = this._idToElement.get(id);716 if (element === undefined) {717 return this._throwAndEmitError(718 Error(719 `Could not find element with id "${id}": no matching node was found in the Store.`,720 ),721 );722 }723 return element;724 }725726 _recalculateWeightAcrossRoots(): void {727 let weightAcrossRoots = 0;728 this._roots.forEach(rootID => {729 weightAcrossRoots += this._getElementByIDOrThrow(rootID).weight;730 });731 this._weightAcrossRoots = weightAcrossRoots;732 }733734 containsSuspense(id: SuspenseNode['id']): boolean {735 return this._idToSuspense.has(id);736 }737738 getSuspenseByID(id: SuspenseNode['id']): SuspenseNode | null {739 const suspense = this._idToSuspense.get(id);740 if (suspense === undefined) {741 console.warn(`No suspense found with id "${id}"`);742 return null;743 }744745 return suspense;746 }747748 // Returns a tuple of [id, index]749 getElementsWithErrorsAndWarnings(): ErrorAndWarningTuples {750 if (!this._shouldShowWarningsAndErrors) {751 return [];752 }753754 if (this._cachedErrorAndWarningTuples !== null) {755 return this._cachedErrorAndWarningTuples;756 }757758 const errorAndWarningTuples: ErrorAndWarningTuples = [];759760 this._errorsAndWarnings.forEach((_, id) => {761 const index = this.getIndexOfElementID(id);762 if (index !== null) {763 let low = 0;764 let high = errorAndWarningTuples.length;765 while (low < high) {766 const mid = (low + high) >> 1;767 if (errorAndWarningTuples[mid].index > index) {768 high = mid;769 } else {770 low = mid + 1;771 }772 }773774 errorAndWarningTuples.splice(low, 0, {id, index});775 }776 });777778 // Cache for later (at least until the tree changes again).779 this._cachedErrorAndWarningTuples = errorAndWarningTuples;780 return errorAndWarningTuples;781 }782783 getErrorAndWarningCountForElementID(id: number): {784 errorCount: number,785 warningCount: number,786 } {787 if (!this._shouldShowWarningsAndErrors) {788 return {errorCount: 0, warningCount: 0};789 }790791 return this._errorsAndWarnings.get(id) || {errorCount: 0, warningCount: 0};792 }793794 getIndexOfElementID(id: number): number | null {795 const element = this.getElementByID(id);796797 if (element === null || element.parentID === 0) {798 return null;799 }800801 // Walk up the tree to the root.802 // Increment the index by one for each node we encounter,803 // and by the weight of all nodes to the left of the current one.804 // This should be a relatively fast way of determining the index of a node within the tree.805 let previousID = id;806 let currentID = element.parentID;807 let index = 0;808 while (true) {809 const current = this._idToElement.get(currentID);810 if (current === undefined) {811 return null;812 }813814 const {children} = current;815 for (let i = 0; i < children.length; i++) {816 const childID = children[i];817 if (childID === previousID) {818 break;819 }820821 const child = this._idToElement.get(childID);822 if (child === undefined) {823 return null;824 }825826 index += child.isCollapsed ? 1 : child.weight;827 }828829 if (current.parentID === 0) {830 // We found the root; stop crawling.831 break;832 }833834 index++;835836 previousID = current.id;837 currentID = current.parentID;838 }839840 // At this point, the current ID is a root (from the previous loop).841 // We also need to offset the index by previous root weights.842 for (let i = 0; i < this._roots.length; i++) {843 const rootID = this._roots[i];844 if (rootID === currentID) {845 break;846 }847848 const root = this._idToElement.get(rootID);849 if (root === undefined) {850 return null;851 }852853 index += root.weight;854 }855856 return index;857 }858859 isDescendantOf(parentId: number, descendantId: number): boolean {860 if (descendantId === 0) {861 return false;862 }863864 const descendant = this.getElementByID(descendantId);865 if (descendant === null) {866 return false;867 }868869 if (descendant.parentID === parentId) {870 return true;871 }872873 const parent = this.getElementByID(parentId);874 if (!parent || parent.depth >= descendant.depth) {875 return false;876 }877878 return this.isDescendantOf(parentId, descendant.parentID);879 }880881 /**882 * Returns index of the lowest descendant element, if available.883 * May not be the deepest element, the lowest is used in a sense of bottom-most from UI Tree representation perspective.884 */885 getIndexOfLowestDescendantElement(element: Element): number | null {886 let current: null | Element = element;887 while (current !== null) {888 if (current.isCollapsed || current.children.length === 0) {889 if (current === element) {890 return null;891 }892893 return this.getIndexOfElementID(current.id);894 } else {895 const lastChildID = current.children[current.children.length - 1];896 current = this.getElementByID(lastChildID);897 }898 }899900 return null;901 }902903 getOwnersListForElement(ownerID: number): Array<Element> {904 const list: Array<Element> = [];905 const element = this._idToElement.get(ownerID);906 if (element !== undefined) {907 list.push({908 ...element,909 depth: 0,910 });911912 const unsortedIDs = this._ownersMap.get(ownerID);913 if (unsortedIDs !== undefined) {914 const depthMap: Map<number, number> = new Map([[ownerID, 0]]);915916 // Items in a set are ordered based on insertion.917 // This does not correlate with their order in the tree.918 // So first we need to order them.919 // I wish we could avoid this sorting operation; we could sort at insertion time,920 // but then we'd have to pay sorting costs even if the owners list was never used.921 // Seems better to defer the cost, since the set of ids is probably pretty small.922 const sortedIDs = Array.from(unsortedIDs).sort(923 (idA, idB) =>924 (this.getIndexOfElementID(idA) || 0) -925 (this.getIndexOfElementID(idB) || 0),926 );927928 // Next we need to determine the appropriate depth for each element in the list.929 // The depth in the list may not correspond to the depth in the tree,930 // because the list has been filtered to remove intermediate components.931 // Perhaps the easiest way to do this is to walk up the tree until we reach either:932 // (1) another node that's already in the tree, or (2) the root (owner)933 // at which point, our depth is just the depth of that node plus one.934 sortedIDs.forEach(id => {935 const innerElement = this._idToElement.get(id);936 if (innerElement !== undefined) {937 let parentID = innerElement.parentID;938939 let depth = 0;940 while (parentID > 0) {941 if (parentID === ownerID || unsortedIDs.has(parentID)) {942 const parentDepth = depthMap.get(parentID);943 if (parentDepth === undefined) {944 return this._throwAndEmitError(945 Error(946 `Invalid owners list: owner depth for element "${parentID}" was not found.`,947 ),948 );949 }950 depth = parentDepth + 1;951 depthMap.set(id, depth);952 break;953 }954 const parent = this._idToElement.get(parentID);955 if (parent === undefined) {956 break;957 }958 parentID = parent.parentID;959 }960961 if (depth === 0) {962 this._throwAndEmitError(Error('Invalid owners list'));963 }964965 list.push({...innerElement, depth});966 }967 });968 }969 }970971 return list;972 }973974 getSuspenseLineage(975 suspenseID: SuspenseNode['id'],976 ): $ReadOnlyArray<SuspenseNode['id']> {977 const lineage: Array<SuspenseNode['id']> = [];978 let next: null | SuspenseNode = this.getSuspenseByID(suspenseID);979 while (next !== null) {980 if (next.parentID === 0) {981 next = null;982 } else {983 lineage.unshift(next.id);984 next = this.getSuspenseByID(next.parentID);985 }986 }987988 return lineage;989 }990991 /**992 * Like {@link getRootIDForElement} but should be used for traversing Suspense since it works with disconnected nodes.993 */994 getSuspenseRootIDForSuspense(id: SuspenseNode['id']): number | null {995 let current = this._idToSuspense.get(id);996 while (current !== undefined) {997 if (current.parentID === 0) {998 return current.id;999 } else {1000 current = this._idToSuspense.get(current.parentID);1001 }1002 }1003 return null;1004 }10051006 /**1007 * @param uniqueSuspendersOnly Filters out boundaries without unique suspenders1008 */1009 getSuspendableDocumentOrderSuspenseInitialPaint(1010 uniqueSuspendersOnly: boolean,1011 ): Array<SuspenseTimelineStep> {1012 const target: Array<SuspenseTimelineStep> = [];1013 const roots = this.roots;1014 let rootStep: null | SuspenseTimelineStep = null;1015 for (let i = 0; i < roots.length; i++) {1016 const rootID = roots[i];1017 this._getElementByIDOrThrow(rootID);1018 const rendererID = this._rootIDToRendererID.get(rootID);1019 if (rendererID === undefined) {1020 return this._throwAndEmitError(1021 Error(1022 'Failed to find renderer ID for root. This is a bug in React DevTools.',1023 ),1024 );1025 }1026 // TODO: This includes boundaries that can't be suspended due to no support from the renderer.10271028 const suspense = this.getSuspenseByID(rootID);1029 if (suspense !== null) {1030 const environments = suspense.environments;1031 const environmentName =1032 environments.length > 01033 ? environments[environments.length - 1]1034 : null;1035 if (rootStep === null) {1036 // Arbitrarily use the first root as the root step id.1037 rootStep = {1038 id: suspense.id,1039 environment: environmentName,1040 endTime: suspense.endTime,1041 rendererID,1042 };1043 target.push(rootStep);1044 } else {1045 if (rootStep.environment === null) {1046 // If any root has an environment name, then let's use it.1047 rootStep.environment = environmentName;1048 }1049 if (suspense.endTime > rootStep.endTime) {1050 // If any root has a higher end time, let's use that.1051 rootStep.endTime = suspense.endTime;1052 }1053 }1054 this.pushTimelineStepsInDocumentOrder(1055 suspense.children,1056 target,1057 uniqueSuspendersOnly,1058 environments,1059 0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.1060 rendererID,1061 );1062 }1063 }10641065 return target;1066 }10671068 _pushSuspenseChildrenInDocumentOrder(1069 children: Array<Element['id']>,1070 target: Array<SuspenseNode['id']>,1071 ): void {1072 for (let i = 0; i < children.length; i++) {1073 const childID = children[i];1074 const suspense = this._idToSuspense.get(childID);1075 if (suspense !== undefined) {1076 target.push(suspense.id);1077 } else {1078 const childElement = this._idToElement.get(childID);1079 if (childElement !== undefined) {1080 this._pushSuspenseChildrenInDocumentOrder(1081 childElement.children,1082 target,1083 );1084 }1085 }1086 }1087 }10881089 getSuspenseChildren(id: Element['id']): Array<SuspenseNode['id']> {1090 const transitionChildren: Array<SuspenseNode['id']> = [];10911092 const root = this._idToElement.get(id);1093 if (root === undefined) {1094 return transitionChildren;1095 }10961097 this._pushSuspenseChildrenInDocumentOrder(1098 root.children,1099 transitionChildren,1100 );11011102 return transitionChildren;1103 }11041105 /**1106 * @param uniqueSuspendersOnly Filters out boundaries without unique suspenders1107 */1108 getSuspendableDocumentOrderSuspenseTransition(1109 uniqueSuspendersOnly: boolean,1110 rendererID: number,1111 ): Array<SuspenseTimelineStep> {1112 const target: Array<SuspenseTimelineStep> = [];1113 const focusedTransitionID = this._focusedTransition;1114 if (focusedTransitionID === 0) {1115 return this._throwAndEmitError(1116 Error(1117 'Cannot get a transition timeline during the initial paint. This is a bug in React DevTools.',1118 ),1119 );1120 }11211122 target.push({1123 id: focusedTransitionID,1124 // TODO: Get environment for Activity1125 environment: null,1126 endTime: 0,1127 rendererID,1128 });11291130 const transitionChildren = this.getSuspenseChildren(focusedTransitionID);11311132 this.pushTimelineStepsInDocumentOrder(1133 transitionChildren,1134 target,1135 uniqueSuspendersOnly,1136 // TODO: Get environment for Activity1137 [],1138 0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.1139 rendererID,1140 );11411142 return target;1143 }11441145 pushTimelineStepsInDocumentOrder(1146 children: Array<SuspenseNode['id']>,1147 target: Array<SuspenseTimelineStep>,1148 uniqueSuspendersOnly: boolean,1149 parentEnvironments: Array<string>,1150 parentEndTime: number,1151 rendererID: number,1152 ): void {1153 for (let i = 0; i < children.length; i++) {1154 const child = this.getSuspenseByID(children[i]);1155 if (child === null) {1156 continue;1157 }1158 // Ignore any suspense boundaries that has no visual representation as this is not1159 // part of the visible loading sequence.1160 // TODO: Consider making visible meta data and other side-effects get virtual rects.1161 const hasRects =1162 child.rects !== null &&1163 child.rects.length > 0 &&1164 child.rects.some(isNonZeroRect);1165 const childEnvironments = child.environments;1166 // Since children are blocked on the parent, they're also blocked by the parent environments.1167 // Only if we discover a novel environment do we add that and it becomes the name we use.1168 const unionEnvironments = unionOfTwoArrays(1169 parentEnvironments,1170 childEnvironments,1171 );1172 const environmentName =1173 unionEnvironments.length > 01174 ? unionEnvironments[unionEnvironments.length - 1]1175 : null;1176 // The end time of a child boundary can in effect never be earlier than its parent even if1177 // everything unsuspended before that.1178 const maxEndTime =1179 parentEndTime > child.endTime ? parentEndTime : child.endTime;1180 if (hasRects && (!uniqueSuspendersOnly || child.hasUniqueSuspenders)) {1181 target.push({1182 id: child.id,1183 environment: environmentName,1184 endTime: maxEndTime,1185 rendererID,1186 });1187 }1188 this.pushTimelineStepsInDocumentOrder(1189 child.children,1190 target,1191 uniqueSuspendersOnly,1192 unionEnvironments,1193 maxEndTime,1194 rendererID,1195 );1196 }1197 }11981199 getEndTimeOrDocumentOrderSuspense(1200 uniqueSuspendersOnly: boolean,1201 ): $ReadOnlyArray<SuspenseTimelineStep> {1202 let timeline: SuspenseTimelineStep[];1203 if (this._focusedTransition === 0) {1204 timeline =1205 this.getSuspendableDocumentOrderSuspenseInitialPaint(1206 uniqueSuspendersOnly,1207 );1208 } else {1209 const focusedTransitionRootID = this.getRootIDForElement(1210 this._focusedTransition,1211 );1212 if (focusedTransitionRootID === null) {1213 return this._throwAndEmitError(1214 Error(1215 'Failed to find root ID for focused transition. This is a bug in React DevTools.',1216 ),1217 );1218 }1219 const rendererID = this._rootIDToRendererID.get(focusedTransitionRootID);1220 if (rendererID === undefined) {1221 return this._throwAndEmitError(1222 Error(1223 'Failed to find renderer ID for focused transition root. This is a bug in React DevTools.',1224 ),1225 );1226 }1227 timeline = this.getSuspendableDocumentOrderSuspenseTransition(1228 uniqueSuspendersOnly,1229 rendererID,1230 );1231 }12321233 if (timeline.length === 0) {1234 return timeline;1235 }1236 const root = timeline[0];1237 // We mutate in place since we assume we've got a fresh array.1238 timeline.sort((a, b) => {1239 // Root is always first1240 return a === root ? -1 : b === root ? 1 : a.endTime - b.endTime;1241 });1242 return timeline;1243 }12441245 getActivities(): Array<{id: Element['id'], depth: number}> {1246 const target: Array<{id: Element['id'], depth: number}> = [];1247 // TODO: Keep a live tree in the backend so we don't need to recalculate1248 // this each time while also including filtered Activities.1249 this._pushActivitiesInDocumentOrder(this.roots, target, 0);1250 return target;1251 }12521253 _pushActivitiesInDocumentOrder(1254 children: $ReadOnlyArray<Element['id']>,1255 target: Array<{id: Element['id'], depth: number}>,1256 depth: number,1257 ): void {1258 for (let i = 0; i < children.length; i++) {1259 const child = this._idToElement.get(children[i]);1260 if (child === undefined) {1261 continue;1262 }1263 if (child.type === ElementTypeActivity && child.nameProp !== null) {1264 target.push({id: child.id, depth});1265 this._pushActivitiesInDocumentOrder(child.children, target, depth + 1);1266 } else {1267 this._pushActivitiesInDocumentOrder(child.children, target, depth);1268 }1269 }1270 }12711272 getRendererIDForElement(id: number): number | null {1273 let current = this._idToElement.get(id);1274 while (current !== undefined) {1275 if (current.parentID === 0) {1276 const rendererID = this._rootIDToRendererID.get(current.id);1277 return rendererID == null ? null : rendererID;1278 } else {1279 current = this._idToElement.get(current.parentID);1280 }1281 }1282 return null;1283 }12841285 getRootIDForElement(id: number): number | null {1286 let current = this._idToElement.get(id);1287 while (current !== undefined) {1288 if (current.parentID === 0) {1289 return current.id;1290 } else {1291 current = this._idToElement.get(current.parentID);1292 }1293 }1294 return null;1295 }12961297 isInsideCollapsedSubTree(id: number): boolean {1298 let current = this._idToElement.get(id);1299 while (current != null) {1300 if (current.parentID === 0) {1301 return false;1302 } else {1303 current = this._idToElement.get(current.parentID);1304 if (current != null && current.isCollapsed) {1305 return true;1306 }1307 }1308 }1309 return false;1310 }13111312 // TODO Maybe split this into two methods: expand() and collapse()1313 toggleIsCollapsed(id: number, isCollapsed: boolean): void {1314 let didMutate = false;13151316 const element = this.getElementByID(id);1317 if (element !== null) {1318 if (isCollapsed) {1319 if (element.type === ElementTypeRoot) {1320 this._throwAndEmitError(Error('Root nodes cannot be collapsed'));1321 }13221323 if (!element.isCollapsed) {1324 didMutate = true;1325 element.isCollapsed = true;13261327 const weightDelta = 1 - element.weight;13281329 let parentElement = this._idToElement.get(element.parentID);1330 while (parentElement !== undefined) {1331 // We don't need to break on a collapsed parent in the same way as the expand case below.1332 // That's because collapsing a node doesn't "bubble" and affect its parents.1333 parentElement.weight += weightDelta;1334 parentElement = this._idToElement.get(parentElement.parentID);1335 }1336 }1337 } else {1338 let currentElement: ?Element = element;1339 while (currentElement != null) {1340 const oldWeight = currentElement.isCollapsed1341 ? 11342 : currentElement.weight;13431344 if (currentElement.isCollapsed) {1345 didMutate = true;1346 currentElement.isCollapsed = false;13471348 const newWeight = currentElement.isCollapsed1349 ? 11350 : currentElement.weight;1351 const weightDelta = newWeight - oldWeight;13521353 let parentElement = this._idToElement.get(currentElement.parentID);1354 while (parentElement !== undefined) {1355 parentElement.weight += weightDelta;1356 if (parentElement.isCollapsed) {1357 // It's important to break on a collapsed parent when expanding nodes.1358 // That's because expanding a node "bubbles" up and expands all parents as well.1359 // Breaking in this case prevents us from over-incrementing the expanded weights.1360 break;1361 }1362 parentElement = this._idToElement.get(parentElement.parentID);1363 }1364 }13651366 currentElement =1367 currentElement.parentID !== 01368 ? this.getElementByID(currentElement.parentID)1369 : null;1370 }1371 }13721373 // Only re-calculate weights and emit an "update" event if the store was mutated.1374 if (didMutate) {1375 this._recalculateWeightAcrossRoots();13761377 // The Tree context's search reducer expects an explicit list of ids for nodes that were added or removed.1378 // In this case, we can pass it empty arrays since nodes in a collapsed tree are still there (just hidden).1379 // Updating the selected search index later may require auto-expanding a collapsed subtree though.1380 this.emit('mutated', [[], new Map(), null]);1381 }1382 }1383 }13841385 _adjustParentTreeWeight: (1386 parentElement: ?Element,1387 weightDelta: number,1388 ) => void = (parentElement, weightDelta) => {1389 let isInsideCollapsedSubTree = false;13901391 while (parentElement != null) {1392 parentElement.weight += weightDelta;13931394 // Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent.1395 // Their weight will bubble up when the parent is expanded.1396 if (parentElement.isCollapsed) {1397 isInsideCollapsedSubTree = true;1398 break;1399 }14001401 parentElement = this._idToElement.get(parentElement.parentID);1402 }14031404 // Additions and deletions within a collapsed subtree should not affect the overall number of elements.1405 if (!isInsideCollapsedSubTree) {1406 this._weightAcrossRoots += weightDelta;1407 }1408 };14091410 _recursivelyUpdateSubtree(1411 id: number,1412 callback: (element: Element) => void,1413 ): void {1414 const element = this._idToElement.get(id);1415 if (element) {1416 callback(element);14171418 element.children.forEach(child =>1419 this._recursivelyUpdateSubtree(child, callback),1420 );1421 }1422 }14231424 onBridgeNativeStyleEditorSupported: ({1425 isSupported: boolean,1426 validAttributes: ?$ReadOnlyArray<string>,1427 }) => void = ({isSupported, validAttributes}) => {1428 this._isNativeStyleEditorSupported = isSupported;1429 this._nativeStyleEditorValidAttributes = validAttributes || null;14301431 this.emit('supportsNativeStyleEditor');1432 };14331434 onBridgeOperations: (operations: Array<number>) => void = operations => {1435 // $FlowFixMe[constant-condition]1436 if (__DEBUG__) {1437 console.groupCollapsed('onBridgeOperations');1438 debug('onBridgeOperations', operations.join(','));1439 }14401441 let haveRootsChanged = false;1442 let haveErrorsOrWarningsChanged = false;1443 let hasSuspenseTreeChanged = false;14441445 // The first two values are always rendererID and rootID1446 const rendererID = operations[0];14471448 const addedElementIDs: Array<number> = [];1449 // This is a mapping of removed ID -> parent ID:1450 // We'll use the parent ID to adjust selection if it gets deleted.1451 const removedElementIDs: Map<number, number> = new Map();1452 const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =1453 new Map();1454 let nextActivitySliceID: Element['id'] | null = null;14551456 let i = 2;14571458 // Reassemble the string table.1459 const stringTable: Array<string | null> = [1460 null, // ID = 0 corresponds to the null string.1461 ];1462 const stringTableSize = operations[i];1463 i++;14641465 const stringTableEnd = i + stringTableSize;14661467 while (i < stringTableEnd) {1468 const nextLength = operations[i];1469 i++;14701471 const nextString = utfDecodeStringWithRanges(1472 operations,1473 i,1474 i + nextLength - 1,1475 );1476 stringTable.push(nextString);1477 i += nextLength;1478 }14791480 while (i < operations.length) {1481 const operation = operations[i];1482 switch (operation) {1483 case TREE_OPERATION_ADD: {1484 const id = operations[i + 1];1485 const rawType = operations[i + 2];1486 const type = parseElementType(rawType);14871488 if (type === null) {1489 return this._throwAndEmitError(1490 Error(1491 `Cannot add node "${id}" because "${rawType}" is not a valid element type.`,1492 ),1493 );1494 }14951496 i += 3;14971498 if (this._idToElement.has(id)) {1499 // We should never reach this. This is a bug in the backend renderer.1500 return this._throwAndEmitError(1501 Error(1502 `Cannot add node "${id}" because a node with that id is already in the Store.`,1503 ),1504 );1505 }15061507 if (type === ElementTypeRoot) {1508 // $FlowFixMe[constant-condition]1509 if (__DEBUG__) {1510 debug('Add', `new root node ${id}`);1511 }15121513 const isStrictModeCompliant = operations[i] > 0;1514 i++;15151516 const profilerFlags = operations[i++];1517 const supportsBasicProfiling =1518 (profilerFlags & PROFILING_FLAG_BASIC_SUPPORT) !== 0;1519 const supportsPerformanceTracks =1520 (profilerFlags & PROFILING_FLAG_PERFORMANCE_TRACKS_SUPPORT) !== 0;1521 let supportsAdvancedProfiling: AdvancedProfiling =1522 ADVANCED_PROFILING_NONE;1523 if (supportsPerformanceTracks) {1524 supportsAdvancedProfiling = ADVANCED_PROFILING_PERFORMANCE_TRACKS;1525 }15261527 let supportsStrictMode = false;1528 let hasOwnerMetadata = false;15291530 // If we don't know the bridge protocol, guess that we're dealing with the latest.1531 // If we do know it, we can take it into consideration when parsing operations.1532 if (1533 this._bridgeProtocol === null ||1534 this._bridgeProtocol.version >= 21535 ) {1536 supportsStrictMode = operations[i] > 0;1537 i++;15381539 hasOwnerMetadata = operations[i] > 0;1540 i++;1541 }15421543 this._roots = this._roots.concat(id);1544 this._rootIDToRendererID.set(id, rendererID);1545 this._rootIDToCapabilities.set(id, {1546 supportsBasicProfiling,1547 hasOwnerMetadata,1548 supportsStrictMode,1549 supportsAdvancedProfiling,1550 });15511552 // Not all roots support StrictMode;1553 // don't flag a root as non-compliant unless it also supports StrictMode.1554 const isStrictModeNonCompliant =1555 !isStrictModeCompliant && supportsStrictMode;15561557 this._idToElement.set(id, {1558 children: [],1559 depth: -1,1560 displayName: null,1561 hocDisplayNames: null,1562 id,1563 isCollapsed: false, // Never collapse roots; it would hide the entire tree.1564 isStrictModeNonCompliant,1565 isActivityHidden: false,1566 isInsideHiddenActivity: false,1567 key: null,1568 nameProp: null,1569 ownerID: 0,1570 parentID: 0,1571 type,1572 weight: 0,1573 compiledWithForget: false,1574 });15751576 haveRootsChanged = true;1577 } else {1578 const parentID = operations[i];1579 i++;15801581 const ownerID = operations[i];1582 i++;15831584 const displayNameStringID = operations[i];1585 const displayName = stringTable[displayNameStringID];1586 i++;15871588 const keyStringID = operations[i];1589 const key = stringTable[keyStringID];1590 i++;15911592 const namePropStringID = operations[i];1593 const nameProp = stringTable[namePropStringID];1594 i++;15951596 // $FlowFixMe[constant-condition]1597 if (__DEBUG__) {1598 debug(1599 'Add',1600 `node ${id} (${displayName || 'null'}) as child of ${parentID}`,1601 );1602 }16031604 const parentElement = this._idToElement.get(parentID);1605 if (parentElement === undefined) {1606 // We should never reach this. This is a bug in the backend renderer.1607 return this._throwAndEmitError(1608 Error(1609 `Cannot add child "${id}" to parent "${parentID}" because parent node was not found in the Store.`,1610 ),1611 );1612 }16131614 parentElement.children.push(id);16151616 const {1617 formattedDisplayName: displayNameWithoutHOCs,1618 hocDisplayNames,1619 compiledWithForget,1620 } = parseElementDisplayNameFromBackend(displayName, type);16211622 const elementDepth = parentElement.depth + 1;1623 this._maximumRecordedDepth = Math.max(1624 this._maximumRecordedDepth,1625 elementDepth,1626 );16271628 const element: Element = {1629 children: [],1630 depth: elementDepth,1631 displayName: displayNameWithoutHOCs,1632 hocDisplayNames,1633 id,1634 isCollapsed: this._collapseNodesByDefault,1635 isStrictModeNonCompliant: parentElement.isStrictModeNonCompliant,1636 isActivityHidden: false,1637 isInsideHiddenActivity:1638 parentElement.isInsideHiddenActivity ||1639 parentElement.isActivityHidden,1640 key,1641 nameProp,1642 ownerID,1643 parentID,1644 type,1645 weight: 1,1646 compiledWithForget,1647 };16481649 this._idToElement.set(id, element);1650 addedElementIDs.push(id);1651 this._adjustParentTreeWeight(parentElement, 1);16521653 if (ownerID > 0) {1654 let set = this._ownersMap.get(ownerID);1655 if (set === undefined) {1656 set = new Set();1657 this._ownersMap.set(ownerID, set);1658 }1659 set.add(id);1660 }16611662 const suspense = this._idToSuspense.get(id);1663 if (suspense !== undefined) {1664 // We're reconnecting a node.1665 if (suspense.name === null) {1666 suspense.name = this._guessSuspenseName(element);1667 }1668 }1669 }1670 break;1671 }1672 case TREE_OPERATION_REMOVE: {1673 const removeLength = operations[i + 1];1674 i += 2;16751676 for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {1677 const id = operations[i];1678 const element = this._idToElement.get(id);16791680 if (element === undefined) {1681 // We should never reach this. This is a bug in the backend renderer.1682 return this._throwAndEmitError(1683 Error(1684 `Cannot remove node "${id}" because no matching node was found in the Store.`,1685 ),1686 );1687 }16881689 i += 1;16901691 const {children, ownerID, parentID, weight} = element;1692 if (children.length > 0) {1693 // We should never reach this. This is a bug in the backend renderer.1694 return this._throwAndEmitError(1695 Error(`Node "${id}" was removed before its children.`),1696 );1697 }16981699 let parentElement: ?Element = null;1700 if (parentID === 0) {1701 // $FlowFixMe[constant-condition]1702 if (__DEBUG__) {1703 debug('Remove', `node ${id} root`);1704 }17051706 this._roots = this._roots.filter(rootID => rootID !== id);1707 this._rootIDToRendererID.delete(id);1708 this._rootIDToCapabilities.delete(id);17091710 haveRootsChanged = true;1711 } else {1712 // $FlowFixMe[constant-condition]1713 if (__DEBUG__) {1714 debug('Remove', `node ${id} from parent ${parentID}`);1715 }17161717 parentElement = this._idToElement.get(parentID);1718 if (parentElement === undefined) {1719 // We should never reach this. This is a bug in the backend renderer.1720 return this._throwAndEmitError(1721 Error(1722 `Cannot remove node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,1723 ),1724 );1725 }17261727 const index = parentElement.children.indexOf(id);1728 if (index === -1) {1729 return this._throwAndEmitError(1730 Error(1731 `Cannot remove node "${id}" from parent "${parentID}" because it is not a child of the parent.`,1732 ),1733 );1734 }1735 parentElement.children.splice(index, 1);1736 }17371738 this._idToElement.delete(id);17391740 this._adjustParentTreeWeight(parentElement, -weight);1741 removedElementIDs.set(id, parentID);17421743 this._ownersMap.delete(id);1744 if (ownerID > 0) {1745 const set = this._ownersMap.get(ownerID);1746 if (set !== undefined) {1747 set.delete(id);1748 }1749 }17501751 if (this._errorsAndWarnings.has(id)) {1752 this._errorsAndWarnings.delete(id);1753 haveErrorsOrWarningsChanged = true;1754 }1755 }17561757 break;1758 }1759 case TREE_OPERATION_REORDER_CHILDREN: {1760 const id = operations[i + 1];1761 const numChildren = operations[i + 2];1762 i += 3;17631764 const element = this._idToElement.get(id);1765 if (element === undefined) {1766 // We should never reach this. This is a bug in the backend renderer.1767 return this._throwAndEmitError(1768 Error(1769 `Cannot reorder children for node "${id}" because no matching node was found in the Store.`,1770 ),1771 );1772 }17731774 const children = element.children;1775 if (children.length !== numChildren) {1776 // We should never reach this. This is a bug in the backend renderer.1777 return this._throwAndEmitError(1778 Error(1779 `Children cannot be added or removed during a reorder operation.`,1780 ),1781 );1782 }17831784 const reorderedChildIDs: Set<Element['id']> = new Set();1785 for (let j = 0; j < numChildren; j++) {1786 const childID = operations[i + j];1787 const childElement = this._idToElement.get(childID);1788 if (1789 childElement === undefined ||1790 childElement.parentID !== id ||1791 reorderedChildIDs.has(childID)1792 ) {1793 return this._throwAndEmitError(1794 Error(1795 `Children cannot be added or removed during a reorder operation.`,1796 ),1797 );1798 }1799 reorderedChildIDs.add(childID);1800 }1801 for (let j = 0; j < numChildren; j++) {1802 children[j] = operations[i + j];1803 }1804 i += numChildren;18051806 // $FlowFixMe[constant-condition]1807 if (__DEBUG__) {1808 debug('Re-order', `Node ${id} children ${children.join(',')}`);1809 }1810 break;1811 }1812 case TREE_OPERATION_SET_SUBTREE_MODE: {1813 const id = operations[i + 1];1814 const mode = operations[i + 2];18151816 i += 3;18171818 // If elements have already been mounted in this subtree, update them.1819 // (In practice, this likely only applies to the root element.)1820 if (mode === StrictMode) {1821 this._recursivelyUpdateSubtree(id, element => {1822 element.isStrictModeNonCompliant = false;1823 });1824 } else if (mode === ActivityHiddenMode) {1825 const element = this._idToElement.get(id);1826 if (element != null) {1827 element.isActivityHidden = true;1828 element.children.forEach(childID =>1829 this._recursivelyUpdateSubtree(childID, child => {1830 child.isInsideHiddenActivity = true;1831 }),1832 );1833 // Collapse hidden Activity subtrees by default.1834 if (!element.isCollapsed) {1835 element.isCollapsed = true;1836 if (element.children.length > 0) {1837 const weightDelta = 1 - element.weight;1838 const parentElement = this._idToElement.get(element.parentID);1839 this._adjustParentTreeWeight(parentElement, weightDelta);1840 }1841 }1842 }1843 } else if (mode === ActivityVisibleMode) {1844 const element = this._idToElement.get(id);1845 if (element != null) {1846 element.isActivityHidden = false;1847 element.children.forEach(childID =>1848 this._recursivelyUpdateSubtree(childID, child => {1849 child.isInsideHiddenActivity = false;1850 }),1851 );1852 // Expand Activity subtree when it becomes visible.1853 if (element.isCollapsed && element.children.length > 0) {1854 element.isCollapsed = false;1855 const weightDelta = element.weight - 1;1856 const parentElement = this._idToElement.get(element.parentID);1857 this._adjustParentTreeWeight(parentElement, weightDelta);1858 }1859 }1860 }18611862 // $FlowFixMe[constant-condition]1863 if (__DEBUG__) {1864 debug(1865 'Subtree mode',1866 `Subtree with root ${id} set to mode ${mode}`,1867 );1868 }1869 break;1870 }1871 case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:1872 // Base duration updates are only sent while profiling is in progress.1873 // We can ignore them at this point.1874 // The profiler UI uses them lazily in order to generate the tree.1875 i += 3;1876 break;1877 case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {1878 const id = operations[i + 1];1879 const errorCount = operations[i + 2];1880 const warningCount = operations[i + 3];18811882 i += 4;18831884 if (errorCount > 0 || warningCount > 0) {1885 this._errorsAndWarnings.set(id, {errorCount, warningCount});1886 } else if (this._errorsAndWarnings.has(id)) {1887 this._errorsAndWarnings.delete(id);1888 }1889 haveErrorsOrWarningsChanged = true;1890 break;1891 }1892 case SUSPENSE_TREE_OPERATION_ADD: {1893 const id = operations[i + 1];1894 const parentID = operations[i + 2];1895 const nameStringID = operations[i + 3];1896 const isSuspended = operations[i + 4] === 1;1897 const numRects = operations[i + 5];1898 let name = stringTable[nameStringID];18991900 if (this._idToSuspense.has(id)) {1901 // We should never reach this. This is a bug in the backend renderer.1902 return this._throwAndEmitError(1903 Error(1904 `Cannot add suspense node "${id}" because a suspense node with that id is already in the Store.`,1905 ),1906 );1907 }19081909 const element = this._idToElement.get(id);1910 if (element === undefined) {1911 // This element isn't connected yet.1912 } else {1913 if (name === null) {1914 // The boundary isn't explicitly named.1915 // Pick a sensible default.1916 if (parentID === 0) {1917 // For Roots we use their display name.1918 name = element.displayName;1919 } else {1920 name = this._guessSuspenseName(element);1921 }1922 }1923 }19241925 i += 6;1926 let rects: SuspenseNode['rects'];1927 if (numRects === -1) {1928 rects = null;1929 } else {1930 rects = [];1931 for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {1932 const x = operations[i + 0] / 1000;1933 const y = operations[i + 1] / 1000;1934 const width = operations[i + 2] / 1000;1935 const height = operations[i + 3] / 1000;1936 const rect = {x, y, width, height};1937 if (parentID !== 0) {1938 // Track all rects except the root.1939 this._rtree.insert(rect);1940 }1941 rects.push(rect);1942 i += 4;1943 }1944 }19451946 // $FlowFixMe[constant-condition]1947 if (__DEBUG__) {1948 debug('Suspense Add', `node ${id} as child of ${parentID}`);1949 }19501951 if (parentID !== 0) {1952 const parentSuspense = this._idToSuspense.get(parentID);1953 if (parentSuspense === undefined) {1954 // We should never reach this. This is a bug in the backend renderer.1955 return this._throwAndEmitError(1956 Error(1957 `Cannot add suspense child "${id}" to parent suspense "${parentID}" because parent suspense node was not found in the Store.`,1958 ),1959 );1960 }19611962 parentSuspense.children.push(id);1963 }19641965 this._idToSuspense.set(id, {1966 id,1967 parentID,1968 children: [],1969 name,1970 rects,1971 hasUniqueSuspenders: false,1972 isSuspended: isSuspended,1973 environments: [],1974 endTime: 0,1975 });19761977 hasSuspenseTreeChanged = true;1978 break;1979 }1980 case SUSPENSE_TREE_OPERATION_REMOVE: {1981 const removeLength = operations[i + 1];1982 i += 2;19831984 for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {1985 const id = operations[i];1986 const suspense = this._idToSuspense.get(id);19871988 if (suspense === undefined) {1989 // We should never reach this. This is a bug in the backend renderer.1990 return this._throwAndEmitError(1991 Error(1992 `Cannot remove suspense node "${id}" because no matching node was found in the Store.`,1993 ),1994 );1995 }19961997 i += 1;19981999 const {children, parentID, rects} = suspense;2000 if (children.length > 0) {
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.