next-pwa
Version:
Next.js with PWA, powered by workbox.
342 lines (313 loc) • 11.9 kB
JavaScript
const path = require('path')
const fs = require('fs')
const globby = require('globby')
const crypto = require('crypto')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const WorkboxPlugin = require('workbox-webpack-plugin')
const defaultCache = require('./cache')
const buildCustomWorker = require('./build-custom-worker')
const buildFallbackWorker = require('./build-fallback-worker')
const getRevision = file => crypto.createHash('md5').update(fs.readFileSync(file)).digest('hex')
module.exports =
(pluginOptions = {}) =>
(nextConfig = {}) =>
Object.assign({}, nextConfig, {
webpack(config, options) {
const {
webpack,
buildId,
dev,
config: { distDir = '.next', pageExtensions = ['tsx', 'ts', 'jsx', 'js', 'mdx'], experimental = {} }
} = options
let basePath = options.config.basePath
if (!basePath) basePath = '/'
// For workbox configurations:
// https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.GenerateSW
const {
disable = false,
register = true,
dest = distDir,
sw = 'sw.js',
cacheStartUrl = true,
dynamicStartUrl = true,
dynamicStartUrlRedirect,
skipWaiting = true,
clientsClaim = true,
cleanupOutdatedCaches = true,
additionalManifestEntries,
ignoreURLParametersMatching = [],
importScripts = [],
publicExcludes = ['!noprecache/**/*'],
buildExcludes = [],
modifyURLPrefix = {},
manifestTransforms = [],
fallbacks = {},
cacheOnFrontEndNav = false,
reloadOnOnline = true,
scope = basePath,
customWorkerDir = 'worker',
subdomainPrefix, // deprecated, use basePath in next.config.js instead
...workbox
} = pluginOptions
if (typeof nextConfig.webpack === 'function') {
config = nextConfig.webpack(config, options)
}
if (disable) {
options.isServer && console.log('> [PWA] PWA support is disabled')
return config
}
if (subdomainPrefix) {
console.error(
'> [PWA] subdomainPrefix is deprecated, use basePath in next.config.js instead: https://nextjs.org/docs/api-reference/next.config.js/basepath'
)
}
console.log(`> [PWA] Compile ${options.isServer ? 'server' : 'client (static)'}`)
let { runtimeCaching = defaultCache } = pluginOptions
const _scope = path.posix.join(scope, '/')
// inject register script to main.js
const _sw = path.posix.join(basePath, sw.startsWith('/') ? sw : `/${sw}`)
config.plugins.push(
new webpack.DefinePlugin({
__PWA_SW__: `'${_sw}'`,
__PWA_SCOPE__: `'${_scope}'`,
__PWA_ENABLE_REGISTER__: `${Boolean(register)}`,
__PWA_START_URL__: dynamicStartUrl ? `'${basePath}'` : undefined,
__PWA_CACHE_ON_FRONT_END_NAV__: `${Boolean(cacheOnFrontEndNav)}`,
__PWA_RELOAD_ON_ONLINE__: `${Boolean(reloadOnOnline)}`
})
)
const registerJs = path.join(__dirname, 'register.js')
const entry = config.entry
config.entry = () =>
entry().then(entries => {
if (entries['main.js'] && !entries['main.js'].includes(registerJs)) {
entries['main.js'].unshift(registerJs)
}
return entries
})
if (!options.isServer) {
const _dest = path.join(options.dir, dest)
const customWorkerScriptName = buildCustomWorker({
id: buildId,
basedir: options.dir,
customWorkerDir,
destdir: _dest,
plugins: config.plugins.filter(plugin => plugin instanceof webpack.DefinePlugin),
minify: !dev
})
if (!!customWorkerScriptName) {
importScripts.unshift(customWorkerScriptName)
}
if (register) {
console.log(`> [PWA] Auto register service worker with: ${path.resolve(registerJs)}`)
} else {
console.log(
`> [PWA] Auto register service worker is disabled, please call following code in componentDidMount callback or useEffect hook`
)
console.log(`> [PWA] window.workbox.register()`)
}
console.log(`> [PWA] Service worker: ${path.join(_dest, sw)}`)
console.log(`> [PWA] url: ${_sw}`)
console.log(`> [PWA] scope: ${_scope}`)
config.plugins.push(
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [
path.join(_dest, 'workbox-*.js'),
path.join(_dest, 'workbox-*.js.map'),
path.join(_dest, sw),
path.join(_dest, `${sw}.map`)
]
})
)
// precache files in public folder
let manifestEntries = additionalManifestEntries
if (!Array.isArray(manifestEntries)) {
manifestEntries = globby
.sync(
[
'**/*',
'!workbox-*.js',
'!workbox-*.js.map',
'!worker-*.js',
'!worker-*.js.map',
'!fallback-*.js',
'!fallback-*.js.map',
`!${sw.replace(/^\/+/, '')}`,
`!${sw.replace(/^\/+/, '')}.map`,
...publicExcludes
],
{
cwd: 'public'
}
)
.map(f => ({
url: path.posix.join(basePath, `/${f}`),
revision: getRevision(`public/${f}`)
}))
}
if (cacheStartUrl) {
if (!dynamicStartUrl) {
manifestEntries.push({
url: basePath,
revision: buildId
})
} else if (typeof dynamicStartUrlRedirect === 'string' && dynamicStartUrlRedirect.length > 0) {
manifestEntries.push({
url: dynamicStartUrlRedirect,
revision: buildId
})
}
}
let _fallbacks = fallbacks
if (_fallbacks) {
const res = buildFallbackWorker({
id: buildId,
fallbacks,
basedir: options.dir,
destdir: _dest,
minify: !dev,
pageExtensions
})
if (res) {
_fallbacks = res.fallbacks
importScripts.unshift(res.name)
res.precaches.forEach(route => {
if (!manifestEntries.find(entry => entry.url.startsWith(route))) {
manifestEntries.push({
url: route,
revision: buildId
})
}
})
} else {
_fallbacks = undefined
}
}
const workboxCommon = {
swDest: path.join(_dest, sw),
additionalManifestEntries: dev ? [] : manifestEntries,
exclude: [
...buildExcludes,
({ asset, compilation }) => {
if (
asset.name.startsWith('server/') ||
asset.name.match(/^(build-manifest\.json|react-loadable-manifest\.json)$/)
) {
return true
}
if (dev && !asset.name.startsWith('static/runtime/')) {
return true
}
if (experimental.modern /* modern */) {
if (asset.name.endsWith('.module.js')) {
return false
}
if (asset.name.endsWith('.js')) {
return true
}
}
return false
}
],
modifyURLPrefix: {
...modifyURLPrefix,
'/_next/../public/': '/'
},
manifestTransforms: [
...manifestTransforms,
async (manifestEntries, compilation) => {
const manifest = manifestEntries.map(m => {
m.url = m.url.replace('/_next//static/image', '/_next/static/image')
m.url = m.url.replace('/_next//static/media', '/_next/static/media')
if (m.revision === null) {
let key = m.url
if (key.startsWith(config.output.publicPath)) {
key = m.url.substring(config.output.publicPath.length)
}
const assset = compilation.assetsInfo.get(key)
m.revision = assset ? assset.contenthash || buildId : buildId
}
m.url = m.url.replace(/\[/g, '%5B').replace(/\]/g, '%5D')
return m
})
return { manifest, warnings: [] }
}
]
}
if (workbox.swSrc) {
const swSrc = path.join(options.dir, workbox.swSrc)
console.log(`> [PWA] Inject manifest in ${swSrc}`)
config.plugins.push(
new WorkboxPlugin.InjectManifest({
...workboxCommon,
...workbox,
swSrc
})
)
} else {
if (dev) {
console.log(
'> [PWA] Build in develop mode, cache and precache are mostly disabled. This means offline support is disabled, but you can continue developing other functions in service worker.'
)
ignoreURLParametersMatching.push(/ts/)
runtimeCaching = [
{
urlPattern: /.*/i,
handler: 'NetworkOnly',
options: {
cacheName: 'dev'
}
}
]
}
if (dynamicStartUrl) {
runtimeCaching.unshift({
urlPattern: basePath,
handler: 'NetworkFirst',
options: {
cacheName: 'start-url',
plugins: [
{
cacheWillUpdate: async ({ request, response, event, state }) => {
if (response && response.type === 'opaqueredirect') {
return new Response(response.body, {
status: 200,
statusText: 'OK',
headers: response.headers
})
}
return response
}
}
]
}
})
}
if (_fallbacks) {
runtimeCaching.forEach(c => {
if (c.options.precacheFallback) return
if (Array.isArray(c.options.plugins) && c.options.plugins.find(p => 'handlerDidError' in p)) return
if (!c.options.plugins) c.options.plugins = []
c.options.plugins.push({
handlerDidError: async ({ request }) => self.fallback(request)
})
})
}
config.plugins.push(
new WorkboxPlugin.GenerateSW({
...workboxCommon,
skipWaiting,
clientsClaim,
cleanupOutdatedCaches,
ignoreURLParametersMatching,
importScripts,
...workbox,
runtimeCaching
})
)
}
}
return config
}
})