@docusaurus/plugin-pwa 2.0.0-beta.138b4c997 → 2.0.0-beta.14

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 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 (context: LoadContext, options: PluginOptions): Plugin<void>;
10
+ export { validateOptions } from './options';
package/lib/index.js ADDED
@@ -0,0 +1,168 @@
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 = (0, tslib_1.__importDefault)(require("@docusaurus/core/lib/webpack/plugins/LogPlugin"));
14
+ const theme_translations_1 = require("@docusaurus/theme-translations");
15
+ const path_1 = (0, tslib_1.__importDefault)(require("path"));
16
+ const webpack_1 = (0, tslib_1.__importDefault)(require("webpack"));
17
+ const terser_webpack_plugin_1 = (0, 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
+ plugins: [
38
+ require.resolve('@babel/plugin-proposal-object-rest-spread'),
39
+ require.resolve('@babel/plugin-proposal-optional-chaining'),
40
+ require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'),
41
+ ],
42
+ },
43
+ };
44
+ }
45
+ function default_1(context, options) {
46
+ const { outDir, baseUrl, i18n: { currentLocale }, } = context;
47
+ const { debug, offlineModeActivationStrategies, injectManifestConfig, reloadPopup, pwaHead, swCustom, swRegister, } = options;
48
+ return {
49
+ name: 'docusaurus-plugin-pwa',
50
+ getThemePath() {
51
+ return path_1.default.resolve(__dirname, './theme');
52
+ },
53
+ getClientModules() {
54
+ return isProd ? [swRegister] : [];
55
+ },
56
+ getDefaultCodeTranslationMessages() {
57
+ return (0, theme_translations_1.readDefaultCodeTranslationMessages)({
58
+ locale: currentLocale,
59
+ name: 'plugin-pwa',
60
+ });
61
+ },
62
+ configureWebpack(config) {
63
+ var _a;
64
+ if (!isProd) {
65
+ return {};
66
+ }
67
+ return {
68
+ plugins: [
69
+ new webpack_1.default.EnvironmentPlugin({
70
+ PWA_DEBUG: debug,
71
+ PWA_SERVICE_WORKER_URL: path_1.default.posix.resolve(`${((_a = config.output) === null || _a === void 0 ? void 0 : _a.publicPath) || '/'}`, 'sw.js'),
72
+ PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES: offlineModeActivationStrategies,
73
+ PWA_RELOAD_POPUP: reloadPopup,
74
+ }),
75
+ ],
76
+ };
77
+ },
78
+ injectHtmlTags() {
79
+ const headTags = [];
80
+ if (isProd && pwaHead) {
81
+ pwaHead.forEach(({ tagName, ...attributes }) => {
82
+ ['href', 'content'].forEach((attribute) => {
83
+ const attributeValue = attributes[attribute];
84
+ if (!attributeValue) {
85
+ return;
86
+ }
87
+ const attributePath = !!path_1.default.extname(attributeValue) && attributeValue;
88
+ if (attributePath && !attributePath.startsWith(baseUrl)) {
89
+ attributes[attribute] = (0, utils_1.normalizeUrl)([baseUrl, attributeValue]);
90
+ }
91
+ });
92
+ return headTags.push({
93
+ tagName,
94
+ attributes,
95
+ });
96
+ });
97
+ }
98
+ return { headTags };
99
+ },
100
+ async postBuild(props) {
101
+ if (!isProd) {
102
+ return;
103
+ }
104
+ const swSourceFileTest = /\.m?js$/;
105
+ const swWebpackConfig = {
106
+ entry: path_1.default.resolve(__dirname, 'sw.js'),
107
+ output: {
108
+ path: outDir,
109
+ filename: 'sw.js',
110
+ publicPath: baseUrl,
111
+ },
112
+ target: 'webworker',
113
+ mode: debug ? 'development' : 'production',
114
+ devtool: debug ? 'source-map' : false,
115
+ optimization: {
116
+ splitChunks: false,
117
+ minimize: !debug,
118
+ // see https://developers.google.com/web/tools/workbox/guides/using-bundlers#webpack
119
+ minimizer: debug
120
+ ? []
121
+ : [
122
+ new terser_webpack_plugin_1.default({
123
+ test: swSourceFileTest,
124
+ }),
125
+ ],
126
+ },
127
+ plugins: [
128
+ new webpack_1.default.EnvironmentPlugin({
129
+ PWA_SW_CUSTOM: swCustom || '', // fallback value required with Webpack 5
130
+ }),
131
+ new LogPlugin_1.default({
132
+ name: 'Service Worker',
133
+ color: 'red',
134
+ }),
135
+ ],
136
+ module: {
137
+ rules: [
138
+ {
139
+ test: swSourceFileTest,
140
+ exclude: /(node_modules)/,
141
+ use: getSWBabelLoader(),
142
+ },
143
+ ],
144
+ },
145
+ };
146
+ await (0, utils_2.compile)([swWebpackConfig]);
147
+ const swDest = path_1.default.resolve(props.outDir, 'sw.js');
148
+ await (0, workbox_build_1.injectManifest)({
149
+ ...injectManifestConfig,
150
+ globPatterns: [
151
+ '**/*.{js,json,css,html}',
152
+ '**/*.{png,jpg,jpeg,gif,svg,ico}',
153
+ '**/*.{woff,woff2,eot,ttf,otf}',
154
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
155
+ // @ts-ignore
156
+ ...(workbox_build_1.injectManifest.globPatterns || []),
157
+ ],
158
+ // those attributes are not overrideable
159
+ swDest,
160
+ swSrc: swDest,
161
+ globDirectory: props.outDir,
162
+ });
163
+ },
164
+ };
165
+ }
166
+ exports.default = default_1;
167
+ var options_1 = require("./options");
168
+ Object.defineProperty(exports, "validateOptions", { enumerable: true, get: function () { return options_1.validateOptions; } });
@@ -0,0 +1,11 @@
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 { Joi } from '@docusaurus/utils-validation';
8
+ import type { ThemeConfig, ValidationResult, OptionValidationContext } from '@docusaurus/types';
9
+ import type { PluginOptions } from '@docusaurus/plugin-pwa';
10
+ export declare const Schema: Joi.ObjectSchema<any>;
11
+ export declare function validateOptions({ validate, options, }: OptionValidationContext<PluginOptions>): ValidationResult<ThemeConfig>;
package/lib/options.js ADDED
@@ -0,0 +1,48 @@
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 = exports.Schema = void 0;
10
+ const tslib_1 = require("tslib");
11
+ const path_1 = (0, tslib_1.__importDefault)(require("path"));
12
+ const utils_validation_1 = require("@docusaurus/utils-validation");
13
+ const DEFAULT_OPTIONS = {
14
+ debug: false,
15
+ offlineModeActivationStrategies: [
16
+ 'appInstalled',
17
+ 'queryString',
18
+ 'standalone',
19
+ ],
20
+ injectManifestConfig: {},
21
+ pwaHead: [],
22
+ swCustom: undefined,
23
+ swRegister: path_1.default.join(__dirname, 'registerSw.js'),
24
+ reloadPopup: '@theme/PwaReloadPopup',
25
+ };
26
+ exports.Schema = utils_validation_1.Joi.object({
27
+ debug: utils_validation_1.Joi.bool().default(DEFAULT_OPTIONS.debug),
28
+ offlineModeActivationStrategies: utils_validation_1.Joi.array()
29
+ .items(utils_validation_1.Joi.string()
30
+ .valid('appInstalled', 'queryString', 'standalone', 'mobile', 'saveData', 'always')
31
+ .required())
32
+ .default(DEFAULT_OPTIONS.offlineModeActivationStrategies),
33
+ injectManifestConfig: utils_validation_1.Joi.object().default(DEFAULT_OPTIONS.injectManifestConfig),
34
+ pwaHead: utils_validation_1.Joi.array()
35
+ .items(utils_validation_1.Joi.object({ tagName: utils_validation_1.Joi.string().required() }).unknown().required())
36
+ .default(DEFAULT_OPTIONS.pwaHead),
37
+ swCustom: utils_validation_1.Joi.string(),
38
+ swRegister: utils_validation_1.Joi.alternatives()
39
+ .try(utils_validation_1.Joi.string(), utils_validation_1.Joi.bool().valid(false))
40
+ .default(DEFAULT_OPTIONS.swRegister),
41
+ reloadPopup: utils_validation_1.Joi.alternatives()
42
+ .try(utils_validation_1.Joi.string(), utils_validation_1.Joi.bool().valid(false))
43
+ .default(DEFAULT_OPTIONS.reloadPopup),
44
+ });
45
+ function validateOptions({ validate, options, }) {
46
+ return validate(exports.Schema, options);
47
+ }
48
+ exports.validateOptions = validateOptions;
@@ -0,0 +1,304 @@
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 && 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 in 2019!
223
+ // 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 in 2019!!!
252
+ window.addEventListener('beforeinstallprompt', async (event) => {
253
+ if (debug) {
254
+ console.log(
255
+ '[Docusaurus-PWA][registerSw]: event beforeinstallprompt',
256
+ event,
257
+ );
258
+ }
259
+ // TODO instead of default browser install UI, show custom docusaurus prompt?
260
+ // event.preventDefault();
261
+ if (debug) {
262
+ console.log(
263
+ '[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.get()',
264
+ AppInstalledEventFiredStorage.get(),
265
+ );
266
+ }
267
+ if (AppInstalledEventFiredStorage.get()) {
268
+ AppInstalledEventFiredStorage.del();
269
+ if (debug) {
270
+ console.log(
271
+ '[Docusaurus-PWA][registerSw]: AppInstalledEventFiredStorage.del()',
272
+ );
273
+ }
274
+ // After uninstalling the app, if the user doesn't clear all data, then
275
+ // the previous service worker will continue serving cached files. We
276
+ // need to clear registrations and reload, otherwise the popup will show.
277
+ await clearRegistrations();
278
+ }
279
+ });
280
+
281
+ if (debug) {
282
+ console.log(
283
+ '[Docusaurus-PWA][registerSw]: legacy appinstalled and beforeinstallprompt event listeners installed',
284
+ );
285
+ }
286
+ }
287
+ }
288
+
289
+ /*
290
+ Init code to run on the client!
291
+ */
292
+ if (typeof window !== 'undefined') {
293
+ if (debug) {
294
+ console.log('[Docusaurus-PWA][registerSw]: debug mode enabled');
295
+ }
296
+
297
+ if ('serviceWorker' in navigator) {
298
+ // First: add the listeners asap/synchronously
299
+ addLegacyAppInstalledEventsListeners();
300
+
301
+ // Then try to register the SW using lazy/dynamic imports
302
+ registerSW().catch((e) => console.error('registerSW failed', e));
303
+ }
304
+ }
@@ -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,145 @@
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 advise 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 I think 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
+ const precacheManifest = self.__WB_MANIFEST;
74
+ const controller = new PrecacheController({
75
+ fallbackToNetwork: true, // safer to turn this true?
76
+ });
77
+
78
+ if (params.offlineMode) {
79
+ controller.addToCacheList(precacheManifest);
80
+ if (params.debug) {
81
+ console.log('[Docusaurus-PWA][SW]: addToCacheList', {
82
+ precacheManifest,
83
+ });
84
+ }
85
+ }
86
+
87
+ await runSWCustomCode(params);
88
+
89
+ self.addEventListener('install', (event) => {
90
+ if (params.debug) {
91
+ console.log('[Docusaurus-PWA][SW]: install event', {
92
+ event,
93
+ });
94
+ }
95
+ event.waitUntil(controller.install(event));
96
+ });
97
+
98
+ self.addEventListener('activate', (event) => {
99
+ if (params.debug) {
100
+ console.log('[Docusaurus-PWA][SW]: activate event', {
101
+ event,
102
+ });
103
+ }
104
+ event.waitUntil(controller.activate(event));
105
+ });
106
+
107
+ self.addEventListener('fetch', async (event) => {
108
+ if (params.offlineMode) {
109
+ const requestURL = event.request.url;
110
+ const possibleURLs = getPossibleURLs(requestURL);
111
+ for (let i = 0; i < possibleURLs.length; i += 1) {
112
+ const possibleURL = possibleURLs[i];
113
+ const cacheKey = controller.getCacheKeyForURL(possibleURL);
114
+ if (cacheKey) {
115
+ const cachedResponse = caches.match(cacheKey);
116
+ if (params.debug) {
117
+ console.log('[Docusaurus-PWA][SW]: serving cached asset', {
118
+ requestURL,
119
+ possibleURL,
120
+ possibleURLs,
121
+ cacheKey,
122
+ cachedResponse,
123
+ });
124
+ }
125
+ event.respondWith(cachedResponse);
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ });
131
+
132
+ self.addEventListener('message', async (event) => {
133
+ if (params.debug) {
134
+ console.log('[Docusaurus-PWA][SW]: message event', {
135
+ event,
136
+ });
137
+ }
138
+
139
+ const type = event.data && event.data.type;
140
+
141
+ if (type === 'SKIP_WAITING') {
142
+ self.skipWaiting();
143
+ }
144
+ });
145
+ })();
@@ -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
+ /// <reference types="react" />
8
+ import type { PwaReloadPopupProps } from '@theme/PwaReloadPopup';
9
+ declare function PwaReloadPopup({ onReload }: PwaReloadPopupProps): JSX.Element | false;
10
+ export default PwaReloadPopup;
@@ -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
+ 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
+ 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
+ }
39
+ export default PwaReloadPopup;
@@ -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.138b4c997",
3
+ "version": "2.0.0-beta.14",
4
4
  "description": "Docusaurus Plugin to add PWA support.",
