UNPKG

7.44 kBJavaScriptView Raw
1const path = require('path')
2const swBuild = require('workbox-build')
3const { readFileSync, writeFileSync, existsSync } = require('fs')
4const hashSum = require('hash-sum')
5const debug = require('debug')('nuxt:pwa')
6const { defaultsDeep, pick } = require('lodash')
7
8const fixUrl = url => url.replace(/\/\//g, '/').replace(':/', '://')
9const isUrl = url => url.indexOf('http') === 0 || url.indexOf('//') === 0
10
11// =============================================
12// workboxModule
13// =============================================
14
15module.exports = function nuxtWorkbox (moduleOptions) {
16 if (this.options.dev) {
17 return
18 }
19
20 let options
21
22 const hook = builder => {
23 debug('Adding workbox')
24 options = getOptions.call(this, moduleOptions)
25 workboxInject.call(this, options)
26 setHeaders.call(this, options)
27 emitAssets.call(this, options)
28 addTemplates.call(this, options)
29 }
30
31 // Get client output path (#83)
32 this.extendBuild((config, { isClient }) => {
33 if (!isClient) {
34 return
35 }
36
37 if (!options.clientBuildDir) {
38 options.clientBuildDir = config.output.path
39 }
40
41 if (!options.globDirectory) {
42 options.globDirectory = options.clientBuildDir
43 }
44 })
45
46 this.nuxt.hook ? this.nuxt.hook('build:before', hook) : this.nuxt.plugin('build', hook)
47}
48
49// =============================================
50// getRouterBase
51// =============================================
52
53function loadScriptExtension (scriptExtension) {
54 if (scriptExtension) {
55 const extPath = this.nuxt.resolveAlias(scriptExtension)
56 if (existsSync(extPath)) {
57 return readFileSync(extPath, 'utf8')
58 }
59 return null
60 }
61}
62
63function getOptions (moduleOptions) {
64 // Router Base
65 const routerBase = this.options.router.base
66 let publicPath = fixUrl(`${routerBase}/${this.options.build.publicPath}`)
67 if (isUrl(this.options.build.publicPath)) { // CDN
68 publicPath = this.options.build.publicPath
69 if (publicPath.indexOf('//') === 0) {
70 publicPath = '/' + publicPath // Escape fixUrl
71 }
72 }
73
74 const defaults = {
75 autoRegister: true,
76 routerBase,
77 publicPath,
78 swSrc: path.resolve(this.options.buildDir, 'sw.template.js'),
79 swDest: path.resolve(this.options.srcDir, this.options.dir.static || 'static', 'sw.js'),
80 directoryIndex: '/',
81 cachingExtensions: null,
82 routingExtensions: null,
83 cacheId: process.env.npm_package_name || 'nuxt',
84 clientsClaim: true,
85 skipWaiting: true,
86 globPatterns: ['**/*.{js,css}'],
87 globDirectory: undefined,
88 modifyUrlPrefix: {
89 '': fixUrl(publicPath)
90 },
91 offline: true,
92 offlinePage: null,
93 offlineAssets: [],
94 _runtimeCaching: [
95 // Cache all _nuxt resources at runtime
96 // They are hashed by webpack so are safe to loaded by cacheFirst handler
97 {
98 urlPattern: fixUrl(publicPath + '/.*'),
99 handler: 'cacheFirst'
100 }
101 ],
102 runtimeCaching: []
103 }
104
105 const options = defaultsDeep({}, this.options.workbox, moduleOptions, defaults)
106
107 // Backward compatibility
108 // https://github.com/nuxt-community/pwa-module/pull/86
109 if (Array.isArray(options.offlinePageAssets)) {
110 options.offlineAssets = options.offlineAssets.concat(options.offlinePageAssets)
111 delete options.offlinePageAssets
112 }
113
114 // Optionally cache other routes for offline
115 if (options.offline && !options.offlinePage) {
116 options._runtimeCaching.push({
117 urlPattern: fixUrl(`${routerBase}/.*`),
118 handler: 'networkFirst'
119 })
120 }
121
122 if (options.cachingExtensions) {
123 options.cachingExtensions = loadScriptExtension.call(this, options.cachingExtensions)
124 }
125
126 if (options.routingExtensions) {
127 options.routingExtensions = loadScriptExtension.call(this, options.routingExtensions)
128 }
129
130 return options
131}
132
133// =============================================
134// addTemplates
135// =============================================
136
137function addTemplates (options) {
138 // Add sw.template.js
139 this.addTemplate({
140 src: path.resolve(__dirname, 'templates/sw.template.js'),
141 fileName: 'sw.template.js',
142 options: {
143 offlinePage: options.offlinePage,
144 offlineAssets: options.offlineAssets,
145 cachingExtensions: options.cachingExtensions,
146 routingExtensions: options.routingExtensions,
147 importScripts: [options.wbDst].concat(options.importScripts || []),
148 runtimeCaching: [].concat(options._runtimeCaching, options.runtimeCaching).map(i => (Object.assign({}, i, {
149 urlPattern: i.urlPattern,
150 handler: i.handler || 'networkFirst',
151 method: i.method || 'GET'
152 }))),
153 clientsClaim: options.clientsClaim,
154 skipWaiting: options.skipWaiting,
155 wbOptions: {
156 cacheId: options.cacheId,
157 directoryIndex: options.directoryIndex,
158 cleanUrls: false
159 }
160 }
161 })
162
163 // Add sw.plugin.js
164 if (options.autoRegister) {
165 const swURL = `${options.routerBase}/${options.swURL || 'sw.js'}`
166 this.addPlugin({
167 src: path.resolve(__dirname, 'templates/sw.plugin.js'),
168 ssr: false,
169 fileName: 'sw.plugin.js',
170 options: {
171 swURL: fixUrl(swURL),
172 swScope: fixUrl(`${options.routerBase}/`)
173 }
174 })
175 }
176}
177
178// =============================================
179// emitAssets
180// =============================================
181
182function emitAssets (options) {
183 const assets = []
184 const emitAsset = (path, name, ext = 'js') => {
185 const source = readFileSync(path)
186 const hash = hashSum(source)
187 const dst = `${name}.${hash}.${ext}`
188 assets.push({ source, dst })
189 return dst
190 }
191
192 // Write assets after build
193 const hook = builder => {
194 assets.forEach(({ source, dst }) => {
195 writeFileSync(path.resolve(options.clientBuildDir, dst), source, 'utf-8')
196 })
197 }
198
199 if (this.nuxt.hook) {
200 this.nuxt.hook('build:done', hook)
201 } else {
202 this.nuxt.plugin('build', builder => {
203 builder.plugin('built', hook)
204 })
205 }
206
207 // workbox.js
208 let wbPath = require.resolve('workbox-sw')
209 if (options.dev) {
210 wbPath = wbPath.replace(/prod/g, 'dev')
211 }
212 options.wbDst = fixUrl(options.publicPath + '/' + emitAsset(wbPath, 'workbox' + (options.dev ? '.dev' : '')))
213}
214
215// =============================================
216// workboxInject
217// =============================================
218
219function workboxInject (options) {
220 const hook = () => {
221 const opts = pick(options, [
222 'swDest', 'swSrc', 'globDirectory', 'globFollow', 'globIgnores', 'globPatterns', 'dontCacheBustUrlsMatching',
223 'globStrict', 'templatedUrls', 'maximumFileSizeToCacheInBytes', 'modifyUrlPrefix', 'manifestTransforms'
224 ])
225 return swBuild.injectManifest(opts)
226 }
227
228 if (this.nuxt.hook) {
229 this.nuxt.hook('build:done', hook)
230 } else {
231 this.nuxt.plugin('build', builder => {
232 builder.plugin('built', hook)
233 })
234 }
235}
236
237// =============================================
238// setHeaders
239// =============================================
240
241function setHeaders (options) {
242 if (options.customHeaders) {
243 return
244 }
245
246 const originalSetHeadersMethod = this.options.render.static.setHeaders
247
248 this.options.render.static.setHeaders = (res, path) => {
249 if (path.match(/sw\.js$/)) {
250 // Prevent caching service worker
251 res.setHeader('Cache-Control', 'no-cache')
252 } else {
253 if (typeof originalSetHeadersMethod !== 'undefined') {
254 originalSetHeadersMethod(res, path)
255 }
256 }
257 }
258}
259
260module.exports.meta = require('./package.json')