Use precache manifest generated by webpack plugin (#2464)

This commit is contained in:
ekzyis 2025-09-04 19:15:52 +02:00 committed by GitHub
parent de463e1f99
commit 8b139f08da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 153 additions and 73 deletions

View File

@ -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({

124
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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 }

View File

@ -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)