Added support for unserializable types (e.g. Set/Map, Immutable)

This commit is contained in:
Brian Vaughn
2019-08-17 10:20:26 -07:00
parent f66a20f439
commit ce65df7092
13 changed files with 558 additions and 124 deletions
@@ -1,10 +1,10 @@
// @flow
import React, { Fragment } from 'react';
import UnserializableProps from './UnserializableProps';
import Contexts from './Contexts';
import CustomHooks from './CustomHooks';
import CustomObject from './CustomObject';
import MapAndSet from './MapAndSet';
import NestedProps from './NestedProps';
import SimpleValues from './SimpleValues';
@@ -15,7 +15,7 @@ export default function InspectableElements() {
<Fragment>
<h1>Inspectable elements</h1>
<SimpleValues />
<MapAndSet />
<UnserializableProps />
<NestedProps />
<Contexts />
<CustomHooks />
@@ -1,19 +0,0 @@
// @flow
import React from 'react';
const set = new Set();
set.add('abc');
set.add(123);
const map = new Map();
map.set('name', 'Brian');
map.set('food', 'sushi');
export default function MapAndSet() {
return <ChildComponent map={map} set={set} />;
}
function ChildComponent(props: any) {
return null;
}
@@ -0,0 +1,35 @@
// @flow
import React from 'react';
import Immutable from 'immutable';
const set = new Set(['abc', 123]);
const map = new Map([['name', 'Brian'], ['food', 'sushi']]);
const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
const mapOfMaps = new Map([['first', map], ['second', map]]);
const typedArray = Int8Array.from([100, -100, 0]);
const immutable = Immutable.fromJS({
a: [{ hello: 'there' }, 'fixed', true],
b: 123,
c: {
'1': 'xyz',
xyz: 1,
},
});
export default function UnserializableProps() {
return (
<ChildComponent
map={map}
set={set}
mapOfMaps={mapOfMaps}
setOfSets={setOfSets}
typedArray={typedArray}
immutable={immutable}
/>
);
}
function ChildComponent(props: any) {
return null;
}
@@ -1,5 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InspectedElementContext should dehydrate complex nested values when requested: 1: Initially inspect element 1`] = `
{
"id": 2,
"owners": null,
"context": null,
"hooks": null,
"props": {
"set_of_sets": {
"0": {},
"1": {}
}
},
"state": null
}
`;
exports[`InspectedElementContext should dehydrate complex nested values when requested: 2: Inspect props.set_of_sets.0 1`] = `
{
"id": 2,
"owners": null,
"context": null,
"hooks": null,
"props": {
"set_of_sets": {
"0": {
"0": 1,
"1": 2,
"2": 3
},
"1": {}
}
},
"state": null
}
`;
exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
{
"id": 2,
@@ -449,13 +485,38 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
"context": null,
"hooks": null,
"props": {
"html_element": {},
"fn": {},
"symbol": {},
"react_element": {},
"array_buffer": {},
"typed_array": {},
"date": {}
"date": {},
"fn": {},
"html_element": {},
"immutable": {
"0": {},
"1": {},
"2": {}
},
"map": {
"0": {},
"1": {}
},
"map_of_maps": {
"0": {},
"1": {}
},
"react_element": {},
"set": {
"0": "abc",
"1": 123
},
"set_of_sets": {
"0": {},
"1": {}
},
"symbol": {},
"typed_array": {
"0": 100,
"1": -100,
"2": 0
}
},
"state": null
}
+143 -28
View File
@@ -390,23 +390,42 @@ describe('InspectedElementContext', () => {
});
it('should support complex data types', async done => {
const Immutable = require('immutable');
const Example = () => null;
const div = document.createElement('div');
const exmapleFunction = () => {};
const typedArray = new Uint8Array(3);
const exampleFunction = () => {};
const setShallow = new Set(['abc', 123]);
const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
const typedArray = Int8Array.from([100, -100, 0]);
const immutableMap = Immutable.fromJS({
a: [{ hello: 'there' }, 'fixed', true],
b: 123,
c: {
'1': 'xyz',
xyz: 1,
},
});
const container = document.createElement('div');
await utils.actAsync(() =>
ReactDOM.render(
<Example
html_element={div}
fn={exmapleFunction}
symbol={Symbol('symbol')}
react_element={<span />}
array_buffer={typedArray.buffer}
typed_array={typedArray}
date={new Date()}
fn={exampleFunction}
html_element={div}
immutable={immutableMap}
map={mapShallow}
map_of_maps={mapOfMaps}
react_element={<span />}
set={setShallow}
set_of_sets={setOfSets}
symbol={Symbol('symbol')}
typed_array={typedArray}
/>,
container
)
@@ -441,37 +460,77 @@ describe('InspectedElementContext', () => {
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
const {
html_element,
fn,
symbol,
react_element,
array_buffer,
typed_array,
date,
fn,
html_element,
immutable,
map,
map_of_maps,
react_element,
set,
set_of_sets,
symbol,
typed_array,
} = (inspectedElement: any).props;
expect(html_element[meta.inspectable]).toBe(false);
expect(html_element[meta.name]).toBe('DIV');
expect(html_element[meta.type]).toBe('html_element');
expect(fn[meta.inspectable]).toBe(false);
expect(fn[meta.name]).toBe('exmapleFunction');
expect(fn[meta.type]).toBe('function');
expect(symbol[meta.inspectable]).toBe(false);
expect(symbol[meta.name]).toBe('Symbol(symbol)');
expect(symbol[meta.type]).toBe('symbol');
expect(react_element[meta.inspectable]).toBe(false);
expect(react_element[meta.name]).toBe('span');
expect(react_element[meta.type]).toBe('react_element');
expect(array_buffer[meta.size]).toBe(3);
expect(array_buffer[meta.inspectable]).toBe(false);
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
expect(array_buffer[meta.type]).toBe('array_buffer');
expect(typed_array[meta.size]).toBe(3);
expect(typed_array[meta.inspectable]).toBe(false);
expect(typed_array[meta.name]).toBe('Uint8Array');
expect(typed_array[meta.type]).toBe('typed_array');
expect(date[meta.inspectable]).toBe(false);
expect(date[meta.type]).toBe('date');
expect(fn[meta.inspectable]).toBe(false);
expect(fn[meta.name]).toBe('exampleFunction');
expect(fn[meta.type]).toBe('function');
expect(html_element[meta.inspectable]).toBe(false);
expect(html_element[meta.name]).toBe('DIV');
expect(html_element[meta.type]).toBe('html_element');
expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type
expect(immutable[meta.name]).toBe('Map');
expect(immutable[meta.type]).toBe('iterator');
expect(map[meta.inspectable]).toBeUndefined(); // Complex type
expect(map[meta.name]).toBe('Map');
expect(map[meta.type]).toBe('iterator');
expect(map[0][meta.type]).toBe('array');
expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type
expect(map_of_maps[meta.name]).toBe('Map');
expect(map_of_maps[meta.type]).toBe('iterator');
expect(map_of_maps[0][meta.type]).toBe('array');
expect(react_element[meta.inspectable]).toBe(false);
expect(react_element[meta.name]).toBe('span');
expect(react_element[meta.type]).toBe('react_element');
expect(set[meta.inspectable]).toBeUndefined(); // Complex type
expect(set[meta.name]).toBe('Set');
expect(set[meta.type]).toBe('iterator');
expect(set[0]).toBe('abc');
expect(set[1]).toBe(123);
expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type
expect(set_of_sets[meta.name]).toBe('Set');
expect(set_of_sets[meta.type]).toBe('iterator');
expect(set_of_sets['0'][meta.inspectable]).toBe(true);
expect(symbol[meta.inspectable]).toBe(false);
expect(symbol[meta.name]).toBe('Symbol(symbol)');
expect(symbol[meta.type]).toBe('symbol');
expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type
expect(typed_array[meta.size]).toBe(3);
expect(typed_array[meta.name]).toBe('Int8Array');
expect(typed_array[meta.type]).toBe('typed_array');
expect(typed_array[0]).toBe(100);
expect(typed_array[1]).toBe(-100);
expect(typed_array[2]).toBe(0);
done();
});
@@ -663,6 +722,62 @@ describe('InspectedElementContext', () => {
done();
});
it('should dehydrate complex nested values when requested', async done => {
const Example = () => null;
const container = document.createElement('div');
await utils.actAsync(() =>
ReactDOM.render(
<Example
set_of_sets={new Set([new Set([1, 2, 3]), new Set(['a', 'b', 'c'])])}
/>,
container
)
);
const id = ((store.getElementIDAtIndex(0): any): number);
let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
let inspectedElement = null;
function Suspender({ target }) {
const context = React.useContext(InspectedElementContext);
getInspectedElementPath = context.getInspectedElementPath;
inspectedElement = context.getInspectedElement(target);
return null;
}
await utils.actAsync(
() =>
TestRenderer.create(
<Contexts
defaultSelectedElementID={id}
defaultSelectedElementIndex={0}
>
<React.Suspense fallback={null}>
<Suspender target={id} />
</React.Suspense>
</Contexts>
),
false
);
expect(getInspectedElementPath).not.toBeNull();
expect(inspectedElement).not.toBeNull();
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
inspectedElement = null;
TestUtils.act(() => {
TestRenderer.act(() => {
getInspectedElementPath(id, ['props', 'set_of_sets', 0]);
jest.runOnlyPendingTimers();
});
});
expect(inspectedElement).not.toBeNull();
expect(inspectedElement).toMatchSnapshot('2: Inspect props.set_of_sets.0');
done();
});
it('should include updates for nested values that were previously hydrated', async done => {
const Example = () => null;
@@ -126,13 +126,38 @@ Object {
"context": {},
"hooks": null,
"props": {
"html_element": {},
"fn": {},
"symbol": {},
"react_element": {},
"array_buffer": {},
"typed_array": {},
"date": {}
"date": {},
"fn": {},
"html_element": {},
"immutable": {
"0": {},
"1": {},
"2": {}
},
"map": {
"0": {},
"1": {}
},
"map_of_maps": {
"0": {},
"1": {}
},
"react_element": {},
"set": {
"0": "abc",
"1": 123
},
"set_of_sets": {
"0": {},
"1": {}
},
"symbol": {},
"typed_array": {
"0": 100,
"1": -100,
"2": 0
}
},
"state": null
},
+92 -29
View File
@@ -23,7 +23,11 @@ describe('InspectedElementContext', () => {
dehydratedData: DehydratedData | null
): Object | null {
if (dehydratedData !== null) {
return hydrate(dehydratedData.data, dehydratedData.cleaned);
return hydrate(
dehydratedData.data,
dehydratedData.cleaned,
dehydratedData.unserializable
);
} else {
return null;
}
@@ -132,22 +136,41 @@ describe('InspectedElementContext', () => {
});
it('should support complex data types', async done => {
const Immutable = require('immutable');
const Example = () => null;
const div = document.createElement('div');
const exmapleFunction = () => {};
const typedArray = new Uint8Array(3);
const exampleFunction = () => {};
const setShallow = new Set(['abc', 123]);
const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
const typedArray = Int8Array.from([100, -100, 0]);
const immutableMap = Immutable.fromJS({
a: [{ hello: 'there' }, 'fixed', true],
b: 123,
c: {
'1': 'xyz',
xyz: 1,
},
});
act(() =>
ReactDOM.render(
<Example
html_element={div}
fn={exmapleFunction}
symbol={Symbol('symbol')}
react_element={<span />}
array_buffer={typedArray.buffer}
typed_array={typedArray}
date={new Date()}
fn={exampleFunction}
html_element={div}
immutable={immutableMap}
map={mapShallow}
map_of_maps={mapOfMaps}
react_element={<span />}
set={setShallow}
set_of_sets={setOfSets}
symbol={Symbol('symbol')}
typed_array={typedArray}
/>,
document.createElement('div')
)
@@ -159,37 +182,77 @@ describe('InspectedElementContext', () => {
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
const {
html_element,
fn,
symbol,
react_element,
array_buffer,
typed_array,
date,
fn,
html_element,
immutable,
map,
map_of_maps,
react_element,
set,
set_of_sets,
symbol,
typed_array,
} = inspectedElement.value.props;
expect(html_element[meta.inspectable]).toBe(false);
expect(html_element[meta.name]).toBe('DIV');
expect(html_element[meta.type]).toBe('html_element');
expect(fn[meta.inspectable]).toBe(false);
expect(fn[meta.name]).toBe('exmapleFunction');
expect(fn[meta.type]).toBe('function');
expect(symbol[meta.inspectable]).toBe(false);
expect(symbol[meta.name]).toBe('Symbol(symbol)');
expect(symbol[meta.type]).toBe('symbol');
expect(react_element[meta.inspectable]).toBe(false);
expect(react_element[meta.name]).toBe('span');
expect(react_element[meta.type]).toBe('react_element');
expect(array_buffer[meta.size]).toBe(3);
expect(array_buffer[meta.inspectable]).toBe(false);
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
expect(array_buffer[meta.type]).toBe('array_buffer');
expect(typed_array[meta.size]).toBe(3);
expect(typed_array[meta.inspectable]).toBe(false);
expect(typed_array[meta.name]).toBe('Uint8Array');
expect(typed_array[meta.type]).toBe('typed_array');
expect(date[meta.inspectable]).toBe(false);
expect(date[meta.type]).toBe('date');
expect(fn[meta.inspectable]).toBe(false);
expect(fn[meta.name]).toBe('exampleFunction');
expect(fn[meta.type]).toBe('function');
expect(html_element[meta.inspectable]).toBe(false);
expect(html_element[meta.name]).toBe('DIV');
expect(html_element[meta.type]).toBe('html_element');
expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type
expect(immutable[meta.name]).toBe('Map');
expect(immutable[meta.type]).toBe('iterator');
expect(map[meta.inspectable]).toBeUndefined(); // Complex type
expect(map[meta.name]).toBe('Map');
expect(map[meta.type]).toBe('iterator');
expect(map[0][meta.type]).toBe('array');
expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type
expect(map_of_maps[meta.name]).toBe('Map');
expect(map_of_maps[meta.type]).toBe('iterator');
expect(map_of_maps[0][meta.type]).toBe('array');
expect(react_element[meta.inspectable]).toBe(false);
expect(react_element[meta.name]).toBe('span');
expect(react_element[meta.type]).toBe('react_element');
expect(set[meta.inspectable]).toBeUndefined(); // Complex type
expect(set[meta.name]).toBe('Set');
expect(set[meta.type]).toBe('iterator');
expect(set[0]).toBe('abc');
expect(set[1]).toBe(123);
expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type
expect(set_of_sets[meta.name]).toBe('Set');
expect(set_of_sets[meta.type]).toBe('iterator');
expect(set_of_sets['0'][meta.inspectable]).toBe(true);
expect(symbol[meta.inspectable]).toBe(false);
expect(symbol[meta.name]).toBe('Symbol(symbol)');
expect(symbol[meta.type]).toBe('symbol');
expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type
expect(typed_array[meta.size]).toBe(3);
expect(typed_array[meta.name]).toBe('Int8Array');
expect(typed_array[meta.type]).toBe('typed_array');
expect(typed_array[0]).toBe(100);
expect(typed_array[1]).toBe(-100);
expect(typed_array[2]).toBe(0);
done();
});
+12 -3
View File
@@ -10,11 +10,20 @@ export function cleanForBridge(
path?: Array<string | number> = []
): DehydratedData | null {
if (data !== null) {
const cleaned = [];
const cleanedPaths = [];
const unserializablePaths = [];
const cleanedData = dehydrate(
data,
cleanedPaths,
unserializablePaths,
path,
isPathWhitelisted
);
return {
data: dehydrate(data, cleaned, path, isPathWhitelisted),
cleaned,
data: cleanedData,
cleaned: cleanedPaths,
unserializable: unserializablePaths,
};
} else {
return null;
@@ -134,7 +134,7 @@ function InspectedElementContextController({ children }: Props) {
const value = hydrateHelper(data.value, data.path);
const inspectedElement = { ...currentlyInspectedElement };
fillInPath(inspectedElement, data.path, value);
fillInPath(inspectedElement, data.value, data.path, value);
resource.write(element, inspectedElement);
@@ -289,7 +289,7 @@ function hydrateHelper(
path?: Array<string | number>
): Object | null {
if (dehydratedData !== null) {
let { cleaned, data } = dehydratedData;
let { cleaned, data, unserializable } = dehydratedData;
if (path) {
const { length } = path;
@@ -297,10 +297,13 @@ function hydrateHelper(
// Hydration helper requires full paths, but inspection dehydrates with relative paths.
// In that event it's important that we adjust the "cleaned" paths to match.
cleaned = cleaned.map(cleanedPath => cleanedPath.slice(length));
unserializable = unserializable.map(unserializablePath =>
unserializablePath.slice(length)
);
}
}
return hydrate(data, cleaned);
return hydrate(data, cleaned, unserializable);
} else {
return null;
}
+19 -7
View File
@@ -16,6 +16,7 @@ type KeyValueProps = {|
depth: number,
hidden?: boolean,
inspectPath?: InspectPath,
isReadOnly?: boolean,
name: string,
overrideValueFn?: ?OverrideValueFn,
path: Array<any>,
@@ -25,6 +26,7 @@ type KeyValueProps = {|
export default function KeyValue({
depth,
inspectPath,
isReadOnly,
hidden,
name,
overrideValueFn,
@@ -78,17 +80,18 @@ export default function KeyValue({
displayValue = 'undefined';
}
const nameClassName =
typeof overrideValueFn === 'function' ? styles.EditableName : styles.Name;
const isEditable = typeof overrideValueFn === 'function' && !isReadOnly;
children = (
<div key="root" className={styles.Item} hidden={hidden} style={style}>
<div className={styles.ExpandCollapseToggleSpacer} />
<span className={nameClassName}>{name}</span>
{typeof overrideValueFn === 'function' ? (
<span className={isEditable ? styles.EditableName : styles.Name}>
{name}
</span>
{isEditable ? (
<EditableValue
dataType={dataType}
overrideValueFn={overrideValueFn}
overrideValueFn={((overrideValueFn: any): OverrideValueFn)}
path={path}
value={value}
/>
@@ -97,7 +100,10 @@ export default function KeyValue({
)}
</div>
);
} else if (value.hasOwnProperty(meta.type)) {
} else if (
value.hasOwnProperty(meta.type) &&
!value.hasOwnProperty(meta.unserializable)
) {
children = (
<div key="root" className={styles.Item} hidden={hidden} style={style}>
{isInspectable ? (
@@ -123,6 +129,7 @@ export default function KeyValue({
key={index}
depth={depth + 1}
inspectPath={inspectPath}
isReadOnly={isReadOnly}
hidden={hidden || !isOpen}
name={index}
overrideValueFn={overrideValueFn}
@@ -156,12 +163,17 @@ export default function KeyValue({
);
} else {
const hasChildren = Object.entries(value).length > 0;
const displayName = value.hasOwnProperty(meta.unserializable)
? getMetaValueLabel(value)
: 'Object';
let areChildrenReadOnly = isReadOnly || !!value[meta.readonly];
children = Object.entries(value).map<Element<any>>(([name, value]) => (
<KeyValue
key={name}
depth={depth + 1}
inspectPath={inspectPath}
isReadOnly={areChildrenReadOnly}
hidden={hidden || !isOpen}
name={name}
overrideValueFn={overrideValueFn}
@@ -188,7 +200,7 @@ export default function KeyValue({
{name}
</span>
<span>
Object{' '}
{`${displayName || ''} `}
{hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
</span>
</div>
+1
View File
@@ -83,5 +83,6 @@ export type InspectedElement = {|
export type DehydratedData = {|
cleaned: Array<Array<string | number>>,
unserializable: Array<Array<string | number>>,
data: Object,
|};
+140 -15
View File
@@ -18,6 +18,8 @@ import {
} from 'react-is';
import { getDisplayName, getInObject, setInObject } from './utils';
import type { DehydratedData } from 'src/devtools/views/Components/types';
export const meta = {
inspectable: Symbol('inspectable'),
inspected: Symbol('inspected'),
@@ -25,6 +27,7 @@ export const meta = {
readonly: Symbol('readonly'),
size: Symbol('size'),
type: Symbol('type'),
unserializable: Symbol('unserializable'),
};
type Dehydrated = {|
@@ -35,6 +38,18 @@ type Dehydrated = {|
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.
type Unserializable = {
name: string | null,
readonly?: boolean,
size?: number,
type: string,
unserializable: boolean,
};
// 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).
@@ -173,12 +188,19 @@ function createDehydrated(
export function dehydrate(
data: Object,
cleaned: Array<Array<string | number>>,
unserializable: Array<Array<string | number>>,
path: Array<string | number>,
isPathWhitelisted: (path: Array<string | number>) => boolean,
level?: number = 0
): string | Dehydrated | { [key: string]: string | Dehydrated } {
):
| string
| Dehydrated
| Unserializable
| { [key: string]: string | Dehydrated | Unserializable } {
const type = getDataType(data);
let isPathWhitelistedCheck;
switch (type) {
case 'html_element':
cleaned.push(path);
@@ -229,23 +251,56 @@ export function dehydrate(
};
case 'array':
const arrayPathCheck = isPathWhitelisted(path);
if (level >= LEVEL_THRESHOLD && !arrayPathCheck) {
isPathWhitelistedCheck = isPathWhitelisted(path);
if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
return createDehydrated(type, true, data, cleaned, path);
}
return data.map((item, i) =>
dehydrate(
item,
cleaned,
unserializable,
path.concat([i]),
isPathWhitelisted,
arrayPathCheck ? 1 : level + 1
isPathWhitelistedCheck ? 1 : level + 1
)
);
case 'typed_array':
case 'iterator':
return createDehydrated(type, false, data, cleaned, path);
isPathWhitelistedCheck = isPathWhitelisted(path);
if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
return createDehydrated(type, true, data, cleaned, path);
} else {
const unserializableValue: Unserializable = {
unserializable: true,
type: type,
readonly: true,
size: type === 'typed_array' ? data.length : undefined,
name:
!data.constructor || data.constructor.name === 'Object'
? ''
: data.constructor.name,
};
if (typeof data[Symbol.iterator]) {
[...data].forEach(
(item, i) =>
(unserializableValue[i] = dehydrate(
item,
cleaned,
unserializable,
path.concat([i]),
isPathWhitelisted,
isPathWhitelistedCheck ? 1 : level + 1
))
);
}
unserializable.push(path);
return unserializableValue;
}
case 'date':
cleaned.push(path);
@@ -256,8 +311,8 @@ export function dehydrate(
};
case 'object':
const objectPathCheck = isPathWhitelisted(path);
if (level >= LEVEL_THRESHOLD && !objectPathCheck) {
isPathWhitelistedCheck = isPathWhitelisted(path);
if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
return createDehydrated(type, true, data, cleaned, path);
} else {
const object = {};
@@ -265,9 +320,10 @@ export function dehydrate(
object[name] = dehydrate(
data[name],
cleaned,
unserializable,
path.concat([name]),
isPathWhitelisted,
objectPathCheck ? 1 : level + 1
isPathWhitelistedCheck ? 1 : level + 1
);
}
return object;
@@ -290,24 +346,33 @@ export function dehydrate(
export function fillInPath(
object: Object,
data: DehydratedData,
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];
if (!target[meta.unserializable]) {
delete target[meta.inspectable];
delete target[meta.inspected];
delete target[meta.name];
delete target[meta.readonly];
delete target[meta.size];
delete target[meta.type];
}
}
if (value !== null && data.unserializable.includes(path)) {
upgradeUnserializable(value, value);
}
setInObject(object, path, value);
}
export function hydrate(
object: Object,
cleaned: Array<Array<string | number>>
cleaned: Array<Array<string | number>>,
unserializable: Array<Array<string | number>>
): Object {
cleaned.forEach((path: Array<string | number>) => {
const length = path.length;
@@ -338,9 +403,69 @@ export function hydrate(
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, {
[meta.inspected]: {
configurable: true,
enumerable: false,
value: !!source.inspected,
},
[meta.name]: {
configurable: true,
enumerable: false,
value: source.name,
},
[meta.size]: {
configurable: true,
enumerable: false,
value: source.size,
},
[meta.readonly]: {
configurable: true,
enumerable: false,
value: !!source.readonly,
},
[meta.type]: {
configurable: true,
enumerable: false,
value: source.type,
},
[meta.unserializable]: {
configurable: true,
enumerable: false,
value: !!source.unserializable,
},
});
delete destination.inspected;
delete destination.name;
delete destination.size;
delete destination.readonly;
delete destination.type;
delete destination.unserializable;
}
export function getDisplayNameForReactElement(
element: React$Element<any>
): string | null {
+10 -6
View File
@@ -266,13 +266,17 @@ export function shallowDiffers(prev: Object, next: Object): boolean {
export function getInObject(object: Object, path: Array<string | number>): any {
return path.reduce((reduced: Object, attr: string | number): any => {
if (typeof reduced === 'object' && reduced !== null) {
return reduced[attr];
} else if (Array.isArray(reduced)) {
return reduced[attr];
} else {
return null;
if (reduced) {
if (hasOwnProperty.call(reduced, attr)) {
return reduced[attr];
}
if (typeof reduced[Symbol.iterator] === 'function') {
// Convert iterable to array and return array[index]
return [...reduced][attr];
}
}
return null;
}, object);
}