|
const fs = require('fs'); |
|
const path = require('path'); |
|
const crypto = require('crypto'); |
|
const readdir = require('recursive-readdir'); |
|
|
|
const SW_NAME = 'sw.js'; |
|
|
|
const fileBlacklist = [ |
|
'sw.js', |
|
'build-manifest.json', |
|
'react-loadable-manifest.json', |
|
]; |
|
|
|
class NextWorkboxPlugin { |
|
constructor(options) { |
|
this.options = options; |
|
} |
|
|
|
getPages() { |
|
try { |
|
const { dir, config, buildId } = this.options; |
|
const dist = path.resolve(dir, config.distDir); |
|
const buildManifestFile = path.resolve(dist, 'build-manifest.json'); |
|
const buildManifest = fs.readFileSync(buildManifestFile, { |
|
encoding: 'utf8', |
|
}); |
|
const buildManifestJson = JSON.parse(buildManifest); |
|
const internals = ['/_app', '/_document', '/_error', '/index']; |
|
return Object.keys(buildManifestJson.pages) |
|
.filter(page => !internals.includes(page)) |
|
.map(page => { |
|
return { |
|
url: page, |
|
revision: buildId, |
|
}; |
|
}); |
|
} catch (err) { |
|
console.error('Error getting pages'); |
|
console.error(err); |
|
return []; |
|
} |
|
} |
|
|
|
getPublicFiles() { |
|
const publicDir = path.resolve(this.options.dir, 'public'); |
|
return readdir(publicDir, fileBlacklist) |
|
.catch(err => { |
|
console.error('Error reading public folder'); |
|
console.error(err); |
|
return []; |
|
}) |
|
.then(files => |
|
Promise.all( |
|
files.map( |
|
file => |
|
new Promise((resolve, reject) => { |
|
const hash = crypto.createHash('sha1'); |
|
const rs = fs.createReadStream(file); |
|
rs.on('error', reject); |
|
rs.on('data', chunk => hash.update(chunk)); |
|
rs.on('end', () => |
|
resolve({ |
|
url: file |
|
.replace(publicDir, '') |
|
.replace(path.sep, '/'), |
|
revision: hash.digest('hex'), |
|
}) |
|
); |
|
}) |
|
) |
|
) |
|
); |
|
} |
|
|
|
getCompilationAssets(compilation) { |
|
return Object.keys(compilation.assets) |
|
.filter(file => !fileBlacklist.includes(file)) |
|
.map(file => file.replace(path.sep, '/')) |
|
.map(file => `/_next/${file}`); |
|
} |
|
|
|
writeServiceWorker(sw) { |
|
const file = path.resolve(this.options.dir, 'public', SW_NAME); |
|
fs.writeFileSync(file, sw); |
|
} |
|
|
|
apply(compiler) { |
|
const { isServer, dev, buildId } = this.options; |
|
if (isServer) { |
|
return; |
|
} |
|
compiler.hooks.afterEmit.tapPromise('NextWorkboxPlugin', compilation => { |
|
if (dev) { |
|
const sw = `console.log('Service worker disabled in dev')`; |
|
this.writeServiceWorker(sw); |
|
return Promise.resolve(); |
|
} |
|
|
|
const urlToString = url => |
|
typeof url === 'string' |
|
? `'${url}'` |
|
: `{ url: '${url.url}', revision: '${url.revision}' }`; |
|
|
|
const flatten = arr => |
|
arr.reduce((acc, val) => acc.concat(val), []); |
|
|
|
const sort = urls => |
|
urls.sort((url1, url2) => { |
|
url1 = typeof url1 === 'string' ? url1 : url1.url; |
|
url2 = typeof url2 === 'string' ? url2 : url2.url; |
|
return url1.localeCompare(url2); |
|
}); |
|
|
|
const createServiceWorker = urls => { |
|
const sw = |
|
'/* global importScripts, workbox */\n' + |
|
"importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');\n\n" + |
|
'workbox.setConfig({\n' + |
|
' debug: true,\n' + |
|
'});\n\n' + |
|
'workbox.precaching.precacheAndRoute([\n' + |
|
urls.map(url => ' ' + urlToString(url)).join(',\n') + |
|
'\n' + |
|
']);\n\n' + |
|
'workbox.googleAnalytics.initialize();\n'; |
|
|
|
this.writeServiceWorker(sw); |
|
}; |
|
|
|
return Promise.all([ |
|
this.getPages(), |
|
this.getPublicFiles(), |
|
this.getCompilationAssets(compilation), |
|
]) |
|
.then(flatten) |
|
.then(sort) |
|
.then(createServiceWorker) |
|
.then(() => { |
|
console.log( |
|
'\x1b[32m%s\x1b[0m', |
|
`\nService worker created at ./public/sw.js (buildId: "${buildId}").\n` |
|
); |
|
}) |
|
.catch(err => { |
|
console.error(err); |
|
}); |
|
}); |
|
} |
|
} |
|
|
|
module.exports = NextWorkboxPlugin; |