Merged changes from 4.0.0 -> 4.0.5 from DevTools fork

This commit is contained in:
Brian Vaughn 2019-08-20 11:34:51 -07:00
commit 4da836af71
44 changed files with 877 additions and 229 deletions

View File

@ -9,6 +9,7 @@
<script src="https://unpkg.com/react@16/umd/react.development.js"></script> <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/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: --> <!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script> <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) { function ChildComponent(props: any) {
return null; return null;
} }
@ -264,6 +292,7 @@
<Fragment> <Fragment>
<SimpleValues /> <SimpleValues />
<ObjectProps /> <ObjectProps />
<UnserializableProps />
<CustomObject /> <CustomObject />
</Fragment> </Fragment>
); );

View File

@ -1,12 +1,13 @@
{ {
"name": "react-devtools-core", "name": "react-devtools-core",
"version": "4.0.0-alpha.9", "version": "4.0.5",
"description": "Use react-devtools outside of the browser", "description": "Use react-devtools outside of the browser",
"license": "MIT", "license": "MIT",
"main": "./dist/backend.js", "main": "./dist/backend.js",
"repository": { "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": [ "files": [
"dist", "dist",

View File

@ -14,7 +14,8 @@ import {
getAppendComponentStack, getAppendComponentStack,
} from 'react-devtools-shared/src/utils'; } from 'react-devtools-shared/src/utils';
import {Server} from 'ws'; 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 {installHook} from 'react-devtools-shared/src/hook';
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools'; import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
import {doesFilePathExist, launchEditor} from './editor'; import {doesFilePathExist, launchEditor} from './editor';
@ -259,14 +260,8 @@ function startServer(port?: number = 8097) {
}); });
httpServer.on('request', (request, response) => { 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. // 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, // The renderer interface doesn't read saved component filters directly,
// because they are generally stored in localStorage within the context of the extension. // because they are generally stored in localStorage within the context of the extension.

View File

@ -40,6 +40,12 @@ module.exports = {
scheduler: resolve(builtModulesDir, 'scheduler'), 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: [ plugins: [
new DefinePlugin({ new DefinePlugin({
__DEV__: false, __DEV__: false,

View File

@ -61,14 +61,13 @@ const build = async (tempPath, manifestPath) => {
); );
const commit = getGitCommit(); const commit = getGitCommit();
const versionDateString = `${commit} (${new Date().toLocaleDateString()})`; const dateString = new Date().toLocaleDateString();
const manifest = JSON.parse(readFileSync(copiedManifestPath).toString()); const manifest = JSON.parse(readFileSync(copiedManifestPath).toString());
const versionDateString = `${manifest.version} (${dateString})`;
if (manifest.version_name) { if (manifest.version_name) {
manifest.version_name = versionDateString; 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)); writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2));

View File

@ -2,8 +2,8 @@
"manifest_version": 2, "manifest_version": 2,
"name": "React Developer Tools", "name": "React Developer Tools",
"description": "Adds React debugging tools to the Chrome Developer Tools.", "description": "Adds React debugging tools to the Chrome Developer Tools.",
"version": "4.0.0", "version": "4.0.5",
"version_name": "4.0.0", "version_name": "4.0.5",
"minimum_chrome_version": "49", "minimum_chrome_version": "49",
@ -40,15 +40,7 @@
"persistent": false "persistent": false
}, },
"permissions": [ "permissions": ["file:///*", "http://*/*", "https://*/*"],
"<all_urls>",
"background",
"tabs",
"webNavigation",
"file:///*",
"http://*/*",
"https://*/*"
],
"content_scripts": [ "content_scripts": [
{ {

View File

@ -18,12 +18,11 @@
<h3> <h3>
Created on <strong>%date%</strong> from 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> </h3>
<p> <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>. This is a preview build of the <a href="https://github.com/facebook/react">React DevTools extension</a>.
It has no developer support.
</p> </p>
<h2>Installation instructions</h2> <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 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). Please include all of the info required to reproduce the bug (e.g. links, code, instructions).
</p> </p>
<h2>Feature requests</h2>
<p>
Feature requests are not being accepted at this time.
</p>
</body> </body>
</html> </html>

View File

@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "React Developer Tools", "name": "React Developer Tools",
"description": "Adds React debugging tools to the Firefox Developer Tools.", "description": "Adds React debugging tools to the Firefox Developer Tools.",
"version": "4.0.0", "version": "4.0.5",
"applications": { "applications": {
"gecko": { "gecko": {
@ -44,15 +44,7 @@
"scripts": ["build/background.js"] "scripts": ["build/background.js"]
}, },
"permissions": [ "permissions": ["file:///*", "http://*/*", "https://*/*"],
"<all_urls>",
"activeTab",
"tabs",
"webNavigation",
"file:///*",
"http://*/*",
"https://*/*"
],
"content_scripts": [ "content_scripts": [
{ {

View File

@ -120,6 +120,10 @@ function createPanelIfReactLoaded() {
localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY); localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY);
} }
if (store !== null) {
profilingData = store.profilerStore.profilingData;
}
store = new Store(bridge, { store = new Store(bridge, {
isProfiling, isProfiling,
supportsReloadAndProfile: getBrowserName() === 'Chrome', supportsReloadAndProfile: getBrowserName() === 'Chrome',
@ -281,21 +285,6 @@ function createPanelIfReactLoaded() {
chrome.devtools.network.onNavigated.removeListener(checkPageForReact); 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. // Re-initialize DevTools panel when a new page is loaded.
chrome.devtools.network.onNavigated.addListener(function onNavigated() { chrome.devtools.network.onNavigated.addListener(function onNavigated() {
// Re-initialize saved filters on navigation, // Re-initialize saved filters on navigation,

View File

@ -1,12 +1,13 @@
{ {
"name": "react-devtools-inline", "name": "react-devtools-inline",
"version": "4.0.0-alpha.9", "version": "4.0.5",
"description": "Embed react-devtools within a website", "description": "Embed react-devtools within a website",
"license": "MIT", "license": "MIT",
"main": "./dist/backend.js", "main": "./dist/backend.js",
"repository": { "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": [ "files": [
"dist", "dist",

View File

@ -1,5 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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`] = ` exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
{ {
"id": 2, "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`] = ` exports[`InspectedElementContext should inspect the currently selected element: 1: Inspected element 2 1`] = `
{ {
"id": 2, "id": 2,
@ -427,13 +485,38 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
"context": null, "context": null,
"hooks": null, "hooks": null,
"props": { "props": {
"html_element": {},
"fn": {},
"symbol": {},
"react_element": {},
"array_buffer": {}, "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 "state": null
} }

View File

@ -385,23 +385,42 @@ describe('InspectedElementContext', () => {
}); });
it('should support complex data types', async done => { it('should support complex data types', async done => {
const Immutable = require('immutable');
const Example = () => null; const Example = () => null;
const div = document.createElement('div'); const div = document.createElement('div');
const exmapleFunction = () => {}; const exampleFunction = () => {};
const typedArray = new Uint8Array(3); 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'); const container = document.createElement('div');
await utils.actAsync(() => await utils.actAsync(() =>
ReactDOM.render( ReactDOM.render(
<Example <Example
html_element={div}
fn={exmapleFunction}
symbol={Symbol('symbol')}
react_element={<span />}
array_buffer={typedArray.buffer} array_buffer={typedArray.buffer}
typed_array={typedArray}
date={new Date()} 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, container,
), ),
@ -435,37 +454,77 @@ describe('InspectedElementContext', () => {
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`); expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
const { const {
html_element,
fn,
symbol,
react_element,
array_buffer, array_buffer,
typed_array,
date, date,
fn,
html_element,
immutable,
map,
map_of_maps,
react_element,
set,
set_of_sets,
symbol,
typed_array,
} = (inspectedElement: any).props; } = (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.size]).toBe(3);
expect(array_buffer[meta.inspectable]).toBe(false); expect(array_buffer[meta.inspectable]).toBe(false);
expect(array_buffer[meta.name]).toBe('ArrayBuffer'); expect(array_buffer[meta.name]).toBe('ArrayBuffer');
expect(array_buffer[meta.type]).toBe('array_buffer'); 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.inspectable]).toBe(false);
expect(date[meta.type]).toBe('date'); 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(); done();
}); });
@ -655,6 +714,61 @@ describe('InspectedElementContext', () => {
done(); 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 => { it('should include updates for nested values that were previously hydrated', async done => {
const Example = () => null; const Example = () => null;
@ -846,4 +960,46 @@ describe('InspectedElementContext', () => {
done(); 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();
});
}); });

View File

@ -126,13 +126,38 @@ Object {
"context": {}, "context": {},
"hooks": null, "hooks": null,
"props": { "props": {
"html_element": {},
"fn": {},
"symbol": {},
"react_element": {},
"array_buffer": {}, "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 "state": null
}, },

View File

@ -23,7 +23,11 @@ describe('InspectedElementContext', () => {
dehydratedData: DehydratedData | null, dehydratedData: DehydratedData | null,
): Object | null { ): Object | null {
if (dehydratedData !== null) { if (dehydratedData !== null) {
return hydrate(dehydratedData.data, dehydratedData.cleaned); return hydrate(
dehydratedData.data,
dehydratedData.cleaned,
dehydratedData.unserializable,
);
} else { } else {
return null; return null;
} }
@ -132,22 +136,41 @@ describe('InspectedElementContext', () => {
}); });
it('should support complex data types', async done => { it('should support complex data types', async done => {
const Immutable = require('immutable');
const Example = () => null; const Example = () => null;
const div = document.createElement('div'); const div = document.createElement('div');
const exmapleFunction = () => {}; const exampleFunction = () => {};
const typedArray = new Uint8Array(3); 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(() => act(() =>
ReactDOM.render( ReactDOM.render(
<Example <Example
html_element={div}
fn={exmapleFunction}
symbol={Symbol('symbol')}
react_element={<span />}
array_buffer={typedArray.buffer} array_buffer={typedArray.buffer}
typed_array={typedArray}
date={new Date()} 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'), document.createElement('div'),
), ),
@ -159,37 +182,77 @@ describe('InspectedElementContext', () => {
expect(inspectedElement).toMatchSnapshot('1: Initial inspection'); expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
const { const {
html_element,
fn,
symbol,
react_element,
array_buffer, array_buffer,
typed_array,
date, date,
fn,
html_element,
immutable,
map,
map_of_maps,
react_element,
set,
set_of_sets,
symbol,
typed_array,
} = inspectedElement.value.props; } = 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.size]).toBe(3);
expect(array_buffer[meta.inspectable]).toBe(false); expect(array_buffer[meta.inspectable]).toBe(false);
expect(array_buffer[meta.name]).toBe('ArrayBuffer'); expect(array_buffer[meta.name]).toBe('ArrayBuffer');
expect(array_buffer[meta.type]).toBe('array_buffer'); 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.inspectable]).toBe(false);
expect(date[meta.type]).toBe('date'); 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(); done();
}); });

View File

@ -136,7 +136,7 @@ describe('ProfilerContext', () => {
expect(context.didRecordCommits).toBe(false); expect(context.didRecordCommits).toBe(false);
expect(context.isProcessingData).toBe(false); expect(context.isProcessingData).toBe(false);
expect(context.isProfiling).toBe(false); expect(context.isProfiling).toBe(false);
expect(context.profilingData).not.toBe(null); expect(context.profilingData).toBe(null);
done(); done();
}); });

View File

@ -882,10 +882,10 @@ export function attach(
throw new Error('setInHook not supported by this renderer'); throw new Error('setInHook not supported by this renderer');
}; };
const startProfiling = () => { 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 = () => { 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 { function getBestMatchForTrackedPath(): PathMatch | null {

View File

@ -1621,7 +1621,9 @@ export function attach(
currentRootID = getFiberID(getPrimaryFiber(root.current)); currentRootID = getFiberID(getPrimaryFiber(root.current));
setRootPseudoKey(currentRootID, 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. // If profiling is active, store commit time and duration, and the current interactions.
// The frontend may request this information after profiling has stopped. // The frontend may request this information after profiling has stopped.
currentCommitProfilingMetadata = { currentCommitProfilingMetadata = {
@ -1665,7 +1667,11 @@ export function attach(
mightBeOnTrackedPath = true; 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. // If profiling is active, store commit time and duration, and the current interactions.
// The frontend may request this information after profiling has stopped. // The frontend may request this information after profiling has stopped.
currentCommitProfilingMetadata = { currentCommitProfilingMetadata = {
@ -1709,7 +1715,7 @@ export function attach(
mountFiberRecursively(current, null); mountFiberRecursively(current, null);
} }
if (isProfiling) { if (isProfiling && isProfilingSupported) {
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get( const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
currentRootID, currentRootID,
); );
@ -2083,6 +2089,7 @@ export function attach(
const { const {
_debugOwner, _debugOwner,
_debugSource, _debugSource,
dependencies,
stateNode, stateNode,
memoizedProps, memoizedProps,
memoizedState, memoizedState,
@ -2094,7 +2101,7 @@ export function attach(
(tag === FunctionComponent || (tag === FunctionComponent ||
tag === SimpleMemoComponent || tag === SimpleMemoComponent ||
tag === ForwardRef) && tag === ForwardRef) &&
!!memoizedState; (!!memoizedState || !!dependencies);
const typeSymbol = getTypeSymbol(type); const typeSymbol = getTypeSymbol(type);

View File

@ -21,6 +21,7 @@ export type Source = {|
fileName: string, fileName: string,
lineNumber: number, lineNumber: number,
|}; |};
export type HookType = export type HookType =
| 'useState' | 'useState'
| 'useReducer' | 'useReducer'
@ -41,6 +42,9 @@ export type Fiber = {|
key: null | string, key: null | string,
// Dependencies (contexts, events) for this fiber, if it has any
dependencies: mixed | null,
elementType: any, elementType: any,
type: any, type: any,

View File

@ -10,11 +10,20 @@ export function cleanForBridge(
path?: Array<string | number> = [], path?: Array<string | number> = [],
): DehydratedData | null { ): DehydratedData | null {
if (data !== null) { if (data !== null) {
const cleaned = []; const cleanedPaths = [];
const unserializablePaths = [];
const cleanedData = dehydrate(
data,
cleanedPaths,
unserializablePaths,
path,
isPathWhitelisted,
);
return { return {
data: dehydrate(data, cleaned, path, isPathWhitelisted), data: cleanedData,
cleaned, cleaned: cleanedPaths,
unserializable: unserializablePaths,
}; };
} else { } else {
return null; return null;

View File

@ -26,7 +26,7 @@ export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
export const PROFILER_EXPORT_VERSION = 4; export const PROFILER_EXPORT_VERSION = 4;
export const CHANGE_LOG_URL = 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 // HACK
// //

View File

@ -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. // When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
_isProfiling: boolean = false; _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. // 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. // So long as this queue is not empty, the store is retrieving and processing profiling data from the backend.
_rendererQueue: Set<number> = new Set(); _rendererQueue: Set<number> = new Set();
@ -233,6 +236,8 @@ export default class ProfilerStore extends EventEmitter<{|
if (!this._initialSnapshotsByRootID.has(rootID)) { if (!this._initialSnapshotsByRootID.has(rootID)) {
this._initialSnapshotsByRootID.set(rootID, new Map()); this._initialSnapshotsByRootID.set(rootID, new Map());
} }
this._rendererIDsThatReportedProfilingData.add(rendererID);
} }
}; };
@ -280,6 +285,7 @@ export default class ProfilerStore extends EventEmitter<{|
this._initialRendererIDs.clear(); this._initialRendererIDs.clear();
this._initialSnapshotsByRootID.clear(); this._initialSnapshotsByRootID.clear();
this._inProgressOperationsByRootID.clear(); this._inProgressOperationsByRootID.clear();
this._rendererIDsThatReportedProfilingData.clear();
this._rendererQueue.clear(); this._rendererQueue.clear();
// Record all renderer IDs initially too (in case of unmount) // 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._dataBackends.splice(0);
this._rendererQueue.clear(); 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)) { if (!this._rendererQueue.has(rendererID)) {
this._rendererQueue.add(rendererID); this._rendererQueue.add(rendererID);

View File

@ -158,6 +158,7 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
) : ( ) : (
<KeyValue <KeyValue
depth={1} depth={1}
alphaSort={false}
inspectPath={inspectPath} inspectPath={inspectPath}
name="subHooks" name="subHooks"
path={path.concat(['subHooks'])} path={path.concat(['subHooks'])}
@ -179,6 +180,7 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
<div className={styles.Children} hidden={!isOpen}> <div className={styles.Children} hidden={!isOpen}>
<KeyValue <KeyValue
depth={1} depth={1}
alphaSort={false}
inspectPath={inspectPath} inspectPath={inspectPath}
name="DebugValue" name="DebugValue"
path={path.concat(['value'])} path={path.concat(['value'])}
@ -237,6 +239,7 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
<div className={styles.Hook}> <div className={styles.Hook}>
<KeyValue <KeyValue
depth={1} depth={1}
alphaSort={false}
inspectPath={inspectPath} inspectPath={inspectPath}
name={name} name={name}
overrideValueFn={overrideValueFn} overrideValueFn={overrideValueFn}

View File

@ -20,6 +20,7 @@ import type {
InspectedElementPayload, InspectedElementPayload,
} from 'react-devtools-shared/src/backend/types'; } from 'react-devtools-shared/src/backend/types';
import type { import type {
DehydratedData,
Element, Element,
InspectedElement as InspectedElementFrontend, InspectedElement as InspectedElementFrontend,
} from 'react-devtools-shared/src/devtools/views/Components/types'; } from 'react-devtools-shared/src/devtools/views/Components/types';
@ -290,11 +291,11 @@ function InspectedElementContextController({children}: Props) {
} }
function hydrateHelper( function hydrateHelper(
dehydratedData: any | null, dehydratedData: DehydratedData | null,
path?: Array<string | number>, path?: Array<string | number>,
): Object | null { ): Object | null {
if (dehydratedData !== null) { if (dehydratedData !== null) {
let {cleaned, data} = dehydratedData; let {cleaned, data, unserializable} = dehydratedData;
if (path) { if (path) {
const {length} = path; const {length} = path;
@ -302,10 +303,13 @@ function hydrateHelper(
// Hydration helper requires full paths, but inspection dehydrates with relative paths. // 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. // In that event it's important that we adjust the "cleaned" paths to match.
cleaned = cleaned.map(cleanedPath => cleanedPath.slice(length)); cleaned = cleaned.map(cleanedPath => cleanedPath.slice(length));
unserializable = unserializable.map(unserializablePath =>
unserializablePath.slice(length),
);
} }
} }
return hydrate(data, cleaned); return hydrate(data, cleaned, unserializable);
} else { } else {
return null; return null;
} }

View File

@ -5,7 +5,7 @@ import React, {useCallback} from 'react';
import Button from '../Button'; import Button from '../Button';
import ButtonIcon from '../ButtonIcon'; import ButtonIcon from '../ButtonIcon';
import KeyValue from './KeyValue'; import KeyValue from './KeyValue';
import {serializeDataForCopy} from '../utils'; import {alphaSortEntries, serializeDataForCopy} from '../utils';
import styles from './InspectedElementTree.css'; import styles from './InspectedElementTree.css';
import type {InspectPath} from './SelectedElement'; import type {InspectPath} from './SelectedElement';
@ -27,7 +27,12 @@ export default function InspectedElementTree({
overrideValueFn, overrideValueFn,
showWhenEmpty = false, showWhenEmpty = false,
}: Props) { }: 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( const handleCopy = useCallback(
() => copy(serializeDataForCopy(((data: any): Object))), () => copy(serializeDataForCopy(((data: any): Object))),
@ -49,15 +54,16 @@ export default function InspectedElementTree({
</div> </div>
{isEmpty && <div className={styles.Empty}>None</div>} {isEmpty && <div className={styles.Empty}>None</div>}
{!isEmpty && {!isEmpty &&
Object.keys((data: any)).map(name => ( (entries: any).map(([name, value]) => (
<KeyValue <KeyValue
key={name} key={name}
alphaSort={true}
depth={1} depth={1}
inspectPath={inspectPath} inspectPath={inspectPath}
name={name} name={name}
overrideValueFn={overrideValueFn} overrideValueFn={overrideValueFn}
path={[name]} path={[name]}
value={(data: any)[name]} value={value}
/> />
))} ))}
</div> </div>

View File

@ -35,3 +35,7 @@
flex: 0 0 1rem; flex: 0 0 1rem;
width: 1rem; width: 1rem;
} }
.Empty {
color: var(--color-dimmer);
}

View File

@ -4,7 +4,7 @@ import React, {useEffect, useRef, useState} from 'react';
import type {Element} from 'react'; import type {Element} from 'react';
import EditableValue from './EditableValue'; import EditableValue from './EditableValue';
import ExpandCollapseToggle from './ExpandCollapseToggle'; import ExpandCollapseToggle from './ExpandCollapseToggle';
import {getMetaValueLabel} from '../utils'; import {alphaSortEntries, getMetaValueLabel} from '../utils';
import {meta} from '../../../hydration'; import {meta} from '../../../hydration';
import styles from './KeyValue.css'; import styles from './KeyValue.css';
@ -13,9 +13,11 @@ import type {InspectPath} from './SelectedElement';
type OverrideValueFn = (path: Array<string | number>, value: any) => void; type OverrideValueFn = (path: Array<string | number>, value: any) => void;
type KeyValueProps = {| type KeyValueProps = {|
alphaSort: boolean,
depth: number, depth: number,
hidden?: boolean, hidden?: boolean,
inspectPath?: InspectPath, inspectPath?: InspectPath,
isReadOnly?: boolean,
name: string, name: string,
overrideValueFn?: ?OverrideValueFn, overrideValueFn?: ?OverrideValueFn,
path: Array<any>, path: Array<any>,
@ -23,8 +25,10 @@ type KeyValueProps = {|
|}; |};
export default function KeyValue({ export default function KeyValue({
alphaSort,
depth, depth,
inspectPath, inspectPath,
isReadOnly,
hidden, hidden,
name, name,
overrideValueFn, overrideValueFn,
@ -81,17 +85,18 @@ export default function KeyValue({
displayValue = 'undefined'; displayValue = 'undefined';
} }
const nameClassName = const isEditable = typeof overrideValueFn === 'function' && !isReadOnly;
typeof overrideValueFn === 'function' ? styles.EditableName : styles.Name;
children = ( children = (
<div key="root" className={styles.Item} hidden={hidden} style={style}> <div key="root" className={styles.Item} hidden={hidden} style={style}>
<div className={styles.ExpandCollapseToggleSpacer} /> <div className={styles.ExpandCollapseToggleSpacer} />
<span className={nameClassName}>{name}</span> <span className={isEditable ? styles.EditableName : styles.Name}>
{typeof overrideValueFn === 'function' ? ( {name}
</span>
{isEditable ? (
<EditableValue <EditableValue
dataType={dataType} dataType={dataType}
overrideValueFn={overrideValueFn} overrideValueFn={((overrideValueFn: any): OverrideValueFn)}
path={path} path={path}
value={value} value={value}
/> />
@ -100,7 +105,10 @@ export default function KeyValue({
)} )}
</div> </div>
); );
} else if (value.hasOwnProperty(meta.type)) { } else if (
value.hasOwnProperty(meta.type) &&
!value.hasOwnProperty(meta.unserializable)
) {
children = ( children = (
<div key="root" className={styles.Item} hidden={hidden} style={style}> <div key="root" className={styles.Item} hidden={hidden} style={style}>
{isInspectable ? ( {isInspectable ? (
@ -123,8 +131,10 @@ export default function KeyValue({
children = value.map((innerValue, index) => ( children = value.map((innerValue, index) => (
<KeyValue <KeyValue
key={index} key={index}
alphaSort={alphaSort}
depth={depth + 1} depth={depth + 1}
inspectPath={inspectPath} inspectPath={inspectPath}
isReadOnly={isReadOnly}
hidden={hidden || !isOpen} hidden={hidden || !isOpen}
name={index} name={index}
overrideValueFn={overrideValueFn} overrideValueFn={overrideValueFn}
@ -148,26 +158,41 @@ export default function KeyValue({
onClick={hasChildren ? toggleIsOpen : undefined}> onClick={hasChildren ? toggleIsOpen : undefined}>
{name} {name}
</span> </span>
<span>Array</span> <span>
Array{' '}
{hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
</span>
</div>, </div>,
); );
} else { } 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>>( const hasChildren = entries.length > 0;
([innerName, innerValue]) => ( const displayName = value.hasOwnProperty(meta.unserializable)
<KeyValue ? getMetaValueLabel(value)
key={innerName} : 'Object';
depth={depth + 1}
inspectPath={inspectPath} let areChildrenReadOnly = isReadOnly || !!value[meta.readonly];
hidden={hidden || !isOpen} children = entries.map<Element<any>>(([key, keyValue]) => (
name={innerName} <KeyValue
overrideValueFn={overrideValueFn} key={key}
path={path.concat(innerName)} alphaSort={alphaSort}
value={innerValue} depth={depth + 1}
/> inspectPath={inspectPath}
), isReadOnly={areChildrenReadOnly}
); hidden={hidden || !isOpen}
name={key}
overrideValueFn={overrideValueFn}
path={path.concat(key)}
value={keyValue}
/>
));
children.unshift( children.unshift(
<div <div
key={`${depth}-root`} key={`${depth}-root`}
@ -184,7 +209,10 @@ export default function KeyValue({
onClick={hasChildren ? toggleIsOpen : undefined}> onClick={hasChildren ? toggleIsOpen : undefined}>
{name} {name}
</span> </span>
<span>Object</span> <span>
{`${displayName || ''} `}
{hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
</span>
</div>, </div>,
); );
} }

View File

@ -177,7 +177,7 @@ function Row({
const validateAndSetLocalValue = newValue => { const validateAndSetLocalValue = newValue => {
let isValid = false; let isValid = false;
try { try {
JSON.parse(newValue); JSON.parse(sanitizeForParse(value));
isValid = true; isValid = true;
} catch (error) {} } catch (error) {}
@ -197,7 +197,7 @@ function Row({
const submitValueChange = () => { const submitValueChange = () => {
if (isValueValid) { if (isValueValid) {
const parsedLocalValue = JSON.parse(localValue); const parsedLocalValue = JSON.parse(sanitizeForParse(localValue));
if (value !== parsedLocalValue) { if (value !== parsedLocalValue) {
changeValue(attribute, 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;
}

View File

@ -89,4 +89,5 @@ export type DehydratedData = {|
| Dehydrated | Dehydrated
| Array<Dehydrated> | Array<Dehydrated>
| {[key: string]: string | Dehydrated}, | {[key: string]: string | Dehydrated},
unserializable: Array<Array<string | number>>,
|}; |};

View File

@ -5,7 +5,7 @@
import '@reach/menu-button/styles.css'; import '@reach/menu-button/styles.css';
import '@reach/tooltip/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 Store from '../store';
import {BridgeContext, StoreContext} from './context'; import {BridgeContext, StoreContext} from './context';
import Components from './Components/Components'; import Components from './Components/Components';
@ -103,6 +103,19 @@ export default function DevTools({
[canViewElementSourceFunction, viewElementSourceFunction], [canViewElementSourceFunction, viewElementSourceFunction],
); );
useEffect(
() => {
return () => {
try {
bridge.shutdown();
} catch (error) {
// Attempting to use a disconnected port.
}
};
},
[bridge],
);
return ( return (
<BridgeContext.Provider value={bridge}> <BridgeContext.Provider value={bridge}>
<StoreContext.Provider value={store}> <StoreContext.Provider value={store}>

View File

@ -53,8 +53,11 @@ export default class ErrorBoundary extends Component<Props, State> {
const title = `Error: "${errorMessage || ''}"`; const title = `Error: "${errorMessage || ''}"`;
const label = 'Component: Developer Tools'; const label = 'Component: Developer Tools';
let body = '<!-- please provide repro information here -->\n'; let body = 'Describe what you were doing when the bug occurred:';
body += '\n---------------------------------------------'; body += '\n1. ';
body += '\n2. ';
body += '\n3. ';
body += '\n\n---------------------------------------------';
body += '\nPlease do not remove the text below this line'; body += '\nPlease do not remove the text below this line';
body += '\n---------------------------------------------'; body += '\n---------------------------------------------';
body += `\n\nDevTools version: ${process.env.DEVTOOLS_VERSION || ''}`; body += `\n\nDevTools version: ${process.env.DEVTOOLS_VERSION || ''}`;

View File

@ -28,6 +28,7 @@ function Profiler(_: {||}) {
didRecordCommits, didRecordCommits,
isProcessingData, isProcessingData,
isProfiling, isProfiling,
selectedCommitIndex,
selectedFiberID, selectedFiberID,
selectedTabID, selectedTabID,
selectTab, selectTab,
@ -67,10 +68,18 @@ function Profiler(_: {||}) {
break; break;
case 'flame-chart': case 'flame-chart':
case 'ranked-chart': case 'ranked-chart':
if (selectedFiberID !== null) { // TRICKY
sidebar = <SidebarSelectedFiberInfo />; // Handle edge case where no commit is selected because of a min-duration filter update.
} else { // In that case, the selected commit index would be null.
sidebar = <SidebarCommitInfo />; // 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; break;
default: default:

View File

@ -126,28 +126,32 @@ function ProfilerContextController({children}: Props) {
const [rootID, setRootID] = useState<number | null>(null); const [rootID, setRootID] = useState<number | null>(null);
if (prevProfilingData !== profilingData) { if (prevProfilingData !== profilingData) {
setPrevProfilingData(profilingData); batchedUpdates(() => {
setPrevProfilingData(profilingData);
const dataForRoots = const dataForRoots =
profilingData !== null ? profilingData.dataForRoots : null; profilingData !== null ? profilingData.dataForRoots : null;
if (dataForRoots != null) { if (dataForRoots != null) {
const firstRootID = dataForRoots.keys().next().value || null; const firstRootID = dataForRoots.keys().next().value || null;
if (rootID === null || !dataForRoots.has(rootID)) { if (rootID === null || !dataForRoots.has(rootID)) {
let selectedElementRootID = null; let selectedElementRootID = null;
if (selectedElementID !== null) { if (selectedElementID !== null) {
selectedElementRootID = store.getRootIDForElement(selectedElementID); selectedElementRootID = store.getRootIDForElement(
} selectedElementID,
if ( );
selectedElementRootID !== null && }
dataForRoots.has(selectedElementRootID) if (
) { selectedElementRootID !== null &&
setRootID(selectedElementRootID); dataForRoots.has(selectedElementRootID)
} else { ) {
setRootID(firstRootID); setRootID(selectedElementRootID);
} else {
setRootID(firstRootID);
}
} }
} }
} });
} }
const startProfiling = useCallback( const startProfiling = useCallback(

View File

@ -88,7 +88,7 @@ export default function SidebarSelectedFiberInfo(_: Props) {
} }
type WhatChangedProps = {| type WhatChangedProps = {|
commitIndex: number, commitIndex: number | null,
fiberID: number, fiberID: number,
profilerStore: ProfilerStore, profilerStore: ProfilerStore,
rootID: number, rootID: number,
@ -100,6 +100,14 @@ function WhatChanged({
profilerStore, profilerStore,
rootID, rootID,
}: WhatChangedProps) { }: 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( const {changeDescriptions} = profilerStore.getCommitData(
((rootID: any): number), ((rootID: any): number),
commitIndex, commitIndex,

View File

@ -62,7 +62,7 @@ export default function SnapshotSelector(_: Props) {
[filteredCommitIndices, selectedCommitIndex], [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). // It doesn't currently know about the filtered commits though (since it doesn't suspend).
// Maybe this component should pass filteredCommitIndices up? // Maybe this component should pass filteredCommitIndices up?
if (selectedFilteredCommitIndex === null) { if (selectedFilteredCommitIndex === null) {

View File

@ -5,6 +5,21 @@ import {meta} from '../../hydration';
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; 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 { export function createRegExp(string: string): RegExp {
// Allow /regex/ syntax with optional last / // Allow /regex/ syntax with optional last /
if (string[0] === '/') { if (string[0] === '/') {

View File

@ -18,6 +18,8 @@ import {
} from 'react-is'; } from 'react-is';
import {getDisplayName, getInObject, setInObject} from './utils'; import {getDisplayName, getInObject, setInObject} from './utils';
import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Components/types';
export const meta = { export const meta = {
inspectable: Symbol('inspectable'), inspectable: Symbol('inspectable'),
inspected: Symbol('inspected'), inspected: Symbol('inspected'),
@ -25,6 +27,7 @@ export const meta = {
readonly: Symbol('readonly'), readonly: Symbol('readonly'),
size: Symbol('size'), size: Symbol('size'),
type: Symbol('type'), type: Symbol('type'),
unserializable: Symbol('unserializable'),
}; };
export type Dehydrated = {| export type Dehydrated = {|
@ -35,6 +38,18 @@ export type Dehydrated = {|
type: string, 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. // 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, // 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). // 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( export function dehydrate(
data: Object, data: Object,
cleaned: Array<Array<string | number>>, cleaned: Array<Array<string | number>>,
unserializable: Array<Array<string | number>>,
path: Array<string | number>, path: Array<string | number>,
isPathWhitelisted: (path: Array<string | number>) => boolean, isPathWhitelisted: (path: Array<string | number>) => boolean,
level?: number = 0, level?: number = 0,
): ):
| string | string
| Dehydrated | Dehydrated
| Array<Dehydrated> | Unserializable
| {[key: string]: string | Dehydrated} { | {[key: string]: string | Dehydrated | Unserializable} {
const type = getDataType(data); const type = getDataType(data);
let isPathWhitelistedCheck;
switch (type) { switch (type) {
case 'html_element': case 'html_element':
cleaned.push(path); cleaned.push(path);
@ -233,23 +251,56 @@ export function dehydrate(
}; };
case 'array': case 'array':
const arrayPathCheck = isPathWhitelisted(path); isPathWhitelistedCheck = isPathWhitelisted(path);
if (level >= LEVEL_THRESHOLD && !arrayPathCheck) { if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
return createDehydrated(type, true, data, cleaned, path); return createDehydrated(type, true, data, cleaned, path);
} }
return data.map((item, i) => return data.map((item, i) =>
dehydrate( dehydrate(
item, item,
cleaned, cleaned,
unserializable,
path.concat([i]), path.concat([i]),
isPathWhitelisted, isPathWhitelisted,
arrayPathCheck ? 1 : level + 1, isPathWhitelistedCheck ? 1 : level + 1,
), ),
); );
case 'typed_array': case 'typed_array':
case 'iterator': 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': case 'date':
cleaned.push(path); cleaned.push(path);
@ -260,8 +311,8 @@ export function dehydrate(
}; };
case 'object': case 'object':
const objectPathCheck = isPathWhitelisted(path); isPathWhitelistedCheck = isPathWhitelisted(path);
if (level >= LEVEL_THRESHOLD && !objectPathCheck) { if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
return createDehydrated(type, true, data, cleaned, path); return createDehydrated(type, true, data, cleaned, path);
} else { } else {
const object = {}; const object = {};
@ -269,9 +320,10 @@ export function dehydrate(
object[name] = dehydrate( object[name] = dehydrate(
data[name], data[name],
cleaned, cleaned,
unserializable,
path.concat([name]), path.concat([name]),
isPathWhitelisted, isPathWhitelisted,
objectPathCheck ? 1 : level + 1, isPathWhitelistedCheck ? 1 : level + 1,
); );
} }
return object; return object;
@ -294,24 +346,43 @@ export function dehydrate(
export function fillInPath( export function fillInPath(
object: Object, object: Object,
data: DehydratedData,
path: Array<string | number>, path: Array<string | number>,
value: any, value: any,
) { ) {
const target = getInObject(object, path); const target = getInObject(object, path);
if (target != null) { if (target != null) {
delete target[meta.inspectable]; if (!target[meta.unserializable]) {
delete target[meta.inspected]; delete target[meta.inspectable];
delete target[meta.name]; delete target[meta.inspected];
delete target[meta.readonly]; delete target[meta.name];
delete target[meta.size]; delete target[meta.readonly];
delete target[meta.type]; 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); setInObject(object, path, value);
} }
export function hydrate( export function hydrate(
object: Object, object: Object,
cleaned: Array<Array<string | number>>, cleaned: Array<Array<string | number>>,
unserializable: Array<Array<string | number>>,
): Object { ): Object {
cleaned.forEach((path: Array<string | number>) => { cleaned.forEach((path: Array<string | number>) => {
const length = path.length; const length = path.length;
@ -342,9 +413,69 @@ export function hydrate(
parent[last] = replaced; 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; 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( export function getDisplayNameForReactElement(
element: React$Element<any>, element: React$Element<any>,
): string | null { ): string | null {

View File

@ -266,13 +266,17 @@ export function shallowDiffers(prev: Object, next: Object): boolean {
export function getInObject(object: Object, path: Array<string | number>): any { export function getInObject(object: Object, path: Array<string | number>): any {
return path.reduce((reduced: Object, attr: string | number): any => { return path.reduce((reduced: Object, attr: string | number): any => {
if (typeof reduced === 'object' && reduced !== null) { if (reduced) {
return reduced[attr]; if (hasOwnProperty.call(reduced, attr)) {
} else if (Array.isArray(reduced)) { return reduced[attr];
return reduced[attr]; }
} else { if (typeof reduced[Symbol.iterator] === 'function') {
return null; // Convert iterable to array and return array[index]
return [...reduced][attr];
}
} }
return null;
}, object); }, object);
} }

View File

@ -8,6 +8,7 @@
"start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open" "start": "cross-env NODE_ENV=development cross-env TARGET=local webpack-dev-server --open"
}, },
"dependencies": { "dependencies": {
"immutable": "^4.0.0-rc.12",
"react-native-web": "^0.11.5" "react-native-web": "^0.11.5"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,7 @@
// @flow // @flow
import React, {Fragment} from 'react'; import React, {Fragment} from 'react';
import UnserializableProps from './UnserializableProps';
import Contexts from './Contexts'; import Contexts from './Contexts';
import CustomHooks from './CustomHooks'; import CustomHooks from './CustomHooks';
import CustomObject from './CustomObject'; import CustomObject from './CustomObject';
@ -14,6 +15,7 @@ export default function InspectableElements() {
<Fragment> <Fragment>
<h1>Inspectable elements</h1> <h1>Inspectable elements</h1>
<SimpleValues /> <SimpleValues />
<UnserializableProps />
<NestedProps /> <NestedProps />
<Contexts /> <Contexts />
<CustomHooks /> <CustomHooks />

View File

@ -46,6 +46,8 @@ export default function ObjectProps() {
}, },
}, },
}} }}
emptyArray={[]}
emptyObject={{}}
/> />
); );
} }

View 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;
}

View File

@ -62,7 +62,9 @@ export default function List(props: Props) {
const toggleItem = useCallback( const toggleItem = useCallback(
itemToToggle => { 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( setItems(
items items

View File

@ -1,11 +1,12 @@
{ {
"name": "react-devtools", "name": "react-devtools",
"version": "4.0.0-alpha.9", "version": "4.0.5",
"description": "Use react-devtools outside of the browser", "description": "Use react-devtools outside of the browser",
"license": "MIT", "license": "MIT",
"repository": { "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": { "bin": {
"react-devtools": "./bin.js" "react-devtools": "./bin.js"
@ -26,7 +27,7 @@
"electron": "^5.0.0", "electron": "^5.0.0",
"ip": "^1.1.4", "ip": "^1.1.4",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"react-devtools-core": "4.0.0-alpha.9", "react-devtools-core": "4.0.5",
"update-notifier": "^2.1.0" "update-notifier": "^2.1.0"
} }
} }

View File

@ -6435,6 +6435,11 @@ immediate@~3.0.5:
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= 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: import-fresh@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118"