mirror of
https://github.com/facebook/react.git
synced 2026-02-25 05:03:03 +00:00
1. Fixed all reported Flow errors 2. Added a few missing package declarations 3. Deleted ReactDebugHooks fork in favor of react-debug-tools
383 lines
9.3 KiB
JavaScript
383 lines
9.3 KiB
JavaScript
// @flow
|
|
|
|
import {
|
|
isElement,
|
|
typeOf,
|
|
AsyncMode,
|
|
ConcurrentMode,
|
|
ContextConsumer,
|
|
ContextProvider,
|
|
ForwardRef,
|
|
Fragment,
|
|
Lazy,
|
|
Memo,
|
|
Portal,
|
|
Profiler,
|
|
StrictMode,
|
|
Suspense,
|
|
} from 'react-is';
|
|
import { getDisplayName, getInObject, setInObject } from './utils';
|
|
|
|
export const meta = {
|
|
inspectable: Symbol('inspectable'),
|
|
inspected: Symbol('inspected'),
|
|
name: Symbol('name'),
|
|
readonly: Symbol('readonly'),
|
|
size: Symbol('size'),
|
|
type: Symbol('type'),
|
|
};
|
|
|
|
export type Dehydrated = {|
|
|
inspectable: boolean,
|
|
name: string | null,
|
|
readonly?: boolean,
|
|
size?: number,
|
|
type: string,
|
|
|};
|
|
|
|
// This threshold determines the depth at which the bridge "dehydrates" nested data.
|
|
// Dehydration means that we don't serialize the data for e.g. postMessage or stringify,
|
|
// unless the frontend explicitly requests it (e.g. a user clicks to expand a props object).
|
|
//
|
|
// Reducing this threshold will improve the speed of initial component inspection,
|
|
// but may decrease the responsiveness of expanding objects/arrays to inspect further.
|
|
const LEVEL_THRESHOLD = 2;
|
|
|
|
type PropType =
|
|
| 'array'
|
|
| 'array_buffer'
|
|
| 'boolean'
|
|
| 'data_view'
|
|
| 'date'
|
|
| 'function'
|
|
| 'html_element'
|
|
| 'infinity'
|
|
| 'iterator'
|
|
| 'nan'
|
|
| 'null'
|
|
| 'number'
|
|
| 'object'
|
|
| 'react_element'
|
|
| 'string'
|
|
| 'symbol'
|
|
| 'typed_array'
|
|
| 'undefined'
|
|
| 'unknown';
|
|
|
|
/**
|
|
* Get a enhanced/artificial type string based on the object instance
|
|
*/
|
|
function getDataType(data: Object): PropType {
|
|
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 '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 (Array.isArray(data)) {
|
|
return 'array';
|
|
} else if (ArrayBuffer.isView(data)) {
|
|
return data instanceof DataView ? 'data_view' : 'typed_array';
|
|
} else if (data instanceof ArrayBuffer) {
|
|
return 'array_buffer';
|
|
} else if (typeof data[Symbol.iterator] === 'function') {
|
|
return 'iterator';
|
|
} else if (Object.prototype.toString.call(data) === '[object Date]') {
|
|
return 'date';
|
|
}
|
|
return 'object';
|
|
case 'string':
|
|
return 'string';
|
|
case 'symbol':
|
|
return 'symbol';
|
|
default:
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate the dehydrated metadata for complex object instances
|
|
*/
|
|
function createDehydrated(
|
|
type: string,
|
|
inspectable: boolean,
|
|
data: Object,
|
|
cleaned: Array<Array<string | number>>,
|
|
path: Array<string | number>
|
|
): Dehydrated {
|
|
cleaned.push(path);
|
|
|
|
const dehydrated: Dehydrated = {
|
|
inspectable,
|
|
type,
|
|
name:
|
|
!data.constructor || data.constructor.name === 'Object'
|
|
? ''
|
|
: data.constructor.name,
|
|
};
|
|
|
|
if (type === 'array' || type === 'typed_array') {
|
|
dehydrated.size = data.length;
|
|
} else if (type === 'object') {
|
|
dehydrated.size = Object.keys(data).length;
|
|
}
|
|
|
|
if (type === 'iterator' || type === 'typed_array') {
|
|
dehydrated.readonly = true;
|
|
}
|
|
|
|
return dehydrated;
|
|
}
|
|
|
|
/**
|
|
* Strip out complex data (instances, functions, and data nested > LEVEL_THRESHOLD levels deep).
|
|
* The paths of the stripped out objects are appended to the `cleaned` list.
|
|
* On the other side of the barrier, the cleaned list is used to "re-hydrate" the cleaned representation into
|
|
* an object with symbols as attributes, so that a sanitized object can be distinguished from a normal object.
|
|
*
|
|
* Input: {"some": {"attr": fn()}, "other": AnInstance}
|
|
* Output: {
|
|
* "some": {
|
|
* "attr": {"name": the fn.name, type: "function"}
|
|
* },
|
|
* "other": {
|
|
* "name": "AnInstance",
|
|
* "type": "object",
|
|
* },
|
|
* }
|
|
* and cleaned = [["some", "attr"], ["other"]]
|
|
*/
|
|
export function dehydrate(
|
|
data: Object,
|
|
cleaned: Array<Array<string | number>>,
|
|
path: Array<string | number>,
|
|
isPathWhitelisted: (path: Array<string | number>) => boolean,
|
|
level?: number = 0
|
|
): string | Dehydrated | Array<Dehydrated> | { [key: string]: string | Dehydrated } {
|
|
const type = getDataType(data);
|
|
|
|
switch (type) {
|
|
case 'html_element':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
name: data.tagName,
|
|
type,
|
|
};
|
|
|
|
case 'function':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
name: data.name,
|
|
type,
|
|
};
|
|
|
|
case 'string':
|
|
return data.length <= 500 ? data : data.slice(0, 500) + '...';
|
|
|
|
case 'symbol':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
name: data.toString(),
|
|
type,
|
|
};
|
|
|
|
// React Elements aren't very inspector-friendly,
|
|
// and often contain private fields or circular references.
|
|
case 'react_element':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
name: getDisplayNameForReactElement(data),
|
|
type,
|
|
};
|
|
|
|
// ArrayBuffers error if you try to inspect them.
|
|
case 'array_buffer':
|
|
case 'data_view':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
name: type === 'data_view' ? 'DataView' : 'ArrayBuffer',
|
|
size: data.byteLength,
|
|
type,
|
|
};
|
|
|
|
case 'array':
|
|
const arrayPathCheck = isPathWhitelisted(path);
|
|
if (level >= LEVEL_THRESHOLD && !arrayPathCheck) {
|
|
return createDehydrated(type, true, data, cleaned, path);
|
|
}
|
|
return data.map((item, i) =>
|
|
dehydrate(
|
|
item,
|
|
cleaned,
|
|
path.concat([i]),
|
|
isPathWhitelisted,
|
|
arrayPathCheck ? 1 : level + 1
|
|
)
|
|
);
|
|
|
|
case 'typed_array':
|
|
case 'iterator':
|
|
return createDehydrated(type, false, data, cleaned, path);
|
|
|
|
case 'date':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
name: data.toString(),
|
|
type,
|
|
};
|
|
|
|
case 'object':
|
|
const objectPathCheck = isPathWhitelisted(path);
|
|
if (level >= LEVEL_THRESHOLD && !objectPathCheck) {
|
|
return createDehydrated(type, true, data, cleaned, path);
|
|
} else {
|
|
const object = {};
|
|
for (let name in data) {
|
|
object[name] = dehydrate(
|
|
data[name],
|
|
cleaned,
|
|
path.concat([name]),
|
|
isPathWhitelisted,
|
|
objectPathCheck ? 1 : level + 1
|
|
);
|
|
}
|
|
return object;
|
|
}
|
|
|
|
case 'infinity':
|
|
case 'nan':
|
|
case 'undefined':
|
|
// Some values are lossy when sent through a WebSocket.
|
|
// We dehydrate+rehydrate them to preserve their type.
|
|
cleaned.push(path);
|
|
return {
|
|
type,
|
|
};
|
|
|
|
default:
|
|
return data;
|
|
}
|
|
}
|
|
|
|
export function fillInPath(
|
|
object: Object,
|
|
path: Array<string | number>,
|
|
value: any
|
|
) {
|
|
const target = getInObject(object, path);
|
|
if (target != null) {
|
|
delete target[meta.inspectable];
|
|
delete target[meta.inspected];
|
|
delete target[meta.name];
|
|
delete target[meta.readonly];
|
|
delete target[meta.size];
|
|
delete target[meta.type];
|
|
}
|
|
setInObject(object, path, value);
|
|
}
|
|
|
|
export function hydrate(
|
|
object: Object,
|
|
cleaned: Array<Array<string | number>>
|
|
): Object {
|
|
cleaned.forEach((path: Array<string | number>) => {
|
|
const length = path.length;
|
|
const last = path[length - 1];
|
|
const parent = getInObject(object, path.slice(0, length - 1));
|
|
if (!parent || !parent.hasOwnProperty(last)) {
|
|
return;
|
|
}
|
|
|
|
const value = parent[last];
|
|
|
|
if (value.type === 'infinity') {
|
|
parent[last] = Infinity;
|
|
} else if (value.type === 'nan') {
|
|
parent[last] = NaN;
|
|
} else if (value.type === 'undefined') {
|
|
parent[last] = undefined;
|
|
} else {
|
|
// Replace the string keys with Symbols so they're non-enumerable.
|
|
const replaced: { [key: Symbol]: boolean | string } = {};
|
|
replaced[meta.inspectable] = !!value.inspectable;
|
|
replaced[meta.inspected] = false;
|
|
replaced[meta.name] = value.name;
|
|
replaced[meta.size] = value.size;
|
|
replaced[meta.readonly] = !!value.readonly;
|
|
replaced[meta.type] = value.type;
|
|
|
|
parent[last] = replaced;
|
|
}
|
|
});
|
|
return object;
|
|
}
|
|
|
|
export function getDisplayNameForReactElement(
|
|
element: React$Element<any>
|
|
): string | null {
|
|
const elementType = typeOf(element);
|
|
switch (elementType) {
|
|
case AsyncMode:
|
|
case ConcurrentMode:
|
|
return 'ConcurrentMode';
|
|
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';
|
|
default:
|
|
const { type } = element;
|
|
if (typeof type === 'string') {
|
|
return type;
|
|
} else if (type != null) {
|
|
return getDisplayName(type, 'Anonymous');
|
|
} else {
|
|
return 'Element';
|
|
}
|
|
}
|
|
}
|