// @flow import Symbol from 'es6-symbol'; import LRU from 'lru-cache'; import { TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REORDER_CHILDREN, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, } from './constants'; import {ElementTypeRoot} from 'react-devtools-shared/src/types'; import { LOCAL_STORAGE_FILTER_PREFERENCES_KEY, LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, } from './constants'; import {ComponentFilterElementType, ElementTypeHostComponent} from './types'; import { ElementTypeClass, ElementTypeForwardRef, ElementTypeFunction, ElementTypeMemo, } from 'react-devtools-shared/src/types'; import {localStorageGetItem, localStorageSetItem} from './storage'; import type {ComponentFilter, ElementType} from './types'; const cachedDisplayNames: WeakMap = new WeakMap(); // On large trees, encoding takes significant time. // Try to reuse the already encoded strings. let encodedStringCache = new LRU({max: 1000}); export function getDisplayName( type: Function, fallbackName: string = 'Anonymous', ): string { const nameFromCache = cachedDisplayNames.get(type); if (nameFromCache != null) { return nameFromCache; } let displayName; // 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; } if (!displayName) { displayName = type.name || fallbackName; } cachedDisplayNames.set(type, displayName); return displayName; } let uidCounter: number = 0; export function getUID(): number { return ++uidCounter; } export function utfDecodeString(array: Array): string { return String.fromCodePoint(...array); } export function utfEncodeString(string: string): Array { let cached = encodedStringCache.get(string); if (cached !== undefined) { return cached; } const encoded = new Array(string.length); for (let i = 0; i < string.length; i++) { encoded[i] = string.codePointAt(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 = [`opertions for renderer:${rendererID} and root:${rootID}`]; let i = 2; // Reassemble the string table. const stringTable = [ 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 = utfDecodeString( (operations.slice(i, i + nextLength): any), ); 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++; // supportsProfiling i++; // hasOwnerMetadata } else { const parentID = ((operations[i]: any): number); i++; i++; // ownerID const displayNameStringID = operations[i]; const displayName = stringTable[displayNameStringID]; i++; i++; // key 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_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; 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_FILTER_PREFERENCES_KEY); if (raw != null) { return JSON.parse(raw); } } catch (error) {} return getDefaultComponentFilters(); } export function saveComponentFilters( componentFilters: Array, ): void { localStorageSetItem( LOCAL_STORAGE_FILTER_PREFERENCES_KEY, JSON.stringify(componentFilters), ); } export function getAppendComponentStack(): boolean { try { const raw = localStorageGetItem(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY); if (raw != null) { return JSON.parse(raw); } } catch (error) {} return true; } export function setAppendComponentStack(value: boolean): void { localStorageSetItem( LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, JSON.stringify(value), ); } export function separateDisplayNameAndHOCs( displayName: string | null, type: ElementType, ): [string | null, Array | null] { if (displayName === null) { return [null, null]; } let hocDisplayNames = null; switch (type) { case ElementTypeClass: case ElementTypeForwardRef: case ElementTypeFunction: case ElementTypeMemo: if (displayName.indexOf('(') >= 0) { const matches = displayName.match(/[^()]+/g); if (matches != null) { displayName = matches.pop(); hocDisplayNames = matches; } } break; default: break; } return [displayName, hocDisplayNames]; } // 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 (let attribute in prev) { if (!(attribute in next)) { return true; } } for (let 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: string | number): 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] return [...reduced][attr]; } } return null; }, object); } 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; } } }