webapp-astro-pwa
Version:
A ready-to-use Astro component library for adding Progressive Web App (PWA) support to your Astro projects. This package provides drop-in components and utilities for manifest injection, service worker registration, install prompts, and more. Includes a w
238 lines (215 loc) • 7.07 kB
JavaScript
import { precacheAndRoute, cleanupOutdatedCaches } from "workbox-precaching";
import { registerRoute, NavigationRoute, Route } from "workbox-routing";
import * as navigationPreload from "workbox-navigation-preload";
import { googleFontsCache } from "workbox-recipes";
import {
NetworkOnly,
CacheOnly,
NetworkFirst,
StaleWhileRevalidate,
CacheFirst,
} from "workbox-strategies";
const SETTINGS = process.env.settings;
const STRATEGY = SETTINGS.strategy;
const CACHE_ASSETS = SETTINGS.cacheAssets;
const DISABLE_DEV_LOGS = SETTINGS.disableDevLogs;
const { scripts } = SETTINGS;
const { notification } = SETTINGS;
const { saveSubscriptionPath } = SETTINGS;
const { applicationServerKey } = SETTINGS;
const { isFirebaseMessaging } = SETTINGS;
/**
* Dynamically imports additional scripts into the Service Worker.
* @param {Array<string>} scripts - Array of script URLs to import.
*/
function addScriptsToSW(scripts) {
if (scripts && scripts.length > 0) {
scripts.forEach((script) => {
importScripts(script);
});
}
}
/**
* Handles web push notifications, including subscription and notification events.
*/
(function runNotifications() {
/**
* Converts a Base64 string to a Uint8Array.
* @param {string} base64String - The Base64 string to convert.
* @returns {Uint8Array} - The converted Uint8Array.
*/
const urlBase64ToUint8Array = (base64String) => {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/");
const rawData = atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
/**
* Sends the subscription object to the server.
* @param {PushSubscription} subscription - The subscription object.
* @param {Object} body - The request body containing subscription details.
* @returns {Promise<Object>} - The server response.
*/
const saveSubscription = async (saveSubscriptionPath, body) => {
const response = await fetch(saveSubscriptionPath, body);
return response.json();
};
/**
* Handles the subscription process for push notifications.
*/
const handleSubscription = async () => {
if (Notification.permission !== "granted") return;
const subscription = await self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(applicationServerKey),
});
const body = {
method: "post",
headers: { "Content-type": "application/json" },
body: JSON.stringify(subscription),
};
if (!saveSubscriptionPath) return;
saveSubscription(saveSubscriptionPath, body);
};
/**
* Handles incoming push notifications and displays them.
* @param {PushEvent} event - The push event.
*/
const getNotification = async (event) => {
let notifications = {
title: "Notification",
body: "",
icon: "",
badge: "",
image: "",
actions: [],
vibrate: [],
sound: "",
url: "/",
};
if (event && event?.data) {
try {
notifications = event.data.json().notification;
} catch {
// fallback if not JSON
notifications.body = event.data.text();
}
}
const options = {
body: notifications.body,
icon: notifications.icon || notifications.image,
data: { notifURL: notifications.url },
badge: notifications.badge,
vibrate: notifications.vibrate,
sound: notifications.sound,
image: notifications.image,
actions: notifications.actions,
};
event?.waitUntil(
self.registration.showNotification(notifications.title || "Notification", options)
);
};
/**
* Handles messages to subscribe to push notifications.
* @param {MessageEvent} event - The message event.
*/
function getNotificationAcceptMessage(event) {
getNotification();
if (isFirebaseMessaging) return;
if (event.data && event.data.action === "subscribeWEBAppPWA") {
handleSubscription();
}
}
/**
* Handles notification click events and opens the associated URL.
* @param {NotificationEvent} event - The notification click event.
*/
function handleNitificationClick(event) {
event.notification.close();
const url = event.notification.data?.notifURL || "/";
event?.waitUntil(self.clients.openWindow(url));
}
/**
* Registers event listeners for push notifications.
*/
function runNotificationsEventsListeners() {
self.addEventListener("message", getNotificationAcceptMessage);
self.addEventListener("push", getNotification);
self.addEventListener("notificationclick", handleNitificationClick);
if (isFirebaseMessaging) return;
self.addEventListener("activate", handleSubscription);
}
if (notification) runNotificationsEventsListeners();
})();
/**
* Configures caching strategies and routes using Workbox.
*/
function workboxCacheAssets() {
cleanupOutdatedCaches();
googleFontsCache();
// INFO: turn off logging
// eslint-disable-next-line no-underscore-dangle
self.__WB_DISABLE_DEV_LOGS = DISABLE_DEV_LOGS;
// Precache the manifest
precacheAndRoute(self.__WB_MANIFEST);
// Enable navigation preload
navigationPreload.enable();
// Create a navigation route with a Network-first strategy
const navigationRoute = new NavigationRoute(
new NetworkFirst({
cacheName: "navigations",
})
);
registerRoute(navigationRoute);
/**
* Returns the caching strategy based on the configured strategy.
* @returns {WorkboxStrategy} - The caching strategy.
*/
// INFO: Possible strategies is CacheFirst, CacheOnly, NetworkFirst, NetworkOnly, StaleWhileRevalidate
function returnStrategy() {
switch (STRATEGY) {
case "CacheFirst":
return new CacheFirst({
cacheName: CACHE_ASSETS,
});
case "CacheOnly":
return new CacheOnly({
cacheName: CACHE_ASSETS,
});
case "NetworkFirst":
return new NetworkFirst({
cacheName: CACHE_ASSETS,
});
case "NetworkOnly":
return new NetworkOnly({
cacheName: CACHE_ASSETS,
});
default:
return new StaleWhileRevalidate({
cacheName: CACHE_ASSETS,
});
}
}
// Create a route for static assets with a caching strategy
const staticAssetsRoute = new Route(
({ request }) =>
["image", "script", "style"].includes(request.destination) ||
request.origin === "https://fonts.googleapis.com",
returnStrategy()
);
registerRoute(staticAssetsRoute);
// Create a route for Google Fonts with a Cache-first strategy
const googleFontsRoute = new Route(
({ url }) => url.origin === "https://fonts.gstatic.com",
new CacheFirst({
cacheName: "google-fonts-stylesheets",
})
);
registerRoute(googleFontsRoute);
}
addScriptsToSW(scripts);
workboxCacheAssets();