Added support for unserializable types (e.g. Set/Map, Immutable)
Brian Vaughn committed
Aug 17, 2019 at 10:20 UTC
ce65df7092ed69b45aa5787b708c1516b2c07427
13 files changed
+548
-114
shells/dev/app/InspectableElements/InspectableElements.js
+2
-2
@@ -1,10 +1,10 @@
1
// @flow
2
3
import React, { Fragment } from 'react';
4
+import UnserializableProps from './UnserializableProps';
5
import Contexts from './Contexts';
6
import CustomHooks from './CustomHooks';
7
import CustomObject from './CustomObject';
7
-import MapAndSet from './MapAndSet';
8
import NestedProps from './NestedProps';
9
import SimpleValues from './SimpleValues';
10
@@ -15,7 +15,7 @@ export default function InspectableElements() {
15
<Fragment>
16
<h1>Inspectable elements</h1>
17
<SimpleValues />
18
- <MapAndSet />
18
+ <UnserializableProps />
19
<NestedProps />
20
<Contexts />
21
<CustomHooks />
shells/dev/app/InspectableElements/MapAndSet.js
deleted
-19
@@ -1,19 +0,0 @@
1
-// @flow
2
-
3
-import React from 'react';
4
-
5
-const set = new Set();
6
-set.add('abc');
7
-set.add(123);
8
-
9
-const map = new Map();
10
-map.set('name', 'Brian');
11
-map.set('food', 'sushi');
12
-
13
-export default function MapAndSet() {
14
- return <ChildComponent map={map} set={set} />;
15
-}
16
-
17
-function ChildComponent(props: any) {
18
- return null;
19
-}
shells/dev/app/InspectableElements/UnserializableProps.js
new
+35
@@ -0,0 +1,35 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import Immutable from 'immutable';
5
+
6
+const set = new Set(['abc', 123]);
7
+const map = new Map([['name', 'Brian'], ['food', 'sushi']]);
8
+const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
9
+const mapOfMaps = new Map([['first', map], ['second', map]]);
10
+const typedArray = Int8Array.from([100, -100, 0]);
11
+const immutable = Immutable.fromJS({
12
+ a: [{ hello: 'there' }, 'fixed', true],
13
+ b: 123,
14
+ c: {
15
+ '1': 'xyz',
16
+ xyz: 1,
17
+ },
18
+});
19
+
20
+export default function UnserializableProps() {
21
+ return (
22
+ <ChildComponent
23
+ map={map}
24
+ set={set}
25
+ mapOfMaps={mapOfMaps}
26
+ setOfSets={setOfSets}
27
+ typedArray={typedArray}
28
+ immutable={immutable}
29
+ />
30
+ );
31
+}
32
+
33
+function ChildComponent(props: any) {
34
+ return null;
35
+}
src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+66
-5
@@ -1,5 +1,41 @@
1
// Jest Snapshot v1, https://goo.gl/fbAQLP
2
3
+exports[`InspectedElementContext should dehydrate complex nested values when requested: 1: Initially inspect element 1`] = `
4
+{
5
+ "id": 2,
6
+ "owners": null,
7
+ "context": null,
8
+ "hooks": null,
9
+ "props": {
10
+ "set_of_sets": {
11
+ "0": {},
12
+ "1": {}
13
+ }
14
+ },
15
+ "state": null
16
+}
17
+`;
18
+
19
+exports[`InspectedElementContext should dehydrate complex nested values when requested: 2: Inspect props.set_of_sets.0 1`] = `
20
+{
21
+ "id": 2,
22
+ "owners": null,
23
+ "context": null,
24
+ "hooks": null,
25
+ "props": {
26
+ "set_of_sets": {
27
+ "0": {
28
+ "0": 1,
29
+ "1": 2,
30
+ "2": 3
31
+ },
32
+ "1": {}
33
+ }
34
+ },
35
+ "state": null
36
+}
37
+`;
38
+
39
exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
40
{
41
"id": 2,
@@ -449,13 +485,38 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
485
"context": null,
486
"hooks": null,
487
"props": {
452
- "html_element": {},
488
+ "array_buffer": {},
489
+ "date": {},
490
"fn": {},
454
- "symbol": {},
491
+ "html_element": {},
492
+ "immutable": {
493
+ "0": {},
494
+ "1": {},
495
+ "2": {}
496
+ },
497
+ "map": {
498
+ "0": {},
499
+ "1": {}
500
+ },
501
+ "map_of_maps": {
502
+ "0": {},
503
+ "1": {}
504
+ },
505
"react_element": {},
456
- "array_buffer": {},
457
- "typed_array": {},
458
- "date": {}
506
+ "set": {
507
+ "0": "abc",
508
+ "1": 123
509
+ },
510
+ "set_of_sets": {
511
+ "0": {},
512
+ "1": {}
513
+ },
514
+ "symbol": {},
515
+ "typed_array": {
516
+ "0": 100,
517
+ "1": -100,
518
+ "2": 0
519
+ }
520
},
521
"state": null
522
}
src/__tests__/inspectedElementContext-test.js
+139
-24
@@ -390,23 +390,42 @@ describe('InspectedElementContext', () => {
390
});
391
392
it('should support complex data types', async done => {
393
+ const Immutable = require('immutable');
394
+
395
const Example = () => null;
396
397
const div = document.createElement('div');
396
- const exmapleFunction = () => {};
397
- const typedArray = new Uint8Array(3);
398
+ const exampleFunction = () => {};
399
+ const setShallow = new Set(['abc', 123]);
400
+ const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
401
+ const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
402
+ const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
403
+ const typedArray = Int8Array.from([100, -100, 0]);
404
+ const immutableMap = Immutable.fromJS({
405
+ a: [{ hello: 'there' }, 'fixed', true],
406
+ b: 123,
407
+ c: {
408
+ '1': 'xyz',
409
+ xyz: 1,
410
+ },
411
+ });
412
413
const container = document.createElement('div');
414
await utils.actAsync(() =>
415
ReactDOM.render(
416
<Example
417
+ array_buffer={typedArray.buffer}
418
+ date={new Date()}
419
+ fn={exampleFunction}
420
html_element={div}
404
- fn={exmapleFunction}
405
- symbol={Symbol('symbol')}
421
+ immutable={immutableMap}
422
+ map={mapShallow}
423
+ map_of_maps={mapOfMaps}
424
react_element={<span />}
407
- array_buffer={typedArray.buffer}
425
+ set={setShallow}
426
+ set_of_sets={setOfSets}
427
+ symbol={Symbol('symbol')}
428
typed_array={typedArray}
409
- date={new Date()}
429
/>,
430
container
431
)
@@ -441,36 +460,76 @@ describe('InspectedElementContext', () => {
460
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
461
462
const {
444
- html_element,
463
+ array_buffer,
464
+ date,
465
fn,
446
- symbol,
466
+ html_element,
467
+ immutable,
468
+ map,
469
+ map_of_maps,
470
react_element,
448
- array_buffer,
471
+ set,
472
+ set_of_sets,
473
+ symbol,
474
typed_array,
450
- date,
475
} = (inspectedElement: any).props;
476
+
477
+ expect(array_buffer[meta.size]).toBe(3);
478
+ expect(array_buffer[meta.inspectable]).toBe(false);
479
+ expect(array_buffer[meta.name]).toBe('ArrayBuffer');
480
+ expect(array_buffer[meta.type]).toBe('array_buffer');
481
+
482
+ expect(date[meta.inspectable]).toBe(false);
483
+ expect(date[meta.type]).toBe('date');
484
+
485
+ expect(fn[meta.inspectable]).toBe(false);
486
+ expect(fn[meta.name]).toBe('exampleFunction');
487
+ expect(fn[meta.type]).toBe('function');
488
+
489
expect(html_element[meta.inspectable]).toBe(false);
490
expect(html_element[meta.name]).toBe('DIV');
491
expect(html_element[meta.type]).toBe('html_element');
455
- expect(fn[meta.inspectable]).toBe(false);
456
- expect(fn[meta.name]).toBe('exmapleFunction');
457
- expect(fn[meta.type]).toBe('function');
458
- expect(symbol[meta.inspectable]).toBe(false);
459
- expect(symbol[meta.name]).toBe('Symbol(symbol)');
460
- expect(symbol[meta.type]).toBe('symbol');
492
+
493
+ expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type
494
+ expect(immutable[meta.name]).toBe('Map');
495
+ expect(immutable[meta.type]).toBe('iterator');
496
+
497
+ expect(map[meta.inspectable]).toBeUndefined(); // Complex type
498
+ expect(map[meta.name]).toBe('Map');
499
+ expect(map[meta.type]).toBe('iterator');
500
+ expect(map[0][meta.type]).toBe('array');
501
+
502
+ expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type
503
+ expect(map_of_maps[meta.name]).toBe('Map');
504
+ expect(map_of_maps[meta.type]).toBe('iterator');
505
+ expect(map_of_maps[0][meta.type]).toBe('array');
506
+
507
expect(react_element[meta.inspectable]).toBe(false);
508
expect(react_element[meta.name]).toBe('span');
509
expect(react_element[meta.type]).toBe('react_element');
464
- expect(array_buffer[meta.size]).toBe(3);
465
- expect(array_buffer[meta.inspectable]).toBe(false);
466
- expect(array_buffer[meta.name]).toBe('ArrayBuffer');
467
- expect(array_buffer[meta.type]).toBe('array_buffer');
510
+
511
+ expect(set[meta.inspectable]).toBeUndefined(); // Complex type
512
+ expect(set[meta.name]).toBe('Set');
513
+ expect(set[meta.type]).toBe('iterator');
514
+ expect(set[0]).toBe('abc');
515
+ expect(set[1]).toBe(123);
516
+
517
+ expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type
518
+ expect(set_of_sets[meta.name]).toBe('Set');
519
+ expect(set_of_sets[meta.type]).toBe('iterator');
520
+ expect(set_of_sets['0'][meta.inspectable]).toBe(true);
521
+
522
+ expect(symbol[meta.inspectable]).toBe(false);
523
+ expect(symbol[meta.name]).toBe('Symbol(symbol)');
524
+ expect(symbol[meta.type]).toBe('symbol');
525
+
526
+ expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type
527
expect(typed_array[meta.size]).toBe(3);
469
- expect(typed_array[meta.inspectable]).toBe(false);
470
- expect(typed_array[meta.name]).toBe('Uint8Array');
528
+ expect(typed_array[meta.name]).toBe('Int8Array');
529
expect(typed_array[meta.type]).toBe('typed_array');
472
- expect(date[meta.inspectable]).toBe(false);
473
- expect(date[meta.type]).toBe('date');
530
+ expect(typed_array[0]).toBe(100);
531
+ expect(typed_array[1]).toBe(-100);
532
+ expect(typed_array[2]).toBe(0);
533
534
done();
535
});
@@ -663,6 +722,62 @@ describe('InspectedElementContext', () => {
722
done();
723
});
724
725
+ it('should dehydrate complex nested values when requested', async done => {
726
+ const Example = () => null;
727
+
728
+ const container = document.createElement('div');
729
+ await utils.actAsync(() =>
730
+ ReactDOM.render(
731
+ <Example
732
+ set_of_sets={new Set([new Set([1, 2, 3]), new Set(['a', 'b', 'c'])])}
733
+ />,
734
+ container
735
+ )
736
+ );
737
+
738
+ const id = ((store.getElementIDAtIndex(0): any): number);
739
+
740
+ let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
741
+ let inspectedElement = null;
742
+
743
+ function Suspender({ target }) {
744
+ const context = React.useContext(InspectedElementContext);
745
+ getInspectedElementPath = context.getInspectedElementPath;
746
+ inspectedElement = context.getInspectedElement(target);
747
+ return null;
748
+ }
749
+
750
+ await utils.actAsync(
751
+ () =>
752
+ TestRenderer.create(
753
+ <Contexts
754
+ defaultSelectedElementID={id}
755
+ defaultSelectedElementIndex={0}
756
+ >
757
+ <React.Suspense fallback={null}>
758
+ <Suspender target={id} />
759
+ </React.Suspense>
760
+ </Contexts>
761
+ ),
762
+ false
763
+ );
764
+ expect(getInspectedElementPath).not.toBeNull();
765
+ expect(inspectedElement).not.toBeNull();
766
+ expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
767
+
768
+ inspectedElement = null;
769
+ TestUtils.act(() => {
770
+ TestRenderer.act(() => {
771
+ getInspectedElementPath(id, ['props', 'set_of_sets', 0]);
772
+ jest.runOnlyPendingTimers();
773
+ });
774
+ });
775
+ expect(inspectedElement).not.toBeNull();
776
+ expect(inspectedElement).toMatchSnapshot('2: Inspect props.set_of_sets.0');
777
+
778
+ done();
779
+ });
780
+
781
it('should include updates for nested values that were previously hydrated', async done => {
782
const Example = () => null;
783
src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
+30
-5
@@ -126,13 +126,38 @@ Object {
126
"context": {},
127
"hooks": null,
128
"props": {
129
- "html_element": {},
129
+ "array_buffer": {},
130
+ "date": {},
131
"fn": {},
131
- "symbol": {},
132
+ "html_element": {},
133
+ "immutable": {
134
+ "0": {},
135
+ "1": {},
136
+ "2": {}
137
+ },
138
+ "map": {
139
+ "0": {},
140
+ "1": {}
141
+ },
142
+ "map_of_maps": {
143
+ "0": {},
144
+ "1": {}
145
+ },
146
"react_element": {},
133
- "array_buffer": {},
134
- "typed_array": {},
135
- "date": {}
147
+ "set": {
148
+ "0": "abc",
149
+ "1": 123
150
+ },
151
+ "set_of_sets": {
152
+ "0": {},
153
+ "1": {}
154
+ },
155
+ "symbol": {},
156
+ "typed_array": {
157
+ "0": 100,
158
+ "1": -100,
159
+ "2": 0
160
+ }
161
},
162
"state": null
163
},
src/__tests__/legacy/inspectElement-test.js
+88
-25
@@ -23,7 +23,11 @@ describe('InspectedElementContext', () => {
23
dehydratedData: DehydratedData | null
24
): Object | null {
25
if (dehydratedData !== null) {
26
- return hydrate(dehydratedData.data, dehydratedData.cleaned);
26
+ return hydrate(
27
+ dehydratedData.data,
28
+ dehydratedData.cleaned,
29
+ dehydratedData.unserializable
30
+ );
31
} else {
32
return null;
33
}
@@ -132,22 +136,41 @@ describe('InspectedElementContext', () => {
136
});
137
138
it('should support complex data types', async done => {
139
+ const Immutable = require('immutable');
140
+
141
const Example = () => null;
142
143
const div = document.createElement('div');
138
- const exmapleFunction = () => {};
139
- const typedArray = new Uint8Array(3);
144
+ const exampleFunction = () => {};
145
+ const setShallow = new Set(['abc', 123]);
146
+ const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
147
+ const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
148
+ const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
149
+ const typedArray = Int8Array.from([100, -100, 0]);
150
+ const immutableMap = Immutable.fromJS({
151
+ a: [{ hello: 'there' }, 'fixed', true],
152
+ b: 123,
153
+ c: {
154
+ '1': 'xyz',
155
+ xyz: 1,
156
+ },
157
+ });
158
159
act(() =>
160
ReactDOM.render(
161
<Example
162
+ array_buffer={typedArray.buffer}
163
+ date={new Date()}
164
+ fn={exampleFunction}
165
html_element={div}
145
- fn={exmapleFunction}
146
- symbol={Symbol('symbol')}
166
+ immutable={immutableMap}
167
+ map={mapShallow}
168
+ map_of_maps={mapOfMaps}
169
react_element={<span />}
148
- array_buffer={typedArray.buffer}
170
+ set={setShallow}
171
+ set_of_sets={setOfSets}
172
+ symbol={Symbol('symbol')}
173
typed_array={typedArray}
150
- date={new Date()}
174
/>,
175
document.createElement('div')
176
)
@@ -159,36 +182,76 @@ describe('InspectedElementContext', () => {
182
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
183
184
const {
162
- html_element,
185
+ array_buffer,
186
+ date,
187
fn,
164
- symbol,
188
+ html_element,
189
+ immutable,
190
+ map,
191
+ map_of_maps,
192
react_element,
166
- array_buffer,
193
+ set,
194
+ set_of_sets,
195
+ symbol,
196
typed_array,
168
- date,
197
} = inspectedElement.value.props;
198
+
199
+ expect(array_buffer[meta.size]).toBe(3);
200
+ expect(array_buffer[meta.inspectable]).toBe(false);
201
+ expect(array_buffer[meta.name]).toBe('ArrayBuffer');
202
+ expect(array_buffer[meta.type]).toBe('array_buffer');
203
+
204
+ expect(date[meta.inspectable]).toBe(false);
205
+ expect(date[meta.type]).toBe('date');
206
+
207
+ expect(fn[meta.inspectable]).toBe(false);
208
+ expect(fn[meta.name]).toBe('exampleFunction');
209
+ expect(fn[meta.type]).toBe('function');
210
+
211
expect(html_element[meta.inspectable]).toBe(false);
212
expect(html_element[meta.name]).toBe('DIV');
213
expect(html_element[meta.type]).toBe('html_element');
173
- expect(fn[meta.inspectable]).toBe(false);
174
- expect(fn[meta.name]).toBe('exmapleFunction');
175
- expect(fn[meta.type]).toBe('function');
176
- expect(symbol[meta.inspectable]).toBe(false);
177
- expect(symbol[meta.name]).toBe('Symbol(symbol)');
178
- expect(symbol[meta.type]).toBe('symbol');
214
+
215
+ expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type
216
+ expect(immutable[meta.name]).toBe('Map');
217
+ expect(immutable[meta.type]).toBe('iterator');
218
+
219
+ expect(map[meta.inspectable]).toBeUndefined(); // Complex type
220
+ expect(map[meta.name]).toBe('Map');
221
+ expect(map[meta.type]).toBe('iterator');
222
+ expect(map[0][meta.type]).toBe('array');
223
+
224
+ expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type
225
+ expect(map_of_maps[meta.name]).toBe('Map');
226
+ expect(map_of_maps[meta.type]).toBe('iterator');
227
+ expect(map_of_maps[0][meta.type]).toBe('array');
228
+
229
expect(react_element[meta.inspectable]).toBe(false);
230
expect(react_element[meta.name]).toBe('span');
231
expect(react_element[meta.type]).toBe('react_element');
182
- expect(array_buffer[meta.size]).toBe(3);
183
- expect(array_buffer[meta.inspectable]).toBe(false);
184
- expect(array_buffer[meta.name]).toBe('ArrayBuffer');
185
- expect(array_buffer[meta.type]).toBe('array_buffer');
232
+
233
+ expect(set[meta.inspectable]).toBeUndefined(); // Complex type
234
+ expect(set[meta.name]).toBe('Set');
235
+ expect(set[meta.type]).toBe('iterator');
236
+ expect(set[0]).toBe('abc');
237
+ expect(set[1]).toBe(123);
238
+
239
+ expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type
240
+ expect(set_of_sets[meta.name]).toBe('Set');
241
+ expect(set_of_sets[meta.type]).toBe('iterator');
242
+ expect(set_of_sets['0'][meta.inspectable]).toBe(true);
243
+
244
+ expect(symbol[meta.inspectable]).toBe(false);
245
+ expect(symbol[meta.name]).toBe('Symbol(symbol)');
246
+ expect(symbol[meta.type]).toBe('symbol');
247
+
248
+ expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type
249
expect(typed_array[meta.size]).toBe(3);
187
- expect(typed_array[meta.inspectable]).toBe(false);
188
- expect(typed_array[meta.name]).toBe('Uint8Array');
250
+ expect(typed_array[meta.name]).toBe('Int8Array');
251
expect(typed_array[meta.type]).toBe('typed_array');
190
- expect(date[meta.inspectable]).toBe(false);
191
- expect(date[meta.type]).toBe('date');
252
+ expect(typed_array[0]).toBe(100);
253
+ expect(typed_array[1]).toBe(-100);
254
+ expect(typed_array[2]).toBe(0);
255
256
done();
257
});
src/backend/utils.js
+12
-3
@@ -10,11 +10,20 @@ export function cleanForBridge(
10
path?: Array<string | number> = []
11
): DehydratedData | null {
12
if (data !== null) {
13
- const cleaned = [];
13
+ const cleanedPaths = [];
14
+ const unserializablePaths = [];
15
+ const cleanedData = dehydrate(
16
+ data,
17
+ cleanedPaths,
18
+ unserializablePaths,
19
+ path,
20
+ isPathWhitelisted
21
+ );
22
23
return {
16
- data: dehydrate(data, cleaned, path, isPathWhitelisted),
17
- cleaned,
24
+ data: cleanedData,
25
+ cleaned: cleanedPaths,
26
+ unserializable: unserializablePaths,
27
};
28
} else {
29
return null;
src/devtools/views/Components/InspectedElementContext.js
+6
-3
@@ -134,7 +134,7 @@ function InspectedElementContextController({ children }: Props) {
134
const value = hydrateHelper(data.value, data.path);
135
const inspectedElement = { ...currentlyInspectedElement };
136
137
- fillInPath(inspectedElement, data.path, value);
137
+ fillInPath(inspectedElement, data.value, data.path, value);
138
139
resource.write(element, inspectedElement);
140
@@ -289,7 +289,7 @@ function hydrateHelper(
289
path?: Array<string | number>
290
): Object | null {
291
if (dehydratedData !== null) {
292
- let { cleaned, data } = dehydratedData;
292
+ let { cleaned, data, unserializable } = dehydratedData;
293
294
if (path) {
295
const { length } = path;
@@ -297,10 +297,13 @@ function hydrateHelper(
297
// Hydration helper requires full paths, but inspection dehydrates with relative paths.
298
// In that event it's important that we adjust the "cleaned" paths to match.
299
cleaned = cleaned.map(cleanedPath => cleanedPath.slice(length));
300
+ unserializable = unserializable.map(unserializablePath =>
301
+ unserializablePath.slice(length)
302
+ );
303
}
304
}
305
303
- return hydrate(data, cleaned);
306
+ return hydrate(data, cleaned, unserializable);
307
} else {
308
return null;
309
}
src/devtools/views/Components/KeyValue.js
+19
-7
@@ -16,6 +16,7 @@ type KeyValueProps = {|
16
depth: number,
17
hidden?: boolean,
18
inspectPath?: InspectPath,
19
+ isReadOnly?: boolean,
20
name: string,
21
overrideValueFn?: ?OverrideValueFn,
22
path: Array<any>,
@@ -25,6 +26,7 @@ type KeyValueProps = {|
26
export default function KeyValue({
27
depth,
28
inspectPath,
29
+ isReadOnly,
30
hidden,
31
name,
32
overrideValueFn,
@@ -78,17 +80,18 @@ export default function KeyValue({
80
displayValue = 'undefined';
81
}
82
81
- const nameClassName =
82
- typeof overrideValueFn === 'function' ? styles.EditableName : styles.Name;
83
+ const isEditable = typeof overrideValueFn === 'function' && !isReadOnly;
84
85
children = (
86
<div key="root" className={styles.Item} hidden={hidden} style={style}>
87
<div className={styles.ExpandCollapseToggleSpacer} />
87
- <span className={nameClassName}>{name}</span>
88
- {typeof overrideValueFn === 'function' ? (
88
+ <span className={isEditable ? styles.EditableName : styles.Name}>
89
+ {name}
90
+ </span>
91
+ {isEditable ? (
92
<EditableValue
93
dataType={dataType}
91
- overrideValueFn={overrideValueFn}
94
+ overrideValueFn={((overrideValueFn: any): OverrideValueFn)}
95
path={path}
96
value={value}
97
/>
@@ -97,7 +100,10 @@ export default function KeyValue({
100
)}
101
</div>
102
);
100
- } else if (value.hasOwnProperty(meta.type)) {
103
+ } else if (
104
+ value.hasOwnProperty(meta.type) &&
105
+ !value.hasOwnProperty(meta.unserializable)
106
+ ) {
107
children = (
108
<div key="root" className={styles.Item} hidden={hidden} style={style}>
109
{isInspectable ? (
@@ -123,6 +129,7 @@ export default function KeyValue({
129
key={index}
130
depth={depth + 1}
131
inspectPath={inspectPath}
132
+ isReadOnly={isReadOnly}
133
hidden={hidden || !isOpen}
134
name={index}
135
overrideValueFn={overrideValueFn}
@@ -156,12 +163,17 @@ export default function KeyValue({
163
);
164
} else {
165
const hasChildren = Object.entries(value).length > 0;
166
+ const displayName = value.hasOwnProperty(meta.unserializable)
167
+ ? getMetaValueLabel(value)
168
+ : 'Object';
169
170
+ let areChildrenReadOnly = isReadOnly || !!value[meta.readonly];
171
children = Object.entries(value).map<Element<any>>(([name, value]) => (
172
<KeyValue
173
key={name}
174
depth={depth + 1}
175
inspectPath={inspectPath}
176
+ isReadOnly={areChildrenReadOnly}
177
hidden={hidden || !isOpen}
178
name={name}
179
overrideValueFn={overrideValueFn}
@@ -188,7 +200,7 @@ export default function KeyValue({
200
{name}
201
</span>
202
<span>
191
- Object{' '}
203
+ {`${displayName || ''} `}
204
{hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
205
</span>
206
</div>
src/devtools/views/Components/types.js
+1
@@ -83,5 +83,6 @@ export type InspectedElement = {|
83
84
export type DehydratedData = {|
85
cleaned: Array<Array<string | number>>,
86
+ unserializable: Array<Array<string | number>>,
87
data: Object,
88
|};
src/hydration.js
+140
-15
@@ -18,6 +18,8 @@ import {
18
} from 'react-is';
19
import { getDisplayName, getInObject, setInObject } from './utils';
20
21
+import type { DehydratedData } from 'src/devtools/views/Components/types';
22
+
23
export const meta = {
24
inspectable: Symbol('inspectable'),
25
inspected: Symbol('inspected'),
@@ -25,6 +27,7 @@ export const meta = {
27
readonly: Symbol('readonly'),
28
size: Symbol('size'),
29
type: Symbol('type'),
30
+ unserializable: Symbol('unserializable'),
31
};
32
33
type Dehydrated = {|
@@ -35,6 +38,18 @@ type Dehydrated = {|
38
type: string,
39
|};
40
41
+// Typed arrays and other complex iteratable objects (e.g. Map, Set, ImmutableJS) need special handling.
42
+// These objects can't be serialized without losing type information,
43
+// so a "Unserializable" type wrapper is used (with meta-data keys) to send nested values-
44
+// while preserving the original type and name.
45
+type Unserializable = {
46
+ name: string | null,
47
+ readonly?: boolean,
48
+ size?: number,
49
+ type: string,
50
+ unserializable: boolean,
51
+};
52
+
53
// This threshold determines the depth at which the bridge "dehydrates" nested data.
54
// Dehydration means that we don't serialize the data for e.g. postMessage or stringify,
55
// unless the frontend explicitly requests it (e.g. a user clicks to expand a props object).
@@ -173,12 +188,19 @@ function createDehydrated(
188
export function dehydrate(
189
data: Object,
190
cleaned: Array<Array<string | number>>,
191
+ unserializable: Array<Array<string | number>>,
192
path: Array<string | number>,
193
isPathWhitelisted: (path: Array<string | number>) => boolean,
194
level?: number = 0
179
-): string | Dehydrated | { [key: string]: string | Dehydrated } {
195
+):
196
+ | string
197
+ | Dehydrated
198
+ | Unserializable
199
+ | { [key: string]: string | Dehydrated | Unserializable } {
200
const type = getDataType(data);
201
202
+ let isPathWhitelistedCheck;
203
+
204
switch (type) {
205
case 'html_element':
206
cleaned.push(path);
@@ -229,23 +251,56 @@ export function dehydrate(
251
};
252
253
case 'array':
232
- const arrayPathCheck = isPathWhitelisted(path);
233
- if (level >= LEVEL_THRESHOLD && !arrayPathCheck) {
254
+ isPathWhitelistedCheck = isPathWhitelisted(path);
255
+ if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
256
return createDehydrated(type, true, data, cleaned, path);
257
}
258
return data.map((item, i) =>
259
dehydrate(
260
item,
261
cleaned,
262
+ unserializable,
263
path.concat([i]),
264
isPathWhitelisted,
242
- arrayPathCheck ? 1 : level + 1
265
+ isPathWhitelistedCheck ? 1 : level + 1
266
)
267
);
268
269
case 'typed_array':
270
case 'iterator':
248
- return createDehydrated(type, false, data, cleaned, path);
271
+ isPathWhitelistedCheck = isPathWhitelisted(path);
272
+ if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
273
+ return createDehydrated(type, true, data, cleaned, path);
274
+ } else {
275
+ const unserializableValue: Unserializable = {
276
+ unserializable: true,
277
+ type: type,
278
+ readonly: true,
279
+ size: type === 'typed_array' ? data.length : undefined,
280
+ name:
281
+ !data.constructor || data.constructor.name === 'Object'
282
+ ? ''
283
+ : data.constructor.name,
284
+ };
285
+
286
+ if (typeof data[Symbol.iterator]) {
287
+ [...data].forEach(
288
+ (item, i) =>
289
+ (unserializableValue[i] = dehydrate(
290
+ item,
291
+ cleaned,
292
+ unserializable,
293
+ path.concat([i]),
294
+ isPathWhitelisted,
295
+ isPathWhitelistedCheck ? 1 : level + 1
296
+ ))
297
+ );
298
+ }
299
+
300
+ unserializable.push(path);
301
+
302
+ return unserializableValue;
303
+ }
304
305
case 'date':
306
cleaned.push(path);
@@ -256,8 +311,8 @@ export function dehydrate(
311
};
312
313
case 'object':
259
- const objectPathCheck = isPathWhitelisted(path);
260
- if (level >= LEVEL_THRESHOLD && !objectPathCheck) {
314
+ isPathWhitelistedCheck = isPathWhitelisted(path);
315
+ if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
316
return createDehydrated(type, true, data, cleaned, path);
317
} else {
318
const object = {};
@@ -265,9 +320,10 @@ export function dehydrate(
320
object[name] = dehydrate(
321
data[name],
322
cleaned,
323
+ unserializable,
324
path.concat([name]),
325
isPathWhitelisted,
270
- objectPathCheck ? 1 : level + 1
326
+ isPathWhitelistedCheck ? 1 : level + 1
327
);
328
}
329
return object;
@@ -290,24 +346,33 @@ export function dehydrate(
346
347
export function fillInPath(
348
object: Object,
349
+ data: DehydratedData,
350
path: Array<string | number>,
351
value: any
352
) {
353
const target = getInObject(object, path);
354
if (target != null) {
298
- delete target[meta.inspectable];
299
- delete target[meta.inspected];
300
- delete target[meta.name];
301
- delete target[meta.readonly];
302
- delete target[meta.size];
303
- delete target[meta.type];
355
+ if (!target[meta.unserializable]) {
356
+ delete target[meta.inspectable];
357
+ delete target[meta.inspected];
358
+ delete target[meta.name];
359
+ delete target[meta.readonly];
360
+ delete target[meta.size];
361
+ delete target[meta.type];
362
+ }
363
+ }
364
+
365
+ if (value !== null && data.unserializable.includes(path)) {
366
+ upgradeUnserializable(value, value);
367
}
368
+
369
setInObject(object, path, value);
370
}
371
372
export function hydrate(
373
object: Object,
310
- cleaned: Array<Array<string | number>>
374
+ cleaned: Array<Array<string | number>>,
375
+ unserializable: Array<Array<string | number>>
376
): Object {
377
cleaned.forEach((path: Array<string | number>) => {
378
const length = path.length;
@@ -338,9 +403,69 @@ export function hydrate(
403
parent[last] = replaced;
404
}
405
});
406
+ unserializable.forEach((path: Array<string | number>) => {
407
+ const length = path.length;
408
+ const last = path[length - 1];
409
+ const parent = getInObject(object, path.slice(0, length - 1));
410
+ if (!parent || !parent.hasOwnProperty(last)) {
411
+ return;
412
+ }
413
+
414
+ const node = parent[last];
415
+
416
+ const replacement = {
417
+ ...node,
418
+ };
419
+
420
+ upgradeUnserializable(replacement, node);
421
+
422
+ parent[last] = replacement;
423
+ });
424
return object;
425
}
426
427
+function upgradeUnserializable(destination: Object, source: Object) {
428
+ Object.defineProperties(destination, {
429
+ [meta.inspected]: {
430
+ configurable: true,
431
+ enumerable: false,
432
+ value: !!source.inspected,
433
+ },
434
+ [meta.name]: {
435
+ configurable: true,
436
+ enumerable: false,
437
+ value: source.name,
438
+ },
439
+ [meta.size]: {
440
+ configurable: true,
441
+ enumerable: false,
442
+ value: source.size,
443
+ },
444
+ [meta.readonly]: {
445
+ configurable: true,
446
+ enumerable: false,
447
+ value: !!source.readonly,
448
+ },
449
+ [meta.type]: {
450
+ configurable: true,
451
+ enumerable: false,
452
+ value: source.type,
453
+ },
454
+ [meta.unserializable]: {
455
+ configurable: true,
456
+ enumerable: false,
457
+ value: !!source.unserializable,
458
+ },
459
+ });
460
+
461
+ delete destination.inspected;
462
+ delete destination.name;
463
+ delete destination.size;
464
+ delete destination.readonly;
465
+ delete destination.type;
466
+ delete destination.unserializable;
467
+}
468
+
469
export function getDisplayNameForReactElement(
470
element: React$Element<any>
471
): string | null {
src/utils.js
+10
-6
@@ -266,13 +266,17 @@ export function shallowDiffers(prev: Object, next: Object): boolean {
266
267
export function getInObject(object: Object, path: Array<string | number>): any {
268
return path.reduce((reduced: Object, attr: string | number): any => {
269
- if (typeof reduced === 'object' && reduced !== null) {
270
- return reduced[attr];
271
- } else if (Array.isArray(reduced)) {
272
- return reduced[attr];
273
- } else {
274
- return null;
269
+ if (reduced) {
270
+ if (hasOwnProperty.call(reduced, attr)) {
271
+ return reduced[attr];
272
+ }
273
+ if (typeof reduced[Symbol.iterator] === 'function') {
274
+ // Convert iterable to array and return array[index]
275
+ return [...reduced][attr];
276
+ }
277
}
278
+
279
+ return null;
280
}, object);
281
}
282