diff --git a/next.config.js b/next.config.js index 3652030d..08b95806 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,6 @@ const { withPlausibleProxy } = require('next-plausible') const { InjectManifest } = require('workbox-webpack-plugin') -const { generatePrecacheManifest } = require('./sw/build.js') +const CopyPlugin = require('copy-webpack-plugin') const webpack = require('webpack') let isProd = process.env.NODE_ENV === 'production' @@ -215,15 +215,24 @@ module.exports = withPlausibleProxy()({ }, webpack: (config, { isServer, dev, defaultLoaders }) => { if (isServer) { - generatePrecacheManifest() const workboxPlugin = new InjectManifest({ - // ignore the precached manifest which includes the webpack assets - // since they are not useful to us - exclude: [/.*/], - // by default, webpack saves service worker at .next/server/ + include: [/\/(icons|maskable|splash)\//, /\.(webp|ttf|woff|woff2)$/], swDest: '../../public/sw.js', swSrc: './sw/index.js', webpackCompilationPlugins: [ + // we want to precache these static assets so we copy them to include them in the webpack pipeline + // so InjectManifest can inject them into the service worker manifest + new CopyPlugin({ + patterns: [ + { from: 'public/icons', to: '../icons' }, + { from: 'public/maskable', to: '../maskable' }, + { from: 'public/splash', to: '../splash' }, + { from: 'public/waiting.webp', to: '../waiting.webp' }, + { from: 'public/Lightningvolt-xoqm.ttf', to: '../Lightningvolt-xoqm.ttf' }, + { from: 'public/Lightningvolt-xoqm.woff', to: '../Lightningvolt-xoqm.woff' }, + { from: 'public/Lightningvolt-xoqm.woff2', to: '../Lightningvolt-xoqm.woff2' } + ] + }), // this is need to allow the service worker to access these environment variables // from lib/constants.js new webpack.DefinePlugin({ diff --git a/package-lock.json b/package-lock.json index 38843d8f..dc3974bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -114,6 +114,7 @@ }, "devDependencies": { "@next/eslint-plugin-next": "^14.2.15", + "copy-webpack-plugin": "^13.0.1", "eslint": "^9.12.0", "jest": "^29.7.0", "standard": "^17.1.2" @@ -6508,6 +6509,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -8048,6 +8067,63 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/copy-webpack-plugin": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz", + "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/core-js-compat": { "version": "3.38.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", @@ -10234,6 +10310,24 @@ "bser": "2.1.1" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -19677,6 +19771,36 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tldts": { "version": "6.1.51", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.51.tgz", diff --git a/package.json b/package.json index 77bf38d4..5dfcd381 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ }, "devDependencies": { "@next/eslint-plugin-next": "^14.2.15", + "copy-webpack-plugin": "^13.0.1", "eslint": "^9.12.0", "jest": "^29.7.0", "standard": "^17.1.2" diff --git a/sw/build.js b/sw/build.js deleted file mode 100644 index cd0400ae..00000000 --- a/sw/build.js +++ /dev/null @@ -1,61 +0,0 @@ -const { createHash } = require('crypto') -const { readdirSync, readFileSync, statSync, writeFileSync } = require('fs') -const { join } = require('path') - -const getRevision = filePath => createHash('md5').update(readFileSync(filePath)).digest('hex') -const walkSync = dir => readdirSync(dir, { withFileTypes: true }).flatMap(file => - file.isDirectory() ? walkSync(join(dir, file.name)) : join(dir, file.name)) - -function formatBytes (bytes, decimals = 2) { - if (bytes === 0) { - return '0 B' - } - - const k = 1024 - const sizes = ['B', 'KB', 'MB'] - - const i = Math.floor(Math.log(bytes) / Math.log(k)) - const formattedSize = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) - - return `${formattedSize} ${sizes[i]}` -} - -function generatePrecacheManifest () { - const manifest = [] - let size = 0 - - const addToManifest = (filePath, url, s) => { - const revision = getRevision(filePath) - manifest.push({ url, revision }) - size += s - } - - const staticDir = join(__dirname, '../public') - const staticFiles = walkSync(staticDir) - const staticMatch = f => [/\.(webp|jpe?g|ico|png|ttf|woff|woff2)$/].some(m => m.test(f)) - staticFiles.filter(staticMatch).forEach(file => { - const stats = statSync(file) - addToManifest(file, file.slice(staticDir.length), stats.size) - }) - - const pagesDir = join(__dirname, '../pages') - const precacheURLs = ['/offline'] - const pagesFiles = walkSync(pagesDir) - const fileToUrl = f => f.slice(pagesDir.length).replace(/\.js$/, '') - const pageMatch = f => precacheURLs.some(url => fileToUrl(f) === url) - pagesFiles.filter(pageMatch).forEach(file => { - const stats = statSync(file) - // This is not ideal since dependencies of the pages may have changed - // but we would still generate the same revision ... - // The ideal solution would be to create a revision from the file generated by webpack - // in .next/server/pages but the file may not exist yet when we run this script - addToManifest(file, fileToUrl(file), stats.size) - }) - - const output = 'sw/precache-manifest.json' - writeFileSync(output, JSON.stringify(manifest, null, 2)) - - console.log(`Created precache manifest at ${output}. Cache will include ${manifest.length} URLs with a size of ${formatBytes(size)}.`) -} - -module.exports = { generatePrecacheManifest } diff --git a/sw/index.js b/sw/index.js index e49ff436..5b3a75b8 100644 --- a/sw/index.js +++ b/sw/index.js @@ -5,7 +5,6 @@ import { setDefaultHandler } from 'workbox-routing' import { NetworkOnly } from 'workbox-strategies' import { enable } from 'workbox-navigation-preload' -import manifest from './precache-manifest.json' import ServiceWorkerStorage from 'serviceworker-storage' import { CLEAR_NOTIFICATIONS, DELETE_SUBSCRIPTION, STORE_SUBSCRIPTION } from '@/components/serviceworker' @@ -19,11 +18,19 @@ self.__WB_DISABLE_DEV_LOGS = true // https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/ enable() -// ignore precache manifest generated by InjectManifest -// they statically check for the presence of this variable -console.log(self.__WB_MANIFEST) -// precache the manifest we generated ourselves -precacheAndRoute(manifest) +precacheAndRoute(self.__WB_MANIFEST, + { + // this returns url fallbacks, we map CDN urls to the origin because that's how they are cached + // source: https://github.com/GoogleChrome/workbox/blob/e26d8d7507f9412ba029922f3d9920e68710f2cf/packages/workbox-precaching/src/utils/generateURLVariations.ts#L54-L59 + urlManipulation: ({ url }) => { + if (url.hostname === 'a.stacker.news') { + const newUrl = new URL(url.pathname, 'https://stacker.news') + return [newUrl] + } + return [url] + } + } +) // immediately replace existing service workers with this one // (no wait until this one becomes active)