mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-02 03:37:48 +02:00
refactor(pwa): simplify registerSW code, fix ESLint errors (#7579)
This commit is contained in:
parent
bada5c11cc
commit
7869e74fd7
14 changed files with 204 additions and 247 deletions
1
.eslintrc.js
vendored
1
.eslintrc.js
vendored
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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});
|
||||||
|
|
Loading…
Add table
Reference in a new issue