diff --git a/lib/icon/module.js b/lib/icon/module.js index 9e2a0817..7439f20e 100755 --- a/lib/icon/module.js +++ b/lib/icon/module.js @@ -3,15 +3,11 @@ const { fork } = require('child_process') const { join } = require('path') const fs = require('fs-extra') const hasha = require('hasha') -const { joinUrl, getRouteParams, sizeName } = require('../utils') +const { joinUrl, getRouteParams, sizeName, emitAsset } = require('../utils') const { version } = require('../../package.json') -module.exports = function (pwa) { - this.nuxt.hook('build:before', () => run.call(this, pwa)) -} - -async function run (pwa) { - const { publicPath } = getRouteParams(this.options) +module.exports = async function (nuxt, pwa, moduleContainer) { + const { publicPath } = getRouteParams(nuxt.options) // Defaults const defaults = { @@ -33,7 +29,7 @@ async function run (pwa) { fileName: 'icon.png', source: null, purpose: ['any', 'maskable'], - cacheDir: join(this.nuxt.options.rootDir, 'node_modules/.cache/pwa/icon'), + cacheDir: join(nuxt.options.rootDir, 'node_modules/.cache/pwa/icon'), targetDir: 'icons', @@ -55,12 +51,12 @@ async function run (pwa) { } // Find source - options.source = await findIcon.call(this, options) + options.source = await findIcon(nuxt, options) // Disable module if no icon specified if (!options.source) { // eslint-disable-next-line no-console - console.warn('[pwa] [icon] Icon not found in ' + path.resolve(this.options.srcDir, this.options.dir.static, options.fileName)) + console.warn('[pwa] [icon] Icon not found in ' + path.resolve(nuxt.options.srcDir, nuxt.options.dir.static, options.fileName)) return } @@ -77,42 +73,42 @@ async function run (pwa) { } // Generate icons - await generateIcons.call(this, options) + await generateIcons(nuxt, options) // Add manifest - addManifest.call(this, options, pwa) + addManifest(nuxt, options, pwa) // Add plugin if (options.plugin) { - addPlugin.call(this, options) + addPlugin(nuxt, options, moduleContainer) } // Emit assets in background - emitAssets.call(this, options) + emitAssets(nuxt, options) } -async function findIcon (options) { +function findIcon (nuxt, options) { const iconSearchPath = [ options.source, - path.resolve(this.options.srcDir, this.options.dir.static, options.fileName), - path.resolve(this.options.srcDir, this.options.dir.assets, options.fileName) + path.resolve(nuxt.options.srcDir, nuxt.options.dir.static, options.fileName), + path.resolve(nuxt.options.srcDir, nuxt.options.dir.assets, options.fileName) ].filter(p => p) for (const source of iconSearchPath) { - if (await fs.exists(source)) { + if (fs.existsSync(source)) { return source } } } -function addPlugin (options) { +function addPlugin (_nuxt, options, moduleContainer) { const icons = {} for (const asset of options._assets) { icons[asset.name] = joinUrl(options.publicPath, asset.target) } if (options.plugin) { - this.addPlugin({ + moduleContainer.addPlugin({ src: path.resolve(__dirname, './plugin.js'), fileName: 'pwa/icons.js', options: { @@ -123,7 +119,7 @@ function addPlugin (options) { } } -async function generateIcons (options) { +async function generateIcons (_nuxt, options) { // Get hash of source image if (!options.iconHash) { options.iconHash = await hasha.fromFile(options.source).then(h => h.substring(0, 6)) @@ -157,7 +153,7 @@ async function generateIcons (options) { } } -function addManifest (options, pwa) { +function addManifest (_nuxt, options, pwa) { if (!pwa.manifest) { pwa.manifest = {} } @@ -172,30 +168,17 @@ function addManifest (options, pwa) { } } -function emitAssets (options) { +function emitAssets (nuxt, options) { // Start resize task in background - const resizePromise = resizeIcons.call(this, options) - - // Register webpack plugin to emit icons - this.extendBuild((config, { isClient }) => { - if (isClient) { - config.plugins.push({ - apply (compiler) { - compiler.hooks.emit.tapPromise('nuxt-pwa-icon', async (compilation) => { - await resizePromise - await Promise.all(options._assets.map(async ({ name, target }) => { - const srcFileName = path.join(options.cacheDir, `${name}.png`) - const src = await fs.readFile(srcFileName) - compilation.assets[target] = { source: () => src, size: () => src.length } - })) - }) - } - }) - } - }) + const resizePromise = resizeIcons(nuxt, options) + + for (const { name, target } of options._assets) { + const srcFileName = path.join(options.cacheDir, `${name}.png`) + emitAsset(nuxt, target, resizePromise.then(() => fs.readFile(srcFileName))) + } } -async function resizeIcons (options) { +async function resizeIcons (_nuxt, options) { const resizeOpts = JSON.stringify({ version, input: options.source, @@ -208,7 +191,7 @@ async function resizeIcons (options) { const integrityFile = path.join(options.cacheDir, '.' + hasha(resizeOpts).substr(0, 8)) - if (await fs.exists(integrityFile)) { + if (fs.existsSync(integrityFile)) { return } await fs.remove(options.cacheDir) diff --git a/lib/manifest/module.js b/lib/manifest/module.js index e6ba3611..741e1fbb 100755 --- a/lib/manifest/module.js +++ b/lib/manifest/module.js @@ -1,13 +1,9 @@ const hash = require('hasha') -const { joinUrl, getRouteParams } = require('../utils') +const { joinUrl, getRouteParams, emitAsset } = require('../utils') -module.exports = function nuxtManifest (pwa) { - this.nuxt.hook('build:before', () => addManifest.call(this, pwa)) -} - -function addManifest (pwa) { - const { routerBase, publicPath } = getRouteParams(this.options) +module.exports = function nuxtManifest (nuxt, pwa) { + const { routerBase, publicPath } = getRouteParams(nuxt.options) // Combine sources const defaults = { @@ -40,23 +36,14 @@ function addManifest (pwa) { .replace('[ext]', options.useWebmanifestExtension ? 'webmanifest' : 'json') // Merge final manifest into options.manifest for other modules - if (!this.options.manifest) { - this.options.manifest = {} + if (!nuxt.options.manifest) { + nuxt.options.manifest = {} } - Object.assign(this.options.manifest, manifest) + Object.assign(nuxt.options.manifest, manifest) // Register webpack plugin to emit manifest const manifestSource = JSON.stringify(manifest, null, 2) - this.options.build.plugins.push({ - apply (compiler) { - compiler.hooks.emit.tap('nuxt-pwa-manifest', (compilation) => { - compilation.assets[manifestFileName] = { - source: () => manifestSource, - size: () => manifestSource.length - } - }) - } - }) + emitAsset(nuxt, manifestFileName, manifestSource) // Add manifest meta const manifestMeta = { rel: 'manifest', href: joinUrl(options.publicPath, manifestFileName), hid: 'manifest' } diff --git a/lib/meta/module.js b/lib/meta/module.js index fa3d02a6..df9eb1a8 100755 --- a/lib/meta/module.js +++ b/lib/meta/module.js @@ -1,29 +1,9 @@ const { join, resolve } = require('path') const { existsSync } = require('fs') const { isUrl } = require('../utils') -const mergeMeta = require('./meta.merge') - -module.exports = function nuxtMeta (pwa) { - const { nuxt } = this - - nuxt.hook('build:before', () => { - generateMeta.call(this, pwa) - }) - - // SPA Support - nuxt.hook('build:done', () => { - SPASupport.call(this, pwa) - }) - if (nuxt.options.target === 'static') { - nuxt.hook('generate:extendRoutes', () => SPASupport.call(this, pwa)) - } else if (!nuxt.options._build) { - SPASupport.call(this, pwa) - } -} - -function generateMeta (pwa) { - const { nuxt } = this +const nuxtMetaRuntime = require('./module.runtime') +module.exports = function nuxtMeta (nuxt, pwa, moduleContainer) { // Defaults const defaults = { name: process.env.npm_package_name, @@ -274,34 +254,23 @@ function generateMeta (pwa) { head.link.push(pwa._manifestMeta) } - this.addPlugin({ + moduleContainer.addPlugin({ src: resolve(__dirname, './plugin.js'), fileName: 'pwa/meta.js', options: {} }) - this.addTemplate({ + moduleContainer.addTemplate({ src: resolve(__dirname, 'meta.json'), fileName: 'pwa/meta.json', options: { head } }) - this.addTemplate({ + moduleContainer.addTemplate({ src: resolve(__dirname, 'meta.merge.js'), fileName: 'pwa/meta.merge.js', options: { head } }) -} -function SPASupport (_pwa) { - const { nuxt } = this - const metaJSON = resolve(nuxt.options.buildDir, 'pwa/meta.json') - if (existsSync(metaJSON)) { - // eslint-disable-next-line no-console - console.debug('[PWA] Loading meta from ' + metaJSON) - mergeMeta(nuxt.options.head, require(metaJSON)) - } else { - // eslint-disable-next-line no-console - console.warn('[PWA] Cannot load meta from ' + metaJSON) - } + nuxtMetaRuntime(nuxt) } diff --git a/lib/meta/module.runtime.js b/lib/meta/module.runtime.js new file mode 100755 index 00000000..dcf92e6c --- /dev/null +++ b/lib/meta/module.runtime.js @@ -0,0 +1,14 @@ +const { resolve } = require('path') +const { existsSync } = require('fs') +const mergeMeta = require('./meta.merge') + +module.exports = function nuxtMetaRuntime (nuxt) { + const spaSupport = () => { + const metaJSON = resolve(nuxt.options.buildDir, 'pwa/meta.json') + if (existsSync(metaJSON)) { + mergeMeta(nuxt.options.head, require(metaJSON)) + } + } + + nuxt.hook('render:resourcesLoaded', () => spaSupport()) +} diff --git a/lib/module.js b/lib/module.js index 5bca853e..01c9318d 100755 --- a/lib/module.js +++ b/lib/module.js @@ -1,14 +1,27 @@ module.exports = async function nuxtPWA (moduleOptions) { + const { nuxt } = this + const moduleContainer = this // TODO: remove dependency when module-utils + + const isBuild = nuxt.options._build + const isGenerate = nuxt.options.target === 'static' && !nuxt.options.dev + const isRuntime = !isBuild && !isGenerate + + if (isRuntime) { + // Load meta.json for SPA renderer + require('./meta/module.runtime')(nuxt) + return + } + const modules = ['icon', 'manifest', 'meta', 'workbox'] // Shared options context - this.options.pwa = { ...(this.options.pwa || {}), ...(moduleOptions || {}) } - const { pwa } = this.options + nuxt.options.pwa = { ...(nuxt.options.pwa || {}), ...(moduleOptions || {}) } + const { pwa } = nuxt.options // Normalize options for (const name of modules) { // Skip disabled modules - if (pwa[name] === false || this.options[name] === false) { + if (pwa[name] === false || nuxt.options[name] === false) { continue } // Ensure options are an object @@ -16,8 +29,8 @@ module.exports = async function nuxtPWA (moduleOptions) { pwa[name] = {} } // Backward compatibility for top-level options - if (this.options[name] !== undefined) { - pwa[name] = { ...this.options[name], ...pwa[name] } + if (nuxt.options[name] !== undefined) { + pwa[name] = { ...nuxt.options[name], ...pwa[name] } } } @@ -26,8 +39,7 @@ module.exports = async function nuxtPWA (moduleOptions) { if (pwa[name] === false) { continue } - const moduleFn = require(`./${name}/module.js`) - await moduleFn.call(this, pwa) + await require(`./${name}/module.js`)(nuxt, pwa, moduleContainer) } } diff --git a/lib/utils/index.js b/lib/utils/index.js index a169a8e0..efad8f4a 100755 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1,11 +1,12 @@ -const path = require('path').posix +const { posix, resolve, dirname } = require('path') +const { writeFile, mkdirp } = require('fs-extra') function isUrl (url) { return url.indexOf('http') === 0 || url.indexOf('//') === 0 } function joinUrl (...args) { - return path.join(...args).replace(':/', '://') + return posix.join(...args).replace(':/', '://') } function normalizeSize (size) { @@ -48,11 +49,32 @@ function startCase (str) { return typeof str === 'string' ? str[0].toUpperCase() + str.substr(1) : str } +async function writeData (path, data) { + path = path.split('?')[0] + await mkdirp(dirname(path)) + await writeFile(path, await data) +} + +function emitAsset (nuxt, fileName, data) { + const emitAsset = async () => { + const buildPath = resolve(nuxt.options.buildDir, 'dist/client', fileName) + await writeData(buildPath, data) + } + + nuxt.hook('build:done', () => emitAsset()) + + const isGenerate = nuxt.options.target === 'static' && !nuxt.options.dev + if (isGenerate) { + nuxt.hook('modules:done', () => emitAsset()) + } +} + module.exports = { isUrl, joinUrl, getRouteParams, startCase, normalizeSize, - sizeName + sizeName, + emitAsset } diff --git a/lib/workbox/module.js b/lib/workbox/module.js index d9b53f1d..4f65c7b3 100755 --- a/lib/workbox/module.js +++ b/lib/workbox/module.js @@ -1,35 +1,35 @@ const { resolve } = require('path') -const { existsSync, writeFile, readFile } = require('fs-extra') - const { getOptions } = require('./options') -const { readJSFiles, pick } = require('./utils') +const { readJSFiles, pick, copyTemplate } = require('./utils') -module.exports = function nuxtWorkbox (pwa) { - const { nuxt } = this - const options = getOptions.call(this, pwa) +module.exports = async function nuxtWorkbox (nuxt, pwa, moduleContainer) { + const options = getOptions(nuxt, pwa) - this.nuxt.hook('build:before', async () => { - // Warning for dev option - if (options.dev) { - // eslint-disable-next-line no-console - console.warn('Workbox is running in development mode') - } + // Warning for dev option + if (options.dev) { + // eslint-disable-next-line no-console + console.warn('Workbox is running in development mode') + } - // Register plugin - if (options.autoRegister) { - this.addPlugin({ - src: resolve(__dirname, `templates/workbox${options.enabled ? '' : '.unregister'}.js`), - ssr: false, - fileName: 'workbox.js', - options: { - ...options - } - }) - } + // Register plugin + if (options.autoRegister) { + moduleContainer.addPlugin({ + src: resolve(__dirname, `templates/workbox${options.enabled ? '' : '.unregister'}.js`), + ssr: false, + fileName: 'workbox.js', + options: { + ...options + } + }) + } - // Add sw.js - if (options.swTemplate) { - const templateOptions = { + // Add sw.js + if (options.swTemplate) { + copyTemplate({ + src: options.swTemplate, + dst: options.swDest, + options: { + dev: nuxt.options.dev, swOptions: pick(options, [ 'workboxURL', 'importScripts', @@ -45,30 +45,9 @@ module.exports = function nuxtWorkbox (pwa) { 'pagesURLPattern', 'offlineStrategy' ]), - routingExtensions: await readJSFiles.call(this, options.routingExtensions), - cachingExtensions: await readJSFiles.call(this, options.cachingExtensions), - workboxExtensions: await readJSFiles.call(this, options.workboxExtensions) - } - - this.addTemplate({ - src: options.swTemplate, - fileName: 'pwa/sw.js', - options: templateOptions - }) - - this.addTemplate({ - src: options.swTemplate, - fileName: options.swDest, - options: templateOptions - }) - } - }) - - if (nuxt.options.target === 'static') { - nuxt.hook('generate:before', async () => { - const swJS = resolve(nuxt.options.buildDir, 'pwa/sw.js') - if (existsSync(swJS)) { - await writeFile(options.swDest, await readFile(swJS)) + routingExtensions: await readJSFiles(nuxt, options.routingExtensions), + cachingExtensions: await readJSFiles(nuxt, options.cachingExtensions), + workboxExtensions: await readJSFiles(nuxt, options.workboxExtensions) } }) } diff --git a/lib/workbox/options.js b/lib/workbox/options.js index f6f01746..858042bc 100644 --- a/lib/workbox/options.js +++ b/lib/workbox/options.js @@ -2,22 +2,22 @@ const path = require('path') const { joinUrl, getRouteParams, startCase } = require('../utils') const defaults = require('./defaults') -function getOptions (pwa) { +function getOptions (nuxt, pwa) { const options = { ...defaults, ...pwa.workbox } // enabled if (options.enabled === undefined) { - options.enabled = !this.options.dev || options.dev /* backward compat */ + options.enabled = !nuxt.options.dev || options.dev /* backward compat */ } // routerBase if (!options.routerBase) { - options.routerBase = this.options.router.base + options.routerBase = nuxt.options.router.base } // publicPath if (!options.publicPath) { - const { publicPath } = getRouteParams(this.options) + const { publicPath } = getRouteParams(nuxt.options) options.publicPath = publicPath } @@ -28,7 +28,7 @@ function getOptions (pwa) { // swDest if (!options.swDest) { - options.swDest = path.resolve(this.options.srcDir, this.options.dir.static || 'static', 'sw.js') + options.swDest = path.resolve(nuxt.options.srcDir, nuxt.options.dir.static || 'static', 'sw.js') } // swURL @@ -46,7 +46,7 @@ function getOptions (pwa) { if (options.cacheAssets) { options.runtimeCaching.push({ urlPattern: options.assetsURLPattern, - handler: this.options.dev ? 'NetworkFirst' : 'CacheFirst' + handler: nuxt.options.dev ? 'NetworkFirst' : 'CacheFirst' }) } @@ -73,7 +73,7 @@ function getOptions (pwa) { // Default cacheId if (options.cacheOptions.cacheId === undefined) { - options.cacheOptions.cacheId = (process.env.npm_package_name || 'nuxt') + (this.options.dev ? '-dev' : '-prod') + options.cacheOptions.cacheId = (process.env.npm_package_name || 'nuxt') + (nuxt.options.dev ? '-dev' : '-prod') } // Normalize runtimeCaching @@ -111,7 +111,7 @@ function getOptions (pwa) { // Workbox Config if (options.config.debug === undefined) { // Debug field is by default set to true for localhost domain which is not always ideal - options.config.debug = options.dev || this.options.dev + options.config.debug = options.dev || nuxt.options.dev } return options diff --git a/lib/workbox/utils.js b/lib/workbox/utils.js index d87ca927..d784d01c 100644 --- a/lib/workbox/utils.js +++ b/lib/workbox/utils.js @@ -1,12 +1,13 @@ -const { readFile, exists } = require('fs-extra') +const { readFile, existsSync, writeFile } = require('fs-extra') +const template = require('lodash.template') -async function readJSFiles (files) { +async function readJSFiles (nuxt, files) { const contents = [] for (const file of Array.isArray(files) ? files : [files]) { - const path = this.nuxt.resolver.resolvePath(file) - if (path && await exists(path)) { - contents.push(await readFile(path, 'utf8')) + const path = nuxt.resolver.resolvePath(file) + if (path && existsSync(path)) { + contents.push(await readFile(path, 'utf8').then(s => s.trim())) } else { throw new Error('Can not read ' + path) } @@ -23,7 +24,13 @@ function pick (obj, props) { return newObj } +async function copyTemplate ({ src, dst, options }) { + const compile = template(await readFile(src, 'utf8')) + await writeFile(dst, compile({ options })) +} + module.exports = { readJSFiles, - pick + pick, + copyTemplate } diff --git a/package.json b/package.json index 6bf9fd6d..4e11075a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "fs-extra": "^9.0.1", "hasha": "^5.2.1", "jimp-compact": "^0.16.1", + "lodash.template": "^4.5.0", "workbox-cdn": "^5.1.3" }, "devDependencies": { diff --git a/types/manifest.d.ts b/types/manifest.d.ts index 3e2c2ec9..1b2174fb 100644 --- a/types/manifest.d.ts +++ b/types/manifest.d.ts @@ -28,7 +28,7 @@ export interface ManifestOptions { */ background_color: string, /** - * Default: `this.options.loading.color` + * Default: undefined */ theme_color: string, /**