mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
881 lines
23 KiB
JavaScript
881 lines
23 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 LRU from 'lru-cache';
|
|
import {
|
|
isElement,
|
|
typeOf,
|
|
ContextConsumer,
|
|
ContextProvider,
|
|
ForwardRef,
|
|
Fragment,
|
|
Lazy,
|
|
Memo,
|
|
Portal,
|
|
Profiler,
|
|
StrictMode,
|
|
Suspense,
|
|
} from 'react-is';
|
|
import {REACT_SUSPENSE_LIST_TYPE as SuspenseList} from 'shared/ReactSymbols';
|
|
import {
|
|
TREE_OPERATION_ADD,
|
|
TREE_OPERATION_REMOVE,
|
|
TREE_OPERATION_REMOVE_ROOT,
|
|
TREE_OPERATION_REORDER_CHILDREN,
|
|
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
|
|
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
|
} from './constants';
|
|
import {ElementTypeRoot} from 'react-devtools-shared/src/types';
|
|
import {
|
|
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
|
|
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
|
|
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
|
|
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
|
|
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
|
|
LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
|
|
} from './constants';
|
|
import {ComponentFilterElementType, ElementTypeHostComponent} from './types';
|
|
import {
|
|
ElementTypeClass,
|
|
ElementTypeForwardRef,
|
|
ElementTypeFunction,
|
|
ElementTypeMemo,
|
|
} from 'react-devtools-shared/src/types';
|
|
import {localStorageGetItem, localStorageSetItem} from './storage';
|
|
import {meta} from './hydration';
|
|
import isArray from './isArray';
|
|
|
|
import type {ComponentFilter, ElementType} from './types';
|
|
import type {LRUCache} from 'react-devtools-shared/src/types';
|
|
|
|
const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
|
|
|
|
// On large trees, encoding takes significant time.
|
|
// Try to reuse the already encoded strings.
|
|
const encodedStringCache: LRUCache<string, Array<number>> = new LRU({
|
|
max: 1000,
|
|
});
|
|
|
|
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<string | number | Symbol> {
|
|
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: 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;
|
|
}
|
|
|
|
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 {
|
|
// 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;
|
|
}
|
|
|
|
function surrogatePairToCodePoint(
|
|
charCode1: number,
|
|
charCode2: number,
|
|
): number {
|
|
return ((charCode1 & 0x3ff) << 10) + (charCode2 & 0x3ff) + 0x10000;
|
|
}
|
|
|
|
// Credit for this encoding approach goes to Tim Down:
|
|
// https://stackoverflow.com/questions/4877326/how-can-i-tell-if-a-string-contains-multibyte-characters-in-javascript
|
|
export function utfEncodeString(string: string): Array<number> {
|
|
const cached = encodedStringCache.get(string);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const encoded = [];
|
|
let i = 0;
|
|
let charCode;
|
|
while (i < string.length) {
|
|
charCode = string.charCodeAt(i);
|
|
// Handle multibyte unicode characters (like emoji).
|
|
if ((charCode & 0xf800) === 0xd800) {
|
|
encoded.push(surrogatePairToCodePoint(charCode, string.charCodeAt(++i)));
|
|
} else {
|
|
encoded.push(charCode);
|
|
}
|
|
++i;
|
|
}
|
|
|
|
encodedStringCache.set(string, encoded);
|
|
|
|
return encoded;
|
|
}
|
|
|
|
export function printOperationsArray(operations: Array<number>) {
|
|
// The first two values are always rendererID and rootID
|
|
const rendererID = operations[0];
|
|
const rootID = operations[1];
|
|
|
|
const logs = [`operations for renderer:${rendererID} and root:${rootID}`];
|
|
|
|
let i = 2;
|
|
|
|
// Reassemble the string table.
|
|
const stringTable = [
|
|
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_REMOVE_ROOT: {
|
|
i += 1;
|
|
|
|
logs.push(`Remove root ${rootID}`);
|
|
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;
|
|
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 getBreakOnConsoleErrors(): boolean {
|
|
try {
|
|
const raw = localStorageGetItem(
|
|
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
|
|
);
|
|
if (raw != null) {
|
|
return JSON.parse(raw);
|
|
}
|
|
} catch (error) {}
|
|
return false;
|
|
}
|
|
|
|
export function setBreakOnConsoleErrors(value: boolean): void {
|
|
localStorageSetItem(
|
|
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
|
|
JSON.stringify(value),
|
|
);
|
|
}
|
|
|
|
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),
|
|
);
|
|
}
|
|
|
|
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),
|
|
);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
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 (const attribute in prev) {
|
|
if (!(attribute in next)) {
|
|
return true;
|
|
}
|
|
}
|
|
for (const attribute in next) {
|
|
if (prev[attribute] !== next[attribute]) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function getInObject(object: Object, path: Array<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 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) {
|
|
if (isArray(parent)) {
|
|
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];
|
|
if (isArray(parent)) {
|
|
parent.splice(((lastOld: any): number), 1);
|
|
} else {
|
|
delete parent[lastOld];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
export type DataType =
|
|
| 'array'
|
|
| 'array_buffer'
|
|
| 'bigint'
|
|
| 'boolean'
|
|
| 'data_view'
|
|
| 'date'
|
|
| 'function'
|
|
| 'html_all_collection'
|
|
| 'html_element'
|
|
| 'infinity'
|
|
| 'iterator'
|
|
| 'opaque_iterator'
|
|
| 'nan'
|
|
| 'null'
|
|
| 'number'
|
|
| 'object'
|
|
| 'react_element'
|
|
| 'regexp'
|
|
| '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':
|
|
if (isArray(data)) {
|
|
return 'array';
|
|
} else if (ArrayBuffer.isView(data)) {
|
|
return hasOwnProperty.call(data.constructor, 'BYTES_PER_ELEMENT')
|
|
? 'typed_array'
|
|
: 'data_view';
|
|
} else if (data.constructor && data.constructor.name === 'ArrayBuffer') {
|
|
// HACK This ArrayBuffer check is gross; is there a better way?
|
|
// We could try to create a new DataView with the value.
|
|
// If it doesn't error, we know it's an ArrayBuffer,
|
|
// but this seems kind of awkward and expensive.
|
|
return 'array_buffer';
|
|
} else if (typeof data[Symbol.iterator] === 'function') {
|
|
const iterator = data[Symbol.iterator]();
|
|
if (!iterator) {
|
|
// Proxies might break assumptoins about iterators.
|
|
// See github.com/facebook/react/issues/21654
|
|
} else {
|
|
return iterator === data ? 'opaque_iterator' : 'iterator';
|
|
}
|
|
} else if (data.constructor && data.constructor.name === 'RegExp') {
|
|
return 'regexp';
|
|
} else {
|
|
const toStringValue = Object.prototype.toString.call(data);
|
|
if (toStringValue === '[object Date]') {
|
|
return 'date';
|
|
} else if (toStringValue === '[object HTMLAllCollection]') {
|
|
return 'html_all_collection';
|
|
}
|
|
}
|
|
return 'object';
|
|
case 'string':
|
|
return 'string';
|
|
case 'symbol':
|
|
return 'symbol';
|
|
case 'undefined':
|
|
if (
|
|
Object.prototype.toString.call(data) === '[object HTMLAllCollection]'
|
|
) {
|
|
return 'html_all_collection';
|
|
}
|
|
return 'undefined';
|
|
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';
|
|
case SuspenseList:
|
|
return 'SuspenseList';
|
|
default:
|
|
const {type} = element;
|
|
if (typeof type === 'string') {
|
|
return type;
|
|
} else if (typeof type === 'function') {
|
|
return getDisplayName(type, 'Anonymous');
|
|
} else if (type != null) {
|
|
return 'NotImplementedInDevtools';
|
|
} else {
|
|
return 'Element';
|
|
}
|
|
}
|
|
}
|
|
|
|
const MAX_PREVIEW_STRING_LENGTH = 50;
|
|
|
|
function truncateForDisplay(
|
|
string: string,
|
|
length: number = MAX_PREVIEW_STRING_LENGTH,
|
|
) {
|
|
if (string.length > length) {
|
|
return string.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 {
|
|
if (data != null && hasOwnProperty.call(data, meta.type)) {
|
|
return showFormattedValue
|
|
? data[meta.preview_long]
|
|
: data[meta.preview_short];
|
|
}
|
|
|
|
const type = getDataType(data);
|
|
|
|
switch (type) {
|
|
case 'html_element':
|
|
return `<${truncateForDisplay(data.tagName.toLowerCase())} />`;
|
|
case 'function':
|
|
return truncateForDisplay(
|
|
`ƒ ${typeof data.name === 'function' ? '' : data.name}() {}`,
|
|
);
|
|
case 'string':
|
|
return `"${data}"`;
|
|
case 'bigint':
|
|
return truncateForDisplay(data.toString() + 'n');
|
|
case 'regexp':
|
|
return truncateForDisplay(data.toString());
|
|
case 'symbol':
|
|
return truncateForDisplay(data.toString());
|
|
case 'react_element':
|
|
return `<${truncateForDisplay(
|
|
getDisplayNameForReactElement(data) || 'Unknown',
|
|
)} />`;
|
|
case 'array_buffer':
|
|
return `ArrayBuffer(${data.byteLength})`;
|
|
case 'data_view':
|
|
return `DataView(${data.buffer.byteLength})`;
|
|
case 'array':
|
|
if (showFormattedValue) {
|
|
let formatted = '';
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (i > 0) {
|
|
formatted += ', ';
|
|
}
|
|
formatted += formatDataForPreview(data[i], false);
|
|
if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
|
|
// Prevent doing a lot of unnecessary iteration...
|
|
break;
|
|
}
|
|
}
|
|
return `[${truncateForDisplay(formatted)}]`;
|
|
} else {
|
|
const length = hasOwnProperty.call(data, meta.size)
|
|
? data[meta.size]
|
|
: data.length;
|
|
return `Array(${length})`;
|
|
}
|
|
case 'typed_array':
|
|
const shortName = `${data.constructor.name}(${data.length})`;
|
|
if (showFormattedValue) {
|
|
let formatted = '';
|
|
for (let i = 0; i < data.length; i++) {
|
|
if (i > 0) {
|
|
formatted += ', ';
|
|
}
|
|
formatted += data[i];
|
|
if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
|
|
// Prevent doing a lot of unnecessary iteration...
|
|
break;
|
|
}
|
|
}
|
|
return `${shortName} [${truncateForDisplay(formatted)}]`;
|
|
} else {
|
|
return shortName;
|
|
}
|
|
case 'iterator':
|
|
const name = data.constructor.name;
|
|
|
|
if (showFormattedValue) {
|
|
// TRICKY
|
|
// Don't use [...spread] syntax for this purpose.
|
|
// This project uses @babel/plugin-transform-spread in "loose" mode which only works with Array values.
|
|
// Other types (e.g. typed arrays, Sets) will not spread correctly.
|
|
const array = Array.from(data);
|
|
|
|
let formatted = '';
|
|
for (let i = 0; i < array.length; i++) {
|
|
const entryOrEntries = array[i];
|
|
|
|
if (i > 0) {
|
|
formatted += ', ';
|
|
}
|
|
|
|
// TRICKY
|
|
// Browsers display Maps and Sets differently.
|
|
// To mimic their behavior, detect if we've been given an entries tuple.
|
|
// Map(2) {"abc" => 123, "def" => 123}
|
|
// Set(2) {"abc", 123}
|
|
if (isArray(entryOrEntries)) {
|
|
const key = formatDataForPreview(entryOrEntries[0], true);
|
|
const value = formatDataForPreview(entryOrEntries[1], false);
|
|
formatted += `${key} => ${value}`;
|
|
} else {
|
|
formatted += formatDataForPreview(entryOrEntries, false);
|
|
}
|
|
|
|
if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
|
|
// Prevent doing a lot of unnecessary iteration...
|
|
break;
|
|
}
|
|
}
|
|
|
|
return `${name}(${data.size}) {${truncateForDisplay(formatted)}}`;
|
|
} else {
|
|
return `${name}(${data.size})`;
|
|
}
|
|
case 'opaque_iterator': {
|
|
return data[Symbol.toStringTag];
|
|
}
|
|
case 'date':
|
|
return data.toString();
|
|
case 'object':
|
|
if (showFormattedValue) {
|
|
const keys = Array.from(getAllEnumerableKeys(data)).sort(alphaSortKeys);
|
|
|
|
let formatted = '';
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
if (i > 0) {
|
|
formatted += ', ';
|
|
}
|
|
formatted += `${key.toString()}: ${formatDataForPreview(
|
|
data[key],
|
|
false,
|
|
)}`;
|
|
if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
|
|
// Prevent doing a lot of unnecessary iteration...
|
|
break;
|
|
}
|
|
}
|
|
return `{${truncateForDisplay(formatted)}}`;
|
|
} else {
|
|
return '{…}';
|
|
}
|
|
case 'boolean':
|
|
case 'number':
|
|
case 'infinity':
|
|
case 'nan':
|
|
case 'null':
|
|
case 'undefined':
|
|
return data;
|
|
default:
|
|
try {
|
|
return truncateForDisplay(String(data));
|
|
} catch (error) {
|
|
return 'unserializable';
|
|
}
|
|
}
|
|
}
|