diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index f1283899feecc..8781d6e22dbbc 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -287,32 +287,30 @@ function createPanelIfReactLoaded() { }; } + // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. + const hookNamesModuleLoaderFunction = () => + import('react-devtools-inline/hookNames'); + root = createRoot(document.createElement('div')); render = (overrideTab = mostRecentOverrideTab) => { mostRecentOverrideTab = overrideTab; - import('react-devtools-shared/src/hooks/parseHookNames').then( - ({parseHookNames, prefetchSourceFiles, purgeCachedMetadata}) => { - root.render( - createElement(DevTools, { - bridge, - browserTheme: getBrowserTheme(), - componentsPortalContainer, - enabledInspectedElementContextMenu: true, - fetchFileWithCaching, - loadHookNames: parseHookNames, - overrideTab, - prefetchSourceFiles, - profilerPortalContainer, - purgeCachedHookNamesMetadata: purgeCachedMetadata, - showTabBar: false, - store, - warnIfUnsupportedVersionDetected: true, - viewAttributeSourceFunction, - viewElementSourceFunction, - }), - ); - }, + root.render( + createElement(DevTools, { + bridge, + browserTheme: getBrowserTheme(), + componentsPortalContainer, + enabledInspectedElementContextMenu: true, + fetchFileWithCaching, + hookNamesModuleLoaderFunction, + overrideTab, + profilerPortalContainer, + showTabBar: false, + store, + warnIfUnsupportedVersionDetected: true, + viewAttributeSourceFunction, + viewElementSourceFunction, + }), ); }; diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index 7f9e632ffab19..233422276ec41 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -52,6 +52,7 @@ module.exports = { path: __dirname + '/build', publicPath: '/build/', filename: '[name].js', + chunkFilename: '[name].chunk.js', }, node: { // Don't define a polyfill on window.setImmediate diff --git a/packages/react-devtools-inline/README.md b/packages/react-devtools-inline/README.md index 88b2c93b7cb05..35b0127f41d54 100644 --- a/packages/react-devtools-inline/README.md +++ b/packages/react-devtools-inline/README.md @@ -64,6 +64,23 @@ const DevTools = initialize(contentWindow); ## Examples +### Supporting named hooks + +DevTools can display hook "names" for an inspected component, although determining the "names" requires loading the source (and source-maps), parsing the code, and infering the names based on which variables hook values get assigned to. Because the code for this is non-trivial, it's lazy-loaded only if the feature is enabled. + +To configure this package to support this functionality, you'll need to provide a prop that dynamically imports the extra functionality: +```js +// Follow code examples above to configure the backend and frontend. +// When rendering DevTools, the important part is to pass a 'hookNamesModuleLoaderFunction' prop. +const hookNamesModuleLoaderFunction = () => import('react-devtools-inline/hookNames'); + +// Render: +; +``` + ### Configuring a same-origin `iframe` The simplest way to use this package is to install the hook from the parent `window`. This is possible if the `iframe` is not sandboxed and there are no cross-origin restrictions. diff --git a/packages/react-devtools-inline/hookNames.js b/packages/react-devtools-inline/hookNames.js new file mode 100644 index 0000000000000..6a319e2de30b5 --- /dev/null +++ b/packages/react-devtools-inline/hookNames.js @@ -0,0 +1 @@ +module.exports = require('./dist/hookNames'); diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index 8b2e745a85856..160723b21af56 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -20,7 +20,10 @@ "prepublish": "yarn run build", "start": "cross-env NODE_ENV=development webpack --config webpack.config.js --watch" }, - "dependencies": {}, + "dependencies": { + "source-map-js": "^0.6.2", + "sourcemap-codec": "^1.4.8" + }, "devDependencies": { "@babel/core": "^7.11.1", "@babel/plugin-proposal-class-properties": "^7.10.4", diff --git a/packages/react-devtools-inline/src/hookNames.js b/packages/react-devtools-inline/src/hookNames.js new file mode 100644 index 0000000000000..7436ef7d01dba --- /dev/null +++ b/packages/react-devtools-inline/src/hookNames.js @@ -0,0 +1,9 @@ +/** @flow */ + +import { + parseHookNames, + parseSourceAndMetadata, + purgeCachedMetadata, +} from 'react-devtools-shared/src/hooks/parseHookNames'; + +export {parseHookNames, parseSourceAndMetadata, purgeCachedMetadata}; diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 169484f8cc271..bc38a8792e4c4 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -37,10 +37,12 @@ module.exports = { entry: { backend: './src/backend.js', frontend: './src/frontend.js', + hookNames: './src/hookNames.js', }, output: { path: __dirname + '/dist', filename: '[name].js', + chunkFilename: '[name].chunk.js', library: '[name]', libraryTarget: 'commonjs2', }, diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js index 4da6aef3441b5..6c0568867ba39 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-fb.js @@ -15,7 +15,7 @@ export const enableProfilerChangedHookIndices = true; export const isInternalFacebookBuild = true; - +export const enableNamedHooksFeature = false; export const consoleManagedByDevToolsDuringStrictMode = false; /************************************************************************ diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js index e6144b7e4f3a3..341af11e17f79 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.core-oss.js @@ -15,7 +15,7 @@ export const enableProfilerChangedHookIndices = false; export const isInternalFacebookBuild = false; - +export const enableNamedHooksFeature = false; export const consoleManagedByDevToolsDuringStrictMode = false; /************************************************************************ diff --git a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js index 0813423712643..e6404254a3850 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.default.js @@ -15,5 +15,5 @@ export const enableProfilerChangedHookIndices = false; export const isInternalFacebookBuild = false; - +export const enableNamedHooksFeature = true; export const consoleManagedByDevToolsDuringStrictMode = true; 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 ba593ffe72c1b..d4e6160b5e860 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-fb.js @@ -15,7 +15,7 @@ export const enableProfilerChangedHookIndices = true; export const isInternalFacebookBuild = true; - +export const enableNamedHooksFeature = true; export const consoleManagedByDevToolsDuringStrictMode = 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 c1bb06855d191..382c3fab4c31b 100644 --- a/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js +++ b/packages/react-devtools-shared/src/config/DevToolsFeatureFlags.extension-oss.js @@ -15,7 +15,7 @@ export const enableProfilerChangedHookIndices = true; export const isInternalFacebookBuild = false; - +export const enableNamedHooksFeature = true; export const consoleManagedByDevToolsDuringStrictMode = true; /************************************************************************ diff --git a/packages/react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext.js b/packages/react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext.js new file mode 100644 index 0000000000000..4647ebe9bf76b --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext.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} from 'react'; + +export type FetchFileWithCaching = (url: string) => Promise; +export type Context = FetchFileWithCaching | null; + +const FetchFileWithCachingContext = createContext(null); +FetchFileWithCachingContext.displayName = 'FetchFileWithCachingContext'; + +export default FetchFileWithCachingContext; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/HookNamesContext.js b/packages/react-devtools-shared/src/devtools/views/Components/HookNamesContext.js deleted file mode 100644 index f9f295c7eb43d..0000000000000 --- a/packages/react-devtools-shared/src/devtools/views/Components/HookNamesContext.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow - -import {createContext} from 'react'; -import type { - FetchFileWithCaching, - LoadHookNamesFunction, - PrefetchSourceFiles, - PurgeCachedHookNamesMetadata, -} from '../DevTools'; - -export type Context = { - fetchFileWithCaching: FetchFileWithCaching | null, - loadHookNames: LoadHookNamesFunction | null, - prefetchSourceFiles: PrefetchSourceFiles | null, - purgeCachedMetadata: PurgeCachedHookNamesMetadata | null, -}; - -const HookNamesContext = createContext({ - fetchFileWithCaching: null, - loadHookNames: null, - prefetchSourceFiles: null, - purgeCachedMetadata: null, -}); -HookNamesContext.displayName = 'HookNamesContext'; - -export default HookNamesContext; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext.js b/packages/react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext.js new file mode 100644 index 0000000000000..c9a09e64c0e19 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext.js @@ -0,0 +1,22 @@ +/** + * 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 type {Thenable} from 'shared/ReactTypes'; + +import {createContext} from 'react'; +import typeof * as ParseHookNamesModule from 'react-devtools-shared/src/hooks/parseHookNames'; + +export type HookNamesModuleLoaderFunction = () => Thenable; +export type Context = HookNamesModuleLoaderFunction | null; + +// TODO (Webpack 5) Hopefully we can remove this context entirely once the Webpack 5 upgrade is completed. +const HookNamesModuleLoaderContext = createContext(null); +HookNamesModuleLoaderContext.displayName = 'HookNamesModuleLoaderContext'; + +export default HookNamesModuleLoaderContext; 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 967196a3b9052..c02dc5bf9263a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js @@ -30,8 +30,11 @@ import { hasAlreadyLoadedHookNames, loadHookNames, } from 'react-devtools-shared/src/hookNamesCache'; -import HookNamesContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesContext'; +import {loadModule} from 'react-devtools-shared/src/dynamicImportCache'; +import FetchFileWithCachingContext from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext'; +import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import {SettingsContext} from '../Settings/SettingsContext'; +import {enableNamedHooksFeature} from 'react-devtools-feature-flags'; import type {HookNames} from 'react-devtools-shared/src/types'; import type {ReactNodeList} from 'shared/ReactTypes'; @@ -64,16 +67,17 @@ export type Props = {| export function InspectedElementContextController({children}: Props) { const {selectedElementID} = useContext(TreeStateContext); - const { - fetchFileWithCaching, - loadHookNames: loadHookNamesFunction, - prefetchSourceFiles, - purgeCachedMetadata, - } = useContext(HookNamesContext); + const fetchFileWithCaching = useContext(FetchFileWithCachingContext); const bridge = useContext(BridgeContext); const store = useContext(StoreContext); const {parseHookNames: parseHookNamesByDefault} = useContext(SettingsContext); + // parseHookNames has a lot of code. + // Embedding it into a build makes the build large. + // This function enables DevTools to make use of Suspense to lazily import() it only if the feature will be used. + // TODO (Webpack 5) Hopefully we can remove this indirection once the Webpack 5 upgrade is completed. + const hookNamesModuleLoader = useContext(HookNamesModuleLoaderContext); + const refresh = useCacheRefresh(); // Temporarily stores most recently-inspected (hydrated) path. @@ -113,24 +117,40 @@ export function InspectedElementContextController({children}: Props) { setParseHookNames(parseHookNamesByDefault || alreadyLoadedHookNames); } + const purgeCachedMetadataRef = useRef(null); + // Don't load a stale element from the backend; it wastes bridge bandwidth. let hookNames: HookNames | null = null; let inspectedElement = null; if (!elementHasChanged && element !== null) { inspectedElement = inspectElement(element, state.path, store, bridge); - if (parseHookNames || alreadyLoadedHookNames) { - if ( - inspectedElement !== null && - inspectedElement.hooks !== null && - loadHookNamesFunction !== null - ) { - hookNames = loadHookNames( - element, - inspectedElement.hooks, - loadHookNamesFunction, - fetchFileWithCaching, - ); + if (enableNamedHooksFeature) { + if (typeof hookNamesModuleLoader === 'function') { + if (parseHookNames || alreadyLoadedHookNames) { + const hookNamesModule = loadModule(hookNamesModuleLoader); + if (hookNamesModule !== null) { + const { + parseHookNames: loadHookNamesFunction, + purgeCachedMetadata, + } = hookNamesModule; + + purgeCachedMetadataRef.current = purgeCachedMetadata; + + if ( + inspectedElement !== null && + inspectedElement.hooks !== null && + loadHookNamesFunction !== null + ) { + hookNames = loadHookNames( + element, + inspectedElement.hooks, + loadHookNamesFunction, + fetchFileWithCaching, + ); + } + } + } } } } @@ -163,14 +183,11 @@ export function InspectedElementContextController({children}: Props) { inspectedElementRef.current !== inspectedElement ) { inspectedElementRef.current = inspectedElement; - - if (typeof prefetchSourceFiles === 'function') { - prefetchSourceFiles(inspectedElement.hooks, fetchFileWithCaching); - } } - }, [inspectedElement, prefetchSourceFiles]); + }, [inspectedElement]); useEffect(() => { + const purgeCachedMetadata = purgeCachedMetadataRef.current; if (typeof purgeCachedMetadata === 'function') { // When Fast Refresh updates a component, any cached AST metadata may be invalid. const fastRefreshScheduled = () => { 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 b478aeb441297..fb80b0152968e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js @@ -22,8 +22,11 @@ import styles from './InspectedElementHooksTree.css'; import useContextMenu from '../../ContextMenu/useContextMenu'; import {meta} from '../../../hydration'; import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache'; -import {enableProfilerChangedHookIndices} from 'react-devtools-feature-flags'; -import HookNamesContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesContext'; +import { + enableNamedHooksFeature, + enableProfilerChangedHookIndices, +} from 'react-devtools-feature-flags'; +import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import type {InspectedElement} from './types'; import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; @@ -53,8 +56,6 @@ export function InspectedElementHooksTree({ }: HooksTreeViewProps) { const {hooks, id} = inspectedElement; - const {loadHookNames: loadHookNamesFunction} = useContext(HookNamesContext); - // 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( @@ -65,6 +66,8 @@ export function InspectedElementHooksTree({ toggleParseHookNames(); }; + const hookNamesModuleLoader = useContext(HookNamesModuleLoaderContext); + const hookParsingFailed = parseHookNames && hookNames === null; let toggleTitle; @@ -85,7 +88,8 @@ export function InspectedElementHooksTree({
hooks
- {loadHookNamesFunction !== null && + {enableNamedHooksFeature && + typeof hookNamesModuleLoader === 'function' && (!parseHookNames || hookParsingFailed) && ( Promise; -export type PrefetchSourceFiles = ( - hooksTree: HooksTree, - fetchFileWithCaching: FetchFileWithCaching | null, -) => void; export type ViewElementSource = ( id: number, inspectedElement: InspectedElement, ) => void; -export type LoadHookNamesFunction = ( - hooksTree: HooksTree, -) => Thenable; -export type PurgeCachedHookNamesMetadata = () => void; export type ViewAttributeSource = ( id: number, path: Array, @@ -107,9 +98,8 @@ export type Props = {| // 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. fetchFileWithCaching?: ?FetchFileWithCaching, - loadHookNames?: ?LoadHookNamesFunction, - prefetchSourceFiles?: ?PrefetchSourceFiles, - purgeCachedHookNamesMetadata?: ?PurgeCachedHookNamesMetadata, + // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. + hookNamesModuleLoaderFunction?: ?HookNamesModuleLoaderFunction, |}; const componentsTab = { @@ -135,11 +125,9 @@ export default function DevTools({ defaultTab = 'components', enabledInspectedElementContextMenu = false, fetchFileWithCaching, - loadHookNames, + hookNamesModuleLoaderFunction, overrideTab, profilerPortalContainer, - prefetchSourceFiles, - purgeCachedHookNamesMetadata, showTabBar = false, store, warnIfLegacyBackendDetected = false, @@ -199,21 +187,6 @@ export default function DevTools({ [enabledInspectedElementContextMenu, viewAttributeSourceFunction], ); - const hookNamesContext = useMemo( - () => ({ - fetchFileWithCaching: fetchFileWithCaching || null, - loadHookNames: loadHookNames || null, - prefetchSourceFiles: prefetchSourceFiles || null, - purgeCachedMetadata: purgeCachedHookNamesMetadata || null, - }), - [ - fetchFileWithCaching, - loadHookNames, - prefetchSourceFiles, - purgeCachedHookNamesMetadata, - ], - ); - const devToolsRef = useRef(null); useEffect(() => { @@ -270,51 +243,55 @@ 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/dynamicImportCache.js b/packages/react-devtools-shared/src/dynamicImportCache.js new file mode 100644 index 0000000000000..78045856abb88 --- /dev/null +++ b/packages/react-devtools-shared/src/dynamicImportCache.js @@ -0,0 +1,159 @@ +/** + * 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 {__DEBUG__} from 'react-devtools-shared/src/constants'; + +import type {Thenable, Wakeable} from 'shared/ReactTypes'; + +const TIMEOUT = 30000; + +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; + +type Module = any; +type ModuleLoaderFunction = () => Thenable; + +// This is intentionally a module-level Map, rather than a React-managed one. +// Otherwise, refreshing the inspected element cache would also clear this cache. +// Modules are static anyway. +const moduleLoaderFunctionToModuleMap: Map< + ModuleLoaderFunction, + Module, +> = new Map(); + +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; + } +} + +// TODO Flow type +export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module { + let record = moduleLoaderFunctionToModuleMap.get(moduleLoaderFunction); + + if (__DEBUG__) { + console.log( + `[dynamicImportCache] loadModule("${moduleLoaderFunction.name}")`, + ); + } + + if (!record) { + 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; + + moduleLoaderFunction().then( + module => { + if (__DEBUG__) { + console.log( + `[dynamicImportCache] loadModule("${moduleLoaderFunction.name}") then()`, + ); + } + + if (didTimeout) { + return; + } + + const resolvedRecord = ((newRecord: any): ResolvedRecord); + resolvedRecord.status = Resolved; + resolvedRecord.value = module; + + wake(); + }, + error => { + if (__DEBUG__) { + console.log( + `[dynamicImportCache] loadModule("${moduleLoaderFunction.name}") catch()`, + ); + } + + if (didTimeout) { + return; + } + + console.log(error); + + const thrownRecord = ((newRecord: any): RejectedRecord); + thrownRecord.status = Rejected; + thrownRecord.value = null; + + wake(); + }, + ); + + // Eventually timeout and stop trying to load the module. + let timeoutID = setTimeout(function onTimeout() { + if (__DEBUG__) { + console.log( + `[dynamicImportCache] loadModule("${moduleLoaderFunction.name}") onTimeout()`, + ); + } + + timeoutID = null; + + didTimeout = true; + + const timedoutRecord = ((newRecord: any): RejectedRecord); + timedoutRecord.status = Rejected; + timedoutRecord.value = null; + + wake(); + }, TIMEOUT); + + moduleLoaderFunctionToModuleMap.set(moduleLoaderFunction, record); + } + + const response = readRecord(record).value; + return response; +} diff --git a/packages/react-devtools-shared/src/hookNamesCache.js b/packages/react-devtools-shared/src/hookNamesCache.js index 3fceaa7d02477..104a1e3455eb8 100644 --- a/packages/react-devtools-shared/src/hookNamesCache.js +++ b/packages/react-devtools-shared/src/hookNamesCache.js @@ -17,7 +17,7 @@ import type { HookSourceLocationKey, } from 'react-devtools-shared/src/types'; import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks'; -import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/DevTools'; +import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext'; const TIMEOUT = 30000; diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/index.js b/packages/react-devtools-shared/src/hooks/parseHookNames/index.js index eae8440399c7b..a400a021a91fa 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/index.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/index.js @@ -10,21 +10,15 @@ import type {HookSourceAndMetadata} from './loadSourceAndMetadata'; import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; import type {HookNames} from 'react-devtools-shared/src/types'; -import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/DevTools'; +import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext'; import {withAsyncPerformanceMark} from 'react-devtools-shared/src/PerformanceMarks'; import WorkerizedParseSourceAndMetadata from './parseSourceAndMetadata.worker'; import typeof * as ParseSourceAndMetadataModule from './parseSourceAndMetadata'; -import { - flattenHooksList, - loadSourceAndMetadata, - prefetchSourceFiles, -} from './loadSourceAndMetadata'; +import {flattenHooksList, loadSourceAndMetadata} from './loadSourceAndMetadata'; const workerizedParseHookNames: ParseSourceAndMetadataModule = WorkerizedParseSourceAndMetadata(); -export {prefetchSourceFiles}; - export function parseSourceAndMetadata( hooksList: Array, locationKeyToHookSourceAndMetadata: Map, diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js index b72c2c68557b7..e8f043be24c19 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js @@ -45,7 +45,6 @@ // This is the fastest option since our custom metadata file is much smaller than a full source map, // and there is no need to convert runtime code to the original source. -import LRU from 'lru-cache'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache'; import {sourceMapIncludesSource} from '../SourceMapUtils'; @@ -55,14 +54,13 @@ import { withSyncPerformanceMark, } from 'react-devtools-shared/src/PerformanceMarks'; -import type {LRUCache} from 'react-devtools-shared/src/types'; import type { HooksNode, HookSource, HooksTree, } from 'react-debug-tools/src/ReactDebugHooks'; import type {MixedSourceMap} from '../SourceMapTypes'; -import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/DevTools'; +import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext'; // Prefer a cached albeit stale response to reduce download time. // We wouldn't want to load/parse a newer version of the source (even if one existed). @@ -70,14 +68,6 @@ const FETCH_OPTIONS = {cache: 'force-cache'}; const MAX_SOURCE_LENGTH = 100_000_000; -// Fetch requests originated from an extension might not have origin headers -// which may prevent subsequent requests from using cached responses -// if the server returns a Vary: 'Origin' header -// so this cache will temporarily store pre-fetches sources in memory. -const prefetchedSources: LRUCache = new LRU({ - max: 15, -}); - export type HookSourceAndMetadata = {| // Generated by react-debug-tools. hookSource: HookSource, @@ -477,109 +467,47 @@ function loadSourceFiles( locationKeyToHookSourceAndMetadata.forEach(hookSourceAndMetadata => { const {runtimeSourceURL} = hookSourceAndMetadata; - const prefetchedSourceCode = prefetchedSources.get(runtimeSourceURL); - if (prefetchedSourceCode != null) { - hookSourceAndMetadata.runtimeSourceCode = prefetchedSourceCode; - } else { - let fetchFileFunction = fetchFile; - if (fetchFileWithCaching != null) { - // If a helper function has been injected to fetch with caching, - // use it to fetch the (already loaded) source file. - fetchFileFunction = url => { - return withAsyncPerformanceMark( - `fetchFileWithCaching("${url}")`, - () => { - return ((fetchFileWithCaching: any): FetchFileWithCaching)(url); - }, - ); - }; - } + let fetchFileFunction = fetchFile; + if (fetchFileWithCaching != null) { + // If a helper function has been injected to fetch with caching, + // use it to fetch the (already loaded) source file. + fetchFileFunction = url => { + return withAsyncPerformanceMark( + `fetchFileWithCaching("${url}")`, + () => { + return ((fetchFileWithCaching: any): FetchFileWithCaching)(url); + }, + ); + }; + } - const fetchPromise = - dedupedFetchPromises.get(runtimeSourceURL) || - fetchFileFunction(runtimeSourceURL).then(runtimeSourceCode => { - // TODO (named hooks) Re-think this; the main case where it matters is when there's no source-maps, - // because then we need to parse the full source file as an AST. - if (runtimeSourceCode.length > MAX_SOURCE_LENGTH) { - throw Error('Source code too large to parse'); - } + const fetchPromise = + dedupedFetchPromises.get(runtimeSourceURL) || + fetchFileFunction(runtimeSourceURL).then(runtimeSourceCode => { + // TODO (named hooks) Re-think this; the main case where it matters is when there's no source-maps, + // because then we need to parse the full source file as an AST. + if (runtimeSourceCode.length > MAX_SOURCE_LENGTH) { + throw Error('Source code too large to parse'); + } - if (__DEBUG__) { - console.groupCollapsed( - `loadSourceFiles() runtimeSourceURL "${runtimeSourceURL}"`, - ); - console.log(runtimeSourceCode); - console.groupEnd(); - } + if (__DEBUG__) { + console.groupCollapsed( + `loadSourceFiles() runtimeSourceURL "${runtimeSourceURL}"`, + ); + console.log(runtimeSourceCode); + console.groupEnd(); + } - return runtimeSourceCode; - }); - dedupedFetchPromises.set(runtimeSourceURL, fetchPromise); + return runtimeSourceCode; + }); + dedupedFetchPromises.set(runtimeSourceURL, fetchPromise); - setterPromises.push( - fetchPromise.then(runtimeSourceCode => { - hookSourceAndMetadata.runtimeSourceCode = runtimeSourceCode; - }), - ); - } + setterPromises.push( + fetchPromise.then(runtimeSourceCode => { + hookSourceAndMetadata.runtimeSourceCode = runtimeSourceCode; + }), + ); }); return Promise.all(setterPromises); } - -export function prefetchSourceFiles( - hooksTree: HooksTree, - fetchFileWithCaching: FetchFileWithCaching | null, -): void { - // Deduplicate fetches, since there can be multiple location keys per source map. - const dedupedFetchPromises = new Set(); - - let fetchFileFunction = null; - if (fetchFileWithCaching != null) { - // If a helper function has been injected to fetch with caching, - // use it to fetch the (already loaded) source file. - fetchFileFunction = url => { - return withAsyncPerformanceMark( - `[pre] fetchFileWithCaching("${url}")`, - () => { - return ((fetchFileWithCaching: any): FetchFileWithCaching)(url); - }, - ); - }; - } else { - fetchFileFunction = url => fetchFile(url, '[pre] fetchFile'); - } - - const hooksQueue = Array.from(hooksTree); - - for (let i = 0; i < hooksQueue.length; i++) { - const hook = hooksQueue.pop(); - if (isUnnamedBuiltInHook(hook)) { - continue; - } - - const hookSource = hook.hookSource; - if (hookSource == null) { - continue; - } - - const runtimeSourceURL = ((hookSource.fileName: any): string); - - if (prefetchedSources.has(runtimeSourceURL)) { - // If we've already fetched this source, skip it. - continue; - } - - if (!dedupedFetchPromises.has(runtimeSourceURL)) { - dedupedFetchPromises.add(runtimeSourceURL); - - fetchFileFunction(runtimeSourceURL).then(text => { - prefetchedSources.set(runtimeSourceURL, text); - }); - } - - if (hook.subHooks.length > 0) { - hooksQueue.push(...hook.subHooks); - } - } -} diff --git a/packages/react-devtools-shell/src/devtools.js b/packages/react-devtools-shell/src/devtools.js index fda4c91b74ffe..7b4171851c1be 100644 --- a/packages/react-devtools-shell/src/devtools.js +++ b/packages/react-devtools-shell/src/devtools.js @@ -10,6 +10,11 @@ import { import {initialize as initializeFrontend} from 'react-devtools-inline/frontend'; import {initDevTools} from 'react-devtools-shared/src/devtools'; +// This is a pretty gross hack to make the runtime loaded named-hooks-code work. +// TODO (Webpack 5) Hoepfully we can remove this once we upgrade to Webpack 5. +// $FlowFixMe +__webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef + const iframe = ((document.getElementById('target'): any): HTMLIFrameElement); const {contentDocument, contentWindow} = iframe; @@ -50,6 +55,11 @@ mountButton.addEventListener('click', function() { } }); +// TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. +function hookNamesModuleLoaderFunction() { + return import('react-devtools-inline/hookNames'); +} + inject('dist/app.js', () => { initDevTools({ connect(cb) { @@ -58,6 +68,7 @@ inject('dist/app.js', () => { createElement(DevTools, { browserTheme: 'light', enabledInspectedElementContextMenu: true, + hookNamesModuleLoaderFunction, showTabBar: true, warnIfLegacyBackendDetected: true, warnIfUnsupportedVersionDetected: true, diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index e60a004a3b391..029f9f5f7db40 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.js @@ -32,7 +32,7 @@ const DEVTOOLS_VERSION = getVersionString(); const config = { mode: __DEV__ ? 'development' : 'production', - devtool: __DEV__ ? 'cheap-module-eval-source-map' : 'source-map', + devtool: __DEV__ ? 'cheap-source-map' : 'source-map', entry: { app: './src/app/index.js', devtools: './src/devtools.js', @@ -108,6 +108,7 @@ const config = { }; if (TARGET === 'local') { + // Local dev server build. config.devServer = { hot: true, port: 8080, @@ -116,6 +117,7 @@ if (TARGET === 'local') { stats: 'errors-only', }; } else { + // Static build to deploy somewhere else. config.output = { path: resolve(__dirname, 'dist'), filename: '[name].js',