mirror of
https://github.com/facebook/react.git
synced 2026-02-26 03:35:25 +00:00
There are not so many changes, most of them are changing imports,
because I've moved types for UI in a single file.
In https://github.com/facebook/react/pull/27357 I've added support for
pausing polling events: when user inspects an element, we start polling
React DevTools backend for updates in props / state. If user switches
tabs, extension's service worker can be killed by browser and this
polling will start spamming errors.
What I've missed is that we also have a separate call for this API, but
which is executed only once when user selects an element. We don't
handle promise rejection here and this can lead to some errors when user
selects an element and switches tabs right after it.
The only change here is that this API now has
`shouldListenToPauseEvents` param, which is `true` for polling, so we
will pause polling once user switches tabs. It is `false` by default, so
we won't pause initial call by accident.
af8beeebf6/packages/react-devtools-shared/src/backendAPI.js (L96)
525 lines
14 KiB
JavaScript
525 lines
14 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {
|
|
getDataType,
|
|
getDisplayNameForReactElement,
|
|
getAllEnumerableKeys,
|
|
getInObject,
|
|
formatDataForPreview,
|
|
setInObject,
|
|
} from 'react-devtools-shared/src/utils';
|
|
|
|
import type {
|
|
DehydratedData,
|
|
InspectedElementPath,
|
|
} from 'react-devtools-shared/src/frontend/types';
|
|
|
|
export const meta = {
|
|
inspectable: (Symbol('inspectable'): symbol),
|
|
inspected: (Symbol('inspected'): symbol),
|
|
name: (Symbol('name'): symbol),
|
|
preview_long: (Symbol('preview_long'): symbol),
|
|
preview_short: (Symbol('preview_short'): symbol),
|
|
readonly: (Symbol('readonly'): symbol),
|
|
size: (Symbol('size'): symbol),
|
|
type: (Symbol('type'): symbol),
|
|
unserializable: (Symbol('unserializable'): symbol),
|
|
};
|
|
|
|
export type Dehydrated = {
|
|
inspectable: boolean,
|
|
name: string | null,
|
|
preview_long: string | null,
|
|
preview_short: string | null,
|
|
readonly?: boolean,
|
|
size?: number,
|
|
type: string,
|
|
};
|
|
|
|
// Typed arrays and other complex iteratable objects (e.g. Map, Set, ImmutableJS) need special handling.
|
|
// These objects can't be serialized without losing type information,
|
|
// so a "Unserializable" type wrapper is used (with meta-data keys) to send nested values-
|
|
// while preserving the original type and name.
|
|
export type Unserializable = {
|
|
name: string | null,
|
|
preview_long: string | null,
|
|
preview_short: string | null,
|
|
readonly?: boolean,
|
|
size?: number,
|
|
type: string,
|
|
unserializable: boolean,
|
|
[string | number]: any,
|
|
};
|
|
|
|
// 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;
|
|
|
|
/**
|
|
* 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,
|
|
preview_long: formatDataForPreview(data, true),
|
|
preview_short: formatDataForPreview(data, false),
|
|
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>>,
|
|
unserializable: Array<Array<string | number>>,
|
|
path: Array<string | number>,
|
|
isPathAllowed: (path: Array<string | number>) => boolean,
|
|
level: number = 0,
|
|
): $PropertyType<DehydratedData, 'data'> {
|
|
const type = getDataType(data);
|
|
|
|
let isPathAllowedCheck;
|
|
|
|
switch (type) {
|
|
case 'html_element':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name: data.tagName,
|
|
type,
|
|
};
|
|
|
|
case 'function':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name:
|
|
typeof data.name === 'function' || !data.name
|
|
? 'function'
|
|
: data.name,
|
|
type,
|
|
};
|
|
|
|
case 'string':
|
|
isPathAllowedCheck = isPathAllowed(path);
|
|
if (isPathAllowedCheck) {
|
|
return data;
|
|
} else {
|
|
return data.length <= 500 ? data : data.slice(0, 500) + '...';
|
|
}
|
|
|
|
case 'bigint':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name: data.toString(),
|
|
type,
|
|
};
|
|
|
|
case 'symbol':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
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,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name: getDisplayNameForReactElement(data) || 'Unknown',
|
|
type,
|
|
};
|
|
|
|
// ArrayBuffers error if you try to inspect them.
|
|
case 'array_buffer':
|
|
case 'data_view':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name: type === 'data_view' ? 'DataView' : 'ArrayBuffer',
|
|
size: data.byteLength,
|
|
type,
|
|
};
|
|
|
|
case 'array':
|
|
isPathAllowedCheck = isPathAllowed(path);
|
|
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
|
|
return createDehydrated(type, true, data, cleaned, path);
|
|
}
|
|
return data.map((item, i) =>
|
|
dehydrate(
|
|
item,
|
|
cleaned,
|
|
unserializable,
|
|
path.concat([i]),
|
|
isPathAllowed,
|
|
isPathAllowedCheck ? 1 : level + 1,
|
|
),
|
|
);
|
|
|
|
case 'html_all_collection':
|
|
case 'typed_array':
|
|
case 'iterator':
|
|
isPathAllowedCheck = isPathAllowed(path);
|
|
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
|
|
return createDehydrated(type, true, data, cleaned, path);
|
|
} else {
|
|
const unserializableValue: Unserializable = {
|
|
unserializable: true,
|
|
type: type,
|
|
readonly: true,
|
|
size: type === 'typed_array' ? data.length : undefined,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name:
|
|
!data.constructor || data.constructor.name === 'Object'
|
|
? ''
|
|
: data.constructor.name,
|
|
};
|
|
|
|
// 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.
|
|
Array.from(data).forEach(
|
|
(item, i) =>
|
|
(unserializableValue[i] = dehydrate(
|
|
item,
|
|
cleaned,
|
|
unserializable,
|
|
path.concat([i]),
|
|
isPathAllowed,
|
|
isPathAllowedCheck ? 1 : level + 1,
|
|
)),
|
|
);
|
|
|
|
unserializable.push(path);
|
|
|
|
return unserializableValue;
|
|
}
|
|
|
|
case 'opaque_iterator':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name: data[Symbol.toStringTag],
|
|
type,
|
|
};
|
|
|
|
case 'date':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name: data.toString(),
|
|
type,
|
|
};
|
|
|
|
case 'regexp':
|
|
cleaned.push(path);
|
|
return {
|
|
inspectable: false,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name: data.toString(),
|
|
type,
|
|
};
|
|
|
|
case 'object':
|
|
isPathAllowedCheck = isPathAllowed(path);
|
|
|
|
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
|
|
return createDehydrated(type, true, data, cleaned, path);
|
|
} else {
|
|
const object: {
|
|
[string]: $PropertyType<DehydratedData, 'data'>,
|
|
} = {};
|
|
getAllEnumerableKeys(data).forEach(key => {
|
|
const name = key.toString();
|
|
object[name] = dehydrate(
|
|
data[key],
|
|
cleaned,
|
|
unserializable,
|
|
path.concat([name]),
|
|
isPathAllowed,
|
|
isPathAllowedCheck ? 1 : level + 1,
|
|
);
|
|
});
|
|
return object;
|
|
}
|
|
|
|
case 'class_instance':
|
|
isPathAllowedCheck = isPathAllowed(path);
|
|
|
|
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
|
|
return createDehydrated(type, true, data, cleaned, path);
|
|
}
|
|
|
|
const value: Unserializable = {
|
|
unserializable: true,
|
|
type,
|
|
readonly: true,
|
|
preview_short: formatDataForPreview(data, false),
|
|
preview_long: formatDataForPreview(data, true),
|
|
name: data.constructor.name,
|
|
};
|
|
|
|
getAllEnumerableKeys(data).forEach(key => {
|
|
const keyAsString = key.toString();
|
|
|
|
value[keyAsString] = dehydrate(
|
|
data[key],
|
|
cleaned,
|
|
unserializable,
|
|
path.concat([keyAsString]),
|
|
isPathAllowed,
|
|
isPathAllowedCheck ? 1 : level + 1,
|
|
);
|
|
});
|
|
|
|
unserializable.push(path);
|
|
|
|
return value;
|
|
|
|
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,
|
|
data: DehydratedData,
|
|
path: InspectedElementPath,
|
|
value: any,
|
|
) {
|
|
const target = getInObject(object, path);
|
|
if (target != null) {
|
|
if (!target[meta.unserializable]) {
|
|
delete target[meta.inspectable];
|
|
delete target[meta.inspected];
|
|
delete target[meta.name];
|
|
delete target[meta.preview_long];
|
|
delete target[meta.preview_short];
|
|
delete target[meta.readonly];
|
|
delete target[meta.size];
|
|
delete target[meta.type];
|
|
}
|
|
}
|
|
|
|
if (value !== null && data.unserializable.length > 0) {
|
|
const unserializablePath = data.unserializable[0];
|
|
let isMatch = unserializablePath.length === path.length;
|
|
for (let i = 0; i < path.length; i++) {
|
|
if (path[i] !== unserializablePath[i]) {
|
|
isMatch = false;
|
|
break;
|
|
}
|
|
}
|
|
if (isMatch) {
|
|
upgradeUnserializable(value, value);
|
|
}
|
|
}
|
|
|
|
setInObject(object, path, value);
|
|
}
|
|
|
|
export function hydrate(
|
|
object: any,
|
|
cleaned: Array<Array<string | number>>,
|
|
unserializable: 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) {
|
|
return;
|
|
} else 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.preview_long] = value.preview_long;
|
|
replaced[meta.preview_short] = value.preview_short;
|
|
replaced[meta.size] = value.size;
|
|
replaced[meta.readonly] = !!value.readonly;
|
|
replaced[meta.type] = value.type;
|
|
|
|
parent[last] = replaced;
|
|
}
|
|
});
|
|
unserializable.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 node = parent[last];
|
|
|
|
const replacement = {
|
|
...node,
|
|
};
|
|
|
|
upgradeUnserializable(replacement, node);
|
|
|
|
parent[last] = replacement;
|
|
});
|
|
return object;
|
|
}
|
|
|
|
function upgradeUnserializable(destination: Object, source: Object) {
|
|
Object.defineProperties(destination, {
|
|
// $FlowFixMe[invalid-computed-prop]
|
|
[meta.inspected]: {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: !!source.inspected,
|
|
},
|
|
// $FlowFixMe[invalid-computed-prop]
|
|
[meta.name]: {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: source.name,
|
|
},
|
|
// $FlowFixMe[invalid-computed-prop]
|
|
[meta.preview_long]: {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: source.preview_long,
|
|
},
|
|
// $FlowFixMe[invalid-computed-prop]
|
|
[meta.preview_short]: {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: source.preview_short,
|
|
},
|
|
// $FlowFixMe[invalid-computed-prop]
|
|
[meta.size]: {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: source.size,
|
|
},
|
|
// $FlowFixMe[invalid-computed-prop]
|
|
[meta.readonly]: {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: !!source.readonly,
|
|
},
|
|
// $FlowFixMe[invalid-computed-prop]
|
|
[meta.type]: {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: source.type,
|
|
},
|
|
// $FlowFixMe[invalid-computed-prop]
|
|
[meta.unserializable]: {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: !!source.unserializable,
|
|
},
|
|
});
|
|
|
|
delete destination.inspected;
|
|
delete destination.name;
|
|
delete destination.preview_long;
|
|
delete destination.preview_short;
|
|
delete destination.size;
|
|
delete destination.readonly;
|
|
delete destination.type;
|
|
delete destination.unserializable;
|
|
}
|