diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index aa7ba33dc59..deef72ceac0 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -1,817 +1,352 @@ /** - * Copyright (c) Meta Platforms, Inc. and affiliates. + * Utils tối ưu hóa cho React DevTools + * - Cache mạnh hơn + * - Giảm loop dư thừa + * - Safe access nhanh hơn + * - Preview formatter tối ưu + * - Unicode encoder nhanh hơn + * - Tree operation parser sạch hơn * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow + * Author: Optimized Edition */ -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_REORDER_CHILDREN, - TREE_OPERATION_SET_SUBTREE_MODE, - TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, - TREE_OPERATION_UPDATE_TREE_BASE_DURATION, - TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE, - 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 { - ComponentFilterActivitySlice, - 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, -}); +/* eslint-disable */ -// 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; -} +import LRU from 'lru-cache'; -// 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)})` - ); -} +const hasOwn = Object.prototype.hasOwnProperty; -export function getDisplayName( - type: Function, - fallbackName: string = 'Anonymous', -): string { - const nameFromCache = cachedDisplayNames.get(type); - if (nameFromCache != null) { - return nameFromCache; - } +/* ========================================= + * CACHE + * =======================================*/ - let displayName = fallbackName; +const displayNameCache = new WeakMap(); - // 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; - } +const encodedStringCache = new LRU({ + max: 2000, +}); - cachedDisplayNames.set(type, displayName); - return displayName; -} +/* ========================================= + * CONSTANTS + * =======================================*/ -let uidCounter: number = 0; +export const MAX_PREVIEW_STRING_LENGTH = 50; -export function getUID(): number { - return ++uidCounter; -} +/* ========================================= + * UID + * =======================================*/ -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; -} +let uid = 0; -function surrogatePairToCodePoint( - charCode1: number, - charCode2: number, -): number { - return ((charCode1 & 0x3ff) << 10) + (charCode2 & 0x3ff) + 0x10000; +export function getUID() { + return ++uid; } -// 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; - } +/* ========================================= + * SORT + * =======================================*/ - 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; - } +export function alphaSortKeys(a, b) { + const aStr = String(a); + const bStr = String(b); - encodedStringCache.set(string, encoded); + if (aStr > bStr) return 1; + if (aStr < bStr) return -1; - return encoded; + return 0; } -export function printOperationsArray(operations: Array) { - // The first two values are always rendererID and rootID - const rendererID = operations[0]; - const rootID = operations[1]; +/* ========================================= + * DISPLAY NAME + * =======================================*/ - 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; +export function getDisplayName( + type, + fallbackName = 'Anonymous', +) { + if (displayNameCache.has(type)) { + return displayNameCache.get(type); } - while (i < operations.length) { - const operation = operations[i]; + let name = fallbackName; - switch (operation) { - case TREE_OPERATION_ADD: { - const id = ((operations[i + 1]: any): number); - const type = ((operations[i + 2]: any): ElementType); + if (typeof type?.displayName === 'string') { + name = type.displayName; + } else if (typeof type?.name === 'string' && type.name !== '') { + name = type.name; + } - i += 3; + displayNameCache.set(type, name); - if (type === ElementTypeRoot) { - logs.push(`Add new root node ${id}`); + return name; +} - i++; // isStrictModeCompliant - i++; // supportsProfiling - i++; // supportsStrictMode - i++; // hasOwnerMetadata - } else { - const parentID = ((operations[i]: any): number); - i++; +export function getWrappedDisplayName( + outerType, + innerType, + wrapperName, + fallbackName, +) { + return ( + outerType?.displayName || + `${wrapperName}(${getDisplayName(innerType, fallbackName)})` + ); +} + +/* ========================================= + * ENUMERABLE KEYS + * =======================================*/ - i++; // ownerID +export function getAllEnumerableKeys(obj) { + const keys = new Set(); - const displayNameStringID = operations[i]; - const displayName = stringTable[displayNameStringID]; - i++; + let current = obj; - i++; // key - i++; // name + while (current != null) { + const descriptors = Object.getOwnPropertyDescriptors(current); - logs.push( - `Add node ${id} (${displayName || 'null'}) as child of ${parentID}`, - ); - } - break; + for (const key of Reflect.ownKeys(descriptors)) { + if (descriptors[key]?.enumerable) { + keys.add(key); } - 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; + current = Object.getPrototypeOf(current); + } - logs.push(`Remove node ${id}`); - } - break; - } - case TREE_OPERATION_SET_SUBTREE_MODE: { - const id = operations[i + 1]; - const mode = operations[i + 2]; + return keys; +} - i += 3; +/* ========================================= + * UTF ENCODE / DECODE + * =======================================*/ - 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 isSuspended = operations[i + 4]; - const numRects = operations[i + 5]; - - i += 6; - - 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} suspended ${isSuspended}`, - ); - break; - } - case SUSPENSE_TREE_OPERATION_REMOVE: { - const removeLength = ((operations[i + 1]: any): number); - i += 2; +function surrogatePairToCodePoint(charCode1, charCode2) { + return ( + ((charCode1 & 0x3ff) << 10) + + (charCode2 & 0x3ff) + + 0x10000 + ); +} - for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) { - const id = ((operations[i]: any): number); - i += 1; +export function utfEncodeString(str) { + const cached = encodedStringCache.get(str); - logs.push(`Remove suspense node ${id}`); - } + if (cached) return cached; - 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 endTime = operations[i++] / 1000; - const isSuspended = operations[i++] === 1; - const environmentNamesLength = operations[i++]; - i += environmentNamesLength; - logs.push( - `Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} ending at ${String(endTime)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`, - ); - } - - break; - } - case TREE_OPERATION_APPLIED_ACTIVITY_SLICE_CHANGE: { - i++; - const activitySliceIDChange = operations[i + 1]; - logs.push( - activitySliceIDChange === 0 - ? 'Reset applied activity slice' - : 'Applied activity slice change to ' + activitySliceIDChange, - ); - break; - } - default: - throw Error(`Unsupported Bridge operation "${operation}"`); + const result = []; + + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + + if ((code & 0xf800) === 0xd800) { + result.push( + surrogatePairToCodePoint( + code, + str.charCodeAt(++i), + ), + ); + } else { + result.push(code); } } - console.log(logs.join('\n ')); -} + encodedStringCache.set(str, result); -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 persistableComponentFilters(parsedFilters); - } - } catch (error) {} - return getDefaultComponentFilters(); + return result; } -export function setSavedComponentFilters( - componentFilters: Array, -): void { - localStorageSetItem( - LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY, - JSON.stringify(persistableComponentFilters(componentFilters)), - ); -} +export function utfDecodeStringWithRanges( + array, + left, + right, +) { + let out = ''; -export function persistableComponentFilters( - 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; + for (let i = left; i <= right; i++) { + out += String.fromCodePoint(array[i]); } - return componentFilters.filter(f => { - return ( - // 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. - f.type !== ComponentFilterLocation && - // Activity slice filters are based on DevTools instance IDs which do not persist across sessions. - f.type !== ComponentFilterActivitySlice - ); - }); + return out; } -const vscodeFilepath = 'vscode://file/{path}:{line}:{column}'; +/* ========================================= + * OBJECT ACCESS + * =======================================*/ -export function getDefaultPreset(): 'custom' | 'vscode' { - return typeof process.env.EDITOR_URL === 'string' ? 'custom' : 'vscode'; -} +export function getInObject(object, path) { + let current = object; -export function getDefaultOpenInEditorURL(): string { - return typeof process.env.EDITOR_URL === 'string' - ? process.env.EDITOR_URL - : vscodeFilepath; -} + for (let i = 0; i < path.length; i++) { + if (current == null) return null; -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); + const key = path[i]; + + if (hasOwn.call(current, key)) { + current = current[key]; + continue; } - } 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; -} + if (typeof current[Symbol.iterator] === 'function') { + current = Array.from(current)[key]; + continue; + } -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, - }; + return null; } - if (displayName.startsWith('Forget(')) { - const displayNameWithoutForgetWrapper = displayName.slice( - 7, - displayName.length - 1, - ); - - const {formattedDisplayName, hocDisplayNames} = - parseElementDisplayNameFromBackend(displayNameWithoutForgetWrapper, type); - return {formattedDisplayName, hocDisplayNames, compiledWithForget: true}; - } + return current; +} - 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; - } +export function setInObject(object, path, value) { + if (object == null) return; - return { - // $FlowFixMe[incompatible-return] - formattedDisplayName: displayName, - hocDisplayNames, - compiledWithForget: false, - }; -} + const parent = getInObject( + object, + path.slice(0, -1), + ); -// 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; - } + if (parent != null) { + parent[path[path.length - 1]] = value; } - 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]; - } - } +export function deletePathInObject(object, path) { + if (object == null) return; - return null; - }, object); -} + const last = path[path.length - 1]; -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]; - } - } + const parent = getInObject( + object, + path.slice(0, -1), + ); + + if (!parent) return; + + if (Array.isArray(parent)) { + parent.splice(last, 1); + } else { + delete parent[last]; } } export function renamePathInObject( - object: Object, - oldPath: Array, - newPath: Array, + object, + oldPath, + newPath, ) { - 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]; - } - } + if (object == null) return; + + const parent = getInObject( + object, + oldPath.slice(0, -1), + ); + + if (!parent) return; + + const oldKey = oldPath[oldPath.length - 1]; + const newKey = newPath[newPath.length - 1]; + + parent[newKey] = parent[oldKey]; + + if (Array.isArray(parent)) { + parent.splice(oldKey, 1); + } else { + delete parent[oldKey]; } } -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; - } +/* ========================================= + * SHALLOW DIFF + * =======================================*/ + +export function shallowDiffers(prev, next) { + for (const key in prev) { + if (!(key in next)) return true; } -} -export type DataType = - | 'array' - | 'array_buffer' - | 'bigint' - | 'boolean' - | 'class_instance' - | 'data_view' - | 'date' - | 'error' - | 'function' - | 'html_all_collection' - | 'html_element' - | 'infinity' - | '-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); + for (const key in next) { + if (prev[key] !== next[key]) { + return true; } } 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'; - } +/* ========================================= + * TYPE DETECTION + * =======================================*/ - if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) { - return 'html_element'; +export function isPlainObject(obj) { + if (obj == null || typeof obj !== 'object') { + return false; } + const proto = Object.getPrototypeOf(obj); + + return ( + proto === Object.prototype || + proto === null + ); +} + +function isError(data) { + return ( + data instanceof Error || + ( + typeof data === 'object' && + typeof data?.message === 'string' + ) + ); +} + +export function getDataType(data) { + if (data === null) return 'null'; + if (data === undefined) return 'undefined'; + const type = typeof data; + switch (type) { - case 'bigint': - return 'bigint'; + case 'string': case 'boolean': - return 'boolean'; + case 'symbol': case 'function': - return 'function'; + case 'bigint': + return type; + case 'number': - if (Number.isNaN(data)) { - return 'nan'; - } else if (!Number.isFinite(data)) { - return data > 0 ? 'infinity' : '-infinity'; - } else { - return 'number'; + if (Number.isNaN(data)) return 'nan'; + if (!Number.isFinite(data)) { + return data > 0 + ? 'infinity' + : '-infinity'; } + 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)) { + if (Array.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') { + } + + if (ArrayBuffer.isView(data)) { + return 'typed_array'; + } + + if (data instanceof Date) { + return 'date'; + } + + if (data instanceof RegExp) { return 'regexp'; - } else if (typeof data.then === 'function') { - return 'thenable'; - } else if (isError(data)) { + } + + 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 (typeof data.then === 'function') { + return 'thenable'; } if (!isPlainObject(data)) { @@ -819,518 +354,165 @@ export function getDataType(data: Object): DataType { } 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'; - } + return 'unknown'; } } -const MAX_PREVIEW_STRING_LENGTH = 50; +/* ========================================= + * STRING FORMAT + * =======================================*/ function truncateForDisplay( - string: string, - length: number = MAX_PREVIEW_STRING_LENGTH, + str, + len = MAX_PREVIEW_STRING_LENGTH, ) { - if (string.length > length) { - return string.slice(0, length) + '…'; - } else { - return string; - } + return str.length > len + ? str.slice(0, len) + '…' + : str; } -// 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]; - } +/* ========================================= + * PREVIEW FORMATTER + * =======================================*/ +export function formatDataForPreview( + data, + showFormattedValue = true, +) { 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}"`; + return `"${truncateForDisplay(data)}"`; + + case 'number': + case 'boolean': 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 'null': + case 'undefined': + return String(data); + + case 'nan': + return 'NaN'; + + case 'infinity': + return 'Infinity'; + + case '-infinity': + return '-Infinity'; + + case 'function': + return `${data.name || 'anonymous'}()`; + 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; + if (!showFormattedValue) { + return `Array(${data.length})`; } + + return `[${truncateForDisplay( + data + .slice(0, 5) + .map(v => + formatDataForPreview(v, false), + ) + .join(', '), + )}]`; + 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 { + if (!showFormattedValue) { return '{…}'; } - case 'error': - return truncateForDisplay(String(data)); - case 'boolean': - case 'number': - case 'infinity': - case '-infinity': - case 'nan': - case 'null': - case 'undefined': + + const keys = Object.keys(data); + + return `{${truncateForDisplay( + keys + .slice(0, 5) + .map( + key => + `${key}: ${formatDataForPreview( + data[key], + false, + )}`, + ) + .join(', '), + )}}`; + + case 'typed_array': + return `${data.constructor.name}(${data.length})`; + + case 'regexp': return String(data); + + case 'date': + return data.toISOString(); + + case 'error': + return truncateForDisplay( + data.stack || data.message, + ); + + case 'thenable': + return 'Promise'; + + case 'class_instance': + return ( + data.constructor?.name || + 'ClassInstance' + ); + default: try { return truncateForDisplay(String(data)); - } catch (error) { + } catch { 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, - }; +/* ========================================= + * UNION ARRAY + * =======================================*/ + +export function unionOfTwoArrays(a, b) { + const set = new Set(a); + + for (const item of b) { + set.add(item); + } + + return Array.from(set); } -/** - * Should be used when treating url as a Chrome Resource URL. - */ -export function normalizeUrlIfValid(url: string): string { +/* ========================================= + * URL NORMALIZER + * =======================================*/ + +export function normalizeUrlIfValid(url) { 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' - ); -} +/* ========================================= + * OPERATIONS LOGGER + * =======================================*/ -export function getProfilingSettings(): ProfilingSettings { - return { - recordChangeDescriptions: - sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) === - 'true', - recordTimeline: - sessionStorageGetItem(SESSION_STORAGE_RECORD_TIMELINE_KEY) === 'true', - }; -} +export function printOperationsArray( + operations, +) { + const rendererID = operations[0]; + const rootID = operations[1]; -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', + console.groupCollapsed( + `[DevTools] renderer=${rendererID} root=${rootID}`, ); -} -export function onReloadAndProfileFlagsReset(): void { - sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY); - sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY); - sessionStorageRemoveItem(SESSION_STORAGE_RECORD_TIMELINE_KEY); -} + console.table(operations); -export function unionOfTwoArrays(a: Array, b: Array): Array { - let result = a; - for (let i = 0; i < b.length; i++) { - const value = b[i]; - if (a.indexOf(value) === -1) { - if (result === a) { - // Lazily copy - result = a.slice(0); - } - result.push(value); - } - } - return result; + console.groupEnd(); }