From 68f2f8f6c0a8c453fa7f89b0aec39a015151c986 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Tue, 7 Jun 2022 19:34:32 +0800 Subject: [PATCH] refactor: avoid using async-await in client module, fix ESLint errors --- .eslintrc.js | 1 + .../src/remark/admonitions/index.ts | 8 +- .../src/remark/transformImage/index.ts | 28 +- .../src/remark/transformLinks/index.ts | 41 +- packages/docusaurus-migrate/bin/index.mjs | 8 +- .../src/__tests__/lastUpdate.test.ts | 6 +- .../src/lastUpdate.ts | 2 +- .../docusaurus-plugin-pwa/src/registerSw.ts | 385 ++++++++---------- .../src/renderReloadPopup.tsx | 7 +- .../src/theme/Admonition/index.tsx | 11 +- packages/docusaurus/bin/docusaurus.mjs | 2 +- website/_dogfooding/clientModuleExample.ts | 4 +- website/docs/api/plugins/plugin-pwa.md | 2 +- website/src/components/Versions.tsx | 18 +- 14 files changed, 246 insertions(+), 277 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c24b8215bd2d..03a67afe1e1e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -191,6 +191,7 @@ module.exports = { 'no-template-curly-in-string': WARNING, 'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}], 'no-useless-escape': WARNING, + 'no-void': [ERROR, {allowAsStatement: true}], 'prefer-destructuring': WARNING, 'prefer-named-capture-group': WARNING, 'prefer-template': WARNING, diff --git a/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts b/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts index d423e6e793ea..d6393b8c4529 100644 --- a/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/admonitions/index.ts @@ -70,7 +70,11 @@ const plugin: Plugin = function plugin( } const now = eat.now(); - const [opening, keyword, title] = match; + const [opening, keyword, title] = match as string[] as [ + string, + string, + string, + ]; const food = []; const content = []; @@ -169,7 +173,7 @@ const plugin: Plugin = function plugin( visit( root, (node: unknown): node is Literal => - (node as Literal)?.type !== admonitionNodeType, + (node as Literal | undefined)?.type !== admonitionNodeType, (node: Literal) => { if (node.value) { node.value = node.value.replace(escapeTag, options.tag); diff --git a/packages/docusaurus-mdx-loader/src/remark/transformImage/index.ts b/packages/docusaurus-mdx-loader/src/remark/transformImage/index.ts index d6988b71ef4d..06892e3ed0b9 100644 --- a/packages/docusaurus-mdx-loader/src/remark/transformImage/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/transformImage/index.ts @@ -21,6 +21,7 @@ import visit from 'unist-util-visit'; import escapeHtml from 'escape-html'; import sizeOf from 'image-size'; import type {Transformer} from 'unified'; +import type {Parent} from 'unist'; import type {Image, Literal} from 'mdast'; const { @@ -36,12 +37,13 @@ type Context = PluginOptions & { filePath: string; }; +type Target = [node: Image, index: number, parent: Parent]; + async function toImageRequireNode( - node: Image, + [node, index, parent]: Target, imagePath: string, filePath: string, ) { - const jsxNode = node as Literal & Partial; let relativeImagePath = posixPath( path.relative(path.dirname(filePath), imagePath), ); @@ -75,12 +77,12 @@ ${(err as Error).message}`; } } - Object.keys(jsxNode).forEach( - (key) => delete jsxNode[key as keyof typeof jsxNode], - ); + const jsxNode: Literal = { + type: 'jsx', + value: ``, + }; - (jsxNode as Literal).type = 'jsx'; - jsxNode.value = ``; + parent.children.splice(index, 1, jsxNode); } async function ensureImageFileExist(imagePath: string, sourceFilePath: string) { @@ -129,7 +131,8 @@ async function getImageAbsolutePath( return imageFilePath; } -async function processImageNode(node: Image, context: Context) { +async function processImageNode(target: Target, context: Context) { + const [node] = target; if (!node.url) { throw new Error( `Markdown image URL is mandatory in "${toMessageRelativeFilePath( @@ -151,15 +154,18 @@ async function processImageNode(node: Image, context: Context) { // We try to convert image urls without protocol to images with require calls // going through webpack ensures that image assets exist at build time const imagePath = await getImageAbsolutePath(parsedUrl.pathname, context); - await toImageRequireNode(node, imagePath, context.filePath); + await toImageRequireNode(target, imagePath, context.filePath); } export default function plugin(options: PluginOptions): Transformer { return async (root, vfile) => { const promises: Promise[] = []; - visit(root, 'image', (node: Image) => { + visit(root, 'image', (node: Image, index, parent) => { promises.push( - processImageNode(node, {...options, filePath: vfile.path!}), + processImageNode([node, index, parent!], { + ...options, + filePath: vfile.path!, + }), ); }); await Promise.all(promises); diff --git a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts index 59e1b4117bf5..33602c0d26f4 100644 --- a/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/transformLinks/index.ts @@ -19,6 +19,7 @@ import visit from 'unist-util-visit'; import escapeHtml from 'escape-html'; import {stringifyContent} from '../utils'; import type {Transformer} from 'unified'; +import type {Parent} from 'unist'; import type {Link, Literal} from 'mdast'; const { @@ -34,16 +35,20 @@ type Context = PluginOptions & { filePath: string; }; +type Target = [node: Link, index: number, parent: Parent]; + /** * Transforms the link node to a JSX `` element with a `require()` call. */ -function toAssetRequireNode(node: Link, assetPath: string, filePath: string) { - const jsxNode = node as Literal & Partial; - let relativeAssetPath = posixPath( - path.relative(path.dirname(filePath), assetPath), - ); +function toAssetRequireNode( + [node, index, parent]: Target, + assetPath: string, + filePath: string, +) { // require("assets/file.pdf") means requiring from a package called assets - relativeAssetPath = `./${relativeAssetPath}`; + const relativeAssetPath = `./${posixPath( + path.relative(path.dirname(filePath), assetPath), + )}`; const parsedUrl = url.parse(node.url); const hash = parsedUrl.hash ?? ''; @@ -60,12 +65,12 @@ function toAssetRequireNode(node: Link, assetPath: string, filePath: string) { const children = stringifyContent(node); const title = node.title ? ` title="${escapeHtml(node.title)}"` : ''; - Object.keys(jsxNode).forEach( - (key) => delete jsxNode[key as keyof typeof jsxNode], - ); + const jsxNode: Literal = { + type: 'jsx', + value: `${children}`, + }; - (jsxNode as Literal).type = 'jsx'; - jsxNode.value = `${children}`; + parent.children.splice(index, 1, jsxNode); } async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) { @@ -106,7 +111,8 @@ async function getAssetAbsolutePath( return null; } -async function processLinkNode(node: Link, context: Context) { +async function processLinkNode(target: Target, context: Context) { + const [node] = target; if (!node.url) { // Try to improve error feedback // see https://github.com/facebook/docusaurus/issues/3309#issuecomment-690371675 @@ -138,15 +144,20 @@ async function processLinkNode(node: Link, context: Context) { context, ); if (assetPath) { - toAssetRequireNode(node, assetPath, context.filePath); + toAssetRequireNode(target, assetPath, context.filePath); } } export default function plugin(options: PluginOptions): Transformer { return async (root, vfile) => { const promises: Promise[] = []; - visit(root, 'link', (node: Link) => { - promises.push(processLinkNode(node, {...options, filePath: vfile.path!})); + visit(root, 'link', (node: Link, index, parent) => { + promises.push( + processLinkNode([node, index, parent!], { + ...options, + filePath: vfile.path!, + }), + ); }); await Promise.all(promises); }; diff --git a/packages/docusaurus-migrate/bin/index.mjs b/packages/docusaurus-migrate/bin/index.mjs index 0ace2050c1ca..8be0d9d3bc9d 100755 --- a/packages/docusaurus-migrate/bin/index.mjs +++ b/packages/docusaurus-migrate/bin/index.mjs @@ -34,19 +34,19 @@ cli .option('--mdx', 'try to migrate MD to MDX too') .option('--page', 'try to migrate pages too') .description('Migrate between versions of Docusaurus website.') - .action((siteDir = '.', newDir = '.', {mdx, page} = {}) => { + .action(async (siteDir = '.', newDir = '.', {mdx, page} = {}) => { const sitePath = path.resolve(siteDir); const newSitePath = path.resolve(newDir); - migrateDocusaurusProject(sitePath, newSitePath, mdx, page); + await migrateDocusaurusProject(sitePath, newSitePath, mdx, page); }); cli .command('mdx [siteDir] [newDir]') .description('Migrate markdown files to MDX.') - .action((siteDir = '.', newDir = '.') => { + .action(async (siteDir = '.', newDir = '.') => { const sitePath = path.resolve(siteDir); const newSitePath = path.resolve(newDir); - migrateMDToMDX(sitePath, newSitePath); + await migrateMDToMDX(sitePath, newSitePath); }); cli.parse(process.argv); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts index 47aff760539e..53f2827f2dc7 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/lastUpdate.test.ts @@ -22,7 +22,7 @@ describe('getFileLastUpdate', () => { const lastUpdateData = await getFileLastUpdate(existingFilePath); expect(lastUpdateData).not.toBeNull(); - const {author, timestamp} = lastUpdateData; + const {author, timestamp} = lastUpdateData!; expect(author).not.toBeNull(); expect(typeof author).toBe('string'); @@ -38,7 +38,7 @@ describe('getFileLastUpdate', () => { const lastUpdateData = await getFileLastUpdate(filePathWithSpace); expect(lastUpdateData).not.toBeNull(); - const {author, timestamp} = lastUpdateData; + const {author, timestamp} = lastUpdateData!; expect(author).not.toBeNull(); expect(typeof author).toBe('string'); @@ -61,8 +61,6 @@ describe('getFileLastUpdate', () => { expect(consoleMock).toHaveBeenLastCalledWith( expect.stringMatching(/because the file does not exist./), ); - await expect(getFileLastUpdate(null)).resolves.toBeNull(); - await expect(getFileLastUpdate(undefined)).resolves.toBeNull(); consoleMock.mockRestore(); }); diff --git a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts index a0eb4d775997..110dff5ab7a1 100644 --- a/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts +++ b/packages/docusaurus-plugin-content-docs/src/lastUpdate.ts @@ -16,7 +16,7 @@ let showedGitRequirementError = false; let showedFileNotTrackedError = false; export async function getFileLastUpdate( - filePath?: string, + filePath: string, ): Promise<{timestamp: number; author: string} | null> { if (!filePath) { return null; diff --git a/packages/docusaurus-plugin-pwa/src/registerSw.ts b/packages/docusaurus-plugin-pwa/src/registerSw.ts index 7ccb4a56a233..7c1a729aebd5 100644 --- a/packages/docusaurus-plugin-pwa/src/registerSw.ts +++ b/packages/docusaurus-plugin-pwa/src/registerSw.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import {createStorageSlot} from '@docusaurus/theme-common'; // First: read the env variables (provided by Webpack) @@ -15,41 +16,45 @@ const PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES = process.env const PWA_DEBUG = process.env.PWA_DEBUG; /* eslint-enable prefer-destructuring */ -const debug = PWA_DEBUG; // Shortcut - -const MAX_MOBILE_WIDTH = 940; +const MAX_MOBILE_WIDTH = 996; const AppInstalledEventFiredStorage = createStorageSlot( 'docusaurus.pwa.event.appInstalled.fired', ); -async function clearRegistrations() { - const registrations = await navigator.serviceWorker.getRegistrations(); - if (debug) { - console.log( - `[Docusaurus-PWA][registerSw]: will unregister all service worker registrations`, - registrations, - ); +declare global { + interface Navigator { + getInstalledRelatedApps: () => Promise<{platform: string}[]>; + connection?: {effectiveType: string; saveData: boolean}; } - await Promise.all( - registrations.map(async (registration) => { - const result = await registration.unregister(); - if (debug) { - console.log( - `[Docusaurus-PWA][registerSw]: unregister() service worker registration`, - registrations, - result, - ); - } - }), - ); - if (debug) { - console.log( - `[Docusaurus-PWA][registerSw]: unregistered all service worker registrations`, - registrations, - ); +} + +function debugLog(msg: string, obj?: unknown) { + if (PWA_DEBUG) { + if (typeof obj === 'undefined') { + console.log(`[Docusaurus-PWA][registerSw]: ${msg}`); + } else { + console.log(`[Docusaurus-PWA][registerSw]: ${msg}`, obj); + } } - window.location.reload(); +} + +function clearRegistrations() { + return navigator.serviceWorker.getRegistrations().then((registrations) => { + debugLog('will unregister all service workers', {registrations}); + return Promise.all( + registrations.map((registration) => + registration + .unregister() + .then((result) => + debugLog('unregister service worker', {registration, result}), + ), + ), + ).then(() => { + debugLog('unregistered all service workers', {registrations}); + window.location.reload(); + }); + }); } /* @@ -61,23 +66,19 @@ https://stackoverflow.com/questions/51735869/check-if-user-has-already-installed - getInstalledRelatedApps() is only supported in recent Chrome and does not seem to reliable either https://github.com/WICG/get-installed-related-apps - display-mode: standalone is not exactly the same concept, but looks like a decent fallback https://petelepage.com/blog/2019/07/is-my-pwa-installed/ */ -async function isAppInstalledEventFired() { +function getIsAppInstalledEventFired() { return AppInstalledEventFiredStorage.get() === 'true'; } -declare global { - interface Navigator { - getInstalledRelatedApps: () => Promise<{platform: string}[]>; - connection?: {effectiveType: string; saveData: boolean}; +function getIsAppInstalledRelatedApps() { + if (!('getInstalledRelatedApps' in window.navigator)) { + return false; } -} - -async function isAppInstalledRelatedApps() { - if ('getInstalledRelatedApps' in window.navigator) { - const relatedApps = await navigator.getInstalledRelatedApps(); - return relatedApps.some((app) => app.platform === 'webapp'); - } - return false; + return navigator + .getInstalledRelatedApps() + .then((relatedApps) => + relatedApps.some((app) => app.platform === 'webapp'), + ); } function isStandaloneDisplayMode() { return window.matchMedia('(display-mode: standalone)').matches; @@ -87,53 +88,37 @@ const OfflineModeActivationStrategiesImplementations = { always: () => true, mobile: () => window.innerWidth <= MAX_MOBILE_WIDTH, saveData: () => !!navigator.connection?.saveData, - appInstalled: async () => { - const installedEventFired = await isAppInstalledEventFired(); - const installedRelatedApps = await isAppInstalledRelatedApps(); - return installedEventFired || installedRelatedApps; - }, + appInstalled: () => + getIsAppInstalledEventFired() || getIsAppInstalledRelatedApps(), standalone: () => isStandaloneDisplayMode(), queryString: () => new URLSearchParams(window.location.search).get('offlineMode') === 'true', }; -async function isStrategyActive( - strategyName: keyof typeof OfflineModeActivationStrategiesImplementations, -) { - return OfflineModeActivationStrategiesImplementations[strategyName](); -} - -async function getActiveStrategies() { - const activeStrategies = await Promise.all( - PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.map(async (strategyName) => { - const isActive = await isStrategyActive(strategyName); - return isActive ? strategyName : undefined; - }), - ); - return activeStrategies.filter(Boolean); +function getActiveStrategies() { + return Promise.all( + PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.map((strategyName) => + Promise.resolve( + OfflineModeActivationStrategiesImplementations[strategyName](), + ).then((isActive) => (isActive ? strategyName : undefined)), + ), + ).then((activeStrategies) => activeStrategies.filter(Boolean)); } -async function isOfflineModeEnabled() { - const activeStrategies = await getActiveStrategies(); - const enabled = activeStrategies.length > 0; - if (debug) { - const logObject = { - activeStrategies, - availableStrategies: PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES, - }; - if (enabled) { - console.log( - '[Docusaurus-PWA][registerSw]: offline mode enabled, because of activation strategies', - logObject, - ); - } else { - console.log( - '[Docusaurus-PWA][registerSw]: offline mode disabled, because none of the offlineModeActivationStrategies could be used', - logObject, - ); - } - } - return enabled; +function getIsOfflineModeEnabled() { + return getActiveStrategies().then((activeStrategies) => { + const enabled = activeStrategies.length > 0; + debugLog( + enabled + ? 'offline mode enabled, because of activation strategies' + : 'offline mode disabled, because none of the offlineModeActivationStrategies could be used', + { + activeStrategies, + availableStrategies: PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES, + }, + ); + return enabled; + }); } function createServiceWorkerUrl(params: object) { @@ -141,170 +126,118 @@ function createServiceWorkerUrl(params: object) { const url = `${PWA_SERVICE_WORKER_URL}?params=${encodeURIComponent( paramsQueryString, )}`; - if (debug) { - console.log(`[Docusaurus-PWA][registerSw]: service worker url`, { - url, - params, - }); - } + debugLog('service worker url', {url, params}); return url; } -async function registerSW() { - const {Workbox} = await import('workbox-window'); - - const offlineMode = await isOfflineModeEnabled(); - - const url = createServiceWorkerUrl({offlineMode, debug}); - const wb = new Workbox(url); - - const registration = await wb.register(); - - const sendSkipWaiting = () => wb.messageSW({type: 'SKIP_WAITING'}); - - const handleServiceWorkerWaiting = async () => { - if (debug) { - console.log('[Docusaurus-PWA][registerSw]: handleServiceWorkerWaiting'); - } - // Immediately load new service worker when files aren't cached - if (!offlineMode) { - sendSkipWaiting(); - } else { - const renderReloadPopup = (await import('./renderReloadPopup')).default; - await renderReloadPopup({ - onReload() { - wb.addEventListener('controlling', () => { - window.location.reload(); - }); - sendSkipWaiting(); - }, - }); - } - }; - - if (debug && registration) { - if (registration.active) { - console.log( - '[Docusaurus-PWA][registerSw]: registration.active', - registration, - ); - } - if (registration.installing) { - console.log( - '[Docusaurus-PWA][registerSw]: registration.installing', - registration, - ); - } - if (registration.waiting) { - console.log( - '[Docusaurus-PWA][registerSw]: registration.waiting', - registration, +function registerSW() { + return Promise.all([ + import('workbox-window'), + getIsOfflineModeEnabled(), + ]).then(([{Workbox}, offlineMode]) => { + const url = createServiceWorkerUrl({offlineMode, debug: PWA_DEBUG}); + const wb = new Workbox(url); + const sendSkipWaiting = () => wb.messageSW({type: 'SKIP_WAITING'}); + const handleServiceWorkerWaiting = () => { + debugLog('handleServiceWorkerWaiting'); + // Immediately load new service worker when files aren't cached + if (!offlineMode) { + return sendSkipWaiting(); + } + return import('./renderReloadPopup').then( + ({default: renderReloadPopup}) => + renderReloadPopup({ + onReload() { + wb.addEventListener('controlling', () => { + window.location.reload(); + }); + return sendSkipWaiting(); + }, + }), ); - } - } + }; - // Update the current service worker when the next one has finished - // installing and transitions to waiting state. - wb.addEventListener('waiting', (event) => { - if (debug) { - console.log('[Docusaurus-PWA][registerSw]: event waiting', event); - } - handleServiceWorkerWaiting(); - }); + // Update the current service worker when the next one has finished + // installing and transitions to waiting state. + wb.addEventListener('waiting', (event) => { + debugLog('event waiting', {event}); + void handleServiceWorkerWaiting(); + }); - // Update current service worker if the next one finishes installing and - // moves to waiting state in another tab. - // @ts-expect-error: not present in the API typings anymore - wb.addEventListener('externalwaiting', (event) => { - if (debug) { - console.log('[Docusaurus-PWA][registerSw]: event externalwaiting', event); - } - handleServiceWorkerWaiting(); - }); + // Update current service worker if the next one finishes installing and + // moves to waiting state in another tab. + // @ts-expect-error: not present in the API typings anymore + wb.addEventListener('externalwaiting', (event) => { + debugLog('event externalwaiting', {event}); + void handleServiceWorkerWaiting(); + }); - // Update service worker if the next one is already in the waiting state. - // This happens when the user doesn't click on `reload` in the popup. - if (registration?.waiting) { - await handleServiceWorkerWaiting(); - } + return wb.register().then((registration) => { + if (registration) { + if (registration.active) { + debugLog('registration.active', {registration}); + } + if (registration.installing) { + debugLog('registration.installing', {registration}); + } + if (registration.waiting) { + debugLog('registration.waiting', {registration}); + // Update service worker if the next one is already in the waiting + // state. This happens when the user doesn't click on `reload` in + // the popup. + return handleServiceWorkerWaiting(); + } + } + return undefined; + }); + }); } // TODO these events still works in chrome but have been removed from the spec // in 2019! See https://github.com/w3c/manifest/pull/836 function addLegacyAppInstalledEventsListeners() { - if (typeof window !== 'undefined') { - if (debug) { - console.log( - '[Docusaurus-PWA][registerSw]: addLegacyAppInstalledEventsListeners', - ); - } - - window.addEventListener('appinstalled', async (event) => { - if (debug) { - console.log('[Docusaurus-PWA][registerSw]: event appinstalled', event); - } - - AppInstalledEventFiredStorage.set('true'); - if (debug) { - console.log( - "[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.set('true')", - ); - } - - // After the app is installed, we register a service worker with the path - // `/sw?enabled`. Since the previous service worker was `/sw`, it'll be - // treated as a new one. The previous registration will need to be - // cleared, otherwise the reload popup will show. - await clearRegistrations(); - }); - - // TODO this event still works in chrome but has been removed from the spec - // in 2019!!! - window.addEventListener('beforeinstallprompt', async (event) => { - if (debug) { - console.log( - '[Docusaurus-PWA][registerSw]: event beforeinstallprompt', - event, - ); - } - // TODO instead of default browser install UI, show custom docusaurus - // prompt? - // event.preventDefault(); - if (debug) { - console.log( - '[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.get()', - AppInstalledEventFiredStorage.get(), - ); - } - if (AppInstalledEventFiredStorage.get()) { - AppInstalledEventFiredStorage.del(); - if (debug) { - console.log( - '[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.del()', - ); - } - // After uninstalling the app, if the user doesn't clear all data, then - // the previous service worker will continue serving cached files. We - // need to clear registrations and reload, otherwise the popup shows. - await clearRegistrations(); - } - }); + debugLog('addLegacyAppInstalledEventsListeners'); + + window.addEventListener('appinstalled', (event) => { + debugLog('event appinstalled', {event}); + AppInstalledEventFiredStorage.set('true'); + debugLog("AppInstalledEventFiredStorage.set('true')"); + // After the app is installed, we register a service worker with the path + // `/sw?enabled`. Since the previous service worker was `/sw`, it'll be + // treated as a new one. The previous registration will need to be + // cleared, otherwise the reload popup will show. + void clearRegistrations(); + }); - if (debug) { - console.log( - '[Docusaurus-PWA][registerSw]: legacy appinstalled and beforeinstallprompt event listeners installed', - ); + // TODO this event still works in chrome but has been removed from the spec + // in 2019!!! + window.addEventListener('beforeinstallprompt', (event) => { + debugLog('event beforeinstallprompt', {event}); + // TODO instead of default browser install UI, show custom docusaurus + // prompt? + // event.preventDefault(); + const appInstalledEventFired = AppInstalledEventFiredStorage.get(); + debugLog('AppInstalledEventFiredStorage.get()', {appInstalledEventFired}); + if (appInstalledEventFired) { + AppInstalledEventFiredStorage.del(); + debugLog('AppInstalledEventFiredStorage.del()'); + // After uninstalling the app, if the user doesn't clear all data, then + // the previous service worker will continue serving cached files. We + // need to clear registrations and reload, otherwise the popup shows. + void clearRegistrations(); } - } + }); + + debugLog( + 'legacy appinstalled and beforeinstallprompt event listeners installed', + ); } /* Init code to run on the client! */ -if (typeof window !== 'undefined') { - if (debug) { - console.log('[Docusaurus-PWA][registerSw]: debug mode enabled'); - } +if (ExecutionEnvironment.canUseDOM) { + debugLog('debug mode enabled'); if ('serviceWorker' in navigator) { // First: add the listeners asap/synchronously diff --git a/packages/docusaurus-plugin-pwa/src/renderReloadPopup.tsx b/packages/docusaurus-plugin-pwa/src/renderReloadPopup.tsx index 2958a7dee833..b814ae35ba63 100644 --- a/packages/docusaurus-plugin-pwa/src/renderReloadPopup.tsx +++ b/packages/docusaurus-plugin-pwa/src/renderReloadPopup.tsx @@ -20,8 +20,9 @@ const createContainer = () => { return container; }; -export default async function renderReloadPopup(props: Props): Promise { +export default function renderReloadPopup(props: Props): Promise { const container = getContainer() ?? createContainer(); - const ReloadPopup = (await import('@theme/PwaReloadPopup')).default; - ReactDOM.render(, container); + return import('@theme/PwaReloadPopup').then(({default: ReloadPopup}) => { + ReactDOM.render(, container); + }); } diff --git a/packages/docusaurus-theme-classic/src/theme/Admonition/index.tsx b/packages/docusaurus-theme-classic/src/theme/Admonition/index.tsx index eab72eda3d6e..aa64d0461842 100644 --- a/packages/docusaurus-theme-classic/src/theme/Admonition/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/Admonition/index.tsx @@ -141,18 +141,17 @@ const aliases = { warning: 'danger', } as const; -function getAdmonitionConfig( - unsafeType: Props['type'] | keyof typeof aliases, -): AdmonitionConfig { - const type = aliases[unsafeType as keyof typeof aliases] ?? unsafeType; - const config = AdmonitionConfigs[type]; +function getAdmonitionConfig(unsafeType: string): AdmonitionConfig { + const type = + (aliases as {[key: string]: Props['type']})[unsafeType] ?? unsafeType; + const config = (AdmonitionConfigs as {[key: string]: AdmonitionConfig})[type]; if (config) { return config; } console.warn( `No admonition config found for admonition type "${type}". Using Info as fallback.`, ); - return AdmonitionConfigs.info as AdmonitionConfig; + return AdmonitionConfigs.info; } // Workaround because it's difficult in MDX v1 to provide a MDX title as props diff --git a/packages/docusaurus/bin/docusaurus.mjs b/packages/docusaurus/bin/docusaurus.mjs index 8ae3d348ecf6..a384de9b7cfe 100755 --- a/packages/docusaurus/bin/docusaurus.mjs +++ b/packages/docusaurus/bin/docusaurus.mjs @@ -55,7 +55,7 @@ cli 'build website without minimizing JS bundles (default: false)', ) .action(async (siteDir, options) => { - build(await resolveDir(siteDir), options); + await build(await resolveDir(siteDir), options); }); cli diff --git a/website/_dogfooding/clientModuleExample.ts b/website/_dogfooding/clientModuleExample.ts index cd61e5a99e81..e7b673e74e94 100644 --- a/website/_dogfooding/clientModuleExample.ts +++ b/website/_dogfooding/clientModuleExample.ts @@ -18,8 +18,8 @@ function logPage( prevLocation: previousLocation, heading: document.getElementsByTagName('h1')[0]?.innerText, title: document.title, - description: ( - document.querySelector('meta[name="description"]') as HTMLMetaElement + description: document.querySelector( + 'meta[name="description"]', )?.content, htmlClassName: document.getElementsByTagName('html')[0]?.className, }); diff --git a/website/docs/api/plugins/plugin-pwa.md b/website/docs/api/plugins/plugin-pwa.md index a9647173182a..4a80f7a5eb51 100644 --- a/website/docs/api/plugins/plugin-pwa.md +++ b/website/docs/api/plugins/plugin-pwa.md @@ -120,7 +120,7 @@ Strategies used to turn the offline mode on: - `appInstalled`: activates for users having installed the site as an app (not 100% reliable) - `standalone`: activates for users running the app as standalone (often the case once a PWA is installed) - `queryString`: activates if queryString contains `offlineMode=true` (convenient for PWA debugging) -- `mobile`: activates for mobile users (width <= 940px) +- `mobile`: activates for mobile users (width <= 996px) - `saveData`: activates for users with `navigator.connection.saveData === true` - `always`: activates for all users diff --git a/website/src/components/Versions.tsx b/website/src/components/Versions.tsx index 3448563166d9..9542c88a8328 100644 --- a/website/src/components/Versions.tsx +++ b/website/src/components/Versions.tsx @@ -5,7 +5,13 @@ * LICENSE file in the root directory of this source tree. */ -import React, {useContext, useEffect, useState, type ReactNode} from 'react'; +import React, { + useContext, + useEffect, + useState, + useRef, + type ReactNode, +} from 'react'; import {useDocsPreferredVersion} from '@docusaurus/theme-common'; import {useVersions} from '@docusaurus/plugin-content-docs/client'; import Translate from '@docusaurus/Translate'; @@ -24,11 +30,21 @@ export function VersionsProvider({ children: ReactNode; }): JSX.Element { const [canaryVersion, setCanaryVersion] = useState(null); + const mounted = useRef(true); + useEffect(() => { + mounted.current = true; + return () => { + mounted.current = false; + }; + }, []); useEffect(() => { fetch('https://registry.npmjs.org/@docusaurus/core') .then((res) => res.json()) .then( (data: {versions: string[]; time: {[versionName: string]: string}}) => { + if (!mounted.current) { + return; + } const name = Object.keys(data.versions).at(-1)!; const time = data.time[name]; setCanaryVersion({name, time});