diff --git a/gulp/service-worker-dev.js.tmpl b/gulp/service-worker-dev.js.tmpl deleted file mode 100644 index 7fae2a7508..0000000000 --- a/gulp/service-worker-dev.js.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -console.warn('Service worker caching disabled in development'); -self.addEventListener('install', function(event) { - self.skipWaiting(); -}); - -self.addEventListener('activate', function(event) { - self.clients.claim(); -}); diff --git a/gulp/service-worker.js.tmpl b/gulp/service-worker.js.tmpl deleted file mode 100644 index d58a4683fa..0000000000 --- a/gulp/service-worker.js.tmpl +++ /dev/null @@ -1,77 +0,0 @@ -self.addEventListener("push", function(event) { - var data; - if (event.data) { - data = event.data.json(); - event.waitUntil( - self.registration.showNotification(data.title, data) - .then(function(notification){ - firePushCallback({ - type: "received", - tag: data.tag, - data: data.data - }, data.data.jwt); - }) - ); - } -}); -self.addEventListener('notificationclick', function(event) { - var url; - - notificationEventCallback('clicked', event); - - event.notification.close(); - - if (!event.notification.data || !event.notification.data.url) { - return; - } - - url = event.notification.data.url; - - if (!url) return; - - event.waitUntil( - clients.matchAll({ - type: 'window', - }) - .then(function (windowClients) { - var i; - var client; - for (i = 0; i < windowClients.length; i++) { - client = windowClients[i]; - if (client.url === url && 'focus' in client) { - return client.focus(); - } - } - if (clients.openWindow) { - return clients.openWindow(url); - } - return undefined; - }) - ); -}); -self.addEventListener('notificationclose', function(event) { - notificationEventCallback('closed', event); -}); - -function notificationEventCallback(event_type, event){ - firePushCallback({ - action: event.action, - data: event.notification.data, - tag: event.notification.tag, - type: event_type - }, event.notification.data.jwt); -} -function firePushCallback(payload, jwt){ - // Don't send the JWT in the payload.data - delete payload.data.jwt; - // If payload.data is empty then just remove the entire payload.data object. - if (Object.keys(payload.data).length === 0 && payload.data.constructor === Object) { - delete payload.data; - } - fetch('/api/notify.html5/callback', { - method: 'POST', - headers: new Headers({'Content-Type': 'application/json', - 'Authorization': 'Bearer '+jwt}), - body: JSON.stringify(payload) - }); -} diff --git a/gulp/tasks/gen-service-worker.js b/gulp/tasks/gen-service-worker.js deleted file mode 100755 index 2d3c3ba850..0000000000 --- a/gulp/tasks/gen-service-worker.js +++ /dev/null @@ -1,129 +0,0 @@ -/* -Generate a caching service worker for HA - -Will be called as part of build_frontend. - -Creates a caching service worker based on the built content of the repo in -{hass_frontend, hass_frontend_es6}. -Output service worker to {build, build-es6}/service_worker.js - -TODO: - - Use gulp streams - - Fix minifying the stream -*/ -const gulp = require('gulp'); -const file = require('gulp-file'); -const fs = require('fs'); -const path = require('path'); -const swPrecache = require('sw-precache'); -const md5 = require('../common/md5.js'); - -const DEV = !!JSON.parse(process.env.BUILD_DEV || 'true'); - -const dynamicUrlToDependencies = {}; - -const staticFingerprinted = [ - 'mdi.html', - 'translations/en.json', -]; - -const staticFingerprintedEs6 = [ - 'core.js', - 'app.js', -]; - -const staticFingerprintedEs5 = [ - 'compatibility.js', - 'core.js', - 'app.js', -]; - -function processStatic(fn, rootDir, urlDir) { - const parts = path.parse(fn); - const base = parts.dir.length > 0 ? parts.dir + '/' + parts.name : parts.name; - const hash = md5(rootDir + '/' + base + parts.ext); - const url = '/' + urlDir + '/' + base + '-' + hash + parts.ext; - const fpath = rootDir + '/' + base + parts.ext; - dynamicUrlToDependencies[url] = [fpath]; -} - -function generateServiceWorker(es6) { - let genPromise = null; - const baseRootDir = 'hass_frontend'; - const rootDir = es6 ? baseRootDir : 'hass_frontend_es5'; - const panelDir = path.resolve(rootDir, 'panels'); - - if (DEV) { - genPromise = Promise.resolve(fs.readFileSync(path.resolve(__dirname, '../service-worker-dev.js.tmpl'), 'UTF-8')); - } else { - // Create fingerprinted versions of our dependencies. - (es6 ? staticFingerprintedEs6 : staticFingerprintedEs5).forEach(fn => processStatic(fn, rootDir, es6 ? 'frontend_latest' : 'frontend_es5')); - staticFingerprinted.forEach(fn => processStatic(fn, baseRootDir, 'static')); - - panelsFingerprinted.forEach((panel) => { - const fpath = panelDir + '/ha-panel-' + panel + '.html'; - const hash = md5(fpath); - const url = '/' + (es6 ? 'frontend_latest' : 'frontend_es5') + '/panels/ha-panel-' + panel + '-' + hash + '.html'; - dynamicUrlToDependencies[url] = [fpath]; - }); - - const options = { - directoryIndex: '', - dynamicUrlToDependencies: dynamicUrlToDependencies, - staticFileGlobs: [ - baseRootDir + '/icons/favicon.ico', - baseRootDir + '/icons/favicon-192x192.png', - baseRootDir + '/webcomponents-lite.min.js', - baseRootDir + '/fonts/roboto/Roboto-Light.ttf', - baseRootDir + '/fonts/roboto/Roboto-Medium.ttf', - baseRootDir + '/fonts/roboto/Roboto-Regular.ttf', - baseRootDir + '/fonts/roboto/Roboto-Bold.ttf', - baseRootDir + '/images/card_media_player_bg.png', - ], - // Rules are proceeded in order and negative per-domain rules are not supported. - runtimeCaching: [ - { // Cache static content (including translations) on first access. - urlPattern: '/(static|frontend_latest|frontend_es5)/*', - handler: 'cacheFirst', - }, - { // Get api (and home-assistant-polymer in dev mode) from network. - urlPattern: '/(home-assistant-polymer|api)/*', - handler: 'networkOnly', - }, - { // Get manifest and service worker from network. - urlPattern: '/(service_worker.js|service_worker_es5.js|manifest.json)', - handler: 'networkOnly', - }, - { // For rest of the files (on Home Assistant domain only) try both cache and network. - // This includes the root "/" or "/states" response and user files from "/local". - // First access might bring stale data from cache, but a single refresh will bring updated - // file. - urlPattern: '*', - handler: 'fastest', - } - ], - stripPrefix: baseRootDir, - replacePrefix: '/static', - verbose: true, - // Allow our users to refresh to get latest version. - clientsClaim: true, - }; - - genPromise = swPrecache.generate(options); - } - - const swHass = fs.readFileSync(path.resolve(__dirname, '../service-worker.js.tmpl'), 'UTF-8'); - - // Fix this - // if (!DEV) { - // genPromise = genPromise.then( - // swString => uglifyJS.minify(swString, { fromString: true }).code); - // } - - return genPromise.then(swString => swString + '\n' + swHass + '\n' + (es6 ? '//es6' : '//es5')) - .then(swString => file('service_worker.js', swString) - .pipe(gulp.dest(es6 ? 'build' : 'build-es5'))); -} - -gulp.task('gen-service-worker-es5', generateServiceWorker.bind(null, /* es6= */ false)); -gulp.task('gen-service-worker', generateServiceWorker.bind(null, /* es6= */ true)); diff --git a/package.json b/package.json index b865693814..b55fcc0726 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,8 @@ "wct-browser-legacy": "^1.0.0", "web-component-tester": "^6.6.0", "webpack": "^4.8.1", - "webpack-cli": "^2.1.3" + "webpack-cli": "^2.1.3", + "workbox-webpack-plugin": "^3.2.0" }, "resolutions": { "inherits": "2.0.3", diff --git a/script/develop b/script/develop index 76afd860ad..fb28cd4af4 100755 --- a/script/develop +++ b/script/develop @@ -17,7 +17,4 @@ cp -r public/__init__.py $OUTPUT_DIR_ES5/ ./node_modules/.bin/gulp build-translations gen-icons cp src/authorize.html $OUTPUT_DIR -# Manually copy over this file as we don't run the ES5 build -cp node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js.map $OUTPUT_DIR - ./node_modules/.bin/webpack --watch --progress diff --git a/src/entrypoints/service-worker-bootstrap.js b/src/entrypoints/service-worker-bootstrap.js new file mode 100644 index 0000000000..5f8ff19a8d --- /dev/null +++ b/src/entrypoints/service-worker-bootstrap.js @@ -0,0 +1,2 @@ +/* global importScripts */ +importScripts('/static/service-worker-hass.js'); diff --git a/src/entrypoints/service-worker-hass.js b/src/entrypoints/service-worker-hass.js new file mode 100644 index 0000000000..f8edefa27c --- /dev/null +++ b/src/entrypoints/service-worker-hass.js @@ -0,0 +1,124 @@ +/* global workbox clients */ + +function initRouting() { + workbox.precaching.precacheAndRoute(self.__precacheManifest || []); + + // Cache static content (including translations) on first access. + workbox.routing.registerRoute( + new RegExp(`${location.host}/(static|frontend_latest|frontend_es5)/.+`), + workbox.strategies.cacheFirst() + ); + + // Get api from network. + workbox.routing.registerRoute( + new RegExp(`${location.host}/api/.*`), + workbox.strategies.networkOnly() + ); + + // Get manifest and service worker from network. + workbox.routing.registerRoute( + new RegExp(`${location.host}/(service_worker.js|service_worker_es5.js|manifest.json)`), + workbox.strategies.networkOnly() + ); + + // For rest of the files (on Home Assistant domain only) try both cache and network. + // This includes the root "/" or "/states" response and user files from "/local". + // First access might bring stale data from cache, but a single refresh will bring updated + // file. + workbox.routing.registerRoute( + new RegExp(`${location.host}/.*`), + workbox.strategies.staleWhileRevalidate() + ); +} + +function initPushNotifications() { + // HTML5 Push Notifications + function firePushCallback(payload, jwt) { + // Don't send the JWT in the payload.data + delete payload.data.jwt; + // If payload.data is empty then just remove the entire payload.data object. + if (Object.keys(payload.data).length === 0 && payload.data.constructor === Object) { + delete payload.data; + } + fetch('/api/notify.html5/callback', { + method: 'POST', + headers: new Headers({ 'Content-Type': 'application/json', + Authorization: 'Bearer ' + jwt }), + body: JSON.stringify(payload) + }); + } + + function notificationEventCallback(eventType, event) { + firePushCallback({ + action: event.action, + data: event.notification.data, + tag: event.notification.tag, + type: eventType + }, event.notification.data.jwt); + } + + self.addEventListener('push', function (event) { + var data; + if (event.data) { + data = event.data.json(); + event.waitUntil(self.registration.showNotification(data.title, data) + .then(function (/* notification */) { + firePushCallback({ + type: 'received', + tag: data.tag, + data: data.data + }, data.data.jwt); + })); + } + }); + + self.addEventListener('notificationclick', function (event) { + var url; + + notificationEventCallback('clicked', event); + + event.notification.close(); + + if (!event.notification.data || !event.notification.data.url) { + return; + } + + url = event.notification.data.url; + + if (!url) return; + + event.waitUntil(clients.matchAll({ + type: 'window', + }) + .then(function (windowClients) { + var i; + var client; + for (i = 0; i < windowClients.length; i++) { + client = windowClients[i]; + if (client.url === url && 'focus' in client) { + return client.focus(); + } + } + if (clients.openWindow) { + return clients.openWindow(url); + } + return undefined; + })); + }); + + self.addEventListener('notificationclose', function (event) { + notificationEventCallback('closed', event); + }); +} + +workbox.setConfig({ + debug: __DEV__ +}); +workbox.skipWaiting(); +workbox.clientsClaim(); + +if (!__DEV__) { + initRouting(); +} + +initPushNotifications(); diff --git a/src/util/custom-panel/load-custom-panel.js b/src/util/custom-panel/load-custom-panel.js index 2a86fd0450..56fbeca86a 100644 --- a/src/util/custom-panel/load-custom-panel.js +++ b/src/util/custom-panel/load-custom-panel.js @@ -6,8 +6,8 @@ const JS_CACHE = {}; export default function loadCustomPanel(panelConfig) { if ('html_url' in panelConfig) { return Promise.all([ - import('../legacy-support.js'), - import('../../resources/html-import/import-href.js'), + import(/* webpackChunkName: "legacy-support" */ '../legacy-support.js'), + import(/* webpackChunkName: "import-href-polyfill" */ '../../resources/html-import/import-href.js'), // eslint-disable-next-line ]).then(([{}, { importHrefPromise }]) => importHrefPromise(panelConfig.html_url)); } else if (panelConfig.js_url) { diff --git a/webpack.config.js b/webpack.config.js index 7c7e42c44e..444979a266 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,6 +3,8 @@ const path = require('path'); const webpack = require('webpack'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); +const WorkboxPlugin = require('workbox-webpack-plugin'); +const translationMetadata = require('./build-translations/translationMetadata.json'); const version = fs.readFileSync('setup.py', 'utf8').match(/\d{8}[^']*/); if (!version) { @@ -34,9 +36,7 @@ function createConfig(isProdBuild, latestBuild) { ], }; - const copyPluginOpts = [ - { from: 'gulp/service-worker.js.tmpl', to: 'service_worker.js' }, - ]; + const copyPluginOpts = []; const plugins = [ new webpack.DefinePlugin({ @@ -64,10 +64,12 @@ function createConfig(isProdBuild, latestBuild) { copyPluginOpts.push({ from: 'build-translations/output', to: `translations` }); copyPluginOpts.push({ from: 'node_modules/@polymer/font-roboto-local/fonts', to: 'fonts' }); copyPluginOpts.push('node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js') + copyPluginOpts.push('node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js.map') copyPluginOpts.push({ from: 'node_modules/leaflet/dist/leaflet.css', to: `images/leaflet/` }); copyPluginOpts.push({ from: 'node_modules/leaflet/dist/images', to: `images/leaflet/` }); copyPluginOpts.push('node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'); entry['hass-icons'] = './src/entrypoints/hass-icons.js'; + entry['service-worker-hass'] = './src/entrypoints/service-worker-hass.js'; } else { babelOptions.presets = [ ['es2015', { modules: false }] @@ -86,6 +88,46 @@ function createConfig(isProdBuild, latestBuild) { })); } + plugins.push(new WorkboxPlugin.InjectManifest({ + swSrc: './src/entrypoints/service-worker-bootstrap.js', + swDest: 'service_worker.js', + importWorkboxFrom: 'local', + include: [ + /core.js$/, + /app.js$/, + /custom-panel.js$/, + /hass-icons.js$/, + /\.chunk\.js$/, + ], + // Static assets get cached during runtime. But these we want to explicetely cache + // Need to be done using templatedUrls because prefix is /static + globDirectory: '.', + globIgnores: [], + modifyUrlPrefix: { + 'hass_frontend': '/static' + }, + templatedUrls: { + [`/static/translations/${translationMetadata['translations']['en']['fingerprints']['en']}`]: [ + 'build-translations/output/en.json' + ], + '/static/icons/favicon-192x192.png': [ + 'public/icons/favicon-192x192.png' + ], + '/static/fonts/roboto/Roboto-Light.ttf': [ + 'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Light.ttf' + ], + '/static/fonts/roboto/Roboto-Medium.ttf': [ + 'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Medium.ttf' + ], + '/static/fonts/roboto/Roboto-Regular.ttf': [ + 'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Regular.ttf' + ], + '/static/fonts/roboto/Roboto-Bold.ttf': [ + 'node_modules/@polymer/font-roboto-local/fonts/roboto/Roboto-Bold.ttf' + ], + } + })); + const chunkFilename = isProdBuild ? '[chunkhash].chunk.js' : '[name].chunk.js'; diff --git a/yarn.lock b/yarn.lock index 012355a5b2..e9286e50a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4124,6 +4124,10 @@ commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" +common-tags@^1.4.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -5829,6 +5833,14 @@ fs-exists-sync@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" +fs-extra@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-minipass@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" @@ -6301,7 +6313,7 @@ graceful-fs@^3.0.0: dependencies: natives "^1.1.0" -graceful-fs@^4.0.0, graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3: +graceful-fs@^4.0.0, graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -7429,6 +7441,12 @@ isbinaryfile@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" +isemail@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.1.2.tgz#937cf919002077999a73ea8b1951d590e84e01dd" + dependencies: + punycode "2.x.x" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -7469,6 +7487,14 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" +joi@^11.1.1: + version "11.4.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-11.4.0.tgz#f674897537b625e9ac3d0b7e1604c828ad913ccb" + dependencies: + hoek "4.x.x" + isemail "3.x.x" + topo "2.x.x" + js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -7574,6 +7600,12 @@ json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -10023,14 +10055,14 @@ punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" +punycode@2.x.x, punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - q@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" @@ -11890,6 +11922,12 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +topo@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" + dependencies: + hoek "4.x.x" + tough-cookie@~2.3.0, tough-cookie@~2.3.3: version "2.3.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" @@ -12117,6 +12155,10 @@ unique-string@^1.0.0: dependencies: crypto-random-string "^1.0.0" +universalify@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -12718,6 +12760,108 @@ wordwrapjs@^3.0.0: reduce-flatten "^1.0.1" typical "^2.6.1" +workbox-background-sync@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-3.2.0.tgz#08d4f79fb82fb61f72fbd0359c4b616cc75612d4" + dependencies: + workbox-core "^3.2.0" + +workbox-broadcast-cache-update@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-broadcast-cache-update/-/workbox-broadcast-cache-update-3.2.0.tgz#65b4d9b3d4594751ab7ce1fee905c08214118fdc" + dependencies: + workbox-core "^3.2.0" + +workbox-build@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-3.2.0.tgz#01f4a4f6fb5a94dadd3f86d04480c84578fa1125" + dependencies: + babel-runtime "^6.26.0" + common-tags "^1.4.0" + fs-extra "^4.0.2" + glob "^7.1.2" + joi "^11.1.1" + lodash.template "^4.4.0" + pretty-bytes "^4.0.2" + workbox-background-sync "^3.2.0" + workbox-broadcast-cache-update "^3.2.0" + workbox-cache-expiration "^3.2.0" + workbox-cacheable-response "^3.2.0" + workbox-core "^3.2.0" + workbox-google-analytics "^3.2.0" + workbox-precaching "^3.2.0" + workbox-range-requests "^3.2.0" + workbox-routing "^3.2.0" + workbox-strategies "^3.2.0" + workbox-streams "^3.2.0" + workbox-sw "^3.2.0" + +workbox-cache-expiration@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-cache-expiration/-/workbox-cache-expiration-3.2.0.tgz#a585761fd5438e439668afc6f862ac5a0ebca1a8" + dependencies: + workbox-core "^3.2.0" + +workbox-cacheable-response@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-3.2.0.tgz#1d8e3d437d60fb80d971d79545bb27acf1fe7653" + dependencies: + workbox-core "^3.2.0" + +workbox-core@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-3.2.0.tgz#d1bd4209447f5350d8dd6b964c86f054c96ffa0a" + +workbox-google-analytics@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-3.2.0.tgz#1005bc71ae03a8948b687896235dafecb1696c46" + dependencies: + workbox-background-sync "^3.2.0" + workbox-core "^3.2.0" + workbox-routing "^3.2.0" + workbox-strategies "^3.2.0" + +workbox-precaching@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-3.2.0.tgz#36568687a5615d8bd4191b38cf0f489a992d7bbc" + dependencies: + workbox-core "^3.2.0" + +workbox-range-requests@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-3.2.0.tgz#5d6cc3621cef0951fc9c0549053f8e117736d321" + dependencies: + workbox-core "^3.2.0" + +workbox-routing@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-3.2.0.tgz#6aef7622ede2412dd116231f4f9408a6485a4832" + dependencies: + workbox-core "^3.2.0" + +workbox-strategies@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-3.2.0.tgz#6cd5f00739764872b77b4c3766a606e43eb7d246" + dependencies: + workbox-core "^3.2.0" + +workbox-streams@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-3.2.0.tgz#cac0e4f5693b5e13029cbd7e5fec4eb7fcb30d97" + dependencies: + workbox-core "^3.2.0" + +workbox-sw@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-3.2.0.tgz#ccda9adff557ba2233bf1837229144b4a86276cb" + +workbox-webpack-plugin@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-3.2.0.tgz#eab74294e88d86f4356a5dbac98cd803b22142b3" + dependencies: + json-stable-stringify "^1.0.1" + workbox-build "^3.2.0" + worker-farm@^1.5.2: version "1.6.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"