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',