diff --git a/.eslintignore b/.eslintignore index 82f28b72dd..2593400242 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/.prettierignore b/.prettierignore index 80650567ef..d944a5e628 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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 diff --git a/package.json b/package.json index 0f650fb1b2..1bf9be46f8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js index e33e4b89c6..9b09448c55 100644 --- a/packages/react-devtools-core/webpack.backend.js +++ b/packages/react-devtools-core/webpack.backend.js @@ -47,6 +47,9 @@ module.exports = { scheduler: resolve(builtModulesDir, 'scheduler'), }, }, + node: { + fs: 'empty', + }, plugins: [ new DefinePlugin({ __DEV__, diff --git a/packages/react-devtools-extensions/build.js b/packages/react-devtools-extensions/build.js index 76b7aee68c..afd4b6fd23 100644 --- a/packages/react-devtools-extensions/build.js +++ b/packages/react-devtools-extensions/build.js @@ -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()); diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 906d50465c..1f689e6c3a 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -32,7 +32,8 @@ "main.html", "panel.html", "build/react_devtools_backend.js", - "build/renderer.js" + "build/renderer.js", + "mappings.wasm" ], "background": { diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index ac13c85424..04d92e4719 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -32,7 +32,8 @@ "main.html", "panel.html", "build/react_devtools_backend.js", - "build/renderer.js" + "build/renderer.js", + "mappings.wasm" ], "background": { diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 480ea1b4c6..2ba0d20445 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -37,7 +37,8 @@ "main.html", "panel.html", "build/react_devtools_backend.js", - "build/renderer.js" + "build/renderer.js", + "mappings.wasm" ], "background": { diff --git a/packages/react-devtools-extensions/package.json b/packages/react-devtools-extensions/package.json index 2771c31c5a..1949edbd02 100644 --- a/packages/react-devtools-extensions/package.json +++ b/packages/react-devtools-extensions/package.json @@ -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", diff --git a/packages/react-devtools-extensions/src/ErrorTester.js b/packages/react-devtools-extensions/src/ErrorTester.js new file mode 100644 index 0000000000..dfdb2a6082 --- /dev/null +++ b/packages/react-devtools-extensions/src/ErrorTester.js @@ -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; +} diff --git a/packages/react-devtools-extensions/src/ErrorTesterCompiled.js b/packages/react-devtools-extensions/src/ErrorTesterCompiled.js new file mode 100644 index 0000000000..9bdbd8582d --- /dev/null +++ b/packages/react-devtools-extensions/src/ErrorTesterCompiled.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/README.md b/packages/react-devtools-extensions/src/__tests__/README.md new file mode 100644 index 0000000000..3b95b09f74 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/README.md @@ -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. \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/ComponentWithCustomHook.js b/packages/react-devtools-extensions/src/__tests__/__source__/ComponentWithCustomHook.js new file mode 100644 index 0000000000..311876ffc5 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/ComponentWithCustomHook.js @@ -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 ( + <> +
Dark mode? {isDarkMode}
+
Count: {count}
+ + + ); +} + +function useIsDarkMode() { + const [isDarkMode] = useState(false); + + useEffect(function useEffectCreate() { + // Here is where we may listen to a "theme" event... + }, []); + + return isDarkMode; +} diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/ComponentWithExternalCustomHooks.js b/packages/react-devtools-extensions/src/__tests__/__source__/ComponentWithExternalCustomHooks.js new file mode 100644 index 0000000000..f7c52ffc9d --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/ComponentWithExternalCustomHooks.js @@ -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
theme: {theme}
; +} diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/Example.js b/packages/react-devtools-extensions/src/__tests__/__source__/Example.js new file mode 100644 index 0000000000..be2049d89c --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/Example.js @@ -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 ( +
+

You clicked {count} times

+ +
+ ); +} diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/InlineRequire.js b/packages/react-devtools-extensions/src/__tests__/__source__/InlineRequire.js new file mode 100644 index 0000000000..617200b1ed --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/InlineRequire.js @@ -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; +} diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/ToDoList.js b/packages/react-devtools-extensions/src/__tests__/__source__/ToDoList.js new file mode 100644 index 0000000000..733aba85fc --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/ToDoList.js @@ -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 ( +
  • + + +
  • + ); +} + +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 ( + +

    List

    + + + +
    + ); +} diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithCustomHook.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithCustomHook.js new file mode 100644 index 0000000000..f76a1acedb --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithCustomHook.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithCustomHook.js.map b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithCustomHook.js.map new file mode 100644 index 0000000000..ac54e5738f --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithCustomHook.js.map @@ -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
    Dark mode? {isDarkMode}
    \n
    Count: {count}
    \n \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}"]} \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithExternalCustomHooks.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithExternalCustomHooks.js new file mode 100644 index 0000000000..dbdc0b3ade --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithExternalCustomHooks.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithExternalCustomHooks.js.map b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithExternalCustomHooks.js.map new file mode 100644 index 0000000000..a4afacf044 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ComponentWithExternalCustomHooks.js.map @@ -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
    theme: {theme}
    ;\n}\n"]} \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/Example.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/Example.js new file mode 100644 index 0000000000..b51d2b2c84 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/Example.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/Example.js.map b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/Example.js.map new file mode 100644 index 0000000000..a3616e735c --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/Example.js.map @@ -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
    \n

    You clicked {count} times

    \n \n
    \n );\n}\n"]} \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/InlineRequire.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/InlineRequire.js new file mode 100644 index 0000000000..4d025dab58 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/InlineRequire.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/InlineRequire.js.map b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/InlineRequire.js.map new file mode 100644 index 0000000000..0777b46927 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/InlineRequire.js.map @@ -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"]} \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ToDoList.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ToDoList.js new file mode 100644 index 0000000000..d9cca2b089 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ToDoList.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ToDoList.js.map b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ToDoList.js.map new file mode 100644 index 0000000000..2408485245 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/ToDoList.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["ToDoList.js"],"names":["ListItem","item","removeItem","toggleItem","handleDelete","handleToggle","isComplete","text","List","props","newItemText","setNewItemText","items","setItems","id","uid","setUID","handleClick","handleKeyPress","event","key","handleChange","currentTarget","value","itemToRemove","filter","itemToToggle","index","findIndex","slice","concat","map"],"mappings":";;;;;;;;AASA;;;;;;AATA;;;;;;;;AAYO,SAASA,QAAT,CAAkB;AAACC,EAAAA,IAAD;AAAOC,EAAAA,UAAP;AAAmBC,EAAAA;AAAnB,CAAlB,EAAkD;AACvD,QAAMC,YAAY,GAAG,uBAAY,MAAM;AACrCF,IAAAA,UAAU,CAACD,IAAD,CAAV;AACD,GAFoB,EAElB,CAACA,IAAD,EAAOC,UAAP,CAFkB,CAArB;AAIA,QAAMG,YAAY,GAAG,uBAAY,MAAM;AACrCF,IAAAA,UAAU,CAACF,IAAD,CAAV;AACD,GAFoB,EAElB,CAACA,IAAD,EAAOE,UAAP,CAFkB,CAArB;AAIA,sBACE,6CACE;AAAQ,IAAA,OAAO,EAAEC;AAAjB,cADF,eAEE,gDACE;AACE,IAAA,OAAO,EAAEH,IAAI,CAACK,UADhB;AAEE,IAAA,QAAQ,EAAED,YAFZ;AAGE,IAAA,IAAI,EAAC;AAHP,IADF,EAKK,GALL,EAMGJ,IAAI,CAACM,IANR,CAFF,CADF;AAaD;;AAEM,SAASC,IAAT,CAAcC,KAAd,EAAqB;AAC1B,QAAM,CAACC,WAAD,EAAcC,cAAd,IAAgC,oBAAS,EAAT,CAAtC;AACA,QAAM,CAACC,KAAD,EAAQC,QAAR,IAAoB,oBAAS,CACjC;AAACC,IAAAA,EAAE,EAAE,CAAL;AAAQR,IAAAA,UAAU,EAAE,IAApB;AAA0BC,IAAAA,IAAI,EAAE;AAAhC,GADiC,EAEjC;AAACO,IAAAA,EAAE,EAAE,CAAL;AAAQR,IAAAA,UAAU,EAAE,IAApB;AAA0BC,IAAAA,IAAI,EAAE;AAAhC,GAFiC,EAGjC;AAACO,IAAAA,EAAE,EAAE,CAAL;AAAQR,IAAAA,UAAU,EAAE,KAApB;AAA2BC,IAAAA,IAAI,EAAE;AAAjC,GAHiC,CAAT,CAA1B;AAKA,QAAM,CAACQ,GAAD,EAAMC,MAAN,IAAgB,oBAAS,CAAT,CAAtB;AAEA,QAAMC,WAAW,GAAG,uBAAY,MAAM;AACpC,QAAIP,WAAW,KAAK,EAApB,EAAwB;AACtBG,MAAAA,QAAQ,CAAC,CACP,GAAGD,KADI,EAEP;AACEE,QAAAA,EAAE,EAAEC,GADN;AAEET,QAAAA,UAAU,EAAE,KAFd;AAGEC,QAAAA,IAAI,EAAEG;AAHR,OAFO,CAAD,CAAR;AAQAM,MAAAA,MAAM,CAACD,GAAG,GAAG,CAAP,CAAN;AACAJ,MAAAA,cAAc,CAAC,EAAD,CAAd;AACD;AACF,GAbmB,EAajB,CAACD,WAAD,EAAcE,KAAd,EAAqBG,GAArB,CAbiB,CAApB;AAeA,QAAMG,cAAc,GAAG,uBACrBC,KAAK,IAAI;AACP,QAAIA,KAAK,CAACC,GAAN,KAAc,OAAlB,EAA2B;AACzBH,MAAAA,WAAW;AACZ;AACF,GALoB,EAMrB,CAACA,WAAD,CANqB,CAAvB;AASA,QAAMI,YAAY,GAAG,uBACnBF,KAAK,IAAI;AACPR,IAAAA,cAAc,CAACQ,KAAK,CAACG,aAAN,CAAoBC,KAArB,CAAd;AACD,GAHkB,EAInB,CAACZ,cAAD,CAJmB,CAArB;AAOA,QAAMT,UAAU,GAAG,uBACjBsB,YAAY,IAAIX,QAAQ,CAACD,KAAK,CAACa,MAAN,CAAaxB,IAAI,IAAIA,IAAI,KAAKuB,YAA9B,CAAD,CADP,EAEjB,CAACZ,KAAD,CAFiB,CAAnB;AAKA,QAAMT,UAAU,GAAG,uBACjBuB,YAAY,IAAI;AACd;AACA;AACA,UAAMC,KAAK,GAAGf,KAAK,CAACgB,SAAN,CAAgB3B,IAAI,IAAIA,IAAI,CAACa,EAAL,KAAYY,YAAY,CAACZ,EAAjD,CAAd;AAEAD,IAAAA,QAAQ,CACND,KAAK,CACFiB,KADH,CACS,CADT,EACYF,KADZ,EAEGG,MAFH,CAEU,EACN,GAAGJ,YADG;AAENpB,MAAAA,UAAU,EAAE,CAACoB,YAAY,CAACpB;AAFpB,KAFV,EAMGwB,MANH,CAMUlB,KAAK,CAACiB,KAAN,CAAYF,KAAK,GAAG,CAApB,CANV,CADM,CAAR;AASD,GAfgB,EAgBjB,CAACf,KAAD,CAhBiB,CAAnB;AAmBA,sBACE,oBAAC,cAAD,qBACE,uCADF,eAEE;AACE,IAAA,IAAI,EAAC,MADP;AAEE,IAAA,WAAW,EAAC,kBAFd;AAGE,IAAA,KAAK,EAAEF,WAHT;AAIE,IAAA,QAAQ,EAAEW,YAJZ;AAKE,IAAA,UAAU,EAAEH;AALd,IAFF,eASE;AAAQ,IAAA,QAAQ,EAAER,WAAW,KAAK,EAAlC;AAAsC,IAAA,OAAO,EAAEO;AAA/C,kBACE;AAAM,IAAA,IAAI,EAAC,KAAX;AAAiB,kBAAW;AAA5B,WADF,CATF,eAcE,gCACGL,KAAK,CAACmB,GAAN,CAAU9B,IAAI,iBACb,oBAAC,QAAD;AACE,IAAA,GAAG,EAAEA,IAAI,CAACa,EADZ;AAEE,IAAA,IAAI,EAAEb,IAFR;AAGE,IAAA,UAAU,EAAEC,UAHd;AAIE,IAAA,UAAU,EAAEC;AAJd,IADD,CADH,CAdF,CADF;AA2BD","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 * as React from 'react';\nimport {Fragment, useCallback, useState} from 'react';\n\nexport function ListItem({item, removeItem, toggleItem}) {\n const handleDelete = useCallback(() => {\n removeItem(item);\n }, [item, removeItem]);\n\n const handleToggle = useCallback(() => {\n toggleItem(item);\n }, [item, toggleItem]);\n\n return (\n
  • \n \n \n
  • \n );\n}\n\nexport function List(props) {\n const [newItemText, setNewItemText] = useState('');\n const [items, setItems] = useState([\n {id: 1, isComplete: true, text: 'First'},\n {id: 2, isComplete: true, text: 'Second'},\n {id: 3, isComplete: false, text: 'Third'},\n ]);\n const [uid, setUID] = useState(4);\n\n const handleClick = useCallback(() => {\n if (newItemText !== '') {\n setItems([\n ...items,\n {\n id: uid,\n isComplete: false,\n text: newItemText,\n },\n ]);\n setUID(uid + 1);\n setNewItemText('');\n }\n }, [newItemText, items, uid]);\n\n const handleKeyPress = useCallback(\n event => {\n if (event.key === 'Enter') {\n handleClick();\n }\n },\n [handleClick],\n );\n\n const handleChange = useCallback(\n event => {\n setNewItemText(event.currentTarget.value);\n },\n [setNewItemText],\n );\n\n const removeItem = useCallback(\n itemToRemove => setItems(items.filter(item => item !== itemToRemove)),\n [items],\n );\n\n const toggleItem = useCallback(\n itemToToggle => {\n // Dont use indexOf()\n // because editing props in DevTools creates a new Object.\n const index = items.findIndex(item => item.id === itemToToggle.id);\n\n setItems(\n items\n .slice(0, index)\n .concat({\n ...itemToToggle,\n isComplete: !itemToToggle.isComplete,\n })\n .concat(items.slice(index + 1)),\n );\n },\n [items],\n );\n\n return (\n \n

    List

    \n \n \n \n
    \n );\n}\n"]} \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/useTheme.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/useTheme.js new file mode 100644 index 0000000000..b32aa12330 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/useTheme.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/useTheme.js.map b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/useTheme.js.map new file mode 100644 index 0000000000..95da9f592f --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/useTheme.js.map @@ -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"]} \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/ComponentWithCustomHook.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/ComponentWithCustomHook.js new file mode 100644 index 0000000000..85ac8a37c8 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/ComponentWithCustomHook.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/ComponentWithExternalCustomHooks.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/ComponentWithExternalCustomHooks.js new file mode 100644 index 0000000000..b9812467b7 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/ComponentWithExternalCustomHooks.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/Example.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/Example.js new file mode 100644 index 0000000000..ea6b7e4b98 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/Example.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/InlineRequire.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/InlineRequire.js new file mode 100644 index 0000000000..3492fe2732 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/InlineRequire.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/ToDoList.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/ToDoList.js new file mode 100644 index 0000000000..b196f598b6 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/ToDoList.js @@ -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=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIlRvRG9MaXN0LmpzIl0sIm5hbWVzIjpbIkxpc3RJdGVtIiwiaXRlbSIsInJlbW92ZUl0ZW0iLCJ0b2dnbGVJdGVtIiwiaGFuZGxlRGVsZXRlIiwiaGFuZGxlVG9nZ2xlIiwiaXNDb21wbGV0ZSIsInRleHQiLCJMaXN0IiwicHJvcHMiLCJuZXdJdGVtVGV4dCIsInNldE5ld0l0ZW1UZXh0IiwiaXRlbXMiLCJzZXRJdGVtcyIsImlkIiwidWlkIiwic2V0VUlEIiwiaGFuZGxlQ2xpY2siLCJoYW5kbGVLZXlQcmVzcyIsImV2ZW50Iiwia2V5IiwiaGFuZGxlQ2hhbmdlIiwiY3VycmVudFRhcmdldCIsInZhbHVlIiwiaXRlbVRvUmVtb3ZlIiwiZmlsdGVyIiwiaXRlbVRvVG9nZ2xlIiwiaW5kZXgiLCJmaW5kSW5kZXgiLCJzbGljZSIsImNvbmNhdCIsIm1hcCJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7QUFTQTs7Ozs7O0FBVEE7Ozs7Ozs7O0FBWU8sU0FBU0EsUUFBVCxDQUFrQjtBQUFDQyxFQUFBQSxJQUFEO0FBQU9DLEVBQUFBLFVBQVA7QUFBbUJDLEVBQUFBO0FBQW5CLENBQWxCLEVBQWtEO0FBQ3ZELFFBQU1DLFlBQVksR0FBRyx1QkFBWSxNQUFNO0FBQ3JDRixJQUFBQSxVQUFVLENBQUNELElBQUQsQ0FBVjtBQUNELEdBRm9CLEVBRWxCLENBQUNBLElBQUQsRUFBT0MsVUFBUCxDQUZrQixDQUFyQjtBQUlBLFFBQU1HLFlBQVksR0FBRyx1QkFBWSxNQUFNO0FBQ3JDRixJQUFBQSxVQUFVLENBQUNGLElBQUQsQ0FBVjtBQUNELEdBRm9CLEVBRWxCLENBQUNBLElBQUQsRUFBT0UsVUFBUCxDQUZrQixDQUFyQjtBQUlBLHNCQUNFLDZDQUNFO0FBQVEsSUFBQSxPQUFPLEVBQUVDO0FBQWpCLGNBREYsZUFFRSxnREFDRTtBQUNFLElBQUEsT0FBTyxFQUFFSCxJQUFJLENBQUNLLFVBRGhCO0FBRUUsSUFBQSxRQUFRLEVBQUVELFlBRlo7QUFHRSxJQUFBLElBQUksRUFBQztBQUhQLElBREYsRUFLSyxHQUxMLEVBTUdKLElBQUksQ0FBQ00sSUFOUixDQUZGLENBREY7QUFhRDs7QUFFTSxTQUFTQyxJQUFULENBQWNDLEtBQWQsRUFBcUI7QUFDMUIsUUFBTSxDQUFDQyxXQUFELEVBQWNDLGNBQWQsSUFBZ0Msb0JBQVMsRUFBVCxDQUF0QztBQUNBLFFBQU0sQ0FBQ0MsS0FBRCxFQUFRQyxRQUFSLElBQW9CLG9CQUFTLENBQ2pDO0FBQUNDLElBQUFBLEVBQUUsRUFBRSxDQUFMO0FBQVFSLElBQUFBLFVBQVUsRUFBRSxJQUFwQjtBQUEwQkMsSUFBQUEsSUFBSSxFQUFFO0FBQWhDLEdBRGlDLEVBRWpDO0FBQUNPLElBQUFBLEVBQUUsRUFBRSxDQUFMO0FBQVFSLElBQUFBLFVBQVUsRUFBRSxJQUFwQjtBQUEwQkMsSUFBQUEsSUFBSSxFQUFFO0FBQWhDLEdBRmlDLEVBR2pDO0FBQUNPLElBQUFBLEVBQUUsRUFBRSxDQUFMO0FBQVFSLElBQUFBLFVBQVUsRUFBRSxLQUFwQjtBQUEyQkMsSUFBQUEsSUFBSSxFQUFFO0FBQWpDLEdBSGlDLENBQVQsQ0FBMUI7QUFLQSxRQUFNLENBQUNRLEdBQUQsRUFBTUMsTUFBTixJQUFnQixvQkFBUyxDQUFULENBQXRCO0FBRUEsUUFBTUMsV0FBVyxHQUFHLHVCQUFZLE1BQU07QUFDcEMsUUFBSVAsV0FBVyxLQUFLLEVBQXBCLEVBQXdCO0FBQ3RCRyxNQUFBQSxRQUFRLENBQUMsQ0FDUCxHQUFHRCxLQURJLEVBRVA7QUFDRUUsUUFBQUEsRUFBRSxFQUFFQyxHQUROO0FBRUVULFFBQUFBLFVBQVUsRUFBRSxLQUZkO0FBR0VDLFFBQUFBLElBQUksRUFBRUc7QUFIUixPQUZPLENBQUQsQ0FBUjtBQVFBTSxNQUFBQSxNQUFNLENBQUNELEdBQUcsR0FBRyxDQUFQLENBQU47QUFDQUosTUFBQUEsY0FBYyxDQUFDLEVBQUQsQ0FBZDtBQUNEO0FBQ0YsR0FibUIsRUFhakIsQ0FBQ0QsV0FBRCxFQUFjRSxLQUFkLEVBQXFCRyxHQUFyQixDQWJpQixDQUFwQjtBQWVBLFFBQU1HLGNBQWMsR0FBRyx1QkFDckJDLEtBQUssSUFBSTtBQUNQLFFBQUlBLEtBQUssQ0FBQ0MsR0FBTixLQUFjLE9BQWxCLEVBQTJCO0FBQ3pCSCxNQUFBQSxXQUFXO0FBQ1o7QUFDRixHQUxvQixFQU1yQixDQUFDQSxXQUFELENBTnFCLENBQXZCO0FBU0EsUUFBTUksWUFBWSxHQUFHLHVCQUNuQkYsS0FBSyxJQUFJO0FBQ1BSLElBQUFBLGNBQWMsQ0FBQ1EsS0FBSyxDQUFDRyxhQUFOLENBQW9CQyxLQUFyQixDQUFkO0FBQ0QsR0FIa0IsRUFJbkIsQ0FBQ1osY0FBRCxDQUptQixDQUFyQjtBQU9BLFFBQU1ULFVBQVUsR0FBRyx1QkFDakJzQixZQUFZLElBQUlYLFFBQVEsQ0FBQ0QsS0FBSyxDQUFDYSxNQUFOLENBQWF4QixJQUFJLElBQUlBLElBQUksS0FBS3VCLFlBQTlCLENBQUQsQ0FEUCxFQUVqQixDQUFDWixLQUFELENBRmlCLENBQW5CO0FBS0EsUUFBTVQsVUFBVSxHQUFHLHVCQUNqQnVCLFlBQVksSUFBSTtBQUNkO0FBQ0E7QUFDQSxVQUFNQyxLQUFLLEdBQUdmLEtBQUssQ0FBQ2dCLFNBQU4sQ0FBZ0IzQixJQUFJLElBQUlBLElBQUksQ0FBQ2EsRUFBTCxLQUFZWSxZQUFZLENBQUNaLEVBQWpELENBQWQ7QUFFQUQsSUFBQUEsUUFBUSxDQUNORCxLQUFLLENBQ0ZpQixLQURILENBQ1MsQ0FEVCxFQUNZRixLQURaLEVBRUdHLE1BRkgsQ0FFVSxFQUNOLEdBQUdKLFlBREc7QUFFTnBCLE1BQUFBLFVBQVUsRUFBRSxDQUFDb0IsWUFBWSxDQUFDcEI7QUFGcEIsS0FGVixFQU1Hd0IsTUFOSCxDQU1VbEIsS0FBSyxDQUFDaUIsS0FBTixDQUFZRixLQUFLLEdBQUcsQ0FBcEIsQ0FOVixDQURNLENBQVI7QUFTRCxHQWZnQixFQWdCakIsQ0FBQ2YsS0FBRCxDQWhCaUIsQ0FBbkI7QUFtQkEsc0JBQ0Usb0JBQUMsY0FBRCxxQkFDRSx1Q0FERixlQUVFO0FBQ0UsSUFBQSxJQUFJLEVBQUMsTUFEUDtBQUVFLElBQUEsV0FBVyxFQUFDLGtCQUZkO0FBR0UsSUFBQSxLQUFLLEVBQUVGLFdBSFQ7QUFJRSxJQUFBLFFBQVEsRUFBRVcsWUFKWjtBQUtFLElBQUEsVUFBVSxFQUFFSDtBQUxkLElBRkYsZUFTRTtBQUFRLElBQUEsUUFBUSxFQUFFUixXQUFXLEtBQUssRUFBbEM7QUFBc0MsSUFBQSxPQUFPLEVBQUVPO0FBQS9DLGtCQUNFO0FBQU0sSUFBQSxJQUFJLEVBQUMsS0FBWDtBQUFpQixrQkFBVztBQUE1QixXQURGLENBVEYsZUFjRSxnQ0FDR0wsS0FBSyxDQUFDbUIsR0FBTixDQUFVOUIsSUFBSSxpQkFDYixvQkFBQyxRQUFEO0FBQ0UsSUFBQSxHQUFHLEVBQUVBLElBQUksQ0FBQ2EsRUFEWjtBQUVFLElBQUEsSUFBSSxFQUFFYixJQUZSO0FBR0UsSUFBQSxVQUFVLEVBQUVDLFVBSGQ7QUFJRSxJQUFBLFVBQVUsRUFBRUM7QUFKZCxJQURELENBREgsQ0FkRixDQURGO0FBMkJEIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDb3B5cmlnaHQgKGMpIEZhY2Vib29rLCBJbmMuIGFuZCBpdHMgYWZmaWxpYXRlcy5cbiAqXG4gKiBUaGlzIHNvdXJjZSBjb2RlIGlzIGxpY2Vuc2VkIHVuZGVyIHRoZSBNSVQgbGljZW5zZSBmb3VuZCBpbiB0aGVcbiAqIExJQ0VOU0UgZmlsZSBpbiB0aGUgcm9vdCBkaXJlY3Rvcnkgb2YgdGhpcyBzb3VyY2UgdHJlZS5cbiAqXG4gKiBAZmxvd1xuICovXG5cbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0JztcbmltcG9ydCB7RnJhZ21lbnQsIHVzZUNhbGxiYWNrLCB1c2VTdGF0ZX0gZnJvbSAncmVhY3QnO1xuXG5leHBvcnQgZnVuY3Rpb24gTGlzdEl0ZW0oe2l0ZW0sIHJlbW92ZUl0ZW0sIHRvZ2dsZUl0ZW19KSB7XG4gIGNvbnN0IGhhbmRsZURlbGV0ZSA9IHVzZUNhbGxiYWNrKCgpID0+IHtcbiAgICByZW1vdmVJdGVtKGl0ZW0pO1xuICB9LCBbaXRlbSwgcmVtb3ZlSXRlbV0pO1xuXG4gIGNvbnN0IGhhbmRsZVRvZ2dsZSA9IHVzZUNhbGxiYWNrKCgpID0+IHtcbiAgICB0b2dnbGVJdGVtKGl0ZW0pO1xuICB9LCBbaXRlbSwgdG9nZ2xlSXRlbV0pO1xuXG4gIHJldHVybiAoXG4gICAgPGxpPlxuICAgICAgPGJ1dHRvbiBvbkNsaWNrPXtoYW5kbGVEZWxldGV9PkRlbGV0ZTwvYnV0dG9uPlxuICAgICAgPGxhYmVsPlxuICAgICAgICA8aW5wdXRcbiAgICAgICAgICBjaGVja2VkPXtpdGVtLmlzQ29tcGxldGV9XG4gICAgICAgICAgb25DaGFuZ2U9e2hhbmRsZVRvZ2dsZX1cbiAgICAgICAgICB0eXBlPVwiY2hlY2tib3hcIlxuICAgICAgICAvPnsnICd9XG4gICAgICAgIHtpdGVtLnRleHR9XG4gICAgICA8L2xhYmVsPlxuICAgIDwvbGk+XG4gICk7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBMaXN0KHByb3BzKSB7XG4gIGNvbnN0IFtuZXdJdGVtVGV4dCwgc2V0TmV3SXRlbVRleHRdID0gdXNlU3RhdGUoJycpO1xuICBjb25zdCBbaXRlbXMsIHNldEl0ZW1zXSA9IHVzZVN0YXRlKFtcbiAgICB7aWQ6IDEsIGlzQ29tcGxldGU6IHRydWUsIHRleHQ6ICdGaXJzdCd9LFxuICAgIHtpZDogMiwgaXNDb21wbGV0ZTogdHJ1ZSwgdGV4dDogJ1NlY29uZCd9LFxuICAgIHtpZDogMywgaXNDb21wbGV0ZTogZmFsc2UsIHRleHQ6ICdUaGlyZCd9LFxuICBdKTtcbiAgY29uc3QgW3VpZCwgc2V0VUlEXSA9IHVzZVN0YXRlKDQpO1xuXG4gIGNvbnN0IGhhbmRsZUNsaWNrID0gdXNlQ2FsbGJhY2soKCkgPT4ge1xuICAgIGlmIChuZXdJdGVtVGV4dCAhPT0gJycpIHtcbiAgICAgIHNldEl0ZW1zKFtcbiAgICAgICAgLi4uaXRlbXMsXG4gICAgICAgIHtcbiAgICAgICAgICBpZDogdWlkLFxuICAgICAgICAgIGlzQ29tcGxldGU6IGZhbHNlLFxuICAgICAgICAgIHRleHQ6IG5ld0l0ZW1UZXh0LFxuICAgICAgICB9LFxuICAgICAgXSk7XG4gICAgICBzZXRVSUQodWlkICsgMSk7XG4gICAgICBzZXROZXdJdGVtVGV4dCgnJyk7XG4gICAgfVxuICB9LCBbbmV3SXRlbVRleHQsIGl0ZW1zLCB1aWRdKTtcblxuICBjb25zdCBoYW5kbGVLZXlQcmVzcyA9IHVzZUNhbGxiYWNrKFxuICAgIGV2ZW50ID0+IHtcbiAgICAgIGlmIChldmVudC5rZXkgPT09ICdFbnRlcicpIHtcbiAgICAgICAgaGFuZGxlQ2xpY2soKTtcbiAgICAgIH1cbiAgICB9LFxuICAgIFtoYW5kbGVDbGlja10sXG4gICk7XG5cbiAgY29uc3QgaGFuZGxlQ2hhbmdlID0gdXNlQ2FsbGJhY2soXG4gICAgZXZlbnQgPT4ge1xuICAgICAgc2V0TmV3SXRlbVRleHQoZXZlbnQuY3VycmVudFRhcmdldC52YWx1ZSk7XG4gICAgfSxcbiAgICBbc2V0TmV3SXRlbVRleHRdLFxuICApO1xuXG4gIGNvbnN0IHJlbW92ZUl0ZW0gPSB1c2VDYWxsYmFjayhcbiAgICBpdGVtVG9SZW1vdmUgPT4gc2V0SXRlbXMoaXRlbXMuZmlsdGVyKGl0ZW0gPT4gaXRlbSAhPT0gaXRlbVRvUmVtb3ZlKSksXG4gICAgW2l0ZW1zXSxcbiAgKTtcblxuICBjb25zdCB0b2dnbGVJdGVtID0gdXNlQ2FsbGJhY2soXG4gICAgaXRlbVRvVG9nZ2xlID0+IHtcbiAgICAgIC8vIERvbnQgdXNlIGluZGV4T2YoKVxuICAgICAgLy8gYmVjYXVzZSBlZGl0aW5nIHByb3BzIGluIERldlRvb2xzIGNyZWF0ZXMgYSBuZXcgT2JqZWN0LlxuICAgICAgY29uc3QgaW5kZXggPSBpdGVtcy5maW5kSW5kZXgoaXRlbSA9PiBpdGVtLmlkID09PSBpdGVtVG9Ub2dnbGUuaWQpO1xuXG4gICAgICBzZXRJdGVtcyhcbiAgICAgICAgaXRlbXNcbiAgICAgICAgICAuc2xpY2UoMCwgaW5kZXgpXG4gICAgICAgICAgLmNvbmNhdCh7XG4gICAgICAgICAgICAuLi5pdGVtVG9Ub2dnbGUsXG4gICAgICAgICAgICBpc0NvbXBsZXRlOiAhaXRlbVRvVG9nZ2xlLmlzQ29tcGxldGUsXG4gICAgICAgICAgfSlcbiAgICAgICAgICAuY29uY2F0KGl0ZW1zLnNsaWNlKGluZGV4ICsgMSkpLFxuICAgICAgKTtcbiAgICB9LFxuICAgIFtpdGVtc10sXG4gICk7XG5cbiAgcmV0dXJuIChcbiAgICA8RnJhZ21lbnQ+XG4gICAgICA8aDE+TGlzdDwvaDE+XG4gICAgICA8aW5wdXRcbiAgICAgICAgdHlwZT1cInRleHRcIlxuICAgICAgICBwbGFjZWhvbGRlcj1cIk5ldyBsaXN0IGl0ZW0uLi5cIlxuICAgICAgICB2YWx1ZT17bmV3SXRlbVRleHR9XG4gICAgICAgIG9uQ2hhbmdlPXtoYW5kbGVDaGFuZ2V9XG4gICAgICAgIG9uS2V5UHJlc3M9e2hhbmRsZUtleVByZXNzfVxuICAgICAgLz5cbiAgICAgIDxidXR0b24gZGlzYWJsZWQ9e25ld0l0ZW1UZXh0ID09PSAnJ30gb25DbGljaz17aGFuZGxlQ2xpY2t9PlxuICAgICAgICA8c3BhbiByb2xlPVwiaW1nXCIgYXJpYS1sYWJlbD1cIkFkZCBpdGVtXCI+XG4gICAgICAgICAgQWRkXG4gICAgICAgIDwvc3Bhbj5cbiAgICAgIDwvYnV0dG9uPlxuICAgICAgPHVsPlxuICAgICAgICB7aXRlbXMubWFwKGl0ZW0gPT4gKFxuICAgICAgICAgIDxMaXN0SXRlbVxuICAgICAgICAgICAga2V5PXtpdGVtLmlkfVxuICAgICAgICAgICAgaXRlbT17aXRlbX1cbiAgICAgICAgICAgIHJlbW92ZUl0ZW09e3JlbW92ZUl0ZW19XG4gICAgICAgICAgICB0b2dnbGVJdGVtPXt0b2dnbGVJdGVtfVxuICAgICAgICAgIC8+XG4gICAgICAgICkpfVxuICAgICAgPC91bD5cbiAgICA8L0ZyYWdtZW50PlxuICApO1xufVxuIl19 +//# sourceURL=ToDoList.js \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/useTheme.js b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/useTheme.js new file mode 100644 index 0000000000..1122f6ab69 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/inline/useTheme.js @@ -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 \ No newline at end of file diff --git a/packages/react-devtools-extensions/src/__tests__/__source__/useTheme.js b/packages/react-devtools-extensions/src/__tests__/__source__/useTheme.js new file mode 100644 index 0000000000..bc8ac51cc6 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/__source__/useTheme.js @@ -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; +} diff --git a/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js b/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js new file mode 100644 index 0000000000..3a65b3709d --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js @@ -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 + }); + }); +}); diff --git a/packages/react-devtools-extensions/src/__tests__/updateMockSourceMaps.js b/packages/react-devtools-extensions/src/__tests__/updateMockSourceMaps.js new file mode 100644 index 0000000000..590cd36cc6 --- /dev/null +++ b/packages/react-devtools-extensions/src/__tests__/updateMockSourceMaps.js @@ -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); + } +}); diff --git a/packages/react-devtools-extensions/src/astUtils.js b/packages/react-devtools-extensions/src/astUtils.js new file mode 100644 index 0000000000..cc976bbd19 --- /dev/null +++ b/packages/react-devtools-extensions/src/astUtils.js @@ -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, + sources: Array, + sourcesContent: Array, + 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, + source: string, +): Array { + 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; +} diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 73924cd85a..32efad807e 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -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, diff --git a/packages/react-devtools-extensions/src/parseHookNames.js b/packages/react-devtools-extensions/src/parseHookNames.js new file mode 100644 index 0000000000..f2d74dc292 --- /dev/null +++ b/packages/react-devtools-extensions/src/parseHookNames.js @@ -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 = 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 { + if (!enableHookNameParsing) { + return Promise.resolve(null); + } + + const hooksList: Array = []; + 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 = 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, +): 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)[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 { + 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, + fileNameToHookSourceData: Map, +): 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, +): 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, +): 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, +): 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, +): 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(); +} diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js index 6f63b44e1b..6f5a1d3860 100644 --- a/packages/react-devtools-extensions/webpack.backend.js +++ b/packages/react-devtools-extensions/webpack.backend.js @@ -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: { diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index df813e82bc..94a12ebdd1 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -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$/, diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 30481673ad..7011e7151c 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -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'), diff --git a/packages/react-devtools-shared/package.json b/packages/react-devtools-shared/package.json index 09551aaddd..808d8d5d34 100644 --- a/packages/react-devtools-shared/package.json +++ b/packages/react-devtools-shared/package.json @@ -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", diff --git a/packages/react-devtools-shared/src/__tests__/hookSerializer.js b/packages/react-devtools-shared/src/__tests__/hookSerializer.js new file mode 100644 index 0000000000..2bf84086ed --- /dev/null +++ b/packages/react-devtools-shared/src/__tests__/hookSerializer.js @@ -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)); +} diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index 5bec9a74f6..4eb4b76a07 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -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', () => { }) => ( - - - - {children} - - - + + + + + {children} + + + + ); diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js b/packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js index d3e87c7110..4721a29722 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElementSerializer.js @@ -10,6 +10,7 @@ export function test(maybeInspectedElement) { const hasOwnProperty = Object.prototype.hasOwnProperty.bind( maybeInspectedElement, ); + return ( hasOwnProperty('canEditFunctionProps') && hasOwnProperty('canEditHooks') && diff --git a/packages/react-devtools-shared/src/__tests__/setupEnv.js b/packages/react-devtools-shared/src/__tests__/setupEnv.js index 8b18798e6f..7bbb693628 100644 --- a/packages/react-devtools-shared/src/__tests__/setupEnv.js +++ b/packages/react-devtools-shared/src/__tests__/setupEnv.js @@ -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; diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index b244c7e0cf..c2db1ae546 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -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__; diff --git a/packages/react-devtools-shared/src/__tests__/utils.js b/packages/react-devtools-shared/src/__tests__/utils.js index 74fcebc441..10a96598d5 100644 --- a/packages/react-devtools-shared/src/__tests__/utils.js +++ b/packages/react-devtools-shared/src/__tests__/utils.js @@ -268,3 +268,13 @@ export function withErrorsOrWarningsIgnored>( } } } + +export function overrideFeatureFlags(overrideFlags) { + jest.mock('react-devtools-feature-flags', () => { + const actualFlags = jest.requireActual('react-devtools-feature-flags'); + return { + ...actualFlags, + ...overrideFlags, + }; + }); +} diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index d9aa2672d4..d56819a8ba 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -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. diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js index b9d3ba23e5..e310e26f43 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js @@ -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; diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js index 104cf26309..c6c319cd6d 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js @@ -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; diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js index 4d1b1472bb..7201754f4f 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js @@ -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; diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index a5449612c7..10750fa7d7 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -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'; diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js index fffd0aec4c..8239a765f9 100644 --- a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js +++ b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js @@ -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"> - + {typeof pathData === 'string' ? ( + + ) : ( + pathData + )} ); } @@ -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 = ( + + + + + + +); + const PATH_PREVIOUS = 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Badge.js b/packages/react-devtools-shared/src/devtools/views/Components/Badge.js index aad4848426..25352ee39a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Badge.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Badge.js @@ -26,7 +26,7 @@ export default function Badge({ type, children, }: Props) { - if (hocDisplayNames === null) { + if (hocDisplayNames === null || hocDisplayNames.length === 0) { return null; } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.css b/packages/react-devtools-shared/src/devtools/views/Components/Components.css index f709913919..5624f23489 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.css @@ -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); } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css index 9e68729283..b6ad3b9983 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css @@ -64,5 +64,5 @@ padding: 0.25rem; color: var(--color-dimmer); font-style: italic; + border-left: 1px solid var(--color-border); } - diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js index ea976767c0..d0774d95f9 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js @@ -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} /> )} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js index 396023b352..d705947ff4 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js @@ -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; 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( @@ -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( + 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(() => { + startTransition(() => { + setParseHookNames(value => !value); + refresh(); + }); + }, [setParseHookNames]); + const inspectPaths: InspectPathFunction = useCallback( (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( () => ({ + hookNames, inspectedElement, inspectPaths, + parseHookNames, + toggleParseHookNames, }), - [inspectedElement, inspectPaths], + [ + hookNames, + inspectedElement, + inspectPaths, + parseHookNames, + toggleParseHookNames, + ], ); return ( diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.css index 612fd7eb28..da08989a72 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementErrorBoundary.css @@ -1,3 +1,3 @@ .Wrapper { - border-left: 1px solid var(--color-border); + height: 100%; } \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.css index e5099abb34..a338a88bf8 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.css @@ -77,4 +77,8 @@ margin-right: 0.25rem; border-radius: 0.125rem; padding: 0.125rem 0.25rem; +} + +.HookName { + color: var(--color-component-name); } \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js index 7ed5290569..910e94098c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js @@ -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({
    hooks
    + {enableHookNameParsing && !parseHookNames && ( + + + + )}
    , |}; -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 && ({hookName})} + + ) : ( + 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) { - {name || 'Anonymous'} + {hookDisplayName || 'Anonymous'} {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) { - {name || 'Anonymous'} + {hookDisplayName || 'Anonymous'} {' '} {/* $FlowFixMe */} @@ -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={[]} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js index e7b4165910..8424621fcf 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js @@ -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) => void; export type InspectPath = (path: Array) => 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({ ); } else { - renderedName = {name}; + renderedName = ( + + {name} + {!!hookName && ({hookName})} + + ); } } else if (canRenameTheCurrentPath) { renderedName = ( @@ -215,7 +222,12 @@ export default function KeyValue({ /> ); } else { - renderedName = {name}; + renderedName = ( + + {name} + {!!hookName && ({hookName})} + + ); } let children = null; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/LoadHookNamesFunctionContext.js b/packages/react-devtools-shared/src/devtools/views/Components/LoadHookNamesFunctionContext.js new file mode 100644 index 0000000000..1874dd7e5a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/LoadHookNamesFunctionContext.js @@ -0,0 +1,11 @@ +// @flow + +import {createContext} from 'react'; +import type {LoadHookNamesFunction} from '../DevTools'; + +export type Context = LoadHookNamesFunction | null; + +const LoadHookNamesFunctionContext = createContext(null); +LoadHookNamesFunctionContext.displayName = 'LoadHookNamesFunctionContext'; + +export default LoadHookNamesFunctionContext; diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index a7ac1c4397..534e2cc4e2 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -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; export type ViewAttributeSource = ( id: number, path: Array, @@ -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 ( @@ -191,40 +204,43 @@ export default function DevTools({ componentsPortalContainer={componentsPortalContainer} profilerPortalContainer={profilerPortalContainer}> - - -
    - {showTabBar && ( -
    - - - {process.env.DEVTOOLS_VERSION} - -
    - + + +
    + {showTabBar && ( +
    + + + {process.env.DEVTOOLS_VERSION} + +
    + +
    + )} + - )} - - -
    - - + + + diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js index 6e25adaa5d..a07b8380aa 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js @@ -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, >(() => [...store.componentFilters]); @@ -252,6 +262,18 @@ export default function ComponentsSettings(_: {||}) { Expand component tree by default + {enableHookNameParsing && ( + + )} +
    Hide components where...
    diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index 93d7bfd9d5..be073adc5d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -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( + 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, diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css index 8bf8b3de1f..58ad5d15d3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css @@ -139,3 +139,7 @@ .ReleaseNotesLink { color: var(--color-button-active); } + +.Warning { + color: var(--color-error-text); +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/hookNamesCache.js b/packages/react-devtools-shared/src/hookNamesCache.js new file mode 100644 index 0000000000..6b3ca25af2 --- /dev/null +++ b/packages/react-devtools-shared/src/hookNamesCache.js @@ -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 = {| + status: 1, + value: T, +|}; + +type RejectedRecord = {| + status: 2, + value: null, +|}; + +type Record = PendingRecord | ResolvedRecord | RejectedRecord; + +function readRecord(record: Record): ResolvedRecord | 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>; + +function createMap(): HookNamesMap { + return new WeakMap(); +} + +function getRecordMap(): WeakMap> { + return getCacheForType(createMap); +} + +export function loadHookNames( + element: Element, + hooksTree: HooksTree, + loadHookNamesFunction: (hookLog: HooksTree) => Thenable, +): 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 = (record = { + status: Pending, + value: wakeable, + }); + + let didTimeout = false; + + loadHookNamesFunction(hooksTree).then( + function onSuccess(hookNames) { + if (didTimeout) { + return; + } + + if (hookNames) { + const resolvedRecord = ((newRecord: any): ResolvedRecord); + 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; +} diff --git a/packages/react-devtools-shared/src/types.js b/packages/react-devtools-shared/src/types.js index e2463a6aad..b9faeb7dbe 100644 --- a/packages/react-devtools-shared/src/types.js +++ b/packages/react-devtools-shared/src/types.js @@ -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; + +export type LRUCache = {| + get: (key: K) => V, + has: (key: K) => boolean, + set: (key: K, value: V) => void, +|}; diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index dc16bfe8e8..573679f635 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -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 = 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> = new LRU({ + max: 1000, +}); export function alphaSortKeys( a: string | number | Symbol, diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index a64d2838db..41fd6fa266 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.js @@ -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'), diff --git a/scripts/jest/config.build-devtools.js b/scripts/jest/config.build-devtools.js index 761479ce45..ed6a0b7db0 100644 --- a/scripts/jest/config.build-devtools.js +++ b/scripts/jest/config.build-devtools.js @@ -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/', '/build2/'], - testRegex: 'packages/react-devtools-shared/src/__tests__/[^]+.test.js$', + transformIgnorePatterns: [ + '/node_modules/', + '/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' ), diff --git a/scripts/jest/config.build.js b/scripts/jest/config.build.js index 95bc0e633c..e99034693e 100644 --- a/scripts/jest/config.build.js +++ b/scripts/jest/config.build.js @@ -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 diff --git a/scripts/jest/config.source-persistent.js b/scripts/jest/config.source-persistent.js index 935c4463c1..80bec669b6 100644 --- a/scripts/jest/config.source-persistent.js +++ b/scripts/jest/config.source-persistent.js @@ -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', diff --git a/scripts/jest/config.source-www.js b/scripts/jest/config.source-www.js index d8d29872c1..31e8841ac3 100644 --- a/scripts/jest/config.source-www.js +++ b/scripts/jest/config.source-www.js @@ -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: [ diff --git a/scripts/jest/config.source.js b/scripts/jest/config.source.js index 2e3401a4ba..710df337b5 100644 --- a/scripts/jest/config.source.js +++ b/scripts/jest/config.source.js @@ -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: [ diff --git a/scripts/prettier/index.js b/scripts/prettier/index.js index 4853557640..0be3fefdca 100644 --- a/scripts/prettier/index.js +++ b/scripts/prettier/index.js @@ -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) { diff --git a/yarn.lock b/yarn.lock index 54504c2fd6..3332b21474 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"