const ID = 'vue-cli:pwa-html-plugin' const defaults = { name: 'PWA app', themeColor: '#4DBA87', // The Vue color msTileColor: '#000000', appleMobileWebAppCapable: 'no', appleMobileWebAppStatusBarStyle: 'default', assetsVersion: '', manifestPath: 'manifest.json', manifestOptions: {}, manifestCrossorigin: undefined } const defaultManifest = { icons: [ { 'src': './img/icons/android-chrome-192x192.png', 'sizes': '192x192', 'type': 'image/png' }, { 'src': './img/icons/android-chrome-512x512.png', 'sizes': '512x512', 'type': 'image/png' }, { 'src': './img/icons/android-chrome-maskable-192x192.png', 'sizes': '192x192', 'type': 'image/png', 'purpose': 'maskable' }, { 'src': './img/icons/android-chrome-maskable-512x512.png', 'sizes': '512x512', 'type': 'image/png', 'purpose': 'maskable' } ], start_url: '.', display: 'standalone', background_color: '#000000' } const defaultIconPaths = { favicon32: 'img/icons/favicon-32x32.png', favicon16: 'img/icons/favicon-16x16.png', appleTouchIcon: 'img/icons/apple-touch-icon-152x152.png', maskIcon: 'img/icons/safari-pinned-tab.svg', msTileImage: 'img/icons/msapplication-icon-144x144.png' } module.exports = class HtmlPwaPlugin { constructor (options = {}) { const iconPaths = Object.assign({}, defaultIconPaths, options.iconPaths) delete options.iconPaths this.options = Object.assign({ iconPaths: iconPaths }, defaults, options) } apply (compiler) { compiler.hooks.compilation.tap(ID, compilation => { compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tapAsync(ID, (data, cb) => { // wrap favicon in the base template with IE only comment data.html = data.html.replace(/]+>/, '') cb(null, data) }) compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(ID, (data, cb) => { const { name, themeColor, msTileColor, appleMobileWebAppCapable, appleMobileWebAppStatusBarStyle, assetsVersion, manifestPath, iconPaths, manifestCrossorigin } = this.options const { publicPath } = compiler.options.output const assetsVersionStr = assetsVersion ? `?v=${assetsVersion}` : '' // Favicons if (iconPaths.favicon32 != null) { data.head.push(makeTag('link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: getTagHref(publicPath, iconPaths.favicon32, assetsVersionStr) })) } if (iconPaths.favicon16 != null) { data.head.push(makeTag('link', { rel: 'icon', type: 'image/png', sizes: '16x16', href: getTagHref(publicPath, iconPaths.favicon16, assetsVersionStr) })) } // Add to home screen for Android and modern mobile browsers data.head.push( makeTag('link', manifestCrossorigin ? { rel: 'manifest', href: getTagHref(publicPath, manifestPath, assetsVersionStr), crossorigin: manifestCrossorigin } : { rel: 'manifest', href: getTagHref(publicPath, manifestPath, assetsVersionStr) } ) ) if (themeColor != null) { data.head.push( makeTag('meta', { name: 'theme-color', content: themeColor }) ) } // Add to home screen for Safari on iOS data.head.push( makeTag('meta', { name: 'apple-mobile-web-app-capable', content: appleMobileWebAppCapable }), makeTag('meta', { name: 'apple-mobile-web-app-status-bar-style', content: appleMobileWebAppStatusBarStyle }), makeTag('meta', { name: 'apple-mobile-web-app-title', content: name }) ) if (iconPaths.appleTouchIcon != null) { data.head.push(makeTag('link', { rel: 'apple-touch-icon', href: getTagHref(publicPath, iconPaths.appleTouchIcon, assetsVersionStr) })) } if (iconPaths.maskIcon != null) { data.head.push(makeTag('link', { rel: 'mask-icon', href: getTagHref(publicPath, iconPaths.maskIcon, assetsVersionStr), color: themeColor })) } // Add to home screen for Windows if (iconPaths.msTileImage != null) { data.head.push(makeTag('meta', { name: 'msapplication-TileImage', content: getTagHref(publicPath, iconPaths.msTileImage, assetsVersionStr) })) } if (msTileColor != null) { data.head.push( makeTag('meta', { name: 'msapplication-TileColor', content: msTileColor }) ) } cb(null, data) }) }) if (!isHrefAbsoluteUrl(this.options.manifestPath)) { compiler.hooks.emit.tapAsync(ID, (data, cb) => { const { name, themeColor, manifestPath, manifestOptions } = this.options const publicOptions = { name, short_name: name, theme_color: themeColor } const outputManifest = JSON.stringify( Object.assign(publicOptions, defaultManifest, manifestOptions) ) data.assets[manifestPath] = { source: () => outputManifest, size: () => outputManifest.length } cb(null, data) }) } } } function makeTag (tagName, attributes, closeTag = false) { return { tagName, closeTag, attributes } } function getTagHref (publicPath, href, assetsVersionStr) { let tagHref = `${href}${assetsVersionStr}` if (!isHrefAbsoluteUrl(href)) { tagHref = `${publicPath}${tagHref}` } return tagHref } function isHrefAbsoluteUrl (href) { return /(http(s?)):\/\//gi.test(href) }