5
- "main": "src/index.js",
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.js"
12
+ },
6
13
  "publishConfig": {
7
14
  "access": "public"
8
15
  },
@@ -13,26 +20,33 @@
13
20
  },
14
21
  "license": "MIT",
15
22
  "dependencies": {
16
- "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.13",
17
- "@babel/plugin-proposal-optional-chaining": "^7.12.16",
18
- "@babel/preset-env": "^7.12.16",
19
- "@docusaurus/core": "2.0.0-beta.138b4c997",
20
- "@docusaurus/theme-common": "2.0.0-beta.138b4c997",
21
- "@docusaurus/utils-validation": "2.0.0-beta.138b4c997",
23
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0",
24
+ "@babel/plugin-proposal-optional-chaining": "^7.16.0",
25
+ "@babel/preset-env": "^7.16.4",
26
+ "@docusaurus/core": "2.0.0-beta.14",
27
+ "@docusaurus/theme-common": "2.0.0-beta.14",
28
+ "@docusaurus/theme-translations": "2.0.0-beta.14",
29
+ "@docusaurus/utils": "2.0.0-beta.14",
30
+ "@docusaurus/utils-validation": "2.0.0-beta.14",
22
31
  "babel-loader": "^8.2.2",
23
32
  "clsx": "^1.1.1",
24
- "core-js": "^2.6.5",
25
- "terser-webpack-plugin": "^5.1.3",
26
- "webpack": "^5.40.0",
33
+ "core-js": "^3.18.0",
34
+ "terser-webpack-plugin": "^5.2.4",
35
+ "tslib": "^2.3.1",
36
+ "webpack": "^5.61.0",
27
37
  "webpack-merge": "^5.7.3",
28
38
  "workbox-build": "^6.1.1",
29
39
  "workbox-precaching": "^6.1.1",
30
40
  "workbox-window": "^6.1.1"
31
41
  },
