mirror of
https://github.com/zebrajr/react.git
synced 2025-12-07 00:20:28 +01:00
* [react-devtools-shared] Added string type check for object name prop in getDisplayName function from utils.js file; tests included; * Re-added empty string check to getDisplayName() * Tweaked tests to use real functions This more closely simulates how the utility is being used in production, and would catch cases like anonymous functions (with empty string names).
308 lines
8.1 KiB
JavaScript
308 lines
8.1 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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 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<Function, string> = 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 = 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 utfDecodeString(array: Array<number>): string {
|
|
return String.fromCodePoint(...array);
|
|
}
|
|
|
|
export function utfEncodeString(string: string): Array<number> {
|
|
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<number>) {
|
|
// 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<ComponentFilter> {
|
|
return [
|
|
{
|
|
type: ComponentFilterElementType,
|
|
value: ElementTypeHostComponent,
|
|
isEnabled: true,
|
|
},
|
|
];
|
|
}
|
|
|
|
export function getSavedComponentFilters(): Array<ComponentFilter> {
|
|
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<ComponentFilter>,
|
|
): 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<string> | 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<string | number>): 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 setInObject(
|
|
object: Object,
|
|
path: Array<string | number>,
|
|
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;
|
|
}
|
|
}
|
|
}
|