@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/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@azerion/bunosaurus-plugin-pwa",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "4.0.0",
4
+ "description": "Docusaurus Plugin to add PWA support.",
5
+ "main": "lib/index.js",
6
+ "types": "src/plugin-pwa.d.ts",
7
+ "scripts": {
8
+ "build": "tsc --build && copy-untyped-files && oxfmt \"lib/theme/**/*.js\"",
9
+ "watch": "run-p -c copy:watch build:watch",
10
+ "build:watch": "tsc --build --watch",
11
+ "copy:watch": "copy-untyped-files --watch"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/azerion/bunosaurus.git",
19
+ "directory": "packages/docusaurus-plugin-pwa"
20
+ },
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@docusaurus/bundler": "npm:@azerion/bunosaurus-bundler@4.0.0",
24
+ "@docusaurus/core": "npm:@azerion/bunosaurus-core@4.0.0",
25
+ "@docusaurus/logger": "npm:@azerion/bunosaurus-logger@4.0.0",
26
+ "@docusaurus/theme-common": "npm:@azerion/bunosaurus-theme-common@4.0.0",
27
+ "@docusaurus/theme-translations": "npm:@azerion/bunosaurus-theme-translations@4.0.0",
28
+ "@docusaurus/types": "npm:@azerion/bunosaurus-types@4.0.0",
29
+ "@docusaurus/utils": "npm:@azerion/bunosaurus-utils@4.0.0",
30
+ "@docusaurus/utils-validation": "npm:@azerion/bunosaurus-utils-validation@4.0.0",
31
+ "clsx": "^2.1.1",
32
+ "tslib": "^2.8.1",
33
+ "workbox-build": "^7.4.1",
34
+ "workbox-precaching": "^7.4.1",
35
+ "workbox-window": "^7.4.1"
36
+ },
37
+ "devDependencies": {
38
+ "@docusaurus/module-type-aliases": "npm:@azerion/bunosaurus-module-type-aliases@4.0.0",
39
+ "@types/react-dom": "^19.3.0"
40
+ },
41
+ "peerDependencies": {
42
+ "react": "^19.3.0",
43
+ "react-dom": "^19.3.0"
44
+ },
45
+ "engines": {
46
+ "bun": ">=1.4.1"
47
+ }
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1,150 @@
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 path from 'node:path';
9
+ import {bunBuild} from '@docusaurus/bundler';
10
+ import {injectManifest} from 'workbox-build';
11
+ import {normalizeUrl} from '@docusaurus/utils';
12
+ import logger from '@docusaurus/logger';
13
+ import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations';
14
+ import type {HtmlTags, LoadContext, Plugin} from '@docusaurus/types';
15
+ import type {PluginOptions} from '@docusaurus/plugin-pwa';
16
+
17
+ const PluginName = 'docusaurus-plugin-pwa';
18
+
19
+ export default function pluginPWA(
20
+ context: LoadContext,
21
+ options: PluginOptions,
22
+ ): Plugin<void> | null {
23
+ if (process.env.NODE_ENV !== 'production') {
24
+ return null;
25
+ }
26
+ if (context.siteConfig.future.experimental_router === 'hash') {
27
+ logger.warn(
28
+ `${PluginName} does not support the Hash Router and will be disabled.`,
29
+ );
30
+ return null;
31
+ }
32
+
33
+ const {
34
+ outDir,
35
+ baseUrl,
36
+ i18n: {currentLocale},
37
+ } = context;
38
+ const {
39
+ debug,
40
+ offlineModeActivationStrategies,
41
+ injectManifestConfig,
42
+ pwaHead,
43
+ swCustom,
44
+ swRegister,
45
+ } = options;
46
+
47
+ return {
48
+ name: PluginName,
49
+
50
+ getThemePath() {
51
+ return '../lib/theme';
52
+ },
53
+ getTypeScriptThemePath() {
54
+ return '../src/theme';
55
+ },
56
+
57
+ getClientModules() {
58
+ return swRegister ? [swRegister] : [];
59
+ },
60
+
61
+ getDefaultCodeTranslationMessages() {
62
+ return readDefaultCodeTranslationMessages({
63
+ locale: currentLocale,
64
+ name: 'plugin-pwa',
65
+ });
66
+ },
67
+
68
+ configureWebpack(config, isServer, {currentBundler}) {
69
+ return {
70
+ plugins: [
71
+ new currentBundler.instance.EnvironmentPlugin({
72
+ PWA_DEBUG: debug,
73
+ PWA_SERVICE_WORKER_URL: path.posix.resolve(
74
+ `${(config.output?.publicPath as string) || '/'}`,
75
+ 'sw.js',
76
+ ),
77
+ PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES:
78
+ offlineModeActivationStrategies,
79
+ }),
80
+ ],
81
+ };
82
+ },
83
+
84
+ injectHtmlTags() {
85
+ const headTags: HtmlTags = [];
86
+ pwaHead.forEach(({tagName, ...attributes}) => {
87
+ (['href', 'content'] as const).forEach((attribute) => {
88
+ const attributeValue = attributes[attribute];
89
+
90
+ if (!attributeValue) {
91
+ return;
92
+ }
93
+
94
+ const attributePath =
95
+ !!path.extname(attributeValue) && attributeValue;
96
+
97
+ if (attributePath && !attributePath.startsWith(baseUrl)) {
98
+ attributes[attribute] = normalizeUrl([baseUrl, attributeValue]);
99
+ }
100
+ });
101
+
102
+ return headTags.push({
103
+ tagName,
104
+ attributes,
105
+ });
106
+ });
107
+ return {headTags};
108
+ },
109
+
110
+ async postBuild(props) {
111
+ // The service worker is a classic script, bundled on its own
112
+ await bunBuild('Service Worker', {
113
+ entrypoints: [require.resolve('./sw.js')],
114
+ outdir: outDir,
115
+ naming: 'sw.js',
116
+ target: 'browser',
117
+ format: 'iife',
118
+ minify: !debug,
119
+ sourcemap: debug ? 'linked' : 'none',
120
+ publicPath: baseUrl,
121
+ env: 'disable',
122
+ define: {
123
+ 'process.env.NODE_ENV': JSON.stringify(
124
+ debug ? 'development' : 'production',
125
+ ),
126
+ 'process.env.PWA_SW_CUSTOM': JSON.stringify(swCustom ?? ''),
127
+ },
128
+ });
129
+
130
+ const swDest = path.resolve(props.outDir, 'sw.js');
131
+
132
+ await injectManifest({
133
+ ...injectManifestConfig,
134
+ globPatterns: [
135
+ '**/*.{js,json,css,html}',
136
+ '**/*.{png,jpg,jpeg,gif,svg,ico}',
137
+ '**/*.{woff,woff2,eot,ttf,otf}',
138
+ // @ts-expect-error: internal API?
139
+ ...((injectManifest.globPatterns as string[] | undefined) ?? []),
140
+ ],
141
+ // Those attributes are not overrideable
142
+ swDest,
143
+ swSrc: swDest,
144
+ globDirectory: props.outDir,
145
+ });
146
+ },
147
+ };
148
+ }
149
+
150
+ export {validateOptions} from './options';
package/src/options.ts ADDED
@@ -0,0 +1,63 @@
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 {Joi} from '@docusaurus/utils-validation';
9
+ import type {OptionValidationContext} from '@docusaurus/types';
10
+ import type {PluginOptions} from '@docusaurus/plugin-pwa';
11
+
12
+ const DEFAULT_OPTIONS = {
13
+ debug: false,
14
+ offlineModeActivationStrategies: [
15
+ 'appInstalled',
16
+ 'queryString',
17
+ 'standalone',
18
+ ],
19
+ injectManifestConfig: {},
20
+ pwaHead: [],
21
+ swCustom: undefined,
22
+ swRegister: './registerSw.js',
23
+ };
24
+
25
+ const optionsSchema = Joi.object<PluginOptions>({
26
+ debug: Joi.bool().default(DEFAULT_OPTIONS.debug),
27
+ offlineModeActivationStrategies: Joi.array()
28
+ .items(
29
+ Joi.string()
30
+ .valid(
31
+ 'appInstalled',
32
+ 'queryString',
33
+ 'standalone',
34
+ 'mobile',
35
+ 'saveData',
36
+ 'always',
37
+ )
38
+ .required(),
39
+ )
40
+ .default(DEFAULT_OPTIONS.offlineModeActivationStrategies),
41
+ injectManifestConfig: Joi.object().default(
42
+ DEFAULT_OPTIONS.injectManifestConfig,
43
+ ),
44
+ pwaHead: Joi.array()
45
+ .items(Joi.object({tagName: Joi.string().required()}).unknown().required())
46
+ .default(DEFAULT_OPTIONS.pwaHead),
47
+ swCustom: Joi.string(),
48
+ swRegister: Joi.alternatives()
49
+ .try(Joi.string(), Joi.bool().valid(false))
50
+ .default(DEFAULT_OPTIONS.swRegister),
51
+ // @ts-expect-error: forbidden
52
+ reloadPopup: Joi.any().forbidden().messages({
53
+ 'any.unknown':
54
+ '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.',
55
+ }),
56
+ });
57
+
58
+ export function validateOptions({
59
+ validate,
60
+ options,
61
+ }: OptionValidationContext<PluginOptions, PluginOptions>): PluginOptions {
62
+ return validate(optionsSchema, options);
63
+ }
@@ -0,0 +1,90 @@
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
+ * Array of objects containing `tagName` and key-value pairs for attributes
51
+ * to inject into the `<head>` tag. Technically you can inject any head tag
52
+ * through this, but it's ideally used for tags to make your site PWA-
53
+ * compliant.
54
+ */
55
+ pwaHead: {
56
+ tagName: string;
57
+ href?: string;
58
+ content?: string;
59
+ [attributeName: string]: string | boolean | undefined;
60
+ }[];
61
+ /**
62
+ * Useful for additional Workbox rules. You can do whatever a service worker
63
+ * can do here, and use the full power of workbox libraries. The code is
64
+ * transpiled, so you can use modern ES6+ syntax here.
65
+ */
66
+ swCustom?: string;
67
+ /**
68
+ * Adds an entry before the Docusaurus app so that registration can happen
69
+ * before the app runs. The default `registerSW.js` file is enough for
70
+ * simple registration. Passing `false` will disable registration entirely.
71
+ */
72
+ swRegister: string | false;
73
+ };
74
+
75
+ export type Options = Partial<PluginOptions>;
76
+ }
77
+
78
+ declare module '@theme/PwaReloadPopup' {
79
+ import type {ReactNode} from 'react';
80
+
81
+ export interface Props {
82
+ /**
83
+ * The popup should call this callback when the `reload` button is clicked.
84
+ * This will tell the service worker to install the waiting service worker
85
+ * and reload the page.
86
+ */
87
+ readonly onReload: () => void;
88
+ }
89
+ export default function PwaReloadPopup(props: Props): ReactNode;
90
+ }
@@ -0,0 +1,242 @@
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 ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
9
+ import {createStorageSlot} from '@docusaurus/theme-common';
10
+
11
+ // First: read the env variables (provided by Webpack)
12
+
13
+ const PWA_SERVICE_WORKER_URL = process.env.PWA_SERVICE_WORKER_URL!;
14
+ const PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES = process.env
15
+ .PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES as unknown as (keyof typeof OfflineModeActivationStrategiesImplementations)[];
16
+ const PWA_DEBUG = process.env.PWA_DEBUG;
17
+
18
+ const MAX_MOBILE_WIDTH = 996;
19
+
20
+ const AppInstalledEventFiredStorage = createStorageSlot(
21
+ 'docusaurus.pwa.event.appInstalled.fired',
22
+ );
23
+
24
+ declare global {
25
+ interface Navigator {
26
+ getInstalledRelatedApps: () => Promise<{platform: string}[]>;
27
+ connection?: {effectiveType: string; saveData: boolean};
28
+ }
29
+ }
30
+
31
+ function debugLog(msg: string, obj?: unknown) {
32
+ if (PWA_DEBUG) {
33
+ if (typeof obj === 'undefined') {
34
+ console.log(`[Docusaurus-PWA][registerSw]: ${msg}`);
35
+ } else {
36
+ console.log(`[Docusaurus-PWA][registerSw]: ${msg}`, obj);
37
+ }
38
+ }
39
+ }
40
+
41
+ async function clearRegistrations() {
42
+ const registrations = await navigator.serviceWorker.getRegistrations();
43
+ debugLog('will unregister all service workers', {registrations});
44
+ await Promise.all(
45
+ registrations.map((registration) =>
46
+ registration
47
+ .unregister()
48
+ .then((result) =>
49
+ debugLog('unregister service worker', {registration, result}),
50
+ ),
51
+ ),
52
+ );
53
+ debugLog('unregistered all service workers', {registrations});
54
+ window.location.reload();
55
+ }
56
+
57
+ /*
58
+ As of 2021:
59
+ It is complicated and not very reliable to detect an app is actually installed.
60
+ https://stackoverflow.com/questions/51735869/check-if-user-has-already-installed-pwa-to-homescreen-on-chrome
61
+
62
+ - 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
63
+ - getInstalledRelatedApps() is only supported in recent Chrome and does not seem to reliable either https://github.com/WICG/get-installed-related-apps
64
+ - 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/
65
+ */
66
+ function getIsAppInstalledEventFired() {
67
+ return AppInstalledEventFiredStorage.get() === 'true';
68
+ }
69
+
70
+ async function getIsAppInstalledRelatedApps() {
71
+ if (!('getInstalledRelatedApps' in window.navigator)) {
72
+ return false;
73
+ }
74
+ try {
75
+ const relatedApps = await navigator.getInstalledRelatedApps();
76
+ return relatedApps.some((app) => app.platform === 'webapp');
77
+ } catch {
78
+ // Error might be thrown when Docusaurus is embedded in an iframe:
79
+ // registerSW failed DOMException: Failed to execute 'getInstalledRelatedApps' on 'Navigator': getInstalledRelatedApps() is only supported in top-level browsing contexts.
80
+ return false;
81
+ }
82
+ }
83
+ function isStandaloneDisplayMode() {
84
+ return window.matchMedia('(display-mode: standalone)').matches;
85
+ }
86
+
87
+ const OfflineModeActivationStrategiesImplementations = {
88
+ always: () => true,
89
+ mobile: () => window.innerWidth <= MAX_MOBILE_WIDTH,
90
+ saveData: () => !!navigator.connection?.saveData,
91
+ appInstalled: () =>
92
+ getIsAppInstalledEventFired() || getIsAppInstalledRelatedApps(),
93
+ standalone: () => isStandaloneDisplayMode(),
94
+ queryString: () =>
95
+ new URLSearchParams(window.location.search).get('offlineMode') === 'true',
96
+ };
97
+
98
+ async function getActiveStrategies() {
99
+ const activeStrategies = await Promise.all(
100
+ PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.map((strategyName) =>
101
+ Promise.resolve(
102
+ OfflineModeActivationStrategiesImplementations[strategyName](),
103
+ ).then((isActive) => (isActive ? strategyName : undefined)),
104
+ ),
105
+ );
106
+ return activeStrategies.filter(Boolean);
107
+ }
108
+
109
+ async function getIsOfflineModeEnabled() {
110
+ const activeStrategies = await getActiveStrategies();
111
+ const enabled = activeStrategies.length > 0;
112
+ debugLog(
113
+ enabled
114
+ ? 'offline mode enabled, because of activation strategies'
115
+ : 'offline mode disabled, because none of the offlineModeActivationStrategies could be used',
116
+ {
117
+ activeStrategies,
118
+ availableStrategies: PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES,
119
+ },
120
+ );
121
+ return enabled;
122
+ }
123
+
124
+ function createServiceWorkerUrl(params: object) {
125
+ const paramsQueryString = JSON.stringify(params);
126
+ const url = `${PWA_SERVICE_WORKER_URL}?params=${encodeURIComponent(
127
+ paramsQueryString,
128
+ )}`;
129
+ debugLog('service worker url', {url, params});
130
+ return url;
131
+ }
132
+
133
+ async function registerSW() {
134
+ const [{Workbox}, offlineMode] = await Promise.all([
135
+ import('workbox-window'),
136
+ getIsOfflineModeEnabled(),
137
+ ]);
138
+ const url = createServiceWorkerUrl({offlineMode, debug: PWA_DEBUG});
139
+ const wb = new Workbox(url);
140
+ const sendSkipWaiting = () => wb.messageSW({type: 'SKIP_WAITING'});
141
+ const handleServiceWorkerWaiting = () => {
142
+ debugLog('handleServiceWorkerWaiting');
143
+ // Immediately load new service worker when files aren't cached
144
+ if (!offlineMode) {
145
+ return sendSkipWaiting();
146
+ }
147
+ return import('./renderReloadPopup.js').then(({renderReloadPopup}) =>
148
+ renderReloadPopup({
149
+ onReload() {
150
+ wb.addEventListener('controlling', () => {
151
+ window.location.reload();
152
+ });
153
+ sendSkipWaiting();
154
+ },
155
+ }),
156
+ );
157
+ };
158
+ // Update the current service worker when the next one has finished
159
+ // installing and transitions to waiting state.
160
+ wb.addEventListener('waiting', (event) => {
161
+ debugLog('event waiting', {event});
162
+ void handleServiceWorkerWaiting();
163
+ });
164
+ // Update current service worker if the next one finishes installing and
165
+ // moves to waiting state in another tab.
166
+ // @ts-expect-error: not present in the API typings anymore
167
+ wb.addEventListener('externalwaiting', (event) => {
168
+ debugLog('event externalwaiting', {event});
169
+ void handleServiceWorkerWaiting();
170
+ });
171
+ const registration = await wb.register();
172
+ if (registration) {
173
+ if (registration.active) {
174
+ debugLog('registration.active', {registration});
175
+ }
176
+ if (registration.installing) {
177
+ debugLog('registration.installing', {registration});
178
+ }
179
+ if (registration.waiting) {
180
+ debugLog('registration.waiting', {registration});
181
+ // Update service worker if the next one is already in the waiting
182
+ // state. This happens when the user doesn't click on `reload` in
183
+ // the popup.
184
+ await handleServiceWorkerWaiting();
185
+ }
186
+ }
187
+ }
188
+
189
+ // TODO these events still works in chrome but have been removed from the spec
190
+ // in 2019! See https://github.com/w3c/manifest/pull/836
191
+ function addLegacyAppInstalledEventsListeners() {
192
+ debugLog('addLegacyAppInstalledEventsListeners');
193
+
194
+ window.addEventListener('appinstalled', (event) => {
195
+ debugLog('event appinstalled', {event});
196
+ AppInstalledEventFiredStorage.set('true');
197
+ debugLog("AppInstalledEventFiredStorage.set('true')");
198
+ // After the app is installed, we register a service worker with the path
199
+ // `/sw?enabled`. Since the previous service worker was `/sw`, it'll be
200
+ // treated as a new one. The previous registration will need to be
201
+ // cleared, otherwise the reload popup will show.
202
+ void clearRegistrations();
203
+ });
204
+
205
+ // TODO this event still works in chrome but has been removed from the spec
206
+ // in 2019!!!
207
+ window.addEventListener('beforeinstallprompt', (event) => {
208
+ debugLog('event beforeinstallprompt', {event});
209
+ // TODO instead of default browser install UI, show custom docusaurus
210
+ // prompt?
211
+ // event.preventDefault();
212
+ const appInstalledEventFired = AppInstalledEventFiredStorage.get();
213
+ debugLog('AppInstalledEventFiredStorage.get()', {appInstalledEventFired});
214
+ if (appInstalledEventFired) {
215
+ AppInstalledEventFiredStorage.del();
216
+ debugLog('AppInstalledEventFiredStorage.del()');
217
+ // After uninstalling the app, if the user doesn't clear all data, then
218
+ // the previous service worker will continue serving cached files. We
219
+ // need to clear registrations and reload, otherwise the popup shows.
220
+ void clearRegistrations();
221
+ }
222
+ });
223
+
224
+ debugLog(
225
+ 'legacy appinstalled and beforeinstallprompt event listeners installed',
226
+ );
227
+ }
228
+
229
+ /*
230
+ Init code to run on the client!
231
+ */
232
+ if (ExecutionEnvironment.canUseDOM) {
233
+ debugLog('debug mode enabled');
234
+
235
+ if ('serviceWorker' in navigator) {
236
+ // First: add the listeners asap/synchronously
237
+ addLegacyAppInstalledEventsListeners();
238
+
239
+ // Then try to register the SW using lazy/dynamic imports
240
+ registerSW().catch((e: unknown) => console.error('registerSW failed', e));
241
+ }
242
+ }
@@ -0,0 +1,28 @@
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 {createRoot} from 'react-dom/client';
9
+ import type {Props} from '@theme/PwaReloadPopup';
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 function renderReloadPopup(props: Props): Promise<void> {
23
+ const container = getContainer() ?? createContainer();
24
+ return import('@theme/PwaReloadPopup').then(({default: ReloadPopup}) => {
25
+ const root = createRoot(container);
26
+ root.render(<ReloadPopup {...props} />);
27
+ });
28
+ }