From 08c3fcbf1097bd317a4050300592afac67698a77 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 7 Sep 2021 11:48:19 -0400 Subject: [PATCH 1/8] Added named hooks feature flag to DevToolsFeatureFlags --- .../src/config/DevToolsFeatureFlags.core-fb.js | 2 +- .../src/config/DevToolsFeatureFlags.core-oss.js | 2 +- .../src/config/DevToolsFeatureFlags.default.js | 2 +- .../src/config/DevToolsFeatureFlags.extension-fb.js | 2 +- .../src/config/DevToolsFeatureFlags.extension-oss.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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; /************************************************************************ From 06fab2dd3193fe980a595c6326d4710fd2198062 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 7 Sep 2021 12:01:05 -0400 Subject: [PATCH 2/8] Added new dynamic import suspense cache And used it to import named hooks code. Note this commit currently breaks the test shell. --- .../react-devtools-extensions/src/main.js | 37 ++-- .../webpack.config.js | 1 + packages/react-devtools-inline/package.json | 5 +- .../react-devtools-inline/webpack.config.js | 1 + .../Components/FetchFileWithCachingContext.js | 11 ++ .../views/Components/HookNamesContext.js | 26 --- .../Components/InspectedElementContext.js | 63 ++++--- .../Components/InspectedElementHooksTree.js | 29 ++-- .../src/devtools/views/DevTools.js | 35 +--- .../src/dynamicImportCache.js | 159 ++++++++++++++++++ .../src/hookNamesCache.js | 2 +- .../src/hooks/parseHookNames/index.js | 2 +- .../parseHookNames/loadSourceAndMetadata.js | 2 +- .../react-devtools-shell/webpack.config.js | 2 + 14 files changed, 259 insertions(+), 116 deletions(-) create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext.js delete mode 100644 packages/react-devtools-shared/src/devtools/views/Components/HookNamesContext.js create mode 100644 packages/react-devtools-shared/src/dynamicImportCache.js diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index f1283899feecc..f8d24a72118e5 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -291,28 +291,21 @@ function createPanelIfReactLoaded() { 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, + 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/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/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 169484f8cc271..17b008c452d88 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -41,6 +41,7 @@ module.exports = { output: { path: __dirname + '/dist', filename: '[name].js', + chunkFilename: '[name].chunk.js', library: '[name]', libraryTarget: 'commonjs2', }, 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..210a3ec83239b --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext.js @@ -0,0 +1,11 @@ +// @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/InspectedElementContext.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js index 967196a3b9052..c5ab69ada2264 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,10 @@ 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 {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'; @@ -58,18 +60,20 @@ export const InspectedElementContext = createContext( const POLL_INTERVAL = 1000; +// parseHookNames has a lot of code. +// Embedding it into a build makes the build large. +// This component uses Suspense to lazily import() it only if the feature will be used. +function loadHookNamesModuleLoaderFunction() { + return import('react-devtools-shared/src/hooks/parseHookNames'); +} + export type Props = {| children: ReactNodeList, |}; 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); @@ -113,24 +117,43 @@ export function InspectedElementContextController({children}: Props) { setParseHookNames(parseHookNamesByDefault || alreadyLoadedHookNames); } + const prefetchSourceFilesRef = useRef(null); + 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 (parseHookNames || alreadyLoadedHookNames) { + const loadHookNamesModule = loadModule( + loadHookNamesModuleLoaderFunction, ); + if (loadHookNamesModule !== null) { + const { + parseHookNames: loadHookNamesFunction, + prefetchSourceFiles, + purgeCachedMetadata, + } = loadHookNamesModule; + + purgeCachedMetadataRef.current = purgeCachedMetadata; + prefetchSourceFilesRef.current = prefetchSourceFiles; + + if ( + inspectedElement !== null && + inspectedElement.hooks !== null && + loadHookNamesFunction !== null + ) { + hookNames = loadHookNames( + element, + inspectedElement.hooks, + loadHookNamesFunction, + fetchFileWithCaching, + ); + } + } } } } @@ -164,13 +187,15 @@ export function InspectedElementContextController({children}: Props) { ) { inspectedElementRef.current = inspectedElement; + const prefetchSourceFiles = prefetchSourceFilesRef.current; 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..2a9d8ef2f7588 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,10 @@ 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 type {InspectedElement} from './types'; import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; @@ -53,8 +55,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( @@ -85,17 +85,16 @@ export function InspectedElementHooksTree({
hooks
- {loadHookNamesFunction !== null && - (!parseHookNames || hookParsingFailed) && ( - - - - )} + {enableNamedHooksFeature && (!parseHookNames || hookParsingFailed) && ( + + + + )} diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index a1280d14bd224..f3507afaf4c97 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -27,7 +27,7 @@ import TabBar from './TabBar'; import {SettingsContextController} from './Settings/SettingsContext'; import {TreeContextController} from './Components/TreeContext'; import ViewElementSourceContext from './Components/ViewElementSourceContext'; -import HookNamesContext from './Components/HookNamesContext'; +import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext'; import {ProfilerContextController} from './Profiler/ProfilerContext'; import {SchedulingProfilerContextController} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; import {ModalDialogContextController} from './ModalDialog'; @@ -44,6 +44,7 @@ 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 {FetchFileWithCaching} from './Components/FetchFileWithCachingContext'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type {HookNames} from 'react-devtools-shared/src/types'; import type {Thenable} from '../cache'; @@ -51,11 +52,6 @@ import type {Thenable} from '../cache'; export type BrowserTheme = 'dark' | 'light'; export type TabID = 'components' | 'profiler'; -export type FetchFileWithCaching = (url: string) => Promise; -export type PrefetchSourceFiles = ( - hooksTree: HooksTree, - fetchFileWithCaching: FetchFileWithCaching | null, -) => void; export type ViewElementSource = ( id: number, inspectedElement: InspectedElement, @@ -63,7 +59,6 @@ export type ViewElementSource = ( export type LoadHookNamesFunction = ( hooksTree: HooksTree, ) => Thenable; -export type PurgeCachedHookNamesMetadata = () => void; export type ViewAttributeSource = ( id: number, path: Array, @@ -107,9 +102,6 @@ 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, |}; const componentsTab = { @@ -135,11 +127,8 @@ export default function DevTools({ defaultTab = 'components', enabledInspectedElementContextMenu = false, fetchFileWithCaching, - loadHookNames, overrideTab, profilerPortalContainer, - prefetchSourceFiles, - purgeCachedHookNamesMetadata, showTabBar = false, store, warnIfLegacyBackendDetected = false, @@ -199,21 +188,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,7 +244,8 @@ export default function DevTools({ componentsPortalContainer={componentsPortalContainer} profilerPortalContainer={profilerPortalContainer}> - + @@ -314,7 +289,7 @@ export default function DevTools({ - + 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..6a5ff1efebd87 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/index.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/index.js @@ -10,7 +10,7 @@ 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'; diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js index b72c2c68557b7..240c9dc8bde5f 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js @@ -62,7 +62,7 @@ import type { 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). diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index e60a004a3b391..efe69ca1a49ba 100644 --- a/packages/react-devtools-shell/webpack.config.js +++ b/packages/react-devtools-shell/webpack.config.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', From b941b0399444143ab29d41240e602069b0daa65b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 7 Sep 2021 14:58:27 -0400 Subject: [PATCH 3/8] Added hookNames entrypoint to react-devtools-inline And changed dynamic import() code to be passed in rather than embedded in the package --- .../react-devtools-extensions/src/main.js | 4 + packages/react-devtools-inline/hookNames.js | 1 + .../react-devtools-inline/src/hookNames.js | 15 +++ .../react-devtools-inline/webpack.config.js | 1 + .../Components/FetchFileWithCachingContext.js | 9 +- .../HookNamesModuleLoaderContext.js | 21 ++++ .../Components/InspectedElementContext.js | 59 ++++++----- .../Components/InspectedElementHooksTree.js | 25 +++-- .../src/devtools/views/DevTools.js | 99 ++++++++++--------- packages/react-devtools-shell/src/devtools.js | 9 ++ 10 files changed, 153 insertions(+), 90 deletions(-) create mode 100644 packages/react-devtools-inline/hookNames.js create mode 100644 packages/react-devtools-inline/src/hookNames.js create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext.js diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index f8d24a72118e5..4670e9a53fce3 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -287,6 +287,9 @@ function createPanelIfReactLoaded() { }; } + const hookNamesModuleLoaderFunction = () => + import('react-devtools-shared/src/hooks/parseHookNames'); + root = createRoot(document.createElement('div')); render = (overrideTab = mostRecentOverrideTab) => { @@ -298,6 +301,7 @@ function createPanelIfReactLoaded() { componentsPortalContainer, enabledInspectedElementContextMenu: true, fetchFileWithCaching, + hookNamesModuleLoaderFunction, overrideTab, profilerPortalContainer, showTabBar: false, 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/src/hookNames.js b/packages/react-devtools-inline/src/hookNames.js new file mode 100644 index 0000000000000..e70424bcdaca3 --- /dev/null +++ b/packages/react-devtools-inline/src/hookNames.js @@ -0,0 +1,15 @@ +/** @flow */ + +import { + parseHookNames, + parseSourceAndMetadata, + prefetchSourceFiles, + purgeCachedMetadata, +} from 'react-devtools-shared/src/hooks/parseHookNames'; + +export { + parseHookNames, + parseSourceAndMetadata, + prefetchSourceFiles, + purgeCachedMetadata, +}; diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 17b008c452d88..bc38a8792e4c4 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -37,6 +37,7 @@ module.exports = { entry: { backend: './src/backend.js', frontend: './src/frontend.js', + hookNames: './src/hookNames.js', }, output: { path: __dirname + '/dist', diff --git a/packages/react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext.js b/packages/react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext.js index 210a3ec83239b..4647ebe9bf76b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext.js @@ -1,4 +1,11 @@ -// @flow +/** + * 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'; 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..a42bc4c7c030e --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext.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 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; + +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 c5ab69ada2264..54b15698614b9 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js @@ -32,6 +32,7 @@ import { } from 'react-devtools-shared/src/hookNamesCache'; 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'; @@ -60,13 +61,6 @@ export const InspectedElementContext = createContext( const POLL_INTERVAL = 1000; -// parseHookNames has a lot of code. -// Embedding it into a build makes the build large. -// This component uses Suspense to lazily import() it only if the feature will be used. -function loadHookNamesModuleLoaderFunction() { - return import('react-devtools-shared/src/hooks/parseHookNames'); -} - export type Props = {| children: ReactNodeList, |}; @@ -78,6 +72,11 @@ export function InspectedElementContextController({children}: Props) { 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. + const hookNamesModuleLoader = useContext(HookNamesModuleLoaderContext); + const refresh = useCacheRefresh(); // Temporarily stores most recently-inspected (hydrated) path. @@ -127,31 +126,31 @@ export function InspectedElementContextController({children}: Props) { inspectedElement = inspectElement(element, state.path, store, bridge); if (enableNamedHooksFeature) { - if (parseHookNames || alreadyLoadedHookNames) { - const loadHookNamesModule = loadModule( - loadHookNamesModuleLoaderFunction, - ); - if (loadHookNamesModule !== null) { - const { - parseHookNames: loadHookNamesFunction, - prefetchSourceFiles, - purgeCachedMetadata, - } = loadHookNamesModule; + if (typeof hookNamesModuleLoader === 'function') { + if (parseHookNames || alreadyLoadedHookNames) { + const hookNamesModule = loadModule(hookNamesModuleLoader); + if (hookNamesModule !== null) { + const { + parseHookNames: loadHookNamesFunction, + prefetchSourceFiles, + purgeCachedMetadata, + } = hookNamesModule; - purgeCachedMetadataRef.current = purgeCachedMetadata; - prefetchSourceFilesRef.current = prefetchSourceFiles; + purgeCachedMetadataRef.current = purgeCachedMetadata; + prefetchSourceFilesRef.current = prefetchSourceFiles; - if ( - inspectedElement !== null && - inspectedElement.hooks !== null && - loadHookNamesFunction !== null - ) { - hookNames = loadHookNames( - element, - inspectedElement.hooks, - loadHookNamesFunction, - fetchFileWithCaching, - ); + if ( + inspectedElement !== null && + inspectedElement.hooks !== null && + loadHookNamesFunction !== null + ) { + hookNames = loadHookNames( + element, + inspectedElement.hooks, + loadHookNamesFunction, + fetchFileWithCaching, + ); + } } } } 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 2a9d8ef2f7588..fb80b0152968e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js @@ -26,6 +26,7 @@ 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'; @@ -65,6 +66,8 @@ export function InspectedElementHooksTree({ toggleParseHookNames(); }; + const hookNamesModuleLoader = useContext(HookNamesModuleLoaderContext); + const hookParsingFailed = parseHookNames && hookNames === null; let toggleTitle; @@ -85,16 +88,18 @@ export function InspectedElementHooksTree({
hooks
- {enableNamedHooksFeature && (!parseHookNames || hookParsingFailed) && ( - - - - )} + {enableNamedHooksFeature && + typeof hookNamesModuleLoader === 'function' && + (!parseHookNames || hookParsingFailed) && ( + + + + )} diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index f3507afaf4c97..5b5dfbfa02bee 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -28,6 +28,7 @@ import {SettingsContextController} from './Settings/SettingsContext'; import {TreeContextController} from './Components/TreeContext'; import ViewElementSourceContext from './Components/ViewElementSourceContext'; import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext'; +import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; import {ProfilerContextController} from './Profiler/ProfilerContext'; import {SchedulingProfilerContextController} from 'react-devtools-scheduling-profiler/src/SchedulingProfilerContext'; import {ModalDialogContextController} from './ModalDialog'; @@ -42,12 +43,10 @@ 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 {FetchFileWithCaching} from './Components/FetchFileWithCachingContext'; +import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext'; 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'; @@ -56,9 +55,6 @@ export type ViewElementSource = ( id: number, inspectedElement: InspectedElement, ) => void; -export type LoadHookNamesFunction = ( - hooksTree: HooksTree, -) => Thenable; export type ViewAttributeSource = ( id: number, path: Array, @@ -102,6 +98,7 @@ 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, + hookNamesModuleLoaderFunction?: ?HookNamesModuleLoaderFunction, |}; const componentsTab = { @@ -127,6 +124,7 @@ export default function DevTools({ defaultTab = 'components', enabledInspectedElementContextMenu = false, fetchFileWithCaching, + hookNamesModuleLoaderFunction, overrideTab, profilerPortalContainer, showTabBar = false, @@ -244,52 +242,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-shell/src/devtools.js b/packages/react-devtools-shell/src/devtools.js index fda4c91b74ffe..8b172825357ec 100644 --- a/packages/react-devtools-shell/src/devtools.js +++ b/packages/react-devtools-shell/src/devtools.js @@ -10,6 +10,10 @@ 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. +// $FlowFixMe +__webpack_public_path__ = '/dist/'; // eslint-disable-line no-undef + const iframe = ((document.getElementById('target'): any): HTMLIFrameElement); const {contentDocument, contentWindow} = iframe; @@ -50,6 +54,10 @@ mountButton.addEventListener('click', function() { } }); +function hookNamesModuleLoaderFunction() { + return import('react-devtools-inline/hookNames'); +} + inject('dist/app.js', () => { initDevTools({ connect(cb) { @@ -58,6 +66,7 @@ inject('dist/app.js', () => { createElement(DevTools, { browserTheme: 'light', enabledInspectedElementContextMenu: true, + hookNamesModuleLoaderFunction, showTabBar: true, warnIfLegacyBackendDetected: true, warnIfUnsupportedVersionDetected: true, From 398dacdee4a3d8156a78a24277adffa2be109acb Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 8 Sep 2021 13:30:03 -0400 Subject: [PATCH 4/8] Removed named hooks prefetch step since we are using cached network responses. --- .../react-devtools-inline/src/hookNames.js | 8 +- .../Components/InspectedElementContext.js | 8 - .../src/hooks/parseHookNames/index.js | 8 +- .../parseHookNames/loadSourceAndMetadata.js | 144 +++++------------- 4 files changed, 38 insertions(+), 130 deletions(-) diff --git a/packages/react-devtools-inline/src/hookNames.js b/packages/react-devtools-inline/src/hookNames.js index e70424bcdaca3..7436ef7d01dba 100644 --- a/packages/react-devtools-inline/src/hookNames.js +++ b/packages/react-devtools-inline/src/hookNames.js @@ -3,13 +3,7 @@ import { parseHookNames, parseSourceAndMetadata, - prefetchSourceFiles, purgeCachedMetadata, } from 'react-devtools-shared/src/hooks/parseHookNames'; -export { - parseHookNames, - parseSourceAndMetadata, - prefetchSourceFiles, - purgeCachedMetadata, -}; +export {parseHookNames, parseSourceAndMetadata, purgeCachedMetadata}; 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 54b15698614b9..014c3ccaa5abd 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js @@ -116,7 +116,6 @@ export function InspectedElementContextController({children}: Props) { setParseHookNames(parseHookNamesByDefault || alreadyLoadedHookNames); } - const prefetchSourceFilesRef = useRef(null); const purgeCachedMetadataRef = useRef(null); // Don't load a stale element from the backend; it wastes bridge bandwidth. @@ -132,12 +131,10 @@ export function InspectedElementContextController({children}: Props) { if (hookNamesModule !== null) { const { parseHookNames: loadHookNamesFunction, - prefetchSourceFiles, purgeCachedMetadata, } = hookNamesModule; purgeCachedMetadataRef.current = purgeCachedMetadata; - prefetchSourceFilesRef.current = prefetchSourceFiles; if ( inspectedElement !== null && @@ -185,11 +182,6 @@ export function InspectedElementContextController({children}: Props) { inspectedElementRef.current !== inspectedElement ) { inspectedElementRef.current = inspectedElement; - - const prefetchSourceFiles = prefetchSourceFilesRef.current; - if (typeof prefetchSourceFiles === 'function') { - prefetchSourceFiles(inspectedElement.hooks, fetchFileWithCaching); - } } }, [inspectedElement]); diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/index.js b/packages/react-devtools-shared/src/hooks/parseHookNames/index.js index 6a5ff1efebd87..a400a021a91fa 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/index.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/index.js @@ -15,16 +15,10 @@ import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/view 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 240c9dc8bde5f..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,7 +54,6 @@ import { withSyncPerformanceMark, } from 'react-devtools-shared/src/PerformanceMarks'; -import type {LRUCache} from 'react-devtools-shared/src/types'; import type { HooksNode, HookSource, @@ -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); - } - } -} From f4c7df5a7743e603f31344556e6deea277f2b24f Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 8 Sep 2021 15:13:43 -0400 Subject: [PATCH 5/8] Added TODO to follow up on HookNamesModuleLoader param Ideally we can remove this param once the Webpack 5 upgrade has finished. --- packages/react-devtools-extensions/src/main.js | 1 + .../devtools/views/Components/HookNamesModuleLoaderContext.js | 1 + .../src/devtools/views/Components/InspectedElementContext.js | 1 + packages/react-devtools-shared/src/devtools/views/DevTools.js | 1 + packages/react-devtools-shell/src/devtools.js | 2 ++ 5 files changed, 6 insertions(+) diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 4670e9a53fce3..30318c1249e8a 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -287,6 +287,7 @@ function createPanelIfReactLoaded() { }; } + // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. const hookNamesModuleLoaderFunction = () => import('react-devtools-shared/src/hooks/parseHookNames'); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext.js b/packages/react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext.js index a42bc4c7c030e..c9a09e64c0e19 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext.js @@ -15,6 +15,7 @@ import typeof * as ParseHookNamesModule from 'react-devtools-shared/src/hooks/pa 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'; 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 014c3ccaa5abd..c02dc5bf9263a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js @@ -75,6 +75,7 @@ export function InspectedElementContextController({children}: Props) { // 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(); diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 5b5dfbfa02bee..6461441710ec5 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -98,6 +98,7 @@ 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, + // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. hookNamesModuleLoaderFunction?: ?HookNamesModuleLoaderFunction, |}; diff --git a/packages/react-devtools-shell/src/devtools.js b/packages/react-devtools-shell/src/devtools.js index 8b172825357ec..7b4171851c1be 100644 --- a/packages/react-devtools-shell/src/devtools.js +++ b/packages/react-devtools-shell/src/devtools.js @@ -11,6 +11,7 @@ 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 @@ -54,6 +55,7 @@ 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'); } From 22d2eb2c0d51a5078e63a6faaf75bff3793257eb Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 8 Sep 2021 17:44:02 -0400 Subject: [PATCH 6/8] Fixed incorrect path in dynamic import() --- packages/react-devtools-extensions/src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 30318c1249e8a..8781d6e22dbbc 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -289,7 +289,7 @@ function createPanelIfReactLoaded() { // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. const hookNamesModuleLoaderFunction = () => - import('react-devtools-shared/src/hooks/parseHookNames'); + import('react-devtools-inline/hookNames'); root = createRoot(document.createElement('div')); From 7c8609dbc4cbf64496c1e22640f08b9fbc9afed4 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 8 Sep 2021 17:44:20 -0400 Subject: [PATCH 7/8] Added instructions to react-devtools-inline README for new import() factory function --- packages/react-devtools-inline/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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. From e89d47d28713aec4faddc4109d8c9a841f5ed19f Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 9 Sep 2021 15:11:16 -0400 Subject: [PATCH 8/8] Replaced test shell Webpack 'cheap-module-eval-source-map' source-map setting with 'cheap-source-map' This fixes named hooks for the test shell and doesn't seem to slow things down noticeably either. --- packages/react-devtools-shell/webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-shell/webpack.config.js b/packages/react-devtools-shell/webpack.config.js index efe69ca1a49ba..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',