main
js 1,326 lines 38.4 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 LRU from 'lru-cache';
11 import {
12 REACT_CONSUMER_TYPE,
13 REACT_CONTEXT_TYPE,
14 REACT_FORWARD_REF_TYPE,
15 REACT_FRAGMENT_TYPE,
16 REACT_LAZY_TYPE,
17 REACT_ELEMENT_TYPE,
18 REACT_LEGACY_ELEMENT_TYPE,
19 REACT_MEMO_TYPE,
20 REACT_PORTAL_TYPE,
21 REACT_PROFILER_TYPE,
22 REACT_STRICT_MODE_TYPE,
23 REACT_SUSPENSE_LIST_TYPE,
24 REACT_SUSPENSE_TYPE,
25 REACT_TRACING_MARKER_TYPE,
26 REACT_VIEW_TRANSITION_TYPE,
27 } from 'shared/ReactSymbols';
28 import {
29 TREE_OPERATION_ADD,
30 TREE_OPERATION_REMOVE,
31 TREE_OPERATION_REORDER_CHILDREN,
32 TREE_OPERATION_SET_SUBTREE_MODE,
33 TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
34 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
35 TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE,
36 LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY,
37 LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
38 LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
39 LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
40 SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
41 SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
42 SUSPENSE_TREE_OPERATION_ADD,
43 SUSPENSE_TREE_OPERATION_REMOVE,
44 SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
45 SUSPENSE_TREE_OPERATION_RESIZE,
46 SUSPENSE_TREE_OPERATION_SUSPENDERS,
47 } from './constants';
48 import {
49 ComponentFilterActivitySlice,
50 ComponentFilterElementType,
51 ComponentFilterLocation,
52 ElementTypeHostComponent,
53 } from './frontend/types';
54 import {
55 ElementTypeRoot,
56 ElementTypeClass,
57 ElementTypeForwardRef,
58 ElementTypeFunction,
59 ElementTypeMemo,
60 ElementTypeVirtual,
61 } from 'react-devtools-shared/src/frontend/types';
62 import {
63 localStorageGetItem,
64 localStorageSetItem,
65 sessionStorageGetItem,
66 sessionStorageRemoveItem,
67 sessionStorageSetItem,
68 } from 'react-devtools-shared/src/storage';
69 import {meta} from './hydration';
70 import isArray from './isArray';
71
72 import type {
73 ComponentFilter,
74 ElementType,
75 SerializedElement as SerializedElementFrontend,
76 LRUCache,
77 } from 'react-devtools-shared/src/frontend/types';
78 import type {
79 ProfilingSettings,
80 SerializedElement as SerializedElementBackend,
81 } from 'react-devtools-shared/src/backend/types';
82 import {isSynchronousXHRSupported} from './backend/utils';
83
84 // $FlowFixMe[method-unbinding]
85 const hasOwnProperty = Object.prototype.hasOwnProperty;
86
87 const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
88
89 // On large trees, encoding takes significant time.
90 // Try to reuse the already encoded strings.
91 const encodedStringCache: LRUCache<string, Array<number>> = new LRU({
92 max: 1000,
93 });
94
95 // Previously, the type of `Context.Provider`.
96 const LEGACY_REACT_PROVIDER_TYPE: symbol = Symbol.for('react.provider');
97
98 export function alphaSortKeys(
99 a: string | number | symbol,
100 b: string | number | symbol,
101 ): number {
102 if (a.toString() > b.toString()) {
103 return 1;
104 } else if (b.toString() > a.toString()) {
105 return -1;
106 } else {
107 return 0;
108 }
109 }
110
111 export function getAllEnumerableKeys(
112 obj: Object,
113 ): Set<string | number | symbol> {
114 const keys = new Set<string | number | symbol>();
115 let current = obj;
116 while (current != null) {
117 const currentKeys = [
118 ...Object.keys(current),
119 ...Object.getOwnPropertySymbols(current),
120 ];
121 const descriptors = Object.getOwnPropertyDescriptors(current);
122 currentKeys.forEach(key => {
123 // $FlowFixMe[incompatible-type]: key can be a Symbol https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
124 if (descriptors[key].enumerable) {
125 keys.add(key);
126 }
127 });
128 current = Object.getPrototypeOf(current);
129 }
130 return keys;
131 }
132
133 // Mirror https://github.com/facebook/react/blob/7c21bf72ace77094fd1910cc350a548287ef8350/packages/shared/getComponentName.js#L27-L37
134 export function getWrappedDisplayName(
135 outerType: mixed,
136 innerType: any,
137 wrapperName: string,
138 fallbackName?: string,
139 ): string {
140 const displayName = (outerType as any)?.displayName;
141 return (
142 displayName || `${wrapperName}(${getDisplayName(innerType, fallbackName)})`
143 );
144 }
145
146 export function getDisplayName(
147 type: Function,
148 fallbackName: string = 'Anonymous',
149 ): string {
150 const nameFromCache = cachedDisplayNames.get(type);
151 if (nameFromCache != null) {
152 return nameFromCache;
153 }
154
155 let displayName = fallbackName;
156
157 // The displayName property is not guaranteed to be a string.
158 // It's only safe to use for our purposes if it's a string.
159 // github.com/facebook/react-devtools/issues/803
160 if (typeof type.displayName === 'string') {
161 displayName = type.displayName;
162 } else if (typeof type.name === 'string' && type.name !== '') {
163 displayName = type.name;
164 }
165
166 cachedDisplayNames.set(type, displayName);
167 return displayName;
168 }
169
170 let uidCounter: number = 0;
171
172 export function getUID(): number {
173 return ++uidCounter;
174 }
175
176 export function utfDecodeStringWithRanges(
177 array: Array<number>,
178 left: number,
179 right: number,
180 ): string {
181 let string = '';
182 for (let i = left; i <= right; i++) {
183 string += String.fromCodePoint(array[i]);
184 }
185 return string;
186 }
187
188 function surrogatePairToCodePoint(
189 charCode1: number,
190 charCode2: number,
191 ): number {
192 return ((charCode1 & 0x3ff) << 10) + (charCode2 & 0x3ff) + 0x10000;
193 }
194
195 // Credit for this encoding approach goes to Tim Down:
196 // https://stackoverflow.com/questions/4877326/how-can-i-tell-if-a-string-contains-multibyte-characters-in-javascript
197 export function utfEncodeString(string: string): Array<number> {
198 const cached = encodedStringCache.get(string);
199 if (cached !== undefined) {
200 return cached;
201 }
202
203 const encoded = [];
204 let i = 0;
205 let charCode;
206 while (i < string.length) {
207 charCode = string.charCodeAt(i);
208 // Handle multibyte unicode characters (like emoji).
209 if ((charCode & 0xf800) === 0xd800) {
210 encoded.push(surrogatePairToCodePoint(charCode, string.charCodeAt(++i)));
211 } else {
212 encoded.push(charCode);
213 }
214 ++i;
215 }
216
217 encodedStringCache.set(string, encoded);
218
219 return encoded;
220 }
221
222 export function printOperationsArray(operations: Array<number>) {
223 // The first two values are always rendererID and rootID
224 const rendererID = operations[0];
225 const rootID = operations[1];
226
227 const logs = [`operations for renderer:${rendererID} and root:${rootID}`];
228
229 let i = 2;
230
231 // Reassemble the string table.
232 const stringTable: Array<null | string> = [
233 null, // ID = 0 corresponds to the null string.
234 ];
235 const stringTableSize = operations[i++];
236 const stringTableEnd = i + stringTableSize;
237 while (i < stringTableEnd) {
238 const nextLength = operations[i++];
239 const nextString = utfDecodeStringWithRanges(
240 operations,
241 i,
242 i + nextLength - 1,
243 );
244 stringTable.push(nextString);
245 i += nextLength;
246 }
247
248 while (i < operations.length) {
249 const operation = operations[i];
250
251 switch (operation) {
252 case TREE_OPERATION_ADD: {
253 const id = operations[i + 1] as any as number;
254 const type = operations[i + 2] as any as ElementType;
255
256 i += 3;
257
258 if (type === ElementTypeRoot) {
259 logs.push(`Add new root node ${id}`);
260
261 i++; // isStrictModeCompliant
262 i++; // supportsProfiling
263 i++; // supportsStrictMode
264 i++; // hasOwnerMetadata
265 } else {
266 const parentID = operations[i] as any as number;
267 i++;
268
269 i++; // ownerID
270
271 const displayNameStringID = operations[i];
272 const displayName = stringTable[displayNameStringID];
273 i++;
274
275 i++; // key
276 i++; // name
277
278 logs.push(
279 `Add node ${id} (${displayName || 'null'}) as child of ${parentID}`,
280 );
281 }
282 break;
283 }
284 case TREE_OPERATION_REMOVE: {
285 const removeLength = operations[i + 1] as any as number;
286 i += 2;
287
288 for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
289 const id = operations[i] as any as number;
290 i += 1;
291
292 logs.push(`Remove node ${id}`);
293 }
294 break;
295 }
296 case TREE_OPERATION_SET_SUBTREE_MODE: {
297 const id = operations[i + 1];
298 const mode = operations[i + 2];
299
300 i += 3;
301
302 logs.push(`Mode ${mode} set for subtree with root ${id}`);
303 break;
304 }
305 case TREE_OPERATION_REORDER_CHILDREN: {
306 const id = operations[i + 1] as any as number;
307 const numChildren = operations[i + 2] as any as number;
308 i += 3;
309 const children = operations.slice(i, i + numChildren);
310 i += numChildren;
311
312 logs.push(`Re-order node ${id} children ${children.join(',')}`);
313 break;
314 }
315 case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
316 // Base duration updates are only sent while profiling is in progress.
317 // We can ignore them at this point.
318 // The profiler UI uses them lazily in order to generate the tree.
319 i += 3;
320 break;
321 case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {
322 const id = operations[i + 1];
323 const numErrors = operations[i + 2];
324 const numWarnings = operations[i + 3];
325
326 i += 4;
327
328 logs.push(
329 `Node ${id} has ${numErrors} errors and ${numWarnings} warnings`,
330 );
331 break;
332 }
333 case SUSPENSE_TREE_OPERATION_ADD: {
334 const fiberID = operations[i + 1];
335 const parentID = operations[i + 2];
336 const nameStringID = operations[i + 3];
337 const isSuspended = operations[i + 4];
338 const numRects = operations[i + 5];
339
340 i += 6;
341
342 const name = stringTable[nameStringID];
343 let rects: string;
344 if (numRects === -1) {
345 rects = 'null';
346 } else {
347 rects = '[';
348 for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
349 const offset = i + rectIndex * 4;
350 const x = operations[offset + 0];
351 const y = operations[offset + 1];
352 const width = operations[offset + 2];
353 const height = operations[offset + 3];
354
355 if (rectIndex > 0) {
356 rects += ', ';
357 }
358 rects += `(${x}, ${y}, ${width}, ${height})`;
359
360 i += 4;
361 }
362 rects += ']';
363 }
364
365 logs.push(
366 `Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID} suspended ${isSuspended}`,
367 );
368 break;
369 }
370 case SUSPENSE_TREE_OPERATION_REMOVE: {
371 const removeLength = operations[i + 1] as any as number;
372 i += 2;
373
374 for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
375 const id = operations[i] as any as number;
376 i += 1;
377
378 logs.push(`Remove suspense node ${id}`);
379 }
380
381 break;
382 }
383 case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: {
384 const id = operations[i + 1] as any as number;
385 const numChildren = operations[i + 2] as any as number;
386 i += 3;
387 const children = operations.slice(i, i + numChildren);
388 i += numChildren;
389
390 logs.push(
391 `Re-order suspense node ${id} children ${children.join(',')}`,
392 );
393 break;
394 }
395 case SUSPENSE_TREE_OPERATION_RESIZE: {
396 const id = operations[i + 1] as any as number;
397 const numRects = operations[i + 2] as any as number;
398 i += 3;
399
400 if (numRects === -1) {
401 logs.push(`Resize suspense node ${id} to null`);
402 } else {
403 let line = `Resize suspense node ${id} to [`;
404 for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
405 const x = operations[i + 0];
406 const y = operations[i + 1];
407 const width = operations[i + 2];
408 const height = operations[i + 3];
409
410 if (rectIndex > 0) {
411 line += ', ';
412 }
413 line += `(${x}, ${y}, ${width}, ${height})`;
414
415 i += 4;
416 }
417 logs.push(line + ']');
418 }
419
420 break;
421 }
422 case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
423 i++;
424 const changeLength = operations[i++] as any as number;
425
426 for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
427 const id = operations[i++];
428 const hasUniqueSuspenders = operations[i++] === 1;
429 const endTime = operations[i++] / 1000;
430 const isSuspended = operations[i++] === 1;
431 const environmentNamesLength = operations[i++];
432 i += environmentNamesLength;
433 logs.push(
434 `Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} ending at ${String(endTime)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`,
435 );
436 }
437
438 break;
439 }
440 case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: {
441 i++;
442 const activitySliceIDChange = operations[i++];
443 logs.push(
444 activitySliceIDChange === 0
445 ? 'Reset applied activity slice'
446 : 'Applied activity slice change to ' + activitySliceIDChange,
447 );
448 break;
449 }
450 default:
451 throw Error(`Unsupported Bridge operation "${operation}"`);
452 }
453 }
454
455 console.log(logs.join('\n '));
456 }
457
458 export function getDefaultComponentFilters(): Array<ComponentFilter> {
459 return [
460 {
461 type: ComponentFilterElementType,
462 value: ElementTypeHostComponent,
463 isEnabled: true,
464 },
465 ];
466 }
467
468 export function getSavedComponentFilters(): Array<ComponentFilter> {
469 try {
470 const raw = localStorageGetItem(
471 LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY,
472 );
473 if (raw != null) {
474 const parsedFilters: Array<ComponentFilter> = JSON.parse(raw);
475 return persistableComponentFilters(parsedFilters);
476 }
477 } catch (error) {}
478 return getDefaultComponentFilters();
479 }
480
481 export function setSavedComponentFilters(
482 componentFilters: Array<ComponentFilter>,
483 ): void {
484 localStorageSetItem(
485 LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY,
486 JSON.stringify(persistableComponentFilters(componentFilters)),
487 );
488 }
489
490 export function persistableComponentFilters(
491 componentFilters: Array<ComponentFilter>,
492 ): Array<ComponentFilter> {
493 // This is just an additional check to preserve the previous state
494 // Filters can be stored on the backend side or in user land (in a window object)
495 if (!Array.isArray(componentFilters)) {
496 return componentFilters;
497 }
498
499 return componentFilters.filter(f => {
500 return (
501 // Following __debugSource removal from Fiber, the new approach for finding the source location
502 // of a component, represented by the Fiber, is based on lazily generating and parsing component stack frames
503 // To find the original location, React DevTools will perform symbolication, source maps are required for that.
504 // In order to start filtering Fibers, we need to find location for all of them, which can't be done lazily.
505 // Eager symbolication can become quite expensive for large applications.
506 f.type !== ComponentFilterLocation &&
507 // Activity slice filters are based on DevTools instance IDs which do not persist across sessions.
508 f.type !== ComponentFilterActivitySlice
509 );
510 });
511 }
512
513 const vscodeFilepath = 'vscode://file/{path}:{line}:{column}';
514
515 export function getDefaultPreset(): 'custom' | 'vscode' {
516 return typeof process.env.EDITOR_URL === 'string' ? 'custom' : 'vscode';
517 }
518
519 export function getDefaultOpenInEditorURL(): string {
520 return typeof process.env.EDITOR_URL === 'string'
521 ? process.env.EDITOR_URL
522 : vscodeFilepath;
523 }
524
525 export function getOpenInEditorURL(): string {
526 try {
527 const rawPreset = localStorageGetItem(
528 LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
529 );
530 switch (rawPreset) {
531 case '"vscode"':
532 return vscodeFilepath;
533 }
534 const raw = localStorageGetItem(LOCAL_STORAGE_OPEN_IN_EDITOR_URL);
535 if (raw != null) {
536 return JSON.parse(raw);
537 }
538 } catch (error) {}
539 return getDefaultOpenInEditorURL();
540 }
541
542 export function getAlwaysOpenInEditor(): boolean {
543 try {
544 const raw = localStorageGetItem(LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR);
545 return raw === 'true';
546 } catch (error) {}
547 return false;
548 }
549
550 type ParseElementDisplayNameFromBackendReturn = {
551 formattedDisplayName: string | null,
552 hocDisplayNames: Array<string> | null,
553 compiledWithForget: boolean,
554 };
555 export function parseElementDisplayNameFromBackend(
556 displayName: string | null,
557 type: ElementType,
558 ): ParseElementDisplayNameFromBackendReturn {
559 if (displayName === null) {
560 return {
561 formattedDisplayName: null,
562 hocDisplayNames: null,
563 compiledWithForget: false,
564 };
565 }
566
567 if (displayName.startsWith('Forget(')) {
568 const displayNameWithoutForgetWrapper = displayName.slice(
569 7,
570 displayName.length - 1,
571 );
572
573 const {formattedDisplayName, hocDisplayNames} =
574 parseElementDisplayNameFromBackend(displayNameWithoutForgetWrapper, type);
575 return {formattedDisplayName, hocDisplayNames, compiledWithForget: true};
576 }
577
578 let hocDisplayNames = null;
579 switch (type) {
580 case ElementTypeClass:
581 case ElementTypeForwardRef:
582 case ElementTypeFunction:
583 case ElementTypeMemo:
584 case ElementTypeVirtual:
585 if (displayName.indexOf('(') >= 0) {
586 const matches = displayName.match(/[^()]+/g);
587 if (matches != null) {
588 // $FlowFixMe[incompatible-type]
589 displayName = matches.pop();
590 hocDisplayNames = matches;
591 }
592 }
593 break;
594 default:
595 break;
596 }
597
598 return {
599 // $FlowFixMe[incompatible-type]
600 formattedDisplayName: displayName,
601 hocDisplayNames,
602 compiledWithForget: false,
603 };
604 }
605
606 // Pulled from react-compat
607 // https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349
608 export function shallowDiffers(prev: Object, next: Object): boolean {
609 for (const attribute in prev) {
610 if (!(attribute in next)) {
611 return true;
612 }
613 }
614 for (const attribute in next) {
615 if (prev[attribute] !== next[attribute]) {
616 return true;
617 }
618 }
619 return false;
620 }
621
622 export function getInObject(object: Object, path: Array<string | number>): any {
623 return path.reduce((reduced: Object, attr: any): any => {
624 if (reduced) {
625 if (hasOwnProperty.call(reduced, attr)) {
626 return reduced[attr];
627 }
628 if (typeof reduced[Symbol.iterator] === 'function') {
629 // Convert iterable to array and return array[index]
630 //
631 // TRICKY
632 // Don't use [...spread] syntax for this purpose.
633 // This project uses @babel/plugin-transform-spread in "loose" mode which only works with Array values.
634 // Other types (e.g. typed arrays, Sets) will not spread correctly.
635 return Array.from(reduced)[attr];
636 }
637 }
638
639 return null;
640 }, object);
641 }
642
643 export function deletePathInObject(
644 object: Object,
645 path: Array<string | number>,
646 ) {
647 const length = path.length;
648 const last = path[length - 1];
649 if (object != null) {
650 const parent = getInObject(object, path.slice(0, length - 1));
651 if (parent) {
652 if (isArray(parent)) {
653 parent.splice(last as any as number, 1);
654 } else {
655 delete parent[last];
656 }
657 }
658 }
659 }
660
661 export function renamePathInObject(
662 object: Object,
663 oldPath: Array<string | number>,
664 newPath: Array<string | number>,
665 ) {
666 const length = oldPath.length;
667 if (object != null) {
668 const parent = getInObject(object, oldPath.slice(0, length - 1));
669 if (parent) {
670 const lastOld = oldPath[length - 1];
671 const lastNew = newPath[length - 1];
672 parent[lastNew] = parent[lastOld];
673 if (isArray(parent)) {
674 parent.splice(lastOld as any as number, 1);
675 } else {
676 delete parent[lastOld];
677 }
678 }
679 }
680 }
681
682 export function setInObject(
683 object: Object,
684 path: Array<string | number>,
685 value: any,
686 ) {
687 const length = path.length;
688 const last = path[length - 1];
689 if (object != null) {
690 const parent = getInObject(object, path.slice(0, length - 1));
691 if (parent) {
692 parent[last] = value;
693 }
694 }
695 }
696
697 export type DataType =
698 | 'array'
699 | 'array_buffer'
700 | 'bigint'
701 | 'boolean'
702 | 'class_instance'
703 | 'data_view'
704 | 'date'
705 | 'error'
706 | 'function'
707 | 'html_all_collection'
708 | 'html_element'
709 | 'infinity'
710 | '-infinity'
711 | 'iterator'
712 | 'opaque_iterator'
713 | 'nan'
714 | 'null'
715 | 'number'
716 | 'thenable'
717 | 'object'
718 | 'react_element'
719 | 'react_lazy'
720 | 'regexp'
721 | 'string'
722 | 'symbol'
723 | 'typed_array'
724 | 'undefined'
725 | 'unknown';
726
727 function isError(data: Object): boolean {
728 // If it doesn't event look like an error, it won't be an actual error.
729 if ('name' in data && 'message' in data) {
730 while (data) {
731 // $FlowFixMe[method-unbinding]
732 if (Object.prototype.toString.call(data) === '[object Error]') {
733 return true;
734 }
735 data = Object.getPrototypeOf(data);
736 }
737 }
738
739 return false;
740 }
741
742 /**
743 * Get a enhanced/artificial type string based on the object instance
744 */
745 export function getDataType(data: Object): DataType {
746 if (data === null) {
747 return 'null';
748 } else if (data === undefined) {
749 return 'undefined';
750 }
751
752 if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
753 return 'html_element';
754 }
755
756 const type = typeof data;
757 switch (type) {
758 case 'bigint':
759 return 'bigint';
760 case 'boolean':
761 return 'boolean';
762 case 'function':
763 return 'function';
764 case 'number':
765 if (Number.isNaN(data)) {
766 return 'nan';
767 } else if (!Number.isFinite(data)) {
768 return data > 0 ? 'infinity' : '-infinity';
769 } else {
770 return 'number';
771 }
772 case 'object':
773 switch (data.$$typeof) {
774 case REACT_ELEMENT_TYPE:
775 case REACT_LEGACY_ELEMENT_TYPE:
776 return 'react_element';
777 case REACT_LAZY_TYPE:
778 return 'react_lazy';
779 }
780 if (isArray(data)) {
781 return 'array';
782 } else if (ArrayBuffer.isView(data)) {
783 return hasOwnProperty.call(data.constructor, 'BYTES_PER_ELEMENT')
784 ? 'typed_array'
785 : 'data_view';
786 } else if (data.constructor && data.constructor.name === 'ArrayBuffer') {
787 // HACK This ArrayBuffer check is gross; is there a better way?
788 // We could try to create a new DataView with the value.
789 // If it doesn't error, we know it's an ArrayBuffer,
790 // but this seems kind of awkward and expensive.
791 return 'array_buffer';
792 } else if (typeof data[Symbol.iterator] === 'function') {
793 const iterator = data[Symbol.iterator]();
794 if (!iterator) {
795 // Proxies might break assumptoins about iterators.
796 // See github.com/facebook/react/issues/21654
797 } else {
798 return iterator === data ? 'opaque_iterator' : 'iterator';
799 }
800 } else if (data.constructor && data.constructor.name === 'RegExp') {
801 return 'regexp';
802 } else if (typeof data.then === 'function') {
803 return 'thenable';
804 } else if (isError(data)) {
805 return 'error';
806 } else {
807 // $FlowFixMe[method-unbinding]
808 const toStringValue = Object.prototype.toString.call(data);
809 if (toStringValue === '[object Date]') {
810 return 'date';
811 } else if (toStringValue === '[object HTMLAllCollection]') {
812 return 'html_all_collection';
813 }
814 }
815
816 if (!isPlainObject(data)) {
817 return 'class_instance';
818 }
819
820 return 'object';
821 case 'string':
822 return 'string';
823 case 'symbol':
824 return 'symbol';
825 case 'undefined':
826 if (
827 // $FlowFixMe[method-unbinding]
828 Object.prototype.toString.call(data) === '[object HTMLAllCollection]'
829 ) {
830 return 'html_all_collection';
831 }
832 return 'undefined';
833 default:
834 return 'unknown';
835 }
836 }
837
838 // Fork of packages/react-is/src/ReactIs.js:30, but with legacy element type
839 // Which has been changed in https://github.com/facebook/react/pull/28813
840 function typeOfWithLegacyElementSymbol(object: any): mixed {
841 if (typeof object === 'object' && object !== null) {
842 const $$typeof = object.$$typeof;
843 switch ($$typeof) {
844 case REACT_ELEMENT_TYPE:
845 case REACT_LEGACY_ELEMENT_TYPE:
846 const type = object.type;
847
848 switch (type) {
849 case REACT_FRAGMENT_TYPE:
850 case REACT_PROFILER_TYPE:
851 case REACT_STRICT_MODE_TYPE:
852 case REACT_SUSPENSE_TYPE:
853 case REACT_SUSPENSE_LIST_TYPE:
854 case REACT_VIEW_TRANSITION_TYPE:
855 return type;
856 default:
857 const $$typeofType = type && type.$$typeof;
858
859 switch ($$typeofType) {
860 case REACT_CONTEXT_TYPE:
861 case REACT_FORWARD_REF_TYPE:
862 case REACT_LAZY_TYPE:
863 case REACT_MEMO_TYPE:
864 return $$typeofType;
865 case REACT_CONSUMER_TYPE:
866 return $$typeofType;
867 // Fall through
868 default:
869 return $$typeof;
870 }
871 }
872 case REACT_PORTAL_TYPE:
873 return $$typeof;
874 }
875 }
876
877 return undefined;
878 }
879
880 export function getDisplayNameForReactElement(
881 element: React$Element<any>,
882 ): string | null {
883 const elementType = typeOfWithLegacyElementSymbol(element);
884 switch (elementType) {
885 case REACT_CONSUMER_TYPE:
886 return 'ContextConsumer';
887 case LEGACY_REACT_PROVIDER_TYPE:
888 return 'ContextProvider';
889 case REACT_CONTEXT_TYPE:
890 return 'Context';
891 case REACT_FORWARD_REF_TYPE:
892 return 'ForwardRef';
893 case REACT_FRAGMENT_TYPE:
894 return 'Fragment';
895 case REACT_LAZY_TYPE:
896 return 'Lazy';
897 case REACT_MEMO_TYPE:
898 return 'Memo';
899 case REACT_PORTAL_TYPE:
900 return 'Portal';
901 case REACT_PROFILER_TYPE:
902 return 'Profiler';
903 case REACT_STRICT_MODE_TYPE:
904 return 'StrictMode';
905 case REACT_SUSPENSE_TYPE:
906 return 'Suspense';
907 case REACT_SUSPENSE_LIST_TYPE:
908 return 'SuspenseList';
909 case REACT_VIEW_TRANSITION_TYPE:
910 return 'ViewTransition';
911 case REACT_TRACING_MARKER_TYPE:
912 return 'TracingMarker';
913 default:
914 const {type} = element;
915 if (typeof type === 'string') {
916 return type;
917 } else if (typeof type === 'function') {
918 return getDisplayName(type, 'Anonymous');
919 } else if (type != null) {
920 return 'NotImplementedInDevtools';
921 } else {
922 return 'Element';
923 }
924 }
925 }
926
927 const MAX_PREVIEW_STRING_LENGTH = 50;
928
929 function truncateForDisplay(
930 string: string,
931 length: number = MAX_PREVIEW_STRING_LENGTH,
932 ) {
933 if (string.length > length) {
934 return string.slice(0, length) + '';
935 } else {
936 return string;
937 }
938 }
939
940 // Attempts to mimic Chrome's inline preview for values.
941 // For example, the following value...
942 // {
943 // foo: 123,
944 // bar: "abc",
945 // baz: [true, false],
946 // qux: { ab: 1, cd: 2 }
947 // };
948 //
949 // Would show a preview of...
950 // {foo: 123, bar: "abc", baz: Array(2), qux: {…}}
951 //
952 // And the following value...
953 // [
954 // 123,
955 // "abc",
956 // [true, false],
957 // { foo: 123, bar: "abc" }
958 // ];
959 //
960 // Would show a preview of...
961 // [123, "abc", Array(2), {…}]
962 export function formatDataForPreview(
963 data: any,
964 showFormattedValue: boolean,
965 ): string {
966 if (data != null && hasOwnProperty.call(data, meta.type)) {
967 return showFormattedValue
968 ? data[meta.preview_long]
969 : data[meta.preview_short];
970 }
971
972 const type = getDataType(data);
973
974 switch (type) {
975 case 'html_element':
976 return `<${truncateForDisplay(data.tagName.toLowerCase())} />`;
977 case 'function':
978 // $FlowFixMe[invalid-compare]
979 if (typeof data.name === 'function' || data.name === '') {
980 return '() => {}';
981 }
982 return `${truncateForDisplay(data.name)}() {}`;
983 case 'string':
984 return `"${data}"`;
985 case 'bigint':
986 return truncateForDisplay(data.toString() + 'n');
987 case 'regexp':
988 return truncateForDisplay(data.toString());
989 case 'symbol':
990 return truncateForDisplay(data.toString());
991 case 'react_element':
992 return `<${truncateForDisplay(
993 getDisplayNameForReactElement(data) || 'Unknown',
994 )} />`;
995 case 'react_lazy':
996 // To avoid actually initialize a lazy to cause a side-effect we make some assumptions
997 // about the structure of the payload even though that's not really part of the contract.
998 // In practice, this is really just coming from React.lazy helper or Flight.
999 const payload = data._payload;
1000 if (payload !== null && typeof payload === 'object') {
1001 if (payload._status === 0) {
1002 // React.lazy constructor pending
1003 return `pending lazy()`;
1004 }
1005 if (payload._status === 1 && payload._result != null) {
1006 // React.lazy constructor fulfilled
1007 if (showFormattedValue) {
1008 const formatted = formatDataForPreview(
1009 payload._result.default,
1010 false,
1011 );
1012 return `fulfilled lazy() {${truncateForDisplay(formatted)}}`;
1013 } else {
1014 return `fulfilled lazy() {…}`;
1015 }
1016 }
1017 if (payload._status === 2) {
1018 // React.lazy constructor rejected
1019 if (showFormattedValue) {
1020 const formatted = formatDataForPreview(payload._result, false);
1021 return `rejected lazy() {${truncateForDisplay(formatted)}}`;
1022 } else {
1023 return `rejected lazy() {…}`;
1024 }
1025 }
1026 if (payload.status === 'pending' || payload.status === 'blocked') {
1027 // React Flight pending
1028 return `pending lazy()`;
1029 }
1030 if (payload.status === 'fulfilled') {
1031 // React Flight fulfilled
1032 if (showFormattedValue) {
1033 const formatted = formatDataForPreview(payload.value, false);
1034 return `fulfilled lazy() {${truncateForDisplay(formatted)}}`;
1035 } else {
1036 return `fulfilled lazy() {…}`;
1037 }
1038 }
1039 if (payload.status === 'rejected') {
1040 // React Flight rejected
1041 if (showFormattedValue) {
1042 const formatted = formatDataForPreview(payload.reason, false);
1043 return `rejected lazy() {${truncateForDisplay(formatted)}}`;
1044 } else {
1045 return `rejected lazy() {…}`;
1046 }
1047 }
1048 }
1049 // Some form of uninitialized
1050 return 'lazy()';
1051 case 'array_buffer':
1052 return `ArrayBuffer(${data.byteLength})`;
1053 case 'data_view':
1054 return `DataView(${data.buffer.byteLength})`;
1055 case 'array':
1056 if (showFormattedValue) {
1057 let formatted = '';
1058 for (let i = 0; i < data.length; i++) {
1059 if (i > 0) {
1060 formatted += ', ';
1061 }
1062 formatted += formatDataForPreview(data[i], false);
1063 if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
1064 // Prevent doing a lot of unnecessary iteration...
1065 break;
1066 }
1067 }
1068 return `[${truncateForDisplay(formatted)}]`;
1069 } else {
1070 const length = hasOwnProperty.call(data, meta.size)
1071 ? data[meta.size]
1072 : data.length;
1073 return `Array(${length})`;
1074 }
1075 case 'typed_array':
1076 const shortName = `${data.constructor.name}(${data.length})`;
1077 if (showFormattedValue) {
1078 let formatted = '';
1079 for (let i = 0; i < data.length; i++) {
1080 if (i > 0) {
1081 formatted += ', ';
1082 }
1083 formatted += data[i];
1084 if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
1085 // Prevent doing a lot of unnecessary iteration...
1086 break;
1087 }
1088 }
1089 return `${shortName} [${truncateForDisplay(formatted)}]`;
1090 } else {
1091 return shortName;
1092 }
1093 case 'iterator':
1094 const name = data.constructor.name;
1095
1096 if (showFormattedValue) {
1097 // TRICKY
1098 // Don't use [...spread] syntax for this purpose.
1099 // This project uses @babel/plugin-transform-spread in "loose" mode which only works with Array values.
1100 // Other types (e.g. typed arrays, Sets) will not spread correctly.
1101 const array = Array.from(data);
1102
1103 let formatted = '';
1104 for (let i = 0; i < array.length; i++) {
1105 const entryOrEntries = array[i];
1106
1107 if (i > 0) {
1108 formatted += ', ';
1109 }
1110
1111 // TRICKY
1112 // Browsers display Maps and Sets differently.
1113 // To mimic their behavior, detect if we've been given an entries tuple.
1114 // Map(2) {"abc" => 123, "def" => 123}
1115 // Set(2) {"abc", 123}
1116 if (isArray(entryOrEntries)) {
1117 const key = formatDataForPreview(entryOrEntries[0], true);
1118 const value = formatDataForPreview(entryOrEntries[1], false);
1119 formatted += `${key} => ${value}`;
1120 } else {
1121 formatted += formatDataForPreview(entryOrEntries, false);
1122 }
1123
1124 if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
1125 // Prevent doing a lot of unnecessary iteration...
1126 break;
1127 }
1128 }
1129
1130 return `${name}(${data.size}) {${truncateForDisplay(formatted)}}`;
1131 } else {
1132 return `${name}(${data.size})`;
1133 }
1134 case 'opaque_iterator': {
1135 return data[Symbol.toStringTag];
1136 }
1137 case 'date':
1138 return data.toString();
1139 case 'class_instance':
1140 try {
1141 let resolvedConstructorName = data.constructor.name;
1142 if (typeof resolvedConstructorName === 'string') {
1143 return resolvedConstructorName;
1144 }
1145
1146 resolvedConstructorName = Object.getPrototypeOf(data).constructor.name;
1147 if (typeof resolvedConstructorName === 'string') {
1148 return resolvedConstructorName;
1149 }
1150
1151 try {
1152 return truncateForDisplay(String(data));
1153 } catch (error) {
1154 return 'unserializable';
1155 }
1156 } catch (error) {
1157 return 'unserializable';
1158 }
1159 case 'thenable':
1160 let displayName: string;
1161 if (isPlainObject(data)) {
1162 displayName = 'Thenable';
1163 } else {
1164 let resolvedConstructorName = data.constructor.name;
1165 if (typeof resolvedConstructorName !== 'string') {
1166 resolvedConstructorName =
1167 Object.getPrototypeOf(data).constructor.name;
1168 }
1169 if (typeof resolvedConstructorName === 'string') {
1170 displayName = resolvedConstructorName;
1171 } else {
1172 displayName = 'Thenable';
1173 }
1174 }
1175 switch (data.status) {
1176 case 'pending':
1177 return `pending ${displayName}`;
1178 case 'fulfilled':
1179 if (showFormattedValue) {
1180 const formatted = formatDataForPreview(data.value, false);
1181 return `fulfilled ${displayName} {${truncateForDisplay(formatted)}}`;
1182 } else {
1183 return `fulfilled ${displayName} {…}`;
1184 }
1185 case 'rejected':
1186 if (showFormattedValue) {
1187 const formatted = formatDataForPreview(data.reason, false);
1188 return `rejected ${displayName} {${truncateForDisplay(formatted)}}`;
1189 } else {
1190 return `rejected ${displayName} {…}`;
1191 }
1192 default:
1193 return displayName;
1194 }
1195 case 'object':
1196 if (showFormattedValue) {
1197 const keys = Array.from(getAllEnumerableKeys(data)).sort(alphaSortKeys);
1198
1199 let formatted = '';
1200 for (let i = 0; i < keys.length; i++) {
1201 const key = keys[i];
1202 if (i > 0) {
1203 formatted += ', ';
1204 }
1205 formatted += `${key.toString()}: ${formatDataForPreview(
1206 data[key],
1207 false,
1208 )}`;
1209 if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
1210 // Prevent doing a lot of unnecessary iteration...
1211 break;
1212 }
1213 }
1214 return `{${truncateForDisplay(formatted)}}`;
1215 } else {
1216 return '{…}';
1217 }
1218 case 'error':
1219 return truncateForDisplay(String(data));
1220 case 'boolean':
1221 case 'number':
1222 case 'infinity':
1223 case '-infinity':
1224 case 'nan':
1225 case 'null':
1226 case 'undefined':
1227 return String(data);
1228 default:
1229 try {
1230 return truncateForDisplay(String(data));
1231 } catch (error) {
1232 return 'unserializable';
1233 }
1234 }
1235 }
1236
1237 // Basically checking that the object only has Object in its prototype chain
1238 export const isPlainObject = (object: Object): boolean => {
1239 const objectPrototype = Object.getPrototypeOf(object);
1240 if (!objectPrototype) return true;
1241
1242 const objectParentPrototype = Object.getPrototypeOf(objectPrototype);
1243 return !objectParentPrototype;
1244 };
1245
1246 export function backendToFrontendSerializedElementMapper(
1247 element: SerializedElementBackend,
1248 ): SerializedElementFrontend {
1249 const {formattedDisplayName, hocDisplayNames, compiledWithForget} =
1250 parseElementDisplayNameFromBackend(element.displayName, element.type);
1251
1252 return {
1253 ...element,
1254 displayName: formattedDisplayName,
1255 hocDisplayNames,
1256 compiledWithForget,
1257 };
1258 }
1259
1260 /**
1261 * Should be used when treating url as a Chrome Resource URL.
1262 */
1263 export function normalizeUrlIfValid(url: string): string {
1264 try {
1265 // TODO: Chrome will use the basepath to create a Resource URL.
1266 return new URL(url).toString();
1267 } catch {
1268 // Giving up if it's not a valid URL without basepath
1269 return url;
1270 }
1271 }
1272
1273 export function getIsReloadAndProfileSupported(): boolean {
1274 // Notify the frontend if the backend supports the Storage API (e.g. localStorage).
1275 // If not, features like reload-and-profile will not work correctly and must be disabled.
1276 let isBackendStorageAPISupported = false;
1277 try {
1278 localStorage.getItem('test');
1279 isBackendStorageAPISupported = true;
1280 } catch (error) {}
1281
1282 return isBackendStorageAPISupported && isSynchronousXHRSupported();
1283 }
1284
1285 // Expected to be used only by browser extension and react-devtools-inline
1286 export function getIfReloadedAndProfiling(): boolean {
1287 return (
1288 sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
1289 );
1290 }
1291
1292 export function getProfilingSettings(): ProfilingSettings {
1293 return {
1294 recordChangeDescriptions:
1295 sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
1296 'true',
1297 };
1298 }
1299
1300 export function onReloadAndProfile(recordChangeDescriptions: boolean): void {
1301 sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
1302 sessionStorageSetItem(
1303 SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
1304 recordChangeDescriptions ? 'true' : 'false',
1305 );
1306 }
1307
1308 export function onReloadAndProfileFlagsReset(): void {
1309 sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY);
1310 sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY);
1311 }
1312
1313 export function unionOfTwoArrays<T>(a: Array<T>, b: Array<T>): Array<T> {
1314 let result = a;
1315 for (let i = 0; i < b.length; i++) {
1316 const value = b[i];
1317 if (a.indexOf(value) === -1) {
1318 if (result === a) {
1319 // Lazily copy
1320 result = a.slice(0);
1321 }
1322 result.push(value);
1323 }
1324 }
1325 return result;
1326 }