main
js 1,313 lines 38.2 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import {
11 ElementTypeClass,
12 ElementTypeFunction,
13 ElementTypeRoot,
14 ElementTypeHostComponent,
15 ElementTypeOtherOrUnknown,
16 } from 'react-devtools-shared/src/frontend/types';
17 import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
18 import {
19 cleanForBridge,
20 copyWithDelete,
21 copyWithRename,
22 copyWithSet,
23 serializeToString,
24 } from '../utils';
25 import {
26 deletePathInObject,
27 getDisplayName,
28 getInObject,
29 renamePathInObject,
30 setInObject,
31 } from 'react-devtools-shared/src/utils';
32 import {
33 __DEBUG__,
34 TREE_OPERATION_ADD,
35 TREE_OPERATION_REMOVE,
36 TREE_OPERATION_REORDER_CHILDREN,
37 SUSPENSE_TREE_OPERATION_ADD,
38 SUSPENSE_TREE_OPERATION_REMOVE,
39 UNKNOWN_SUSPENDERS_NONE,
40 } from '../../constants';
41 import {decorateMany, forceUpdate, restoreMany} from './utils';
42
43 import type {
44 DevToolsHook,
45 GetElementIDForHostInstance,
46 InspectedElementPayload,
47 InstanceAndStyle,
48 HostInstance,
49 PathFrame,
50 PathMatch,
51 RendererInterface,
52 } from '../types';
53 import type {
54 ComponentFilter,
55 ElementType,
56 } from 'react-devtools-shared/src/frontend/types';
57 import type {InspectedElement, SerializedElement} from '../types';
58
59 export type InternalInstance = Object;
60 type LegacyRenderer = Object;
61
62 function getData(internalInstance: InternalInstance) {
63 let displayName = null;
64 let key = null;
65
66 // != used deliberately here to catch undefined and null
67 if (internalInstance._currentElement != null) {
68 if (internalInstance._currentElement.key) {
69 key = String(internalInstance._currentElement.key);
70 }
71
72 const elementType = internalInstance._currentElement.type;
73 if (typeof elementType === 'string') {
74 displayName = elementType;
75 } else if (typeof elementType === 'function') {
76 displayName = getDisplayName(elementType);
77 }
78 }
79
80 return {
81 displayName,
82 key,
83 };
84 }
85
86 function getElementType(internalInstance: InternalInstance): ElementType {
87 // != used deliberately here to catch undefined and null
88 if (internalInstance._currentElement != null) {
89 const elementType = internalInstance._currentElement.type;
90 if (typeof elementType === 'function') {
91 const publicInstance = internalInstance.getPublicInstance();
92 if (publicInstance !== null) {
93 return ElementTypeClass;
94 } else {
95 return ElementTypeFunction;
96 }
97 } else if (typeof elementType === 'string') {
98 return ElementTypeHostComponent;
99 }
100 }
101 return ElementTypeOtherOrUnknown;
102 }
103
104 function getChildren(internalInstance: Object): Array<any> {
105 const children = [];
106
107 // If the parent is a native node without rendered children, but with
108 // multiple string children, then the `element` that gets passed in here is
109 // a plain value -- a string or number.
110 if (typeof internalInstance !== 'object') {
111 // No children
112 } else if (
113 internalInstance._currentElement === null ||
114 internalInstance._currentElement === false
115 ) {
116 // No children
117 } else if (internalInstance._renderedComponent) {
118 const child = internalInstance._renderedComponent;
119 if (getElementType(child) !== ElementTypeOtherOrUnknown) {
120 children.push(child);
121 }
122 } else if (internalInstance._renderedChildren) {
123 const renderedChildren = internalInstance._renderedChildren;
124 for (const name in renderedChildren) {
125 const child = renderedChildren[name];
126 if (getElementType(child) !== ElementTypeOtherOrUnknown) {
127 children.push(child);
128 }
129 }
130 }
131 // Note: we skip the case where children are just strings or numbers
132 // because the new DevTools skips over host text nodes anyway.
133 return children;
134 }
135
136 export function attach(
137 hook: DevToolsHook,
138 rendererID: number,
139 renderer: LegacyRenderer,
140 global: Object,
141 ): RendererInterface {
142 const idToInternalInstanceMap: Map<number, InternalInstance> = new Map();
143 const internalInstanceToIDMap: WeakMap<InternalInstance, number> =
144 new WeakMap();
145 const internalInstanceToRootIDMap: WeakMap<InternalInstance, number> =
146 new WeakMap();
147
148 let getElementIDForHostInstance: GetElementIDForHostInstance =
149 null as any as GetElementIDForHostInstance;
150 let findHostInstanceForInternalID: (id: number) => ?HostInstance;
151 let getNearestMountedDOMNode = (node: Element): null | Element => {
152 // Not implemented.
153 return null;
154 };
155
156 if (renderer.ComponentTree) {
157 getElementIDForHostInstance = node => {
158 const internalInstance =
159 renderer.ComponentTree.getClosestInstanceFromNode(node);
160 return internalInstanceToIDMap.get(internalInstance) || null;
161 };
162 findHostInstanceForInternalID = (id: number) => {
163 const internalInstance = idToInternalInstanceMap.get(id);
164 return renderer.ComponentTree.getNodeFromInstance(internalInstance);
165 };
166 getNearestMountedDOMNode = (node: Element): null | Element => {
167 const internalInstance =
168 renderer.ComponentTree.getClosestInstanceFromNode(node);
169 if (internalInstance != null) {
170 return renderer.ComponentTree.getNodeFromInstance(internalInstance);
171 }
172 return null;
173 };
174 } else if (renderer.Mount.getID && renderer.Mount.getNode) {
175 getElementIDForHostInstance = node => {
176 // Not implemented.
177 return null;
178 };
179 findHostInstanceForInternalID = (id: number) => {
180 // Not implemented.
181 return null;
182 };
183 }
184
185 const supportsTogglingSuspense = false;
186
187 function getDisplayNameForElementID(id: number): string | null {
188 const internalInstance = idToInternalInstanceMap.get(id);
189 return internalInstance ? getData(internalInstance).displayName : null;
190 }
191
192 function getID(internalInstance: InternalInstance): number {
193 if (typeof internalInstance !== 'object' || internalInstance === null) {
194 throw new Error('Invalid internal instance: ' + internalInstance);
195 }
196 if (!internalInstanceToIDMap.has(internalInstance)) {
197 const id = getUID();
198 internalInstanceToIDMap.set(internalInstance, id);
199 idToInternalInstanceMap.set(id, internalInstance);
200 }
201 return internalInstanceToIDMap.get(internalInstance) as any as number;
202 }
203
204 function areEqualArrays(a: Array<any>, b: Array<any>) {
205 if (a.length !== b.length) {
206 return false;
207 }
208 for (let i = 0; i < a.length; i++) {
209 if (a[i] !== b[i]) {
210 return false;
211 }
212 }
213 return true;
214 }
215
216 // This is shared mutable state that lets us keep track of where we are.
217 let parentIDStack = [];
218
219 let oldReconcilerMethods = null;
220 if (renderer.Reconciler) {
221 // React 15
222 oldReconcilerMethods = decorateMany(renderer.Reconciler, {
223 mountComponent(fn, args) {
224 const internalInstance = args[0];
225 const hostContainerInfo = args[3];
226 if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
227 // $FlowFixMe[object-this-reference] found when upgrading Flow
228 return fn.apply(this, args);
229 }
230 if (hostContainerInfo._topLevelWrapper === undefined) {
231 // SSR
232 // $FlowFixMe[object-this-reference] found when upgrading Flow
233 return fn.apply(this, args);
234 }
235
236 const id = getID(internalInstance);
237 // Push the operation.
238 const parentID =
239 parentIDStack.length > 0
240 ? parentIDStack[parentIDStack.length - 1]
241 : 0;
242 recordMount(internalInstance, id, parentID);
243 parentIDStack.push(id);
244
245 // Remember the root.
246 internalInstanceToRootIDMap.set(
247 internalInstance,
248 getID(hostContainerInfo._topLevelWrapper),
249 );
250
251 try {
252 // $FlowFixMe[object-this-reference] found when upgrading Flow
253 const result = fn.apply(this, args);
254 parentIDStack.pop();
255 return result;
256 } catch (err) {
257 parentIDStack = [];
258 throw err;
259 } finally {
260 if (parentIDStack.length === 0) {
261 const rootID = internalInstanceToRootIDMap.get(internalInstance);
262 if (rootID === undefined) {
263 throw new Error('Expected to find root ID.');
264 }
265 flushPendingEvents(rootID);
266 }
267 }
268 },
269 performUpdateIfNecessary(fn, args) {
270 const internalInstance = args[0];
271 if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
272 // $FlowFixMe[object-this-reference] found when upgrading Flow
273 return fn.apply(this, args);
274 }
275
276 const id = getID(internalInstance);
277 parentIDStack.push(id);
278
279 const prevChildren = getChildren(internalInstance);
280 try {
281 // $FlowFixMe[object-this-reference] found when upgrading Flow
282 const result = fn.apply(this, args);
283
284 const nextChildren = getChildren(internalInstance);
285 if (!areEqualArrays(prevChildren, nextChildren)) {
286 // Push the operation
287 recordReorder(internalInstance, id, nextChildren);
288 }
289
290 parentIDStack.pop();
291 return result;
292 } catch (err) {
293 parentIDStack = [];
294 throw err;
295 } finally {
296 if (parentIDStack.length === 0) {
297 const rootID = internalInstanceToRootIDMap.get(internalInstance);
298 if (rootID === undefined) {
299 throw new Error('Expected to find root ID.');
300 }
301 flushPendingEvents(rootID);
302 }
303 }
304 },
305 receiveComponent(fn, args) {
306 const internalInstance = args[0];
307 if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
308 // $FlowFixMe[object-this-reference] found when upgrading Flow
309 return fn.apply(this, args);
310 }
311
312 const id = getID(internalInstance);
313 parentIDStack.push(id);
314
315 const prevChildren = getChildren(internalInstance);
316 try {
317 // $FlowFixMe[object-this-reference] found when upgrading Flow
318 const result = fn.apply(this, args);
319
320 const nextChildren = getChildren(internalInstance);
321 if (!areEqualArrays(prevChildren, nextChildren)) {
322 // Push the operation
323 recordReorder(internalInstance, id, nextChildren);
324 }
325
326 parentIDStack.pop();
327 return result;
328 } catch (err) {
329 parentIDStack = [];
330 throw err;
331 } finally {
332 if (parentIDStack.length === 0) {
333 const rootID = internalInstanceToRootIDMap.get(internalInstance);
334 if (rootID === undefined) {
335 throw new Error('Expected to find root ID.');
336 }
337 flushPendingEvents(rootID);
338 }
339 }
340 },
341 unmountComponent(fn, args) {
342 const internalInstance = args[0];
343 if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
344 // $FlowFixMe[object-this-reference] found when upgrading Flow
345 return fn.apply(this, args);
346 }
347
348 const id = getID(internalInstance);
349 parentIDStack.push(id);
350 try {
351 // $FlowFixMe[object-this-reference] found when upgrading Flow
352 const result = fn.apply(this, args);
353 parentIDStack.pop();
354
355 // Push the operation.
356 recordUnmount(internalInstance, id);
357
358 return result;
359 } catch (err) {
360 parentIDStack = [];
361 throw err;
362 } finally {
363 if (parentIDStack.length === 0) {
364 const rootID = internalInstanceToRootIDMap.get(internalInstance);
365 if (rootID === undefined) {
366 throw new Error('Expected to find root ID.');
367 }
368 flushPendingEvents(rootID);
369 }
370 }
371 },
372 });
373 }
374
375 function cleanup() {
376 if (oldReconcilerMethods !== null) {
377 if (renderer.Component) {
378 restoreMany(renderer.Component.Mixin, oldReconcilerMethods);
379 } else {
380 restoreMany(renderer.Reconciler, oldReconcilerMethods);
381 }
382 }
383 oldReconcilerMethods = null;
384 }
385
386 function recordMount(
387 internalInstance: InternalInstance,
388 id: number,
389 parentID: number,
390 ) {
391 const isRoot = parentID === 0;
392
393 // $FlowFixMe[constant-condition]
394 if (__DEBUG__) {
395 console.log(
396 '%crecordMount()',
397 'color: green; font-weight: bold;',
398 id,
399 getData(internalInstance).displayName,
400 );
401 }
402
403 if (isRoot) {
404 // TODO Is this right? For all versions?
405 const hasOwnerMetadata =
406 internalInstance._currentElement != null &&
407 internalInstance._currentElement._owner != null;
408
409 pushOperation(TREE_OPERATION_ADD);
410 pushOperation(id);
411 pushOperation(ElementTypeRoot);
412 pushOperation(0); // StrictMode compliant?
413 pushOperation(0); // Profiling flag
414 pushOperation(0); // StrictMode supported?
415 pushOperation(hasOwnerMetadata ? 1 : 0);
416
417 pushOperation(SUSPENSE_TREE_OPERATION_ADD);
418 pushOperation(id);
419 pushOperation(parentID);
420 pushOperation(getStringID(null)); // name
421 pushOperation(0); // isSuspended
422 // TODO: Measure rect of root
423 pushOperation(-1);
424 } else {
425 const type = getElementType(internalInstance);
426 const {displayName, key} = getData(internalInstance);
427
428 const ownerID =
429 internalInstance._currentElement != null &&
430 internalInstance._currentElement._owner != null
431 ? getID(internalInstance._currentElement._owner)
432 : 0;
433
434 const displayNameStringID = getStringID(displayName);
435 const keyStringID = getStringID(key);
436 pushOperation(TREE_OPERATION_ADD);
437 pushOperation(id);
438 pushOperation(type);
439 pushOperation(parentID);
440 pushOperation(ownerID);
441 pushOperation(displayNameStringID);
442 pushOperation(keyStringID);
443 pushOperation(getStringID(null)); // name prop
444 }
445 }
446
447 function recordReorder(
448 internalInstance: InternalInstance,
449 id: number,
450 nextChildren: Array<InternalInstance>,
451 ) {
452 pushOperation(TREE_OPERATION_REORDER_CHILDREN);
453 pushOperation(id);
454 const nextChildIDs = nextChildren.map(getID);
455 pushOperation(nextChildIDs.length);
456 for (let i = 0; i < nextChildIDs.length; i++) {
457 pushOperation(nextChildIDs[i]);
458 }
459 }
460
461 function recordUnmount(internalInstance: InternalInstance, id: number) {
462 const isRoot = parentIDStack.length === 0;
463 if (isRoot) {
464 pendingUnmountedRootID = id;
465 } else {
466 pendingUnmountedIDs.push(id);
467 }
468 idToInternalInstanceMap.delete(id);
469 }
470
471 function crawlAndRecordInitialMounts(
472 id: number,
473 parentID: number,
474 rootID: number,
475 ) {
476 // $FlowFixMe[constant-condition]
477 if (__DEBUG__) {
478 console.group('crawlAndRecordInitialMounts() id:', id);
479 }
480
481 const internalInstance = idToInternalInstanceMap.get(id);
482 if (internalInstance != null) {
483 internalInstanceToRootIDMap.set(internalInstance, rootID);
484 recordMount(internalInstance, id, parentID);
485 getChildren(internalInstance).forEach(child =>
486 crawlAndRecordInitialMounts(getID(child), id, rootID),
487 );
488 }
489
490 // $FlowFixMe[constant-condition]
491 if (__DEBUG__) {
492 console.groupEnd();
493 }
494 }
495
496 function flushInitialOperations() {
497 // Crawl roots though and register any nodes that mounted before we were injected.
498
499 const roots =
500 renderer.Mount._instancesByReactRootID ||
501 renderer.Mount._instancesByContainerID;
502
503 for (const key in roots) {
504 const internalInstance = roots[key];
505 const id = getID(internalInstance);
506 crawlAndRecordInitialMounts(id, 0, id);
507 flushPendingEvents(id);
508 }
509 }
510
511 const pendingOperations: Array<number> = [];
512 const pendingStringTable: Map<string, number> = new Map();
513 let pendingUnmountedIDs: Array<number> = [];
514 let pendingStringTableLength: number = 0;
515 let pendingUnmountedRootID: number | null = null;
516
517 function flushPendingEvents(rootID: number) {
518 if (
519 pendingOperations.length === 0 &&
520 pendingUnmountedIDs.length === 0 &&
521 pendingUnmountedRootID === null
522 ) {
523 return;
524 }
525
526 const numUnmountIDs =
527 pendingUnmountedIDs.length + (pendingUnmountedRootID === null ? 0 : 1);
528
529 const operations = new Array<number>(
530 // Identify which renderer this update is coming from.
531 2 + // [rendererID, rootFiberID]
532 // How big is the string table?
533 1 + // [stringTableLength]
534 // Then goes the actual string table.
535 pendingStringTableLength +
536 // All unmounts are batched in a single message.
537 // [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
538 (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
539 // [SUSPENSE_TREE_OPERATION_REMOVE, 1, pendingUnmountedRootID]
540 (pendingUnmountedRootID === null ? 0 : 3) +
541 // Mount operations
542 pendingOperations.length,
543 );
544
545 // Identify which renderer this update is coming from.
546 // This enables roots to be mapped to renderers,
547 // Which in turn enables fiber properations, states, and hooks to be inspected.
548 let i = 0;
549 operations[i++] = rendererID;
550 operations[i++] = rootID;
551
552 // Now fill in the string table.
553 // [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...]
554 operations[i++] = pendingStringTableLength;
555 pendingStringTable.forEach((value, key) => {
556 operations[i++] = key.length;
557 const encodedKey = utfEncodeString(key);
558 for (let j = 0; j < encodedKey.length; j++) {
559 operations[i + j] = encodedKey[j];
560 }
561 i += key.length;
562 });
563
564 if (numUnmountIDs > 0) {
565 // All unmounts except roots are batched in a single message.
566 operations[i++] = TREE_OPERATION_REMOVE;
567 // The first number is how many unmounted IDs we're gonna send.
568 operations[i++] = numUnmountIDs;
569 // Fill in the unmounts
570 for (let j = 0; j < pendingUnmountedIDs.length; j++) {
571 operations[i++] = pendingUnmountedIDs[j];
572 }
573 // The root ID should always be unmounted last.
574 if (pendingUnmountedRootID !== null) {
575 operations[i] = pendingUnmountedRootID;
576 i++;
577
578 operations[i++] = SUSPENSE_TREE_OPERATION_REMOVE;
579 operations[i++] = 1;
580 operations[i++] = pendingUnmountedRootID;
581 }
582 }
583
584 // Fill in the rest of the operations.
585 for (let j = 0; j < pendingOperations.length; j++) {
586 operations[i + j] = pendingOperations[j];
587 }
588 i += pendingOperations.length;
589
590 // $FlowFixMe[constant-condition]
591 if (__DEBUG__) {
592 printOperationsArray(operations);
593 }
594
595 // If we've already connected to the frontend, just pass the operations through.
596 hook.emit('operations', operations);
597
598 pendingOperations.length = 0;
599 pendingUnmountedIDs = [];
600 pendingUnmountedRootID = null;
601 pendingStringTable.clear();
602 pendingStringTableLength = 0;
603 }
604
605 function pushOperation(op: number): void {
606 if (__DEV__) {
607 if (!Number.isInteger(op)) {
608 console.error(
609 'pushOperation() was called but the value is not an integer.',
610 op,
611 );
612 }
613 }
614 pendingOperations.push(op);
615 }
616
617 function getStringID(str: string | null): number {
618 if (str === null) {
619 return 0;
620 }
621 const existingID = pendingStringTable.get(str);
622 if (existingID !== undefined) {
623 return existingID;
624 }
625 const stringID = pendingStringTable.size + 1;
626 pendingStringTable.set(str, stringID);
627 // The string table total length needs to account
628 // both for the string length, and for the array item
629 // that contains the length itself. Hence + 1.
630 pendingStringTableLength += str.length + 1;
631 return stringID;
632 }
633
634 let currentlyInspectedElementID: number | null = null;
635 let currentlyInspectedPaths: Object = {};
636
637 // Track the intersection of currently inspected paths,
638 // so that we can send their data along if the element is re-rendered.
639 function mergeInspectedPaths(path: Array<string | number>) {
640 let current = currentlyInspectedPaths;
641 path.forEach(key => {
642 if (!current[key]) {
643 current[key] = {};
644 }
645 current = current[key];
646 });
647 }
648
649 function createIsPathAllowed(key: string) {
650 // This function helps prevent previously-inspected paths from being dehydrated in updates.
651 // This is important to avoid a bad user experience where expanded toggles collapse on update.
652 return function isPathAllowed(path: Array<string | number>): boolean {
653 let current = currentlyInspectedPaths[key];
654 if (!current) {
655 return false;
656 }
657 for (let i = 0; i < path.length; i++) {
658 current = current[path[i]];
659 if (!current) {
660 return false;
661 }
662 }
663 return true;
664 };
665 }
666
667 // Fast path props lookup for React Native style editor.
668 function getInstanceAndStyle(id: number): InstanceAndStyle {
669 let instance = null;
670 let style = null;
671
672 const internalInstance = idToInternalInstanceMap.get(id);
673 if (internalInstance != null) {
674 instance = internalInstance._instance || null;
675
676 const element = internalInstance._currentElement;
677 if (element != null && element.props != null) {
678 style = element.props.style || null;
679 }
680 }
681
682 return {
683 instance,
684 style,
685 };
686 }
687
688 function updateSelectedElement(id: number): void {
689 const internalInstance = idToInternalInstanceMap.get(id);
690 if (internalInstance == null) {
691 console.warn(`Could not find instance with id "${id}"`);
692 return;
693 }
694
695 switch (getElementType(internalInstance)) {
696 case ElementTypeClass:
697 global.$r = internalInstance._instance;
698 break;
699 case ElementTypeFunction:
700 const element = internalInstance._currentElement;
701 if (element == null) {
702 console.warn(`Could not find element with id "${id}"`);
703 return;
704 }
705
706 global.$r = {
707 props: element.props,
708 type: element.type,
709 };
710 break;
711 default:
712 global.$r = null;
713 break;
714 }
715 }
716
717 function storeAsGlobal(
718 id: number,
719 path: Array<string | number>,
720 count: number,
721 ): void {
722 const inspectedElement = inspectElementRaw(id);
723 if (inspectedElement !== null) {
724 const value = getInObject(inspectedElement, path);
725 const key = `$reactTemp${count}`;
726
727 window[key] = value;
728
729 console.log(key);
730 console.log(value);
731 }
732 }
733
734 function getSerializedElementValueByPath(
735 id: number,
736 path: Array<string | number>,
737 ): ?string {
738 const inspectedElement = inspectElementRaw(id);
739 if (inspectedElement !== null) {
740 const valueToCopy = getInObject(inspectedElement, path);
741
742 return serializeToString(valueToCopy);
743 }
744 }
745
746 function inspectElement(
747 requestID: number,
748 id: number,
749 path: Array<string | number> | null,
750 forceFullData: boolean,
751 ): InspectedElementPayload {
752 if (forceFullData || currentlyInspectedElementID !== id) {
753 currentlyInspectedElementID = id;
754 currentlyInspectedPaths = {};
755 }
756
757 const inspectedElement = inspectElementRaw(id);
758 if (inspectedElement === null) {
759 return {
760 id,
761 responseID: requestID,
762 type: 'not-found',
763 };
764 }
765
766 if (path !== null) {
767 mergeInspectedPaths(path);
768 }
769
770 // Any time an inspected element has an update,
771 // we should update the selected $r value as wel.
772 // Do this before dehydration (cleanForBridge).
773 updateSelectedElement(id);
774
775 inspectedElement.context = cleanForBridge(
776 inspectedElement.context,
777 createIsPathAllowed('context'),
778 );
779 inspectedElement.props = cleanForBridge(
780 inspectedElement.props,
781 createIsPathAllowed('props'),
782 );
783 inspectedElement.state = cleanForBridge(
784 inspectedElement.state,
785 createIsPathAllowed('state'),
786 );
787 inspectedElement.suspendedBy = cleanForBridge(
788 inspectedElement.suspendedBy,
789 createIsPathAllowed('suspendedBy'),
790 );
791
792 return {
793 id,
794 responseID: requestID,
795 type: 'full-data',
796 value: inspectedElement,
797 };
798 }
799
800 function inspectElementRaw(id: number): InspectedElement | null {
801 const internalInstance = idToInternalInstanceMap.get(id);
802
803 if (internalInstance == null) {
804 return null;
805 }
806
807 const rootID = internalInstanceToRootIDMap.get(internalInstance);
808 if (rootID === undefined) {
809 throw new Error('Expected to find root ID.');
810 }
811 const isRoot = rootID === id;
812 return isRoot
813 ? inspectRootsRaw(rootID)
814 : inspectInternalInstanceRaw(id, internalInstance);
815 }
816
817 function inspectInternalInstanceRaw(
818 id: number,
819 internalInstance: InternalInstance,
820 ): InspectedElement | null {
821 const {key} = getData(internalInstance);
822 const type = getElementType(internalInstance);
823
824 let context = null;
825 let owners = null;
826 let props = null;
827 let state = null;
828
829 const element = internalInstance._currentElement;
830 if (element !== null) {
831 props = element.props;
832
833 let owner = element._owner;
834 if (owner) {
835 owners = [] as Array<SerializedElement>;
836 while (owner != null) {
837 owners.push({
838 displayName: getData(owner).displayName || 'Unknown',
839 id: getID(owner),
840 key: element.key,
841 env: null,
842 stack: null,
843 type: getElementType(owner),
844 });
845 if (owner._currentElement) {
846 owner = owner._currentElement._owner;
847 }
848 }
849 }
850 }
851
852 const publicInstance = internalInstance._instance;
853 if (publicInstance != null) {
854 context = publicInstance.context || null;
855 state = publicInstance.state || null;
856 }
857
858 // Not implemented
859 const errors: Array<[string, number]> = [];
860 const warnings: Array<[string, number]> = [];
861
862 return {
863 id,
864
865 // Does the current renderer support editable hooks and function props?
866 canEditHooks: false,
867 canEditFunctionProps: false,
868
869 // Does the current renderer support advanced editing interface?
870 canEditHooksAndDeletePaths: false,
871 canEditHooksAndRenamePaths: false,
872 canEditFunctionPropsDeletePaths: false,
873 canEditFunctionPropsRenamePaths: false,
874
875 // Toggle error boundary did not exist in legacy versions
876 canToggleError: false,
877 isErrored: false,
878
879 // Suspense did not exist in legacy versions
880 canToggleSuspense: false,
881 isSuspended: null,
882
883 source: null,
884
885 stack: null,
886
887 // Only legacy context exists in legacy versions.
888 hasLegacyContext: true,
889
890 type: type,
891
892 key: key != null ? key : null,
893
894 // Inspectable properties.
895 context,
896 hooks: null,
897 props,
898 state,
899 errors,
900 warnings,
901
902 // Not supported in legacy renderers.
903 suspendedBy: [],
904 suspendedByRange: null,
905 unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
906
907 // List of owners
908 owners,
909
910 env: null,
911
912 rootType: null,
913 rendererPackageName: null,
914 rendererVersion: null,
915
916 plugins: {
917 stylex: null,
918 },
919
920 nativeTag: null,
921 };
922 }
923
924 function inspectRootsRaw(arbitraryRootID: number): InspectedElement | null {
925 const roots =
926 renderer.Mount._instancesByReactRootID ||
927 renderer.Mount._instancesByContainerID;
928
929 const inspectedRoots: InspectedElement = {
930 // invariants
931 id: arbitraryRootID,
932 type: ElementTypeRoot,
933 // Properties we merge
934 isErrored: false,
935 errors: [],
936 warnings: [],
937 suspendedBy: [],
938 suspendedByRange: null,
939 // TODO: How to merge these?
940 unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
941 // Properties where merging doesn't make sense so we ignore them entirely in the UI
942 rootType: null,
943 plugins: {stylex: null},
944 nativeTag: null,
945 env: null,
946 source: null,
947 stack: null,
948 // TODO: We could make the Frontend accept an array to display
949 // a list of unique renderers contributing to this Screen.
950 rendererPackageName: null,
951 rendererVersion: null,
952 // These don't make sense for a Root. They're just bottom values.
953 key: null,
954 canEditFunctionProps: false,
955 canEditHooks: false,
956 canEditFunctionPropsDeletePaths: false,
957 canEditFunctionPropsRenamePaths: false,
958 canEditHooksAndDeletePaths: false,
959 canEditHooksAndRenamePaths: false,
960 canToggleError: false,
961 canToggleSuspense: false,
962 isSuspended: false,
963 hasLegacyContext: false,
964 context: null,
965 hooks: null,
966 props: null,
967 state: null,
968 owners: null,
969 };
970
971 let minSuspendedByRange = Infinity;
972 let maxSuspendedByRange = -Infinity;
973
974 for (const rootKey in roots) {
975 const internalInstance = roots[rootKey];
976 const id = getID(internalInstance);
977 const inspectedRoot = inspectInternalInstanceRaw(id, internalInstance);
978
979 if (inspectedRoot === null) {
980 return null;
981 }
982
983 if (inspectedRoot.isErrored) {
984 inspectedRoots.isErrored = true;
985 }
986 for (let i = 0; i < inspectedRoot.errors.length; i++) {
987 inspectedRoots.errors.push(inspectedRoot.errors[i]);
988 }
989 for (let i = 0; i < inspectedRoot.warnings.length; i++) {
990 inspectedRoots.warnings.push(inspectedRoot.warnings[i]);
991 }
992 for (let i = 0; i < inspectedRoot.suspendedBy.length; i++) {
993 inspectedRoots.suspendedBy.push(inspectedRoot.suspendedBy[i]);
994 }
995 const suspendedByRange = inspectedRoot.suspendedByRange;
996 if (suspendedByRange !== null) {
997 if (suspendedByRange[0] < minSuspendedByRange) {
998 minSuspendedByRange = suspendedByRange[0];
999 }
1000 if (suspendedByRange[1] > maxSuspendedByRange) {
1001 maxSuspendedByRange = suspendedByRange[1];
1002 }
1003 }
1004 }
1005
1006 if (minSuspendedByRange !== Infinity || maxSuspendedByRange !== -Infinity) {
1007 inspectedRoots.suspendedByRange = [
1008 minSuspendedByRange,
1009 maxSuspendedByRange,
1010 ];
1011 }
1012
1013 return inspectedRoots;
1014 }
1015
1016 function logElementToConsole(id: number): void {
1017 const result = inspectElementRaw(id);
1018 if (result === null) {
1019 console.warn(`Could not find element with id "${id}"`);
1020 return;
1021 }
1022
1023 const displayName = getDisplayNameForElementID(id);
1024
1025 const supportsGroup = typeof console.groupCollapsed === 'function';
1026 if (supportsGroup) {
1027 console.groupCollapsed(
1028 `[Click to expand] %c<${displayName || 'Component'} />`,
1029 // --dom-tag-name-color is the CSS variable Chrome styles HTML elements with in the console.
1030 'color: var(--dom-tag-name-color); font-weight: normal;',
1031 );
1032 }
1033 if (result.props !== null) {
1034 console.log('Props:', result.props);
1035 }
1036 if (result.state !== null) {
1037 console.log('State:', result.state);
1038 }
1039 if (result.context !== null) {
1040 console.log('Context:', result.context);
1041 }
1042 const hostInstance = findHostInstanceForInternalID(id);
1043 if (hostInstance !== null) {
1044 console.log('Node:', hostInstance);
1045 }
1046 if (window.chrome || /firefox/i.test(navigator.userAgent)) {
1047 console.log(
1048 'Right-click any value to save it as a global variable for further inspection.',
1049 );
1050 }
1051 if (supportsGroup) {
1052 console.groupEnd();
1053 }
1054 }
1055
1056 function getElementAttributeByPath(
1057 id: number,
1058 path: Array<string | number>,
1059 ): mixed {
1060 const inspectedElement = inspectElementRaw(id);
1061 if (inspectedElement !== null) {
1062 return getInObject(inspectedElement, path);
1063 }
1064 return undefined;
1065 }
1066
1067 function getElementSourceFunctionById(id: number): null | Function {
1068 const internalInstance = idToInternalInstanceMap.get(id);
1069 if (internalInstance == null) {
1070 console.warn(`Could not find instance with id "${id}"`);
1071 return null;
1072 }
1073
1074 const element = internalInstance._currentElement;
1075 if (element == null) {
1076 console.warn(`Could not find element with id "${id}"`);
1077 return null;
1078 }
1079
1080 return element.type;
1081 }
1082
1083 function deletePath(
1084 type: 'context' | 'hooks' | 'props' | 'state',
1085 id: number,
1086 hookID: ?number,
1087 path: Array<string | number>,
1088 ): void {
1089 const internalInstance = idToInternalInstanceMap.get(id);
1090 if (internalInstance != null) {
1091 const publicInstance = internalInstance._instance;
1092 if (publicInstance != null) {
1093 switch (type) {
1094 case 'context':
1095 deletePathInObject(publicInstance.context, path);
1096 forceUpdate(publicInstance);
1097 break;
1098 case 'hooks':
1099 throw new Error('Hooks not supported by this renderer');
1100 case 'props':
1101 const element = internalInstance._currentElement;
1102 internalInstance._currentElement = {
1103 ...element,
1104 props: copyWithDelete(element.props, path),
1105 };
1106 forceUpdate(publicInstance);
1107 break;
1108 case 'state':
1109 deletePathInObject(publicInstance.state, path);
1110 forceUpdate(publicInstance);
1111 break;
1112 }
1113 }
1114 }
1115 }
1116
1117 function renamePath(
1118 type: 'context' | 'hooks' | 'props' | 'state',
1119 id: number,
1120 hookID: ?number,
1121 oldPath: Array<string | number>,
1122 newPath: Array<string | number>,
1123 ): void {
1124 const internalInstance = idToInternalInstanceMap.get(id);
1125 if (internalInstance != null) {
1126 const publicInstance = internalInstance._instance;
1127 if (publicInstance != null) {
1128 switch (type) {
1129 case 'context':
1130 renamePathInObject(publicInstance.context, oldPath, newPath);
1131 forceUpdate(publicInstance);
1132 break;
1133 case 'hooks':
1134 throw new Error('Hooks not supported by this renderer');
1135 case 'props':
1136 const element = internalInstance._currentElement;
1137 internalInstance._currentElement = {
1138 ...element,
1139 props: copyWithRename(element.props, oldPath, newPath),
1140 };
1141 forceUpdate(publicInstance);
1142 break;
1143 case 'state':
1144 renamePathInObject(publicInstance.state, oldPath, newPath);
1145 forceUpdate(publicInstance);
1146 break;
1147 }
1148 }
1149 }
1150 }
1151
1152 function overrideValueAtPath(
1153 type: 'context' | 'hooks' | 'props' | 'state',
1154 id: number,
1155 hookID: ?number,
1156 path: Array<string | number>,
1157 value: any,
1158 ): void {
1159 const internalInstance = idToInternalInstanceMap.get(id);
1160 if (internalInstance != null) {
1161 const publicInstance = internalInstance._instance;
1162 if (publicInstance != null) {
1163 switch (type) {
1164 case 'context':
1165 setInObject(publicInstance.context, path, value);
1166 forceUpdate(publicInstance);
1167 break;
1168 case 'hooks':
1169 throw new Error('Hooks not supported by this renderer');
1170 case 'props':
1171 const element = internalInstance._currentElement;
1172 internalInstance._currentElement = {
1173 ...element,
1174 props: copyWithSet(element.props, path, value),
1175 };
1176 forceUpdate(publicInstance);
1177 break;
1178 case 'state':
1179 setInObject(publicInstance.state, path, value);
1180 forceUpdate(publicInstance);
1181 break;
1182 }
1183 }
1184 }
1185 }
1186
1187 // v16+ only features
1188 const getProfilingData = () => {
1189 throw new Error('getProfilingData not supported by this renderer');
1190 };
1191 const handleCommitFiberRoot = () => {
1192 throw new Error('handleCommitFiberRoot not supported by this renderer');
1193 };
1194 const handleCommitFiberUnmount = () => {
1195 throw new Error('handleCommitFiberUnmount not supported by this renderer');
1196 };
1197 const handlePostCommitFiberRoot = () => {
1198 throw new Error('handlePostCommitFiberRoot not supported by this renderer');
1199 };
1200 const overrideError = () => {
1201 throw new Error('overrideError not supported by this renderer');
1202 };
1203 const overrideSuspense = () => {
1204 throw new Error('overrideSuspense not supported by this renderer');
1205 };
1206 const overrideSuspenseMilestone = () => {
1207 throw new Error('overrideSuspenseMilestone not supported by this renderer');
1208 };
1209 const startProfiling = () => {
1210 // Do not throw, since this would break a multi-root scenario where v15 and v16 were both present.
1211 };
1212 const stopProfiling = () => {
1213 // Do not throw, since this would break a multi-root scenario where v15 and v16 were both present.
1214 };
1215
1216 function getBestMatchForTrackedPath(): PathMatch | null {
1217 // Not implemented.
1218 return null;
1219 }
1220
1221 function getPathForElement(id: number): Array<PathFrame> | null {
1222 // Not implemented.
1223 return null;
1224 }
1225
1226 function updateComponentFilters(componentFilters: Array<ComponentFilter>) {
1227 // Not implemented.
1228 }
1229
1230 function getEnvironmentNames(): Array<string> {
1231 // No RSC support.
1232 return [];
1233 }
1234
1235 function setTraceUpdatesEnabled(enabled: boolean) {
1236 // Not implemented.
1237 }
1238
1239 function setTrackedPath(path: Array<PathFrame> | null) {
1240 // Not implemented.
1241 }
1242
1243 function getOwnersList(id: number): Array<SerializedElement> | null {
1244 // Not implemented.
1245 return null;
1246 }
1247
1248 function clearErrorsAndWarnings() {
1249 // Not implemented
1250 }
1251
1252 function clearErrorsForElementID(id: number) {
1253 // Not implemented
1254 }
1255
1256 function clearWarningsForElementID(id: number) {
1257 // Not implemented
1258 }
1259
1260 function hasElementWithId(id: number): boolean {
1261 return idToInternalInstanceMap.has(id);
1262 }
1263
1264 return {
1265 clearErrorsAndWarnings,
1266 clearErrorsForElementID,
1267 clearWarningsForElementID,
1268 cleanup,
1269 getSerializedElementValueByPath,
1270 deletePath,
1271 flushInitialOperations,
1272 getBestMatchForTrackedPath,
1273 getDisplayNameForElementID,
1274 getNearestMountedDOMNode,
1275 getElementIDForHostInstance,
1276 getSuspenseNodeIDForHostInstance(id: number): null {
1277 return null;
1278 },
1279 getInstanceAndStyle,
1280 findHostInstancesForElementID: (id: number) => {
1281 const hostInstance = findHostInstanceForInternalID(id);
1282 return hostInstance == null ? null : [hostInstance];
1283 },
1284 findLastKnownRectsForID() {
1285 return null;
1286 },
1287 getOwnersList,
1288 getPathForElement,
1289 getProfilingData,
1290 handleCommitFiberRoot,
1291 handleCommitFiberUnmount,
1292 handlePostCommitFiberRoot,
1293 hasElementWithId,
1294 inspectElement,
1295 logElementToConsole,
1296 overrideError,
1297 overrideSuspense,
1298 overrideSuspenseMilestone,
1299 overrideValueAtPath,
1300 renamePath,
1301 getElementAttributeByPath,
1302 getElementSourceFunctionById,
1303 renderer,
1304 setTraceUpdatesEnabled,
1305 setTrackedPath,
1306 startProfiling,
1307 stopProfiling,
1308 storeAsGlobal,
1309 supportsTogglingSuspense,
1310 updateComponentFilters,
1311 getEnvironmentNames,
1312 };
1313 }