42
+ "devDependencies": {
43
+ "@docusaurus/module-type-aliases": "2.0.0-beta.14",
44
+ "fs-extra": "^10.0.0"
45
+ },
32
46
  "peerDependencies": {
33
47
  "@babel/core": "^7.0.0",
34
48
  "react": "^16.8.4 || ^17.0.0",
35
49
  "react-dom": "^16.8.4 || ^17.0.0"
36
50
  },
37
- "gitHead": "8cc620648710971acc459aabd2c6f505ef757701"
51
+ "gitHead": "c4824a8937d8f1aa0806667749cbc74058e2b294"
38
52
  }
@@ -5,14 +5,18 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- const LogPlugin = require('@docusaurus/core/lib/webpack/plugins/LogPlugin')
9
- .default;
10
- const {compile} = require('@docusaurus/core/lib/webpack/utils');
11
- const path = require('path');
12
- const webpack = require('webpack');
13
- const {injectManifest} = require('workbox-build');
14
- const {PluginOptionSchema} = require('./pluginOptionSchema');
15
- const Terser = require('terser-webpack-plugin');
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, {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
 
@@ -42,8 +46,15 @@ function getSWBabelLoader() {
42
46
  };
43
47
  }
44
48
 
45
- function plugin(context, options) {
46
- const {outDir, baseUrl} = context;
49
+ export default function (
50
+ context: LoadContext,
51
+ options: PluginOptions,
52
+ ): Plugin<void> {
53
+ const {
54
+ outDir,
55
+ baseUrl,
56
+ i18n: {currentLocale},
57
+ } = context;
47
58
  const {
48
59
  debug,
49
60
  offlineModeActivationStrategies,
@@ -65,6 +76,13 @@ function plugin(context, options) {
65
76
  return isProd ? [swRegister] : [];
66
77
  },
67
78
 
79
+ getDefaultCodeTranslationMessages() {
80
+ return readDefaultCodeTranslationMessages({
81
+ locale: currentLocale,
82
+ name: 'plugin-pwa',
83
+ });
84
+ },
85
+
68
86
  configureWebpack(config) {
69
87
  if (!isProd) {
70
88
  return {};
@@ -75,10 +93,11 @@ function plugin(context, options) {
75
93
  new webpack.EnvironmentPlugin({
76
94
  PWA_DEBUG: debug,
77
95
  PWA_SERVICE_WORKER_URL: path.posix.resolve(
78
- `${config.output.publicPath || '/'}`,
96
+ `${config.output?.publicPath || '/'}`,
79
97
  'sw.js',
80
98
  ),
81
- PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES: offlineModeActivationStrategies,
99
+ PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES:
100
+ offlineModeActivationStrategies,
82
101
  PWA_RELOAD_POPUP: reloadPopup,
83
102
  }),
84
103
  ],
@@ -86,14 +105,29 @@ function plugin(context, options) {
86
105
  },
87
106
 
88
107
  injectHtmlTags() {
89
- const headTags = [];
108
+ const headTags: HtmlTags = [];
90
109
  if (isProd && pwaHead) {
91
- pwaHead.forEach(({tagName, ...attributes}) =>
92
- headTags.push({
110
+ pwaHead.forEach(({tagName, ...attributes}) => {
111
+ (['href', 'content'] as const).forEach((attribute) => {
112
+ const attributeValue = attributes[attribute];
113
+
114
+ if (!attributeValue) {
115
+ return;
116
+ }
117
+
118
+ const attributePath =
119
+ !!path.extname(attributeValue) && attributeValue;
120
+
121
+ if (attributePath && !attributePath.startsWith(baseUrl)) {
122
+ attributes[attribute] = normalizeUrl([baseUrl, attributeValue]);
123
+ }
124
+ });
125
+
126
+ return headTags.push({
93
127
  tagName,
94
128
  attributes,
95
- }),
96
- );
129
+ });
130
+ });
97
131
  }
98
132
  return {headTags};
99
133
  },
@@ -105,7 +139,7 @@ function plugin(context, options) {
105
139
 
106
140
  const swSourceFileTest = /\.m?js$/;
107
141
 
108
- const swWebpackConfig = {
142
+ const swWebpackConfig: Configuration = {
109
143
  entry: path.resolve(__dirname, 'sw.js'),
110
144
  output: {
111
145
  path: outDir,
@@ -119,12 +153,13 @@ function plugin(context, options) {
119
153
  splitChunks: false,
120
154
  minimize: !debug,
121
155
  // see https://developers.google.com/web/tools/workbox/guides/using-bundlers#webpack
122
- minimizer: [
123
- !debug &&
124
- new Terser({
125
- test: swSourceFileTest,
126
- }),
127
- ].filter(Boolean),
156
+ minimizer: debug
157
+ ? []
158
+ : [
159
+ new Terser({
160
+ test: swSourceFileTest,
161
+ }),
162
+ ],
128
163
  },
129
164
  plugins: [
130
165
  new webpack.EnvironmentPlugin({
@@ -156,6 +191,8 @@ function plugin(context, options) {
156
191
  '**/*.{js,json,css,html}',
157
192
  '**/*.{png,jpg,jpeg,gif,svg,ico}',
158
193
  '**/*.{woff,woff2,eot,ttf,otf}',
194
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
195
+ // @ts-ignore
159
196
  ...(injectManifest.globPatterns || []),
160
197
  ],
161
198
  // those attributes are not overrideable
@@ -167,8 +204,4 @@ function plugin(context, options) {
167
204
  };
168
205
  }
169
206
 
170
- module.exports = plugin;
171
-
172
- plugin.validateOptions = function validateOptions({validate, options}) {
173
- return validate(PluginOptionSchema, options);
174
- };
207
+ export {validateOptions} from './options';
@@ -5,8 +5,14 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- const {Joi} = require('@docusaurus/utils-validation');
9
- const path = require('path');
8
+ import path from 'path';
9
+ import {Joi} from '@docusaurus/utils-validation';
10
+ import type {
11
+ ThemeConfig,
12
+ ValidationResult,
13
+ OptionValidationContext,
14
+ } from '@docusaurus/types';
15
+ import type {PluginOptions} from '@docusaurus/plugin-pwa';
10
16
 
11
17
  const DEFAULT_OPTIONS = {
12
18
  debug: false,
@@ -22,7 +28,7 @@ const DEFAULT_OPTIONS = {
22
28
  reloadPopup: '@theme/PwaReloadPopup',
23
29
  };
24
30
 
25
- exports.PluginOptionSchema = Joi.object({
31
+ export const Schema = Joi.object({
26
32
  debug: Joi.bool().default(DEFAULT_OPTIONS.debug),
27
33
  offlineModeActivationStrategies: Joi.array()
28
34
  .items(
@@ -52,3 +58,10 @@ exports.PluginOptionSchema = Joi.object({
52
58
  .try(Joi.string(), Joi.bool().valid(false))
53
59
  .default(DEFAULT_OPTIONS.reloadPopup),
54
60
  });
61
+
62
+ export function validateOptions({
63
+ validate,
64
+ options,
65
+ }: OptionValidationContext<PluginOptions>): ValidationResult<ThemeConfig> {
66
+ return validate(Schema, options);
67
+ }
@@ -0,0 +1,34 @@
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
+ export type pwaHead = {
10
+ tagName: string;
11
+ href?: string;
12
+ content?: string;
13
+ [attributeName: string]: string | boolean;
14
+ };
15
+
16
+ export type PluginOptions = {
17
+ debug?: boolean;
18
+ offlineModeActivationStrategies;
19
+ injectManifestConfig;
20
+ reloadPopup;
21
+ pwaHead: pwaHead[];
22
+ swCustom;
23
+ swRegister;
24
+ };
25
+ }
26
+
27
+ declare module '@theme/PwaReloadPopup' {
28
+ export type PwaReloadPopupProps = {
29
+ readonly onReload: () => void;
30
+ };
31
+
32
+ const PwaReloadPopup: (props: PwaReloadPopupProps) => JSX.Element;
33
+ export default PwaReloadPopup;
34
+ }
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)
@@ -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 {PwaReloadPopupProps} from '@theme/PwaReloadPopup';
13
+
12
14
  import styles from './styles.module.css';
13
15
 
14
- export default function PwaReloadPopup({onReload}) {
16
+ function PwaReloadPopup({onReload}: PwaReloadPopupProps): JSX.Element | false {
15
17
  const [isVisible, setIsVisible] = useState(true);
16
18
 
17
19
  return (
@@ -56,3 +58,5 @@ export default function PwaReloadPopup({onReload}) {
56
58
  )
57
59
  );
58
60
  }
61
+
62
+ export default PwaReloadPopup;
package/src/types.d.ts ADDED
@@ -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
+
8
+ /// <reference types="@docusaurus/module-type-aliases" />