diff --git a/fixtures/devtools/standalone/index.html b/fixtures/devtools/standalone/index.html index 17e133bae891b..28255cb67ee6c 100644 --- a/fixtures/devtools/standalone/index.html +++ b/fixtures/devtools/standalone/index.html @@ -9,6 +9,7 @@ + @@ -255,6 +256,33 @@

List

); } + 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 ( + + ); + } + function ChildComponent(props: any) { return null; } @@ -264,6 +292,7 @@

List

+ ); diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index f34c8ea23092f..49611b51a02c6 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -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", diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index 8b8e33adcb396..e286de9660a6b 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -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. diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js index d6b41ced0b1f7..d14e6514ba934 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -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, diff --git a/packages/react-devtools-extensions/build.js b/packages/react-devtools-extensions/build.js index eb90e7e2af281..a38e909dd4630 100644 --- a/packages/react-devtools-extensions/build.js +++ b/packages/react-devtools-extensions/build.js @@ -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)); diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 3a1dfb1db4bdf..a915ccc18cdfe 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -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": [ - "", - "background", - "tabs", - "webNavigation", - "file:///*", - "http://*/*", - "https://*/*" - ], + "permissions": ["file:///*", "http://*/*", "https://*/*"], "content_scripts": [ { diff --git a/packages/react-devtools-extensions/deploy.html b/packages/react-devtools-extensions/deploy.html index 34b908f30101d..f6fa5b2de8938 100644 --- a/packages/react-devtools-extensions/deploy.html +++ b/packages/react-devtools-extensions/deploy.html @@ -18,12 +18,11 @@

Created on %date% from - %commit% + %commit%

- This is a preview build of an unreleased DevTools extension. - It has no developer support. + This is a preview build of the React DevTools extension.

Installation instructions

@@ -37,10 +36,5 @@

Bug reports

Please report bugs as GitHub issues. Please include all of the info required to reproduce the bug (e.g. links, code, instructions).

- -

Feature requests

-

- Feature requests are not being accepted at this time. -

diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 02f24a0fef864..50da7c0c83e7a 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -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": [ - "", - "activeTab", - "tabs", - "webNavigation", - "file:///*", - "http://*/*", - "https://*/*" - ], + "permissions": ["file:///*", "http://*/*", "https://*/*"], "content_scripts": [ { diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index b439fe1d00b65..1e8e29df0a113 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -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, diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index 6204f647c0f3f..26f30ccf91bfc 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -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", diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap index a27a73527ca04..712fdb59b0562 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap @@ -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": {}, + "array_buffer": {}, + "date": {}, "fn": {}, - "symbol": {}, + "html_element": {}, + "immutable": { + "0": {}, + "1": {}, + "2": {} + }, + "map": { + "0": {}, + "1": {} + }, + "map_of_maps": { + "0": {}, + "1": {} + }, "react_element": {}, - "array_buffer": {}, - "typed_array": {}, - "date": {} + "set": { + "0": "abc", + "1": 123 + }, + "set_of_sets": { + "0": {}, + "1": {} + }, + "symbol": {}, + "typed_array": { + "0": 100, + "1": -100, + "2": 0 + } }, "state": null } diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js index 6749fa530892c..cd62dcd19d015 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js @@ -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( } - array_buffer={typedArray.buffer} + set={setShallow} + set_of_sets={setOfSets} + symbol={Symbol('symbol')} typed_array={typedArray} - date={new Date()} />, container, ), @@ -435,36 +454,76 @@ describe('InspectedElementContext', () => { expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`); const { - html_element, + array_buffer, + date, fn, - symbol, + html_element, + immutable, + map, + map_of_maps, react_element, - array_buffer, + set, + set_of_sets, + symbol, typed_array, - date, } = (inspectedElement: any).props; + + 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(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(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(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(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(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.inspectable]).toBe(false); - expect(typed_array[meta.name]).toBe('Uint8Array'); + expect(typed_array[meta.name]).toBe('Int8Array'); expect(typed_array[meta.type]).toBe('typed_array'); - expect(date[meta.inspectable]).toBe(false); - expect(date[meta.type]).toBe('date'); + 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( + , + 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( + + + + + , + ), + 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(, 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( + + + + + , + ), + false, + ); + expect(didFinish).toBe(true); + + done(); + }); }); diff --git a/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap b/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap index 1cfc30e884442..b0d83ec67885e 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap @@ -126,13 +126,38 @@ Object { "context": {}, "hooks": null, "props": { - "html_element": {}, + "array_buffer": {}, + "date": {}, "fn": {}, - "symbol": {}, + "html_element": {}, + "immutable": { + "0": {}, + "1": {}, + "2": {} + }, + "map": { + "0": {}, + "1": {} + }, + "map_of_maps": { + "0": {}, + "1": {} + }, "react_element": {}, - "array_buffer": {}, - "typed_array": {}, - "date": {} + "set": { + "0": "abc", + "1": 123 + }, + "set_of_sets": { + "0": {}, + "1": {} + }, + "symbol": {}, + "typed_array": { + "0": 100, + "1": -100, + "2": 0 + } }, "state": null }, diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js index 3452745c21cf3..2dbc8867e0b49 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js @@ -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( } - array_buffer={typedArray.buffer} + set={setShallow} + set_of_sets={setOfSets} + symbol={Symbol('symbol')} typed_array={typedArray} - date={new Date()} />, document.createElement('div'), ), @@ -159,36 +182,76 @@ describe('InspectedElementContext', () => { expect(inspectedElement).toMatchSnapshot('1: Initial inspection'); const { - html_element, + array_buffer, + date, fn, - symbol, + html_element, + immutable, + map, + map_of_maps, react_element, - array_buffer, + set, + set_of_sets, + symbol, typed_array, - date, } = inspectedElement.value.props; + + 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(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(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(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(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(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.inspectable]).toBe(false); - expect(typed_array[meta.name]).toBe('Uint8Array'); + expect(typed_array[meta.name]).toBe('Int8Array'); expect(typed_array[meta.type]).toBe('typed_array'); - expect(date[meta.inspectable]).toBe(false); - expect(date[meta.type]).toBe('date'); + expect(typed_array[0]).toBe(100); + expect(typed_array[1]).toBe(-100); + expect(typed_array[2]).toBe(0); done(); }); diff --git a/packages/react-devtools-shared/src/__tests__/profilerContext-test.js b/packages/react-devtools-shared/src/__tests__/profilerContext-test.js index 342fd47f0c455..c61cb9b43155c 100644 --- a/packages/react-devtools-shared/src/__tests__/profilerContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilerContext-test.js @@ -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(); }); diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index dfed4c433f988..59a20cc9a77fc 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -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 { diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index b4f466c16157e..f150ae71beaa9 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -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); diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 6a23c0132c7b9..94db834ba9a30 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -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, diff --git a/packages/react-devtools-shared/src/backend/utils.js b/packages/react-devtools-shared/src/backend/utils.js index af36eb3db7f09..6b5b8999d91bc 100644 --- a/packages/react-devtools-shared/src/backend/utils.js +++ b/packages/react-devtools-shared/src/backend/utils.js @@ -10,11 +10,20 @@ export function cleanForBridge( path?: Array = [], ): 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; diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 641602e3d82ed..73165ce456cc3 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -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 // diff --git a/packages/react-devtools-shared/src/devtools/ProfilerStore.js b/packages/react-devtools-shared/src/devtools/ProfilerStore.js index 01664ee6437b8..ca1f44ee39d05 100644 --- a/packages/react-devtools-shared/src/devtools/ProfilerStore.js +++ b/packages/react-devtools-shared/src/devtools/ProfilerStore.js @@ -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 = 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 = 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); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js index 5b239ee5231c4..f8678f39326ce 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js @@ -158,6 +158,7 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) { ) : (