diff --git a/packages/docusaurus-module-type-aliases/src/index.d.ts b/packages/docusaurus-module-type-aliases/src/index.d.ts index b4a243ee96fb..29602072a49e 100644 --- a/packages/docusaurus-module-type-aliases/src/index.d.ts +++ b/packages/docusaurus-module-type-aliases/src/index.d.ts @@ -346,3 +346,10 @@ declare module '*.css' { const src: string; export default src; } + +interface Window { + docusaurus: { + prefetch: (url: string) => false | Promise; + preload: (url: string) => false | Promise; + }; +} diff --git a/packages/docusaurus/src/client/PendingNavigation.tsx b/packages/docusaurus/src/client/PendingNavigation.tsx index a60825cc5c7e..3c2cca17f7b8 100644 --- a/packages/docusaurus/src/client/PendingNavigation.tsx +++ b/packages/docusaurus/src/client/PendingNavigation.tsx @@ -62,7 +62,9 @@ class PendingNavigation extends React.Component { location: nextLocation, })!; - // Load data while the old screen remains. + // Load data while the old screen remains. Force preload instead of using + // `window.docusaurus`, because we want to avoid loading screen even when + // user is on saveData preload(nextLocation.pathname) .then(() => { this.routeUpdateCleanupCb?.(); diff --git a/packages/docusaurus/src/client/docusaurus.ts b/packages/docusaurus/src/client/docusaurus.ts index 3307bb6d392d..80fa879e30cb 100644 --- a/packages/docusaurus/src/client/docusaurus.ts +++ b/packages/docusaurus/src/client/docusaurus.ts @@ -12,8 +12,8 @@ import prefetchHelper from './prefetch'; import preloadHelper from './preload'; import flat from './flat'; -const fetched: {[key: string]: boolean} = {}; -const loaded: {[key: string]: boolean} = {}; +const fetched = new Set(); +const loaded = new Set(); declare global { // eslint-disable-next-line camelcase, no-underscore-dangle @@ -25,14 +25,14 @@ declare global { // If user is on slow or constrained connection. const isSlowConnection = () => - navigator.connection?.effectiveType.includes('2g') && + navigator.connection?.effectiveType.includes('2g') || navigator.connection?.saveData; const canPrefetch = (routePath: string) => - !isSlowConnection() && !loaded[routePath] && !fetched[routePath]; + !isSlowConnection() && !loaded.has(routePath) && !fetched.has(routePath); const canPreload = (routePath: string) => - !isSlowConnection() && !loaded[routePath]; + !isSlowConnection() && !loaded.has(routePath); const getChunkNamesToLoad = (path: string): string[] => Object.entries(routesChunkNames) @@ -46,12 +46,11 @@ const getChunkNamesToLoad = (path: string): string[] => .flatMap(([, routeChunks]) => Object.values(flat(routeChunks))); const docusaurus = { - prefetch: (routePath: string): boolean => { + prefetch(routePath: string): false | Promise { if (!canPrefetch(routePath)) { return false; } - // Prevent future duplicate prefetch of routePath. - fetched[routePath] = true; + fetched.add(routePath); // Find all webpack chunk names needed. const matches = matchRoutes(routes, routePath); @@ -61,32 +60,30 @@ const docusaurus = { ); // Prefetch all webpack chunk assets file needed. - chunkNamesNeeded.forEach((chunkName) => { - // "__webpack_require__.gca" is a custom function provided by - // ChunkAssetPlugin. Pass it the chunkName or chunkId you want to load and - // it will return the URL for that chunk. - // eslint-disable-next-line camelcase - const chunkAsset = __webpack_require__.gca(chunkName); - - // In some cases, webpack might decide to optimize further & hence the - // chunk assets are merged to another chunk/previous chunk. - // Hence, we can safely filter it out/don't need to load it. - if (chunkAsset && !/undefined/.test(chunkAsset)) { - prefetchHelper(chunkAsset); - } - }); - - return true; + return Promise.all( + chunkNamesNeeded.map((chunkName) => { + // "__webpack_require__.gca" is injected by ChunkAssetPlugin. Pass it + // the name of the chunk you want to load and it will return its URL. + // eslint-disable-next-line camelcase + const chunkAsset = __webpack_require__.gca(chunkName); + + // In some cases, webpack might decide to optimize further, leading to + // the chunk assets being merged to another chunk. In this case, we can + // safely filter it out and don't need to load it. + if (chunkAsset && !/undefined/.test(chunkAsset)) { + return prefetchHelper(chunkAsset); + } + return Promise.resolve(); + }), + ); }, - preload: (routePath: string): boolean => { + preload(routePath: string): false | Promise { if (!canPreload(routePath)) { return false; } - - loaded[routePath] = true; - preloadHelper(routePath); - return true; + loaded.add(routePath); + return preloadHelper(routePath); }, }; diff --git a/packages/docusaurus/src/client/exports/Link.tsx b/packages/docusaurus/src/client/exports/Link.tsx index a6760e7fdfce..3628e7495439 100644 --- a/packages/docusaurus/src/client/exports/Link.tsx +++ b/packages/docusaurus/src/client/exports/Link.tsx @@ -21,13 +21,6 @@ import {useBaseUrlUtils} from './useBaseUrl'; import {applyTrailingSlash} from '@docusaurus/utils-common'; import type {Props} from '@docusaurus/Link'; -import type docusaurus from '../docusaurus'; - -declare global { - interface Window { - docusaurus: typeof docusaurus; - } -} // TODO all this wouldn't be necessary if we used ReactRouter basename feature // We don't automatically add base urls to all links, diff --git a/packages/docusaurus/src/client/flat.ts b/packages/docusaurus/src/client/flat.ts index c015d106955d..8413842b8b76 100644 --- a/packages/docusaurus/src/client/flat.ts +++ b/packages/docusaurus/src/client/flat.ts @@ -25,18 +25,18 @@ export default function flat(target: ChunkNames): {[keyPath: string]: string} { const delimiter = '.'; const output: {[keyPath: string]: string} = {}; - function step(object: Tree, prefix?: string | number) { + function dfs(object: Tree, prefix?: string | number) { Object.entries(object).forEach(([key, value]) => { const newKey = prefix ? `${prefix}${delimiter}${key}` : key; if (isTree(value)) { - step(value, newKey); + dfs(value, newKey); } else { output[newKey] = value; } }); } - step(target); + dfs(target); return output; } diff --git a/packages/docusaurus/src/client/prefetch.ts b/packages/docusaurus/src/client/prefetch.ts index 5c28025eef1d..a7a58008a753 100644 --- a/packages/docusaurus/src/client/prefetch.ts +++ b/packages/docusaurus/src/client/prefetch.ts @@ -14,7 +14,7 @@ function supports(feature: string) { } } -function linkPrefetchStrategy(url: string) { +function linkPrefetchStrategy(url: string): Promise { return new Promise((resolve, reject) => { if (typeof document === 'undefined') { reject(); @@ -25,8 +25,8 @@ function linkPrefetchStrategy(url: string) { link.setAttribute('rel', 'prefetch'); link.setAttribute('href', url); - link.onload = resolve; - link.onerror = reject; + link.onload = () => resolve(); + link.onerror = () => reject(); const parentElement = document.getElementsByTagName('head')[0] ?? @@ -57,20 +57,6 @@ const supportedPrefetchStrategy = supports('prefetch') ? linkPrefetchStrategy : xhrPrefetchStrategy; -const preFetched: {[url: string]: boolean} = {}; - export default function prefetch(url: string): Promise { - return new Promise((resolve) => { - if (preFetched[url]) { - resolve(); - return; - } - - supportedPrefetchStrategy(url) - .then(() => { - resolve(); - preFetched[url] = true; - }) - .catch(() => {}); // 404s are logged to the console anyway. - }); + return supportedPrefetchStrategy(url).catch(() => {}); // 404s are logged to the console anyway. }