@docusaurus/plugin-pwa 2.0.0-beta.15d451942 → 2.0.0-beta.18
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/lib/index.d.ts +10 -0
- package/lib/index.js +164 -0
- package/lib/options.d.ts +9 -0
- package/lib/options.js +46 -0
- package/lib/registerSw.js +306 -0
- package/lib/renderReloadPopup.js +26 -0
- package/lib/sw.js +146 -0
- package/lib/theme/PwaReloadPopup/index.d.ts +9 -0
- package/lib/theme/PwaReloadPopup/index.js +38 -0
- package/lib/theme/PwaReloadPopup/styles.module.css +39 -0
- package/package.json +30 -18
- package/src/{index.js → index.ts} +69 -39
- package/src/{pluginOptionSchema.js → options.ts} +12 -4
- package/src/plugin-pwa.d.ts +96 -0
- package/src/registerSw.js +9 -6
- package/src/sw.js +4 -3
- package/src/theme/PwaReloadPopup/{index.js → index.tsx} +3 -1
- package/src/types.d.ts +8 -0
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>;
|
|
10
|
+
export { validateOptions } from './options';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
const tslib_1 = require("tslib");
|
|
11
|
+
const utils_1 = require("@docusaurus/utils");
|
|
12
|
+
const utils_2 = require("@docusaurus/core/lib/webpack/utils");
|
|
13
|
+
const LogPlugin_1 = tslib_1.__importDefault(require("@docusaurus/core/lib/webpack/plugins/LogPlugin"));
|
|
14
|
+
const theme_translations_1 = require("@docusaurus/theme-translations");
|
|
15
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
16
|
+
const webpack_1 = tslib_1.__importDefault(require("webpack"));
|
|
17
|
+
const terser_webpack_plugin_1 = tslib_1.__importDefault(require("terser-webpack-plugin"));
|
|
18
|
+
const workbox_build_1 = require("workbox-build");
|
|
19
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
20
|
+
function getSWBabelLoader() {
|
|
21
|
+
return {
|
|
22
|
+
loader: 'babel-loader',
|
|
23
|
+
options: {
|
|
24
|
+
babelrc: false,
|
|
25
|
+
configFile: false,
|
|
26
|
+
presets: [
|
|
27
|
+
[
|
|
28
|
+
require.resolve('@babel/preset-env'),
|
|
29
|
+
{
|
|
30
|
+
useBuiltIns: 'entry',
|
|
31
|
+
corejs: '3',
|
|
32
|
+
// See https://twitter.com/jeffposnick/status/1280223070876315649
|
|
33
|
+
targets: 'chrome >= 56',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function pluginPWA(context, options) {
|
|
41
|
+
const { outDir, baseUrl, i18n: { currentLocale }, } = context;
|
|
42
|
+
const { debug, offlineModeActivationStrategies, injectManifestConfig, reloadPopup, pwaHead, swCustom, swRegister, } = options;
|
|
43
|
+
return {
|
|
44
|
+
name: 'docusaurus-plugin-pwa',
|
|
45
|
+
getThemePath() {
|
|
46
|
+
return '../lib/theme';
|
|
47
|
+
},
|
|
48
|
+
getTypeScriptThemePath() {
|
|
49
|
+
return '../src/theme';
|
|
50
|
+
},
|
|
51
|
+
getClientModules() {
|
|
52
|
+
return isProd && swRegister ? [swRegister] : [];
|
|
53
|
+
},
|
|
54
|
+
getDefaultCodeTranslationMessages() {
|
|
55
|
+
return (0, theme_translations_1.readDefaultCodeTranslationMessages)({
|
|
56
|
+
locale: currentLocale,
|
|
57
|
+
name: 'plugin-pwa',
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
configureWebpack(config) {
|
|
61
|
+
if (!isProd) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
plugins: [
|
|
66
|
+
new webpack_1.default.EnvironmentPlugin({
|
|
67
|
+
PWA_DEBUG: debug,
|
|
68
|
+
PWA_SERVICE_WORKER_URL: path_1.default.posix.resolve(`${config.output?.publicPath || '/'}`, 'sw.js'),
|
|
69
|
+
PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES: offlineModeActivationStrategies,
|
|
70
|
+
PWA_RELOAD_POPUP: reloadPopup,
|
|
71
|
+
}),
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
injectHtmlTags() {
|
|
76
|
+
const headTags = [];
|
|
77
|
+
if (isProd && pwaHead) {
|
|
78
|
+
pwaHead.forEach(({ tagName, ...attributes }) => {
|
|
79
|
+
['href', 'content'].forEach((attribute) => {
|
|
80
|
+
const attributeValue = attributes[attribute];
|
|
81
|
+
if (!attributeValue) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const attributePath = !!path_1.default.extname(attributeValue) && attributeValue;
|
|
85
|
+
if (attributePath && !attributePath.startsWith(baseUrl)) {
|
|
86
|
+
attributes[attribute] = (0, utils_1.normalizeUrl)([baseUrl, attributeValue]);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return headTags.push({
|
|
90
|
+
tagName,
|
|
91
|
+
attributes,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return { headTags };
|
|
96
|
+
},
|
|
97
|
+
async postBuild(props) {
|
|
98
|
+
if (!isProd) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const swSourceFileTest = /\.m?js$/;
|
|
102
|
+
const swWebpackConfig = {
|
|
103
|
+
entry: require.resolve('./sw.js'),
|
|
104
|
+
output: {
|
|
105
|
+
path: outDir,
|
|
106
|
+
filename: 'sw.js',
|
|
107
|
+
publicPath: baseUrl,
|
|
108
|
+
},
|
|
109
|
+
target: 'webworker',
|
|
110
|
+
mode: debug ? 'development' : 'production',
|
|
111
|
+
devtool: debug ? 'source-map' : false,
|
|
112
|
+
optimization: {
|
|
113
|
+
splitChunks: false,
|
|
114
|
+
minimize: !debug,
|
|
115
|
+
// see https://developers.google.com/web/tools/workbox/guides/using-bundlers#webpack
|
|
116
|
+
minimizer: debug
|
|
117
|
+
? []
|
|
118
|
+
: [
|
|
119
|
+
new terser_webpack_plugin_1.default({
|
|
120
|
+
test: swSourceFileTest,
|
|
121
|
+
}),
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
plugins: [
|
|
125
|
+
new webpack_1.default.EnvironmentPlugin({
|
|
126
|
+
PWA_SW_CUSTOM: swCustom || '', // fallback value required with Webpack 5
|
|
127
|
+
}),
|
|
128
|
+
new LogPlugin_1.default({
|
|
129
|
+
name: 'Service Worker',
|
|
130
|
+
color: 'red',
|
|
131
|
+
}),
|
|
132
|
+
],
|
|
133
|
+
module: {
|
|
134
|
+
rules: [
|
|
135
|
+
{
|
|
136
|
+
test: swSourceFileTest,
|
|
137
|
+
exclude: /node_modules/,
|
|
138
|
+
use: getSWBabelLoader(),
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
await (0, utils_2.compile)([swWebpackConfig]);
|
|
144
|
+
const swDest = path_1.default.resolve(props.outDir, 'sw.js');
|
|
145
|
+
await (0, workbox_build_1.injectManifest)({
|
|
146
|
+
...injectManifestConfig,
|
|
147
|
+
globPatterns: [
|
|
148
|
+
'**/*.{js,json,css,html}',
|
|
149
|
+
'**/*.{png,jpg,jpeg,gif,svg,ico}',
|
|
150
|
+
'**/*.{woff,woff2,eot,ttf,otf}',
|
|
151
|
+
// @ts-expect-error: internal API?
|
|
152
|
+
...(workbox_build_1.injectManifest.globPatterns ?? []),
|
|
153
|
+
],
|
|
154
|
+
// those attributes are not overrideable
|
|
155
|
+
swDest,
|
|
156
|
+
swSrc: swDest,
|
|
157
|
+
globDirectory: props.outDir,
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
exports.default = pluginPWA;
|
|
163
|
+
var options_1 = require("./options");
|
|
164
|
+
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,46 @@
|
|
|
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
|
+
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
|
+
reloadPopup: '@theme/PwaReloadPopup',
|
|
23
|
+
};
|
|
24
|
+
const Schema = utils_validation_1.Joi.object({
|
|
25
|
+
debug: utils_validation_1.Joi.bool().default(DEFAULT_OPTIONS.debug),
|
|
26
|
+
offlineModeActivationStrategies: utils_validation_1.Joi.array()
|
|
27
|
+
.items(utils_validation_1.Joi.string()
|
|
28
|
+
.valid('appInstalled', 'queryString', 'standalone', 'mobile', 'saveData', 'always')
|
|
29
|
+
.required())
|
|
30
|
+
.default(DEFAULT_OPTIONS.offlineModeActivationStrategies),
|
|
31
|
+
injectManifestConfig: utils_validation_1.Joi.object().default(DEFAULT_OPTIONS.injectManifestConfig),
|
|
32
|
+
pwaHead: utils_validation_1.Joi.array()
|
|
33
|
+
.items(utils_validation_1.Joi.object({ tagName: utils_validation_1.Joi.string().required() }).unknown().required())
|
|
34
|
+
.default(DEFAULT_OPTIONS.pwaHead),
|
|
35
|
+
swCustom: utils_validation_1.Joi.string(),
|
|
36
|
+
swRegister: utils_validation_1.Joi.alternatives()
|
|
37
|
+
.try(utils_validation_1.Joi.string(), utils_validation_1.Joi.bool().valid(false))
|
|
38
|
+
.default(DEFAULT_OPTIONS.swRegister),
|
|
39
|
+
reloadPopup: utils_validation_1.Joi.alternatives()
|
|
40
|
+
.try(utils_validation_1.Joi.string(), utils_validation_1.Joi.bool().valid(false))
|
|
41
|
+
.default(DEFAULT_OPTIONS.reloadPopup),
|
|
42
|
+
});
|
|
43
|
+
function validateOptions({ validate, options, }) {
|
|
44
|
+
return validate(Schema, options);
|
|
45
|
+
}
|
|
46
|
+
exports.validateOptions = validateOptions;
|
|
@@ -0,0 +1,306 @@
|
|
|
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
|
+
import {createStorageSlot} from '@docusaurus/theme-common';
|
|
9
|
+
|
|
10
|
+
// First: read the env variables (provided by Webpack)
|
|
11
|
+
/* eslint-disable prefer-destructuring */
|
|
12
|
+
const PWA_SERVICE_WORKER_URL = process.env.PWA_SERVICE_WORKER_URL;
|
|
13
|
+
const PWA_RELOAD_POPUP = process.env.PWA_RELOAD_POPUP;
|
|
14
|
+
const PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES =
|
|
15
|
+
process.env.PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES;
|
|
16
|
+
const PWA_DEBUG = process.env.PWA_DEBUG;
|
|
17
|
+
/* eslint-enable prefer-destructuring */
|
|
18
|
+
|
|
19
|
+
const debug = PWA_DEBUG; // shortcut
|
|
20
|
+
|
|
21
|
+
const MAX_MOBILE_WIDTH = 940;
|
|
22
|
+
|
|
23
|
+
const AppInstalledEventFiredStorage = createStorageSlot(
|
|
24
|
+
'docusaurus.pwa.event.appInstalled.fired',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
async function clearRegistrations() {
|
|
28
|
+
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
29
|
+
if (debug) {
|
|
30
|
+
console.log(
|
|
31
|
+
`[Docusaurus-PWA][registerSw]: will unregister all service worker registrations`,
|
|
32
|
+
registrations,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
await Promise.all(
|
|
36
|
+
registrations.map(async (registration) => {
|
|
37
|
+
const result = await registration?.registration?.unregister();
|
|
38
|
+
if (debug) {
|
|
39
|
+
console.log(
|
|
40
|
+
`[Docusaurus-PWA][registerSw]: unregister() service worker registration`,
|
|
41
|
+
registrations,
|
|
42
|
+
result,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
if (debug) {
|
|
48
|
+
console.log(
|
|
49
|
+
`[Docusaurus-PWA][registerSw]: unregistered all service worker registrations`,
|
|
50
|
+
registrations,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
window.location.reload();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
As of 2021:
|
|
58
|
+
It is complicated and not very reliable to detect an app is actually installed.
|
|
59
|
+
https://stackoverflow.com/questions/51735869/check-if-user-has-already-installed-pwa-to-homescreen-on-chrome
|
|
60
|
+
|
|
61
|
+
- 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
|
|
62
|
+
- getInstalledRelatedApps() is only supported in recent Chrome and does not seem to reliable either https://github.com/WICG/get-installed-related-apps
|
|
63
|
+
- 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/
|
|
64
|
+
*/
|
|
65
|
+
async function isAppInstalledEventFired() {
|
|
66
|
+
return AppInstalledEventFiredStorage.get() === 'true';
|
|
67
|
+
}
|
|
68
|
+
async function isAppInstalledRelatedApps() {
|
|
69
|
+
if ('getInstalledRelatedApps' in window.navigator) {
|
|
70
|
+
const relatedApps = await navigator.getInstalledRelatedApps();
|
|
71
|
+
return relatedApps.some((app) => app.platform === 'webapp');
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
function isStandaloneDisplayMode() {
|
|
76
|
+
return window.matchMedia('(display-mode: standalone)').matches;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const OfflineModeActivationStrategiesImplementations = {
|
|
80
|
+
always: () => true,
|
|
81
|
+
mobile: () => window.innerWidth <= MAX_MOBILE_WIDTH,
|
|
82
|
+
saveData: () => !!navigator.connection?.saveData,
|
|
83
|
+
appInstalled: async () => {
|
|
84
|
+
const installedEventFired = await isAppInstalledEventFired();
|
|
85
|
+
const installedRelatedApps = await isAppInstalledRelatedApps();
|
|
86
|
+
return installedEventFired || installedRelatedApps;
|
|
87
|
+
},
|
|
88
|
+
standalone: () => isStandaloneDisplayMode(),
|
|
89
|
+
queryString: () =>
|
|
90
|
+
new URLSearchParams(window.location.search).get('offlineMode') === 'true',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
async function isStrategyActive(strategyName) {
|
|
94
|
+
return OfflineModeActivationStrategiesImplementations[strategyName]();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function getActiveStrategies() {
|
|
98
|
+
const activeStrategies = await Promise.all(
|
|
99
|
+
PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.map(async (strategyName) => {
|
|
100
|
+
const isActive = await isStrategyActive(strategyName);
|
|
101
|
+
return isActive ? strategyName : undefined;
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
return activeStrategies.filter(Boolean); // remove undefined values
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function isOfflineModeEnabled() {
|
|
108
|
+
const activeStrategies = await getActiveStrategies();
|
|
109
|
+
const enabled = activeStrategies.length > 0;
|
|
110
|
+
if (debug) {
|
|
111
|
+
const logObject = {
|
|
112
|
+
activeStrategies,
|
|
113
|
+
availableStrategies: PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES,
|
|
114
|
+
};
|
|
115
|
+
if (enabled) {
|
|
116
|
+
console.log(
|
|
117
|
+
'[Docusaurus-PWA][registerSw]: offline mode enabled, because of activation strategies',
|
|
118
|
+
logObject,
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
console.log(
|
|
122
|
+
'[Docusaurus-PWA][registerSw]: offline mode disabled, because none of the offlineModeActivationStrategies could be used',
|
|
123
|
+
logObject,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return enabled;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createServiceWorkerUrl(params) {
|
|
131
|
+
const paramsQueryString = JSON.stringify(params);
|
|
132
|
+
const url = `${PWA_SERVICE_WORKER_URL}?params=${encodeURIComponent(
|
|
133
|
+
paramsQueryString,
|
|
134
|
+
)}`;
|
|
135
|
+
if (debug) {
|
|
136
|
+
console.log(`[Docusaurus-PWA][registerSw]: service worker url`, {
|
|
137
|
+
url,
|
|
138
|
+
params,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return url;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function registerSW() {
|
|
145
|
+
const {Workbox} = await import('workbox-window');
|
|
146
|
+
|
|
147
|
+
const offlineMode = await isOfflineModeEnabled();
|
|
148
|
+
|
|
149
|
+
const url = createServiceWorkerUrl({offlineMode, debug});
|
|
150
|
+
const wb = new Workbox(url);
|
|
151
|
+
|
|
152
|
+
const registration = await wb.register();
|
|
153
|
+
|
|
154
|
+
const sendSkipWaiting = () => wb.messageSW({type: 'SKIP_WAITING'});
|
|
155
|
+
|
|
156
|
+
const handleServiceWorkerWaiting = async () => {
|
|
157
|
+
if (debug) {
|
|
158
|
+
console.log('[Docusaurus-PWA][registerSw]: handleServiceWorkerWaiting');
|
|
159
|
+
}
|
|
160
|
+
// Immediately load new service worker when files aren't cached
|
|
161
|
+
if (!offlineMode) {
|
|
162
|
+
sendSkipWaiting();
|
|
163
|
+
} else if (PWA_RELOAD_POPUP) {
|
|
164
|
+
const renderReloadPopup = (await import('./renderReloadPopup')).default;
|
|
165
|
+
await renderReloadPopup({
|
|
166
|
+
onReload() {
|
|
167
|
+
wb.addEventListener('controlling', () => {
|
|
168
|
+
window.location.reload();
|
|
169
|
+
});
|
|
170
|
+
sendSkipWaiting();
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (debug) {
|
|
177
|
+
if (registration.active) {
|
|
178
|
+
console.log(
|
|
179
|
+
'[Docusaurus-PWA][registerSw]: registration.active',
|
|
180
|
+
registration,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (registration.installing) {
|
|
184
|
+
console.log(
|
|
185
|
+
'[Docusaurus-PWA][registerSw]: registration.installing',
|
|
186
|
+
registration,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (registration.waiting) {
|
|
190
|
+
console.log(
|
|
191
|
+
'[Docusaurus-PWA][registerSw]: registration.waiting',
|
|
192
|
+
registration,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Update the current service worker when the next one has finished
|
|
198
|
+
// installing and transitions to waiting state.
|
|
199
|
+
wb.addEventListener('waiting', (event) => {
|
|
200
|
+
if (debug) {
|
|
201
|
+
console.log('[Docusaurus-PWA][registerSw]: event waiting', event);
|
|
202
|
+
}
|
|
203
|
+
handleServiceWorkerWaiting();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Update current service worker if the next one finishes installing and
|
|
207
|
+
// moves to waiting state in another tab.
|
|
208
|
+
wb.addEventListener('externalwaiting', (event) => {
|
|
209
|
+
if (debug) {
|
|
210
|
+
console.log('[Docusaurus-PWA][registerSw]: event externalwaiting', event);
|
|
211
|
+
}
|
|
212
|
+
handleServiceWorkerWaiting();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Update service worker if the next one is already in the waiting state.
|
|
216
|
+
// This happens when the user doesn't click on `reload` in the popup.
|
|
217
|
+
if (registration.waiting) {
|
|
218
|
+
await handleServiceWorkerWaiting();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// TODO these events still works in chrome but have been removed from the spec
|
|
223
|
+
// in 2019! See https://github.com/w3c/manifest/pull/836
|
|
224
|
+
function addLegacyAppInstalledEventsListeners() {
|
|
225
|
+
if (typeof window !== 'undefined') {
|
|
226
|
+
if (debug) {
|
|
227
|
+
console.log(
|
|
228
|
+
'[Docusaurus-PWA][registerSw]: addLegacyAppInstalledEventsListeners',
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
window.addEventListener('appinstalled', async (event) => {
|
|
233
|
+
if (debug) {
|
|
234
|
+
console.log('[Docusaurus-PWA][registerSw]: event appinstalled', event);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
AppInstalledEventFiredStorage.set('true');
|
|
238
|
+
if (debug) {
|
|
239
|
+
console.log(
|
|
240
|
+
"[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.set('true')",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// After the app is installed, we register a service worker with the path
|
|
245
|
+
// `/sw?enabled`. Since the previous service worker was `/sw`, it'll be
|
|
246
|
+
// treated as a new one. The previous registration will need to be
|
|
247
|
+
// cleared, otherwise the reload popup will show.
|
|
248
|
+
await clearRegistrations();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// TODO this event still works in chrome but has been removed from the spec
|
|
252
|
+
// in 2019!!!
|
|
253
|
+
window.addEventListener('beforeinstallprompt', async (event) => {
|
|
254
|
+
if (debug) {
|
|
255
|
+
console.log(
|
|
256
|
+
'[Docusaurus-PWA][registerSw]: event beforeinstallprompt',
|
|
257
|
+
event,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
// TODO instead of default browser install UI, show custom docusaurus
|
|
261
|
+
// prompt?
|
|
262
|
+
// event.preventDefault();
|
|
263
|
+
if (debug) {
|
|
264
|
+
console.log(
|
|
265
|
+
'[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.get()',
|
|
266
|
+
AppInstalledEventFiredStorage.get(),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (AppInstalledEventFiredStorage.get()) {
|
|
270
|
+
AppInstalledEventFiredStorage.del();
|
|
271
|
+
if (debug) {
|
|
272
|
+
console.log(
|
|
273
|
+
'[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.del()',
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
// After uninstalling the app, if the user doesn't clear all data, then
|
|
277
|
+
// the previous service worker will continue serving cached files. We
|
|
278
|
+
// need to clear registrations and reload, otherwise the popup shows.
|
|
279
|
+
await clearRegistrations();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (debug) {
|
|
284
|
+
console.log(
|
|
285
|
+
'[Docusaurus-PWA][registerSw]: legacy appinstalled and beforeinstallprompt event listeners installed',
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/*
|
|
292
|
+
Init code to run on the client!
|
|
293
|
+
*/
|
|
294
|
+
if (typeof window !== 'undefined') {
|
|
295
|
+
if (debug) {
|
|
296
|
+
console.log('[Docusaurus-PWA][registerSw]: debug mode enabled');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if ('serviceWorker' in navigator) {
|
|
300
|
+
// First: add the listeners asap/synchronously
|
|
301
|
+
addLegacyAppInstalledEventsListeners();
|
|
302
|
+
|
|
303
|
+
// Then try to register the SW using lazy/dynamic imports
|
|
304
|
+
registerSW().catch((e) => console.error('registerSW failed', e));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
import React from 'react';
|
|
9
|
+
import {render} from 'react-dom';
|
|
10
|
+
|
|
11
|
+
const POPUP_CONTAINER_ID = 'pwa-popup-container';
|
|
12
|
+
|
|
13
|
+
const getContainer = () => document.getElementById(POPUP_CONTAINER_ID);
|
|
14
|
+
|
|
15
|
+
const createContainer = () => {
|
|
16
|
+
const container = document.createElement('div');
|
|
17
|
+
container.id = POPUP_CONTAINER_ID;
|
|
18
|
+
document.body.appendChild(container);
|
|
19
|
+
return container;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default async function renderReloadPopup(props) {
|
|
23
|
+
const container = getContainer() || createContainer();
|
|
24
|
+
const {default: ReloadPopup} = await import(process.env.PWA_RELOAD_POPUP);
|
|
25
|
+
render(<ReloadPopup {...props} />, container);
|
|
26
|
+
}
|
package/lib/sw.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
/* eslint-disable no-restricted-globals */
|
|
8
|
+
|
|
9
|
+
import {PrecacheController} from 'workbox-precaching';
|
|
10
|
+
|
|
11
|
+
function parseSwParams() {
|
|
12
|
+
const params = JSON.parse(
|
|
13
|
+
new URLSearchParams(self.location.search).get('params'),
|
|
14
|
+
);
|
|
15
|
+
if (params.debug) {
|
|
16
|
+
console.log('[Docusaurus-PWA][SW]: Service Worker params:', params);
|
|
17
|
+
}
|
|
18
|
+
return params;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// doc advises against dynamic imports in SW
|
|
22
|
+
// https://developers.google.com/web/tools/workbox/guides/using-bundlers#code_splitting_and_dynamic_imports
|
|
23
|
+
// https://twitter.com/sebastienlorber/status/1280155204575518720
|
|
24
|
+
// but looks it's working fine as it's inlined by webpack, need to double check
|
|
25
|
+
async function runSWCustomCode(params) {
|
|
26
|
+
if (process.env.PWA_SW_CUSTOM) {
|
|
27
|
+
const customSW = await import(process.env.PWA_SW_CUSTOM);
|
|
28
|
+
if (typeof customSW.default === 'function') {
|
|
29
|
+
customSW.default(params);
|
|
30
|
+
} else if (params.debug) {
|
|
31
|
+
console.warn(
|
|
32
|
+
'[Docusaurus-PWA][SW]: swCustom should have a default export function',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets different possible variations for a request URL. Similar to
|
|
40
|
+
* https://git.io/JvixK
|
|
41
|
+
*
|
|
42
|
+
* @param {string} url
|
|
43
|
+
*/
|
|
44
|
+
function getPossibleURLs(url) {
|
|
45
|
+
const possibleURLs = [];
|
|
46
|
+
const urlObject = new URL(url, self.location.href);
|
|
47
|
+
|
|
48
|
+
if (urlObject.origin !== self.location.origin) {
|
|
49
|
+
return possibleURLs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Ignore search params and hash
|
|
53
|
+
urlObject.search = '';
|
|
54
|
+
urlObject.hash = '';
|
|
55
|
+
|
|
56
|
+
// /blog.html
|
|
57
|
+
possibleURLs.push(urlObject.href);
|
|
58
|
+
|
|
59
|
+
// /blog/ => /blog/index.html
|
|
60
|
+
if (urlObject.pathname.endsWith('/')) {
|
|
61
|
+
possibleURLs.push(`${urlObject.href}index.html`);
|
|
62
|
+
} else {
|
|
63
|
+
// /blog => /blog/index.html
|
|
64
|
+
possibleURLs.push(`${urlObject.href}/index.html`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return possibleURLs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
(async () => {
|
|
71
|
+
const params = parseSwParams();
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
74
|
+
const precacheManifest = self.__WB_MANIFEST;
|
|
75
|
+
const controller = new PrecacheController({
|
|
76
|
+
fallbackToNetwork: true, // safer to turn this true?
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (params.offlineMode) {
|
|
80
|
+
controller.addToCacheList(precacheManifest);
|
|
81
|
+
if (params.debug) {
|
|
82
|
+
console.log('[Docusaurus-PWA][SW]: addToCacheList', {
|
|
83
|
+
precacheManifest,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await runSWCustomCode(params);
|
|
89
|
+
|
|
90
|
+
self.addEventListener('install', (event) => {
|
|
91
|
+
if (params.debug) {
|
|
92
|
+
console.log('[Docusaurus-PWA][SW]: install event', {
|
|
93
|
+
event,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
event.waitUntil(controller.install(event));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
self.addEventListener('activate', (event) => {
|
|
100
|
+
if (params.debug) {
|
|
101
|
+
console.log('[Docusaurus-PWA][SW]: activate event', {
|
|
102
|
+
event,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
event.waitUntil(controller.activate(event));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
self.addEventListener('fetch', async (event) => {
|
|
109
|
+
if (params.offlineMode) {
|
|
110
|
+
const requestURL = event.request.url;
|
|
111
|
+
const possibleURLs = getPossibleURLs(requestURL);
|
|
112
|
+
for (let i = 0; i < possibleURLs.length; i += 1) {
|
|
113
|
+
const possibleURL = possibleURLs[i];
|
|
114
|
+
const cacheKey = controller.getCacheKeyForURL(possibleURL);
|
|
115
|
+
if (cacheKey) {
|
|
116
|
+
const cachedResponse = caches.match(cacheKey);
|
|
117
|
+
if (params.debug) {
|
|
118
|
+
console.log('[Docusaurus-PWA][SW]: serving cached asset', {
|
|
119
|
+
requestURL,
|
|
120
|
+
possibleURL,
|
|
121
|
+
possibleURLs,
|
|
122
|
+
cacheKey,
|
|
123
|
+
cachedResponse,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
event.respondWith(cachedResponse);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
self.addEventListener('message', async (event) => {
|
|
134
|
+
if (params.debug) {
|
|
135
|
+
console.log('[Docusaurus-PWA][SW]: message event', {
|
|
136
|
+
event,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const type = event.data?.type;
|
|
141
|
+
|
|
142
|
+
if (type === 'SKIP_WAITING') {
|
|
143
|
+
self.skipWaiting();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
})();
|
|
@@ -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
|
+
/// <reference types="react" />
|
|
8
|
+
import type { Props } from '@theme/PwaReloadPopup';
|
|
9
|
+
export default function PwaReloadPopup({ onReload }: Props): JSX.Element | false;
|
|
@@ -0,0 +1,38 @@
|
|
|
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 React, { 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 (isVisible && (<div className={clsx('alert', 'alert--secondary', styles.popup)}>
|
|
14
|
+
<p>
|
|
15
|
+
<Translate id="theme.PwaReloadPopup.info" description="The text for PWA reload popup">
|
|
16
|
+
New version available
|
|
17
|
+
</Translate>
|
|
18
|
+
</p>
|
|
19
|
+
<div className={styles.buttonContainer}>
|
|
20
|
+
<button className="button button--link" type="button" onClick={() => {
|
|
21
|
+
setIsVisible(false);
|
|
22
|
+
onReload();
|
|
23
|
+
}}>
|
|
24
|
+
<Translate id="theme.PwaReloadPopup.refreshButtonText" description="The text for PWA reload button">
|
|
25
|
+
Refresh
|
|
26
|
+
</Translate>
|
|
27
|
+
</button>
|
|
28
|
+
|
|
29
|
+
<button aria-label={translate({
|
|
30
|
+
id: 'theme.PwaReloadPopup.closeButtonAriaLabel',
|
|
31
|
+
message: 'Close',
|
|
32
|
+
description: 'The ARIA label for close button of PWA reload popup',
|
|
33
|
+
})} className="close" type="button" onClick={() => setIsVisible(false)}>
|
|
34
|
+
<span aria-hidden="true">×</span>
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>));
|
|
38
|
+
}
|
|
@@ -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 (max-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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docusaurus/plugin-pwa",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.18",
|
|
4
4
|
"description": "Docusaurus Plugin to add PWA support.",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "src/plugin-pwa.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "yarn build:server && yarn build:browser && yarn build:copy",
|
|
9
|
+
"build:server": "tsc --project tsconfig.server.json",
|
|
10
|
+
"build:browser": "tsc --project tsconfig.browser.json",
|
|
11
|
+
"build:copy": "node copyUntypedFiles.mjs"
|
|
12
|
+
},
|
|
6
13
|
"publishConfig": {
|
|
7
14
|
"access": "public"
|
|
8
15
|
},
|
|
@@ -13,26 +20,31 @@
|
|
|
13
20
|
},
|
|
14
21
|
"license": "MIT",
|
|
15
22
|
"dependencies": {
|
|
16
|
-
"@babel/
|
|
17
|
-
"@babel/
|
|
18
|
-
"@
|
|
19
|
-
"@docusaurus/
|
|
20
|
-
"@docusaurus/theme-
|
|
21
|
-
"@docusaurus/utils
|
|
22
|
-
"
|
|
23
|
+
"@babel/core": "^7.17.8",
|
|
24
|
+
"@babel/preset-env": "^7.16.11",
|
|
25
|
+
"@docusaurus/core": "2.0.0-beta.18",
|
|
26
|
+
"@docusaurus/theme-common": "2.0.0-beta.18",
|
|
27
|
+
"@docusaurus/theme-translations": "2.0.0-beta.18",
|
|
28
|
+
"@docusaurus/utils": "2.0.0-beta.18",
|
|
29
|
+
"@docusaurus/utils-validation": "2.0.0-beta.18",
|
|
30
|
+
"babel-loader": "^8.2.4",
|
|
23
31
|
"clsx": "^1.1.1",
|
|
24
|
-
"core-js": "^
|
|
25
|
-
"terser-webpack-plugin": "^5.
|
|
26
|
-
"
|
|
27
|
-
"webpack
|
|
28
|
-
"
|
|
29
|
-
"workbox-
|
|
30
|
-
"workbox-
|
|
32
|
+
"core-js": "^3.21.1",
|
|
33
|
+
"terser-webpack-plugin": "^5.3.1",
|
|
34
|
+
"tslib": "^2.3.1",
|
|
35
|
+
"webpack": "^5.70.0",
|
|
36
|
+
"webpack-merge": "^5.8.0",
|
|
37
|
+
"workbox-build": "^6.5.2",
|
|
38
|
+
"workbox-precaching": "^6.5.2",
|
|
39
|
+
"workbox-window": "^6.5.2"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@docusaurus/module-type-aliases": "2.0.0-beta.18",
|
|
43
|
+
"fs-extra": "^10.0.1"
|
|
31
44
|
},
|
|
32
45
|
"peerDependencies": {
|
|
33
|
-
"@babel/core": "^7.0.0",
|
|
34
46
|
"react": "^16.8.4 || ^17.0.0",
|
|
35
47
|
"react-dom": "^16.8.4 || ^17.0.0"
|
|
36
48
|
},
|
|
37
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "1a945d06993d53376e61bed2c942799fe07dc336"
|
|
38
50
|
}
|
|
@@ -5,14 +5,18 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
|
|
9
|
+
import type {PluginOptions} from '@docusaurus/plugin-pwa';
|
|
10
|
+
import {normalizeUrl} from '@docusaurus/utils';
|
|
11
|
+
import {compile} from '@docusaurus/core/lib/webpack/utils';
|
|
12
|
+
import LogPlugin from '@docusaurus/core/lib/webpack/plugins/LogPlugin';
|
|
13
|
+
import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
|
|
14
|
+
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import webpack, {type Configuration} from 'webpack';
|
|
17
|
+
import Terser from 'terser-webpack-plugin';
|
|
18
|
+
|
|
19
|
+
import {injectManifest} from 'workbox-build';
|
|
16
20
|
|
|
17
21
|
const isProd = process.env.NODE_ENV === 'production';
|
|
18
22
|
|
|
@@ -33,17 +37,19 @@ function getSWBabelLoader() {
|
|
|
33
37
|
},
|
|
34
38
|
],
|
|
35
39
|
],
|
|
36
|
-
plugins: [
|
|
37
|
-
require.resolve('@babel/plugin-proposal-object-rest-spread'),
|
|
38
|
-
require.resolve('@babel/plugin-proposal-optional-chaining'),
|
|
39
|
-
require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'),
|
|
40
|
-
],
|
|
41
40
|
},
|
|
42
41
|
};
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
function
|
|
46
|
-
|
|
44
|
+
export default function pluginPWA(
|
|
45
|
+
context: LoadContext,
|
|
46
|
+
options: PluginOptions,
|
|
47
|
+
): Plugin<void> {
|
|
48
|
+
const {
|
|
49
|
+
outDir,
|
|
50
|
+
baseUrl,
|
|
51
|
+
i18n: {currentLocale},
|
|
52
|
+
} = context;
|
|
47
53
|
const {
|
|
48
54
|
debug,
|
|
49
55
|
offlineModeActivationStrategies,
|
|
@@ -58,11 +64,21 @@ function plugin(context, options) {
|
|
|
58
64
|
name: 'docusaurus-plugin-pwa',
|
|
59
65
|
|
|
60
66
|
getThemePath() {
|
|
61
|
-
return
|
|
67
|
+
return '../lib/theme';
|
|
68
|
+
},
|
|
69
|
+
getTypeScriptThemePath() {
|
|
70
|
+
return '../src/theme';
|
|
62
71
|
},
|
|
63
72
|
|
|
64
73
|
getClientModules() {
|
|
65
|
-
return isProd ? [swRegister] : [];
|
|
74
|
+
return isProd && swRegister ? [swRegister] : [];
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
getDefaultCodeTranslationMessages() {
|
|
78
|
+
return readDefaultCodeTranslationMessages({
|
|
79
|
+
locale: currentLocale,
|
|
80
|
+
name: 'plugin-pwa',
|
|
81
|
+
});
|
|
66
82
|
},
|
|
67
83
|
|
|
68
84
|
configureWebpack(config) {
|
|
@@ -75,10 +91,11 @@ function plugin(context, options) {
|
|
|
75
91
|
new webpack.EnvironmentPlugin({
|
|
76
92
|
PWA_DEBUG: debug,
|
|
77
93
|
PWA_SERVICE_WORKER_URL: path.posix.resolve(
|
|
78
|
-
`${config.output
|
|
94
|
+
`${config.output?.publicPath || '/'}`,
|
|
79
95
|
'sw.js',
|
|
80
96
|
),
|
|
81
|
-
PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES:
|
|
97
|
+
PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES:
|
|
98
|
+
offlineModeActivationStrategies,
|
|
82
99
|
PWA_RELOAD_POPUP: reloadPopup,
|
|
83
100
|
}),
|
|
84
101
|
],
|
|
@@ -86,14 +103,29 @@ function plugin(context, options) {
|
|
|
86
103
|
},
|
|
87
104
|
|
|
88
105
|
injectHtmlTags() {
|
|
89
|
-
const headTags = [];
|
|
106
|
+
const headTags: HtmlTags = [];
|
|
90
107
|
if (isProd && pwaHead) {
|
|
91
|
-
pwaHead.forEach(({tagName, ...attributes}) =>
|
|
92
|
-
|
|
108
|
+
pwaHead.forEach(({tagName, ...attributes}) => {
|
|
109
|
+
(['href', 'content'] as const).forEach((attribute) => {
|
|
110
|
+
const attributeValue = attributes[attribute];
|
|
111
|
+
|
|
112
|
+
if (!attributeValue) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const attributePath =
|
|
117
|
+
!!path.extname(attributeValue) && attributeValue;
|
|
118
|
+
|
|
119
|
+
if (attributePath && !attributePath.startsWith(baseUrl)) {
|
|
120
|
+
attributes[attribute] = normalizeUrl([baseUrl, attributeValue]);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return headTags.push({
|
|
93
125
|
tagName,
|
|
94
126
|
attributes,
|
|
95
|
-
})
|
|
96
|
-
);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
97
129
|
}
|
|
98
130
|
return {headTags};
|
|
99
131
|
},
|
|
@@ -105,8 +137,8 @@ function plugin(context, options) {
|
|
|
105
137
|
|
|
106
138
|
const swSourceFileTest = /\.m?js$/;
|
|
107
139
|
|
|
108
|
-
const swWebpackConfig = {
|
|
109
|
-
entry:
|
|
140
|
+
const swWebpackConfig: Configuration = {
|
|
141
|
+
entry: require.resolve('./sw.js'),
|
|
110
142
|
output: {
|
|
111
143
|
path: outDir,
|
|
112
144
|
filename: 'sw.js',
|
|
@@ -119,12 +151,13 @@ function plugin(context, options) {
|
|
|
119
151
|
splitChunks: false,
|
|
120
152
|
minimize: !debug,
|
|
121
153
|
// see https://developers.google.com/web/tools/workbox/guides/using-bundlers#webpack
|
|
122
|
-
minimizer:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
154
|
+
minimizer: debug
|
|
155
|
+
? []
|
|
156
|
+
: [
|
|
157
|
+
new Terser({
|
|
158
|
+
test: swSourceFileTest,
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
128
161
|
},
|
|
129
162
|
plugins: [
|
|
130
163
|
new webpack.EnvironmentPlugin({
|
|
@@ -139,7 +172,7 @@ function plugin(context, options) {
|
|
|
139
172
|
rules: [
|
|
140
173
|
{
|
|
141
174
|
test: swSourceFileTest,
|
|
142
|
-
exclude: /
|
|
175
|
+
exclude: /node_modules/,
|
|
143
176
|
use: getSWBabelLoader(),
|
|
144
177
|
},
|
|
145
178
|
],
|
|
@@ -156,7 +189,8 @@ function plugin(context, options) {
|
|
|
156
189
|
'**/*.{js,json,css,html}',
|
|
157
190
|
'**/*.{png,jpg,jpeg,gif,svg,ico}',
|
|
158
191
|
'**/*.{woff,woff2,eot,ttf,otf}',
|
|
159
|
-
|
|
192
|
+
// @ts-expect-error: internal API?
|
|
193
|
+
...(injectManifest.globPatterns ?? []),
|
|
160
194
|
],
|
|
161
195
|
// those attributes are not overrideable
|
|
162
196
|
swDest,
|
|
@@ -167,8 +201,4 @@ function plugin(context, options) {
|
|
|
167
201
|
};
|
|
168
202
|
}
|
|
169
203
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
plugin.validateOptions = function validateOptions({validate, options}) {
|
|
173
|
-
return validate(PluginOptionSchema, options);
|
|
174
|
-
};
|
|
204
|
+
export {validateOptions} from './options';
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import {Joi} from '@docusaurus/utils-validation';
|
|
9
|
+
import type {OptionValidationContext} from '@docusaurus/types';
|
|
10
|
+
import type {PluginOptions} from '@docusaurus/plugin-pwa';
|
|
10
11
|
|
|
11
12
|
const DEFAULT_OPTIONS = {
|
|
12
13
|
debug: false,
|
|
@@ -18,11 +19,11 @@ const DEFAULT_OPTIONS = {
|
|
|
18
19
|
injectManifestConfig: {},
|
|
19
20
|
pwaHead: [],
|
|
20
21
|
swCustom: undefined,
|
|
21
|
-
swRegister:
|
|
22
|
+
swRegister: './registerSw.js',
|
|
22
23
|
reloadPopup: '@theme/PwaReloadPopup',
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
const Schema = Joi.object({
|
|
26
27
|
debug: Joi.bool().default(DEFAULT_OPTIONS.debug),
|
|
27
28
|
offlineModeActivationStrategies: Joi.array()
|
|
28
29
|
.items(
|
|
@@ -52,3 +53,10 @@ exports.PluginOptionSchema = Joi.object({
|
|
|
52
53
|
.try(Joi.string(), Joi.bool().valid(false))
|
|
53
54
|
.default(DEFAULT_OPTIONS.reloadPopup),
|
|
54
55
|
});
|
|
56
|
+
|
|
57
|
+
export function validateOptions({
|
|
58
|
+
validate,
|
|
59
|
+
options,
|
|
60
|
+
}: OptionValidationContext<PluginOptions, PluginOptions>): PluginOptions {
|
|
61
|
+
return validate(Schema, options);
|
|
62
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
declare module '@docusaurus/plugin-pwa' {
|
|
9
|
+
import type {InjectManifestOptions} from 'workbox-build';
|
|
10
|
+
|
|
11
|
+
export type PluginOptions = {
|
|
12
|
+
/**
|
|
13
|
+
* Turn debug mode on:
|
|
14
|
+
*
|
|
15
|
+
* - Workbox logs
|
|
16
|
+
* - Additional Docusaurus logs
|
|
17
|
+
* - Unoptimized SW file output
|
|
18
|
+
* - Source maps
|
|
19
|
+
*/
|
|
20
|
+
debug?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Strategies used to turn the offline mode on:
|
|
23
|
+
*
|
|
24
|
+
* - `appInstalled`: activates for users having installed the site as an app
|
|
25
|
+
* (not 100% reliable)
|
|
26
|
+
* - `standalone`: activates for users running the app as standalone (often
|
|
27
|
+
* the case once a PWA is installed)
|
|
28
|
+
* - `queryString`: activates if queryString contains `offlineMode=true`
|
|
29
|
+
* (convenient for PWA debugging)
|
|
30
|
+
* - `mobile`: activates for mobile users (width <= 940px)
|
|
31
|
+
* - `saveData`: activates for users with `navigator.connection.saveData ===
|
|
32
|
+
* true`
|
|
33
|
+
* - `always`: activates for all users
|
|
34
|
+
*/
|
|
35
|
+
offlineModeActivationStrategies: (
|
|
36
|
+
| 'appInstalled'
|
|
37
|
+
| 'queryString'
|
|
38
|
+
| 'standalone'
|
|
39
|
+
| 'mobile'
|
|
40
|
+
| 'saveData'
|
|
41
|
+
| 'always'
|
|
42
|
+
)[];
|
|
43
|
+
/**
|
|
44
|
+
* Workbox options to pass to `workbox.injectManifest()`. This gives you
|
|
45
|
+
* control over which assets will be precached, and be available offline.
|
|
46
|
+
* @see https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build#.injectManifest
|
|
47
|
+
*/
|
|
48
|
+
injectManifestConfig: InjectManifestOptions;
|
|
49
|
+
/**
|
|
50
|
+
* Module path to reload popup component. This popup is rendered when a new
|
|
51
|
+
* service worker is waiting to be installed, and we suggest a reload to
|
|
52
|
+
* the user.
|
|
53
|
+
*
|
|
54
|
+
* Passing `false` will disable the popup, but this is not recommended:
|
|
55
|
+
* users won't have a way to get up-to-date content.
|
|
56
|
+
* @see {@link @theme/PwaReloadPopup}
|
|
57
|
+
*/
|
|
58
|
+
reloadPopup: string | false;
|
|
59
|
+
/**
|
|
60
|
+
* Array of objects containing `tagName` and key-value pairs for attributes
|
|
61
|
+
* to inject into the `<head>` tag. Technically you can inject any head tag
|
|
62
|
+
* through this, but it's ideally used for tags to make your site PWA-
|
|
63
|
+
* compliant.
|
|
64
|
+
*/
|
|
65
|
+
pwaHead: {
|
|
66
|
+
tagName: string;
|
|
67
|
+
href?: string;
|
|
68
|
+
content?: string;
|
|
69
|
+
[attributeName: string]: string | boolean;
|
|
70
|
+
}[];
|
|
71
|
+
/**
|
|
72
|
+
* Useful for additional Workbox rules. You can do whatever a service worker
|
|
73
|
+
* can do here, and use the full power of workbox libraries. The code is
|
|
74
|
+
* transpiled, so you can use modern ES6+ syntax here.
|
|
75
|
+
*/
|
|
76
|
+
swCustom?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Adds an entry before the Docusaurus app so that registration can happen
|
|
79
|
+
* before the app runs. The default `registerSW.js` file is enough for
|
|
80
|
+
* simple registration. Passing `false` will disable registration entirely.
|
|
81
|
+
*/
|
|
82
|
+
swRegister: string | false;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
declare module '@theme/PwaReloadPopup' {
|
|
87
|
+
export interface Props {
|
|
88
|
+
/**
|
|
89
|
+
* The popup should call this callback when the `reload` button is clicked.
|
|
90
|
+
* This will tell the service worker to install the waiting service worker
|
|
91
|
+
* and reload the page.
|
|
92
|
+
*/
|
|
93
|
+
readonly onReload: () => void;
|
|
94
|
+
}
|
|
95
|
+
export default function PwaReloadPopup(props: Props): JSX.Element;
|
|
96
|
+
}
|
package/src/registerSw.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This source code is licensed under the MIT license found in the
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
|
+
|
|
7
8
|
import {createStorageSlot} from '@docusaurus/theme-common';
|
|
8
9
|
|
|
9
10
|
// First: read the env variables (provided by Webpack)
|
|
@@ -78,7 +79,7 @@ function isStandaloneDisplayMode() {
|
|
|
78
79
|
const OfflineModeActivationStrategiesImplementations = {
|
|
79
80
|
always: () => true,
|
|
80
81
|
mobile: () => window.innerWidth <= MAX_MOBILE_WIDTH,
|
|
81
|
-
saveData: () => !!
|
|
82
|
+
saveData: () => !!navigator.connection?.saveData,
|
|
82
83
|
appInstalled: async () => {
|
|
83
84
|
const installedEventFired = await isAppInstalledEventFired();
|
|
84
85
|
const installedRelatedApps = await isAppInstalledRelatedApps();
|
|
@@ -218,8 +219,8 @@ async function registerSW() {
|
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
// TODO these events still works in chrome but have been removed from the spec
|
|
222
|
-
// See https://github.com/w3c/manifest/pull/836
|
|
222
|
+
// TODO these events still works in chrome but have been removed from the spec
|
|
223
|
+
// in 2019! See https://github.com/w3c/manifest/pull/836
|
|
223
224
|
function addLegacyAppInstalledEventsListeners() {
|
|
224
225
|
if (typeof window !== 'undefined') {
|
|
225
226
|
if (debug) {
|
|
@@ -247,7 +248,8 @@ function addLegacyAppInstalledEventsListeners() {
|
|
|
247
248
|
await clearRegistrations();
|
|
248
249
|
});
|
|
249
250
|
|
|
250
|
-
// TODO this event still works in chrome but has been removed from the spec
|
|
251
|
+
// TODO this event still works in chrome but has been removed from the spec
|
|
252
|
+
// in 2019!!!
|
|
251
253
|
window.addEventListener('beforeinstallprompt', async (event) => {
|
|
252
254
|
if (debug) {
|
|
253
255
|
console.log(
|
|
@@ -255,7 +257,8 @@ function addLegacyAppInstalledEventsListeners() {
|
|
|
255
257
|
event,
|
|
256
258
|
);
|
|
257
259
|
}
|
|
258
|
-
// TODO instead of default browser install UI, show custom docusaurus
|
|
260
|
+
// TODO instead of default browser install UI, show custom docusaurus
|
|
261
|
+
// prompt?
|
|
259
262
|
// event.preventDefault();
|
|
260
263
|
if (debug) {
|
|
261
264
|
console.log(
|
|
@@ -272,7 +275,7 @@ function addLegacyAppInstalledEventsListeners() {
|
|
|
272
275
|
}
|
|
273
276
|
// After uninstalling the app, if the user doesn't clear all data, then
|
|
274
277
|
// the previous service worker will continue serving cached files. We
|
|
275
|
-
// need to clear registrations and reload, otherwise the popup
|
|
278
|
+
// need to clear registrations and reload, otherwise the popup shows.
|
|
276
279
|
await clearRegistrations();
|
|
277
280
|
}
|
|
278
281
|
});
|
package/src/sw.js
CHANGED
|
@@ -18,10 +18,10 @@ function parseSwParams() {
|
|
|
18
18
|
return params;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// doc
|
|
21
|
+
// doc advises against dynamic imports in SW
|
|
22
22
|
// https://developers.google.com/web/tools/workbox/guides/using-bundlers#code_splitting_and_dynamic_imports
|
|
23
23
|
// https://twitter.com/sebastienlorber/status/1280155204575518720
|
|
24
|
-
// but
|
|
24
|
+
// but looks it's working fine as it's inlined by webpack, need to double check
|
|
25
25
|
async function runSWCustomCode(params) {
|
|
26
26
|
if (process.env.PWA_SW_CUSTOM) {
|
|
27
27
|
const customSW = await import(process.env.PWA_SW_CUSTOM);
|
|
@@ -70,6 +70,7 @@ function getPossibleURLs(url) {
|
|
|
70
70
|
(async () => {
|
|
71
71
|
const params = parseSwParams();
|
|
72
72
|
|
|
73
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
73
74
|
const precacheManifest = self.__WB_MANIFEST;
|
|
74
75
|
const controller = new PrecacheController({
|
|
75
76
|
fallbackToNetwork: true, // safer to turn this true?
|
|
@@ -136,7 +137,7 @@ function getPossibleURLs(url) {
|
|
|
136
137
|
});
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
const type = event.data
|
|
140
|
+
const type = event.data?.type;
|
|
140
141
|
|
|
141
142
|
if (type === 'SKIP_WAITING') {
|
|
142
143
|
self.skipWaiting();
|
|
@@ -9,9 +9,11 @@ import React, {useState} from 'react';
|
|
|
9
9
|
import clsx from 'clsx';
|
|
10
10
|
import Translate, {translate} from '@docusaurus/Translate';
|
|
11
11
|
|
|
12
|
+
import type {Props} from '@theme/PwaReloadPopup';
|
|
13
|
+
|
|
12
14
|
import styles from './styles.module.css';
|
|
13
15
|
|
|
14
|
-
export default function PwaReloadPopup({onReload}) {
|
|
16
|
+
export default function PwaReloadPopup({onReload}: Props): JSX.Element | false {
|
|
15
17
|
const [isVisible, setIsVisible] = useState(true);
|
|
16
18
|
|
|
17
19
|
return (
|
package/src/types.d.ts
ADDED