2019-08-28 01:54:01 +08:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
2019-01-29 07:50:48 +08:00
|
|
|
|
2019-04-17 01:22:26 +08:00
|
|
|
import LRU from 'lru-cache';
|
2019-12-12 09:52:17 +08:00
|
|
|
import {
|
|
|
|
|
isElement,
|
|
|
|
|
typeOf,
|
|
|
|
|
ContextConsumer,
|
|
|
|
|
ContextProvider,
|
|
|
|
|
ForwardRef,
|
|
|
|
|
Fragment,
|
|
|
|
|
Lazy,
|
|
|
|
|
Memo,
|
|
|
|
|
Portal,
|
|
|
|
|
Profiler,
|
|
|
|
|
StrictMode,
|
|
|
|
|
Suspense,
|
|
|
|
|
} from 'react-is';
|
2020-08-27 01:04:43 +08:00
|
|
|
import {REACT_SUSPENSE_LIST_TYPE as SuspenseList} from 'shared/ReactSymbols';
|
2019-04-17 01:22:26 +08:00
|
|
|
import {
|
|
|
|
|
TREE_OPERATION_ADD,
|
|
|
|
|
TREE_OPERATION_REMOVE,
|
2020-12-23 00:09:29 +08:00
|
|
|
TREE_OPERATION_REMOVE_ROOT,
|
2019-05-26 00:10:43 +08:00
|
|
|
TREE_OPERATION_REORDER_CHILDREN,
|
2021-12-11 00:05:18 +08:00
|
|
|
TREE_OPERATION_SET_SUBTREE_MODE,
|
2020-12-23 00:09:29 +08:00
|
|
|
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
2019-04-17 01:22:26 +08:00
|
|
|
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
|
|
|
|
} from './constants';
|
2019-08-14 08:58:03 +08:00
|
|
|
import {ElementTypeRoot} from 'react-devtools-shared/src/types';
|
2019-07-18 02:12:39 +08:00
|
|
|
import {
|
|
|
|
|
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
|
2021-11-03 23:27:30 +08:00
|
|
|
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
|
2020-05-30 05:34:43 +08:00
|
|
|
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
|
2019-07-18 02:12:39 +08:00
|
|
|
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
|
2020-12-23 00:09:29 +08:00
|
|
|
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
|
2021-08-26 06:35:38 +08:00
|
|
|
LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
|
2019-07-18 02:12:39 +08:00
|
|
|
} from './constants';
|
2019-08-14 08:58:03 +08:00
|
|
|
import {ComponentFilterElementType, ElementTypeHostComponent} from './types';
|
2019-06-01 23:24:24 +08:00
|
|
|
import {
|
|
|
|
|
ElementTypeClass,
|
|
|
|
|
ElementTypeForwardRef,
|
|
|
|
|
ElementTypeFunction,
|
|
|
|
|
ElementTypeMemo,
|
2019-08-14 06:59:43 +08:00
|
|
|
} from 'react-devtools-shared/src/types';
|
2019-08-14 08:58:03 +08:00
|
|
|
import {localStorageGetItem, localStorageSetItem} from './storage';
|
2019-12-12 09:52:17 +08:00
|
|
|
import {meta} from './hydration';
|
2021-09-28 05:17:30 +08:00
|
|
|
import isArray from './isArray';
|
2021-07-02 02:39:18 +08:00
|
|
|
|
2019-08-14 08:58:03 +08:00
|
|
|
import type {ComponentFilter, ElementType} from './types';
|
2021-07-02 02:39:18 +08:00
|
|
|
import type {LRUCache} from 'react-devtools-shared/src/types';
|
2019-04-15 21:41:58 +08:00
|
|
|
|
2019-02-05 18:25:33 +08:00
|
|
|
const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
|
|
|
|
|
|
2019-04-15 21:41:58 +08:00
|
|
|
// On large trees, encoding takes significant time.
|
|
|
|
|
// Try to reuse the already encoded strings.
|
2021-07-02 02:39:18 +08:00
|
|
|
const encodedStringCache: LRUCache<string, Array<number>> = new LRU({
|
|
|
|
|
max: 1000,
|
|
|
|
|
});
|
2019-04-15 21:41:58 +08:00
|
|
|
|
2020-09-14 21:55:19 +08:00
|
|
|
export function alphaSortKeys(
|
|
|
|
|
a: string | number | Symbol,
|
|
|
|
|
b: string | number | Symbol,
|
|
|
|
|
): number {
|
|
|
|
|
if (a.toString() > b.toString()) {
|
2020-01-16 01:45:16 +08:00
|
|
|
return 1;
|
2020-09-14 21:55:19 +08:00
|
|
|
} else if (b.toString() > a.toString()) {
|
2020-01-16 01:45:16 +08:00
|
|
|
return -1;
|
|
|
|
|
} else {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-14 21:55:19 +08:00
|
|
|
export function getAllEnumerableKeys(
|
|
|
|
|
obj: Object,
|
2021-01-19 23:26:52 +08:00
|
|
|
): Set<string | number | Symbol> {
|
|
|
|
|
const keys = new Set();
|
2020-09-14 21:55:19 +08:00
|
|
|
let current = obj;
|
|
|
|
|
while (current != null) {
|
|
|
|
|
const currentKeys = [
|
|
|
|
|
...Object.keys(current),
|
|
|
|
|
...Object.getOwnPropertySymbols(current),
|
|
|
|
|
];
|
|
|
|
|
const descriptors = Object.getOwnPropertyDescriptors(current);
|
|
|
|
|
currentKeys.forEach(key => {
|
|
|
|
|
// $FlowFixMe: key can be a Symbol https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
|
|
|
|
|
if (descriptors[key].enumerable) {
|
2021-01-19 23:26:52 +08:00
|
|
|
keys.add(key);
|
2020-09-14 21:55:19 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
current = Object.getPrototypeOf(current);
|
|
|
|
|
}
|
|
|
|
|
return keys;
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-05 18:25:33 +08:00
|
|
|
export function getDisplayName(
|
|
|
|
|
type: Function,
|
2019-08-14 08:58:03 +08:00
|
|
|
fallbackName: string = 'Anonymous',
|
2019-02-05 18:25:33 +08:00
|
|
|
): string {
|
|
|
|
|
const nameFromCache = cachedDisplayNames.get(type);
|
|
|
|
|
if (nameFromCache != null) {
|
|
|
|
|
return nameFromCache;
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-18 04:17:04 +08:00
|
|
|
let displayName = fallbackName;
|
2019-02-05 18:25:33 +08:00
|
|
|
|
|
|
|
|
// 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;
|
2019-09-18 04:17:04 +08:00
|
|
|
} else if (typeof type.name === 'string' && type.name !== '') {
|
|
|
|
|
displayName = type.name;
|
2019-02-05 18:25:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cachedDisplayNames.set(type, displayName);
|
|
|
|
|
return displayName;
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-05 01:49:30 +08:00
|
|
|
let uidCounter: number = 0;
|
|
|
|
|
|
|
|
|
|
export function getUID(): number {
|
|
|
|
|
return ++uidCounter;
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-14 01:05:04 +08:00
|
|
|
export function utfDecodeString(array: Array<number>): string {
|
2021-09-17 21:52:52 +08:00
|
|
|
// Avoid spreading the array (e.g. String.fromCodePoint(...array))
|
|
|
|
|
// Functions arguments are first placed on the stack before the function is called
|
|
|
|
|
// which throws a RangeError for large arrays.
|
|
|
|
|
// See github.com/facebook/react/issues/22293
|
|
|
|
|
let string = '';
|
|
|
|
|
for (let i = 0; i < array.length; i++) {
|
|
|
|
|
const char = array[i];
|
|
|
|
|
string += String.fromCodePoint(char);
|
|
|
|
|
}
|
|
|
|
|
return string;
|
2019-01-29 07:50:48 +08:00
|
|
|
}
|
|
|
|
|
|
2021-09-28 01:34:39 +08:00
|
|
|
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
|
2019-07-14 01:05:04 +08:00
|
|
|
export function utfEncodeString(string: string): Array<number> {
|
2020-04-02 03:35:52 +08:00
|
|
|
const cached = encodedStringCache.get(string);
|
2019-04-15 21:41:58 +08:00
|
|
|
if (cached !== undefined) {
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-28 01:34:39 +08:00
|
|
|
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;
|
2019-07-14 01:05:04 +08:00
|
|
|
}
|
2021-09-28 01:34:39 +08:00
|
|
|
|
2019-04-15 21:41:58 +08:00
|
|
|
encodedStringCache.set(string, encoded);
|
2021-09-28 01:34:39 +08:00
|
|
|
|
2019-04-15 21:41:58 +08:00
|
|
|
return encoded;
|
2019-02-19 15:03:53 +08:00
|
|
|
}
|
|
|
|
|
|
2019-07-14 01:05:04 +08:00
|
|
|
export function printOperationsArray(operations: Array<number>) {
|
2019-05-26 00:10:43 +08:00
|
|
|
// The first two values are always rendererID and rootID
|
2019-04-17 01:22:26 +08:00
|
|
|
const rendererID = operations[0];
|
|
|
|
|
const rootID = operations[1];
|
|
|
|
|
|
2020-01-10 06:13:03 +08:00
|
|
|
const logs = [`operations for renderer:${rendererID} and root:${rootID}`];
|
2019-04-17 01:22:26 +08:00
|
|
|
|
|
|
|
|
let i = 2;
|
|
|
|
|
|
2019-05-26 00:10:43 +08:00
|
|
|
// 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(
|
2019-08-14 08:58:03 +08:00
|
|
|
(operations.slice(i, i + nextLength): any),
|
2019-05-26 00:10:43 +08:00
|
|
|
);
|
|
|
|
|
stringTable.push(nextString);
|
|
|
|
|
i += nextLength;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (i < operations.length) {
|
2019-04-17 01:22:26 +08:00
|
|
|
const operation = operations[i];
|
|
|
|
|
|
|
|
|
|
switch (operation) {
|
2019-05-26 00:10:43 +08:00
|
|
|
case TREE_OPERATION_ADD: {
|
|
|
|
|
const id = ((operations[i + 1]: any): number);
|
|
|
|
|
const type = ((operations[i + 2]: any): ElementType);
|
2019-04-17 01:22:26 +08:00
|
|
|
|
2019-05-26 00:10:43 +08:00
|
|
|
i += 3;
|
2019-04-17 01:22:26 +08:00
|
|
|
|
|
|
|
|
if (type === ElementTypeRoot) {
|
2019-05-26 00:10:43 +08:00
|
|
|
logs.push(`Add new root node ${id}`);
|
2019-04-17 01:22:26 +08:00
|
|
|
|
2021-12-11 00:05:18 +08:00
|
|
|
i++; // isStrictModeCompliant
|
2019-04-17 01:22:26 +08:00
|
|
|
i++; // supportsProfiling
|
2021-12-11 00:05:18 +08:00
|
|
|
i++; // supportsStrictMode
|
2019-04-17 01:22:26 +08:00
|
|
|
i++; // hasOwnerMetadata
|
|
|
|
|
} else {
|
2019-05-26 00:10:43 +08:00
|
|
|
const parentID = ((operations[i]: any): number);
|
2019-04-17 01:22:26 +08:00
|
|
|
i++;
|
|
|
|
|
|
|
|
|
|
i++; // ownerID
|
|
|
|
|
|
2019-05-26 00:10:43 +08:00
|
|
|
const displayNameStringID = operations[i];
|
|
|
|
|
const displayName = stringTable[displayNameStringID];
|
2019-04-17 01:22:26 +08:00
|
|
|
i++;
|
|
|
|
|
|
2019-05-26 00:10:43 +08:00
|
|
|
i++; // key
|
|
|
|
|
|
|
|
|
|
logs.push(
|
2019-08-14 08:58:03 +08:00
|
|
|
`Add node ${id} (${displayName || 'null'}) as child of ${parentID}`,
|
2019-04-17 01:22:26 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case TREE_OPERATION_REMOVE: {
|
2019-05-26 00:10:43 +08:00
|
|
|
const removeLength = ((operations[i + 1]: any): number);
|
|
|
|
|
i += 2;
|
2019-04-17 01:22:26 +08:00
|
|
|
|
2019-05-26 00:10:43 +08:00
|
|
|
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
|
|
|
|
|
const id = ((operations[i]: any): number);
|
|
|
|
|
i += 1;
|
2019-04-17 01:22:26 +08:00
|
|
|
|
2019-05-26 00:10:43 +08:00
|
|
|
logs.push(`Remove node ${id}`);
|
|
|
|
|
}
|
2019-04-17 01:22:26 +08:00
|
|
|
break;
|
|
|
|
|
}
|
2020-12-23 00:09:29 +08:00
|
|
|
case TREE_OPERATION_REMOVE_ROOT: {
|
|
|
|
|
i += 1;
|
|
|
|
|
|
|
|
|
|
logs.push(`Remove root ${rootID}`);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2021-12-11 00:05:18 +08:00
|
|
|
case TREE_OPERATION_SET_SUBTREE_MODE: {
|
|
|
|
|
const id = operations[i + 1];
|
|
|
|
|
const mode = operations[i + 1];
|
|
|
|
|
|
|
|
|
|
i += 3;
|
|
|
|
|
|
|
|
|
|
logs.push(`Mode ${mode} set for subtree with root ${id}`);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-05-26 00:10:43 +08:00
|
|
|
case TREE_OPERATION_REORDER_CHILDREN: {
|
|
|
|
|
const id = ((operations[i + 1]: any): number);
|
2019-04-17 01:22:26 +08:00
|
|
|
const numChildren = ((operations[i + 2]: any): number);
|
2019-05-26 00:10:43 +08:00
|
|
|
i += 3;
|
|
|
|
|
const children = operations.slice(i, i + numChildren);
|
|
|
|
|
i += numChildren;
|
2019-04-17 01:22:26 +08:00
|
|
|
|
2019-05-26 00:10:43 +08:00
|
|
|
logs.push(`Re-order node ${id} children ${children.join(',')}`);
|
2019-04-17 01:22:26 +08:00
|
|
|
break;
|
2019-05-26 00:10:43 +08:00
|
|
|
}
|
2019-04-17 01:22:26 +08:00
|
|
|
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.
|
2019-05-26 00:10:43 +08:00
|
|
|
i += 3;
|
2019-04-17 01:22:26 +08:00
|
|
|
break;
|
2020-12-23 00:09:29 +08:00
|
|
|
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;
|
2019-04-17 01:22:26 +08:00
|
|
|
default:
|
2021-04-20 01:05:28 +08:00
|
|
|
throw Error(`Unsupported Bridge operation "${operation}"`);
|
2019-04-17 01:22:26 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-26 00:10:43 +08:00
|
|
|
console.log(logs.join('\n '));
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:43:40 +08:00
|
|
|
export function getDefaultComponentFilters(): Array<ComponentFilter> {
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
type: ComponentFilterElementType,
|
|
|
|
|
value: ElementTypeHostComponent,
|
|
|
|
|
isEnabled: true,
|
|
|
|
|
},
|
|
|
|
|
];
|
2019-04-28 01:31:40 +08:00
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:43:40 +08:00
|
|
|
export function getSavedComponentFilters(): Array<ComponentFilter> {
|
|
|
|
|
try {
|
2019-06-08 01:51:01 +08:00
|
|
|
const raw = localStorageGetItem(LOCAL_STORAGE_FILTER_PREFERENCES_KEY);
|
2019-05-02 01:43:40 +08:00
|
|
|
if (raw != null) {
|
|
|
|
|
return JSON.parse(raw);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {}
|
|
|
|
|
return getDefaultComponentFilters();
|
2019-05-01 03:52:43 +08:00
|
|
|
}
|
|
|
|
|
|
2019-05-02 01:43:40 +08:00
|
|
|
export function saveComponentFilters(
|
2019-08-14 08:58:03 +08:00
|
|
|
componentFilters: Array<ComponentFilter>,
|
2019-04-28 01:31:40 +08:00
|
|
|
): void {
|
2019-06-08 01:51:01 +08:00
|
|
|
localStorageSetItem(
|
2019-04-28 01:31:40 +08:00
|
|
|
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
|
2019-08-14 08:58:03 +08:00
|
|
|
JSON.stringify(componentFilters),
|
2019-04-28 01:31:40 +08:00
|
|
|
);
|
2019-04-17 01:22:26 +08:00
|
|
|
}
|
2019-06-01 23:24:24 +08:00
|
|
|
|
2019-07-18 02:12:39 +08:00
|
|
|
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,
|
2019-08-14 08:58:03 +08:00
|
|
|
JSON.stringify(value),
|
2019-07-18 02:12:39 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-30 05:34:43 +08:00
|
|
|
export function getBreakOnConsoleErrors(): boolean {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorageGetItem(
|
|
|
|
|
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
|
|
|
|
|
);
|
|
|
|
|
if (raw != null) {
|
|
|
|
|
return JSON.parse(raw);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {}
|
2020-07-10 21:11:32 +08:00
|
|
|
return false;
|
2020-05-30 05:34:43 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function setBreakOnConsoleErrors(value: boolean): void {
|
|
|
|
|
localStorageSetItem(
|
|
|
|
|
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
|
|
|
|
|
JSON.stringify(value),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-26 06:35:38 +08:00
|
|
|
export function getHideConsoleLogsInStrictMode(): boolean {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorageGetItem(
|
|
|
|
|
LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
|
|
|
|
|
);
|
|
|
|
|
if (raw != null) {
|
|
|
|
|
return JSON.parse(raw);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function sethideConsoleLogsInStrictMode(value: boolean): void {
|
|
|
|
|
localStorageSetItem(
|
|
|
|
|
LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
|
|
|
|
|
JSON.stringify(value),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-23 00:09:29 +08:00
|
|
|
export function getShowInlineWarningsAndErrors(): boolean {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorageGetItem(
|
|
|
|
|
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
|
|
|
|
|
);
|
|
|
|
|
if (raw != null) {
|
|
|
|
|
return JSON.parse(raw);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function setShowInlineWarningsAndErrors(value: boolean): void {
|
|
|
|
|
localStorageSetItem(
|
|
|
|
|
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
|
|
|
|
|
JSON.stringify(value),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-03 23:27:30 +08:00
|
|
|
export function getDefaultOpenInEditorURL(): string {
|
|
|
|
|
return typeof process.env.EDITOR_URL === 'string'
|
|
|
|
|
? process.env.EDITOR_URL
|
|
|
|
|
: '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getOpenInEditorURL(): string {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorageGetItem(LOCAL_STORAGE_OPEN_IN_EDITOR_URL);
|
|
|
|
|
if (raw != null) {
|
|
|
|
|
return JSON.parse(raw);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {}
|
|
|
|
|
return getDefaultOpenInEditorURL();
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-01 23:24:24 +08:00
|
|
|
export function separateDisplayNameAndHOCs(
|
|
|
|
|
displayName: string | null,
|
2019-08-14 08:58:03 +08:00
|
|
|
type: ElementType,
|
2019-06-01 23:24:24 +08:00
|
|
|
): [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);
|
2019-08-14 06:59:43 +08:00
|
|
|
if (matches != null) {
|
2019-06-01 23:24:24 +08:00
|
|
|
displayName = matches.pop();
|
|
|
|
|
hocDisplayNames = matches;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-16 01:37:34 +08:00
|
|
|
if (type === ElementTypeMemo) {
|
|
|
|
|
if (hocDisplayNames === null) {
|
|
|
|
|
hocDisplayNames = ['Memo'];
|
|
|
|
|
} else {
|
|
|
|
|
hocDisplayNames.unshift('Memo');
|
|
|
|
|
}
|
|
|
|
|
} else if (type === ElementTypeForwardRef) {
|
|
|
|
|
if (hocDisplayNames === null) {
|
|
|
|
|
hocDisplayNames = ['ForwardRef'];
|
|
|
|
|
} else {
|
|
|
|
|
hocDisplayNames.unshift('ForwardRef');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-01 23:24:24 +08:00
|
|
|
return [displayName, hocDisplayNames];
|
|
|
|
|
}
|
2019-06-11 05:57:26 +08:00
|
|
|
|
|
|
|
|
// Pulled from react-compat
|
|
|
|
|
// https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349
|
|
|
|
|
export function shallowDiffers(prev: Object, next: Object): boolean {
|
2020-04-02 03:35:52 +08:00
|
|
|
for (const attribute in prev) {
|
2019-06-11 05:57:26 +08:00
|
|
|
if (!(attribute in next)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-02 03:35:52 +08:00
|
|
|
for (const attribute in next) {
|
2019-06-11 05:57:26 +08:00
|
|
|
if (prev[attribute] !== next[attribute]) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2019-06-17 08:32:37 +08:00
|
|
|
|
|
|
|
|
export function getInObject(object: Object, path: Array<string | number>): any {
|
2019-08-27 23:50:36 +08:00
|
|
|
return path.reduce((reduced: Object, attr: any): any => {
|
2019-08-18 01:20:26 +08:00
|
|
|
if (reduced) {
|
|
|
|
|
if (hasOwnProperty.call(reduced, attr)) {
|
|
|
|
|
return reduced[attr];
|
|
|
|
|
}
|
|
|
|
|
if (typeof reduced[Symbol.iterator] === 'function') {
|
|
|
|
|
// Convert iterable to array and return array[index]
|
2019-08-27 23:50:36 +08:00
|
|
|
//
|
|
|
|
|
// 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];
|
2019-08-18 01:20:26 +08:00
|
|
|
}
|
2019-06-17 08:32:37 +08:00
|
|
|
}
|
2019-08-18 01:20:26 +08:00
|
|
|
|
|
|
|
|
return null;
|
2019-06-17 08:32:37 +08:00
|
|
|
}, object);
|
|
|
|
|
}
|
|
|
|
|
|
Improve DevTools editing interface (#19774)
* Improve DevTools editing interface
This commit adds the ability to rename or delete keys in the props/state/hooks/context editor and adds tests to cover this functionality. DevTools will degrade gracefully for older versions of React that do not inject the new reconciler rename* or delete* methods.
Specifically, this commit includes the following changes:
* Adds unit tests (for modern and legacy renderers) to cover overriding props, renaming keys, and deleting keys.
* Refactor backend override methods to reduce redundant Bridge/Agent listeners and methods.
* Inject new (DEV-only) methods from reconciler into DevTools to rename and delete paths.
* Refactor 'inspected element' UI components to improve readability.
* Improve auto-size input to better mimic Chrome's Style editor panel. (See this Code Sandbox for a proof of concept.)
It also contains the following code cleanup:
* Additional unit tests have been added for modifying values as well as renaming or deleting paths.
* Four new DEV-only methods have been added to the reconciler to be injected into the DevTools hook: overrideHookStateDeletePath, overrideHookStateRenamePath, overridePropsDeletePath, and overridePropsRenamePath. (DevTools will degrade gracefully for older renderers without these methods.)
* I also took this as an opportunity to refactor some of the existing code in a few places:
* Rather than the backend implementing separate methods for editing props, state, hooks, and context– there are now three methods: deletePath, renamePath, and overrideValueAtPath that accept a type argument to differentiate between props, state, context, or hooks.
* The various UI components for the DevTools frontend have been refactored to remove some unnecessary repetition.
This commit also adds temporary support for override* commands with mismatched backend/frontend versions:
* Add message forwarding for older backend methods (overrideContext, overrideHookState, overrideProps, and overrideState) to the new overrideValueAtPath method. This was done in both the frontend Bridge (for newer frontends passing messages to older embedded backends) and in the backend Agent (for older frontends passing messages to newer backends). We do this because React Native embeds the React DevTools backend, but cannot control which version of the frontend users use.
* Additional unit tests have been added as well to cover the older frontend to newer backend case. Our DevTools test infra does not make it easy to write tests for the other way around.
2020-09-18 23:07:18 +08:00
|
|
|
export function deletePathInObject(
|
|
|
|
|
object: Object,
|
|
|
|
|
path: Array<string | number>,
|
|
|
|
|
) {
|
|
|
|
|
const length = path.length;
|
|
|
|
|
const last = path[length - 1];
|
|
|
|
|
if (object != null) {
|
|
|
|
|
const parent = getInObject(object, path.slice(0, length - 1));
|
|
|
|
|
if (parent) {
|
2021-09-28 05:17:30 +08:00
|
|
|
if (isArray(parent)) {
|
Improve DevTools editing interface (#19774)
* Improve DevTools editing interface
This commit adds the ability to rename or delete keys in the props/state/hooks/context editor and adds tests to cover this functionality. DevTools will degrade gracefully for older versions of React that do not inject the new reconciler rename* or delete* methods.
Specifically, this commit includes the following changes:
* Adds unit tests (for modern and legacy renderers) to cover overriding props, renaming keys, and deleting keys.
* Refactor backend override methods to reduce redundant Bridge/Agent listeners and methods.
* Inject new (DEV-only) methods from reconciler into DevTools to rename and delete paths.
* Refactor 'inspected element' UI components to improve readability.
* Improve auto-size input to better mimic Chrome's Style editor panel. (See this Code Sandbox for a proof of concept.)
It also contains the following code cleanup:
* Additional unit tests have been added for modifying values as well as renaming or deleting paths.
* Four new DEV-only methods have been added to the reconciler to be injected into the DevTools hook: overrideHookStateDeletePath, overrideHookStateRenamePath, overridePropsDeletePath, and overridePropsRenamePath. (DevTools will degrade gracefully for older renderers without these methods.)
* I also took this as an opportunity to refactor some of the existing code in a few places:
* Rather than the backend implementing separate methods for editing props, state, hooks, and context– there are now three methods: deletePath, renamePath, and overrideValueAtPath that accept a type argument to differentiate between props, state, context, or hooks.
* The various UI components for the DevTools frontend have been refactored to remove some unnecessary repetition.
This commit also adds temporary support for override* commands with mismatched backend/frontend versions:
* Add message forwarding for older backend methods (overrideContext, overrideHookState, overrideProps, and overrideState) to the new overrideValueAtPath method. This was done in both the frontend Bridge (for newer frontends passing messages to older embedded backends) and in the backend Agent (for older frontends passing messages to newer backends). We do this because React Native embeds the React DevTools backend, but cannot control which version of the frontend users use.
* Additional unit tests have been added as well to cover the older frontend to newer backend case. Our DevTools test infra does not make it easy to write tests for the other way around.
2020-09-18 23:07:18 +08:00
|
|
|
parent.splice(((last: any): number), 1);
|
|
|
|
|
} else {
|
|
|
|
|
delete parent[last];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function renamePathInObject(
|
|
|
|
|
object: Object,
|
|
|
|
|
oldPath: Array<string | number>,
|
|
|
|
|
newPath: Array<string | number>,
|
|
|
|
|
) {
|
|
|
|
|
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];
|
2021-09-28 05:17:30 +08:00
|
|
|
if (isArray(parent)) {
|
Improve DevTools editing interface (#19774)
* Improve DevTools editing interface
This commit adds the ability to rename or delete keys in the props/state/hooks/context editor and adds tests to cover this functionality. DevTools will degrade gracefully for older versions of React that do not inject the new reconciler rename* or delete* methods.
Specifically, this commit includes the following changes:
* Adds unit tests (for modern and legacy renderers) to cover overriding props, renaming keys, and deleting keys.
* Refactor backend override methods to reduce redundant Bridge/Agent listeners and methods.
* Inject new (DEV-only) methods from reconciler into DevTools to rename and delete paths.
* Refactor 'inspected element' UI components to improve readability.
* Improve auto-size input to better mimic Chrome's Style editor panel. (See this Code Sandbox for a proof of concept.)
It also contains the following code cleanup:
* Additional unit tests have been added for modifying values as well as renaming or deleting paths.
* Four new DEV-only methods have been added to the reconciler to be injected into the DevTools hook: overrideHookStateDeletePath, overrideHookStateRenamePath, overridePropsDeletePath, and overridePropsRenamePath. (DevTools will degrade gracefully for older renderers without these methods.)
* I also took this as an opportunity to refactor some of the existing code in a few places:
* Rather than the backend implementing separate methods for editing props, state, hooks, and context– there are now three methods: deletePath, renamePath, and overrideValueAtPath that accept a type argument to differentiate between props, state, context, or hooks.
* The various UI components for the DevTools frontend have been refactored to remove some unnecessary repetition.
This commit also adds temporary support for override* commands with mismatched backend/frontend versions:
* Add message forwarding for older backend methods (overrideContext, overrideHookState, overrideProps, and overrideState) to the new overrideValueAtPath method. This was done in both the frontend Bridge (for newer frontends passing messages to older embedded backends) and in the backend Agent (for older frontends passing messages to newer backends). We do this because React Native embeds the React DevTools backend, but cannot control which version of the frontend users use.
* Additional unit tests have been added as well to cover the older frontend to newer backend case. Our DevTools test infra does not make it easy to write tests for the other way around.
2020-09-18 23:07:18 +08:00
|
|
|
parent.splice(((lastOld: any): number), 1);
|
|
|
|
|
} else {
|
|
|
|
|
delete parent[lastOld];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-17 08:32:37 +08:00
|
|
|
export function setInObject(
|
|
|
|
|
object: Object,
|
|
|
|
|
path: Array<string | number>,
|
2019-08-14 08:58:03 +08:00
|
|
|
value: any,
|
2019-06-17 08:32:37 +08:00
|
|
|
) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-12-12 09:52:17 +08:00
|
|
|
|
|
|
|
|
export type DataType =
|
|
|
|
|
| 'array'
|
|
|
|
|
| 'array_buffer'
|
|
|
|
|
| 'bigint'
|
|
|
|
|
| 'boolean'
|
|
|
|
|
| 'data_view'
|
|
|
|
|
| 'date'
|
|
|
|
|
| 'function'
|
2020-08-21 21:31:16 +08:00
|
|
|
| 'html_all_collection'
|
2019-12-12 09:52:17 +08:00
|
|
|
| 'html_element'
|
|
|
|
|
| 'infinity'
|
|
|
|
|
| 'iterator'
|
2020-09-23 02:23:20 +08:00
|
|
|
| 'opaque_iterator'
|
2019-12-12 09:52:17 +08:00
|
|
|
| 'nan'
|
|
|
|
|
| 'null'
|
|
|
|
|
| 'number'
|
|
|
|
|
| 'object'
|
|
|
|
|
| 'react_element'
|
2019-12-23 03:49:28 +08:00
|
|
|
| 'regexp'
|
2019-12-12 09:52:17 +08:00
|
|
|
| 'string'
|
|
|
|
|
| 'symbol'
|
|
|
|
|
| 'typed_array'
|
|
|
|
|
| 'undefined'
|
|
|
|
|
| 'unknown';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 (isElement(data)) {
|
|
|
|
|
return 'react_element';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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':
|
2021-09-28 05:17:30 +08:00
|
|
|
if (isArray(data)) {
|
2019-12-12 09:52:17 +08:00
|
|
|
return 'array';
|
|
|
|
|
} else if (ArrayBuffer.isView(data)) {
|
2020-01-04 01:34:12 +08:00
|
|
|
return hasOwnProperty.call(data.constructor, 'BYTES_PER_ELEMENT')
|
2019-12-12 09:52:17 +08:00
|
|
|
? 'typed_array'
|
|
|
|
|
: 'data_view';
|
2020-01-03 00:27:29 +08:00
|
|
|
} else if (data.constructor && data.constructor.name === 'ArrayBuffer') {
|
2019-12-12 09:52:17 +08:00
|
|
|
// 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') {
|
2021-06-11 22:15:48 +08:00
|
|
|
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';
|
|
|
|
|
}
|
2020-01-03 00:27:29 +08:00
|
|
|
} else if (data.constructor && data.constructor.name === 'RegExp') {
|
2019-12-23 03:49:28 +08:00
|
|
|
return 'regexp';
|
2020-08-21 21:31:16 +08:00
|
|
|
} else {
|
|
|
|
|
const toStringValue = Object.prototype.toString.call(data);
|
|
|
|
|
if (toStringValue === '[object Date]') {
|
|
|
|
|
return 'date';
|
|
|
|
|
} else if (toStringValue === '[object HTMLAllCollection]') {
|
|
|
|
|
return 'html_all_collection';
|
|
|
|
|
}
|
2019-12-12 09:52:17 +08:00
|
|
|
}
|
|
|
|
|
return 'object';
|
|
|
|
|
case 'string':
|
|
|
|
|
return 'string';
|
|
|
|
|
case 'symbol':
|
|
|
|
|
return 'symbol';
|
2020-08-21 21:31:16 +08:00
|
|
|
case 'undefined':
|
|
|
|
|
if (
|
|
|
|
|
Object.prototype.toString.call(data) === '[object HTMLAllCollection]'
|
|
|
|
|
) {
|
|
|
|
|
return 'html_all_collection';
|
|
|
|
|
}
|
|
|
|
|
return 'undefined';
|
2019-12-12 09:52:17 +08:00
|
|
|
default:
|
|
|
|
|
return 'unknown';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getDisplayNameForReactElement(
|
|
|
|
|
element: React$Element<any>,
|
|
|
|
|
): string | null {
|
|
|
|
|
const elementType = typeOf(element);
|
|
|
|
|
switch (elementType) {
|
|
|
|
|
case ContextConsumer:
|
|
|
|
|
return 'ContextConsumer';
|
|
|
|
|
case ContextProvider:
|
|
|
|
|
return 'ContextProvider';
|
|
|
|
|
case ForwardRef:
|
|
|
|
|
return 'ForwardRef';
|
|
|
|
|
case Fragment:
|
|
|
|
|
return 'Fragment';
|
|
|
|
|
case Lazy:
|
|
|
|
|
return 'Lazy';
|
|
|
|
|
case Memo:
|
|
|
|
|
return 'Memo';
|
|
|
|
|
case Portal:
|
|
|
|
|
return 'Portal';
|
|
|
|
|
case Profiler:
|
|
|
|
|
return 'Profiler';
|
|
|
|
|
case StrictMode:
|
|
|
|
|
return 'StrictMode';
|
|
|
|
|
case Suspense:
|
|
|
|
|
return 'Suspense';
|
2020-08-27 01:04:43 +08:00
|
|
|
case SuspenseList:
|
|
|
|
|
return 'SuspenseList';
|
2019-12-12 09:52:17 +08:00
|
|
|
default:
|
|
|
|
|
const {type} = element;
|
|
|
|
|
if (typeof type === 'string') {
|
|
|
|
|
return type;
|
2020-08-27 01:04:43 +08:00
|
|
|
} else if (typeof type === 'function') {
|
2019-12-12 09:52:17 +08:00
|
|
|
return getDisplayName(type, 'Anonymous');
|
2020-08-27 01:04:43 +08:00
|
|
|
} else if (type != null) {
|
|
|
|
|
return 'NotImplementedInDevtools';
|
2019-12-12 09:52:17 +08:00
|
|
|
} 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.substr(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 {
|
2020-01-04 01:34:12 +08:00
|
|
|
if (data != null && hasOwnProperty.call(data, meta.type)) {
|
2019-12-12 09:52:17 +08:00
|
|
|
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':
|
2020-08-13 00:15:53 +08:00
|
|
|
return truncateForDisplay(
|
|
|
|
|
`ƒ ${typeof data.name === 'function' ? '' : data.name}() {}`,
|
|
|
|
|
);
|
2019-12-12 09:52:17 +08:00
|
|
|
case 'string':
|
|
|
|
|
return `"${data}"`;
|
|
|
|
|
case 'bigint':
|
|
|
|
|
return truncateForDisplay(data.toString() + 'n');
|
2019-12-23 03:49:28 +08:00
|
|
|
case 'regexp':
|
|
|
|
|
return truncateForDisplay(data.toString());
|
2019-12-12 09:52:17 +08:00
|
|
|
case 'symbol':
|
|
|
|
|
return truncateForDisplay(data.toString());
|
|
|
|
|
case 'react_element':
|
|
|
|
|
return `<${truncateForDisplay(
|
|
|
|
|
getDisplayNameForReactElement(data) || 'Unknown',
|
|
|
|
|
)} />`;
|
|
|
|
|
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 {
|
2020-01-04 01:34:12 +08:00
|
|
|
const length = hasOwnProperty.call(data, meta.size)
|
2019-12-12 09:52:17 +08:00
|
|
|
? 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;
|
2020-09-23 02:23:20 +08:00
|
|
|
|
2019-12-12 09:52:17 +08:00
|
|
|
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}
|
2021-09-28 05:17:30 +08:00
|
|
|
if (isArray(entryOrEntries)) {
|
2019-12-12 09:52:17 +08:00
|
|
|
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})`;
|
|
|
|
|
}
|
2020-09-23 02:23:20 +08:00
|
|
|
case 'opaque_iterator': {
|
|
|
|
|
return data[Symbol.toStringTag];
|
|
|
|
|
}
|
2019-12-12 09:52:17 +08:00
|
|
|
case 'date':
|
|
|
|
|
return data.toString();
|
|
|
|
|
case 'object':
|
|
|
|
|
if (showFormattedValue) {
|
2021-01-19 23:26:52 +08:00
|
|
|
const keys = Array.from(getAllEnumerableKeys(data)).sort(alphaSortKeys);
|
2019-12-12 09:52:17 +08:00
|
|
|
|
|
|
|
|
let formatted = '';
|
|
|
|
|
for (let i = 0; i < keys.length; i++) {
|
|
|
|
|
const key = keys[i];
|
|
|
|
|
if (i > 0) {
|
|
|
|
|
formatted += ', ';
|
|
|
|
|
}
|
2020-09-14 21:55:19 +08:00
|
|
|
formatted += `${key.toString()}: ${formatDataForPreview(
|
|
|
|
|
data[key],
|
|
|
|
|
false,
|
|
|
|
|
)}`;
|
2019-12-12 09:52:17 +08:00
|
|
|
if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
|
|
|
|
|
// Prevent doing a lot of unnecessary iteration...
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return `{${truncateForDisplay(formatted)}}`;
|
|
|
|
|
} else {
|
|
|
|
|
return '{…}';
|
|
|
|
|
}
|
|
|
|
|
case 'boolean':
|
|
|
|
|
case 'number':
|
|
|
|
|
case 'infinity':
|
|
|
|
|
case 'nan':
|
|
|
|
|
case 'null':
|
|
|
|
|
case 'undefined':
|
|
|
|
|
return data;
|
|
|
|
|
default:
|
|
|
|
|
try {
|
Improve DEV errors if string coercion throws (Temporal.*, Symbol, etc.) (#22064)
* Revise ESLint rules for string coercion
Currently, react uses `'' + value` to coerce mixed values to strings.
This code will throw for Temporal objects or symbols.
To make string-coercion safer and to improve user-facing error messages,
This commit adds a new ESLint rule called `safe-string-coercion`.
This rule has two modes: a production mode and a non-production mode.
* If the `isProductionUserAppCode` option is true, then `'' + value`
coercions are allowed (because they're faster, although they may
throw) and `String(value)` coercions are disallowed. Exception:
when building error messages or running DEV-only code in prod
files, `String()` should be used because it won't throw.
* If the `isProductionUserAppCode` option is false, then `'' + value`
coercions are disallowed (because they may throw, and in non-prod
code it's not worth the risk) and `String(value)` are allowed.
Production mode is used for all files which will be bundled with
developers' userland apps. Non-prod mode is used for all other React
code: tests, DEV blocks, devtools extension, etc.
In production mode, in addiiton to flagging `String(value)` calls,
the rule will also flag `'' + value` or `value + ''` coercions that may
throw. The rule is smart enough to silence itself in the following
"will never throw" cases:
* When the coercion is wrapped in a `typeof` test that restricts to safe
(non-symbol, non-object) types. Example:
if (typeof value === 'string' || typeof value === 'number') {
thisWontReport('' + value);
}
* When what's being coerced is a unary function result, because unary
functions never return an object or a symbol.
* When the coerced value is a commonly-used numeric identifier:
`i`, `idx`, or `lineNumber`.
* When the statement immeidately before the coercion is a DEV-only
call to a function from shared/CheckStringCoercion.js. This call is a
no-op in production, but in DEV it will show a console error
explaining the problem, then will throw right after a long explanatory
code comment so that debugger users will have an idea what's going on.
The check function call must be in the following format:
if (__DEV__) {
checkXxxxxStringCoercion(value);
};
Manually disabling the rule is usually not necessary because almost all
prod use of the `'' + value` pattern falls into one of the categories
above. But in the rare cases where the rule isn't smart enough to detect
safe usage (e.g. when a coercion is inside a nested ternary operator),
manually disabling the rule will be needed.
The rule should also be manually disabled in prod error handling code
where `String(value)` should be used for coercions, because it'd be
bad to throw while building an error message or stack trace!
The prod and non-prod modes have differentiated error messages to
explain how to do a proper coercion in that mode.
If a production check call is needed but is missing or incorrect
(e.g. not in a DEV block or not immediately before the coercion), then
a context-sensitive error message will be reported so that developers
can figure out what's wrong and how to fix the problem.
Because string coercions are now handled by the `safe-string-coercion`
rule, the `no-primitive-constructor` rule no longer flags `String()`
usage. It still flags `new String(value)` because that usage is almost
always a bug.
* Add DEV-only string coercion check functions
This commit adds DEV-only functions to check whether coercing
values to strings using the `'' + value` pattern will throw. If it will
throw, these functions will:
1. Display a console error with a friendly error message describing
the problem and the developer can fix it.
2. Perform the coercion, which will throw. Right before the line where
the throwing happens, there's a long code comment that will help
debugger users (or others looking at the exception call stack) figure
out what happened and how to fix the problem.
One of these check functions should be called before all string coercion
of user-provided values, except when the the coercion is guaranteed not
to throw, e.g.
* if inside a typeof check like `if (typeof value === 'string')`
* if coercing the result of a unary function like `+value` or `value++`
* if coercing a variable named in a whitelist of numeric identifiers:
`i`, `idx`, or `lineNumber`.
The new `safe-string-coercion` internal ESLint rule enforces that
these check functions are called when they are required.
Only use these check functions in production code that will be bundled
with user apps. For non-prod code (and for production error-handling
code), use `String(value)` instead which may be a little slower but will
never throw.
* Add failing tests for string coercion
Added failing tests to verify:
* That input, select, and textarea elements with value and defaultValue
set to Temporal-like objects which will throw when coerced to string
using the `'' + value` pattern.
* That text elements will throw for Temporal-like objects
* That dangerouslySetInnerHTML will *not* throw for Temporal-like
objects because this value is not cast to a string before passing to
the DOM.
* That keys that are Temporal-like objects will throw
All tests above validate the friendly error messages thrown.
* Use `String(value)` for coercion in non-prod files
This commit switches non-production code from `'' + value` (which
throws for Temporal objects and symbols) to instead use `String(value)`
which won't throw for these or other future plus-phobic types.
"Non-produciton code" includes anything not bundled into user apps:
* Tests and test utilities. Note that I didn't change legacy React
test fixtures because I assumed it was good for those files to
act just like old React, including coercion behavior.
* Build scripts
* Dev tools package - In addition to switching to `String`, I also
removed special-case code for coercing symbols which is now
unnecessary.
* Add DEV-only string coercion checks to prod files
This commit adds DEV-only function calls to to check if string coercion
using `'' + value` will throw, which it will if the value is a Temporal
object or a symbol because those types can't be added with `+`.
If it will throw, then in DEV these checks will show a console error
to help the user undertsand what went wrong and how to fix the
problem. After emitting the console error, the check functions will
retry the coercion which will throw with a call stack that's easy (or
at least easier!) to troubleshoot because the exception happens right
after a long comment explaining the issue. So whether the user is in
a debugger, looking at the browser console, or viewing the in-browser
DEV call stack, it should be easy to understand and fix the problem.
In most cases, the safe-string-coercion ESLint rule is smart enough to
detect when a coercion is safe. But in rare cases (e.g. when a coercion
is inside a ternary) this rule will have to be manually disabled.
This commit also switches error-handling code to use `String(value)`
for coercion, because it's bad to crash when you're trying to build
an error message or a call stack! Because `String()` is usually
disallowed by the `safe-string-coercion` ESLint rule in production
code, the rule must be disabled when `String()` is used.
2021-09-28 01:05:07 +08:00
|
|
|
return truncateForDisplay(String(data));
|
2019-12-12 09:52:17 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
return 'unserializable';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|