refactor(pwa): simplify registerSW code, fix ESLint errors (#7579)

This commit is contained in:
Joshua Chen 2022-06-07 21:42:17 +08:00 committed by GitHub
parent bada5c11cc
commit 7869e74fd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 204 additions and 247 deletions

1
.eslintrc.js vendored
View file

@ -191,6 +191,7 @@ module.exports = {
'no-template-curly-in-string': WARNING, 'no-template-curly-in-string': WARNING,
'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}], 'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}],
'no-useless-escape': WARNING, 'no-useless-escape': WARNING,
'no-void': [ERROR, {allowAsStatement: true}],
'prefer-destructuring': WARNING, 'prefer-destructuring': WARNING,
'prefer-named-capture-group': WARNING, 'prefer-named-capture-group': WARNING,
'prefer-template': WARNING, 'prefer-template': WARNING,

View file

@ -70,7 +70,11 @@ const plugin: Plugin = function plugin(
} }
const now = eat.now(); const now = eat.now();
const [opening, keyword, title] = match; const [opening, keyword, title] = match as string[] as [
string,
string,
string,
];
const food = []; const food = [];
const content = []; const content = [];
@ -169,7 +173,7 @@ const plugin: Plugin = function plugin(
visit( visit(
root, root,
(node: unknown): node is Literal => (node: unknown): node is Literal =>
(node as Literal)?.type !== admonitionNodeType, (node as Literal | undefined)?.type !== admonitionNodeType,
(node: Literal) => { (node: Literal) => {
if (node.value) { if (node.value) {
node.value = node.value.replace(escapeTag, options.tag); node.value = node.value.replace(escapeTag, options.tag);

View file

@ -21,6 +21,7 @@ import visit from 'unist-util-visit';
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import sizeOf from 'image-size'; import sizeOf from 'image-size';
import type {Transformer} from 'unified'; import type {Transformer} from 'unified';
import type {Parent} from 'unist';
import type {Image, Literal} from 'mdast'; import type {Image, Literal} from 'mdast';
const { const {
@ -36,12 +37,13 @@ type Context = PluginOptions & {
filePath: string; filePath: string;
}; };
type Target = [node: Image, index: number, parent: Parent];
async function toImageRequireNode( async function toImageRequireNode(
node: Image, [node, index, parent]: Target,
imagePath: string, imagePath: string,
filePath: string, filePath: string,
) { ) {
const jsxNode = node as Literal & Partial<Image>;
let relativeImagePath = posixPath( let relativeImagePath = posixPath(
path.relative(path.dirname(filePath), imagePath), path.relative(path.dirname(filePath), imagePath),
); );
@ -75,12 +77,12 @@ ${(err as Error).message}`;
} }
} }
Object.keys(jsxNode).forEach( const jsxNode: Literal = {
(key) => delete jsxNode[key as keyof typeof jsxNode], type: 'jsx',
); value: `<img ${alt}src={${src}}${title}${width}${height} />`,
};
(jsxNode as Literal).type = 'jsx'; parent.children.splice(index, 1, jsxNode);
jsxNode.value = `<img ${alt}src={${src}}${title}${width}${height} />`;
} }
async function ensureImageFileExist(imagePath: string, sourceFilePath: string) { async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
@ -129,7 +131,8 @@ async function getImageAbsolutePath(
return imageFilePath; return imageFilePath;
} }
async function processImageNode(node: Image, context: Context) { async function processImageNode(target: Target, context: Context) {
const [node] = target;
if (!node.url) { if (!node.url) {
throw new Error( throw new Error(
`Markdown image URL is mandatory in "${toMessageRelativeFilePath( `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 // We try to convert image urls without protocol to images with require calls
// going through webpack ensures that image assets exist at build time // going through webpack ensures that image assets exist at build time
const imagePath = await getImageAbsolutePath(parsedUrl.pathname, context); 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 { export default function plugin(options: PluginOptions): Transformer {
return async (root, vfile) => { return async (root, vfile) => {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
visit(root, 'image', (node: Image) => { visit(root, 'image', (node: Image, index, parent) => {
promises.push( promises.push(
processImageNode(node, {...options, filePath: vfile.path!}), processImageNode([node, index, parent!], {
...options,
filePath: vfile.path!,
}),
); );
}); });
await Promise.all(promises); await Promise.all(promises);

View file

@ -19,6 +19,7 @@ import visit from 'unist-util-visit';
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import {stringifyContent} from '../utils'; import {stringifyContent} from '../utils';
import type {Transformer} from 'unified'; import type {Transformer} from 'unified';
import type {Parent} from 'unist';
import type {Link, Literal} from 'mdast'; import type {Link, Literal} from 'mdast';
const { const {
@ -34,16 +35,20 @@ type Context = PluginOptions & {
filePath: string; filePath: string;
}; };
type Target = [node: Link, index: number, parent: Parent];
/** /**
* Transforms the link node to a JSX `<a>` element with a `require()` call. * Transforms the link node to a JSX `<a>` element with a `require()` call.
*/ */
function toAssetRequireNode(node: Link, assetPath: string, filePath: string) { function toAssetRequireNode(
const jsxNode = node as Literal & Partial<Link>; [node, index, parent]: Target,
let relativeAssetPath = posixPath( assetPath: string,
path.relative(path.dirname(filePath), assetPath), filePath: string,
); ) {
// require("assets/file.pdf") means requiring from a package called assets // 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 parsedUrl = url.parse(node.url);
const hash = parsedUrl.hash ?? ''; const hash = parsedUrl.hash ?? '';
@ -60,12 +65,12 @@ function toAssetRequireNode(node: Link, assetPath: string, filePath: string) {
const children = stringifyContent(node); const children = stringifyContent(node);
const title = node.title ? ` title="${escapeHtml(node.title)}"` : ''; const title = node.title ? ` title="${escapeHtml(node.title)}"` : '';
Object.keys(jsxNode).forEach( const jsxNode: Literal = {
(key) => delete jsxNode[key as keyof typeof jsxNode], type: 'jsx',
); value: `<a target="_blank" href={${href}}${title}>${children}</a>`,
};
(jsxNode as Literal).type = 'jsx'; parent.children.splice(index, 1, jsxNode);
jsxNode.value = `<a target="_blank" href={${href}}${title}>${children}</a>`;
} }
async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) { async function ensureAssetFileExist(assetPath: string, sourceFilePath: string) {
@ -106,7 +111,8 @@ async function getAssetAbsolutePath(
return null; return null;
} }
async function processLinkNode(node: Link, context: Context) { async function processLinkNode(target: Target, context: Context) {
const [node] = target;
if (!node.url) { if (!node.url) {
// Try to improve error feedback // Try to improve error feedback
// see https://github.com/facebook/docusaurus/issues/3309#issuecomment-690371675 // see https://github.com/facebook/docusaurus/issues/3309#issuecomment-690371675
@ -138,15 +144,20 @@ async function processLinkNode(node: Link, context: Context) {
context, context,
); );
if (assetPath) { if (assetPath) {
toAssetRequireNode(node, assetPath, context.filePath); toAssetRequireNode(target, assetPath, context.filePath);
} }
} }
export default function plugin(options: PluginOptions): Transformer { export default function plugin(options: PluginOptions): Transformer {
return async (root, vfile) => { return async (root, vfile) => {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
visit(root, 'link', (node: Link) => { visit(root, 'link', (node: Link, index, parent) => {
promises.push(processLinkNode(node, {...options, filePath: vfile.path!})); promises.push(
processLinkNode([node, index, parent!], {
...options,
filePath: vfile.path!,
}),
);
}); });
await Promise.all(promises); await Promise.all(promises);
}; };

View file

@ -34,19 +34,19 @@ cli
.option('--mdx', 'try to migrate MD to MDX too') .option('--mdx', 'try to migrate MD to MDX too')
.option('--page', 'try to migrate pages too') .option('--page', 'try to migrate pages too')
.description('Migrate between versions of Docusaurus website.') .description('Migrate between versions of Docusaurus website.')
.action((siteDir = '.', newDir = '.', {mdx, page} = {}) => { .action(async (siteDir = '.', newDir = '.', {mdx, page} = {}) => {
const sitePath = path.resolve(siteDir); const sitePath = path.resolve(siteDir);
const newSitePath = path.resolve(newDir); const newSitePath = path.resolve(newDir);
migrateDocusaurusProject(sitePath, newSitePath, mdx, page); await migrateDocusaurusProject(sitePath, newSitePath, mdx, page);
}); });
cli cli
.command('mdx [siteDir] [newDir]') .command('mdx [siteDir] [newDir]')
.description('Migrate markdown files to MDX.') .description('Migrate markdown files to MDX.')
.action((siteDir = '.', newDir = '.') => { .action(async (siteDir = '.', newDir = '.') => {
const sitePath = path.resolve(siteDir); const sitePath = path.resolve(siteDir);
const newSitePath = path.resolve(newDir); const newSitePath = path.resolve(newDir);
migrateMDToMDX(sitePath, newSitePath); await migrateMDToMDX(sitePath, newSitePath);
}); });
cli.parse(process.argv); cli.parse(process.argv);

View file

@ -22,7 +22,7 @@ describe('getFileLastUpdate', () => {
const lastUpdateData = await getFileLastUpdate(existingFilePath); const lastUpdateData = await getFileLastUpdate(existingFilePath);
expect(lastUpdateData).not.toBeNull(); expect(lastUpdateData).not.toBeNull();
const {author, timestamp} = lastUpdateData; const {author, timestamp} = lastUpdateData!;
expect(author).not.toBeNull(); expect(author).not.toBeNull();
expect(typeof author).toBe('string'); expect(typeof author).toBe('string');
@ -38,7 +38,7 @@ describe('getFileLastUpdate', () => {
const lastUpdateData = await getFileLastUpdate(filePathWithSpace); const lastUpdateData = await getFileLastUpdate(filePathWithSpace);
expect(lastUpdateData).not.toBeNull(); expect(lastUpdateData).not.toBeNull();
const {author, timestamp} = lastUpdateData; const {author, timestamp} = lastUpdateData!;
expect(author).not.toBeNull(); expect(author).not.toBeNull();
expect(typeof author).toBe('string'); expect(typeof author).toBe('string');
@ -61,8 +61,6 @@ describe('getFileLastUpdate', () => {
expect(consoleMock).toHaveBeenLastCalledWith( expect(consoleMock).toHaveBeenLastCalledWith(
expect.stringMatching(/because the file does not exist./), expect.stringMatching(/because the file does not exist./),
); );
await expect(getFileLastUpdate(null)).resolves.toBeNull();
await expect(getFileLastUpdate(undefined)).resolves.toBeNull();
consoleMock.mockRestore(); consoleMock.mockRestore();
}); });

View file

@ -16,7 +16,7 @@ let showedGitRequirementError = false;
let showedFileNotTrackedError = false; let showedFileNotTrackedError = false;
export async function getFileLastUpdate( export async function getFileLastUpdate(
filePath?: string, filePath: string,
): Promise<{timestamp: number; author: string} | null> { ): Promise<{timestamp: number; author: string} | null> {
if (!filePath) { if (!filePath) {
return null; return null;

View file

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {createStorageSlot} from '@docusaurus/theme-common'; import {createStorageSlot} from '@docusaurus/theme-common';
// First: read the env variables (provided by Webpack) // First: read the env variables (provided by Webpack)
@ -15,40 +16,42 @@ const PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES = process.env
const PWA_DEBUG = process.env.PWA_DEBUG; const PWA_DEBUG = process.env.PWA_DEBUG;
/* eslint-enable prefer-destructuring */ /* eslint-enable prefer-destructuring */
const debug = PWA_DEBUG; // Shortcut const MAX_MOBILE_WIDTH = 996;
const MAX_MOBILE_WIDTH = 940;
const AppInstalledEventFiredStorage = createStorageSlot( const AppInstalledEventFiredStorage = createStorageSlot(
'docusaurus.pwa.event.appInstalled.fired', 'docusaurus.pwa.event.appInstalled.fired',
); );
declare global {
interface Navigator {
getInstalledRelatedApps: () => Promise<{platform: string}[]>;
connection?: {effectiveType: string; saveData: boolean};
}
}
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);
}
}
}
async function clearRegistrations() { async function clearRegistrations() {
const registrations = await navigator.serviceWorker.getRegistrations(); const registrations = await navigator.serviceWorker.getRegistrations();
if (debug) { debugLog('will unregister all service workers', {registrations});
console.log(
`[Docusaurus-PWA][registerSw]: will unregister all service worker registrations`,
registrations,
);
}
await Promise.all( await Promise.all(
registrations.map(async (registration) => { registrations.map((registration) =>
const result = await registration.unregister(); registration
if (debug) { .unregister()
console.log( .then((result) =>
`[Docusaurus-PWA][registerSw]: unregister() service worker registration`, debugLog('unregister service worker', {registration, result}),
registrations, ),
result, ),
); );
} debugLog('unregistered all service workers', {registrations});
}),
);
if (debug) {
console.log(
`[Docusaurus-PWA][registerSw]: unregistered all service worker registrations`,
registrations,
);
}
window.location.reload(); window.location.reload();
} }
@ -61,24 +64,17 @@ 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 - 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/ - 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'; return AppInstalledEventFiredStorage.get() === 'true';
} }
declare global { async function getIsAppInstalledRelatedApps() {
interface Navigator { if (!('getInstalledRelatedApps' in window.navigator)) {
getInstalledRelatedApps: () => Promise<{platform: string}[]>; return false;
connection?: {effectiveType: string; saveData: boolean};
} }
}
async function isAppInstalledRelatedApps() {
if ('getInstalledRelatedApps' in window.navigator) {
const relatedApps = await navigator.getInstalledRelatedApps(); const relatedApps = await navigator.getInstalledRelatedApps();
return relatedApps.some((app) => app.platform === 'webapp'); return relatedApps.some((app) => app.platform === 'webapp');
} }
return false;
}
function isStandaloneDisplayMode() { function isStandaloneDisplayMode() {
return window.matchMedia('(display-mode: standalone)').matches; return window.matchMedia('(display-mode: standalone)').matches;
} }
@ -87,52 +83,36 @@ const OfflineModeActivationStrategiesImplementations = {
always: () => true, always: () => true,
mobile: () => window.innerWidth <= MAX_MOBILE_WIDTH, mobile: () => window.innerWidth <= MAX_MOBILE_WIDTH,
saveData: () => !!navigator.connection?.saveData, saveData: () => !!navigator.connection?.saveData,
appInstalled: async () => { appInstalled: () =>
const installedEventFired = await isAppInstalledEventFired(); getIsAppInstalledEventFired() || getIsAppInstalledRelatedApps(),
const installedRelatedApps = await isAppInstalledRelatedApps();
return installedEventFired || installedRelatedApps;
},
standalone: () => isStandaloneDisplayMode(), standalone: () => isStandaloneDisplayMode(),
queryString: () => queryString: () =>
new URLSearchParams(window.location.search).get('offlineMode') === 'true', new URLSearchParams(window.location.search).get('offlineMode') === 'true',
}; };
async function isStrategyActive(
strategyName: keyof typeof OfflineModeActivationStrategiesImplementations,
) {
return OfflineModeActivationStrategiesImplementations[strategyName]();
}
async function getActiveStrategies() { async function getActiveStrategies() {
const activeStrategies = await Promise.all( const activeStrategies = await Promise.all(
PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.map(async (strategyName) => { PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.map((strategyName) =>
const isActive = await isStrategyActive(strategyName); Promise.resolve(
return isActive ? strategyName : undefined; OfflineModeActivationStrategiesImplementations[strategyName](),
}), ).then((isActive) => (isActive ? strategyName : undefined)),
),
); );
return activeStrategies.filter(Boolean); return activeStrategies.filter(Boolean);
} }
async function isOfflineModeEnabled() { async function getIsOfflineModeEnabled() {
const activeStrategies = await getActiveStrategies(); const activeStrategies = await getActiveStrategies();
const enabled = activeStrategies.length > 0; const enabled = activeStrategies.length > 0;
if (debug) { debugLog(
const logObject = { enabled
? 'offline mode enabled, because of activation strategies'
: 'offline mode disabled, because none of the offlineModeActivationStrategies could be used',
{
activeStrategies, activeStrategies,
availableStrategies: PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES, 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; return enabled;
} }
@ -141,170 +121,111 @@ function createServiceWorkerUrl(params: object) {
const url = `${PWA_SERVICE_WORKER_URL}?params=${encodeURIComponent( const url = `${PWA_SERVICE_WORKER_URL}?params=${encodeURIComponent(
paramsQueryString, paramsQueryString,
)}`; )}`;
if (debug) { debugLog('service worker url', {url, params});
console.log(`[Docusaurus-PWA][registerSw]: service worker url`, {
url,
params,
});
}
return url; return url;
} }
async function registerSW() { async function registerSW() {
const {Workbox} = await import('workbox-window'); const [{Workbox}, offlineMode] = await Promise.all([
import('workbox-window'),
const offlineMode = await isOfflineModeEnabled(); getIsOfflineModeEnabled(),
]);
const url = createServiceWorkerUrl({offlineMode, debug}); const url = createServiceWorkerUrl({offlineMode, debug: PWA_DEBUG});
const wb = new Workbox(url); const wb = new Workbox(url);
const registration = await wb.register();
const sendSkipWaiting = () => wb.messageSW({type: 'SKIP_WAITING'}); const sendSkipWaiting = () => wb.messageSW({type: 'SKIP_WAITING'});
const handleServiceWorkerWaiting = () => {
const handleServiceWorkerWaiting = async () => { debugLog('handleServiceWorkerWaiting');
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: handleServiceWorkerWaiting');
}
// Immediately load new service worker when files aren't cached // Immediately load new service worker when files aren't cached
if (!offlineMode) { if (!offlineMode) {
sendSkipWaiting(); return sendSkipWaiting();
} else { }
const renderReloadPopup = (await import('./renderReloadPopup')).default; return import('./renderReloadPopup').then(({default: renderReloadPopup}) =>
await renderReloadPopup({ renderReloadPopup({
onReload() { onReload() {
wb.addEventListener('controlling', () => { wb.addEventListener('controlling', () => {
window.location.reload(); window.location.reload();
}); });
sendSkipWaiting(); 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,
);
}
}
// Update the current service worker when the next one has finished // Update the current service worker when the next one has finished
// installing and transitions to waiting state. // installing and transitions to waiting state.
wb.addEventListener('waiting', (event) => { wb.addEventListener('waiting', (event) => {
if (debug) { debugLog('event waiting', {event});
console.log('[Docusaurus-PWA][registerSw]: event waiting', event); void handleServiceWorkerWaiting();
}
handleServiceWorkerWaiting();
}); });
// Update current service worker if the next one finishes installing and // Update current service worker if the next one finishes installing and
// moves to waiting state in another tab. // moves to waiting state in another tab.
// @ts-expect-error: not present in the API typings anymore // @ts-expect-error: not present in the API typings anymore
wb.addEventListener('externalwaiting', (event) => { wb.addEventListener('externalwaiting', (event) => {
if (debug) { debugLog('event externalwaiting', {event});
console.log('[Docusaurus-PWA][registerSw]: event externalwaiting', event); void handleServiceWorkerWaiting();
}
handleServiceWorkerWaiting();
}); });
const registration = await wb.register();
// Update service worker if the next one is already in the waiting state. if (registration) {
// This happens when the user doesn't click on `reload` in the popup. if (registration.active) {
if (registration?.waiting) { 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.
await handleServiceWorkerWaiting(); await handleServiceWorkerWaiting();
} }
} }
}
// TODO these events still works in chrome but have been removed from the spec // TODO these events still works in chrome but have been removed from the spec
// in 2019! See https://github.com/w3c/manifest/pull/836 // in 2019! See https://github.com/w3c/manifest/pull/836
function addLegacyAppInstalledEventsListeners() { function addLegacyAppInstalledEventsListeners() {
if (typeof window !== 'undefined') { debugLog('addLegacyAppInstalledEventsListeners');
if (debug) {
console.log(
'[Docusaurus-PWA][registerSw]: addLegacyAppInstalledEventsListeners',
);
}
window.addEventListener('appinstalled', async (event) => {
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: event appinstalled', event);
}
window.addEventListener('appinstalled', (event) => {
debugLog('event appinstalled', {event});
AppInstalledEventFiredStorage.set('true'); AppInstalledEventFiredStorage.set('true');
if (debug) { debugLog("AppInstalledEventFiredStorage.set('true')");
console.log(
"[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.set('true')",
);
}
// After the app is installed, we register a service worker with the path // 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 // `/sw?enabled`. Since the previous service worker was `/sw`, it'll be
// treated as a new one. The previous registration will need to be // treated as a new one. The previous registration will need to be
// cleared, otherwise the reload popup will show. // cleared, otherwise the reload popup will show.
await clearRegistrations(); void clearRegistrations();
}); });
// TODO this event still works in chrome but has been removed from the spec // TODO this event still works in chrome but has been removed from the spec
// in 2019!!! // in 2019!!!
window.addEventListener('beforeinstallprompt', async (event) => { window.addEventListener('beforeinstallprompt', (event) => {
if (debug) { debugLog('event beforeinstallprompt', {event});
console.log(
'[Docusaurus-PWA][registerSw]: event beforeinstallprompt',
event,
);
}
// TODO instead of default browser install UI, show custom docusaurus // TODO instead of default browser install UI, show custom docusaurus
// prompt? // prompt?
// event.preventDefault(); // event.preventDefault();
if (debug) { const appInstalledEventFired = AppInstalledEventFiredStorage.get();
console.log( debugLog('AppInstalledEventFiredStorage.get()', {appInstalledEventFired});
'[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.get()', if (appInstalledEventFired) {
AppInstalledEventFiredStorage.get(),
);
}
if (AppInstalledEventFiredStorage.get()) {
AppInstalledEventFiredStorage.del(); AppInstalledEventFiredStorage.del();
if (debug) { debugLog('AppInstalledEventFiredStorage.del()');
console.log(
'[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.del()',
);
}
// After uninstalling the app, if the user doesn't clear all data, then // After uninstalling the app, if the user doesn't clear all data, then
// the previous service worker will continue serving cached files. We // the previous service worker will continue serving cached files. We
// need to clear registrations and reload, otherwise the popup shows. // need to clear registrations and reload, otherwise the popup shows.
await clearRegistrations(); void clearRegistrations();
} }
}); });
if (debug) { debugLog(
console.log( 'legacy appinstalled and beforeinstallprompt event listeners installed',
'[Docusaurus-PWA][registerSw]: legacy appinstalled and beforeinstallprompt event listeners installed',
); );
} }
}
}
/* /*
Init code to run on the client! Init code to run on the client!
*/ */
if (typeof window !== 'undefined') { if (ExecutionEnvironment.canUseDOM) {
if (debug) { debugLog('debug mode enabled');
console.log('[Docusaurus-PWA][registerSw]: debug mode enabled');
}
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
// First: add the listeners asap/synchronously // First: add the listeners asap/synchronously

View file

@ -20,8 +20,9 @@ const createContainer = () => {
return container; return container;
}; };
export default async function renderReloadPopup(props: Props): Promise<void> { export default function renderReloadPopup(props: Props): Promise<void> {
const container = getContainer() ?? createContainer(); const container = getContainer() ?? createContainer();
const ReloadPopup = (await import('@theme/PwaReloadPopup')).default; return import('@theme/PwaReloadPopup').then(({default: ReloadPopup}) => {
ReactDOM.render(<ReloadPopup {...props} />, container); ReactDOM.render(<ReloadPopup {...props} />, container);
});
} }

View file

@ -141,18 +141,17 @@ const aliases = {
warning: 'danger', warning: 'danger',
} as const; } as const;
function getAdmonitionConfig( function getAdmonitionConfig(unsafeType: string): AdmonitionConfig {
unsafeType: Props['type'] | keyof typeof aliases, const type =
): AdmonitionConfig { (aliases as {[key: string]: Props['type']})[unsafeType] ?? unsafeType;
const type = aliases[unsafeType as keyof typeof aliases] ?? unsafeType; const config = (AdmonitionConfigs as {[key: string]: AdmonitionConfig})[type];
const config = AdmonitionConfigs[type];
if (config) { if (config) {
return config; return config;
} }
console.warn( console.warn(
`No admonition config found for admonition type "${type}". Using Info as fallback.`, `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 // Workaround because it's difficult in MDX v1 to provide a MDX title as props

View file

@ -55,7 +55,7 @@ cli
'build website without minimizing JS bundles (default: false)', 'build website without minimizing JS bundles (default: false)',
) )
.action(async (siteDir, options) => { .action(async (siteDir, options) => {
build(await resolveDir(siteDir), options); await build(await resolveDir(siteDir), options);
}); });
cli cli

View file

@ -18,8 +18,8 @@ function logPage(
prevLocation: previousLocation, prevLocation: previousLocation,
heading: document.getElementsByTagName('h1')[0]?.innerText, heading: document.getElementsByTagName('h1')[0]?.innerText,
title: document.title, title: document.title,
description: ( description: document.querySelector<HTMLMetaElement>(
document.querySelector('meta[name="description"]') as HTMLMetaElement 'meta[name="description"]',
)?.content, )?.content,
htmlClassName: document.getElementsByTagName('html')[0]?.className, htmlClassName: document.getElementsByTagName('html')[0]?.className,
}); });

View file

@ -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) - `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) - `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) - `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` - `saveData`: activates for users with `navigator.connection.saveData === true`
- `always`: activates for all users - `always`: activates for all users

View file

@ -5,7 +5,13 @@
* LICENSE file in the root directory of this source tree. * 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 {useDocsPreferredVersion} from '@docusaurus/theme-common';
import {useVersions} from '@docusaurus/plugin-content-docs/client'; import {useVersions} from '@docusaurus/plugin-content-docs/client';
import Translate from '@docusaurus/Translate'; import Translate from '@docusaurus/Translate';
@ -24,11 +30,21 @@ export function VersionsProvider({
children: ReactNode; children: ReactNode;
}): JSX.Element { }): JSX.Element {
const [canaryVersion, setCanaryVersion] = useState<ContextValue | null>(null); const [canaryVersion, setCanaryVersion] = useState<ContextValue | null>(null);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
useEffect(() => { useEffect(() => {
fetch('https://registry.npmjs.org/@docusaurus/core') fetch('https://registry.npmjs.org/@docusaurus/core')
.then((res) => res.json()) .then((res) => res.json())
.then( .then(
(data: {versions: string[]; time: {[versionName: string]: string}}) => { (data: {versions: string[]; time: {[versionName: string]: string}}) => {
if (!mounted.current) {
return;
}
const name = Object.keys(data.versions).at(-1)!; const name = Object.keys(data.versions).at(-1)!;
const time = data.time[name]; const time = data.time[name];
setCanaryVersion({name, time}); setCanaryVersion({name, time});