/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import LRU from 'lru-cache'; import { REACT_CONSUMER_TYPE, REACT_CONTEXT_TYPE, REACT_FORWARD_REF_TYPE, REACT_FRAGMENT_TYPE, REACT_LAZY_TYPE, REACT_ELEMENT_TYPE, REACT_LEGACY_ELEMENT_TYPE, REACT_MEMO_TYPE, REACT_PORTAL_TYPE, REACT_PROFILER_TYPE, REACT_STRICT_MODE_TYPE, REACT_SUSPENSE_LIST_TYPE, REACT_SUSPENSE_TYPE, REACT_TRACING_MARKER_TYPE, REACT_VIEW_TRANSITION_TYPE, } from 'shared/ReactSymbols'; import { TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REMOVE_ROOT, TREE_OPERATION_REORDER_CHILDREN, TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY, LOCAL_STORAGE_OPEN_IN_EDITOR_URL, LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR, SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, SESSION_STORAGE_RECORD_TIMELINE_KEY, SUSPENSE_TREE_OPERATION_ADD, SUSPENSE_TREE_OPERATION_REMOVE, SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, SUSPENSE_TREE_OPERATION_RESIZE, SUSPENSE_TREE_OPERATION_SUSPENDERS, } from './constants'; import { ComponentFilterElementType, ComponentFilterLocation, ElementTypeHostComponent, } from './frontend/types'; import { ElementTypeRoot, ElementTypeClass, ElementTypeForwardRef, ElementTypeFunction, ElementTypeMemo, ElementTypeVirtual, } from 'react-devtools-shared/src/frontend/types'; import { localStorageGetItem, localStorageSetItem, sessionStorageGetItem, sessionStorageRemoveItem, sessionStorageSetItem, } from 'react-devtools-shared/src/storage'; import {meta} from './hydration'; import isArray from './isArray'; import type { ComponentFilter, ElementType, SerializedElement as SerializedElementFrontend, LRUCache, } from 'react-devtools-shared/src/frontend/types'; import type { ProfilingSettings, SerializedElement as SerializedElementBackend, } from 'react-devtools-shared/src/backend/types'; import {isSynchronousXHRSupported} from './backend/utils'; // $FlowFixMe[method-unbinding] const hasOwnProperty = Object.prototype.hasOwnProperty; const cachedDisplayNames: WeakMap = new WeakMap(); // On large trees, encoding takes significant time. // Try to reuse the already encoded strings. const encodedStringCache: LRUCache> = new LRU({ max: 1000, }); // Previously, the type of `Context.Provider`. const LEGACY_REACT_PROVIDER_TYPE: symbol = Symbol.for('react.provider'); export function alphaSortKeys( a: string | number | symbol, b: string | number | symbol, ): number { if (a.toString() > b.toString()) { return 1; } else if (b.toString() > a.toString()) { return -1; } else { return 0; } } export function getAllEnumerableKeys( obj: Object, ): Set { const keys = new Set(); let current = obj; while (current != null) { const currentKeys = [ ...Object.keys(current), ...Object.getOwnPropertySymbols(current), ]; const descriptors = Object.getOwnPropertyDescriptors(current); currentKeys.forEach(key => { // $FlowFixMe[incompatible-type]: key can be a Symbol https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor if (descriptors[key].enumerable) { keys.add(key); } }); current = Object.getPrototypeOf(current); } return keys; } // Mirror https://github.com/facebook/react/blob/7c21bf72ace77094fd1910cc350a548287ef8350/packages/shared/getComponentName.js#L27-L37 export function getWrappedDisplayName( outerType: mixed, innerType: any, wrapperName: string, fallbackName?: string, ): string { const displayName = (outerType: any)?.displayName; return ( displayName || `${wrapperName}(${getDisplayName(innerType, fallbackName)})` ); } export function getDisplayName( type: Function, fallbackName: string = 'Anonymous', ): string { const nameFromCache = cachedDisplayNames.get(type); if (nameFromCache != null) { return nameFromCache; } let displayName = fallbackName; // The displayName property is not guaranteed to be a string. // It's only safe to use for our purposes if it's a string. // github.com/facebook/react-devtools/issues/803 if (typeof type.displayName === 'string') { displayName = type.displayName; } else if (typeof type.name === 'string' && type.name !== '') { displayName = type.name; } cachedDisplayNames.set(type, displayName); return displayName; } let uidCounter: number = 0; export function getUID(): number { return ++uidCounter; } export function utfDecodeStringWithRanges( array: Array, left: number, right: number, ): string { let string = ''; for (let i = left; i <= right; i++) { string += String.fromCodePoint(array[i]); } return string; } function surrogatePairToCodePoint( charCode1: number, charCode2: number, ): number { return ((charCode1 & 0x3ff) << 10) + (charCode2 & 0x3ff) + 0x10000; } // Credit for this encoding approach goes to Tim Down: // https://stackoverflow.com/questions/4877326/how-can-i-tell-if-a-string-contains-multibyte-characters-in-javascript export function utfEncodeString(string: string): Array { const cached = encodedStringCache.get(string); if (cached !== undefined) { return cached; } const encoded = []; let i = 0; let charCode; while (i < string.length) { charCode = string.charCodeAt(i); // Handle multibyte unicode characters (like emoji). if ((charCode & 0xf800) === 0xd800) { encoded.push(surrogatePairToCodePoint(charCode, string.charCodeAt(++i))); } else { encoded.push(charCode); } ++i; } encodedStringCache.set(string, encoded); return encoded; } export function printOperationsArray(operations: Array) { // The first two values are always rendererID and rootID const rendererID = operations[0]; const rootID = operations[1]; const logs = [`operations for renderer:${rendererID} and root:${rootID}`]; let i = 2; // Reassemble the string table. const stringTable: Array = [ null, // ID = 0 corresponds to the null string. ]; const stringTableSize = operations[i++]; const stringTableEnd = i + stringTableSize; while (i < stringTableEnd) { const nextLength = operations[i++]; const nextString = utfDecodeStringWithRanges( operations, i, i + nextLength - 1, ); stringTable.push(nextString); i += nextLength; } while (i < operations.length) { const operation = operations[i]; switch (operation) { case TREE_OPERATION_ADD: { const id = ((operations[i + 1]: any): number); const type = ((operations[i + 2]: any): ElementType); i += 3; if (type === ElementTypeRoot) { logs.push(`Add new root node ${id}`); i++; // isStrictModeCompliant i++; // supportsProfiling i++; // supportsStrictMode i++; // hasOwnerMetadata } else { const parentID = ((operations[i]: any): number); i++; i++; // ownerID const displayNameStringID = operations[i]; const displayName = stringTable[displayNameStringID]; i++; i++; // key i++; // name logs.push( `Add node ${id} (${displayName || 'null'}) as child of ${parentID}`, ); } break; } case TREE_OPERATION_REMOVE: { const removeLength = ((operations[i + 1]: any): number); i += 2; for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) { const id = ((operations[i]: any): number); i += 1; logs.push(`Remove node ${id}`); } break; } case TREE_OPERATION_REMOVE_ROOT: { i += 1; logs.push(`Remove root ${rootID}`); break; } case TREE_OPERATION_SET_SUBTREE_MODE: { const id = operations[i + 1]; const mode = operations[i + 2]; i += 3; logs.push(`Mode ${mode} set for subtree with root ${id}`); break; } case TREE_OPERATION_REORDER_CHILDREN: { const id = ((operations[i + 1]: any): number); const numChildren = ((operations[i + 2]: any): number); i += 3; const children = operations.slice(i, i + numChildren); i += numChildren; logs.push(`Re-order node ${id} children ${children.join(',')}`); break; } case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: // Base duration updates are only sent while profiling is in progress. // We can ignore them at this point. // The profiler UI uses them lazily in order to generate the tree. i += 3; break; case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: { const id = operations[i + 1]; const numErrors = operations[i + 2]; const numWarnings = operations[i + 3]; i += 4; logs.push( `Node ${id} has ${numErrors} errors and ${numWarnings} warnings`, ); break; } case SUSPENSE_TREE_OPERATION_ADD: { const fiberID = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; const numRects = operations[i + 4]; i += 5; const name = stringTable[nameStringID]; let rects: string; if (numRects === -1) { rects = 'null'; } else { rects = '['; for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { const offset = i + rectIndex * 4; const x = operations[offset + 0]; const y = operations[offset + 1]; const width = operations[offset + 2]; const height = operations[offset + 3]; if (rectIndex > 0) { rects += ', '; } rects += `(${x}, ${y}, ${width}, ${height})`; i += 4; } rects += ']'; } logs.push( `Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID}`, ); break; } case SUSPENSE_TREE_OPERATION_REMOVE: { const removeLength = ((operations[i + 1]: any): number); i += 2; for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) { const id = ((operations[i]: any): number); i += 1; logs.push(`Remove suspense node ${id}`); } break; } case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: { const id = ((operations[i + 1]: any): number); const numChildren = ((operations[i + 2]: any): number); i += 3; const children = operations.slice(i, i + numChildren); i += numChildren; logs.push( `Re-order suspense node ${id} children ${children.join(',')}`, ); break; } case SUSPENSE_TREE_OPERATION_RESIZE: { const id = ((operations[i + 1]: any): number); const numRects = ((operations[i + 2]: any): number); i += 3; if (numRects === -1) { logs.push(`Resize suspense node ${id} to null`); } else { let line = `Resize suspense node ${id} to [`; for (let rectIndex = 0; rectIndex < numRects; rectIndex++) { const x = operations[i + 0]; const y = operations[i + 1]; const width = operations[i + 2]; const height = operations[i + 3]; if (rectIndex > 0) { line += ', '; } line += `(${x}, ${y}, ${width}, ${height})`; i += 4; } logs.push(line + ']'); } break; } case SUSPENSE_TREE_OPERATION_SUSPENDERS: { i++; const changeLength = ((operations[i++]: any): number); for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) { const id = operations[i++]; const hasUniqueSuspenders = operations[i++] === 1; const environmentNamesLength = operations[i++]; i += environmentNamesLength; logs.push( `Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} with ${String(environmentNamesLength)} environments`, ); } break; } default: throw Error(`Unsupported Bridge operation "${operation}"`); } } console.log(logs.join('\n ')); } export function getDefaultComponentFilters(): Array { return [ { type: ComponentFilterElementType, value: ElementTypeHostComponent, isEnabled: true, }, ]; } export function getSavedComponentFilters(): Array { try { const raw = localStorageGetItem( LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY, ); if (raw != null) { const parsedFilters: Array = JSON.parse(raw); return filterOutLocationComponentFilters(parsedFilters); } } catch (error) {} return getDefaultComponentFilters(); } export function setSavedComponentFilters( componentFilters: Array, ): void { localStorageSetItem( LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY, JSON.stringify(filterOutLocationComponentFilters(componentFilters)), ); } // Following __debugSource removal from Fiber, the new approach for finding the source location // of a component, represented by the Fiber, is based on lazily generating and parsing component stack frames // To find the original location, React DevTools will perform symbolication, source maps are required for that. // In order to start filtering Fibers, we need to find location for all of them, which can't be done lazily. // Eager symbolication can become quite expensive for large applications. export function filterOutLocationComponentFilters( componentFilters: Array, ): Array { // This is just an additional check to preserve the previous state // Filters can be stored on the backend side or in user land (in a window object) if (!Array.isArray(componentFilters)) { return componentFilters; } return componentFilters.filter(f => f.type !== ComponentFilterLocation); } const vscodeFilepath = 'vscode://file/{path}:{line}:{column}'; export function getDefaultPreset(): 'custom' | 'vscode' { return typeof process.env.EDITOR_URL === 'string' ? 'custom' : 'vscode'; } export function getDefaultOpenInEditorURL(): string { return typeof process.env.EDITOR_URL === 'string' ? process.env.EDITOR_URL : vscodeFilepath; } export function getOpenInEditorURL(): string { try { const rawPreset = localStorageGetItem( LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, ); switch (rawPreset) { case '"vscode"': return vscodeFilepath; } const raw = localStorageGetItem(LOCAL_STORAGE_OPEN_IN_EDITOR_URL); if (raw != null) { return JSON.parse(raw); } } catch (error) {} return getDefaultOpenInEditorURL(); } export function getAlwaysOpenInEditor(): boolean { try { const raw = localStorageGetItem(LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR); return raw === 'true'; } catch (error) {} return false; } type ParseElementDisplayNameFromBackendReturn = { formattedDisplayName: string | null, hocDisplayNames: Array | null, compiledWithForget: boolean, }; export function parseElementDisplayNameFromBackend( displayName: string | null, type: ElementType, ): ParseElementDisplayNameFromBackendReturn { if (displayName === null) { return { formattedDisplayName: null, hocDisplayNames: null, compiledWithForget: false, }; } if (displayName.startsWith('Forget(')) { const displayNameWithoutForgetWrapper = displayName.slice( 7, displayName.length - 1, ); const {formattedDisplayName, hocDisplayNames} = parseElementDisplayNameFromBackend(displayNameWithoutForgetWrapper, type); return {formattedDisplayName, hocDisplayNames, compiledWithForget: true}; } let hocDisplayNames = null; switch (type) { case ElementTypeClass: case ElementTypeForwardRef: case ElementTypeFunction: case ElementTypeMemo: case ElementTypeVirtual: if (displayName.indexOf('(') >= 0) { const matches = displayName.match(/[^()]+/g); if (matches != null) { // $FlowFixMe[incompatible-type] displayName = matches.pop(); hocDisplayNames = matches; } } break; default: break; } return { // $FlowFixMe[incompatible-return] formattedDisplayName: displayName, hocDisplayNames, compiledWithForget: false, }; } // Pulled from react-compat // https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349 export function shallowDiffers(prev: Object, next: Object): boolean { for (const attribute in prev) { if (!(attribute in next)) { return true; } } for (const attribute in next) { if (prev[attribute] !== next[attribute]) { return true; } } return false; } export function getInObject(object: Object, path: Array): any { return path.reduce((reduced: Object, attr: any): any => { if (reduced) { if (hasOwnProperty.call(reduced, attr)) { return reduced[attr]; } if (typeof reduced[Symbol.iterator] === 'function') { // Convert iterable to array and return array[index] // // TRICKY // Don't use [...spread] syntax for this purpose. // This project uses @babel/plugin-transform-spread in "loose" mode which only works with Array values. // Other types (e.g. typed arrays, Sets) will not spread correctly. return Array.from(reduced)[attr]; } } return null; }, object); } export function deletePathInObject( object: Object, path: Array, ) { const length = path.length; const last = path[length - 1]; if (object != null) { const parent = getInObject(object, path.slice(0, length - 1)); if (parent) { if (isArray(parent)) { parent.splice(((last: any): number), 1); } else { delete parent[last]; } } } } export function renamePathInObject( object: Object, oldPath: Array, newPath: Array, ) { const length = oldPath.length; if (object != null) { const parent = getInObject(object, oldPath.slice(0, length - 1)); if (parent) { const lastOld = oldPath[length - 1]; const lastNew = newPath[length - 1]; parent[lastNew] = parent[lastOld]; if (isArray(parent)) { parent.splice(((lastOld: any): number), 1); } else { delete parent[lastOld]; } } } } export function setInObject( object: Object, path: Array, value: any, ) { const length = path.length; const last = path[length - 1]; if (object != null) { const parent = getInObject(object, path.slice(0, length - 1)); if (parent) { parent[last] = value; } } } export type DataType = | 'array' | 'array_buffer' | 'bigint' | 'boolean' | 'class_instance' | 'data_view' | 'date' | 'error' | 'function' | 'html_all_collection' | 'html_element' | 'infinity' | 'iterator' | 'opaque_iterator' | 'nan' | 'null' | 'number' | 'thenable' | 'object' | 'react_element' | 'react_lazy' | 'regexp' | 'string' | 'symbol' | 'typed_array' | 'undefined' | 'unknown'; function isError(data: Object): boolean { // If it doesn't event look like an error, it won't be an actual error. if ('name' in data && 'message' in data) { while (data) { // $FlowFixMe[method-unbinding] if (Object.prototype.toString.call(data) === '[object Error]') { return true; } data = Object.getPrototypeOf(data); } } return false; } /** * Get a enhanced/artificial type string based on the object instance */ export function getDataType(data: Object): DataType { if (data === null) { return 'null'; } else if (data === undefined) { return 'undefined'; } if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) { return 'html_element'; } const type = typeof data; switch (type) { case 'bigint': return 'bigint'; case 'boolean': return 'boolean'; case 'function': return 'function'; case 'number': if (Number.isNaN(data)) { return 'nan'; } else if (!Number.isFinite(data)) { return 'infinity'; } else { return 'number'; } case 'object': switch (data.$$typeof) { case REACT_ELEMENT_TYPE: case REACT_LEGACY_ELEMENT_TYPE: return 'react_element'; case REACT_LAZY_TYPE: return 'react_lazy'; } if (isArray(data)) { return 'array'; } else if (ArrayBuffer.isView(data)) { return hasOwnProperty.call(data.constructor, 'BYTES_PER_ELEMENT') ? 'typed_array' : 'data_view'; } else if (data.constructor && data.constructor.name === 'ArrayBuffer') { // HACK This ArrayBuffer check is gross; is there a better way? // We could try to create a new DataView with the value. // If it doesn't error, we know it's an ArrayBuffer, // but this seems kind of awkward and expensive. return 'array_buffer'; } else if (typeof data[Symbol.iterator] === 'function') { const iterator = data[Symbol.iterator](); if (!iterator) { // Proxies might break assumptoins about iterators. // See github.com/facebook/react/issues/21654 } else { return iterator === data ? 'opaque_iterator' : 'iterator'; } } else if (data.constructor && data.constructor.name === 'RegExp') { return 'regexp'; } else if (typeof data.then === 'function') { return 'thenable'; } else if (isError(data)) { return 'error'; } else { // $FlowFixMe[method-unbinding] const toStringValue = Object.prototype.toString.call(data); if (toStringValue === '[object Date]') { return 'date'; } else if (toStringValue === '[object HTMLAllCollection]') { return 'html_all_collection'; } } if (!isPlainObject(data)) { return 'class_instance'; } return 'object'; case 'string': return 'string'; case 'symbol': return 'symbol'; case 'undefined': if ( // $FlowFixMe[method-unbinding] Object.prototype.toString.call(data) === '[object HTMLAllCollection]' ) { return 'html_all_collection'; } return 'undefined'; default: return 'unknown'; } } // Fork of packages/react-is/src/ReactIs.js:30, but with legacy element type // Which has been changed in https://github.com/facebook/react/pull/28813 function typeOfWithLegacyElementSymbol(object: any): mixed { if (typeof object === 'object' && object !== null) { const $$typeof = object.$$typeof; switch ($$typeof) { case REACT_ELEMENT_TYPE: case REACT_LEGACY_ELEMENT_TYPE: const type = object.type; switch (type) { case REACT_FRAGMENT_TYPE: case REACT_PROFILER_TYPE: case REACT_STRICT_MODE_TYPE: case REACT_SUSPENSE_TYPE: case REACT_SUSPENSE_LIST_TYPE: case REACT_VIEW_TRANSITION_TYPE: return type; default: const $$typeofType = type && type.$$typeof; switch ($$typeofType) { case REACT_CONTEXT_TYPE: case REACT_FORWARD_REF_TYPE: case REACT_LAZY_TYPE: case REACT_MEMO_TYPE: return $$typeofType; case REACT_CONSUMER_TYPE: return $$typeofType; // Fall through default: return $$typeof; } } case REACT_PORTAL_TYPE: return $$typeof; } } return undefined; } export function getDisplayNameForReactElement( element: React$Element, ): string | null { const elementType = typeOfWithLegacyElementSymbol(element); switch (elementType) { case REACT_CONSUMER_TYPE: return 'ContextConsumer'; case LEGACY_REACT_PROVIDER_TYPE: return 'ContextProvider'; case REACT_CONTEXT_TYPE: return 'Context'; case REACT_FORWARD_REF_TYPE: return 'ForwardRef'; case REACT_FRAGMENT_TYPE: return 'Fragment'; case REACT_LAZY_TYPE: return 'Lazy'; case REACT_MEMO_TYPE: return 'Memo'; case REACT_PORTAL_TYPE: return 'Portal'; case REACT_PROFILER_TYPE: return 'Profiler'; case REACT_STRICT_MODE_TYPE: return 'StrictMode'; case REACT_SUSPENSE_TYPE: return 'Suspense'; case REACT_SUSPENSE_LIST_TYPE: return 'SuspenseList'; case REACT_VIEW_TRANSITION_TYPE: return 'ViewTransition'; case REACT_TRACING_MARKER_TYPE: return 'TracingMarker'; default: const {type} = element; if (typeof type === 'string') { return type; } else if (typeof type === 'function') { return getDisplayName(type, 'Anonymous'); } else if (type != null) { return 'NotImplementedInDevtools'; } else { return 'Element'; } } } const MAX_PREVIEW_STRING_LENGTH = 50; function truncateForDisplay( string: string, length: number = MAX_PREVIEW_STRING_LENGTH, ) { if (string.length > length) { return string.slice(0, length) + '…'; } else { return string; } } // Attempts to mimic Chrome's inline preview for values. // For example, the following value... // { // foo: 123, // bar: "abc", // baz: [true, false], // qux: { ab: 1, cd: 2 } // }; // // Would show a preview of... // {foo: 123, bar: "abc", baz: Array(2), qux: {…}} // // And the following value... // [ // 123, // "abc", // [true, false], // { foo: 123, bar: "abc" } // ]; // // Would show a preview of... // [123, "abc", Array(2), {…}] export function formatDataForPreview( data: any, showFormattedValue: boolean, ): string { if (data != null && hasOwnProperty.call(data, meta.type)) { return showFormattedValue ? data[meta.preview_long] : data[meta.preview_short]; } const type = getDataType(data); switch (type) { case 'html_element': return `<${truncateForDisplay(data.tagName.toLowerCase())} />`; case 'function': if (typeof data.name === 'function' || data.name === '') { return '() => {}'; } return `${truncateForDisplay(data.name)}() {}`; case 'string': return `"${data}"`; case 'bigint': return truncateForDisplay(data.toString() + 'n'); case 'regexp': return truncateForDisplay(data.toString()); case 'symbol': return truncateForDisplay(data.toString()); case 'react_element': return `<${truncateForDisplay( getDisplayNameForReactElement(data) || 'Unknown', )} />`; case 'react_lazy': // To avoid actually initialize a lazy to cause a side-effect we make some assumptions // about the structure of the payload even though that's not really part of the contract. // In practice, this is really just coming from React.lazy helper or Flight. const payload = data._payload; if (payload !== null && typeof payload === 'object') { if (payload._status === 0) { // React.lazy constructor pending return `pending lazy()`; } if (payload._status === 1 && payload._result != null) { // React.lazy constructor fulfilled if (showFormattedValue) { const formatted = formatDataForPreview( payload._result.default, false, ); return `fulfilled lazy() {${truncateForDisplay(formatted)}}`; } else { return `fulfilled lazy() {…}`; } } if (payload._status === 2) { // React.lazy constructor rejected if (showFormattedValue) { const formatted = formatDataForPreview(payload._result, false); return `rejected lazy() {${truncateForDisplay(formatted)}}`; } else { return `rejected lazy() {…}`; } } if (payload.status === 'pending' || payload.status === 'blocked') { // React Flight pending return `pending lazy()`; } if (payload.status === 'fulfilled') { // React Flight fulfilled if (showFormattedValue) { const formatted = formatDataForPreview(payload.value, false); return `fulfilled lazy() {${truncateForDisplay(formatted)}}`; } else { return `fulfilled lazy() {…}`; } } if (payload.status === 'rejected') { // React Flight rejected if (showFormattedValue) { const formatted = formatDataForPreview(payload.reason, false); return `rejected lazy() {${truncateForDisplay(formatted)}}`; } else { return `rejected lazy() {…}`; } } } // Some form of uninitialized return 'lazy()'; case 'array_buffer': return `ArrayBuffer(${data.byteLength})`; case 'data_view': return `DataView(${data.buffer.byteLength})`; case 'array': if (showFormattedValue) { let formatted = ''; for (let i = 0; i < data.length; i++) { if (i > 0) { formatted += ', '; } formatted += formatDataForPreview(data[i], false); if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { // Prevent doing a lot of unnecessary iteration... break; } } return `[${truncateForDisplay(formatted)}]`; } else { const length = hasOwnProperty.call(data, meta.size) ? data[meta.size] : data.length; return `Array(${length})`; } case 'typed_array': const shortName = `${data.constructor.name}(${data.length})`; if (showFormattedValue) { let formatted = ''; for (let i = 0; i < data.length; i++) { if (i > 0) { formatted += ', '; } formatted += data[i]; if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { // Prevent doing a lot of unnecessary iteration... break; } } return `${shortName} [${truncateForDisplay(formatted)}]`; } else { return shortName; } case 'iterator': const name = data.constructor.name; if (showFormattedValue) { // TRICKY // Don't use [...spread] syntax for this purpose. // This project uses @babel/plugin-transform-spread in "loose" mode which only works with Array values. // Other types (e.g. typed arrays, Sets) will not spread correctly. const array = Array.from(data); let formatted = ''; for (let i = 0; i < array.length; i++) { const entryOrEntries = array[i]; if (i > 0) { formatted += ', '; } // TRICKY // Browsers display Maps and Sets differently. // To mimic their behavior, detect if we've been given an entries tuple. // Map(2) {"abc" => 123, "def" => 123} // Set(2) {"abc", 123} if (isArray(entryOrEntries)) { const key = formatDataForPreview(entryOrEntries[0], true); const value = formatDataForPreview(entryOrEntries[1], false); formatted += `${key} => ${value}`; } else { formatted += formatDataForPreview(entryOrEntries, false); } if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { // Prevent doing a lot of unnecessary iteration... break; } } return `${name}(${data.size}) {${truncateForDisplay(formatted)}}`; } else { return `${name}(${data.size})`; } case 'opaque_iterator': { return data[Symbol.toStringTag]; } case 'date': return data.toString(); case 'class_instance': try { let resolvedConstructorName = data.constructor.name; if (typeof resolvedConstructorName === 'string') { return resolvedConstructorName; } resolvedConstructorName = Object.getPrototypeOf(data).constructor.name; if (typeof resolvedConstructorName === 'string') { return resolvedConstructorName; } try { return truncateForDisplay(String(data)); } catch (error) { return 'unserializable'; } } catch (error) { return 'unserializable'; } case 'thenable': let displayName: string; if (isPlainObject(data)) { displayName = 'Thenable'; } else { let resolvedConstructorName = data.constructor.name; if (typeof resolvedConstructorName !== 'string') { resolvedConstructorName = Object.getPrototypeOf(data).constructor.name; } if (typeof resolvedConstructorName === 'string') { displayName = resolvedConstructorName; } else { displayName = 'Thenable'; } } switch (data.status) { case 'pending': return `pending ${displayName}`; case 'fulfilled': if (showFormattedValue) { const formatted = formatDataForPreview(data.value, false); return `fulfilled ${displayName} {${truncateForDisplay(formatted)}}`; } else { return `fulfilled ${displayName} {…}`; } case 'rejected': if (showFormattedValue) { const formatted = formatDataForPreview(data.reason, false); return `rejected ${displayName} {${truncateForDisplay(formatted)}}`; } else { return `rejected ${displayName} {…}`; } default: return displayName; } case 'object': if (showFormattedValue) { const keys = Array.from(getAllEnumerableKeys(data)).sort(alphaSortKeys); let formatted = ''; for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (i > 0) { formatted += ', '; } formatted += `${key.toString()}: ${formatDataForPreview( data[key], false, )}`; if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { // Prevent doing a lot of unnecessary iteration... break; } } return `{${truncateForDisplay(formatted)}}`; } else { return '{…}'; } case 'error': return truncateForDisplay(String(data)); case 'boolean': case 'number': case 'infinity': case 'nan': case 'null': case 'undefined': return String(data); default: try { return truncateForDisplay(String(data)); } catch (error) { return 'unserializable'; } } } // Basically checking that the object only has Object in its prototype chain export const isPlainObject = (object: Object): boolean => { const objectPrototype = Object.getPrototypeOf(object); if (!objectPrototype) return true; const objectParentPrototype = Object.getPrototypeOf(objectPrototype); return !objectParentPrototype; }; export function backendToFrontendSerializedElementMapper( element: SerializedElementBackend, ): SerializedElementFrontend { const {formattedDisplayName, hocDisplayNames, compiledWithForget} = parseElementDisplayNameFromBackend(element.displayName, element.type); return { ...element, displayName: formattedDisplayName, hocDisplayNames, compiledWithForget, }; } /** * Should be used when treating url as a Chrome Resource URL. */ export function normalizeUrlIfValid(url: string): string { try { // TODO: Chrome will use the basepath to create a Resource URL. return new URL(url).toString(); } catch { // Giving up if it's not a valid URL without basepath return url; } } export function getIsReloadAndProfileSupported(): boolean { // Notify the frontend if the backend supports the Storage API (e.g. localStorage). // If not, features like reload-and-profile will not work correctly and must be disabled. let isBackendStorageAPISupported = false; try { localStorage.getItem('test'); isBackendStorageAPISupported = true; } catch (error) {} return isBackendStorageAPISupported && isSynchronousXHRSupported(); } // Expected to be used only by browser extension and react-devtools-inline export function getIfReloadedAndProfiling(): boolean { return ( sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true' ); } export function getProfilingSettings(): ProfilingSettings { return { recordChangeDescriptions: sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) === 'true', recordTimeline: sessionStorageGetItem(SESSION_STORAGE_RECORD_TIMELINE_KEY) === 'true', }; } export function onReloadAndProfile( recordChangeDescriptions: boolean, recordTimeline: boolean, ): void { sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true'); sessionStorageSetItem( SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, recordChangeDescriptions ? 'true' : 'false', ); sessionStorageSetItem( SESSION_STORAGE_RECORD_TIMELINE_KEY, recordTimeline ? 'true' : 'false', ); } export function onReloadAndProfileFlagsReset(): void { sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY); sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY); sessionStorageRemoveItem(SESSION_STORAGE_RECORD_TIMELINE_KEY); }