@azerion/bunosaurus-plugin-pwa 0.0.0-stage → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/lib/index.d.ts +10 -0
- package/lib/index.js +113 -0
- package/lib/options.d.ts +9 -0
- package/lib/options.js +45 -0
- package/lib/registerSw.d.ts +18 -0
- package/lib/registerSw.js +191 -0
- package/lib/renderReloadPopup.d.ts +8 -0
- package/lib/renderReloadPopup.js +22 -0
- package/lib/sw.d.ts +7 -0
- package/lib/sw.js +109 -0
- package/lib/theme/PwaReloadPopup/index.d.ts +9 -0
- package/lib/theme/PwaReloadPopup/index.js +54 -0
- package/lib/theme/PwaReloadPopup/styles.module.css +39 -0
- package/package.json +46 -4
- package/src/index.ts +150 -0
- package/src/options.ts +63 -0
- package/src/plugin-pwa.d.ts +90 -0
- package/src/registerSw.ts +242 -0
- package/src/renderReloadPopup.tsx +28 -0
- package/src/sw.ts +135 -0
- package/src/theme/PwaReloadPopup/index.tsx +59 -0
- package/src/theme/PwaReloadPopup/styles.module.css +39 -0
- package/src/types.d.ts +8 -0
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@docusaurus/plugin-pwa`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Progressive web app plugin for Docusaurus.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
See [plugin-pwa documentation](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-pwa).
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import type { LoadContext, Plugin } from '@docusaurus/types';
|
|
8
|
+
import type { PluginOptions } from '@docusaurus/plugin-pwa';
|
|
9
|
+
export default function pluginPWA(context: LoadContext, options: PluginOptions): Plugin<void> | null;
|
|
10
|
+
export { validateOptions } from './options';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.validateOptions = void 0;
|
|
10
|
+
exports.default = pluginPWA;
|
|
11
|
+
const tslib_1 = require("tslib");
|
|
12
|
+
const node_path_1 = tslib_1.__importDefault(require("node:path"));
|
|
13
|
+
const bundler_1 = require("@docusaurus/bundler");
|
|
14
|
+
const workbox_build_1 = require("workbox-build");
|
|
15
|
+
const utils_1 = require("@docusaurus/utils");
|
|
16
|
+
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
|
|
17
|
+
const theme_translations_1 = require("@docusaurus/theme-translations");
|
|
18
|
+
const PluginName = 'docusaurus-plugin-pwa';
|
|
19
|
+
function pluginPWA(context, options) {
|
|
20
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (context.siteConfig.future.experimental_router === 'hash') {
|
|
24
|
+
logger_1.default.warn(`${PluginName} does not support the Hash Router and will be disabled.`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const { outDir, baseUrl, i18n: { currentLocale }, } = context;
|
|
28
|
+
const { debug, offlineModeActivationStrategies, injectManifestConfig, pwaHead, swCustom, swRegister, } = options;
|
|
29
|
+
return {
|
|
30
|
+
name: PluginName,
|
|
31
|
+
getThemePath() {
|
|
32
|
+
return '../lib/theme';
|
|
33
|
+
},
|
|
34
|
+
getTypeScriptThemePath() {
|
|
35
|
+
return '../src/theme';
|
|
36
|
+
},
|
|
37
|
+
getClientModules() {
|
|
38
|
+
return swRegister ? [swRegister] : [];
|
|
39
|
+
},
|
|
40
|
+
getDefaultCodeTranslationMessages() {
|
|
41
|
+
return (0, theme_translations_1.readDefaultCodeTranslationMessages)({
|
|
42
|
+
locale: currentLocale,
|
|
43
|
+
name: 'plugin-pwa',
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
configureWebpack(config, isServer, { currentBundler }) {
|
|
47
|
+
return {
|
|
48
|
+
plugins: [
|
|
49
|
+
new currentBundler.instance.EnvironmentPlugin({
|
|
50
|
+
PWA_DEBUG: debug,
|
|
51
|
+
PWA_SERVICE_WORKER_URL: node_path_1.default.posix.resolve(`${config.output?.publicPath || '/'}`, 'sw.js'),
|
|
52
|
+
PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES: offlineModeActivationStrategies,
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
injectHtmlTags() {
|
|
58
|
+
const headTags = [];
|
|
59
|
+
pwaHead.forEach(({ tagName, ...attributes }) => {
|
|
60
|
+
['href', 'content'].forEach((attribute) => {
|
|
61
|
+
const attributeValue = attributes[attribute];
|
|
62
|
+
if (!attributeValue) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const attributePath = !!node_path_1.default.extname(attributeValue) && attributeValue;
|
|
66
|
+
if (attributePath && !attributePath.startsWith(baseUrl)) {
|
|
67
|
+
attributes[attribute] = (0, utils_1.normalizeUrl)([baseUrl, attributeValue]);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return headTags.push({
|
|
71
|
+
tagName,
|
|
72
|
+
attributes,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
return { headTags };
|
|
76
|
+
},
|
|
77
|
+
async postBuild(props) {
|
|
78
|
+
// The service worker is a classic script, bundled on its own
|
|
79
|
+
await (0, bundler_1.bunBuild)('Service Worker', {
|
|
80
|
+
entrypoints: [require.resolve('./sw.js')],
|
|
81
|
+
outdir: outDir,
|
|
82
|
+
naming: 'sw.js',
|
|
83
|
+
target: 'browser',
|
|
84
|
+
format: 'iife',
|
|
85
|
+
minify: !debug,
|
|
86
|
+
sourcemap: debug ? 'linked' : 'none',
|
|
87
|
+
publicPath: baseUrl,
|
|
88
|
+
env: 'disable',
|
|
89
|
+
define: {
|
|
90
|
+
'process.env.NODE_ENV': JSON.stringify(debug ? 'development' : 'production'),
|
|
91
|
+
'process.env.PWA_SW_CUSTOM': JSON.stringify(swCustom ?? ''),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const swDest = node_path_1.default.resolve(props.outDir, 'sw.js');
|
|
95
|
+
await (0, workbox_build_1.injectManifest)({
|
|
96
|
+
...injectManifestConfig,
|
|
97
|
+
globPatterns: [
|
|
98
|
+
'**/*.{js,json,css,html}',
|
|
99
|
+
'**/*.{png,jpg,jpeg,gif,svg,ico}',
|
|
100
|
+
'**/*.{woff,woff2,eot,ttf,otf}',
|
|
101
|
+
// @ts-expect-error: internal API?
|
|
102
|
+
...(workbox_build_1.injectManifest.globPatterns ?? []),
|
|
103
|
+
],
|
|
104
|
+
// Those attributes are not overrideable
|
|
105
|
+
swDest,
|
|
106
|
+
swSrc: swDest,
|
|
107
|
+
globDirectory: props.outDir,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
var options_1 = require("./options");
|
|
113
|
+
Object.defineProperty(exports, "validateOptions", { enumerable: true, get: function () { return options_1.validateOptions; } });
|
package/lib/options.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import type { OptionValidationContext } from '@docusaurus/types';
|
|
8
|
+
import type { PluginOptions } from '@docusaurus/plugin-pwa';
|
|
9
|
+
export declare function validateOptions({ validate, options, }: OptionValidationContext<PluginOptions, PluginOptions>): PluginOptions;
|
package/lib/options.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.validateOptions = validateOptions;
|
|
10
|
+
const utils_validation_1 = require("@docusaurus/utils-validation");
|
|
11
|
+
const DEFAULT_OPTIONS = {
|
|
12
|
+
debug: false,
|
|
13
|
+
offlineModeActivationStrategies: [
|
|
14
|
+
'appInstalled',
|
|
15
|
+
'queryString',
|
|
16
|
+
'standalone',
|
|
17
|
+
],
|
|
18
|
+
injectManifestConfig: {},
|
|
19
|
+
pwaHead: [],
|
|
20
|
+
swCustom: undefined,
|
|
21
|
+
swRegister: './registerSw.js',
|
|
22
|
+
};
|
|
23
|
+
const optionsSchema = utils_validation_1.Joi.object({
|
|
24
|
+
debug: utils_validation_1.Joi.bool().default(DEFAULT_OPTIONS.debug),
|
|
25
|
+
offlineModeActivationStrategies: utils_validation_1.Joi.array()
|
|
26
|
+
.items(utils_validation_1.Joi.string()
|
|
27
|
+
.valid('appInstalled', 'queryString', 'standalone', 'mobile', 'saveData', 'always')
|
|
28
|
+
.required())
|
|
29
|
+
.default(DEFAULT_OPTIONS.offlineModeActivationStrategies),
|
|
30
|
+
injectManifestConfig: utils_validation_1.Joi.object().default(DEFAULT_OPTIONS.injectManifestConfig),
|
|
31
|
+
pwaHead: utils_validation_1.Joi.array()
|
|
32
|
+
.items(utils_validation_1.Joi.object({ tagName: utils_validation_1.Joi.string().required() }).unknown().required())
|
|
33
|
+
.default(DEFAULT_OPTIONS.pwaHead),
|
|
34
|
+
swCustom: utils_validation_1.Joi.string(),
|
|
35
|
+
swRegister: utils_validation_1.Joi.alternatives()
|
|
36
|
+
.try(utils_validation_1.Joi.string(), utils_validation_1.Joi.bool().valid(false))
|
|
37
|
+
.default(DEFAULT_OPTIONS.swRegister),
|
|
38
|
+
// @ts-expect-error: forbidden
|
|
39
|
+
reloadPopup: utils_validation_1.Joi.any().forbidden().messages({
|
|
40
|
+
'any.unknown': 'The reloadPopup option is removed in favor of swizzling. See https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-pwa#customizing-reload-popup for how to customize the reload popup using swizzling.',
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
function validateOptions({ validate, options, }) {
|
|
44
|
+
return validate(optionsSchema, options);
|
|
45
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
declare global {
|
|
8
|
+
interface Navigator {
|
|
9
|
+
getInstalledRelatedApps: () => Promise<{
|
|
10
|
+
platform: string;
|
|
11
|
+
}[]>;
|
|
12
|
+
connection?: {
|
|
13
|
+
effectiveType: string;
|
|
14
|
+
saveData: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|
8
|
+
import { createStorageSlot } from '@docusaurus/theme-common';
|
|
9
|
+
// First: read the env variables (provided by Webpack)
|
|
10
|
+
const PWA_SERVICE_WORKER_URL = process.env.PWA_SERVICE_WORKER_URL;
|
|
11
|
+
const PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES = process.env
|
|
12
|
+
.PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES;
|
|
13
|
+
const PWA_DEBUG = process.env.PWA_DEBUG;
|
|
14
|
+
const MAX_MOBILE_WIDTH = 996;
|
|
15
|
+
const AppInstalledEventFiredStorage = createStorageSlot('docusaurus.pwa.event.appInstalled.fired');
|
|
16
|
+
function debugLog(msg, obj) {
|
|
17
|
+
if (PWA_DEBUG) {
|
|
18
|
+
if (typeof obj === 'undefined') {
|
|
19
|
+
console.log(`[Docusaurus-PWA][registerSw]: ${msg}`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log(`[Docusaurus-PWA][registerSw]: ${msg}`, obj);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function clearRegistrations() {
|
|
27
|
+
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
28
|
+
debugLog('will unregister all service workers', { registrations });
|
|
29
|
+
await Promise.all(registrations.map((registration) => registration
|
|
30
|
+
.unregister()
|
|
31
|
+
.then((result) => debugLog('unregister service worker', { registration, result }))));
|
|
32
|
+
debugLog('unregistered all service workers', { registrations });
|
|
33
|
+
window.location.reload();
|
|
34
|
+
}
|
|
35
|
+
/*
|
|
36
|
+
As of 2021:
|
|
37
|
+
It is complicated and not very reliable to detect an app is actually installed.
|
|
38
|
+
https://stackoverflow.com/questions/51735869/check-if-user-has-already-installed-pwa-to-homescreen-on-chrome
|
|
39
|
+
|
|
40
|
+
- appinstalled event is not in the spec anymore and seems to not fire? https://firt.dev/pwa-2021#less-capabilities-%E2%98%B9%EF%B8%8F
|
|
41
|
+
- getInstalledRelatedApps() is only supported in recent Chrome and does not seem to reliable either https://github.com/WICG/get-installed-related-apps
|
|
42
|
+
- 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/
|
|
43
|
+
*/
|
|
44
|
+
function getIsAppInstalledEventFired() {
|
|
45
|
+
return AppInstalledEventFiredStorage.get() === 'true';
|
|
46
|
+
}
|
|
47
|
+
async function getIsAppInstalledRelatedApps() {
|
|
48
|
+
if (!('getInstalledRelatedApps' in window.navigator)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const relatedApps = await navigator.getInstalledRelatedApps();
|
|
53
|
+
return relatedApps.some((app) => app.platform === 'webapp');
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Error might be thrown when Docusaurus is embedded in an iframe:
|
|
57
|
+
// registerSW failed DOMException: Failed to execute 'getInstalledRelatedApps' on 'Navigator': getInstalledRelatedApps() is only supported in top-level browsing contexts.
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function isStandaloneDisplayMode() {
|
|
62
|
+
return window.matchMedia('(display-mode: standalone)').matches;
|
|
63
|
+
}
|
|
64
|
+
const OfflineModeActivationStrategiesImplementations = {
|
|
65
|
+
always: () => true,
|
|
66
|
+
mobile: () => window.innerWidth <= MAX_MOBILE_WIDTH,
|
|
67
|
+
saveData: () => !!navigator.connection?.saveData,
|
|
68
|
+
appInstalled: () => getIsAppInstalledEventFired() || getIsAppInstalledRelatedApps(),
|
|
69
|
+
standalone: () => isStandaloneDisplayMode(),
|
|
70
|
+
queryString: () => new URLSearchParams(window.location.search).get('offlineMode') === 'true',
|
|
71
|
+
};
|
|
72
|
+
async function getActiveStrategies() {
|
|
73
|
+
const activeStrategies = await Promise.all(PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.map((strategyName) => Promise.resolve(OfflineModeActivationStrategiesImplementations[strategyName]()).then((isActive) => (isActive ? strategyName : undefined))));
|
|
74
|
+
return activeStrategies.filter(Boolean);
|
|
75
|
+
}
|
|
76
|
+
async function getIsOfflineModeEnabled() {
|
|
77
|
+
const activeStrategies = await getActiveStrategies();
|
|
78
|
+
const enabled = activeStrategies.length > 0;
|
|
79
|
+
debugLog(enabled
|
|
80
|
+
? 'offline mode enabled, because of activation strategies'
|
|
81
|
+
: 'offline mode disabled, because none of the offlineModeActivationStrategies could be used', {
|
|
82
|
+
activeStrategies,
|
|
83
|
+
availableStrategies: PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES,
|
|
84
|
+
});
|
|
85
|
+
return enabled;
|
|
86
|
+
}
|
|
87
|
+
function createServiceWorkerUrl(params) {
|
|
88
|
+
const paramsQueryString = JSON.stringify(params);
|
|
89
|
+
const url = `${PWA_SERVICE_WORKER_URL}?params=${encodeURIComponent(paramsQueryString)}`;
|
|
90
|
+
debugLog('service worker url', { url, params });
|
|
91
|
+
return url;
|
|
92
|
+
}
|
|
93
|
+
async function registerSW() {
|
|
94
|
+
const [{ Workbox }, offlineMode] = await Promise.all([
|
|
95
|
+
import('workbox-window'),
|
|
96
|
+
getIsOfflineModeEnabled(),
|
|
97
|
+
]);
|
|
98
|
+
const url = createServiceWorkerUrl({ offlineMode, debug: PWA_DEBUG });
|
|
99
|
+
const wb = new Workbox(url);
|
|
100
|
+
const sendSkipWaiting = () => wb.messageSW({ type: 'SKIP_WAITING' });
|
|
101
|
+
const handleServiceWorkerWaiting = () => {
|
|
102
|
+
debugLog('handleServiceWorkerWaiting');
|
|
103
|
+
// Immediately load new service worker when files aren't cached
|
|
104
|
+
if (!offlineMode) {
|
|
105
|
+
return sendSkipWaiting();
|
|
106
|
+
}
|
|
107
|
+
return import('./renderReloadPopup.js').then(({ renderReloadPopup }) => renderReloadPopup({
|
|
108
|
+
onReload() {
|
|
109
|
+
wb.addEventListener('controlling', () => {
|
|
110
|
+
window.location.reload();
|
|
111
|
+
});
|
|
112
|
+
sendSkipWaiting();
|
|
113
|
+
},
|
|
114
|
+
}));
|
|
115
|
+
};
|
|
116
|
+
// Update the current service worker when the next one has finished
|
|
117
|
+
// installing and transitions to waiting state.
|
|
118
|
+
wb.addEventListener('waiting', (event) => {
|
|
119
|
+
debugLog('event waiting', { event });
|
|
120
|
+
void handleServiceWorkerWaiting();
|
|
121
|
+
});
|
|
122
|
+
// Update current service worker if the next one finishes installing and
|
|
123
|
+
// moves to waiting state in another tab.
|
|
124
|
+
// @ts-expect-error: not present in the API typings anymore
|
|
125
|
+
wb.addEventListener('externalwaiting', (event) => {
|
|
126
|
+
debugLog('event externalwaiting', { event });
|
|
127
|
+
void handleServiceWorkerWaiting();
|
|
128
|
+
});
|
|
129
|
+
const registration = await wb.register();
|
|
130
|
+
if (registration) {
|
|
131
|
+
if (registration.active) {
|
|
132
|
+
debugLog('registration.active', { registration });
|
|
133
|
+
}
|
|
134
|
+
if (registration.installing) {
|
|
135
|
+
debugLog('registration.installing', { registration });
|
|
136
|
+
}
|
|
137
|
+
if (registration.waiting) {
|
|
138
|
+
debugLog('registration.waiting', { registration });
|
|
139
|
+
// Update service worker if the next one is already in the waiting
|
|
140
|
+
// state. This happens when the user doesn't click on `reload` in
|
|
141
|
+
// the popup.
|
|
142
|
+
await handleServiceWorkerWaiting();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// TODO these events still works in chrome but have been removed from the spec
|
|
147
|
+
// in 2019! See https://github.com/w3c/manifest/pull/836
|
|
148
|
+
function addLegacyAppInstalledEventsListeners() {
|
|
149
|
+
debugLog('addLegacyAppInstalledEventsListeners');
|
|
150
|
+
window.addEventListener('appinstalled', (event) => {
|
|
151
|
+
debugLog('event appinstalled', { event });
|
|
152
|
+
AppInstalledEventFiredStorage.set('true');
|
|
153
|
+
debugLog("AppInstalledEventFiredStorage.set('true')");
|
|
154
|
+
// After the app is installed, we register a service worker with the path
|
|
155
|
+
// `/sw?enabled`. Since the previous service worker was `/sw`, it'll be
|
|
156
|
+
// treated as a new one. The previous registration will need to be
|
|
157
|
+
// cleared, otherwise the reload popup will show.
|
|
158
|
+
void clearRegistrations();
|
|
159
|
+
});
|
|
160
|
+
// TODO this event still works in chrome but has been removed from the spec
|
|
161
|
+
// in 2019!!!
|
|
162
|
+
window.addEventListener('beforeinstallprompt', (event) => {
|
|
163
|
+
debugLog('event beforeinstallprompt', { event });
|
|
164
|
+
// TODO instead of default browser install UI, show custom docusaurus
|
|
165
|
+
// prompt?
|
|
166
|
+
// event.preventDefault();
|
|
167
|
+
const appInstalledEventFired = AppInstalledEventFiredStorage.get();
|
|
168
|
+
debugLog('AppInstalledEventFiredStorage.get()', { appInstalledEventFired });
|
|
169
|
+
if (appInstalledEventFired) {
|
|
170
|
+
AppInstalledEventFiredStorage.del();
|
|
171
|
+
debugLog('AppInstalledEventFiredStorage.del()');
|
|
172
|
+
// After uninstalling the app, if the user doesn't clear all data, then
|
|
173
|
+
// the previous service worker will continue serving cached files. We
|
|
174
|
+
// need to clear registrations and reload, otherwise the popup shows.
|
|
175
|
+
void clearRegistrations();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
debugLog('legacy appinstalled and beforeinstallprompt event listeners installed');
|
|
179
|
+
}
|
|
180
|
+
/*
|
|
181
|
+
Init code to run on the client!
|
|
182
|
+
*/
|
|
183
|
+
if (ExecutionEnvironment.canUseDOM) {
|
|
184
|
+
debugLog('debug mode enabled');
|
|
185
|
+
if ('serviceWorker' in navigator) {
|
|
186
|
+
// First: add the listeners asap/synchronously
|
|
187
|
+
addLegacyAppInstalledEventsListeners();
|
|
188
|
+
// Then try to register the SW using lazy/dynamic imports
|
|
189
|
+
registerSW().catch((e) => console.error('registerSW failed', e));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import type { Props } from '@theme/PwaReloadPopup';
|
|
8
|
+
export declare function renderReloadPopup(props: Props): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { createRoot } from 'react-dom/client';
|
|
8
|
+
const POPUP_CONTAINER_ID = 'pwa-popup-container';
|
|
9
|
+
const getContainer = () => document.getElementById(POPUP_CONTAINER_ID);
|
|
10
|
+
const createContainer = () => {
|
|
11
|
+
const container = document.createElement('div');
|
|
12
|
+
container.id = POPUP_CONTAINER_ID;
|
|
13
|
+
document.body.appendChild(container);
|
|
14
|
+
return container;
|
|
15
|
+
};
|
|
16
|
+
export function renderReloadPopup(props) {
|
|
17
|
+
const container = getContainer() ?? createContainer();
|
|
18
|
+
return import('@theme/PwaReloadPopup').then(({ default: ReloadPopup }) => {
|
|
19
|
+
const root = createRoot(container);
|
|
20
|
+
root.render(<ReloadPopup {...props}/>);
|
|
21
|
+
});
|
|
22
|
+
}
|
package/lib/sw.d.ts
ADDED
package/lib/sw.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { PrecacheController } from 'workbox-precaching';
|
|
8
|
+
function parseSwParams() {
|
|
9
|
+
const params = JSON.parse(new URLSearchParams(self.location.search).get('params'));
|
|
10
|
+
if (params.debug) {
|
|
11
|
+
console.log('[Docusaurus-PWA][SW]: Service Worker params:', params);
|
|
12
|
+
}
|
|
13
|
+
return params;
|
|
14
|
+
}
|
|
15
|
+
// Doc advises against dynamic imports in SW
|
|
16
|
+
// https://developers.google.com/web/tools/workbox/guides/using-bundlers#code_splitting_and_dynamic_imports
|
|
17
|
+
// https://x.com/sebastienlorber/status/1280155204575518720
|
|
18
|
+
// but looks it's working fine as it's inlined by webpack, need to double check
|
|
19
|
+
async function runSWCustomCode(params) {
|
|
20
|
+
if (process.env.PWA_SW_CUSTOM) {
|
|
21
|
+
const customSW = await import(process.env.PWA_SW_CUSTOM);
|
|
22
|
+
if (typeof customSW.default === 'function') {
|
|
23
|
+
customSW.default(params);
|
|
24
|
+
}
|
|
25
|
+
else if (params.debug) {
|
|
26
|
+
console.warn('[Docusaurus-PWA][SW]: swCustom should have a default export function');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Gets different possible variations for a request URL. Similar to
|
|
32
|
+
* https://git.io/JvixK
|
|
33
|
+
*/
|
|
34
|
+
function getPossibleURLs(url) {
|
|
35
|
+
const urlObject = new URL(url, self.location.href);
|
|
36
|
+
if (urlObject.origin !== self.location.origin) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
// Ignore search params and hash
|
|
40
|
+
urlObject.search = '';
|
|
41
|
+
urlObject.hash = '';
|
|
42
|
+
return [
|
|
43
|
+
// /blog.html
|
|
44
|
+
urlObject.href,
|
|
45
|
+
// /blog/ => /blog/index.html
|
|
46
|
+
// /blog => /blog/index.html
|
|
47
|
+
`${urlObject.href}${urlObject.pathname.endsWith('/') ? '' : '/'}index.html`,
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
(async () => {
|
|
51
|
+
const params = parseSwParams();
|
|
52
|
+
const precacheManifest = self.__WB_MANIFEST;
|
|
53
|
+
const controller = new PrecacheController({
|
|
54
|
+
// Safer to turn this true?
|
|
55
|
+
fallbackToNetwork: true,
|
|
56
|
+
});
|
|
57
|
+
if (params.offlineMode) {
|
|
58
|
+
controller.addToCacheList(precacheManifest);
|
|
59
|
+
if (params.debug) {
|
|
60
|
+
console.log('[Docusaurus-PWA][SW]: addToCacheList', { precacheManifest });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
await runSWCustomCode(params);
|
|
64
|
+
self.addEventListener('install', (event) => {
|
|
65
|
+
if (params.debug) {
|
|
66
|
+
console.log('[Docusaurus-PWA][SW]: install event', { event });
|
|
67
|
+
}
|
|
68
|
+
event.waitUntil(controller.install(event));
|
|
69
|
+
});
|
|
70
|
+
self.addEventListener('activate', (event) => {
|
|
71
|
+
if (params.debug) {
|
|
72
|
+
console.log('[Docusaurus-PWA][SW]: activate event', { event });
|
|
73
|
+
}
|
|
74
|
+
event.waitUntil(controller.activate(event));
|
|
75
|
+
});
|
|
76
|
+
self.addEventListener('fetch', async (event) => {
|
|
77
|
+
if (params.offlineMode) {
|
|
78
|
+
const requestURL = event.request.url;
|
|
79
|
+
const possibleURLs = getPossibleURLs(requestURL);
|
|
80
|
+
for (const possibleURL of possibleURLs) {
|
|
81
|
+
const cacheKey = controller.getCacheKeyForURL(possibleURL);
|
|
82
|
+
if (cacheKey) {
|
|
83
|
+
const cachedResponse = caches.match(cacheKey);
|
|
84
|
+
if (params.debug) {
|
|
85
|
+
console.log('[Docusaurus-PWA][SW]: serving cached asset', {
|
|
86
|
+
requestURL,
|
|
87
|
+
possibleURL,
|
|
88
|
+
possibleURLs,
|
|
89
|
+
cacheKey,
|
|
90
|
+
cachedResponse,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
event.respondWith(cachedResponse);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
self.addEventListener('message', async (event) => {
|
|
100
|
+
if (params.debug) {
|
|
101
|
+
console.log('[Docusaurus-PWA][SW]: message event', { event });
|
|
102
|
+
}
|
|
103
|
+
const type = event.data?.type;
|
|
104
|
+
if (type === 'SKIP_WAITING') {
|
|
105
|
+
// lib def bug, see https://github.com/microsoft/TypeScript/issues/14877
|
|
106
|
+
self.skipWaiting();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
})();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import { type ReactNode } from 'react';
|
|
8
|
+
import type { Props } from '@theme/PwaReloadPopup';
|
|
9
|
+
export default function PwaReloadPopup({ onReload }: Props): ReactNode;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
import {useState} from 'react';
|
|
8
|
+
import clsx from 'clsx';
|
|
9
|
+
import Translate, {translate} from '@docusaurus/Translate';
|
|
10
|
+
import styles from './styles.module.css';
|
|
11
|
+
export default function PwaReloadPopup({onReload}) {
|
|
12
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
13
|
+
return (
|
|
14
|
+
isVisible && (
|
|
15
|
+
<div className={clsx('alert', 'alert--secondary', styles.popup)}>
|
|
16
|
+
<p>
|
|
17
|
+
<Translate
|
|
18
|
+
id="theme.PwaReloadPopup.info"
|
|
19
|
+
description="The text for PWA reload popup">
|
|
20
|
+
New version available
|
|
21
|
+
</Translate>
|
|
22
|
+
</p>
|
|
23
|
+
<div className={styles.buttonContainer}>
|
|
24
|
+
<button
|
|
25
|
+
className="button button--link"
|
|
26
|
+
type="button"
|
|
27
|
+
onClick={() => {
|
|
28
|
+
setIsVisible(false);
|
|
29
|
+
onReload();
|
|
30
|
+
}}>
|
|
31
|
+
<Translate
|
|
32
|
+
id="theme.PwaReloadPopup.refreshButtonText"
|
|
33
|
+
description="The text for PWA reload button">
|
|
34
|
+
Refresh
|
|
35
|
+
</Translate>
|
|
36
|
+
</button>
|
|
37
|
+
|
|
38
|
+
<button
|
|
39
|
+
aria-label={translate({
|
|
40
|
+
id: 'theme.PwaReloadPopup.closeButtonAriaLabel',
|
|
41
|
+
message: 'Close',
|
|
42
|
+
description:
|
|
43
|
+
'The ARIA label for close button of PWA reload popup',
|
|
44
|
+
})}
|
|
45
|
+
className="close"
|
|
46
|
+
type="button"
|
|
47
|
+
onClick={() => setIsVisible(false)}>
|
|
48
|
+
<span aria-hidden="true">×</span>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.popup {
|
|
9
|
+
position: fixed;
|
|
10
|
+
bottom: 16px;
|
|
11
|
+
right: 16px;
|
|
12
|
+
width: 344px;
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
z-index: 999;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.popup p {
|
|
20
|
+
margin: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@media screen and (width <= 500px) {
|
|
24
|
+
.popup {
|
|
25
|
+
width: 100%;
|
|
26
|
+
bottom: 0;
|
|
27
|
+
right: 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.buttonContainer {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.buttonContainer :global(.close) {
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding: 0;
|
|
39
|
+
}
|