mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
281 lines
6.9 KiB
JavaScript
281 lines
6.9 KiB
JavaScript
// @flow
|
||
|
||
import {
|
||
isElement,
|
||
typeOf,
|
||
AsyncMode,
|
||
ConcurrentMode,
|
||
ContextConsumer,
|
||
ContextProvider,
|
||
ForwardRef,
|
||
Fragment,
|
||
Lazy,
|
||
Memo,
|
||
Portal,
|
||
Profiler,
|
||
StrictMode,
|
||
Suspense,
|
||
} from 'react-is';
|
||
import { getDisplayName } from './utils';
|
||
|
||
export const meta = {
|
||
name: Symbol('name'),
|
||
type: Symbol('type'),
|
||
inspected: Symbol('inspected'),
|
||
meta: Symbol('meta'),
|
||
proto: Symbol('proto'),
|
||
};
|
||
|
||
// 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).
|
||
// We tried reducing this value from 2 to 1 to improve performance:
|
||
// https://github.com/facebook/react-devtools/issues/1200
|
||
// But this caused problems with the Profiler's interaction tracing output.
|
||
// Because React mutates Fibers, profiling data that is dehydrated for old commits–
|
||
// will not be available later from within the Profiler.
|
||
// This impacts props/state as well as Interactions.
|
||
// https://github.com/facebook/react-devtools/issues/1262
|
||
const LEVEL_THRESHOLD = 6;
|
||
|
||
/**
|
||
* Get a enhanced/artificial type string based on the object instance
|
||
*/
|
||
function getPropType(data: Object): string | null {
|
||
if (!data) {
|
||
return null;
|
||
}
|
||
|
||
if (isElement(data)) {
|
||
return 'react_element';
|
||
}
|
||
|
||
const type = typeof data;
|
||
if (type === 'object') {
|
||
if (Array.isArray(data)) {
|
||
return 'array';
|
||
}
|
||
if (ArrayBuffer.isView(data)) {
|
||
if (data instanceof DataView) {
|
||
return 'data_view';
|
||
}
|
||
return 'typed_array';
|
||
}
|
||
if (data instanceof ArrayBuffer) {
|
||
return 'array_buffer';
|
||
}
|
||
if (typeof data[Symbol.iterator] === 'function') {
|
||
return 'iterator';
|
||
}
|
||
if (Object.prototype.toString.call(data) === '[object Date]') {
|
||
return 'date';
|
||
}
|
||
}
|
||
|
||
return type;
|
||
}
|
||
|
||
/**
|
||
* Generate the dehydrated metadata for complex object instances
|
||
*/
|
||
function createDehydrated(
|
||
type: string,
|
||
data: Object,
|
||
cleaned: Array<Array<string>>,
|
||
path: Array<string>
|
||
): Object {
|
||
const meta = {};
|
||
|
||
if (type === 'array' || type === 'typed_array') {
|
||
meta.length = data.length;
|
||
}
|
||
if (type === 'iterator' || type === 'typed_array') {
|
||
meta.readOnly = true;
|
||
}
|
||
|
||
cleaned.push(path);
|
||
|
||
return {
|
||
type,
|
||
meta,
|
||
name:
|
||
!data.constructor || data.constructor.name === 'Object'
|
||
? ''
|
||
: data.constructor.name,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 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>>,
|
||
path?: Array<string> = [],
|
||
level?: number = 0
|
||
): string | Object {
|
||
const type = getPropType(data);
|
||
|
||
switch (type) {
|
||
case 'function':
|
||
cleaned.push(path);
|
||
return {
|
||
name: data.name,
|
||
type: 'function',
|
||
};
|
||
|
||
case 'string':
|
||
return data.length <= 500 ? data : data.slice(0, 500) + '...';
|
||
|
||
// We have to do this assignment b/c Flow doesn't think "symbol" is
|
||
// something typeof would return. Error 'unexpected predicate "symbol"'
|
||
case 'symbol':
|
||
cleaned.push(path);
|
||
return {
|
||
type: 'symbol',
|
||
name: data.toString(),
|
||
};
|
||
|
||
// React Elements aren't very inspector-friendly,
|
||
// and often contain private fields or circular references.
|
||
case 'react_element':
|
||
cleaned.push(path);
|
||
return {
|
||
name: getDisplayNameForReactElement(data),
|
||
type: 'react_element',
|
||
};
|
||
|
||
// ArrayBuffers error if you try to inspect them.
|
||
case 'array_buffer':
|
||
case 'data_view':
|
||
cleaned.push(path);
|
||
return {
|
||
type,
|
||
name: type === 'data_view' ? 'DataView' : 'ArrayBuffer',
|
||
meta: {
|
||
length: data.byteLength,
|
||
uninspectable: true,
|
||
},
|
||
};
|
||
|
||
case 'array':
|
||
if (level > LEVEL_THRESHOLD) {
|
||
return createDehydrated(type, data, cleaned, path);
|
||
}
|
||
return data.map((item, i) =>
|
||
dehydrate(item, cleaned, path.concat([i]), level + 1)
|
||
);
|
||
|
||
case 'typed_array':
|
||
case 'iterator':
|
||
return createDehydrated(type, data, cleaned, path);
|
||
case 'date':
|
||
cleaned.push(path);
|
||
return {
|
||
name: data.toString(),
|
||
type: 'date',
|
||
meta: {
|
||
uninspectable: true,
|
||
},
|
||
};
|
||
case 'object':
|
||
if (
|
||
level > LEVEL_THRESHOLD ||
|
||
(data.constructor &&
|
||
typeof data.constructor === 'function' &&
|
||
data.constructor.name !== 'Object')
|
||
) {
|
||
return createDehydrated(type, data, cleaned, path);
|
||
} else {
|
||
const res = {};
|
||
for (let name in data) {
|
||
res[name] = dehydrate(
|
||
data[name],
|
||
cleaned,
|
||
path.concat([name]),
|
||
level + 1
|
||
);
|
||
}
|
||
return res;
|
||
}
|
||
|
||
default:
|
||
return data;
|
||
}
|
||
}
|
||
|
||
export function hydrate(data: Object, cleaned: Array<Array<string>>): Object {
|
||
cleaned.forEach((path: Array<string>) => {
|
||
const last = path.pop();
|
||
const reduced: Object = path.reduce(
|
||
(object: Object, attr: string) => (object ? object[attr] : (null: any)),
|
||
data
|
||
);
|
||
if (!reduced || !reduced[last]) {
|
||
return;
|
||
}
|
||
const replace: { [key: Symbol]: boolean | string } = {};
|
||
replace[meta.name] = reduced[last].name;
|
||
replace[meta.type] = reduced[last].type;
|
||
replace[meta.meta] = reduced[last].meta;
|
||
replace[meta.inspected] = false;
|
||
reduced[last] = replace;
|
||
});
|
||
return data;
|
||
}
|
||
|
||
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, 'Unknown');
|
||
} else {
|
||
return 'Element';
|
||
}
|
||
}
|
||
}
|