mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
Merged changes from 4.0.0 -> 4.0.5 from DevTools fork
This commit is contained in:
commit
4da836af71
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/immutable@4.0.0-rc.12/dist/immutable.js"></script>
|
||||
|
||||
<!-- Don't use this in production: -->
|
||||
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
|
||||
|
|
@ -255,6 +256,33 @@
|
|||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
function UnserializableProps() {
|
||||
return (
|
||||
<ChildComponent
|
||||
map={map}
|
||||
set={set}
|
||||
mapOfMaps={mapOfMaps}
|
||||
setOfSets={setOfSets}
|
||||
typedArray={typedArray}
|
||||
immutable={immutable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ChildComponent(props: any) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -264,6 +292,7 @@
|
|||
<Fragment>
|
||||
<SimpleValues />
|
||||
<ObjectProps />
|
||||
<UnserializableProps />
|
||||
<CustomObject />
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"name": "react-devtools-core",
|
||||
"version": "4.0.0-alpha.9",
|
||||
"version": "4.0.5",
|
||||
"description": "Use react-devtools outside of the browser",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
"repository": {
|
||||
"url": "https://github.com/bvaughn/react-devtools-experimental.git",
|
||||
"type": "git"
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
"directory": "packages/react-devtools-core"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
|
|
|||
11
packages/react-devtools-core/src/standalone.js
vendored
11
packages/react-devtools-core/src/standalone.js
vendored
|
|
@ -14,7 +14,8 @@ import {
|
|||
getAppendComponentStack,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {Server} from 'ws';
|
||||
import {existsSync, readFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {readFileSync} from 'fs';
|
||||
import {installHook} from 'react-devtools-shared/src/hook';
|
||||
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
|
||||
import {doesFilePathExist, launchEditor} from './editor';
|
||||
|
|
@ -259,14 +260,8 @@ function startServer(port?: number = 8097) {
|
|||
});
|
||||
|
||||
httpServer.on('request', (request, response) => {
|
||||
// NPM installs should read from node_modules,
|
||||
// But local dev mode needs to use a relative path.
|
||||
const basePath = existsSync('./node_modules/react-devtools-core')
|
||||
? 'node_modules/react-devtools-core'
|
||||
: '../react-devtools-core';
|
||||
|
||||
// Serve a file that immediately sets up the connection.
|
||||
const backendFile = readFileSync(`${basePath}/dist/backend.js`);
|
||||
const backendFile = readFileSync(join(__dirname, 'backend.js'));
|
||||
|
||||
// The renderer interface doesn't read saved component filters directly,
|
||||
// because they are generally stored in localStorage within the context of the extension.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ module.exports = {
|
|||
scheduler: resolve(builtModulesDir, 'scheduler'),
|
||||
},
|
||||
},
|
||||
node: {
|
||||
// Don't replace __dirname!
|
||||
// This would break the standalone DevTools ability to load the backend.
|
||||
// see https://github.com/facebook/react-devtools/issues/1269
|
||||
__dirname: false,
|
||||
},
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
__DEV__: false,
|
||||
|
|
|
|||
7
packages/react-devtools-extensions/build.js
vendored
7
packages/react-devtools-extensions/build.js
vendored
|
|
@ -61,14 +61,13 @@ const build = async (tempPath, manifestPath) => {
|
|||
);
|
||||
|
||||
const commit = getGitCommit();
|
||||
const versionDateString = `${commit} (${new Date().toLocaleDateString()})`;
|
||||
|
||||
const dateString = new Date().toLocaleDateString();
|
||||
const manifest = JSON.parse(readFileSync(copiedManifestPath).toString());
|
||||
const versionDateString = `${manifest.version} (${dateString})`;
|
||||
if (manifest.version_name) {
|
||||
manifest.version_name = versionDateString;
|
||||
} else {
|
||||
manifest.description += `\n\nCreated from revision ${versionDateString}`;
|
||||
}
|
||||
manifest.description += `\n\nCreated from revision ${commit} on ${dateString}.`;
|
||||
|
||||
writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"manifest_version": 2,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Chrome Developer Tools.",
|
||||
"version": "4.0.0",
|
||||
"version_name": "4.0.0",
|
||||
"version": "4.0.5",
|
||||
"version_name": "4.0.5",
|
||||
|
||||
"minimum_chrome_version": "49",
|
||||
|
||||
|
|
@ -40,15 +40,7 @@
|
|||
"persistent": false
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"background",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"file:///*",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"permissions": ["file:///*", "http://*/*", "https://*/*"],
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,12 +18,11 @@
|
|||
|
||||
<h3>
|
||||
Created on <strong>%date%</strong> from
|
||||
<a href="http://github.com/bvaughn/react-devtools-experimental/commit/%commit%"><code>%commit%</code></a>
|
||||
<a href="http://github.com/facebook/react/commit/%commit%"><code>%commit%</code></a>
|
||||
</h3>
|
||||
|
||||
<p>
|
||||
This is a preview build of an <a href="https://github.com/facebook/react/tree/master/packages/react-devtools-extensions">unreleased DevTools extension</a>.
|
||||
It has no developer support.
|
||||
This is a preview build of the <a href="https://github.com/facebook/react">React DevTools extension</a>.
|
||||
</p>
|
||||
|
||||
<h2>Installation instructions</h2>
|
||||
|
|
@ -37,10 +36,5 @@
|
|||
Please report bugs as <a href="https://github.com/facebook/react/issues/new?labels=Component:%20Developer%20Tools">GitHub issues</a>.
|
||||
Please include all of the info required to reproduce the bug (e.g. links, code, instructions).
|
||||
</p>
|
||||
|
||||
<h2>Feature requests</h2>
|
||||
<p>
|
||||
Feature requests are not being accepted at this time.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"manifest_version": 2,
|
||||
"name": "React Developer Tools",
|
||||
"description": "Adds React debugging tools to the Firefox Developer Tools.",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.5",
|
||||
|
||||
"applications": {
|
||||
"gecko": {
|
||||
|
|
@ -44,15 +44,7 @@
|
|||
"scripts": ["build/background.js"]
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"file:///*",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"permissions": ["file:///*", "http://*/*", "https://*/*"],
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
|
|
|
|||
19
packages/react-devtools-extensions/src/main.js
vendored
19
packages/react-devtools-extensions/src/main.js
vendored
|
|
@ -120,6 +120,10 @@ function createPanelIfReactLoaded() {
|
|||
localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY);
|
||||
}
|
||||
|
||||
if (store !== null) {
|
||||
profilingData = store.profilerStore.profilingData;
|
||||
}
|
||||
|
||||
store = new Store(bridge, {
|
||||
isProfiling,
|
||||
supportsReloadAndProfile: getBrowserName() === 'Chrome',
|
||||
|
|
@ -281,21 +285,6 @@ function createPanelIfReactLoaded() {
|
|||
|
||||
chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
|
||||
|
||||
// Shutdown bridge before a new page is loaded.
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(
|
||||
function onBeforeNavigate(details) {
|
||||
// Ignore navigation events from other tabs (or from within frames).
|
||||
if (details.tabId !== tabId || details.frameId !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// `bridge.shutdown()` will remove all listeners we added, so we don't have to.
|
||||
bridge.shutdown();
|
||||
|
||||
profilingData = store.profilerStore.profilingData;
|
||||
},
|
||||
);
|
||||
|
||||
// Re-initialize DevTools panel when a new page is loaded.
|
||||
chrome.devtools.network.onNavigated.addListener(function onNavigated() {
|
||||
// Re-initialize saved filters on navigation,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"name": "react-devtools-inline",
|
||||
"version": "4.0.0-alpha.9",
|
||||
"version": "4.0.5",
|
||||
"description": "Embed react-devtools within a website",
|
||||
"license": "MIT",
|
||||
"main": "./dist/backend.js",
|
||||
"repository": {
|
||||
"url": "https://github.com/bvaughn/react-devtools-experimental.git",
|
||||
"type": "git"
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
"directory": "packages/react-devtools-inline"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -91,6 +127,28 @@ exports[`InspectedElementContext should include updates for nested values that w
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should inspect hooks for components that only use context: 1: Inspected element 2 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": null,
|
||||
"isStateEditable": false,
|
||||
"name": "Context",
|
||||
"value": true,
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"a": 1,
|
||||
"b": "abc"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should inspect the currently selected element: 1: Inspected element 2 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
|
|
@ -427,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -385,23 +385,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,
|
||||
),
|
||||
|
|
@ -435,37 +454,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();
|
||||
});
|
||||
|
||||
|
|
@ -655,6 +714,61 @@ 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;
|
||||
|
||||
|
|
@ -846,4 +960,46 @@ describe('InspectedElementContext', () => {
|
|||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should inspect hooks for components that only use context', async done => {
|
||||
const Context = React.createContext(true);
|
||||
const Example = () => {
|
||||
const value = React.useContext(Context);
|
||||
return value;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example a={1} b="abc" />, container),
|
||||
);
|
||||
|
||||
const id = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({target}) {
|
||||
const {getInspectedElement} = React.useContext(InspectedElementContext);
|
||||
const inspectedElement = getInspectedElement(id);
|
||||
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={0}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>,
|
||||
),
|
||||
false,
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ describe('ProfilerContext', () => {
|
|||
expect(context.didRecordCommits).toBe(false);
|
||||
expect(context.isProcessingData).toBe(false);
|
||||
expect(context.isProfiling).toBe(false);
|
||||
expect(context.profilingData).not.toBe(null);
|
||||
expect(context.profilingData).toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -882,10 +882,10 @@ export function attach(
|
|||
throw new Error('setInHook not supported by this renderer');
|
||||
};
|
||||
const startProfiling = () => {
|
||||
throw new Error('startProfiling not supported by this renderer');
|
||||
// Do not throw, since this would break a multi-root scenario where v15 and v16 were both present.
|
||||
};
|
||||
const stopProfiling = () => {
|
||||
throw new Error('stopProfiling not supported by this renderer');
|
||||
// Do not throw, since this would break a multi-root scenario where v15 and v16 were both present.
|
||||
};
|
||||
|
||||
function getBestMatchForTrackedPath(): PathMatch | null {
|
||||
|
|
|
|||
|
|
@ -1621,7 +1621,9 @@ export function attach(
|
|||
currentRootID = getFiberID(getPrimaryFiber(root.current));
|
||||
setRootPseudoKey(currentRootID, root.current);
|
||||
|
||||
if (isProfiling) {
|
||||
// Checking root.memoizedInteractions handles multi-renderer edge-case-
|
||||
// where some v16 renderers support profiling and others don't.
|
||||
if (isProfiling && root.memoizedInteractions != null) {
|
||||
// If profiling is active, store commit time and duration, and the current interactions.
|
||||
// The frontend may request this information after profiling has stopped.
|
||||
currentCommitProfilingMetadata = {
|
||||
|
|
@ -1665,7 +1667,11 @@ export function attach(
|
|||
mightBeOnTrackedPath = true;
|
||||
}
|
||||
|
||||
if (isProfiling) {
|
||||
// Checking root.memoizedInteractions handles multi-renderer edge-case-
|
||||
// where some v16 renderers support profiling and others don't.
|
||||
const isProfilingSupported = root.memoizedInteractions != null;
|
||||
|
||||
if (isProfiling && isProfilingSupported) {
|
||||
// If profiling is active, store commit time and duration, and the current interactions.
|
||||
// The frontend may request this information after profiling has stopped.
|
||||
currentCommitProfilingMetadata = {
|
||||
|
|
@ -1709,7 +1715,7 @@ export function attach(
|
|||
mountFiberRecursively(current, null);
|
||||
}
|
||||
|
||||
if (isProfiling) {
|
||||
if (isProfiling && isProfilingSupported) {
|
||||
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
|
||||
currentRootID,
|
||||
);
|
||||
|
|
@ -2083,6 +2089,7 @@ export function attach(
|
|||
const {
|
||||
_debugOwner,
|
||||
_debugSource,
|
||||
dependencies,
|
||||
stateNode,
|
||||
memoizedProps,
|
||||
memoizedState,
|
||||
|
|
@ -2094,7 +2101,7 @@ export function attach(
|
|||
(tag === FunctionComponent ||
|
||||
tag === SimpleMemoComponent ||
|
||||
tag === ForwardRef) &&
|
||||
!!memoizedState;
|
||||
(!!memoizedState || !!dependencies);
|
||||
|
||||
const typeSymbol = getTypeSymbol(type);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export type Source = {|
|
|||
fileName: string,
|
||||
lineNumber: number,
|
||||
|};
|
||||
|
||||
export type HookType =
|
||||
| 'useState'
|
||||
| 'useReducer'
|
||||
|
|
@ -41,6 +42,9 @@ export type Fiber = {|
|
|||
|
||||
key: null | string,
|
||||
|
||||
// Dependencies (contexts, events) for this fiber, if it has any
|
||||
dependencies: mixed | null,
|
||||
|
||||
elementType: any,
|
||||
|
||||
type: any,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
|
|||
export const PROFILER_EXPORT_VERSION = 4;
|
||||
|
||||
export const CHANGE_LOG_URL =
|
||||
'https://github.com/bvaughn/react-devtools-experimental/blob/master/CHANGELOG.md';
|
||||
'https://github.com/facebook/react/blob/master/packages/react-devtools/CHANGELOG.md';
|
||||
|
||||
// HACK
|
||||
//
|
||||
|
|
|
|||
|
|
@ -62,6 +62,9 @@ export default class ProfilerStore extends EventEmitter<{|
|
|||
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
|
||||
_isProfiling: boolean = false;
|
||||
|
||||
// Tracks whether a specific renderer logged any profiling data during the most recent session.
|
||||
_rendererIDsThatReportedProfilingData: Set<number> = new Set();
|
||||
|
||||
// After profiling, data is requested from each attached renderer using this queue.
|
||||
// So long as this queue is not empty, the store is retrieving and processing profiling data from the backend.
|
||||
_rendererQueue: Set<number> = new Set();
|
||||
|
|
@ -233,6 +236,8 @@ export default class ProfilerStore extends EventEmitter<{|
|
|||
if (!this._initialSnapshotsByRootID.has(rootID)) {
|
||||
this._initialSnapshotsByRootID.set(rootID, new Map());
|
||||
}
|
||||
|
||||
this._rendererIDsThatReportedProfilingData.add(rendererID);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -280,6 +285,7 @@ export default class ProfilerStore extends EventEmitter<{|
|
|||
this._initialRendererIDs.clear();
|
||||
this._initialSnapshotsByRootID.clear();
|
||||
this._inProgressOperationsByRootID.clear();
|
||||
this._rendererIDsThatReportedProfilingData.clear();
|
||||
this._rendererQueue.clear();
|
||||
|
||||
// Record all renderer IDs initially too (in case of unmount)
|
||||
|
|
@ -316,7 +322,10 @@ export default class ProfilerStore extends EventEmitter<{|
|
|||
this._dataBackends.splice(0);
|
||||
this._rendererQueue.clear();
|
||||
|
||||
this._initialRendererIDs.forEach(rendererID => {
|
||||
// Only request data from renderers that actually logged it.
|
||||
// This avoids unnecessary bridge requests and also avoids edge case mixed renderer bugs.
|
||||
// (e.g. when v15 and v16 are both present)
|
||||
this._rendererIDsThatReportedProfilingData.forEach(rendererID => {
|
||||
if (!this._rendererQueue.has(rendererID)) {
|
||||
this._rendererQueue.add(rendererID);
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
|
|||
) : (
|
||||
<KeyValue
|
||||
depth={1}
|
||||
alphaSort={false}
|
||||
inspectPath={inspectPath}
|
||||
name="subHooks"
|
||||
path={path.concat(['subHooks'])}
|
||||
|
|
@ -179,6 +180,7 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
|
|||
<div className={styles.Children} hidden={!isOpen}>
|
||||
<KeyValue
|
||||
depth={1}
|
||||
alphaSort={false}
|
||||
inspectPath={inspectPath}
|
||||
name="DebugValue"
|
||||
path={path.concat(['value'])}
|
||||
|
|
@ -237,6 +239,7 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
|
|||
<div className={styles.Hook}>
|
||||
<KeyValue
|
||||
depth={1}
|
||||
alphaSort={false}
|
||||
inspectPath={inspectPath}
|
||||
name={name}
|
||||
overrideValueFn={overrideValueFn}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import type {
|
|||
InspectedElementPayload,
|
||||
} from 'react-devtools-shared/src/backend/types';
|
||||
import type {
|
||||
DehydratedData,
|
||||
Element,
|
||||
InspectedElement as InspectedElementFrontend,
|
||||
} from 'react-devtools-shared/src/devtools/views/Components/types';
|
||||
|
|
@ -290,11 +291,11 @@ function InspectedElementContextController({children}: Props) {
|
|||
}
|
||||
|
||||
function hydrateHelper(
|
||||
dehydratedData: any | null,
|
||||
dehydratedData: DehydratedData | null,
|
||||
path?: Array<string | number>,
|
||||
): Object | null {
|
||||
if (dehydratedData !== null) {
|
||||
let {cleaned, data} = dehydratedData;
|
||||
let {cleaned, data, unserializable} = dehydratedData;
|
||||
|
||||
if (path) {
|
||||
const {length} = path;
|
||||
|
|
@ -302,10 +303,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import React, {useCallback} from 'react';
|
|||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import KeyValue from './KeyValue';
|
||||
import {serializeDataForCopy} from '../utils';
|
||||
import {alphaSortEntries, serializeDataForCopy} from '../utils';
|
||||
import styles from './InspectedElementTree.css';
|
||||
|
||||
import type {InspectPath} from './SelectedElement';
|
||||
|
|
@ -27,7 +27,12 @@ export default function InspectedElementTree({
|
|||
overrideValueFn,
|
||||
showWhenEmpty = false,
|
||||
}: Props) {
|
||||
const isEmpty = data === null || Object.keys(data).length === 0;
|
||||
const entries = data != null ? Object.entries(data) : null;
|
||||
if (entries !== null) {
|
||||
entries.sort(alphaSortEntries);
|
||||
}
|
||||
|
||||
const isEmpty = entries === null || entries.length === 0;
|
||||
|
||||
const handleCopy = useCallback(
|
||||
() => copy(serializeDataForCopy(((data: any): Object))),
|
||||
|
|
@ -49,15 +54,16 @@ export default function InspectedElementTree({
|
|||
</div>
|
||||
{isEmpty && <div className={styles.Empty}>None</div>}
|
||||
{!isEmpty &&
|
||||
Object.keys((data: any)).map(name => (
|
||||
(entries: any).map(([name, value]) => (
|
||||
<KeyValue
|
||||
key={name}
|
||||
alphaSort={true}
|
||||
depth={1}
|
||||
inspectPath={inspectPath}
|
||||
name={name}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={[name]}
|
||||
value={(data: any)[name]}
|
||||
value={value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,3 +35,7 @@
|
|||
flex: 0 0 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.Empty {
|
||||
color: var(--color-dimmer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React, {useEffect, useRef, useState} from 'react';
|
|||
import type {Element} from 'react';
|
||||
import EditableValue from './EditableValue';
|
||||
import ExpandCollapseToggle from './ExpandCollapseToggle';
|
||||
import {getMetaValueLabel} from '../utils';
|
||||
import {alphaSortEntries, getMetaValueLabel} from '../utils';
|
||||
import {meta} from '../../../hydration';
|
||||
import styles from './KeyValue.css';
|
||||
|
||||
|
|
@ -13,9 +13,11 @@ import type {InspectPath} from './SelectedElement';
|
|||
type OverrideValueFn = (path: Array<string | number>, value: any) => void;
|
||||
|
||||
type KeyValueProps = {|
|
||||
alphaSort: boolean,
|
||||
depth: number,
|
||||
hidden?: boolean,
|
||||
inspectPath?: InspectPath,
|
||||
isReadOnly?: boolean,
|
||||
name: string,
|
||||
overrideValueFn?: ?OverrideValueFn,
|
||||
path: Array<any>,
|
||||
|
|
@ -23,8 +25,10 @@ type KeyValueProps = {|
|
|||
|};
|
||||
|
||||
export default function KeyValue({
|
||||
alphaSort,
|
||||
depth,
|
||||
inspectPath,
|
||||
isReadOnly,
|
||||
hidden,
|
||||
name,
|
||||
overrideValueFn,
|
||||
|
|
@ -81,17 +85,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}
|
||||
/>
|
||||
|
|
@ -100,7 +105,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,8 +131,10 @@ export default function KeyValue({
|
|||
children = value.map((innerValue, index) => (
|
||||
<KeyValue
|
||||
key={index}
|
||||
alphaSort={alphaSort}
|
||||
depth={depth + 1}
|
||||
inspectPath={inspectPath}
|
||||
isReadOnly={isReadOnly}
|
||||
hidden={hidden || !isOpen}
|
||||
name={index}
|
||||
overrideValueFn={overrideValueFn}
|
||||
|
|
@ -148,26 +158,41 @@ export default function KeyValue({
|
|||
onClick={hasChildren ? toggleIsOpen : undefined}>
|
||||
{name}
|
||||
</span>
|
||||
<span>Array</span>
|
||||
<span>
|
||||
Array{' '}
|
||||
{hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
|
||||
</span>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
const hasChildren = Object.entries(value).length > 0;
|
||||
// TRICKY
|
||||
// It's important to use Object.entries() rather than Object.keys()
|
||||
// because of the hidden meta Symbols used for hydration and unserializable values.
|
||||
const entries = Object.entries(value);
|
||||
if (alphaSort) {
|
||||
entries.sort(alphaSortEntries);
|
||||
}
|
||||
|
||||
children = Object.entries(value).map<Element<any>>(
|
||||
([innerName, innerValue]) => (
|
||||
<KeyValue
|
||||
key={innerName}
|
||||
depth={depth + 1}
|
||||
inspectPath={inspectPath}
|
||||
hidden={hidden || !isOpen}
|
||||
name={innerName}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={path.concat(innerName)}
|
||||
value={innerValue}
|
||||
/>
|
||||
),
|
||||
);
|
||||
const hasChildren = entries.length > 0;
|
||||
const displayName = value.hasOwnProperty(meta.unserializable)
|
||||
? getMetaValueLabel(value)
|
||||
: 'Object';
|
||||
|
||||
let areChildrenReadOnly = isReadOnly || !!value[meta.readonly];
|
||||
children = entries.map<Element<any>>(([key, keyValue]) => (
|
||||
<KeyValue
|
||||
key={key}
|
||||
alphaSort={alphaSort}
|
||||
depth={depth + 1}
|
||||
inspectPath={inspectPath}
|
||||
isReadOnly={areChildrenReadOnly}
|
||||
hidden={hidden || !isOpen}
|
||||
name={key}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={path.concat(key)}
|
||||
value={keyValue}
|
||||
/>
|
||||
));
|
||||
children.unshift(
|
||||
<div
|
||||
key={`${depth}-root`}
|
||||
|
|
@ -184,7 +209,10 @@ export default function KeyValue({
|
|||
onClick={hasChildren ? toggleIsOpen : undefined}>
|
||||
{name}
|
||||
</span>
|
||||
<span>Object</span>
|
||||
<span>
|
||||
{`${displayName || ''} `}
|
||||
{hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
|
||||
</span>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ function Row({
|
|||
const validateAndSetLocalValue = newValue => {
|
||||
let isValid = false;
|
||||
try {
|
||||
JSON.parse(newValue);
|
||||
JSON.parse(sanitizeForParse(value));
|
||||
isValid = true;
|
||||
} catch (error) {}
|
||||
|
||||
|
|
@ -197,7 +197,7 @@ function Row({
|
|||
|
||||
const submitValueChange = () => {
|
||||
if (isValueValid) {
|
||||
const parsedLocalValue = JSON.parse(localValue);
|
||||
const parsedLocalValue = JSON.parse(sanitizeForParse(localValue));
|
||||
if (value !== parsedLocalValue) {
|
||||
changeValue(attribute, parsedLocalValue);
|
||||
}
|
||||
|
|
@ -281,3 +281,16 @@ function Field({
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// We use JSON.parse to parse string values
|
||||
// e.g. 'foo' is not valid JSON but it is a valid string
|
||||
// so this method replaces e.g. 'foo' with "foo"
|
||||
function sanitizeForParse(value: any) {
|
||||
if (typeof value === 'string') {
|
||||
if (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'") {
|
||||
return '"' + value.substr(1, value.length - 2) + '"';
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,4 +89,5 @@ export type DehydratedData = {|
|
|||
| Dehydrated
|
||||
| Array<Dehydrated>
|
||||
| {[key: string]: string | Dehydrated},
|
||||
unserializable: Array<Array<string | number>>,
|
||||
|};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import '@reach/menu-button/styles.css';
|
||||
import '@reach/tooltip/styles.css';
|
||||
|
||||
import React, {useMemo, useState} from 'react';
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import Store from '../store';
|
||||
import {BridgeContext, StoreContext} from './context';
|
||||
import Components from './Components/Components';
|
||||
|
|
@ -103,6 +103,19 @@ export default function DevTools({
|
|||
[canViewElementSourceFunction, viewElementSourceFunction],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
return () => {
|
||||
try {
|
||||
bridge.shutdown();
|
||||
} catch (error) {
|
||||
// Attempting to use a disconnected port.
|
||||
}
|
||||
};
|
||||
},
|
||||
[bridge],
|
||||
);
|
||||
|
||||
return (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
|
|
|
|||
|
|
@ -53,8 +53,11 @@ export default class ErrorBoundary extends Component<Props, State> {
|
|||
const title = `Error: "${errorMessage || ''}"`;
|
||||
const label = 'Component: Developer Tools';
|
||||
|
||||
let body = '<!-- please provide repro information here -->\n';
|
||||
body += '\n---------------------------------------------';
|
||||
let body = 'Describe what you were doing when the bug occurred:';
|
||||
body += '\n1. ';
|
||||
body += '\n2. ';
|
||||
body += '\n3. ';
|
||||
body += '\n\n---------------------------------------------';
|
||||
body += '\nPlease do not remove the text below this line';
|
||||
body += '\n---------------------------------------------';
|
||||
body += `\n\nDevTools version: ${process.env.DEVTOOLS_VERSION || ''}`;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ function Profiler(_: {||}) {
|
|||
didRecordCommits,
|
||||
isProcessingData,
|
||||
isProfiling,
|
||||
selectedCommitIndex,
|
||||
selectedFiberID,
|
||||
selectedTabID,
|
||||
selectTab,
|
||||
|
|
@ -67,10 +68,18 @@ function Profiler(_: {||}) {
|
|||
break;
|
||||
case 'flame-chart':
|
||||
case 'ranked-chart':
|
||||
if (selectedFiberID !== null) {
|
||||
sidebar = <SidebarSelectedFiberInfo />;
|
||||
} else {
|
||||
sidebar = <SidebarCommitInfo />;
|
||||
// TRICKY
|
||||
// Handle edge case where no commit is selected because of a min-duration filter update.
|
||||
// In that case, the selected commit index would be null.
|
||||
// We could still show a sidebar for the previously selected fiber,
|
||||
// but it would be an odd user experience.
|
||||
// TODO (ProfilerContext) This check should not be necessary.
|
||||
if (selectedCommitIndex !== null) {
|
||||
if (selectedFiberID !== null) {
|
||||
sidebar = <SidebarSelectedFiberInfo />;
|
||||
} else {
|
||||
sidebar = <SidebarCommitInfo />;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -126,28 +126,32 @@ function ProfilerContextController({children}: Props) {
|
|||
const [rootID, setRootID] = useState<number | null>(null);
|
||||
|
||||
if (prevProfilingData !== profilingData) {
|
||||
setPrevProfilingData(profilingData);
|
||||
batchedUpdates(() => {
|
||||
setPrevProfilingData(profilingData);
|
||||
|
||||
const dataForRoots =
|
||||
profilingData !== null ? profilingData.dataForRoots : null;
|
||||
if (dataForRoots != null) {
|
||||
const firstRootID = dataForRoots.keys().next().value || null;
|
||||
const dataForRoots =
|
||||
profilingData !== null ? profilingData.dataForRoots : null;
|
||||
if (dataForRoots != null) {
|
||||
const firstRootID = dataForRoots.keys().next().value || null;
|
||||
|
||||
if (rootID === null || !dataForRoots.has(rootID)) {
|
||||
let selectedElementRootID = null;
|
||||
if (selectedElementID !== null) {
|
||||
selectedElementRootID = store.getRootIDForElement(selectedElementID);
|
||||
}
|
||||
if (
|
||||
selectedElementRootID !== null &&
|
||||
dataForRoots.has(selectedElementRootID)
|
||||
) {
|
||||
setRootID(selectedElementRootID);
|
||||
} else {
|
||||
setRootID(firstRootID);
|
||||
if (rootID === null || !dataForRoots.has(rootID)) {
|
||||
let selectedElementRootID = null;
|
||||
if (selectedElementID !== null) {
|
||||
selectedElementRootID = store.getRootIDForElement(
|
||||
selectedElementID,
|
||||
);
|
||||
}
|
||||
if (
|
||||
selectedElementRootID !== null &&
|
||||
dataForRoots.has(selectedElementRootID)
|
||||
) {
|
||||
setRootID(selectedElementRootID);
|
||||
} else {
|
||||
setRootID(firstRootID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const startProfiling = useCallback(
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export default function SidebarSelectedFiberInfo(_: Props) {
|
|||
}
|
||||
|
||||
type WhatChangedProps = {|
|
||||
commitIndex: number,
|
||||
commitIndex: number | null,
|
||||
fiberID: number,
|
||||
profilerStore: ProfilerStore,
|
||||
rootID: number,
|
||||
|
|
@ -100,6 +100,14 @@ function WhatChanged({
|
|||
profilerStore,
|
||||
rootID,
|
||||
}: WhatChangedProps) {
|
||||
// TRICKY
|
||||
// Handle edge case where no commit is selected because of a min-duration filter update.
|
||||
// If the commit index is null, suspending for data below would throw an error.
|
||||
// TODO (ProfilerContext) This check should not be necessary.
|
||||
if (commitIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {changeDescriptions} = profilerStore.getCommitData(
|
||||
((rootID: any): number),
|
||||
commitIndex,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export default function SnapshotSelector(_: Props) {
|
|||
[filteredCommitIndices, selectedCommitIndex],
|
||||
);
|
||||
|
||||
// TODO (profiling) This should be managed by the context controller (reducer).
|
||||
// TODO (ProfilerContext) This should be managed by the context controller (reducer).
|
||||
// It doesn't currently know about the filtered commits though (since it doesn't suspend).
|
||||
// Maybe this component should pass filteredCommitIndices up?
|
||||
if (selectedFilteredCommitIndex === null) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,21 @@ import {meta} from '../../hydration';
|
|||
|
||||
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
|
||||
|
||||
export function alphaSortEntries(
|
||||
entryA: [string, mixed],
|
||||
entryB: [string, mixed],
|
||||
): number {
|
||||
const a = entryA[0];
|
||||
const b = entryB[0];
|
||||
if ('' + +a === a) {
|
||||
if ('' + +b !== b) {
|
||||
return -1;
|
||||
}
|
||||
return +a < +b ? -1 : 1;
|
||||
}
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
|
||||
export function createRegExp(string: string): RegExp {
|
||||
// Allow /regex/ syntax with optional last /
|
||||
if (string[0] === '/') {
|
||||
|
|
|
|||
161
packages/react-devtools-shared/src/hydration.js
vendored
161
packages/react-devtools-shared/src/hydration.js
vendored
|
|
@ -18,6 +18,8 @@ import {
|
|||
} from 'react-is';
|
||||
import {getDisplayName, getInObject, setInObject} from './utils';
|
||||
|
||||
import type {DehydratedData} from 'react-devtools-shared/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'),
|
||||
};
|
||||
|
||||
export type Dehydrated = {|
|
||||
|
|
@ -35,6 +38,18 @@ export 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,16 +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
|
||||
| Array<Dehydrated>
|
||||
| {[key: string]: string | Dehydrated} {
|
||||
| Unserializable
|
||||
| {[key: string]: string | Dehydrated | Unserializable} {
|
||||
const type = getDataType(data);
|
||||
|
||||
let isPathWhitelistedCheck;
|
||||
|
||||
switch (type) {
|
||||
case 'html_element':
|
||||
cleaned.push(path);
|
||||
|
|
@ -233,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);
|
||||
|
|
@ -260,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 = {};
|
||||
|
|
@ -269,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;
|
||||
|
|
@ -294,24 +346,43 @@ 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.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: Object,
|
||||
cleaned: Array<Array<string | number>>,
|
||||
unserializable: Array<Array<string | number>>,
|
||||
): Object {
|
||||
cleaned.forEach((path: Array<string | number>) => {
|
||||
const length = path.length;
|
||||
|
|
@ -342,9 +413,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 {
|
||||
|
|
|
|||
16
packages/react-devtools-shared/src/utils.js
vendored
16
packages/react-devtools-shared/src/utils.js
vendored
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open"
|
||||
},
|
||||
"dependencies": {
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"react-native-web": "^0.11.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import UnserializableProps from './UnserializableProps';
|
||||
import Contexts from './Contexts';
|
||||
import CustomHooks from './CustomHooks';
|
||||
import CustomObject from './CustomObject';
|
||||
|
|
@ -14,6 +15,7 @@ export default function InspectableElements() {
|
|||
<Fragment>
|
||||
<h1>Inspectable elements</h1>
|
||||
<SimpleValues />
|
||||
<UnserializableProps />
|
||||
<NestedProps />
|
||||
<Contexts />
|
||||
<CustomHooks />
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ export default function ObjectProps() {
|
|||
},
|
||||
},
|
||||
}}
|
||||
emptyArray={[]}
|
||||
emptyObject={{}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
35
packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js
vendored
Normal file
35
packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js
vendored
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -62,7 +62,9 @@ export default function List(props: Props) {
|
|||
|
||||
const toggleItem = useCallback(
|
||||
itemToToggle => {
|
||||
const index = items.indexOf(itemToToggle);
|
||||
// Dont use indexOf()
|
||||
// because editing props in DevTools creates a new Object.
|
||||
const index = items.findIndex(item => item.id === itemToToggle.id);
|
||||
|
||||
setItems(
|
||||
items
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"name": "react-devtools",
|
||||
"version": "4.0.0-alpha.9",
|
||||
"version": "4.0.5",
|
||||
"description": "Use react-devtools outside of the browser",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"url": "https://github.com/bvaughn/react-devtools-experimental.git",
|
||||
"type": "git"
|
||||
"type": "git",
|
||||
"url": "https://github.com/facebook/react.git",
|
||||
"directory": "packages/react-devtools"
|
||||
},
|
||||
"bin": {
|
||||
"react-devtools": "./bin.js"
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
"electron": "^5.0.0",
|
||||
"ip": "^1.1.4",
|
||||
"minimist": "^1.2.0",
|
||||
"react-devtools-core": "4.0.0-alpha.9",
|
||||
"react-devtools-core": "4.0.5",
|
||||
"update-notifier": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6435,6 +6435,11 @@ immediate@~3.0.5:
|
|||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
|
||||
|
||||
immutable@^4.0.0-rc.12:
|
||||
version "4.0.0-rc.12"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0-rc.12.tgz#ca59a7e4c19ae8d9bf74a97bdf0f6e2f2a5d0217"
|
||||
integrity sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==
|
||||
|
||||
import-fresh@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user