serwist
Version:
A Swiss Army knife for service workers.
791 lines (765 loc) • 30.8 kB
JavaScript
import { P as PrecacheStrategy, c as createCacheKey, h as PrecacheInstallReportPlugin, i as parallel, j as printInstallDetails, k as printCleanupDetails, R as Route, g as generateURLVariations, f as defaultMethod, n as normalizeHandler, p as parseRoute, S as Strategy, b as NavigationRoute, B as BackgroundSyncPlugin, N as NetworkFirst, a as NetworkOnly, e as enableNavigationPreload, s as setCacheNameDetails, d as disableDevLogs } from './chunks/printInstallDetails.js';
import { c as cacheNames, f as finalAssertExports, S as SerwistError, l as logger, w as waitUntil, g as getFriendlyURL, b as cleanupOutdatedCaches, a as clientsClaim } from './chunks/waitUntil.js';
import 'idb';
class PrecacheCacheKeyPlugin {
_precacheController;
constructor({ precacheController }){
this._precacheController = precacheController;
}
cacheKeyWillBeUsed = async ({ request, params })=>{
const cacheKey = params?.cacheKey || this._precacheController.getCacheKeyForURL(request.url);
return cacheKey ? new Request(cacheKey, {
headers: request.headers
}) : request;
};
}
class PrecacheController {
_installAndActiveListenersAdded;
_concurrentPrecaching;
_strategy;
_urlsToCacheKeys = new Map();
_urlsToCacheModes = new Map();
_cacheKeysToIntegrities = new Map();
constructor({ cacheName, plugins = [], fallbackToNetwork = true, concurrentPrecaching = 1 } = {}){
this._concurrentPrecaching = concurrentPrecaching;
this._strategy = new PrecacheStrategy({
cacheName: cacheNames.getPrecacheName(cacheName),
plugins: [
...plugins,
new PrecacheCacheKeyPlugin({
precacheController: this
})
],
fallbackToNetwork
});
this.install = this.install.bind(this);
this.activate = this.activate.bind(this);
}
get strategy() {
return this._strategy;
}
precache(entries) {
this.addToCacheList(entries);
if (!this._installAndActiveListenersAdded) {
self.addEventListener("install", this.install);
self.addEventListener("activate", this.activate);
this._installAndActiveListenersAdded = true;
}
}
addToCacheList(entries) {
if (process.env.NODE_ENV !== "production") {
finalAssertExports.isArray(entries, {
moduleName: "serwist/legacy",
className: "PrecacheController",
funcName: "addToCacheList",
paramName: "entries"
});
}
const urlsToWarnAbout = [];
for (const entry of entries){
if (typeof entry === "string") {
urlsToWarnAbout.push(entry);
} else if (entry && !entry.integrity && entry.revision === undefined) {
urlsToWarnAbout.push(entry.url);
}
const { cacheKey, url } = createCacheKey(entry);
const cacheMode = typeof entry !== "string" && entry.revision ? "reload" : "default";
if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) {
throw new SerwistError("add-to-cache-list-conflicting-entries", {
firstEntry: this._urlsToCacheKeys.get(url),
secondEntry: cacheKey
});
}
if (typeof entry !== "string" && entry.integrity) {
if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) {
throw new SerwistError("add-to-cache-list-conflicting-integrities", {
url
});
}
this._cacheKeysToIntegrities.set(cacheKey, entry.integrity);
}
this._urlsToCacheKeys.set(url, cacheKey);
this._urlsToCacheModes.set(url, cacheMode);
if (urlsToWarnAbout.length > 0) {
const warningMessage = `Serwist is precaching URLs without revision info: ${urlsToWarnAbout.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;
if (process.env.NODE_ENV === "production") {
console.warn(warningMessage);
} else {
logger.warn(warningMessage);
}
}
}
}
install(event) {
return waitUntil(event, async ()=>{
const installReportPlugin = new PrecacheInstallReportPlugin();
this.strategy.plugins.push(installReportPlugin);
await parallel(this._concurrentPrecaching, Array.from(this._urlsToCacheKeys.entries()), async ([url, cacheKey])=>{
const integrity = this._cacheKeysToIntegrities.get(cacheKey);
const cacheMode = this._urlsToCacheModes.get(url);
const request = new Request(url, {
integrity,
cache: cacheMode,
credentials: "same-origin"
});
await Promise.all(this.strategy.handleAll({
event,
request,
url: new URL(request.url),
params: {
cacheKey
}
}));
});
const { updatedURLs, notUpdatedURLs } = installReportPlugin;
if (process.env.NODE_ENV !== "production") {
printInstallDetails(updatedURLs, notUpdatedURLs);
}
return {
updatedURLs,
notUpdatedURLs
};
});
}
activate(event) {
return waitUntil(event, async ()=>{
const cache = await self.caches.open(this.strategy.cacheName);
const currentlyCachedRequests = await cache.keys();
const expectedCacheKeys = new Set(this._urlsToCacheKeys.values());
const deletedCacheRequests = [];
for (const request of currentlyCachedRequests){
if (!expectedCacheKeys.has(request.url)) {
await cache.delete(request);
deletedCacheRequests.push(request.url);
}
}
if (process.env.NODE_ENV !== "production") {
printCleanupDetails(deletedCacheRequests);
}
return {
deletedCacheRequests
};
});
}
getURLsToCacheKeys() {
return this._urlsToCacheKeys;
}
getCachedURLs() {
return [
...this._urlsToCacheKeys.keys()
];
}
getCacheKeyForURL(url) {
const urlObject = new URL(url, location.href);
return this._urlsToCacheKeys.get(urlObject.href);
}
getIntegrityForCacheKey(cacheKey) {
return this._cacheKeysToIntegrities.get(cacheKey);
}
async matchPrecache(request) {
const url = request instanceof Request ? request.url : request;
const cacheKey = this.getCacheKeyForURL(url);
if (cacheKey) {
const cache = await self.caches.open(this.strategy.cacheName);
return cache.match(cacheKey);
}
return undefined;
}
createHandlerBoundToURL(url) {
const cacheKey = this.getCacheKeyForURL(url);
if (!cacheKey) {
throw new SerwistError("non-precached-url", {
url
});
}
return (options)=>{
options.request = new Request(url);
options.params = {
cacheKey,
...options.params
};
return this.strategy.handle(options);
};
}
}
let defaultPrecacheController = undefined;
const getSingletonPrecacheController = ()=>{
if (!defaultPrecacheController) {
defaultPrecacheController = new PrecacheController();
}
return defaultPrecacheController;
};
const setSingletonPrecacheController = (precacheController)=>{
defaultPrecacheController = precacheController;
return defaultPrecacheController;
};
class PrecacheFallbackPlugin {
_fallbackUrls;
_precacheController;
constructor({ fallbackUrls, precacheController }){
this._fallbackUrls = fallbackUrls;
this._precacheController = precacheController || getSingletonPrecacheController();
}
async handlerDidError(param) {
for (const fallback of this._fallbackUrls){
if (typeof fallback === "string") {
const fallbackResponse = await this._precacheController.matchPrecache(fallback);
if (fallbackResponse !== undefined) {
return fallbackResponse;
}
} else if (fallback.matcher(param)) {
const fallbackResponse = await this._precacheController.matchPrecache(fallback.url);
if (fallbackResponse !== undefined) {
return fallbackResponse;
}
}
}
return undefined;
}
}
class PrecacheRoute extends Route {
constructor(precacheController, options){
const match = ({ request })=>{
const urlsToCacheKeys = precacheController.getURLsToCacheKeys();
for (const possibleURL of generateURLVariations(request.url, options)){
const cacheKey = urlsToCacheKeys.get(possibleURL);
if (cacheKey) {
const integrity = precacheController.getIntegrityForCacheKey(cacheKey);
return {
cacheKey,
integrity
};
}
}
if (process.env.NODE_ENV !== "production") {
logger.debug(`Precaching did not find a match for ${getFriendlyURL(request.url)}`);
}
return;
};
super(match, precacheController.strategy);
}
}
class Router {
_routes;
_defaultHandlerMap;
_fetchListenerHandler = null;
_cacheListenerHandler = null;
_catchHandler;
constructor(){
this._routes = new Map();
this._defaultHandlerMap = new Map();
}
get routes() {
return this._routes;
}
addFetchListener() {
if (!this._fetchListenerHandler) {
this._fetchListenerHandler = (event)=>{
const { request } = event;
const responsePromise = this.handleRequest({
request,
event
});
if (responsePromise) {
event.respondWith(responsePromise);
}
};
self.addEventListener("fetch", this._fetchListenerHandler);
}
}
removeFetchListener() {
if (this._fetchListenerHandler) {
self.removeEventListener("fetch", this._fetchListenerHandler);
this._fetchListenerHandler = null;
}
}
addCacheListener() {
if (!this._cacheListenerHandler) {
this._cacheListenerHandler = (event)=>{
if (event.data && event.data.type === "CACHE_URLS") {
const { payload } = event.data;
if (process.env.NODE_ENV !== "production") {
logger.debug("Caching URLs from the window", payload.urlsToCache);
}
const requestPromises = Promise.all(payload.urlsToCache.map((entry)=>{
if (typeof entry === "string") {
entry = [
entry
];
}
const request = new Request(...entry);
return this.handleRequest({
request,
event
});
}));
event.waitUntil(requestPromises);
if (event.ports?.[0]) {
void requestPromises.then(()=>event.ports[0].postMessage(true));
}
}
};
self.addEventListener("message", this._cacheListenerHandler);
}
}
removeCacheListener() {
if (this._cacheListenerHandler) {
self.removeEventListener("message", this._cacheListenerHandler);
}
}
handleRequest({ request, event }) {
if (process.env.NODE_ENV !== "production") {
finalAssertExports.isInstance(request, Request, {
moduleName: "serwist/legacy",
className: "Router",
funcName: "handleRequest",
paramName: "options.request"
});
}
const url = new URL(request.url, location.href);
if (!url.protocol.startsWith("http")) {
if (process.env.NODE_ENV !== "production") {
logger.debug("Router only supports URLs that start with 'http'.");
}
return;
}
const sameOrigin = url.origin === location.origin;
const { params, route } = this.findMatchingRoute({
event,
request,
sameOrigin,
url
});
let handler = route?.handler;
const debugMessages = [];
if (process.env.NODE_ENV !== "production") {
if (handler) {
debugMessages.push([
"Found a route to handle this request:",
route
]);
if (params) {
debugMessages.push([
`Passing the following params to the route's handler:`,
params
]);
}
}
}
const method = request.method;
if (!handler && this._defaultHandlerMap.has(method)) {
if (process.env.NODE_ENV !== "production") {
debugMessages.push(`Failed to find a matching route. Falling back to the default handler for ${method}.`);
}
handler = this._defaultHandlerMap.get(method);
}
if (!handler) {
if (process.env.NODE_ENV !== "production") {
logger.debug(`No route found for: ${getFriendlyURL(url)}`);
}
return;
}
if (process.env.NODE_ENV !== "production") {
logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`);
for (const msg of debugMessages){
if (Array.isArray(msg)) {
logger.log(...msg);
} else {
logger.log(msg);
}
}
logger.groupEnd();
}
let responsePromise;
try {
responsePromise = handler.handle({
url,
request,
event,
params
});
} catch (err) {
responsePromise = Promise.reject(err);
}
const catchHandler = route?.catchHandler;
if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) {
responsePromise = responsePromise.catch(async (err)=>{
if (catchHandler) {
if (process.env.NODE_ENV !== "production") {
logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`);
logger.error("Error thrown by:", route);
logger.error(err);
logger.groupEnd();
}
try {
return await catchHandler.handle({
url,
request,
event,
params
});
} catch (catchErr) {
if (catchErr instanceof Error) {
err = catchErr;
}
}
}
if (this._catchHandler) {
if (process.env.NODE_ENV !== "production") {
logger.groupCollapsed(`Error thrown when responding to: ${getFriendlyURL(url)}. Falling back to global Catch Handler.`);
logger.error("Error thrown by:", route);
logger.error(err);
logger.groupEnd();
}
return this._catchHandler.handle({
url,
request,
event
});
}
throw err;
});
}
return responsePromise;
}
findMatchingRoute({ url, sameOrigin, request, event }) {
const routes = this._routes.get(request.method) || [];
for (const route of routes){
let params;
const matchResult = route.match({
url,
sameOrigin,
request,
event
});
if (matchResult) {
if (process.env.NODE_ENV !== "production") {
if (matchResult instanceof Promise) {
logger.warn(`While routing ${getFriendlyURL(url)}, an async matchCallback function was used. Please convert the following route to use a synchronous matchCallback function:`, route);
}
}
params = matchResult;
if (Array.isArray(params) && params.length === 0) {
params = undefined;
} else if (matchResult.constructor === Object && Object.keys(matchResult).length === 0) {
params = undefined;
} else if (typeof matchResult === "boolean") {
params = undefined;
}
return {
route,
params
};
}
}
return {};
}
setDefaultHandler(handler, method = defaultMethod) {
this._defaultHandlerMap.set(method, normalizeHandler(handler));
}
setCatchHandler(handler) {
this._catchHandler = normalizeHandler(handler);
}
registerCapture(capture, handler, method) {
const route = parseRoute(capture, handler, method);
this.registerRoute(route);
return route;
}
registerRoute(route) {
if (process.env.NODE_ENV !== "production") {
finalAssertExports.isType(route, "object", {
moduleName: "serwist/legacy",
className: "Router",
funcName: "registerRoute",
paramName: "route"
});
finalAssertExports.hasMethod(route, "match", {
moduleName: "serwist/legacy",
className: "Router",
funcName: "registerRoute",
paramName: "route"
});
finalAssertExports.isType(route.handler, "object", {
moduleName: "serwist/legacy",
className: "Router",
funcName: "registerRoute",
paramName: "route"
});
finalAssertExports.hasMethod(route.handler, "handle", {
moduleName: "serwist/legacy",
className: "Router",
funcName: "registerRoute",
paramName: "route.handler"
});
finalAssertExports.isType(route.method, "string", {
moduleName: "serwist/legacy",
className: "Router",
funcName: "registerRoute",
paramName: "route.method"
});
}
if (!this._routes.has(route.method)) {
this._routes.set(route.method, []);
}
this._routes.get(route.method).push(route);
}
unregisterRoute(route) {
if (!this._routes.has(route.method)) {
throw new SerwistError("unregister-route-but-not-found-with-method", {
method: route.method
});
}
const routeIndex = this._routes.get(route.method).indexOf(route);
if (routeIndex > -1) {
this._routes.get(route.method).splice(routeIndex, 1);
} else {
throw new SerwistError("unregister-route-route-not-registered");
}
}
}
const addPlugins = (plugins)=>{
const precacheController = getSingletonPrecacheController();
precacheController.strategy.plugins.push(...plugins);
};
let defaultRouter = undefined;
const getSingletonRouter = ()=>{
if (!defaultRouter) {
defaultRouter = new Router();
defaultRouter.addFetchListener();
defaultRouter.addCacheListener();
}
return defaultRouter;
};
const setSingletonRouter = (router)=>{
if (defaultRouter) {
defaultRouter.removeFetchListener();
defaultRouter.removeCacheListener();
}
defaultRouter = router;
defaultRouter.addFetchListener();
defaultRouter.addCacheListener();
return defaultRouter;
};
const registerRoute = (capture, handler, method)=>{
return getSingletonRouter().registerCapture(capture, handler, method);
};
const addRoute = (options)=>{
const precacheRoute = new PrecacheRoute(getSingletonPrecacheController(), options);
registerRoute(precacheRoute);
};
const createHandlerBoundToURL = (url)=>{
const precacheController = getSingletonPrecacheController();
return precacheController.createHandlerBoundToURL(url);
};
const fallbacks = ({ precacheController = getSingletonPrecacheController(), router = getSingletonRouter(), runtimeCaching, entries, precacheOptions })=>{
precacheController.precache(entries);
router.registerRoute(new PrecacheRoute(precacheController, precacheOptions));
const fallbackPlugin = new PrecacheFallbackPlugin({
fallbackUrls: entries
});
runtimeCaching.forEach((cacheEntry)=>{
if (cacheEntry.handler instanceof Strategy && !cacheEntry.handler.plugins.some((plugin)=>"handlerDidError" in plugin)) {
cacheEntry.handler.plugins.push(fallbackPlugin);
}
return cacheEntry;
});
return runtimeCaching;
};
const getCacheKeyForURL = (url)=>{
const precacheController = getSingletonPrecacheController();
return precacheController.getCacheKeyForURL(url);
};
const handlePrecaching = ({ precacheController = getSingletonPrecacheController(), router = getSingletonRouter(), precacheEntries, precacheOptions, cleanupOutdatedCaches: cleanupOutdatedCaches$1 = false, navigateFallback, navigateFallbackAllowlist, navigateFallbackDenylist })=>{
if (!!precacheEntries && precacheEntries.length > 0) {
precacheController.precache(precacheEntries);
router.registerRoute(new PrecacheRoute(precacheController, precacheOptions));
if (cleanupOutdatedCaches$1) cleanupOutdatedCaches();
if (navigateFallback) {
router.registerRoute(new NavigationRoute(createHandlerBoundToURL(navigateFallback), {
allowlist: navigateFallbackAllowlist,
denylist: navigateFallbackDenylist
}));
}
}
};
const QUEUE_NAME = "serwist-google-analytics";
const MAX_RETENTION_TIME = 60 * 48;
const GOOGLE_ANALYTICS_HOST = "www.google-analytics.com";
const GTM_HOST = "www.googletagmanager.com";
const ANALYTICS_JS_PATH = "/analytics.js";
const GTAG_JS_PATH = "/gtag/js";
const GTM_JS_PATH = "/gtm.js";
const COLLECT_PATHS_REGEX = /^\/(\w+\/)?collect/;
const createOnSyncCallback = (config)=>{
return async ({ queue })=>{
let entry = undefined;
while(entry = await queue.shiftRequest()){
const { request, timestamp } = entry;
const url = new URL(request.url);
try {
const params = request.method === "POST" ? new URLSearchParams(await request.clone().text()) : url.searchParams;
const originalHitTime = timestamp - (Number(params.get("qt")) || 0);
const queueTime = Date.now() - originalHitTime;
params.set("qt", String(queueTime));
if (config.parameterOverrides) {
for (const param of Object.keys(config.parameterOverrides)){
const value = config.parameterOverrides[param];
params.set(param, value);
}
}
if (typeof config.hitFilter === "function") {
config.hitFilter.call(null, params);
}
await fetch(new Request(url.origin + url.pathname, {
body: params.toString(),
method: "POST",
mode: "cors",
credentials: "omit",
headers: {
"Content-Type": "text/plain"
}
}));
if (process.env.NODE_ENV !== "production") {
logger.log(`Request for '${getFriendlyURL(url.href)}' has been replayed`);
}
} catch (err) {
await queue.unshiftRequest(entry);
if (process.env.NODE_ENV !== "production") {
logger.log(`Request for '${getFriendlyURL(url.href)}' failed to replay, putting it back in the queue.`);
}
throw err;
}
}
if (process.env.NODE_ENV !== "production") {
logger.log("All Google Analytics request successfully replayed; " + "the queue is now empty!");
}
};
};
const createCollectRoutes = (bgSyncPlugin)=>{
const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && COLLECT_PATHS_REGEX.test(url.pathname);
const handler = new NetworkOnly({
plugins: [
bgSyncPlugin
]
});
return [
new Route(match, handler, "GET"),
new Route(match, handler, "POST")
];
};
const createAnalyticsJsRoute = (cacheName)=>{
const match = ({ url })=>url.hostname === GOOGLE_ANALYTICS_HOST && url.pathname === ANALYTICS_JS_PATH;
const handler = new NetworkFirst({
cacheName
});
return new Route(match, handler, "GET");
};
const createGtagJsRoute = (cacheName)=>{
const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTAG_JS_PATH;
const handler = new NetworkFirst({
cacheName
});
return new Route(match, handler, "GET");
};
const createGtmJsRoute = (cacheName)=>{
const match = ({ url })=>url.hostname === GTM_HOST && url.pathname === GTM_JS_PATH;
const handler = new NetworkFirst({
cacheName
});
return new Route(match, handler, "GET");
};
const initializeGoogleAnalytics = ({ router = getSingletonRouter(), cacheName, ...options } = {})=>{
const resolvedCacheName = cacheNames.getGoogleAnalyticsName(cacheName);
const bgSyncPlugin = new BackgroundSyncPlugin(QUEUE_NAME, {
maxRetentionTime: MAX_RETENTION_TIME,
onSync: createOnSyncCallback(options)
});
const routes = [
createGtmJsRoute(resolvedCacheName),
createAnalyticsJsRoute(resolvedCacheName),
createGtagJsRoute(resolvedCacheName),
...createCollectRoutes(bgSyncPlugin)
];
for (const route of routes){
router.registerRoute(route);
}
};
const registerRuntimeCaching = (...runtimeCachingList)=>{
for (const entry of runtimeCachingList){
registerRoute(entry.matcher, entry.handler, entry.method);
}
};
const installSerwist = ({ precacheController = getSingletonPrecacheController(), router = getSingletonRouter(), precacheEntries, precacheOptions, cleanupOutdatedCaches, navigateFallback, navigateFallbackAllowlist, navigateFallbackDenylist, skipWaiting, importScripts, navigationPreload = false, cacheId, clientsClaim: clientsClaim$1 = false, runtimeCaching, offlineAnalyticsConfig, disableDevLogs: disableDevLogs$1 = false, fallbacks: fallbacks$1 })=>{
if (!!importScripts && importScripts.length > 0) self.importScripts(...importScripts);
if (navigationPreload) enableNavigationPreload();
if (cacheId !== undefined) {
setCacheNameDetails({
prefix: cacheId
});
}
if (skipWaiting) {
self.skipWaiting();
} else {
self.addEventListener("message", (event)=>{
if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
}
if (clientsClaim$1) clientsClaim();
handlePrecaching({
precacheController,
router,
precacheEntries,
precacheOptions,
cleanupOutdatedCaches,
navigateFallback,
navigateFallbackAllowlist,
navigateFallbackDenylist
});
if (runtimeCaching !== undefined) {
if (fallbacks$1 !== undefined) {
runtimeCaching = fallbacks({
precacheController,
router,
runtimeCaching,
entries: fallbacks$1.entries,
precacheOptions
});
}
registerRuntimeCaching(...runtimeCaching);
}
if (offlineAnalyticsConfig !== undefined) {
if (typeof offlineAnalyticsConfig === "boolean") {
offlineAnalyticsConfig && initializeGoogleAnalytics({
router
});
} else {
initializeGoogleAnalytics({
...offlineAnalyticsConfig,
router
});
}
}
if (disableDevLogs$1) disableDevLogs();
};
const matchPrecache = (request)=>{
return getSingletonPrecacheController().matchPrecache(request);
};
const precache = (entries)=>{
getSingletonPrecacheController().precache(entries);
};
const precacheAndRoute = (entries, options)=>{
precache(entries);
addRoute(options);
};
const setCatchHandler = (handler)=>{
getSingletonRouter().setCatchHandler(handler);
};
const setDefaultHandler = (handler)=>{
getSingletonRouter().setDefaultHandler(handler);
};
const unregisterRoute = (route)=>{
getSingletonRouter().unregisterRoute(route);
};
export { PrecacheController, PrecacheFallbackPlugin, PrecacheRoute, Router, addPlugins, addRoute, createHandlerBoundToURL, fallbacks, getCacheKeyForURL, getSingletonPrecacheController, getSingletonRouter, handlePrecaching, initializeGoogleAnalytics, installSerwist, matchPrecache, precache, precacheAndRoute, registerRoute, registerRuntimeCaching, setCatchHandler, setDefaultHandler, setSingletonPrecacheController, setSingletonRouter, unregisterRoute };