Skip to content

Instantly share code, notes, and snippets.

@dsebastien
Created January 31, 2020 11:35
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dsebastien/12c47fdb6517cfdab9473297f4472d22 to your computer and use it in GitHub Desktop.
Save dsebastien/12c47fdb6517cfdab9473297f4472d22 to your computer and use it in GitHub Desktop.
Workbox 5 + Workbox build + TypeScript SW + Webpack build + Angular app
{
...
"scripts": {
"start:web:prod": "npm run build:prod:web && http-server dist/apps/web -d -c-1 -a 0.0.0.0 --proxy http://127.0.0.1:4200? --port 4200",
"start:web:prod:local": "npm run build:prod:web:local && http-server dist/apps/web -d -c-1 -a 0.0.0.0 --proxy http://127.0.0.1:4200? --port 4200",
"build:prod:web": "ng build web --prod",
"postbuild:prod:web": "npm run build:pwa:web",
"build:prod:web:local": "ng build web --prod",
"postbuild:prod:web:local": "npm run build:pwa:web:local",
"build:pwa:web": "rimraf ./dist/apps/web/service-worker.js && webpack --config ./service-worker/webpack.prod.config.js --progress --colors && node ./workbox-build-inject.js",
"build:pwa:web:local": "rimraf ./dist/apps/web/service-worker.js && webpack --config ./service-worker/webpack.dev.config.js --progress --colors && node ./workbox-build-inject.js",
},
"private": true,
"dependencies": {
...
"workbox-core": "5.0.0",
"workbox-routing": "5.0.0",
"workbox-strategies": "5.0.0",
"workbox-precaching": "5.0.0",
"workbox-expiration": "5.0.0",
"workbox-background-sync": "5.0.0",
"workbox-cacheable-response": "5.0.0",
"workbox-window": "5.0.0",
"workbox-navigation-preload": "5.0.0",
"workbox-broadcast-update": "5.0.0",
},
"devDependencies": {
...
"ts-loader": "6.2.1",
"typescript": "3.5.3",
"webpack": "4.41.5",
"webpack-cli": "3.3.10",
"workbox-build": "5.0.0"
},
}
// Service worker
//
// References
// https://github.com/webmaxru/pwatter/blob/workbox/src/sw-default.js
// Caching strategies: https://developers.google.com/web/tools/workbox/modules/workbox-strategies#stale-while-revalidate
// Example: https://github.com/JeremieLitzler/mws.nd.2018.s3/blob/master/sw.js
import { CacheableResponsePlugin } from "workbox-cacheable-response";
import { ExpirationPlugin } from "workbox-expiration";
import { precacheAndRoute, cleanupOutdatedCaches, createHandlerBoundToURL } from "workbox-precaching";
import { registerRoute, NavigationRoute } from "workbox-routing";
import { NetworkFirst, NetworkOnly, StaleWhileRevalidate, CacheFirst } from "workbox-strategies";
declare const self: any;
const componentName = "Service Worker";
// Enable debug mode during development
const DEBUG_MODE = location.hostname.endsWith(".app.local") || location.hostname === "localhost";
const DAY_IN_SECONDS = 24 * 60 * 60;
const MONTH_IN_SECONDS = DAY_IN_SECONDS * 30;
const YEAR_IN_SECONDS = DAY_IN_SECONDS * 365;
/**
* The current version of the service worker.
*/
const SERVICE_WORKER_VERSION = "1.0.0";
if (DEBUG_MODE) {
console.debug(`Service worker version ${SERVICE_WORKER_VERSION} loading...`);
}
// ------------------------------------------------------------------------------------------
// Precaching configuration
// ------------------------------------------------------------------------------------------
cleanupOutdatedCaches();
// Precaching
// Make sure that all the assets passed in the array below are fetched and cached
// The empty array below is replaced at build time with the full list of assets to cache
// This is done by workbox-build-inject.js for the production build
const assetsToCache = self.__WB_MANIFEST;
// To customize the assets afterwards:
//assetsToCache = [...assetsToCache, ???];
if (DEBUG_MODE) {
console.trace(`${componentName}:: Assets that will be cached: `, assetsToCache);
}
precacheAndRoute(assetsToCache);
// ------------------------------------------------------------------------------------------
// Routes
// ------------------------------------------------------------------------------------------
// Default page handler for offline usage,
// where the browser does not how to handle deep links
// it's a SPA, so each path that is a navigation should default to index.html
const defaultRouteHandler = createHandlerBoundToURL("/index.html");
const defaultNavigationRoute = new NavigationRoute(defaultRouteHandler, {
//allowlist: [],
//denylist: [],
});
registerRoute(defaultNavigationRoute);
// Cache the Google Fonts stylesheets with a stale while revalidate strategy.
registerRoute(
/^https:\/\/fonts\.googleapis\.com/,
new StaleWhileRevalidate({
cacheName: "google-fonts-stylesheets",
}),
);
// Cache the Google Fonts webfont files with a cache first strategy for 1 year.
registerRoute(
/^https:\/\/fonts\.gstatic\.com/,
new CacheFirst({
cacheName: "google-fonts-webfonts",
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxAgeSeconds: YEAR_IN_SECONDS,
maxEntries: 30,
purgeOnQuotaError: true, // Automatically cleanup if quota is exceeded.
}),
],
}),
);
// Make JS/CSS fast by returning assets from the cache
// But make sure they're updating in the background for next use
registerRoute(/\.(?:js|css)$/, new StaleWhileRevalidate());
// Cache images
// But clean up after a while
registerRoute(
/\.(?:png|gif|jpg|jpeg|svg)$/,
new CacheFirst({
cacheName: "images",
plugins: [
new ExpirationPlugin({
maxEntries: 250,
maxAgeSeconds: MONTH_IN_SECONDS,
purgeOnQuotaError: true, // Automatically cleanup if quota is exceeded.
}),
],
}),
);
// Anything authentication related MUST be performed online
registerRoute(/(https:\/\/)?([^\/\s]+\/)api\/v1\/auth\/.*/, new NetworkOnly());
// Database access is only supported while online
registerRoute(/(https:\/\/)?([^\/\s]+\/)database\/.*/, new NetworkOnly());
// ------------------------------------------------------------------------------------------
// Messages
// ------------------------------------------------------------------------------------------
self.addEventListener("message", (event: { data: any; type: any; ports: any }) => {
// TODO define/use correct data type
if (event && event.data && event.data.type) {
// return the version of this service worker
if ("GET_VERSION" === event.data.type) {
if (DEBUG_MODE) {
console.debug(`${componentName}:: Returning the service worker version: ${SERVICE_WORKER_VERSION}`);
}
event.ports[0].postMessage(SERVICE_WORKER_VERSION);
}
// When this message is received, we can skip waiting and become active
// (i.e., this version of the service worker becomes active)
// Reference about why we wait: https://stackoverflow.com/questions/51715127/what-are-the-downsides-to-using-skipwaiting-and-clientsclaim-with-workbox
if ("SKIP_WAITING" === event.data.type) {
if (DEBUG_MODE) {
console.debug(`${componentName}:: Skipping waiting...`);
}
self.skipWaiting();
}
// When this message is received, we can take control of the clients with this version
// of the service worker
if ("CLIENTS_CLAIM" === event.data.type) {
if (DEBUG_MODE) {
console.debug(`${componentName}:: Claiming clients and cleaning old caches`);
}
self.clients.claim();
}
}
});
// DEV Webpack configuration used to build the service worker
const path = require("path");
const webBuildTargetFolder = path.join(__dirname, "..", "dist", "apps", "web");
const targetServiceWorkerFilename = "service-worker.js";
module.exports = {
target: "node",
mode: "none",
// WARNING: commented out to disable source maps
//devtool: 'inline-source-map',
entry: {
index: path.join(__dirname, "src", "service-worker.ts"),
},
resolve: { extensions: [".js", ".ts"] },
output: {
path: webBuildTargetFolder,
filename: targetServiceWorkerFilename,
},
module: {
rules: [
{
test: /\.ts$/,
loader: "ts-loader",
options: {
onlyCompileBundledFiles: true,
},
},
],
},
plugins: [],
};
// PROD Webpack configuration used to build the service worker
const webpackDevConfig = require("./webpack.dev.config");
module.exports = Object.assign({}, webpackDevConfig, {
mode: "production",
});
// Script that modifies the service-worker.js configuration using workbox-build
// Reference: https://developers.google.com/web/tools/workbox/modules/workbox-build
const { injectManifest } = require("workbox-build");
// Workbox configuration
const workboxConfig = require("./workbox-config");
console.log(`Workbox configuration: `, workboxConfig);
// We use injectManifest to inject everything we need into service-worker.js
// Reference: https://developers.google.com/web/tools/workbox/modules/workbox-build
injectManifest(workboxConfig).then(({ count, size }) => {
console.log(`Generated ${workboxConfig.swDest}, which will precache ${count} files (${size} bytes)`);
});
module.exports = {
globDirectory: "dist/apps/web/",
globPatterns: ["**/*.{css,eot,html,ico,jpg,js,json,png,svg,ttf,txt,webmanifest,woff,woff2,webm,xml}"],
globFollow: true, // follow symlinks
globStrict: true, // fail the build if anything goes wrong while reading the files
globIgnores: [
// Ignore Angular's ES5 bundles
// With this, we eagerly load the es2015
// bundles and we only load/cache the es5 bundles when requested
// i.e., on browsers that need them
// Reference: https://github.com/angular/angular/issues/31256#issuecomment-506507021
`**/*-es5.*.js`,
],
// Look for a 20 character hex string in the file names
// Allows to avoid using cache busting for Angular files because Angular already takes care of that!
dontCacheBustURLsMatching: new RegExp(".+.[a-f0-9]{20}..+"),
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, // 4Mb
swSrc: "dist/apps/web/service-worker.js",
swDest: "dist/apps/web/service-worker.js",
};
@samvloeberghs
Copy link

    devtool: 'inline-source-map',
....
test: /\.ts$/,
                loader: 'ts-loader',
                options: {
                    onlyCompileBundledFiles: true,
                    compilerOptions: {
                        sourceMap: true,
                    },
                },

for sourcemaps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment