DevTools: Show hook names based on variable usage (#21641)

Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
Co-authored-by: Saphal Patro <saphal1998@gmail.com>
Co-authored-by: VibhorCodecianGupta <vibhordelgupta@gmail.com>
This commit is contained in:
Brian Vaughn 2021-07-01 14:39:18 -04:00 committed by GitHub
parent ab390c65ee
commit c5cfa71948
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 2966 additions and 74 deletions

View File

@ -20,6 +20,8 @@ packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/firefox/build
packages/react-devtools-extensions/shared/build
packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/
packages/react-devtools-extensions/src/ErrorTesterCompiled.js
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-scheduling-profiler/dist

View File

@ -2,6 +2,8 @@ packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build
packages/react-devtools-extensions/firefox/build
packages/react-devtools-extensions/shared/build
packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/
packages/react-devtools-extensions/src/ErrorTesterCompiled.js
packages/react-devtools-inline/dist
packages/react-devtools-shell/dist
packages/react-devtools-scheduling-profiler/dist

View File

@ -107,7 +107,7 @@
"scripts": {
"build": "node ./scripts/rollup/build.js",
"build-combined": "node ./scripts/rollup/build-all-release-channels.js",
"build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh --type=NODE && rm -rf build2 && mkdir build2 && cp -r ./build/node_modules build2/oss-experimental/",
"build-for-devtools": "cross-env RELEASE_CHANNEL=experimental yarn build react/index,react/jsx,react-dom,react-is,react-debug-tools,scheduler,react-test-renderer,react-refresh --type=NODE && rm -rf build2 && mkdir build2 && cp -r ./build/node_modules build2/oss-experimental/",
"build-for-devtools-dev": "yarn build-for-devtools --type=NODE_DEV",
"build-for-devtools-prod": "yarn build-for-devtools --type=NODE_PROD",
"linc": "node ./scripts/tasks/linc.js",

View File

@ -47,6 +47,9 @@ module.exports = {
scheduler: resolve(builtModulesDir, 'scheduler'),
},
},
node: {
fs: 'empty',
},
plugins: [
new DefinePlugin({
__DEV__,

View File

@ -93,6 +93,12 @@ const build = async (tempPath, manifestPath) => {
STATIC_FILES.map(file => copy(join(__dirname, file), join(zipPath, file))),
);
// The "source-map" library requires this chunk of WASM to be bundled at runtime.
await copy(
join(__dirname, 'node_modules', 'source-map', 'lib', 'mappings.wasm'),
join(zipPath, 'mappings.wasm'),
);
const commit = getGitCommit();
const dateString = new Date().toLocaleDateString();
const manifest = JSON.parse(readFileSync(copiedManifestPath).toString());

View File

@ -32,7 +32,8 @@
"main.html",
"panel.html",
"build/react_devtools_backend.js",
"build/renderer.js"
"build/renderer.js",
"mappings.wasm"
],
"background": {

View File

@ -32,7 +32,8 @@
"main.html",
"panel.html",
"build/react_devtools_backend.js",
"build/renderer.js"
"build/renderer.js",
"mappings.wasm"
],
"background": {

View File

@ -37,7 +37,8 @@
"main.html",
"panel.html",
"build/react_devtools_backend.js",
"build/renderer.js"
"build/renderer.js",
"mappings.wasm"
],
"background": {

View File

@ -15,12 +15,14 @@
"build:edge:dev": "cross-env NODE_ENV=development node ./edge/build",
"test:chrome": "node ./chrome/test",
"test:firefox": "node ./firefox/test",
"test:edge": "node ./edge/test"
"test:edge": "node ./edge/test",
"update-mock-source-maps": "node ./src/__tests__/updateMockSourceMaps.js"
},
"devDependencies": {
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-transform-flow-strip-types": "^7.10.4",
"@babel/plugin-transform-modules-commonjs": "^7.10.4",
"@babel/plugin-transform-react-jsx-source": "^7.10.5",
"@babel/preset-env": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
@ -29,16 +31,20 @@
"babel-core": "^7.0.0-bridge",
"babel-eslint": "^9.0.0",
"babel-loader": "^8.0.4",
"babel-preset-minify": "^0.5.1",
"child-process-promise": "^2.2.1",
"chrome-launch": "^1.1.4",
"crx": "^5.0.0",
"css-loader": "^1.0.1",
"firefox-profile": "^1.0.2",
"fs-extra": "^4.0.2",
"jest-fetch-mock": "^3.0.3",
"node-libs-browser": "0.5.3",
"nullthrows": "^1.0.0",
"open": "^7.0.2",
"os-name": "^3.1.0",
"raw-loader": "^3.1.0",
"source-map": "^0.8.0-beta.0",
"style-loader": "^0.23.1",
"web-ext": "^3.0.0",
"webpack": "^4.43.0",

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import ErrorStackParser from 'error-stack-parser';
import testErrorStack, {
SOURCE_STACK_FRAME_LINE_NUMBER,
} from './ErrorTesterCompiled';
let sourceMapsAreAppliedToErrors: boolean | null = null;
// Source maps are automatically applied to Error stack frames.
export function areSourceMapsAppliedToErrors(): boolean {
if (sourceMapsAreAppliedToErrors === null) {
try {
testErrorStack();
sourceMapsAreAppliedToErrors = false;
} catch (error) {
const parsed = ErrorStackParser.parse(error);
const topStackFrame = parsed[0];
const lineNumber = topStackFrame.lineNumber;
if (lineNumber === SOURCE_STACK_FRAME_LINE_NUMBER) {
sourceMapsAreAppliedToErrors = true;
}
}
}
return sourceMapsAreAppliedToErrors === true;
}

View File

@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = testErrorStack;
exports.SOURCE_STACK_FRAME_LINE_NUMBER = void 0;
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function testErrorStack() {
// If source maps are applied, this Error will have a stack frame with line 12.
throw Error('Test Error stack');
}
const SOURCE_STACK_FRAME_LINE_NUMBER = 12;
exports.SOURCE_STACK_FRAME_LINE_NUMBER = SOURCE_STACK_FRAME_LINE_NUMBER;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3RFcm9yclN0YWNrLmpzIl0sIm5hbWVzIjpbInRlc3RFcm9yclN0YWNrIiwiRXJyb3IiLCJTT1VSQ0VfU1RBQ0tfRlJBTUVfTElORV9OVU1CRVIiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7O0FBQUE7Ozs7Ozs7O0FBU2UsU0FBU0EsY0FBVCxHQUEwQjtBQUN2QztBQUNBLFFBQU1DLEtBQUssQ0FBQyxrQkFBRCxDQUFYO0FBQ0Q7O0FBRU0sTUFBTUMsOEJBQThCLEdBQUcsRUFBdkMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIENvcHlyaWdodCAoYykgRmFjZWJvb2ssIEluYy4gYW5kIGl0cyBhZmZpbGlhdGVzLlxuICpcbiAqIFRoaXMgc291cmNlIGNvZGUgaXMgbGljZW5zZWQgdW5kZXIgdGhlIE1JVCBsaWNlbnNlIGZvdW5kIGluIHRoZVxuICogTElDRU5TRSBmaWxlIGluIHRoZSByb290IGRpcmVjdG9yeSBvZiB0aGlzIHNvdXJjZSB0cmVlLlxuICpcbiAqIEBmbG93XG4gKi9cblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gdGVzdEVyb3JyU3RhY2soKSB7XG4gIC8vIElmIHNvdXJjZSBtYXBzIGFyZSBhcHBsaWVkLCB0aGlzIEVycm9yIHdpbGwgaGF2ZSBhIHN0YWNrIGZyYW1lIHdpdGggbGluZSAxMi5cbiAgdGhyb3cgRXJyb3IoJ1Rlc3QgRXJyb3Igc3RhY2snKTtcbn1cblxuZXhwb3J0IGNvbnN0IFNPVVJDRV9TVEFDS19GUkFNRV9MSU5FX05VTUJFUiA9IDEyOyJdfQ==
//# sourceURL=testErrorStack.js

View File

@ -0,0 +1 @@
The JavaScript files and source maps in this directory are used to test DevTools hook name parsing code. The files here use source maps in different formats to mirror real world applications. The source for the files is located in the parnet `__tests__` folder. To regenerate these compiled files, run `yarn update-mock-source-maps` in the `react-devtools-extensions` directory.

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import React, {useEffect, useState} from 'react';
export function Component() {
const [count, setCount] = useState(0);
const isDarkMode = useIsDarkMode();
useEffect(() => {
// ...
}, []);
const handleClick = () => setCount(count + 1);
return (
<>
<div>Dark mode? {isDarkMode}</div>
<div>Count: {count}</div>
<button onClick={handleClick}>Update count</button>
</>
);
}
function useIsDarkMode() {
const [isDarkMode] = useState(false);
useEffect(function useEffectCreate() {
// Here is where we may listen to a "theme" event...
}, []);
return isDarkMode;
}

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import React from 'react';
import useTheme from './useTheme';
export function Component() {
const theme = useTheme();
return <div>theme: {theme}</div>;
}

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import React, {useState} from 'react';
export function Component() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export function Component() {
const [count] = require('react').useState(0);
return count;
}

View File

@ -0,0 +1,128 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {Fragment, useCallback, useState} from 'react';
export function ListItem({item, removeItem, toggleItem}) {
const handleDelete = useCallback(() => {
removeItem(item);
}, [item, removeItem]);
const handleToggle = useCallback(() => {
toggleItem(item);
}, [item, toggleItem]);
return (
<li>
<button onClick={handleDelete}>Delete</button>
<label>
<input
checked={item.isComplete}
onChange={handleToggle}
type="checkbox"
/>{' '}
{item.text}
</label>
</li>
);
}
export function List(props) {
const [newItemText, setNewItemText] = useState('');
const [items, setItems] = useState([
{id: 1, isComplete: true, text: 'First'},
{id: 2, isComplete: true, text: 'Second'},
{id: 3, isComplete: false, text: 'Third'},
]);
const [uid, setUID] = useState(4);
const handleClick = useCallback(() => {
if (newItemText !== '') {
setItems([
...items,
{
id: uid,
isComplete: false,
text: newItemText,
},
]);
setUID(uid + 1);
setNewItemText('');
}
}, [newItemText, items, uid]);
const handleKeyPress = useCallback(
event => {
if (event.key === 'Enter') {
handleClick();
}
},
[handleClick],
);
const handleChange = useCallback(
event => {
setNewItemText(event.currentTarget.value);
},
[setNewItemText],
);
const removeItem = useCallback(
itemToRemove => setItems(items.filter(item => item !== itemToRemove)),
[items],
);
const toggleItem = useCallback(
itemToToggle => {
// Dont use indexOf()
// because editing props in DevTools creates a new Object.
const index = items.findIndex(item => item.id === itemToToggle.id);
setItems(
items
.slice(0, index)
.concat({
...itemToToggle,
isComplete: !itemToToggle.isComplete,
})
.concat(items.slice(index + 1)),
);
},
[items],
);
return (
<Fragment>
<h1>List</h1>
<input
type="text"
placeholder="New list item..."
value={newItemText}
onChange={handleChange}
onKeyPress={handleKeyPress}
/>
<button disabled={newItemText === ''} onClick={handleClick}>
<span role="img" aria-label="Add item">
Add
</span>
</button>
<ul>
{items.map(item => (
<ListItem
key={item.id}
item={item}
removeItem={removeItem}
toggleItem={toggleItem}
/>
))}
</ul>
</Fragment>
);
}

View File

@ -0,0 +1,42 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Component = Component;
var _react = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function Component() {
const [count, setCount] = (0, _react.useState)(0);
const isDarkMode = useIsDarkMode();
(0, _react.useEffect)(() => {// ...
}, []);
const handleClick = () => setCount(count + 1);
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("div", null, "Dark mode? ", isDarkMode), /*#__PURE__*/_react.default.createElement("div", null, "Count: ", count), /*#__PURE__*/_react.default.createElement("button", {
onClick: handleClick
}, "Update count"));
}
function useIsDarkMode() {
const [isDarkMode, setIsDarkMode] = (0, _react.useState)(0);
(0, _react.useEffect)(function useEffectCreate() {// Here is where we may listen to a "theme" event...
}, []);
return isDarkMode;
}
//# sourceMappingURL=ComponentWithCustomHook.js.map
//# sourceURL=ComponentWithCustomHook.js

View File

@ -0,0 +1 @@
{"version":3,"sources":["ComponentWithCustomHook.js"],"names":["Component","count","setCount","isDarkMode","useIsDarkMode","handleClick","setIsDarkMode","useEffectCreate"],"mappings":";;;;;;;AASA;;;;;;AATA;;;;;;;;AAWO,SAASA,SAAT,GAAqB;AAC1B,QAAM,CAACC,KAAD,EAAQC,QAAR,IAAoB,qBAAS,CAAT,CAA1B;AACA,QAAMC,UAAU,GAAGC,aAAa,EAAhC;AAEA,wBAAU,MAAM,CACd;AACD,GAFD,EAEG,EAFH;;AAIA,QAAMC,WAAW,GAAG,MAAMH,QAAQ,CAACD,KAAK,GAAG,CAAT,CAAlC;;AAEA,sBACE,yEACE,yDAAiBE,UAAjB,CADF,eAEE,qDAAaF,KAAb,CAFF,eAGE;AAAQ,IAAA,OAAO,EAAEI;AAAjB,oBAHF,CADF;AAOD;;AAED,SAASD,aAAT,GAAyB;AACvB,QAAM,CAACD,UAAD,EAAaG,aAAb,IAA8B,qBAAS,CAAT,CAApC;AAEA,wBAAU,SAASC,eAAT,GAA2B,CACnC;AACD,GAFD,EAEG,EAFH;AAIA,SAAOJ,UAAP;AACD","sourcesContent":["/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nimport React, {useEffect, useState} from 'react';\n\nexport function Component() {\n const [count, setCount] = useState(0);\n const isDarkMode = useIsDarkMode();\n\n useEffect(() => {\n // ...\n }, []);\n\n const handleClick = () => setCount(count + 1);\n\n return (\n <>\n <div>Dark mode? {isDarkMode}</div>\n <div>Count: {count}</div>\n <button onClick={handleClick}>Update count</button>\n </>\n );\n}\n\nfunction useIsDarkMode() {\n const [isDarkMode, setIsDarkMode] = useState(0);\n\n useEffect(function useEffectCreate() {\n // Here is where we may listen to a \"theme\" event...\n }, []);\n\n return isDarkMode;\n}"]}

View File

@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Component = Component;
var _react = _interopRequireDefault(require("react"));
var _useTheme = _interopRequireDefault(require("./useTheme"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function Component() {
const theme = (0, _useTheme.default)();
return /*#__PURE__*/_react.default.createElement("div", null, "theme: ", theme);
}
//# sourceMappingURL=ComponentWithExternalCustomHooks.js.map
//# sourceURL=ComponentWithExternalCustomHooks.js

View File

@ -0,0 +1 @@
{"version":3,"sources":["ComponentWithExternalCustomHooks.js"],"names":["Component","theme"],"mappings":";;;;;;;AASA;;AACA;;;;AAVA;;;;;;;;AAYO,SAASA,SAAT,GAAqB;AAC1B,QAAMC,KAAK,GAAG,wBAAd;AAEA,sBAAO,qDAAaA,KAAb,CAAP;AACD","sourcesContent":["/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nimport React from 'react';\nimport useTheme from './useTheme';\n\nexport function Component() {\n const theme = useTheme();\n\n return <div>theme: {theme}</div>;\n}\n"]}

View File

@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Component = Component;
var _react = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function Component() {
const [count, setCount] = (0, _react.useState)(0);
return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("p", null, "You clicked ", count, " times"), /*#__PURE__*/_react.default.createElement("button", {
onClick: () => setCount(count + 1)
}, "Click me"));
}
//# sourceMappingURL=Example.js.map
//# sourceURL=Example.js

View File

@ -0,0 +1 @@
{"version":3,"sources":["Example.js"],"names":["Component","count","setCount"],"mappings":";;;;;;;AASA;;;;;;AATA;;;;;;;;AAWO,SAASA,SAAT,GAAqB;AAC1B,QAAM,CAACC,KAAD,EAAQC,QAAR,IAAoB,qBAAS,CAAT,CAA1B;AAEA,sBACE,uDACE,wDAAgBD,KAAhB,WADF,eAEE;AAAQ,IAAA,OAAO,EAAE,MAAMC,QAAQ,CAACD,KAAK,GAAG,CAAT;AAA/B,gBAFF,CADF;AAMD","sourcesContent":["/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nimport React, {useState} from 'react';\n\nexport function Component() {\n const [count, setCount] = useState(0);\n\n return (\n <div>\n <p>You clicked {count} times</p>\n <button onClick={() => setCount(count + 1)}>Click me</button>\n </div>\n );\n}\n"]}

View File

@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Component = Component;
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function Component() {
const [count] = require('react').useState(0);
return count;
}
//# sourceMappingURL=InlineRequire.js.map
//# sourceURL=InlineRequire.js

View File

@ -0,0 +1 @@
{"version":3,"sources":["InlineRequire.js"],"names":["Component","count","require","useState"],"mappings":";;;;;;;AAAA;;;;;;;;AASO,SAASA,SAAT,GAAqB;AAC1B,QAAM,CAACC,KAAD,IAAUC,OAAO,CAAC,OAAD,CAAP,CAAiBC,QAAjB,CAA0B,CAA1B,CAAhB;;AAEA,SAAOF,KAAP;AACD","sourcesContent":["/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nexport function Component() {\n const [count] = require('react').useState(0);\n\n return count;\n}\n"]}

View File

@ -0,0 +1,107 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ListItem = ListItem;
exports.List = List;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function ListItem({
item,
removeItem,
toggleItem
}) {
const handleDelete = (0, React.useCallback)(() => {
removeItem(item);
}, [item, removeItem]);
const handleToggle = (0, React.useCallback)(() => {
toggleItem(item);
}, [item, toggleItem]);
return /*#__PURE__*/React.createElement("li", null, /*#__PURE__*/React.createElement("button", {
onClick: handleDelete
}, "Delete"), /*#__PURE__*/React.createElement("label", null, /*#__PURE__*/React.createElement("input", {
checked: item.isComplete,
onChange: handleToggle,
type: "checkbox"
}), ' ', item.text));
}
function List(props) {
const [newItemText, setNewItemText] = (0, React.useState)('');
const [items, setItems] = (0, React.useState)([{
id: 1,
isComplete: true,
text: 'First'
}, {
id: 2,
isComplete: true,
text: 'Second'
}, {
id: 3,
isComplete: false,
text: 'Third'
}]);
const [uid, setUID] = (0, React.useState)(4);
const handleClick = (0, React.useCallback)(() => {
if (newItemText !== '') {
setItems([...items, {
id: uid,
isComplete: false,
text: newItemText
}]);
setUID(uid + 1);
setNewItemText('');
}
}, [newItemText, items, uid]);
const handleKeyPress = (0, React.useCallback)(event => {
if (event.key === 'Enter') {
handleClick();
}
}, [handleClick]);
const handleChange = (0, React.useCallback)(event => {
setNewItemText(event.currentTarget.value);
}, [setNewItemText]);
const removeItem = (0, React.useCallback)(itemToRemove => setItems(items.filter(item => item !== itemToRemove)), [items]);
const toggleItem = (0, React.useCallback)(itemToToggle => {
// Dont use indexOf()
// because editing props in DevTools creates a new Object.
const index = items.findIndex(item => item.id === itemToToggle.id);
setItems(items.slice(0, index).concat({ ...itemToToggle,
isComplete: !itemToToggle.isComplete
}).concat(items.slice(index + 1)));
}, [items]);
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", null, "List"), /*#__PURE__*/React.createElement("input", {
type: "text",
placeholder: "New list item...",
value: newItemText,
onChange: handleChange,
onKeyPress: handleKeyPress
}), /*#__PURE__*/React.createElement("button", {
disabled: newItemText === '',
onClick: handleClick
}, /*#__PURE__*/React.createElement("span", {
role: "img",
"aria-label": "Add item"
}, "Add")), /*#__PURE__*/React.createElement("ul", null, items.map(item => /*#__PURE__*/React.createElement(ListItem, {
key: item.id,
item: item,
removeItem: removeItem,
toggleItem: toggleItem
}))));
}
//# sourceMappingURL=ToDoList.js.map
//# sourceURL=ToDoList.js

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useTheme;
exports.ThemeContext = void 0;
var _react = require("react");
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
const ThemeContext = /*#__PURE__*/(0, _react.createContext)('bright');
exports.ThemeContext = ThemeContext;
function useTheme() {
const theme = (0, _react.useContext)(ThemeContext);
(0, _react.useDebugValue)(theme);
return theme;
}
//# sourceMappingURL=useTheme.js.map
//# sourceURL=useTheme.js

View File

@ -0,0 +1 @@
{"version":3,"sources":["useTheme.js"],"names":["ThemeContext","useTheme","theme"],"mappings":";;;;;;;;AASA;;AATA;;;;;;;;AAWO,MAAMA,YAAY,gBAAG,0BAAc,QAAd,CAArB;;;AAEQ,SAASC,QAAT,GAAoB;AACjC,QAAMC,KAAK,GAAG,uBAAWF,YAAX,CAAd;AACA,4BAAcE,KAAd;AACA,SAAOA,KAAP;AACD","sourcesContent":["/**\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n */\n\nimport {createContext, useContext, useDebugValue} from 'react';\n\nexport const ThemeContext = createContext('bright');\n\nexport default function useTheme() {\n const theme = useContext(ThemeContext);\n useDebugValue(theme);\n return theme;\n}\n"]}

View File

@ -0,0 +1,42 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Component = Component;
var _react = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function Component() {
const [count, setCount] = (0, _react.useState)(0);
const isDarkMode = useIsDarkMode();
(0, _react.useEffect)(() => {// ...
}, []);
const handleClick = () => setCount(count + 1);
return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("div", null, "Dark mode? ", isDarkMode), /*#__PURE__*/_react.default.createElement("div", null, "Count: ", count), /*#__PURE__*/_react.default.createElement("button", {
onClick: handleClick
}, "Update count"));
}
function useIsDarkMode() {
const [isDarkMode, setIsDarkMode] = (0, _react.useState)(0);
(0, _react.useEffect)(function useEffectCreate() {// Here is where we may listen to a "theme" event...
}, []);
return isDarkMode;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIkNvbXBvbmVudFdpdGhDdXN0b21Ib29rLmpzIl0sIm5hbWVzIjpbIkNvbXBvbmVudCIsImNvdW50Iiwic2V0Q291bnQiLCJpc0RhcmtNb2RlIiwidXNlSXNEYXJrTW9kZSIsImhhbmRsZUNsaWNrIiwic2V0SXNEYXJrTW9kZSIsInVzZUVmZmVjdENyZWF0ZSJdLCJtYXBwaW5ncyI6Ijs7Ozs7OztBQVNBOzs7Ozs7QUFUQTs7Ozs7Ozs7QUFXTyxTQUFTQSxTQUFULEdBQXFCO0FBQzFCLFFBQU0sQ0FBQ0MsS0FBRCxFQUFRQyxRQUFSLElBQW9CLHFCQUFTLENBQVQsQ0FBMUI7QUFDQSxRQUFNQyxVQUFVLEdBQUdDLGFBQWEsRUFBaEM7QUFFQSx3QkFBVSxNQUFNLENBQ2Q7QUFDRCxHQUZELEVBRUcsRUFGSDs7QUFJQSxRQUFNQyxXQUFXLEdBQUcsTUFBTUgsUUFBUSxDQUFDRCxLQUFLLEdBQUcsQ0FBVCxDQUFsQzs7QUFFQSxzQkFDRSx5RUFDRSx5REFBaUJFLFVBQWpCLENBREYsZUFFRSxxREFBYUYsS0FBYixDQUZGLGVBR0U7QUFBUSxJQUFBLE9BQU8sRUFBRUk7QUFBakIsb0JBSEYsQ0FERjtBQU9EOztBQUVELFNBQVNELGFBQVQsR0FBeUI7QUFDdkIsUUFBTSxDQUFDRCxVQUFELEVBQWFHLGFBQWIsSUFBOEIscUJBQVMsQ0FBVCxDQUFwQztBQUVBLHdCQUFVLFNBQVNDLGVBQVQsR0FBMkIsQ0FDbkM7QUFDRCxHQUZELEVBRUcsRUFGSDtBQUlBLFNBQU9KLFVBQVA7QUFDRCIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQ29weXJpZ2h0IChjKSBGYWNlYm9vaywgSW5jLiBhbmQgaXRzIGFmZmlsaWF0ZXMuXG4gKlxuICogVGhpcyBzb3VyY2UgY29kZSBpcyBsaWNlbnNlZCB1bmRlciB0aGUgTUlUIGxpY2Vuc2UgZm91bmQgaW4gdGhlXG4gKiBMSUNFTlNFIGZpbGUgaW4gdGhlIHJvb3QgZGlyZWN0b3J5IG9mIHRoaXMgc291cmNlIHRyZWUuXG4gKlxuICogQGZsb3dcbiAqL1xuXG5pbXBvcnQgUmVhY3QsIHt1c2VFZmZlY3QsIHVzZVN0YXRlfSBmcm9tICdyZWFjdCc7XG5cbmV4cG9ydCBmdW5jdGlvbiBDb21wb25lbnQoKSB7XG4gIGNvbnN0IFtjb3VudCwgc2V0Q291bnRdID0gdXNlU3RhdGUoMCk7XG4gIGNvbnN0IGlzRGFya01vZGUgPSB1c2VJc0RhcmtNb2RlKCk7XG5cbiAgdXNlRWZmZWN0KCgpID0+IHtcbiAgICAvLyAuLi5cbiAgfSwgW10pO1xuXG4gIGNvbnN0IGhhbmRsZUNsaWNrID0gKCkgPT4gc2V0Q291bnQoY291bnQgKyAxKTtcblxuICByZXR1cm4gKFxuICAgIDw+XG4gICAgICA8ZGl2PkRhcmsgbW9kZT8ge2lzRGFya01vZGV9PC9kaXY+XG4gICAgICA8ZGl2PkNvdW50OiB7Y291bnR9PC9kaXY+XG4gICAgICA8YnV0dG9uIG9uQ2xpY2s9e2hhbmRsZUNsaWNrfT5VcGRhdGUgY291bnQ8L2J1dHRvbj5cbiAgICA8Lz5cbiAgKTtcbn1cblxuZnVuY3Rpb24gdXNlSXNEYXJrTW9kZSgpIHtcbiAgY29uc3QgW2lzRGFya01vZGUsIHNldElzRGFya01vZGVdID0gdXNlU3RhdGUoMCk7XG5cbiAgdXNlRWZmZWN0KGZ1bmN0aW9uIHVzZUVmZmVjdENyZWF0ZSgpIHtcbiAgICAvLyBIZXJlIGlzIHdoZXJlIHdlIG1heSBsaXN0ZW4gdG8gYSBcInRoZW1lXCIgZXZlbnQuLi5cbiAgfSwgW10pO1xuXG4gIHJldHVybiBpc0RhcmtNb2RlO1xufSJdfQ==
//# sourceURL=ComponentWithCustomHook.js

View File

@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Component = Component;
var _react = _interopRequireDefault(require("react"));
var _useTheme = _interopRequireDefault(require("./useTheme"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function Component() {
const theme = (0, _useTheme.default)();
return /*#__PURE__*/_react.default.createElement("div", null, "theme: ", theme);
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIkNvbXBvbmVudFdpdGhFeHRlcm5hbEN1c3RvbUhvb2tzLmpzIl0sIm5hbWVzIjpbIkNvbXBvbmVudCIsInRoZW1lIl0sIm1hcHBpbmdzIjoiOzs7Ozs7O0FBU0E7O0FBQ0E7Ozs7QUFWQTs7Ozs7Ozs7QUFZTyxTQUFTQSxTQUFULEdBQXFCO0FBQzFCLFFBQU1DLEtBQUssR0FBRyx3QkFBZDtBQUVBLHNCQUFPLHFEQUFhQSxLQUFiLENBQVA7QUFDRCIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQ29weXJpZ2h0IChjKSBGYWNlYm9vaywgSW5jLiBhbmQgaXRzIGFmZmlsaWF0ZXMuXG4gKlxuICogVGhpcyBzb3VyY2UgY29kZSBpcyBsaWNlbnNlZCB1bmRlciB0aGUgTUlUIGxpY2Vuc2UgZm91bmQgaW4gdGhlXG4gKiBMSUNFTlNFIGZpbGUgaW4gdGhlIHJvb3QgZGlyZWN0b3J5IG9mIHRoaXMgc291cmNlIHRyZWUuXG4gKlxuICogQGZsb3dcbiAqL1xuXG5pbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnO1xuaW1wb3J0IHVzZVRoZW1lIGZyb20gJy4vdXNlVGhlbWUnO1xuXG5leHBvcnQgZnVuY3Rpb24gQ29tcG9uZW50KCkge1xuICBjb25zdCB0aGVtZSA9IHVzZVRoZW1lKCk7XG5cbiAgcmV0dXJuIDxkaXY+dGhlbWU6IHt0aGVtZX08L2Rpdj47XG59XG4iXX0=
//# sourceURL=ComponentWithExternalCustomHooks.js

View File

@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Component = Component;
var _react = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function Component() {
const [count, setCount] = (0, _react.useState)(0);
return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("p", null, "You clicked ", count, " times"), /*#__PURE__*/_react.default.createElement("button", {
onClick: () => setCount(count + 1)
}, "Click me"));
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIkV4YW1wbGUuanMiXSwibmFtZXMiOlsiQ29tcG9uZW50IiwiY291bnQiLCJzZXRDb3VudCJdLCJtYXBwaW5ncyI6Ijs7Ozs7OztBQVNBOzs7Ozs7QUFUQTs7Ozs7Ozs7QUFXTyxTQUFTQSxTQUFULEdBQXFCO0FBQzFCLFFBQU0sQ0FBQ0MsS0FBRCxFQUFRQyxRQUFSLElBQW9CLHFCQUFTLENBQVQsQ0FBMUI7QUFFQSxzQkFDRSx1REFDRSx3REFBZ0JELEtBQWhCLFdBREYsZUFFRTtBQUFRLElBQUEsT0FBTyxFQUFFLE1BQU1DLFFBQVEsQ0FBQ0QsS0FBSyxHQUFHLENBQVQ7QUFBL0IsZ0JBRkYsQ0FERjtBQU1EIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDb3B5cmlnaHQgKGMpIEZhY2Vib29rLCBJbmMuIGFuZCBpdHMgYWZmaWxpYXRlcy5cbiAqXG4gKiBUaGlzIHNvdXJjZSBjb2RlIGlzIGxpY2Vuc2VkIHVuZGVyIHRoZSBNSVQgbGljZW5zZSBmb3VuZCBpbiB0aGVcbiAqIExJQ0VOU0UgZmlsZSBpbiB0aGUgcm9vdCBkaXJlY3Rvcnkgb2YgdGhpcyBzb3VyY2UgdHJlZS5cbiAqXG4gKiBAZmxvd1xuICovXG5cbmltcG9ydCBSZWFjdCwge3VzZVN0YXRlfSBmcm9tICdyZWFjdCc7XG5cbmV4cG9ydCBmdW5jdGlvbiBDb21wb25lbnQoKSB7XG4gIGNvbnN0IFtjb3VudCwgc2V0Q291bnRdID0gdXNlU3RhdGUoMCk7XG5cbiAgcmV0dXJuIChcbiAgICA8ZGl2PlxuICAgICAgPHA+WW91IGNsaWNrZWQge2NvdW50fSB0aW1lczwvcD5cbiAgICAgIDxidXR0b24gb25DbGljaz17KCkgPT4gc2V0Q291bnQoY291bnQgKyAxKX0+Q2xpY2sgbWU8L2J1dHRvbj5cbiAgICA8L2Rpdj5cbiAgKTtcbn1cbiJdfQ==
//# sourceURL=Example.js

View File

@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Component = Component;
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function Component() {
const [count] = require('react').useState(0);
return count;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIklubGluZVJlcXVpcmUuanMiXSwibmFtZXMiOlsiQ29tcG9uZW50IiwiY291bnQiLCJyZXF1aXJlIiwidXNlU3RhdGUiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7QUFBQTs7Ozs7Ozs7QUFTTyxTQUFTQSxTQUFULEdBQXFCO0FBQzFCLFFBQU0sQ0FBQ0MsS0FBRCxJQUFVQyxPQUFPLENBQUMsT0FBRCxDQUFQLENBQWlCQyxRQUFqQixDQUEwQixDQUExQixDQUFoQjs7QUFFQSxTQUFPRixLQUFQO0FBQ0QiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIENvcHlyaWdodCAoYykgRmFjZWJvb2ssIEluYy4gYW5kIGl0cyBhZmZpbGlhdGVzLlxuICpcbiAqIFRoaXMgc291cmNlIGNvZGUgaXMgbGljZW5zZWQgdW5kZXIgdGhlIE1JVCBsaWNlbnNlIGZvdW5kIGluIHRoZVxuICogTElDRU5TRSBmaWxlIGluIHRoZSByb290IGRpcmVjdG9yeSBvZiB0aGlzIHNvdXJjZSB0cmVlLlxuICpcbiAqIEBmbG93XG4gKi9cblxuZXhwb3J0IGZ1bmN0aW9uIENvbXBvbmVudCgpIHtcbiAgY29uc3QgW2NvdW50XSA9IHJlcXVpcmUoJ3JlYWN0JykudXNlU3RhdGUoMCk7XG5cbiAgcmV0dXJuIGNvdW50O1xufVxuIl19
//# sourceURL=InlineRequire.js

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useTheme;
exports.ThemeContext = void 0;
var _react = require("react");
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
const ThemeContext = /*#__PURE__*/(0, _react.createContext)('bright');
exports.ThemeContext = ThemeContext;
function useTheme() {
const theme = (0, _react.useContext)(ThemeContext);
(0, _react.useDebugValue)(theme);
return theme;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInVzZVRoZW1lLmpzIl0sIm5hbWVzIjpbIlRoZW1lQ29udGV4dCIsInVzZVRoZW1lIiwidGhlbWUiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7O0FBU0E7O0FBVEE7Ozs7Ozs7O0FBV08sTUFBTUEsWUFBWSxnQkFBRywwQkFBYyxRQUFkLENBQXJCOzs7QUFFUSxTQUFTQyxRQUFULEdBQW9CO0FBQ2pDLFFBQU1DLEtBQUssR0FBRyx1QkFBV0YsWUFBWCxDQUFkO0FBQ0EsNEJBQWNFLEtBQWQ7QUFDQSxTQUFPQSxLQUFQO0FBQ0QiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIENvcHlyaWdodCAoYykgRmFjZWJvb2ssIEluYy4gYW5kIGl0cyBhZmZpbGlhdGVzLlxuICpcbiAqIFRoaXMgc291cmNlIGNvZGUgaXMgbGljZW5zZWQgdW5kZXIgdGhlIE1JVCBsaWNlbnNlIGZvdW5kIGluIHRoZVxuICogTElDRU5TRSBmaWxlIGluIHRoZSByb290IGRpcmVjdG9yeSBvZiB0aGlzIHNvdXJjZSB0cmVlLlxuICpcbiAqIEBmbG93XG4gKi9cblxuaW1wb3J0IHtjcmVhdGVDb250ZXh0LCB1c2VDb250ZXh0LCB1c2VEZWJ1Z1ZhbHVlfSBmcm9tICdyZWFjdCc7XG5cbmV4cG9ydCBjb25zdCBUaGVtZUNvbnRleHQgPSBjcmVhdGVDb250ZXh0KCdicmlnaHQnKTtcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gdXNlVGhlbWUoKSB7XG4gIGNvbnN0IHRoZW1lID0gdXNlQ29udGV4dChUaGVtZUNvbnRleHQpO1xuICB1c2VEZWJ1Z1ZhbHVlKHRoZW1lKTtcbiAgcmV0dXJuIHRoZW1lO1xufVxuIl19
//# sourceURL=useTheme.js

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {createContext, useContext, useDebugValue} from 'react';
export const ThemeContext = createContext('bright');
export default function useTheme() {
const theme = useContext(ThemeContext);
useDebugValue(theme);
return theme;
}

View File

@ -0,0 +1,287 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
describe('parseHookNames', () => {
let fetchMock;
let inspectHooks;
let parseHookNames;
beforeEach(() => {
jest.resetModules();
jest.mock('source-map-support', () => {
console.trace('source-map-support');
});
const {
overrideFeatureFlags,
} = require('react-devtools-shared/src/__tests__/utils');
overrideFeatureFlags({enableHookNameParsing: true});
fetchMock = require('jest-fetch-mock');
fetchMock.enableMocks();
inspectHooks = require('react-debug-tools/src/ReactDebugHooks')
.inspectHooks;
parseHookNames = require('../parseHookNames').default;
// Jest (jest-runner?) configures Errors to automatically account for source maps.
// This changes behavior between our tests and the browser.
// To "fix" this, clear the prepareStackTrace() method on the Error object.
delete Error.prepareStackTrace;
fetchMock.mockIf(/.+$/, request => {
const {resolve} = require('path');
const url = request.url;
if (url.endsWith('js.map')) {
// Source maps are relative URLs (e.g. "path/to/Exmaple.js" specifies "Exmaple.js.map").
const sourceMapURL = resolve(
__dirname,
'__source__',
'__compiled__',
'external',
url,
);
return Promise.resolve(requireText(sourceMapURL, 'utf8'));
} else {
return Promise.resolve(requireText(url, 'utf8'));
}
});
// Mock out portion of browser API used by parseHookNames to initialize "source-map".
global.chrome = {
extension: {
getURL: jest.fn((...args) => {
const {join} = require('path');
return join(
__dirname,
'..',
'..',
'node_modules',
'source-map',
'lib',
'mappings.wasm',
);
}),
},
};
});
afterEach(() => {
fetch.resetMocks();
});
function expectHookNamesToEqual(map, expectedNamesArray) {
// Slightly hacky since it relies on the iterable order of values()
expect(Array.from(map.values())).toEqual(expectedNamesArray);
}
function requireText(path, encoding) {
const {readFileSync} = require('fs');
return readFileSync(path, encoding);
}
async function getHookNamesForComponent(Component, props = {}) {
const hooksTree = inspectHooks(Component, props, undefined, true);
const hookNames = await parseHookNames(hooksTree);
return hookNames;
}
it('should parse names for useState()', async () => {
const React = require('react');
const {useState} = React;
function Component(props) {
const [foo] = useState(true);
const bar = useState(true);
const [baz] = React.useState(true);
return `${foo}-${bar}-${baz}`;
}
const hookNames = await getHookNamesForComponent(Component);
expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz']);
});
it('should parse names for useReducer()', async () => {
const React = require('react');
const {useReducer} = React;
function Component(props) {
const [foo] = useReducer(true);
const [bar] = useReducer(true);
const [baz] = React.useReducer(true);
return `${foo}-${bar}-${baz}`;
}
const hookNames = await getHookNamesForComponent(Component);
expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz']);
});
it('should return null for hooks without names like useEffect', async () => {
const React = require('react');
const {useEffect} = React;
function Component(props) {
useEffect(() => {});
React.useLayoutEffect(() => () => {});
return null;
}
const hookNames = await getHookNamesForComponent(Component);
expectHookNamesToEqual(hookNames, []); // No hooks with names
});
it('should parse names for custom hooks', async () => {
const {useDebugValue, useState} = require('react');
function useCustomHookOne() {
// DebugValue hook should not appear in log.
useDebugValue('example');
return true;
}
function useCustomHookTwo() {
const [baz, setBaz] = useState(true);
return [baz, setBaz];
}
function Component(props) {
const foo = useCustomHookOne();
// This cae is ignored;
// the meaning of a tuple assignment for a custom hook is unclear.
const [bar] = useCustomHookTwo();
return `${foo}-${bar}`;
}
const hookNames = await getHookNamesForComponent(Component);
expectHookNamesToEqual(hookNames, [
'foo',
null, // Custom hooks can have names, but not when using destructuring.
'baz',
]);
});
it('should return null for custom hooks without explicit names', async () => {
const {useDebugValue} = require('react');
function useCustomHookOne() {
// DebugValue hook should not appear in log.
useDebugValue('example');
}
function useCustomHookTwo() {
// DebugValue hook should not appear in log.
useDebugValue('example');
return [true];
}
function Component(props) {
useCustomHookOne();
const [bar] = useCustomHookTwo();
return bar;
}
const hookNames = await getHookNamesForComponent(Component);
expectHookNamesToEqual(hookNames, [
null, // Custom hooks can have names, but this one does not even return a value.
null, // Custom hooks can have names, but not when using destructuring.
]);
});
describe('inline and external source maps', () => {
it('should work for simple components', async () => {
async function test(path) {
const Component = require(path).Component;
const hookNames = await getHookNamesForComponent(Component);
expectHookNamesToEqual(hookNames, [
'count', // useState
]);
}
await test('./__source__/Example'); // original source (uncompiled)
await test('./__source__/__compiled__/inline/Example'); // inline source map
await test('./__source__/__compiled__/external/Example'); // external source map
});
it('should work with more complex files and components', async () => {
async function test(path) {
const components = require(path);
let hookNames = await getHookNamesForComponent(components.List);
expectHookNamesToEqual(hookNames, [
'newItemText', // useState
'items', // useState
'uid', // useState
'handleClick', // useCallback
'handleKeyPress', // useCallback
'handleChange', // useCallback
'removeItem', // useCallback
'toggleItem', // useCallback
]);
hookNames = await getHookNamesForComponent(components.ListItem, {
item: {},
});
expectHookNamesToEqual(hookNames, [
'handleDelete', // useCallback
'handleToggle', // useCallback
]);
}
await test('./__source__/ToDoList'); // original source (uncompiled)
await test('./__source__/__compiled__/inline/ToDoList'); // inline source map
await test('./__source__/__compiled__/external/ToDoList'); // external source map
});
it('should work for custom hook', async () => {
async function test(path) {
const Component = require(path).Component;
const hookNames = await getHookNamesForComponent(Component);
expectHookNamesToEqual(hookNames, [
'count', // useState()
'isDarkMode', // useIsDarkMode()
'isDarkMode', // useIsDarkMode -> useState()
]);
}
await test('./__source__/ComponentWithCustomHook'); // original source (uncompiled)
await test('./__source__/__compiled__/inline/ComponentWithCustomHook'); // inline source map
await test('./__source__/__compiled__/external/ComponentWithCustomHook'); // external source map
});
it('should work for external hooks', async () => {
async function test(path) {
const Component = require(path).Component;
const hookNames = await getHookNamesForComponent(Component);
expectHookNamesToEqual(hookNames, [
'theme', // useTheme()
'theme', // useContext()
]);
}
// We can't test the uncompiled source here, because it either needs to get transformed,
// which would break the source mapping, or the import statements will fail.
await test(
'./__source__/__compiled__/inline/ComponentWithExternalCustomHooks',
); // inline source map
await test(
'./__source__/__compiled__/external/ComponentWithExternalCustomHooks',
); // external source map
});
// TODO Inline require (e.g. require("react").useState()) isn't supported yet.
// Maybe this isn't an important use case to support,
// since inline requires are most likely to exist in compiled source (if at all).
xit('should work for inline requires', async () => {
async function test(path) {
const Component = require(path).Component;
const hookNames = await getHookNamesForComponent(Component);
expectHookNamesToEqual(hookNames, [
'count', // useState()
]);
}
await test('./__source__/InlineRequire'); // original source (uncompiled)
await test('./__source__/__compiled__/inline/InlineRequire'); // inline source map
await test('./__source__/__compiled__/external/InlineRequire'); // external source map
});
});
});

View File

@ -0,0 +1,76 @@
const {transformSync} = require('@babel/core');
const {btoa} = require('base64');
const {
lstatSync,
mkdirSync,
readdirSync,
readFileSync,
writeFileSync,
} = require('fs');
const {emptyDirSync} = require('fs-extra');
const {resolve} = require('path');
const sourceDir = resolve(__dirname, '__source__');
const buildRoot = resolve(sourceDir, '__compiled__');
const externalDir = resolve(buildRoot, 'external');
const inlineDir = resolve(buildRoot, 'inline');
// Remove previous builds
emptyDirSync(buildRoot);
mkdirSync(externalDir);
mkdirSync(inlineDir);
function compile(fileName) {
const code = readFileSync(resolve(sourceDir, fileName), 'utf8');
const transformed = transformSync(code, {
plugins: ['@babel/plugin-transform-modules-commonjs'],
presets: [
// 'minify',
[
'@babel/react',
// {
// runtime: 'automatic',
// development: false,
// },
],
],
sourceMap: true,
});
const sourceMap = transformed.map;
sourceMap.sources = [fileName];
// Generate compiled output with external source maps
writeFileSync(
resolve(externalDir, fileName),
transformed.code +
`\n//# sourceMappingURL=${fileName}.map` +
`\n//# sourceURL=${fileName}`,
'utf8',
);
writeFileSync(
resolve(externalDir, `${fileName}.map`),
JSON.stringify(sourceMap),
'utf8',
);
// Generate compiled output with external inline base64 source maps
writeFileSync(
resolve(inlineDir, fileName),
transformed.code +
'\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,' +
btoa(JSON.stringify(sourceMap)) +
`\n//# sourceURL=${fileName}`,
'utf8',
);
}
// Compile all files in the current directory
const entries = readdirSync(sourceDir);
entries.forEach(entry => {
const stat = lstatSync(resolve(sourceDir, entry));
if (!stat.isDirectory() && entry.endsWith('.js')) {
compile(entry);
}
});

View File

@ -0,0 +1,366 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import traverse, {NodePath, Node} from '@babel/traverse';
import {File} from '@babel/types';
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
export type SourceConsumer = any;
export type SourceFileASTWithHookDetails = {
sourceFileAST: File,
line: number,
source: string,
};
export type SourceMap = {|
mappings: string,
names: Array<string>,
sources: Array<string>,
sourcesContent: Array<string>,
version: number,
|};
const AST_NODE_TYPES = Object.freeze({
CALL_EXPRESSION: 'CallExpression',
MEMBER_EXPRESSION: 'MemberExpression',
ARRAY_PATTERN: 'ArrayPattern',
IDENTIFIER: 'Identifier',
NUMERIC_LITERAL: 'NumericLiteral',
});
// Check if line number obtained from source map and the line number in hook node match
function checkNodeLocation(path: NodePath, line: number): boolean {
const {start, end} = path.node.loc;
return line >= start.line && line <= end.line;
}
// Checks whether hookNode is a member of targetHookNode
function filterMemberNodesOfTargetHook(
targetHookNode: NodePath,
hookNode: NodePath,
): boolean {
const targetHookName = targetHookNode.node.id.name;
return (
targetHookName ===
(hookNode.node.init.object && hookNode.node.init.object.name) ||
targetHookName === hookNode.node.init.name
);
}
// Checks whether hook is the first member node of a state variable declaration node
function filterMemberWithHookVariableName(hook: NodePath): boolean {
return (
hook.node.init.property.type === AST_NODE_TYPES.NUMERIC_LITERAL &&
hook.node.init.property.value === 0
);
}
// Returns all AST Nodes associated with 'potentialReactHookASTNode'
function getFilteredHookASTNodes(
potentialReactHookASTNode: NodePath,
potentialHooksFound: Array<NodePath>,
source: string,
): Array<NodePath> {
let nodesAssociatedWithReactHookASTNode: NodePath[] = [];
if (nodeContainsHookVariableName(potentialReactHookASTNode)) {
// made custom hooks to enter this, always
// Case 1.
// Directly usable Node -> const ref = useRef(null);
// -> const [tick, setTick] = useState(1);
// Case 2.
// Custom Hooks -> const someVariable = useSomeCustomHook();
// -> const [someVariable, someFunction] = useAnotherCustomHook();
nodesAssociatedWithReactHookASTNode.unshift(potentialReactHookASTNode);
} else {
// Case 3.
// Indirectly usable Node -> const tickState = useState(1);
// [tick, setTick] = tickState;
// -> const tickState = useState(1);
// const tick = tickState[0];
// const setTick = tickState[1];
nodesAssociatedWithReactHookASTNode = potentialHooksFound.filter(hookNode =>
filterMemberNodesOfTargetHook(potentialReactHookASTNode, hookNode),
);
}
return nodesAssociatedWithReactHookASTNode;
}
// Returns Hook name
export function getHookName(
hook: HooksNode,
originalSourceAST: mixed,
originalSourceCode: string,
originalSourceLineNumber: number,
): string | null {
const hooksFromAST = getPotentialHookDeclarationsFromAST(originalSourceAST);
const potentialReactHookASTNode = hooksFromAST.find(node => {
const nodeLocationCheck = checkNodeLocation(
node,
((originalSourceLineNumber: any): number),
);
const hookDeclaractionCheck = isConfirmedHookDeclaration(node);
return nodeLocationCheck && hookDeclaractionCheck;
});
if (!potentialReactHookASTNode) {
return null;
}
// nodesAssociatedWithReactHookASTNode could directly be used to obtain the hook variable name
// depending on the type of potentialReactHookASTNode
try {
const nodesAssociatedWithReactHookASTNode = getFilteredHookASTNodes(
potentialReactHookASTNode,
hooksFromAST,
originalSourceCode,
);
return getHookNameFromNode(
hook,
nodesAssociatedWithReactHookASTNode,
potentialReactHookASTNode,
);
} catch (error) {
console.error(error);
return null;
}
}
function getHookNameFromNode(
originalHook: HooksNode,
nodesAssociatedWithReactHookASTNode: NodePath[],
potentialReactHookASTNode: NodePath,
): string | null {
let hookVariableName: string | null;
const isCustomHook = originalHook.id === null;
switch (nodesAssociatedWithReactHookASTNode.length) {
case 1:
// CASE 1A (nodesAssociatedWithReactHookASTNode[0] !== potentialReactHookASTNode):
// const flagState = useState(true); -> later referenced as
// const [flag, setFlag] = flagState;
//
// CASE 1B (nodesAssociatedWithReactHookASTNode[0] === potentialReactHookASTNode):
// const [flag, setFlag] = useState(true); -> we have access to the hook variable straight away
//
// CASE 1C (isCustomHook && nodesAssociatedWithReactHookASTNode[0] === potentialReactHookASTNode):
// const someVariable = useSomeCustomHook(); -> we have access to hook variable straight away
// const [someVariable, someFunction] = useAnotherCustomHook(); -> we ignore variable names in this case
// as it is unclear what variable name to show
if (
isCustomHook &&
nodesAssociatedWithReactHookASTNode[0] === potentialReactHookASTNode
) {
hookVariableName = getHookVariableName(
potentialReactHookASTNode,
isCustomHook,
);
break;
}
hookVariableName = getHookVariableName(
nodesAssociatedWithReactHookASTNode[0],
);
break;
case 2:
// const flagState = useState(true); -> later referenced as
// const flag = flagState[0];
// const setFlag = flagState[1];
nodesAssociatedWithReactHookASTNode = nodesAssociatedWithReactHookASTNode.filter(
hookPath => filterMemberWithHookVariableName(hookPath),
);
if (nodesAssociatedWithReactHookASTNode.length !== 1) {
// Something went wrong, only a single desirable hook should remain here
throw new Error("Couldn't isolate AST Node containing hook variable.");
}
hookVariableName = getHookVariableName(
nodesAssociatedWithReactHookASTNode[0],
);
break;
default:
// Case 0:
// const flagState = useState(true); -> which is not accessed anywhere
//
// Case > 2 (fallback):
// const someState = React.useState(() => 0)
//
// const stateVariable = someState[0]
// const setStateVariable = someState[1]
//
// const [number2, setNumber2] = someState
//
// We assign the state variable for 'someState' to multiple variables,
// and hence cannot isolate a unique variable name. In such cases,
// default to showing 'someState'
hookVariableName = getHookVariableName(potentialReactHookASTNode);
break;
}
return hookVariableName;
}
// Extracts the variable name from hook node path
function getHookVariableName(
hook: NodePath,
isCustomHook: boolean = false,
): string | null {
const nodeType = hook.node.id.type;
switch (nodeType) {
case AST_NODE_TYPES.ARRAY_PATTERN:
return !isCustomHook ? hook.node.id.elements[0].name : null;
case AST_NODE_TYPES.IDENTIFIER:
return hook.node.id.name;
default:
throw new Error(`Invalid node type: ${nodeType}`);
}
}
function getPotentialHookDeclarationsFromAST(sourceAST: File): NodePath[] {
const potentialHooksFound: NodePath[] = [];
traverse(sourceAST, {
enter(path) {
if (path.isVariableDeclarator() && isPotentialHookDeclaration(path)) {
potentialHooksFound.push(path);
}
},
});
return potentialHooksFound;
}
// Check if 'path' contains declaration of the form const X = useState(0);
function isConfirmedHookDeclaration(path: NodePath): boolean {
const node = path.node.init;
if (node.type !== AST_NODE_TYPES.CALL_EXPRESSION) {
return false;
}
const callee = node.callee;
return isHook(callee);
}
// We consider hooks to be a hook name identifier or a member expression containing a hook name.
function isHook(node: Node): boolean {
if (node.type === AST_NODE_TYPES.IDENTIFIER) {
return isHookName(node.name);
} else if (
node.type === AST_NODE_TYPES.MEMBER_EXPRESSION &&
!node.computed &&
isHook(node.property)
) {
const obj = node.object;
const isPascalCaseNameSpace = /^[A-Z].*/;
return (
obj.type === AST_NODE_TYPES.IDENTIFIER &&
isPascalCaseNameSpace.test(obj.name)
);
} else {
// TODO Possibly handle inline require statements e.g. require("useStable")(...)
// This does not seem like a high priority, since inline requires are probably
// not common and are also typically in compiled code rather than source code.
return false;
}
}
// Catch all identifiers that begin with "use"
// followed by an uppercase Latin character to exclude identifiers like "user".
// Copied from packages/eslint-plugin-react-hooks/src/RulesOfHooks
function isHookName(name: string): boolean {
return /^use[A-Z0-9].*$/.test(name);
}
// Determines whether incoming hook is a primitive hook that gets assigned to variables.
export function isNonDeclarativePrimitiveHook(hook: HooksNode) {
return ['Effect', 'ImperativeHandle', 'LayoutEffect', 'DebugValue'].includes(
hook.name,
);
}
// Check if the AST Node COULD be a React Hook
function isPotentialHookDeclaration(path: NodePath): boolean {
// The array potentialHooksFound will contain all potential hook declaration cases we support
const nodePathInit = path.node.init;
if (nodePathInit != null) {
if (nodePathInit.type === AST_NODE_TYPES.CALL_EXPRESSION) {
// CASE: CallExpression
// 1. const [count, setCount] = useState(0); -> destructured pattern
// 2. const [A, setA] = useState(0), const [B, setB] = useState(0); -> multiple inline declarations
// 3. const [
// count,
// setCount
// ] = useState(0); -> multiline hook declaration
// 4. const ref = useRef(null); -> generic hooks
const callee = nodePathInit.callee;
return isHook(callee);
} else if (
nodePathInit.type === AST_NODE_TYPES.MEMBER_EXPRESSION ||
nodePathInit.type === AST_NODE_TYPES.IDENTIFIER
) {
// CASE: MemberExpression
// const countState = React.useState(0);
// const count = countState[0];
// const setCount = countState[1]; -> Accessing members following hook declaration
// CASE: Identifier
// const countState = React.useState(0);
// const [count, setCount] = countState; -> destructuring syntax following hook declaration
return true;
}
}
return false;
}
/// Check whether 'node' is hook decalration of form useState(0); OR React.useState(0);
function isReactFunction(node: Node, functionName: string): boolean {
return (
node.name === functionName ||
(node.type === 'MemberExpression' &&
node.object.name === 'React' &&
node.property.name === functionName)
);
}
// Check if 'path' is either State or Reducer hook
function isStateOrReducerHook(path: NodePath): boolean {
const callee = path.node.init.callee;
return (
isReactFunction(callee, 'useState') || isReactFunction(callee, 'useReducer')
);
}
// Check whether hookNode of a declaration contains obvious variable name
function nodeContainsHookVariableName(hookNode: NodePath): boolean {
// We determine cases where variable names are obvious in declarations. Examples:
// const [tick, setTick] = useState(1); OR const ref = useRef(null);
// Here tick/ref are obvious hook variables in the hook declaration node itself
// 1. True for satisfying above cases
// 2. False for everything else. Examples:
// const countState = React.useState(0);
// const count = countState[0];
// const setCount = countState[1]; -> not obvious, hook variable can't be determined
// from the hook declaration node alone
// 3. For custom hooks we force pass true since we are only concerned with the AST node
// regardless of how it is accessed in source code. (See: getHookVariableName)
const node = hookNode.node.id;
if (
node.type === AST_NODE_TYPES.ARRAY_PATTERN ||
(node.type === AST_NODE_TYPES.IDENTIFIER && !isStateOrReducerHook(hookNode))
) {
return true;
}
return false;
}

View File

@ -12,6 +12,7 @@ import {
getSavedComponentFilters,
getShowInlineWarningsAndErrors,
} from 'react-devtools-shared/src/utils';
import parseHookNames from './parseHookNames';
import {
localStorageGetItem,
localStorageRemoveItem,
@ -214,6 +215,7 @@ function createPanelIfReactLoaded() {
browserTheme: getBrowserTheme(),
componentsPortalContainer,
enabledInspectedElementContextMenu: true,
loadHookNamesFunction: parseHookNames,
overrideTab,
profilerPortalContainer,
showTabBar: false,

View File

@ -0,0 +1,501 @@
/* global chrome */
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {parse} from '@babel/parser';
import {enableHookNameParsing} from 'react-devtools-feature-flags';
import LRU from 'lru-cache';
import {SourceMapConsumer} from 'source-map';
import {getHookName, isNonDeclarativePrimitiveHook} from './astUtils';
import {areSourceMapsAppliedToErrors} from './ErrorTester';
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import type {
HooksNode,
HookSource,
HooksTree,
} from 'react-debug-tools/src/ReactDebugHooks';
import type {HookNames, LRUCache} from 'react-devtools-shared/src/types';
import type {Thenable} from 'shared/ReactTypes';
import type {SourceConsumer, SourceMap} from './astUtils';
const SOURCE_MAP_REGEX = / ?sourceMappingURL=([^\s'"]+)/gm;
const ABSOLUTE_URL_REGEX = /^https?:\/\//i;
const MAX_SOURCE_LENGTH = 100_000_000;
type AST = mixed;
type HookSourceData = {|
// Generated by react-debug-tools.
hookSource: HookSource,
// AST for original source code; typically comes from a consumed source map.
originalSourceAST: AST | null,
// Source code (React components or custom hooks) containing primitive hook calls.
// If no source map has been provided, this code will be the same as runtimeSourceCode.
originalSourceCode: string | null,
// Compiled code (React components or custom hooks) containing primitive hook calls.
runtimeSourceCode: string | null,
// APIs from source-map for parsing source maps (if detected).
sourceConsumer: SourceConsumer | null,
// External URL of source map.
// Sources without source maps (or with inline source maps) won't have this.
sourceMapURL: string | null,
// Parsed source map object.
sourceMapContents: SourceMap | null,
|};
type CachedMetadata = {|
originalSourceAST: AST,
originalSourceCode: string,
sourceConsumer: SourceConsumer | null,
|};
// On large trees, encoding takes significant time.
// Try to reuse the already encoded strings.
const fileNameToMetadataCache: LRUCache<string, CachedMetadata> = new LRU({
max: 50,
dispose: (fileName: string, metadata: CachedMetadata) => {
if (__DEBUG__) {
console.log(
'fileNameToHookSourceData.dispose() Evicting cached metadata for "' +
fileName +
'"',
);
}
const sourceConsumer = metadata.sourceConsumer;
if (sourceConsumer !== null) {
sourceConsumer.destroy();
}
},
});
export default async function parseHookNames(
hooksTree: HooksTree,
): Thenable<HookNames | null> {
if (!enableHookNameParsing) {
return Promise.resolve(null);
}
const hooksList: Array<HooksNode> = [];
flattenHooksList(hooksTree, hooksList);
if (__DEBUG__) {
console.log('parseHookNames() hooksList:', hooksList);
}
// Gather the unique set of source files to load for the built-in hooks.
const fileNameToHookSourceData: Map<string, HookSourceData> = new Map();
for (let i = 0; i < hooksList.length; i++) {
const hook = hooksList[i];
const hookSource = hook.hookSource;
if (hookSource == null) {
// Older versions of react-debug-tools don't include this information.
// In this case, we can't continue.
throw Error('Hook source code location not found.');
}
const fileName = hookSource.fileName;
if (fileName == null) {
throw Error('Hook source code location not found.');
} else {
if (!fileNameToHookSourceData.has(fileName)) {
const hookSourceData: HookSourceData = {
hookSource,
originalSourceAST: null,
originalSourceCode: null,
runtimeSourceCode: null,
sourceConsumer: null,
sourceMapURL: null,
sourceMapContents: null,
};
// If we've already loaded source/source map info for this file,
// we can skip reloading it (and more importantly, re-parsing it).
const metadata = fileNameToMetadataCache.get(fileName);
if (metadata != null) {
if (__DEBUG__) {
console.groupCollapsed(
'parseHookNames() Found cached metadata for file "' +
fileName +
'"',
);
console.log(metadata);
console.groupEnd();
}
hookSourceData.originalSourceAST = metadata.originalSourceAST;
hookSourceData.originalSourceCode = metadata.originalSourceCode;
hookSourceData.sourceConsumer = metadata.sourceConsumer;
}
fileNameToHookSourceData.set(fileName, hookSourceData);
}
}
}
return loadSourceFiles(fileNameToHookSourceData)
.then(() => extractAndLoadSourceMaps(fileNameToHookSourceData))
.then(() => parseSourceAST(fileNameToHookSourceData))
.then(() => updateLruCache(fileNameToHookSourceData))
.then(() => findHookNames(hooksList, fileNameToHookSourceData));
}
function decodeBase64String(encoded: string): Object {
if (typeof atob === 'function') {
return atob(encoded);
} else if (
typeof Buffer !== 'undefined' &&
Buffer !== null &&
typeof Buffer.from === 'function'
) {
return Buffer.from(encoded, 'base64');
} else {
throw Error('Cannot decode base64 string');
}
}
function extractAndLoadSourceMaps(
fileNameToHookSourceData: Map<string, HookSourceData>,
): Promise<*> {
const promises = [];
fileNameToHookSourceData.forEach(hookSourceData => {
if (hookSourceData.originalSourceAST !== null) {
// Use cached metadata.
return;
}
const runtimeSourceCode = ((hookSourceData.runtimeSourceCode: any): string);
const sourceMappingURLs = runtimeSourceCode.match(SOURCE_MAP_REGEX);
if (sourceMappingURLs == null) {
// Maybe file has not been transformed; we'll try to parse it as-is in parseSourceAST().
if (__DEBUG__) {
console.log('extractAndLoadSourceMaps() No source map found');
}
} else {
for (let i = 0; i < sourceMappingURLs.length; i++) {
const sourceMappingURL = sourceMappingURLs[i];
const index = sourceMappingURL.indexOf('base64,');
if (index >= 0) {
// Web apps like Code Sandbox embed multiple inline source maps.
// In this case, we need to loop through and find the right one.
// We may also need to trim any part of this string that isn't based64 encoded data.
const trimmed = ((sourceMappingURL.match(
/base64,([a-zA-Z0-9+\/=]+)/,
): any): Array<string>)[1];
const decoded = decodeBase64String(trimmed);
const parsed = JSON.parse(decoded);
if (__DEBUG__) {
console.groupCollapsed(
'extractAndLoadSourceMaps() Inline source map',
);
console.log(parsed);
console.groupEnd();
}
// Hook source might be a URL like "https://4syus.csb.app/src/App.js"
// Parsed source map might be a partial path like "src/App.js"
const fileName = ((hookSourceData.hookSource.fileName: any): string);
const match = parsed.sources.find(
source =>
source === 'Inline Babel script' || fileName.includes(source),
);
if (match) {
hookSourceData.sourceMapContents = parsed;
break;
}
} else {
if (sourceMappingURLs.length > 1) {
console.warn(
'More than one external source map detected in the source file',
);
}
let url = sourceMappingURLs[0].split('=')[1];
if (ABSOLUTE_URL_REGEX.test(url)) {
const baseURL = url.slice(0, url.lastIndexOf('/'));
url = `${baseURL}/${url}`;
if (!isValidUrl(url)) {
throw new Error(`Invalid source map URL "${url}"`);
}
}
hookSourceData.sourceMapURL = url;
if (__DEBUG__) {
console.log(
'extractAndLoadSourceMaps() External source map "' + url + '"',
);
}
promises.push(
fetchFile(url).then(sourceMapContents => {
hookSourceData.sourceMapContents = JSON.parse(sourceMapContents);
}),
);
break;
}
}
}
});
return Promise.all(promises);
}
function fetchFile(url: string): Promise<string> {
return new Promise((resolve, reject) => {
fetch(url).then(response => {
if (response.ok) {
response
.text()
.then(text => {
resolve(text);
})
.catch(error => {
reject(null);
});
} else {
reject(null);
}
});
});
}
function findHookNames(
hooksList: Array<HooksNode>,
fileNameToHookSourceData: Map<string, HookSourceData>,
): HookNames {
const map: HookNames = new Map();
hooksList.map(hook => {
if (isNonDeclarativePrimitiveHook(hook)) {
if (__DEBUG__) {
console.log('findHookNames() Non declarative primitive hook');
}
// Not all hooks have names (e.g. useEffect or useLayoutEffect)
return null;
}
// We already guard against a null HookSource in parseHookNames()
const hookSource = ((hook.hookSource: any): HookSource);
const fileName = hookSource.fileName;
if (!fileName) {
return null; // Should not be reachable.
}
const hookSourceData = fileNameToHookSourceData.get(fileName);
if (!hookSourceData) {
return null; // Should not be reachable.
}
const {lineNumber, columnNumber} = hookSource;
if (!lineNumber || !columnNumber) {
return null; // Should not be reachable.
}
const sourceConsumer = hookSourceData.sourceConsumer;
let originalSourceLineNumber;
if (areSourceMapsAppliedToErrors() || !sourceConsumer) {
// Either the current environment automatically applies source maps to errors,
// or the current code had no source map to begin with.
// Either way, we don't need to convert the Error stack frame locations.
originalSourceLineNumber = lineNumber;
} else {
originalSourceLineNumber = sourceConsumer.originalPositionFor({
line: lineNumber,
column: columnNumber,
}).line;
}
if (__DEBUG__) {
console.log(
'findHookNames() mapped line number',
lineNumber,
'to',
originalSourceLineNumber,
);
}
if (originalSourceLineNumber === null) {
return null;
}
const name = getHookName(
hook,
hookSourceData.originalSourceAST,
((hookSourceData.originalSourceCode: any): string),
((originalSourceLineNumber: any): number),
);
if (__DEBUG__) {
console.log('findHookNames() Found name "' + (name || '-') + '"');
}
map.set(hook, name);
});
return map;
}
function isValidUrl(possibleURL: string): boolean {
try {
// eslint-disable-next-line no-new
new URL(possibleURL);
} catch (_) {
return false;
}
return true;
}
function loadSourceFiles(
fileNameToHookSourceData: Map<string, HookSourceData>,
): Promise<*> {
const promises = [];
fileNameToHookSourceData.forEach((hookSourceData, fileName) => {
promises.push(
fetchFile(fileName).then(runtimeSourceCode => {
if (runtimeSourceCode.length > MAX_SOURCE_LENGTH) {
throw Error('Source code too large to parse');
}
if (__DEBUG__) {
console.groupCollapsed(
'loadSourceFiles() fileName "' + fileName + '"',
);
console.log(runtimeSourceCode);
console.groupEnd();
}
hookSourceData.runtimeSourceCode = runtimeSourceCode;
}),
);
});
return Promise.all(promises);
}
async function parseSourceAST(
fileNameToHookSourceData: Map<string, HookSourceData>,
): Promise<*> {
// SourceMapConsumer.initialize() does nothing when running in Node (aka Jest)
// because the wasm file is automatically read from the file system
// so we can avoid triggering a warning message about this.
if (!__TEST__) {
if (__DEBUG__) {
console.log('parseSourceAST() Initializing source-map library ...');
}
// $FlowFixMe
const wasmMappingsURL = chrome.extension.getURL('mappings.wasm');
SourceMapConsumer.initialize({'lib/mappings.wasm': wasmMappingsURL});
}
const promises = [];
fileNameToHookSourceData.forEach(hookSourceData => {
if (hookSourceData.originalSourceAST !== null) {
// Use cached metadata.
return;
}
const {runtimeSourceCode, sourceMapContents} = hookSourceData;
if (sourceMapContents !== null) {
// Parse and extract the AST from the source map.
promises.push(
SourceMapConsumer.with(
sourceMapContents,
null,
(sourceConsumer: SourceConsumer) => {
hookSourceData.sourceConsumer = sourceConsumer;
// Now that the source map has been loaded,
// extract the original source for later.
const source = sourceMapContents.sources[0];
const originalSourceCode = sourceConsumer.sourceContentFor(
source,
true,
);
if (__DEBUG__) {
console.groupCollapsed(
'parseSourceAST() Extracted source code from source map',
);
console.log(originalSourceCode);
console.groupEnd();
}
hookSourceData.originalSourceCode = originalSourceCode;
// TODO Parsing should ideally be done off of the main thread.
hookSourceData.originalSourceAST = parse(originalSourceCode, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});
},
),
);
} else {
// There's no source map to parse here so we can just parse the original source itself.
hookSourceData.originalSourceCode = runtimeSourceCode;
// TODO Parsing should ideally be done off of the main thread.
hookSourceData.originalSourceAST = parse(runtimeSourceCode, {
sourceType: 'unambiguous',
plugins: ['jsx', 'typescript'],
});
}
});
return Promise.all(promises);
}
function flattenHooksList(
hooksTree: HooksTree,
hooksList: Array<HooksNode>,
): void {
for (let i = 0; i < hooksTree.length; i++) {
const hook = hooksTree[i];
hooksList.push(hook);
if (hook.subHooks.length > 0) {
flattenHooksList(hook.subHooks, hooksList);
}
}
}
function updateLruCache(
fileNameToHookSourceData: Map<string, HookSourceData>,
): Promise<*> {
fileNameToHookSourceData.forEach(
({originalSourceAST, originalSourceCode, sourceConsumer}, fileName) => {
// Only set once to avoid triggering eviction/cleanup code.
if (!fileNameToMetadataCache.has(fileName)) {
if (__DEBUG__) {
console.log('updateLruCache() Caching metada for "' + fileName + '"');
}
fileNameToMetadataCache.set(fileName, {
originalSourceAST,
originalSourceCode: ((originalSourceCode: any): string),
sourceConsumer,
});
}
},
);
return Promise.resolve();
}

View File

@ -32,6 +32,10 @@ module.exports = {
node: {
// Don't define a polyfill on window.setImmediate
setImmediate: false,
// source-maps package has a dependency on 'fs'
// but this build won't trigger that code path
fs: 'empty',
},
resolve: {
alias: {

View File

@ -37,6 +37,10 @@ module.exports = {
node: {
// Don't define a polyfill on window.setImmediate
setImmediate: false,
// source-maps package has a dependency on 'fs'
// but this build won't trigger that code path
fs: 'empty',
},
resolve: {
alias: {
@ -65,6 +69,17 @@ module.exports = {
}),
],
module: {
defaultRules: [
{
type: 'javascript/auto',
resolve: {},
},
{
test: /\.json$/i,
type: 'json',
},
],
rules: [
{
test: /\.js$/,

View File

@ -37,6 +37,11 @@ module.exports = {
'react-is': 'react-is',
scheduler: 'scheduler',
},
node: {
// source-maps package has a dependency on 'fs'
// but this build won't trigger that code path
fs: 'empty',
},
resolve: {
alias: {
'react-devtools-feature-flags': resolveFeatureFlags('inline'),

View File

@ -7,7 +7,9 @@
"react-dom-15": "npm:react-dom@^15"
},
"dependencies": {
"@babel/parser": "^7.12.5",
"@babel/runtime": "^7.11.2",
"@babel/traverse": "^7.12.5",
"@reach/menu-button": "^0.1.17",
"@reach/tooltip": "^0.2.2",
"clipboard-js": "^0.3.6",

View File

@ -0,0 +1,60 @@
function hasAbsoluteFileName(hook) {
const fileName = hook.hookSource ? hook.hookSource.fileName : null;
if (fileName == null) {
return false;
} else {
return fileName.indexOf('/react-devtools-shared/') > 0;
}
}
function serializeHook(hook) {
if (!hook.hookSource) return hook;
// Remove user-specific portions of this file path.
let fileName = hook.hookSource.fileName;
const index = fileName.lastIndexOf('/react-devtools-shared/');
fileName = fileName.substring(index + 1);
let subHooks = hook.subHooks;
if (subHooks) {
subHooks = subHooks.map(serializeHook);
}
return {
...hook,
hookSource: {
...hook.hookSource,
fileName,
// Otherwise changes in any test case or formatting might invalidate other tests.
columnNumber: 'removed by Jest serializer',
lineNumber: 'removed by Jest serializer',
},
subHooks,
};
}
// test() is part of Jest's serializer API
export function test(maybeHook) {
if (maybeHook === null || typeof maybeHook !== 'object') {
return false;
}
const hasOwnProperty = Object.prototype.hasOwnProperty.bind(maybeHook);
return (
hasOwnProperty('id') &&
hasOwnProperty('isStateEditable') &&
hasOwnProperty('name') &&
hasOwnProperty('subHooks') &&
hasOwnProperty('value') &&
// Don't re-process an already printed hook.
hasAbsoluteFileName(maybeHook)
);
}
// print() is part of Jest's serializer API
export function print(hook, serialize, indent) {
// Don't stringify this object; that would break nested serializers.
return serialize(serializeHook(hook));
}

View File

@ -25,6 +25,7 @@ describe('InspectedElement', () => {
let BridgeContext;
let InspectedElementContext;
let InspectedElementContextController;
let SettingsContextController;
let StoreContext;
let TreeContextController;
@ -57,6 +58,8 @@ describe('InspectedElement', () => {
.InspectedElementContext;
InspectedElementContextController = require('react-devtools-shared/src/devtools/views/Components/InspectedElementContext')
.InspectedElementContextController;
SettingsContextController = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext')
.SettingsContextController;
StoreContext = require('react-devtools-shared/src/devtools/views/context')
.StoreContext;
TreeContextController = require('react-devtools-shared/src/devtools/views/Components/TreeContext')
@ -79,15 +82,17 @@ describe('InspectedElement', () => {
}) => (
<BridgeContext.Provider value={bridge}>
<StoreContext.Provider value={store}>
<TreeContextController
defaultSelectedElementID={defaultSelectedElementID}
defaultSelectedElementIndex={defaultSelectedElementIndex}>
<React.Suspense fallback="Loading...">
<InspectedElementContextController>
{children}
</InspectedElementContextController>
</React.Suspense>
</TreeContextController>
<SettingsContextController>
<TreeContextController
defaultSelectedElementID={defaultSelectedElementID}
defaultSelectedElementIndex={defaultSelectedElementIndex}>
<React.Suspense fallback="Loading...">
<InspectedElementContextController>
{children}
</InspectedElementContextController>
</React.Suspense>
</TreeContextController>
</SettingsContextController>
</StoreContext.Provider>
</BridgeContext.Provider>
);

View File

@ -10,6 +10,7 @@ export function test(maybeInspectedElement) {
const hasOwnProperty = Object.prototype.hasOwnProperty.bind(
maybeInspectedElement,
);
return (
hasOwnProperty('canEditFunctionProps') &&
hasOwnProperty('canEditHooks') &&

View File

@ -7,3 +7,4 @@ if (!global.hasOwnProperty('localStorage')) {
// Mimic the global we set with Webpack's DefinePlugin
global.__DEV__ = process.env.NODE_ENV !== 'production';
global.__TEST__ = true;

View File

@ -122,6 +122,16 @@ env.beforeEach(() => {
global.agent = agent;
global.bridge = bridge;
global.store = store;
const readFileSync = require('fs').readFileSync;
async function mockFetch(url) {
return {
ok: true,
status: 200,
text: async () => readFileSync(__dirname + url, 'utf-8'),
};
}
global.fetch = mockFetch;
});
env.afterEach(() => {
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;

View File

@ -268,3 +268,13 @@ export function withErrorsOrWarningsIgnored<T: void | Promise<void>>(
}
}
}
export function overrideFeatureFlags(overrideFlags) {
jest.mock('react-devtools-feature-flags', () => {
const actualFlags = jest.requireActual('react-devtools-feature-flags');
return {
...actualFlags,
...overrideFlags,
};
});
}

View File

@ -80,7 +80,10 @@ import {
MEMO_SYMBOL_STRING,
} from './ReactSymbols';
import {format} from './utils';
import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
import {
enableHookNameParsing,
enableProfilerChangedHookIndices,
} from 'react-devtools-feature-flags';
import is from 'shared/objectIs';
import isArray from 'shared/isArray';
@ -3092,6 +3095,7 @@ export function attach(
hooks = inspectHooksOfFiber(
fiber,
(renderer.currentDispatcherRef: any),
enableHookNameParsing, // Include source location info for hooks
);
} finally {
// Restore original console functionality.

View File

@ -13,5 +13,6 @@
* It should always be imported from "react-devtools-feature-flags".
************************************************************************/
export const enableHookNameParsing = false;
export const enableProfilerChangedHookIndices = false;
export const isInternalFacebookBuild = false;

View File

@ -13,6 +13,7 @@
* It should always be imported from "react-devtools-feature-flags".
************************************************************************/
export const enableHookNameParsing = true;
export const enableProfilerChangedHookIndices = true;
export const isInternalFacebookBuild = true;

View File

@ -13,6 +13,7 @@
* It should always be imported from "react-devtools-feature-flags".
************************************************************************/
export const enableHookNameParsing = false;
export const enableProfilerChangedHookIndices = false;
export const isInternalFacebookBuild = false;

View File

@ -23,6 +23,9 @@ export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
export const SESSION_STORAGE_LAST_SELECTION_KEY =
'React::DevTools::lastSelection';
export const LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY =
'React::DevTools::parseHookNames';
export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
'React::DevTools::recordChangeDescriptions';

View File

@ -26,6 +26,7 @@ export type IconType =
| 'log-data'
| 'more'
| 'next'
| 'parse-hook-names'
| 'previous'
| 'record'
| 'reload'
@ -92,6 +93,9 @@ export default function ButtonIcon({className = '', type}: Props) {
case 'next':
pathData = PATH_NEXT;
break;
case 'parse-hook-names':
pathData = PATH_PARSE_HOOK_NAMES;
break;
case 'previous':
pathData = PATH_PREVIOUS;
break;
@ -141,7 +145,11 @@ export default function ButtonIcon({className = '', type}: Props) {
height="24"
viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path fill="currentColor" d={pathData} />
{typeof pathData === 'string' ? (
<path fill="currentColor" d={pathData} />
) : (
pathData
)}
</svg>
);
}
@ -197,6 +205,15 @@ const PATH_MORE = `
const PATH_NEXT = 'M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z';
const PATH_PARSE_HOOK_NAMES = (
<g>
<polygon points="20,7 20.94,4.94 23,4 20.94,3.06 20,1 19.06,3.06 17,4 19.06,4.94" />
<polygon points="8.5,7 9.44,4.94 11.5,4 9.44,3.06 8.5,1 7.56,3.06 5.5,4 7.56,4.94" />
<polygon points="20,12.5 19.06,14.56 17,15.5 19.06,16.44 20,18.5 20.94,16.44 23,15.5 20.94,14.56" />
<path d="M17.71,9.12l-2.83-2.83C14.68,6.1,14.43,6,14.17,6c-0.26,0-0.51,0.1-0.71,0.29L2.29,17.46c-0.39,0.39-0.39,1.02,0,1.41 l2.83,2.83C5.32,21.9,5.57,22,5.83,22s0.51-0.1,0.71-0.29l11.17-11.17C18.1,10.15,18.1,9.51,17.71,9.12z M14.17,8.42l1.41,1.41 L14.41,11L13,9.59L14.17,8.42z M5.83,19.59l-1.41-1.41L11.59,11L13,12.41L5.83,19.59z" />
</g>
);
const PATH_PREVIOUS =
'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z';

View File

@ -26,7 +26,7 @@ export default function Badge({
type,
children,
}: Props) {
if (hocDisplayNames === null) {
if (hocDisplayNames === null || hocDisplayNames.length === 0) {
return null;
}

View File

@ -68,4 +68,5 @@
justify-content: center;
font-size: var(--font-size-sans-large);
color: var(--color-dim);
border-left: 1px solid var(--color-border);
}

View File

@ -64,5 +64,5 @@
padding: 0.25rem;
color: var(--color-dimmer);
font-style: italic;
border-left: 1px solid var(--color-border);
}

View File

@ -39,7 +39,12 @@ export default function InspectedElementWrapper(_: Props) {
const store = useContext(StoreContext);
const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
const {inspectedElement} = useContext(InspectedElementContext);
const {
hookNames,
inspectedElement,
parseHookNames,
toggleParseHookNames,
} = useContext(InspectedElementContext);
const element =
inspectedElementID !== null
@ -268,7 +273,10 @@ export default function InspectedElementWrapper(_: Props) {
inspectedElementID /* Force reset when selected Element changes */
}
element={element}
hookNames={hookNames}
inspectedElement={inspectedElement}
parseHookNames={parseHookNames}
toggleParseHookNames={toggleParseHookNames}
/>
)}
</div>

View File

@ -18,13 +18,19 @@ import {
useMemo,
useState,
} from 'react';
import {enableHookNameParsing} from 'react-devtools-feature-flags';
import {TreeStateContext} from './TreeContext';
import {BridgeContext, StoreContext} from '../context';
import {
checkForUpdate,
inspectElement,
} from 'react-devtools-shared/src/inspectedElementCache';
import {loadHookNames} from 'react-devtools-shared/src/hookNamesCache';
import {ElementTypeFunction} from 'react-devtools-shared/src/types';
import LoadHookNamesFunctionContext from 'react-devtools-shared/src/devtools/views/Components/LoadHookNamesFunctionContext';
import {SettingsContext} from '../Settings/SettingsContext';
import type {HookNames} from 'react-devtools-shared/src/types';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {
Element,
@ -33,10 +39,14 @@ import type {
type Path = Array<string | number>;
type InspectPathFunction = (path: Path) => void;
export type ToggleParseHookNames = () => void;
type Context = {|
hookNames: HookNames | null,
inspectedElement: InspectedElement | null,
inspectPaths: InspectPathFunction,
parseHookNames: boolean,
toggleParseHookNames: ToggleParseHookNames,
|};
export const InspectedElementContext = createContext<Context>(
@ -51,8 +61,10 @@ export type Props = {|
export function InspectedElementContextController({children}: Props) {
const {selectedElementID} = useContext(TreeStateContext);
const loadHookNamesFunction = useContext(LoadHookNamesFunctionContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const {parseHookNames: parseHookNamesByDefault} = useContext(SettingsContext);
const refresh = useCacheRefresh();
@ -68,6 +80,13 @@ export function InspectedElementContextController({children}: Props) {
path: null,
});
// Parse the currently inspected element's hook names.
// This may be enabled by default (for all elements)
// or it may be opted into on a per-element basis (if it's too slow to be on by default).
const [parseHookNames, setParseHookNames] = useState<boolean>(
parseHookNamesByDefault,
);
const element =
selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
@ -79,14 +98,41 @@ export function InspectedElementContextController({children}: Props) {
element,
path: null,
});
setParseHookNames(parseHookNamesByDefault);
}
// Don't load a stale element from the backend; it wastes bridge bandwidth.
let inspectedElement = null;
let hookNames: HookNames | null = null;
if (!elementHasChanged && element !== null) {
inspectedElement = inspectElement(element, state.path, store, bridge);
if (enableHookNameParsing) {
if (parseHookNames) {
if (
inspectedElement !== null &&
inspectedElement.type === ElementTypeFunction &&
inspectedElement.hooks !== null &&
loadHookNamesFunction !== null
) {
hookNames = loadHookNames(
element,
inspectedElement.hooks,
loadHookNamesFunction,
);
}
}
}
}
const toggleParseHookNames: ToggleParseHookNames = useCallback<ToggleParseHookNames>(() => {
startTransition(() => {
setParseHookNames(value => !value);
refresh();
});
}, [setParseHookNames]);
const inspectPaths: InspectPathFunction = useCallback<InspectPathFunction>(
(path: Path) => {
startTransition(() => {
@ -125,6 +171,7 @@ export function InspectedElementContextController({children}: Props) {
}
}, [
element,
hookNames,
// Reset this timer any time the element we're inspecting gets a new response.
// No sense to ping right away after e.g. inspecting/hydrating a path.
inspectedElement,
@ -133,10 +180,19 @@ export function InspectedElementContextController({children}: Props) {
const value = useMemo<Context>(
() => ({
hookNames,
inspectedElement,
inspectPaths,
parseHookNames,
toggleParseHookNames,
}),
[inspectedElement, inspectPaths],
[
hookNames,
inspectedElement,
inspectPaths,
parseHookNames,
toggleParseHookNames,
],
);
return (

View File

@ -1,3 +1,3 @@
.Wrapper {
border-left: 1px solid var(--color-border);
height: 100%;
}

View File

@ -77,4 +77,8 @@
margin-right: 0.25rem;
border-radius: 0.125rem;
padding: 0.125rem 0.25rem;
}
.HookName {
color: var(--color-component-name);
}

View File

@ -13,6 +13,7 @@ import {useCallback, useContext, useRef, useState} from 'react';
import {BridgeContext, StoreContext} from '../context';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Toggle from '../Toggle';
import ExpandCollapseToggle from './ExpandCollapseToggle';
import KeyValue from './KeyValue';
import {getMetaValueLabel, serializeHooksForCopy} from '../utils';
@ -20,28 +21,49 @@ import Store from '../../store';
import styles from './InspectedElementHooksTree.css';
import useContextMenu from '../../ContextMenu/useContextMenu';
import {meta} from '../../../hydration';
import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags';
import {
enableHookNameParsing,
enableProfilerChangedHookIndices,
} from 'react-devtools-feature-flags';
import type {InspectedElement} from './types';
import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {HookNames} from 'react-devtools-shared/src/types';
import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types';
import type {ToggleParseHookNames} from './InspectedElementContext';
type HooksTreeViewProps = {|
bridge: FrontendBridge,
element: Element,
hookNames: HookNames | null,
inspectedElement: InspectedElement,
parseHookNames: boolean,
store: Store,
toggleParseHookNames: ToggleParseHookNames,
|};
export function InspectedElementHooksTree({
bridge,
element,
hookNames,
inspectedElement,
parseHookNames,
store,
toggleParseHookNames,
}: HooksTreeViewProps) {
const {hooks, id} = inspectedElement;
// Changing parseHookNames is done in a transition, because it suspends.
// This value is done outside of the transition, so the UI toggle feels responsive.
const [parseHookNamesOptimistic, setParseHookNamesOptimistic] = useState(
parseHookNames,
);
const handleChange = () => {
setParseHookNamesOptimistic(!parseHookNames);
toggleParseHookNames();
};
const handleCopy = () => copy(serializeHooksForCopy(hooks));
if (hooks === null) {
@ -51,11 +73,25 @@ export function InspectedElementHooksTree({
<div className={styles.HooksTreeView}>
<div className={styles.HeaderRow}>
<div className={styles.Header}>hooks</div>
{enableHookNameParsing && !parseHookNames && (
<Toggle
isChecked={parseHookNamesOptimistic}
isDisabled={parseHookNamesOptimistic}
onChange={handleChange}
title={
parseHookNames
? 'Parse hook names'
: 'Parse hook names (may be slow)'
}>
<ButtonIcon type="parse-hook-names" />
</Toggle>
)}
<Button onClick={handleCopy} title="Copy to clipboard">
<ButtonIcon type="copy" />
</Button>
</div>
<InnerHooksTreeView
hookNames={hookNames}
hooks={hooks}
id={id}
element={element}
@ -69,6 +105,7 @@ export function InspectedElementHooksTree({
type InnerHooksTreeViewProps = {|
element: Element,
hookNames: HookNames | null,
hooks: HooksTree,
id: number,
inspectedElement: InspectedElement,
@ -77,6 +114,7 @@ type InnerHooksTreeViewProps = {|
export function InnerHooksTreeView({
element,
hookNames,
hooks,
id,
inspectedElement,
@ -88,6 +126,7 @@ export function InnerHooksTreeView({
key={index}
element={element}
hook={hooks[index]}
hookNames={hookNames}
id={id}
inspectedElement={inspectedElement}
path={path.concat([index])}
@ -98,12 +137,20 @@ export function InnerHooksTreeView({
type HookViewProps = {|
element: Element,
hook: HooksNode,
hookNames: HookNames | null,
id: number,
inspectedElement: InspectedElement,
path: Array<string | number>,
|};
function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
function HookView({
element,
hook,
hookNames,
id,
inspectedElement,
path,
}: HookViewProps) {
const {
canEditHooks,
canEditHooksAndDeletePaths,
@ -180,6 +227,16 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
let displayValue;
let isComplexDisplayValue = false;
const hookName = hookNames != null ? hookNames.get(hook) : null;
const hookDisplayName = hookName ? (
<>
{name}
{!!hookName && <span className={styles.HookName}>({hookName})</span>}
</>
) : (
name
);
// Format data for display to mimic the props/state/context for now.
if (type === 'string') {
displayValue = `"${((value: any): string)}"`;
@ -204,6 +261,7 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
<InnerHooksTreeView
element={element}
hooks={subHooks}
hookNames={hookNames}
id={id}
inspectedElement={inspectedElement}
path={path.concat(['subHooks'])}
@ -219,6 +277,7 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
depth={1}
element={element}
hookID={hookID}
hookName={hookName}
inspectedElement={inspectedElement}
name="subHooks"
path={path.concat(['subHooks'])}
@ -236,7 +295,7 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
<span
onClick={toggleIsOpen}
className={name !== '' ? styles.Name : styles.NameAnonymous}>
{name || 'Anonymous'}
{hookDisplayName || 'Anonymous'}
</span>
<span className={styles.Value} onClick={toggleIsOpen}>
{isOpen || getMetaValueLabel(value)}
@ -253,6 +312,7 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
depth={1}
element={element}
hookID={hookID}
hookName={hookName}
inspectedElement={inspectedElement}
name="DebugValue"
path={path.concat(['value'])}
@ -272,7 +332,7 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
<span
onClick={toggleIsOpen}
className={name !== '' ? styles.Name : styles.NameAnonymous}>
{name || 'Anonymous'}
{hookDisplayName || 'Anonymous'}
</span>{' '}
{/* $FlowFixMe */}
<span className={styles.Value} onClick={toggleIsOpen}>
@ -299,6 +359,7 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
depth={1}
element={element}
hookID={hookID}
hookName={hookName}
inspectedElement={inspectedElement}
name={name}
path={path.concat(['value'])}
@ -320,6 +381,7 @@ function HookView({element, hook, id, inspectedElement, path}: HookViewProps) {
depth={1}
element={element}
hookID={hookID}
hookName={hookName}
inspectedElement={inspectedElement}
name={name}
path={[]}

View File

@ -36,19 +36,26 @@ import styles from './InspectedElementView.css';
import type {ContextMenuContextType} from '../context';
import type {Element, InspectedElement, SerializedElement} from './types';
import type {ElementType} from 'react-devtools-shared/src/types';
import type {ElementType, HookNames} from 'react-devtools-shared/src/types';
import type {ToggleParseHookNames} from './InspectedElementContext';
export type CopyPath = (path: Array<string | number>) => void;
export type InspectPath = (path: Array<string | number>) => void;
type Props = {|
element: Element,
hookNames: HookNames | null,
inspectedElement: InspectedElement,
parseHookNames: boolean,
toggleParseHookNames: ToggleParseHookNames,
|};
export default function InspectedElementView({
element,
hookNames,
inspectedElement,
parseHookNames,
toggleParseHookNames,
}: Props) {
const {id} = element;
const {
@ -103,8 +110,11 @@ export default function InspectedElementView({
<InspectedElementHooksTree
bridge={bridge}
element={element}
hookNames={hookNames}
inspectedElement={inspectedElement}
parseHookNames={parseHookNames}
store={store}
toggleParseHookNames={toggleParseHookNames}
/>
<InspectedElementContextTree

View File

@ -11,6 +11,7 @@
flex: 0 0 auto;
cursor: default;
}
.EditableName {
color: var(--color-attribute-name);
flex: 0 0 auto;
@ -48,4 +49,8 @@
.DeleteArrayItemButton {
padding: 0;
margin-right: 0.125rem;
}
.HookName {
color: var(--color-component-name);
}

View File

@ -43,6 +43,7 @@ type KeyValueProps = {|
element: Element,
hidden: boolean,
hookID?: ?number,
hookName?: ?string,
inspectedElement: InspectedElement,
isDirectChildOfAnArray?: boolean,
name: string,
@ -65,6 +66,7 @@ export default function KeyValue({
isDirectChildOfAnArray,
hidden,
hookID,
hookName,
name,
path,
pathRoot,
@ -202,7 +204,12 @@ export default function KeyValue({
<DeleteToggle name={name} deletePath={deletePath} path={path} />
);
} else {
renderedName = <span className={styles.Name}>{name}</span>;
renderedName = (
<span className={styles.Name}>
{name}
{!!hookName && <span className={styles.HookName}>({hookName})</span>}
</span>
);
}
} else if (canRenameTheCurrentPath) {
renderedName = (
@ -215,7 +222,12 @@ export default function KeyValue({
/>
);
} else {
renderedName = <span className={styles.Name}>{name}</span>;
renderedName = (
<span className={styles.Name}>
{name}
{!!hookName && <span className={styles.HookName}>({hookName})</span>}
</span>
);
}
let children = null;

View File

@ -0,0 +1,11 @@
// @flow
import {createContext} from 'react';
import type {LoadHookNamesFunction} from '../DevTools';
export type Context = LoadHookNamesFunction | null;
const LoadHookNamesFunctionContext = createContext<Context>(null);
LoadHookNamesFunctionContext.displayName = 'LoadHookNamesFunctionContext';
export default LoadHookNamesFunctionContext;

View File

@ -22,6 +22,7 @@ import TabBar from './TabBar';
import {SettingsContextController} from './Settings/SettingsContext';
import {TreeContextController} from './Components/TreeContext';
import ViewElementSourceContext from './Components/ViewElementSourceContext';
import LoadHookNamesFunctionContext from './Components/LoadHookNamesFunctionContext';
import {ProfilerContextController} from './Profiler/ProfilerContext';
import {ModalDialogContextController} from './ModalDialog';
import ReactLogo from './ReactLogo';
@ -34,15 +35,22 @@ import styles from './DevTools.css';
import './root.css';
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
import type {InspectedElement} from 'react-devtools-shared/src/devtools/views/Components/types';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {HookNames} from 'react-devtools-shared/src/types';
import type {Thenable} from '../cache';
export type BrowserTheme = 'dark' | 'light';
export type TabID = 'components' | 'profiler';
export type ViewElementSource = (
id: number,
inspectedElement: InspectedElement,
) => void;
export type LoadHookNamesFunction = (
hooksTree: HooksTree,
) => Thenable<HookNames>;
export type ViewAttributeSource = (
id: number,
path: Array<string | number>,
@ -75,6 +83,11 @@ export type Props = {|
// but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
componentsPortalContainer?: Element,
profilerPortalContainer?: Element,
// Loads and parses source maps for function components
// and extracts hook "names" based on the variables the hook return values get assigned to.
// Not every DevTools build can load source maps, so this property is optional.
loadHookNamesFunction?: ?LoadHookNamesFunction,
|};
const componentsTab = {
@ -99,6 +112,7 @@ export default function DevTools({
componentsPortalContainer,
defaultTab = 'components',
enabledInspectedElementContextMenu = false,
loadHookNamesFunction,
overrideTab,
profilerPortalContainer,
showTabBar = false,
@ -180,7 +194,6 @@ export default function DevTools({
}
};
}, [bridge]);
return (
<BridgeContext.Provider value={bridge}>
<StoreContext.Provider value={store}>
@ -191,40 +204,43 @@ export default function DevTools({
componentsPortalContainer={componentsPortalContainer}
profilerPortalContainer={profilerPortalContainer}>
<ViewElementSourceContext.Provider value={viewElementSource}>
<TreeContextController>
<ProfilerContextController>
<div className={styles.DevTools} ref={devToolsRef}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={setTab}
tabs={tabs}
type="navigation"
<LoadHookNamesFunctionContext.Provider
value={loadHookNamesFunction || null}>
<TreeContextController>
<ProfilerContextController>
<div className={styles.DevTools} ref={devToolsRef}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={setTab}
tabs={tabs}
type="navigation"
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={componentsPortalContainer}
/>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler portalContainer={profilerPortalContainer} />
</div>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler portalContainer={profilerPortalContainer} />
</div>
</div>
</ProfilerContextController>
</TreeContextController>
</ProfilerContextController>
</TreeContextController>
</LoadHookNamesFunctionContext.Provider>
</ViewElementSourceContext.Provider>
</SettingsContextController>
<UnsupportedBridgeProtocolDialog />

View File

@ -16,11 +16,13 @@ import {
useRef,
useState,
} from 'react';
import {enableHookNameParsing} from 'react-devtools-feature-flags';
import {useSubscription} from '../hooks';
import {StoreContext} from '../context';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Toggle from '../Toggle';
import {SettingsContext} from '../Settings/SettingsContext';
import {
ComponentFilterDisplayName,
ComponentFilterElementType,
@ -50,6 +52,7 @@ import type {
export default function ComponentsSettings(_: {||}) {
const store = useContext(StoreContext);
const {parseHookNames, setParseHookNames} = useContext(SettingsContext);
const collapseNodesByDefaultSubscription = useMemo(
() => ({
@ -72,6 +75,13 @@ export default function ComponentsSettings(_: {||}) {
[store],
);
const updateParseHookNames = useCallback(
({currentTarget}) => {
setParseHookNames(currentTarget.checked);
},
[setParseHookNames],
);
const [componentFilters, setComponentFilters] = useState<
Array<ComponentFilter>,
>(() => [...store.componentFilters]);
@ -252,6 +262,18 @@ export default function ComponentsSettings(_: {||}) {
Expand component tree by default
</label>
{enableHookNameParsing && (
<label className={styles.Setting}>
<input
type="checkbox"
checked={parseHookNames}
onChange={updateParseHookNames}
/>{' '}
Always parse hook names from source{' '}
<span className={styles.Warning}>(may be slow)</span>
</label>
)}
<div className={styles.Header}>Hide components where...</div>
<table className={styles.Table}>

View File

@ -18,6 +18,7 @@ import {
import {
COMFORTABLE_LINE_HEIGHT,
COMPACT_LINE_HEIGHT,
LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
@ -45,6 +46,9 @@ type Context = {|
breakOnConsoleErrors: boolean,
setBreakOnConsoleErrors: (value: boolean) => void,
parseHookNames: boolean,
setParseHookNames: (value: boolean) => void,
showInlineWarningsAndErrors: boolean,
setShowInlineWarningsAndErrors: (value: boolean) => void,
@ -94,6 +98,10 @@ function SettingsContextController({
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
false,
);
const [parseHookNames, setParseHookNames] = useLocalStorage<boolean>(
LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
false,
);
const [
showInlineWarningsAndErrors,
setShowInlineWarningsAndErrors,
@ -180,9 +188,11 @@ function SettingsContextController({
displayDensity === 'compact'
? COMPACT_LINE_HEIGHT
: COMFORTABLE_LINE_HEIGHT,
parseHookNames,
setAppendComponentStack,
setBreakOnConsoleErrors,
setDisplayDensity,
setParseHookNames,
setTheme,
setTraceUpdatesEnabled,
setShowInlineWarningsAndErrors,
@ -194,9 +204,11 @@ function SettingsContextController({
appendComponentStack,
breakOnConsoleErrors,
displayDensity,
parseHookNames,
setAppendComponentStack,
setBreakOnConsoleErrors,
setDisplayDensity,
setParseHookNames,
setTheme,
setTraceUpdatesEnabled,
setShowInlineWarningsAndErrors,

View File

@ -139,3 +139,7 @@
.ReleaseNotesLink {
color: var(--color-button-active);
}
.Warning {
color: var(--color-error-text);
}

View File

@ -0,0 +1,154 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {unstable_getCacheForType as getCacheForType} from 'react';
import {enableHookNameParsing} from 'react-devtools-feature-flags';
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
import type {Thenable, Wakeable} from 'shared/ReactTypes';
import type {Element} from './devtools/views/Components/types';
import type {HookNames} from 'react-devtools-shared/src/types';
const TIMEOUT = 3000;
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
type PendingRecord = {|
status: 0,
value: Wakeable,
|};
type ResolvedRecord<T> = {|
status: 1,
value: T,
|};
type RejectedRecord = {|
status: 2,
value: null,
|};
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
if (record.status === Resolved) {
// This is just a type refinement.
return record;
} else if (record.status === Rejected) {
// This is just a type refinement.
return record;
} else {
throw record.value;
}
}
type HookNamesMap = WeakMap<Element, Record<HookNames>>;
function createMap(): HookNamesMap {
return new WeakMap();
}
function getRecordMap(): WeakMap<Element, Record<HookNames>> {
return getCacheForType(createMap);
}
export function loadHookNames(
element: Element,
hooksTree: HooksTree,
loadHookNamesFunction: (hookLog: HooksTree) => Thenable<HookNames>,
): HookNames | null {
if (!enableHookNameParsing) {
return null;
}
const map = getRecordMap();
let record = map.get(element);
if (record) {
// TODO Do we need to update the Map to use new the hooks list objects as keys
// or will these be stable between inspections as a component updates?
// It seems like they're stable.
} else {
const callbacks = new Set();
const wakeable: Wakeable = {
then(callback) {
callbacks.add(callback);
},
};
const wake = () => {
if (timeoutID) {
clearTimeout(timeoutID);
timeoutID = null;
}
// This assumes they won't throw.
callbacks.forEach(callback => callback());
callbacks.clear();
};
const newRecord: Record<HookNames> = (record = {
status: Pending,
value: wakeable,
});
let didTimeout = false;
loadHookNamesFunction(hooksTree).then(
function onSuccess(hookNames) {
if (didTimeout) {
return;
}
if (hookNames) {
const resolvedRecord = ((newRecord: any): ResolvedRecord<HookNames>);
resolvedRecord.status = Resolved;
resolvedRecord.value = hookNames;
} else {
const notFoundRecord = ((newRecord: any): RejectedRecord);
notFoundRecord.status = Rejected;
notFoundRecord.value = null;
}
wake();
},
function onError(error) {
if (didTimeout) {
return;
}
const thrownRecord = ((newRecord: any): RejectedRecord);
thrownRecord.status = Rejected;
thrownRecord.value = null;
wake();
},
);
// Eventually timeout and stop trying to load names.
let timeoutID = setTimeout(() => {
timeoutID = null;
didTimeout = true;
const timedoutRecord = ((newRecord: any): RejectedRecord);
timedoutRecord.status = Rejected;
timedoutRecord.value = null;
wake();
}, TIMEOUT);
map.set(element, record);
}
const response = readRecord(record).value;
return response;
}

View File

@ -7,6 +7,8 @@
* @flow
*/
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
export type Wall = {|
// `listen` returns the "unlisten" function.
listen: (fn: Function) => Function,
@ -76,3 +78,12 @@ export type ComponentFilter =
| BooleanComponentFilter
| ElementTypeComponentFilter
| RegExpComponentFilter;
export type HookName = string | null;
export type HookNames = Map<HooksNode, HookName>;
export type LRUCache<K, V> = {|
get: (key: K) => V,
has: (key: K) => boolean,
set: (key: K, value: V) => void,
|};

View File

@ -47,13 +47,17 @@ import {
} from 'react-devtools-shared/src/types';
import {localStorageGetItem, localStorageSetItem} from './storage';
import {meta} from './hydration';
import type {ComponentFilter, ElementType} from './types';
import type {LRUCache} from 'react-devtools-shared/src/types';
const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
// On large trees, encoding takes significant time.
// Try to reuse the already encoded strings.
const encodedStringCache = new LRU({max: 1000});
const encodedStringCache: LRUCache<string, Array<number>> = new LRU({
max: 1000,
});
export function alphaSortKeys(
a: string | number | Symbol,

View File

@ -31,6 +31,11 @@ const config = {
app: './src/app/index.js',
devtools: './src/devtools.js',
},
node: {
// source-maps package has a dependency on 'fs'
// but this build won't trigger that code path
fs: 'empty',
},
resolve: {
alias: {
react: resolve(builtModulesDir, 'react'),

View File

@ -55,12 +55,20 @@ module.exports = Object.assign({}, baseConfig, {
// Don't run bundle tests on -test.internal.* files
testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'],
// Exclude the build output from transforms
transformIgnorePatterns: ['/node_modules/', '<rootDir>/build2/'],
testRegex: 'packages/react-devtools-shared/src/__tests__/[^]+.test.js$',
transformIgnorePatterns: [
'/node_modules/',
'<rootDir>/build2/',
'/__compiled__/',
],
testRegex:
'packages/react-devtools-(extensions|shared)/src/__tests__/[^]+.test.js$',
snapshotSerializers: [
require.resolve(
'../../packages/react-devtools-shared/src/__tests__/dehydratedValueSerializer.js'
),
require.resolve(
'../../packages/react-devtools-shared/src/__tests__/hookSerializer.js'
),
require.resolve(
'../../packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js'
),

View File

@ -52,6 +52,7 @@ module.exports = Object.assign({}, baseConfig, {
moduleNameMapper,
modulePathIgnorePatterns: [
...baseConfig.modulePathIgnorePatterns,
'packages/react-devtools-extensions',
'packages/react-devtools-shared',
],
// Don't run bundle tests on -test.internal.* files

View File

@ -5,6 +5,7 @@ const baseConfig = require('./config.base');
module.exports = Object.assign({}, baseConfig, {
modulePathIgnorePatterns: [
...baseConfig.modulePathIgnorePatterns,
'packages/react-devtools-extensions',
'packages/react-devtools-shared',
'ReactIncrementalPerf',
'ReactIncrementalUpdatesMinimalism',

View File

@ -5,6 +5,7 @@ const baseConfig = require('./config.base');
module.exports = Object.assign({}, baseConfig, {
modulePathIgnorePatterns: [
...baseConfig.modulePathIgnorePatterns,
'packages/react-devtools-extensions',
'packages/react-devtools-shared',
],
setupFiles: [

View File

@ -5,6 +5,7 @@ const baseConfig = require('./config.base');
module.exports = Object.assign({}, baseConfig, {
modulePathIgnorePatterns: [
...baseConfig.modulePathIgnorePatterns,
'packages/react-devtools-extensions',
'packages/react-devtools-shared',
],
setupFiles: [

View File

@ -25,7 +25,14 @@ let didWarn = false;
let didError = false;
const files = glob
.sync('**/*.js', {ignore: ['**/node_modules/**', '**/cjs/**']})
.sync('**/*.js', {
ignore: [
'**/node_modules/**',
'**/cjs/**',
'**/__compiled__/**',
'packages/react-devtools-extensions/src/ErrorTesterCompiled.js',
],
})
.filter(f => !onlyChanged || changedFiles.has(f));
if (!files.length) {

289
yarn.lock
View File

@ -124,6 +124,15 @@
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/generator@^7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de"
integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==
dependencies:
"@babel/types" "^7.12.5"
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/generator@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.3.tgz#0e22c005b0a94c1c74eafe19ef78ce53a4d45c03"
@ -481,7 +490,7 @@
dependencies:
"@babel/types" "^7.8.3"
"@babel/helper-validator-identifier@^7.14.0":
"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.14.0":
version "7.14.0"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288"
integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==
@ -547,6 +556,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.3.tgz#9b530eecb071fd0c93519df25c5ff9f14759f298"
integrity sha512-7MpZDIfI7sUC5zWo2+foJ50CSI5lcqDehZ0lVgIhSi4bFEk94fLAKlF3Q0nzSQQ+ca0lm+O6G9ztKVBeu8PMRQ==
"@babel/parser@^7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0"
integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ==
"@babel/parser@^7.8.6", "@babel/parser@^7.9.0":
version "7.9.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
@ -1507,6 +1521,21 @@
globals "^11.1.0"
lodash "^4.17.19"
"@babel/traverse@^7.12.5":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095"
integrity sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/generator" "^7.12.5"
"@babel/helper-function-name" "^7.10.4"
"@babel/helper-split-export-declaration" "^7.11.0"
"@babel/parser" "^7.12.5"
"@babel/types" "^7.12.5"
debug "^4.1.0"
globals "^11.1.0"
lodash "^4.17.19"
"@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0":
version "7.9.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2"
@ -1539,6 +1568,15 @@
"@babel/helper-validator-identifier" "^7.14.0"
to-fast-properties "^2.0.0"
"@babel/types@^7.12.5":
version "7.12.6"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96"
integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA==
dependencies:
"@babel/helper-validator-identifier" "^7.10.4"
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5":
version "7.9.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444"
@ -3353,6 +3391,41 @@ babel-eslint@^9.0.0:
eslint-scope "3.7.1"
eslint-visitor-keys "^1.0.0"
babel-helper-evaluate-path@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/babel-helper-evaluate-path/-/babel-helper-evaluate-path-0.5.0.tgz#a62fa9c4e64ff7ea5cea9353174ef023a900a67c"
integrity sha512-mUh0UhS607bGh5wUMAQfOpt2JX2ThXMtppHRdRU1kL7ZLRWIXxoV2UIV1r2cAeeNeU1M5SB5/RSUgUxrK8yOkA==
babel-helper-flip-expressions@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-helper-flip-expressions/-/babel-helper-flip-expressions-0.4.3.tgz#3696736a128ac18bc25254b5f40a22ceb3c1d3fd"
integrity sha1-NpZzahKKwYvCUlS19AoizrPB0/0=
babel-helper-is-nodes-equiv@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/babel-helper-is-nodes-equiv/-/babel-helper-is-nodes-equiv-0.0.1.tgz#34e9b300b1479ddd98ec77ea0bbe9342dfe39684"
integrity sha1-NOmzALFHnd2Y7HfqC76TQt/jloQ=
babel-helper-is-void-0@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-helper-is-void-0/-/babel-helper-is-void-0-0.4.3.tgz#7d9c01b4561e7b95dbda0f6eee48f5b60e67313e"
integrity sha1-fZwBtFYee5Xb2g9u7kj1tg5nMT4=
babel-helper-mark-eval-scopes@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-helper-mark-eval-scopes/-/babel-helper-mark-eval-scopes-0.4.3.tgz#d244a3bef9844872603ffb46e22ce8acdf551562"
integrity sha1-0kSjvvmESHJgP/tG4izorN9VFWI=
babel-helper-remove-or-void@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-helper-remove-or-void/-/babel-helper-remove-or-void-0.4.3.tgz#a4f03b40077a0ffe88e45d07010dee241ff5ae60"
integrity sha1-pPA7QAd6D/6I5F0HAQ3uJB/1rmA=
babel-helper-to-multiple-sequence-expressions@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz#a3f924e3561882d42fcf48907aa98f7979a4588d"
integrity sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA==
babel-jest@^26.6.3:
version "26.6.3"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056"
@ -3423,6 +3496,82 @@ babel-plugin-jest-hoist@^26.6.2:
"@types/babel__core" "^7.0.0"
"@types/babel__traverse" "^7.0.6"
babel-plugin-minify-builtins@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-builtins/-/babel-plugin-minify-builtins-0.5.0.tgz#31eb82ed1a0d0efdc31312f93b6e4741ce82c36b"
integrity sha512-wpqbN7Ov5hsNwGdzuzvFcjgRlzbIeVv1gMIlICbPj0xkexnfoIDe7q+AZHMkQmAE/F9R5jkrB6TLfTegImlXag==
babel-plugin-minify-constant-folding@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-constant-folding/-/babel-plugin-minify-constant-folding-0.5.0.tgz#f84bc8dbf6a561e5e350ff95ae216b0ad5515b6e"
integrity sha512-Vj97CTn/lE9hR1D+jKUeHfNy+m1baNiJ1wJvoGyOBUx7F7kJqDZxr9nCHjO/Ad+irbR3HzR6jABpSSA29QsrXQ==
dependencies:
babel-helper-evaluate-path "^0.5.0"
babel-plugin-minify-dead-code-elimination@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-dead-code-elimination/-/babel-plugin-minify-dead-code-elimination-0.5.1.tgz#1a0c68e44be30de4976ca69ffc535e08be13683f"
integrity sha512-x8OJOZIrRmQBcSqxBcLbMIK8uPmTvNWPXH2bh5MDCW1latEqYiRMuUkPImKcfpo59pTUB2FT7HfcgtG8ZlR5Qg==
dependencies:
babel-helper-evaluate-path "^0.5.0"
babel-helper-mark-eval-scopes "^0.4.3"
babel-helper-remove-or-void "^0.4.3"
lodash "^4.17.11"
babel-plugin-minify-flip-comparisons@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-flip-comparisons/-/babel-plugin-minify-flip-comparisons-0.4.3.tgz#00ca870cb8f13b45c038b3c1ebc0f227293c965a"
integrity sha1-AMqHDLjxO0XAOLPB68DyJyk8llo=
dependencies:
babel-helper-is-void-0 "^0.4.3"
babel-plugin-minify-guarded-expressions@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-guarded-expressions/-/babel-plugin-minify-guarded-expressions-0.4.4.tgz#818960f64cc08aee9d6c75bec6da974c4d621135"
integrity sha512-RMv0tM72YuPPfLT9QLr3ix9nwUIq+sHT6z8Iu3sLbqldzC1Dls8DPCywzUIzkTx9Zh1hWX4q/m9BPoPed9GOfA==
dependencies:
babel-helper-evaluate-path "^0.5.0"
babel-helper-flip-expressions "^0.4.3"
babel-plugin-minify-infinity@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-infinity/-/babel-plugin-minify-infinity-0.4.3.tgz#dfb876a1b08a06576384ef3f92e653ba607b39ca"
integrity sha1-37h2obCKBldjhO8/kuZTumB7Oco=
babel-plugin-minify-mangle-names@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-mangle-names/-/babel-plugin-minify-mangle-names-0.5.0.tgz#bcddb507c91d2c99e138bd6b17a19c3c271e3fd3"
integrity sha512-3jdNv6hCAw6fsX1p2wBGPfWuK69sfOjfd3zjUXkbq8McbohWy23tpXfy5RnToYWggvqzuMOwlId1PhyHOfgnGw==
dependencies:
babel-helper-mark-eval-scopes "^0.4.3"
babel-plugin-minify-numeric-literals@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-numeric-literals/-/babel-plugin-minify-numeric-literals-0.4.3.tgz#8e4fd561c79f7801286ff60e8c5fd9deee93c0bc"
integrity sha1-jk/VYcefeAEob/YOjF/Z3u6TwLw=
babel-plugin-minify-replace@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-replace/-/babel-plugin-minify-replace-0.5.0.tgz#d3e2c9946c9096c070efc96761ce288ec5c3f71c"
integrity sha512-aXZiaqWDNUbyNNNpWs/8NyST+oU7QTpK7J9zFEFSA0eOmtUNMU3fczlTTTlnCxHmq/jYNFEmkkSG3DDBtW3Y4Q==
babel-plugin-minify-simplify@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-simplify/-/babel-plugin-minify-simplify-0.5.1.tgz#f21613c8b95af3450a2ca71502fdbd91793c8d6a"
integrity sha512-OSYDSnoCxP2cYDMk9gxNAed6uJDiDz65zgL6h8d3tm8qXIagWGMLWhqysT6DY3Vs7Fgq7YUDcjOomhVUb+xX6A==
dependencies:
babel-helper-evaluate-path "^0.5.0"
babel-helper-flip-expressions "^0.4.3"
babel-helper-is-nodes-equiv "^0.0.1"
babel-helper-to-multiple-sequence-expressions "^0.5.0"
babel-plugin-minify-type-constructors@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-plugin-minify-type-constructors/-/babel-plugin-minify-type-constructors-0.4.3.tgz#1bc6f15b87f7ab1085d42b330b717657a2156500"
integrity sha1-G8bxW4f3qxCF1CszC3F2V6IVZQA=
dependencies:
babel-helper-is-void-0 "^0.4.3"
babel-plugin-syntax-trailing-function-commas@^6.5.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
@ -3433,6 +3582,65 @@ babel-plugin-syntax-trailing-function-commas@^7.0.0-beta.0:
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz#aa213c1435e2bffeb6fca842287ef534ad05d5cf"
integrity sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==
babel-plugin-transform-inline-consecutive-adds@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz#323d47a3ea63a83a7ac3c811ae8e6941faf2b0d1"
integrity sha1-Mj1Ho+pjqDp6w8gRro5pQfrysNE=
babel-plugin-transform-member-expression-literals@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-member-expression-literals/-/babel-plugin-transform-member-expression-literals-6.9.4.tgz#37039c9a0c3313a39495faac2ff3a6b5b9d038bf"
integrity sha1-NwOcmgwzE6OUlfqsL/OmtbnQOL8=
babel-plugin-transform-merge-sibling-variables@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-merge-sibling-variables/-/babel-plugin-transform-merge-sibling-variables-6.9.4.tgz#85b422fc3377b449c9d1cde44087203532401dae"
integrity sha1-hbQi/DN3tEnJ0c3kQIcgNTJAHa4=
babel-plugin-transform-minify-booleans@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-minify-booleans/-/babel-plugin-transform-minify-booleans-6.9.4.tgz#acbb3e56a3555dd23928e4b582d285162dd2b198"
integrity sha1-rLs+VqNVXdI5KOS1gtKFFi3SsZg=
babel-plugin-transform-property-literals@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-property-literals/-/babel-plugin-transform-property-literals-6.9.4.tgz#98c1d21e255736573f93ece54459f6ce24985d39"
integrity sha1-mMHSHiVXNlc/k+zlRFn2ziSYXTk=
dependencies:
esutils "^2.0.2"
babel-plugin-transform-regexp-constructors@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-regexp-constructors/-/babel-plugin-transform-regexp-constructors-0.4.3.tgz#58b7775b63afcf33328fae9a5f88fbd4fb0b4965"
integrity sha1-WLd3W2OvzzMyj66aX4j71PsLSWU=
babel-plugin-transform-remove-console@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz#b980360c067384e24b357a588d807d3c83527780"
integrity sha1-uYA2DAZzhOJLNXpYjYB9PINSd4A=
babel-plugin-transform-remove-debugger@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-debugger/-/babel-plugin-transform-remove-debugger-6.9.4.tgz#42b727631c97978e1eb2d199a7aec84a18339ef2"
integrity sha1-QrcnYxyXl44estGZp67IShgznvI=
babel-plugin-transform-remove-undefined@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-remove-undefined/-/babel-plugin-transform-remove-undefined-0.5.0.tgz#80208b31225766c630c97fa2d288952056ea22dd"
integrity sha512-+M7fJYFaEE/M9CXa0/IRkDbiV3wRELzA1kKQFCJ4ifhrzLKn/9VCCgj9OFmYWwBd8IB48YdgPkHYtbYq+4vtHQ==
dependencies:
babel-helper-evaluate-path "^0.5.0"
babel-plugin-transform-simplify-comparison-operators@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-simplify-comparison-operators/-/babel-plugin-transform-simplify-comparison-operators-6.9.4.tgz#f62afe096cab0e1f68a2d753fdf283888471ceb9"
integrity sha1-9ir+CWyrDh9ootdT/fKDiIRxzrk=
babel-plugin-transform-undefined-to-void@^6.9.4:
version "6.9.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz#be241ca81404030678b748717322b89d0c8fe280"
integrity sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA=
babel-preset-current-node-syntax@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b"
@ -3492,6 +3700,35 @@ babel-preset-jest@^26.6.2:
babel-plugin-jest-hoist "^26.6.2"
babel-preset-current-node-syntax "^1.0.0"
babel-preset-minify@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/babel-preset-minify/-/babel-preset-minify-0.5.1.tgz#25f5d0bce36ec818be80338d0e594106e21eaa9f"
integrity sha512-1IajDumYOAPYImkHbrKeiN5AKKP9iOmRoO2IPbIuVp0j2iuCcj0n7P260z38siKMZZ+85d3mJZdtW8IgOv+Tzg==
dependencies:
babel-plugin-minify-builtins "^0.5.0"
babel-plugin-minify-constant-folding "^0.5.0"
babel-plugin-minify-dead-code-elimination "^0.5.1"
babel-plugin-minify-flip-comparisons "^0.4.3"
babel-plugin-minify-guarded-expressions "^0.4.4"
babel-plugin-minify-infinity "^0.4.3"
babel-plugin-minify-mangle-names "^0.5.0"
babel-plugin-minify-numeric-literals "^0.4.3"
babel-plugin-minify-replace "^0.5.0"
babel-plugin-minify-simplify "^0.5.1"
babel-plugin-minify-type-constructors "^0.4.3"
babel-plugin-transform-inline-consecutive-adds "^0.4.3"
babel-plugin-transform-member-expression-literals "^6.9.4"
babel-plugin-transform-merge-sibling-variables "^6.9.4"
babel-plugin-transform-minify-booleans "^6.9.4"
babel-plugin-transform-property-literals "^6.9.4"
babel-plugin-transform-regexp-constructors "^0.4.3"
babel-plugin-transform-remove-console "^6.9.4"
babel-plugin-transform-remove-debugger "^6.9.4"
babel-plugin-transform-remove-undefined "^0.5.0"
babel-plugin-transform-simplify-comparison-operators "^6.9.4"
babel-plugin-transform-undefined-to-void "^6.9.4"
lodash "^4.17.11"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -4754,6 +4991,13 @@ cross-env@^3.1.4:
cross-spawn "^5.1.0"
is-windows "^1.0.0"
cross-fetch@^3.0.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.4.tgz#9723f3a3a247bf8b89039f3a380a9244e8fa2f39"
integrity sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==
dependencies:
node-fetch "2.6.1"
cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -6780,6 +7024,15 @@ fs-exists-sync@^0.1.0:
resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=
fs-extra@^4.0.2, fs-extra@~4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
@ -6789,15 +7042,6 @@ fs-extra@^8.1.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@~4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
dependencies:
graceful-fs "^4.1.2"
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@ -8512,6 +8756,14 @@ jest-environment-node@^26.6.2:
jest-mock "^26.6.2"
jest-util "^26.6.2"
jest-fetch-mock@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
dependencies:
cross-fetch "^3.0.4"
promise-polyfill "^8.1.3"
jest-get-type@^26.3.0:
version "26.3.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
@ -10041,6 +10293,11 @@ node-cleanup@^2.1.2:
resolved "https://registry.yarnpkg.com/node-cleanup/-/node-cleanup-2.1.2.tgz#7ac19abd297e09a7f72a71545d951b517e4dde2c"
integrity sha1-esGavSl+Caf3KnFUXZUbUX5N3iw=
node-fetch@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@ -11267,6 +11524,11 @@ promise-polyfill@^6.0.1:
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057"
integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=
promise-polyfill@^8.1.3:
version "8.2.0"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0"
integrity sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==
promise-retry@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d"
@ -12778,6 +13040,13 @@ source-map@^0.7.3:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
source-map@^0.8.0-beta.0:
version "0.8.0-beta.0"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11"
integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==
dependencies:
whatwg-url "^7.0.0"
sourcemap-codec@^1.4.1, sourcemap-codec@^1.4.4:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"