From 66855a57d39b8c18748861a0f87a0d80beb3653d Mon Sep 17 00:00:00 2001 From: Akihiko Odaki Date: Tue, 20 Mar 2018 12:32:39 +0900 Subject: [PATCH] Allow to apply arbitrary plugins when compiling ServiceWorker script --- docs/options.md | 4 + lib/default-options.js | 1 + lib/index.js | 5 + lib/service-worker.js | 5 + src/default-options.js | 1 + src/index.js | 5 + src/service-worker.js | 3 + .../webpack2/appcache/manifest.appcache | 8 + .../webpack2/appcache/manifest.html | 2 + .../sw-plugins/__expected/webpack2/main.js | 76 ++ .../sw-plugins/__expected/webpack2/sw.js | 927 +++++++++++++++++ .../webpack3/appcache/manifest.appcache | 8 + .../webpack3/appcache/manifest.html | 2 + .../sw-plugins/__expected/webpack3/main.js | 73 ++ .../sw-plugins/__expected/webpack3/sw.js | 924 +++++++++++++++++ .../webpack4/appcache/manifest.appcache | 8 + .../webpack4/appcache/manifest.html | 2 + .../sw-plugins/__expected/webpack4/main.js | 79 ++ .../sw-plugins/__expected/webpack4/sw.js | 930 ++++++++++++++++++ tests/legacy/fixtures/sw-plugins/main.js | 0 tests/legacy/fixtures/sw-plugins/sw-entry.js | 1 + .../fixtures/sw-plugins/webpack.config.js | 18 + 22 files changed, 3082 insertions(+) create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack2/appcache/manifest.appcache create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack2/appcache/manifest.html create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack2/main.js create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack2/sw.js create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack3/appcache/manifest.appcache create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack3/appcache/manifest.html create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack3/main.js create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack3/sw.js create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack4/appcache/manifest.appcache create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack4/appcache/manifest.html create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack4/main.js create mode 100644 tests/legacy/fixtures/sw-plugins/__expected/webpack4/sw.js create mode 100644 tests/legacy/fixtures/sw-plugins/main.js create mode 100644 tests/legacy/fixtures/sw-plugins/sw-entry.js create mode 100644 tests/legacy/fixtures/sw-plugins/webpack.config.js diff --git a/docs/options.md b/docs/options.md index c36e0628..a189c90a 100755 --- a/docs/options.md +++ b/docs/options.md @@ -131,6 +131,10 @@ _Example:_ `{ credentials: 'include' }` * **`minify`**: `boolean`. If set to `true` or `false`, the `ServiceWorker`'s output will be minified or not accordingly. If set to something else, the `ServiceWorker` output will be minified **if** you are using `webpack.optimize.UglifyJsPlugin` in your configuration. _Default:_ `null` +* **`plugins`**: `Array`. The plugins which will be applied when compling the `ServiceWorker`'s script. +_Default:_ `[]` +_Example:_ `[new require('webpack').DefinePlugin({ CAT: 'MEOW' })]` + #### `AppCache: Object | null | false` Settings for the `AppCache` cache. Use `null` or `false` to disable `AppCache` generation. diff --git a/lib/default-options.js b/lib/default-options.js index 83a9177e..ddc46486 100644 --- a/lib/default-options.js +++ b/lib/default-options.js @@ -57,6 +57,7 @@ exports['default'] = { events: false, minify: null, forceInstall: false, + plugins: [], updateViaCache: 'imports', diff --git a/lib/index.js b/lib/index.js index 132ad557..476da0c8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -67,6 +67,11 @@ var OfflinePlugin = (function () { AppCache: false }); + if (options.ServiceWorker && options.ServiceWorker.plugins) { + // plugins are class instances and should not be modified. + this.options.ServiceWorker.plugins = options.ServiceWorker.plugins; + } + this.hash = null; this.assets = null; this.hashesMap = null; diff --git a/lib/service-worker.js b/lib/service-worker.js index 164e9f0e..63b2fc3b 100644 --- a/lib/service-worker.js +++ b/lib/service-worker.js @@ -46,6 +46,7 @@ var ServiceWorker = (function () { // Tool specific properties this.entry = options.entry; + this.plugins = options.plugins; this.scope = options.scope ? options.scope + '' : void 0; this.events = !!options.events; this.prefetchRequest = this.validatePrefetch(options.prefetchRequest); @@ -126,6 +127,10 @@ var ServiceWorker = (function () { }); } + this.plugins.forEach(function (plugin) { + return plugin.apply(childCompiler); + }); + // Needed for HMR. offline-plugin doesn't support it, // but added just in case to prevent other errors var compilationFn = function compilationFn(compilation) { diff --git a/src/default-options.js b/src/default-options.js index 9105f6af..f01564ba 100644 --- a/src/default-options.js +++ b/src/default-options.js @@ -44,6 +44,7 @@ export default { events: false, minify: null, forceInstall: false, + plugins: [], updateViaCache: 'imports', diff --git a/src/index.js b/src/index.js index df10d4f3..d28e1717 100644 --- a/src/index.js +++ b/src/index.js @@ -32,6 +32,11 @@ export default class OfflinePlugin { AppCache: false }); + if (options.ServiceWorker && options.ServiceWorker.plugins) { + // plugins are class instances and should not be modified. + this.options.ServiceWorker.plugins = options.ServiceWorker.plugins; + } + this.hash = null; this.assets = null; this.hashesMap = null; diff --git a/src/service-worker.js b/src/service-worker.js index e04700f1..c69649ad 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -26,6 +26,7 @@ export default class ServiceWorker { // Tool specific properties this.entry = options.entry; + this.plugins = options.plugins; this.scope = options.scope ? options.scope + '' : void 0; this.events = !!options.events; this.prefetchRequest = this.validatePrefetch(options.prefetchRequest); @@ -104,6 +105,8 @@ export default class ServiceWorker { }); } + this.plugins.forEach((plugin) => plugin.apply(childCompiler)); + // Needed for HMR. offline-plugin doesn't support it, // but added just in case to prevent other errors const compilationFn = (compilation) => { diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack2/appcache/manifest.appcache b/tests/legacy/fixtures/sw-plugins/__expected/webpack2/appcache/manifest.appcache new file mode 100644 index 00000000..5a7cf4dd --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack2/appcache/manifest.appcache @@ -0,0 +1,8 @@ +CACHE MANIFEST +#ver:da39a3ee5e6b4b0d3255bfef95601890afd80709 + +CACHE: +../external.js + +NETWORK: +* \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack2/appcache/manifest.html b/tests/legacy/fixtures/sw-plugins/__expected/webpack2/appcache/manifest.html new file mode 100644 index 00000000..7e4d9c0b --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack2/appcache/manifest.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack2/main.js b/tests/legacy/fixtures/sw-plugins/__expected/webpack2/main.js new file mode 100644 index 00000000..42acaad3 --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack2/main.js @@ -0,0 +1,76 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + + + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack2/sw.js b/tests/legacy/fixtures/sw-plugins/__expected/webpack2/sw.js new file mode 100644 index 00000000..40109117 --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack2/sw.js @@ -0,0 +1,927 @@ +var __wpo = { + "assets": { + "main": [ + "./external.js" + ], + "additional": [], + "optional": [] + }, + "externals": [ + "./external.js" + ], + "hashesMap": {}, + "strategy": "changed", + "responseStrategy": "cache-first", + "version": "da39a3ee5e6b4b0d3255bfef95601890afd80709", + "name": "webpack-offline", + "relativePaths": true +}; + +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 1); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +'MEOW' + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +(function () { + var waitUntil = ExtendableEvent.prototype.waitUntil; + var respondWith = FetchEvent.prototype.respondWith; + var promisesMap = new WeakMap(); + + ExtendableEvent.prototype.waitUntil = function (promise) { + var extendableEvent = this; + var promises = promisesMap.get(extendableEvent); + + if (promises) { + promises.push(Promise.resolve(promise)); + return; + } + + promises = [Promise.resolve(promise)]; + promisesMap.set(extendableEvent, promises); + + // call original method + return waitUntil.call(extendableEvent, Promise.resolve().then(function processPromises() { + var len = promises.length; + + // wait for all to settle + return Promise.all(promises.map(function (p) { + return p["catch"](function () {}); + })).then(function () { + // have new items been added? If so, wait again + if (promises.length != len) return processPromises(); + // we're done! + promisesMap["delete"](extendableEvent); + // reject if one of the promises rejected + return Promise.all(promises); + }); + })); + }; + + FetchEvent.prototype.respondWith = function (promise) { + this.waitUntil(promise); + return respondWith.call(this, promise); + }; +})();; + 'use strict'; + +if (typeof DEBUG === 'undefined') { + var DEBUG = false; +} + +function WebpackServiceWorker(params, helpers) { + var loaders = helpers.loaders; + var cacheMaps = helpers.cacheMaps; + // navigationPreload: true, { map: (URL) => URL, test: (URL) => boolean } + var navigationPreload = helpers.navigationPreload; + + // (update)strategy: changed, all + var strategy = params.strategy; + // responseStrategy: cache-first, network-first + var responseStrategy = params.responseStrategy; + + var assets = params.assets; + var loadersMap = params.loaders || {}; + + var hashesMap = params.hashesMap; + var externals = params.externals; + + var prefetchRequest = params.prefetchRequest || { + credentials: 'same-origin', + mode: 'cors' + }; + + var CACHE_PREFIX = params.name; + var CACHE_TAG = params.version; + var CACHE_NAME = CACHE_PREFIX + ':' + CACHE_TAG; + + var PRELOAD_CACHE_NAME = CACHE_PREFIX + '$preload'; + var STORED_DATA_KEY = '__offline_webpack__data'; + + mapAssets(); + + var allAssets = [].concat(assets.main, assets.additional, assets.optional); + + // Deprecated { + var navigateFallbackURL = params.navigateFallbackURL; + var navigateFallbackForRedirects = params.navigateFallbackForRedirects; + // } + + self.addEventListener('install', function (event) { + console.log('[SW]:', 'Install event'); + + var installing = undefined; + + if (strategy === 'changed') { + installing = cacheChanged('main'); + } else { + installing = cacheAssets('main'); + } + + event.waitUntil(installing); + }); + + self.addEventListener('activate', function (event) { + console.log('[SW]:', 'Activate event'); + + var activation = cacheAdditional(); + + // Delete all assets which name starts with CACHE_PREFIX and + // is not current cache (CACHE_NAME) + activation = activation.then(storeCacheData); + activation = activation.then(deleteObsolete); + activation = activation.then(function () { + if (self.clients && self.clients.claim) { + return self.clients.claim(); + } + }); + + if (navigationPreload && self.registration.navigationPreload) { + activation = Promise.all([activation, self.registration.navigationPreload.enable()]); + } + + event.waitUntil(activation); + }); + + function cacheAdditional() { + if (!assets.additional.length) { + return Promise.resolve(); + } + + if (DEBUG) { + console.log('[SW]:', 'Caching additional'); + } + + var operation = undefined; + + if (strategy === 'changed') { + operation = cacheChanged('additional'); + } else { + operation = cacheAssets('additional'); + } + + // Ignore fail of `additional` cache section + return operation['catch'](function (e) { + console.error('[SW]:', 'Cache section `additional` failed to load'); + }); + } + + function cacheAssets(section) { + var batch = assets[section]; + + return caches.open(CACHE_NAME).then(function (cache) { + return addAllNormalized(cache, batch, { + bust: params.version, + request: prefetchRequest + }); + }).then(function () { + logGroup('Cached assets: ' + section, batch); + })['catch'](function (e) { + console.error(e); + throw e; + }); + } + + function cacheChanged(section) { + return getLastCache().then(function (args) { + if (!args) { + return cacheAssets(section); + } + + var lastCache = args[0]; + var lastKeys = args[1]; + var lastData = args[2]; + + var lastMap = lastData.hashmap; + var lastVersion = lastData.version; + + if (!lastData.hashmap || lastVersion === params.version) { + return cacheAssets(section); + } + + var lastHashedAssets = Object.keys(lastMap).map(function (hash) { + return lastMap[hash]; + }); + + var lastUrls = lastKeys.map(function (req) { + var url = new URL(req.url); + url.search = ''; + url.hash = ''; + + return url.toString(); + }); + + var sectionAssets = assets[section]; + var moved = []; + var changed = sectionAssets.filter(function (url) { + if (lastUrls.indexOf(url) === -1 || lastHashedAssets.indexOf(url) === -1) { + return true; + } + + return false; + }); + + Object.keys(hashesMap).forEach(function (hash) { + var asset = hashesMap[hash]; + + // Return if not in sectionAssets or in changed or moved array + if (sectionAssets.indexOf(asset) === -1 || changed.indexOf(asset) !== -1 || moved.indexOf(asset) !== -1) return; + + var lastAsset = lastMap[hash]; + + if (lastAsset && lastUrls.indexOf(lastAsset) !== -1) { + moved.push([lastAsset, asset]); + } else { + changed.push(asset); + } + }); + + logGroup('Changed assets: ' + section, changed); + logGroup('Moved assets: ' + section, moved); + + var movedResponses = Promise.all(moved.map(function (pair) { + return lastCache.match(pair[0]).then(function (response) { + return [pair[1], response]; + }); + })); + + return caches.open(CACHE_NAME).then(function (cache) { + var move = movedResponses.then(function (responses) { + return Promise.all(responses.map(function (pair) { + return cache.put(pair[0], pair[1]); + })); + }); + + return Promise.all([move, addAllNormalized(cache, changed, { + bust: params.version, + request: prefetchRequest + })]); + }); + }); + } + + function deleteObsolete() { + return caches.keys().then(function (keys) { + var all = keys.map(function (key) { + if (key.indexOf(CACHE_PREFIX) !== 0 || key.indexOf(CACHE_NAME) === 0) return; + + console.log('[SW]:', 'Delete cache:', key); + return caches['delete'](key); + }); + + return Promise.all(all); + }); + } + + function getLastCache() { + return caches.keys().then(function (keys) { + var index = keys.length; + var key = undefined; + + while (index--) { + key = keys[index]; + + if (key.indexOf(CACHE_PREFIX) === 0) { + break; + } + } + + if (!key) return; + + var cache = undefined; + + return caches.open(key).then(function (_cache) { + cache = _cache; + return _cache.match(new URL(STORED_DATA_KEY, location).toString()); + }).then(function (response) { + if (!response) return; + + return Promise.all([cache, cache.keys(), response.json()]); + }); + }); + } + + function storeCacheData() { + return caches.open(CACHE_NAME).then(function (cache) { + var data = new Response(JSON.stringify({ + version: params.version, + hashmap: hashesMap + })); + + return cache.put(new URL(STORED_DATA_KEY, location).toString(), data); + }); + } + + self.addEventListener('fetch', function (event) { + var url = new URL(event.request.url); + url.hash = ''; + + var urlString = url.toString(); + + // Not external, so search part of the URL should be stripped, + // if it's external URL, the search part should be kept + if (externals.indexOf(urlString) === -1) { + url.search = ''; + urlString = url.toString(); + } + + // Handle only GET requests + var isGET = event.request.method === 'GET'; + var assetMatches = allAssets.indexOf(urlString) !== -1; + var cacheUrl = urlString; + + if (!assetMatches) { + var cacheRewrite = matchCacheMap(event.request); + + if (cacheRewrite) { + cacheUrl = cacheRewrite; + assetMatches = true; + } + } + + if (!assetMatches && isGET) { + // If isn't a cached asset and is a navigation request, + // perform network request and fallback to navigateFallbackURL if available. + // + // Requesting with fetchWithPreload(). + // Preload is used only if navigationPreload is enabled and + // navigationPreload mapping is not used. + if (navigateFallbackURL && isNavigateRequest(event.request)) { + event.respondWith(handleNavigateFallback(fetchWithPreload(event))); + + return; + } + + if (navigationPreload === true) { + event.respondWith(fetchWithPreload(event)); + return; + } + + // Something else, positive, but not `true` + if (navigationPreload) { + var preloadedResponse = retrivePreloadedResponse(event); + + if (preloadedResponse) { + event.respondWith(preloadedResponse); + return; + } + } + + // Logic exists here if no cache match, or no preload + return; + } + + if (!assetMatches || !isGET) { + // Fix for https://twitter.com/wanderview/status/696819243262873600 + if (url.origin !== location.origin && navigator.userAgent.indexOf('Firefox/44.') !== -1) { + event.respondWith(fetch(event.request)); + } + + // Logic exists here if no cache match + return; + } + + // Cache handling/storing/fetching starts here + + var resource = undefined; + + if (responseStrategy === 'network-first') { + resource = networkFirstResponse(event, urlString, cacheUrl); + } + // 'cache-first' otherwise + // (responseStrategy has been validated before) + else { + resource = cacheFirstResponse(event, urlString, cacheUrl); + } + + if (navigateFallbackURL && isNavigateRequest(event.request)) { + resource = handleNavigateFallback(resource); + } + + event.respondWith(resource); + }); + + self.addEventListener('message', function (e) { + var data = e.data; + if (!data) return; + + switch (data.action) { + case 'skipWaiting': + { + if (self.skipWaiting) self.skipWaiting(); + }break; + } + }); + + function cacheFirstResponse(event, urlString, cacheUrl) { + handleNavigationPreload(event); + + return cachesMatch(cacheUrl, CACHE_NAME).then(function (response) { + if (response) { + if (DEBUG) { + console.log('[SW]:', 'URL [' + cacheUrl + '](' + urlString + ') from cache'); + } + + return response; + } + + // Load and cache known assets + var fetching = fetch(event.request).then(function (response) { + if (!response.ok) { + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] wrong response: [' + response.status + '] ' + response.type); + } + + return response; + } + + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] from network'); + } + + if (cacheUrl === urlString) { + (function () { + var responseClone = response.clone(); + var storing = caches.open(CACHE_NAME).then(function (cache) { + return cache.put(urlString, responseClone); + }).then(function () { + console.log('[SW]:', 'Cache asset: ' + urlString); + }); + + event.waitUntil(storing); + })(); + } + + return response; + }); + + return fetching; + }); + } + + function networkFirstResponse(event, urlString, cacheUrl) { + return fetchWithPreload(event).then(function (response) { + if (response.ok) { + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] from network'); + } + + return response; + } + + // Throw to reach the code in the catch below + throw new Error('Response is not ok'); + }) + // This needs to be in a catch() and not just in the then() above + // cause if your network is down, the fetch() will throw + ['catch'](function () { + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] from cache if possible'); + } + + return cachesMatch(cacheUrl, CACHE_NAME); + }); + } + + function handleNavigationPreload(event) { + if (navigationPreload && typeof navigationPreload.map === 'function' && + // Use request.mode === 'navigate' instead of isNavigateRequest + // because everything what supports navigationPreload supports + // 'navigate' request.mode + event.preloadResponse && event.request.mode === 'navigate') { + var mapped = navigationPreload.map(new URL(event.request.url), event.request); + + if (mapped) { + storePreloadedResponse(mapped, event); + } + } + } + + // Temporary in-memory store for faster access + var navigationPreloadStore = new Map(); + + function storePreloadedResponse(_url, event) { + var url = new URL(_url, location); + var preloadResponsePromise = event.preloadResponse; + + navigationPreloadStore.set(preloadResponsePromise, { + url: url, + response: preloadResponsePromise + }); + + var isSamePreload = function isSamePreload() { + return navigationPreloadStore.has(preloadResponsePromise); + }; + + var storing = preloadResponsePromise.then(function (res) { + // Return if preload isn't enabled or hasn't happened + if (!res) return; + + // If navigationPreloadStore already consumed + // or navigationPreloadStore already contains another preload, + // then do not store anything and return + if (!isSamePreload()) { + return; + } + + var clone = res.clone(); + + // Storing the preload response for later consume (hasn't yet been consumed) + return caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + if (!isSamePreload()) return; + + return cache.put(url, clone).then(function () { + if (!isSamePreload()) { + return caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + return cache['delete'](url); + }); + } + }); + }); + }); + + event.waitUntil(storing); + } + + function retriveInMemoryPreloadedResponse(url) { + if (!navigationPreloadStore) { + return; + } + + var foundResponse = undefined; + var foundKey = undefined; + + navigationPreloadStore.forEach(function (store, key) { + if (store.url.href === url.href) { + foundResponse = store.response; + foundKey = key; + } + }); + + if (foundResponse) { + navigationPreloadStore['delete'](foundKey); + return foundResponse; + } + } + + function retrivePreloadedResponse(event) { + var url = new URL(event.request.url); + + if (self.registration.navigationPreload && navigationPreload && navigationPreload.test && navigationPreload.test(url, event.request)) {} else { + return; + } + + var fromMemory = retriveInMemoryPreloadedResponse(url); + var request = event.request; + + if (fromMemory) { + event.waitUntil(caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + return cache['delete'](request); + })); + + return fromMemory; + } + + return cachesMatch(request, PRELOAD_CACHE_NAME).then(function (response) { + if (response) { + event.waitUntil(caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + return cache['delete'](request); + })); + } + + return response || fetch(event.request); + }); + } + + function handleNavigateFallback(fetching) { + return fetching['catch'](function () {}).then(function (response) { + var isOk = response && response.ok; + var isRedirect = response && response.type === 'opaqueredirect'; + + if (isOk || isRedirect && !navigateFallbackForRedirects) { + return response; + } + + if (DEBUG) { + console.log('[SW]:', 'Loading navigation fallback [' + navigateFallbackURL + '] from cache'); + } + + return cachesMatch(navigateFallbackURL, CACHE_NAME); + }); + } + + function mapAssets() { + Object.keys(assets).forEach(function (key) { + assets[key] = assets[key].map(function (path) { + var url = new URL(path, location); + + url.hash = ''; + + if (externals.indexOf(path) === -1) { + url.search = ''; + } + + return url.toString(); + }); + }); + + Object.keys(loadersMap).forEach(function (key) { + loadersMap[key] = loadersMap[key].map(function (path) { + var url = new URL(path, location); + + url.hash = ''; + + if (externals.indexOf(path) === -1) { + url.search = ''; + } + + return url.toString(); + }); + }); + + hashesMap = Object.keys(hashesMap).reduce(function (result, hash) { + var url = new URL(hashesMap[hash], location); + url.search = ''; + url.hash = ''; + + result[hash] = url.toString(); + return result; + }, {}); + + externals = externals.map(function (path) { + var url = new URL(path, location); + url.hash = ''; + + return url.toString(); + }); + } + + function addAllNormalized(cache, requests, options) { + var allowLoaders = options.allowLoaders !== false; + var bustValue = options && options.bust; + var requestInit = options.request || { + credentials: 'omit', + mode: 'cors' + }; + + return Promise.all(requests.map(function (request) { + if (bustValue) { + request = applyCacheBust(request, bustValue); + } + + return fetch(request, requestInit).then(fixRedirectedResponse); + })).then(function (responses) { + if (responses.some(function (response) { + return !response.ok; + })) { + return Promise.reject(new Error('Wrong response status')); + } + + var extracted = []; + var addAll = responses.map(function (response, i) { + if (allowLoaders) { + extracted.push(extractAssetsWithLoaders(requests[i], response)); + } + + return cache.put(requests[i], response); + }); + + if (extracted.length) { + (function () { + var newOptions = copyObject(options); + newOptions.allowLoaders = false; + + var waitAll = addAll; + + addAll = Promise.all(extracted).then(function (all) { + var extractedRequests = [].concat.apply([], all); + + if (requests.length) { + waitAll = waitAll.concat(addAllNormalized(cache, extractedRequests, newOptions)); + } + + return Promise.all(waitAll); + }); + })(); + } else { + addAll = Promise.all(addAll); + } + + return addAll; + }); + } + + function extractAssetsWithLoaders(request, response) { + var all = Object.keys(loadersMap).map(function (key) { + var loader = loadersMap[key]; + + if (loader.indexOf(request) !== -1 && loaders[key]) { + return loaders[key](response.clone()); + } + }).filter(function (a) { + return !!a; + }); + + return Promise.all(all).then(function (all) { + return [].concat.apply([], all); + }); + } + + function matchCacheMap(request) { + var urlString = request.url; + var url = new URL(urlString); + + var requestType = undefined; + + if (isNavigateRequest(request)) { + requestType = 'navigate'; + } else if (url.origin === location.origin) { + requestType = 'same-origin'; + } else { + requestType = 'cross-origin'; + } + + for (var i = 0; i < cacheMaps.length; i++) { + var map = cacheMaps[i]; + + if (!map) continue; + if (map.requestTypes && map.requestTypes.indexOf(requestType) === -1) { + continue; + } + + var newString = undefined; + + if (typeof map.match === 'function') { + newString = map.match(url, request); + } else { + newString = urlString.replace(map.match, map.to); + } + + if (newString && newString !== urlString) { + return newString; + } + } + } + + function fetchWithPreload(event) { + if (!event.preloadResponse || navigationPreload !== true) { + return fetch(event.request); + } + + return event.preloadResponse.then(function (response) { + return response || fetch(event.request); + }); + } +} + +function cachesMatch(request, cacheName) { + return caches.match(request, { + cacheName: cacheName + }).then(function (response) { + if (isNotRedirectedResponse()) { + return response; + } + + // Fix already cached redirected responses + return fixRedirectedResponse(response).then(function (fixedResponse) { + return caches.open(cacheName).then(function (cache) { + return cache.put(request, fixedResponse); + }).then(function () { + return fixedResponse; + }); + }); + }) + // Return void if error happened (cache not found) + ['catch'](function () {}); +} + +function applyCacheBust(asset, key) { + var hasQuery = asset.indexOf('?') !== -1; + return asset + (hasQuery ? '&' : '?') + '__uncache=' + encodeURIComponent(key); +} + +function isNavigateRequest(request) { + return request.mode === 'navigate' || request.headers.get('Upgrade-Insecure-Requests') || (request.headers.get('Accept') || '').indexOf('text/html') !== -1; +} + +function isNotRedirectedResponse(response) { + return !response || !response.redirected || !response.ok || response.type === 'opaqueredirect'; +} + +// Based on https://github.com/GoogleChrome/sw-precache/pull/241/files#diff-3ee9060dc7a312c6a822cac63a8c630bR85 +function fixRedirectedResponse(response) { + if (isNotRedirectedResponse(response)) { + return Promise.resolve(response); + } + + var body = 'body' in response ? Promise.resolve(response.body) : response.blob(); + + return body.then(function (data) { + return new Response(data, { + headers: response.headers, + status: response.status + }); + }); +} + +function copyObject(original) { + return Object.keys(original).reduce(function (result, key) { + result[key] = original[key]; + return result; + }, {}); +} + +function logGroup(title, assets) { + console.groupCollapsed('[SW]:', title); + + assets.forEach(function (asset) { + console.log('Asset:', asset); + }); + + console.groupEnd(); +} + WebpackServiceWorker(__wpo, { +loaders: {}, +cacheMaps: [], +navigationPreload: false, +}); + module.exports = __webpack_require__(0) + + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack3/appcache/manifest.appcache b/tests/legacy/fixtures/sw-plugins/__expected/webpack3/appcache/manifest.appcache new file mode 100644 index 00000000..5a7cf4dd --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack3/appcache/manifest.appcache @@ -0,0 +1,8 @@ +CACHE MANIFEST +#ver:da39a3ee5e6b4b0d3255bfef95601890afd80709 + +CACHE: +../external.js + +NETWORK: +* \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack3/appcache/manifest.html b/tests/legacy/fixtures/sw-plugins/__expected/webpack3/appcache/manifest.html new file mode 100644 index 00000000..7e4d9c0b --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack3/appcache/manifest.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack3/main.js b/tests/legacy/fixtures/sw-plugins/__expected/webpack3/main.js new file mode 100644 index 00000000..30d65ea5 --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack3/main.js @@ -0,0 +1,73 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + + + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack3/sw.js b/tests/legacy/fixtures/sw-plugins/__expected/webpack3/sw.js new file mode 100644 index 00000000..304cb375 --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack3/sw.js @@ -0,0 +1,924 @@ +var __wpo = { + "assets": { + "main": [ + "./external.js" + ], + "additional": [], + "optional": [] + }, + "externals": [ + "./external.js" + ], + "hashesMap": {}, + "strategy": "changed", + "responseStrategy": "cache-first", + "version": "da39a3ee5e6b4b0d3255bfef95601890afd80709", + "name": "webpack-offline", + "relativePaths": true +}; + +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +(function () { + var waitUntil = ExtendableEvent.prototype.waitUntil; + var respondWith = FetchEvent.prototype.respondWith; + var promisesMap = new WeakMap(); + + ExtendableEvent.prototype.waitUntil = function (promise) { + var extendableEvent = this; + var promises = promisesMap.get(extendableEvent); + + if (promises) { + promises.push(Promise.resolve(promise)); + return; + } + + promises = [Promise.resolve(promise)]; + promisesMap.set(extendableEvent, promises); + + // call original method + return waitUntil.call(extendableEvent, Promise.resolve().then(function processPromises() { + var len = promises.length; + + // wait for all to settle + return Promise.all(promises.map(function (p) { + return p["catch"](function () {}); + })).then(function () { + // have new items been added? If so, wait again + if (promises.length != len) return processPromises(); + // we're done! + promisesMap["delete"](extendableEvent); + // reject if one of the promises rejected + return Promise.all(promises); + }); + })); + }; + + FetchEvent.prototype.respondWith = function (promise) { + this.waitUntil(promise); + return respondWith.call(this, promise); + }; +})();; + 'use strict'; + +if (typeof DEBUG === 'undefined') { + var DEBUG = false; +} + +function WebpackServiceWorker(params, helpers) { + var loaders = helpers.loaders; + var cacheMaps = helpers.cacheMaps; + // navigationPreload: true, { map: (URL) => URL, test: (URL) => boolean } + var navigationPreload = helpers.navigationPreload; + + // (update)strategy: changed, all + var strategy = params.strategy; + // responseStrategy: cache-first, network-first + var responseStrategy = params.responseStrategy; + + var assets = params.assets; + var loadersMap = params.loaders || {}; + + var hashesMap = params.hashesMap; + var externals = params.externals; + + var prefetchRequest = params.prefetchRequest || { + credentials: 'same-origin', + mode: 'cors' + }; + + var CACHE_PREFIX = params.name; + var CACHE_TAG = params.version; + var CACHE_NAME = CACHE_PREFIX + ':' + CACHE_TAG; + + var PRELOAD_CACHE_NAME = CACHE_PREFIX + '$preload'; + var STORED_DATA_KEY = '__offline_webpack__data'; + + mapAssets(); + + var allAssets = [].concat(assets.main, assets.additional, assets.optional); + + // Deprecated { + var navigateFallbackURL = params.navigateFallbackURL; + var navigateFallbackForRedirects = params.navigateFallbackForRedirects; + // } + + self.addEventListener('install', function (event) { + console.log('[SW]:', 'Install event'); + + var installing = undefined; + + if (strategy === 'changed') { + installing = cacheChanged('main'); + } else { + installing = cacheAssets('main'); + } + + event.waitUntil(installing); + }); + + self.addEventListener('activate', function (event) { + console.log('[SW]:', 'Activate event'); + + var activation = cacheAdditional(); + + // Delete all assets which name starts with CACHE_PREFIX and + // is not current cache (CACHE_NAME) + activation = activation.then(storeCacheData); + activation = activation.then(deleteObsolete); + activation = activation.then(function () { + if (self.clients && self.clients.claim) { + return self.clients.claim(); + } + }); + + if (navigationPreload && self.registration.navigationPreload) { + activation = Promise.all([activation, self.registration.navigationPreload.enable()]); + } + + event.waitUntil(activation); + }); + + function cacheAdditional() { + if (!assets.additional.length) { + return Promise.resolve(); + } + + if (DEBUG) { + console.log('[SW]:', 'Caching additional'); + } + + var operation = undefined; + + if (strategy === 'changed') { + operation = cacheChanged('additional'); + } else { + operation = cacheAssets('additional'); + } + + // Ignore fail of `additional` cache section + return operation['catch'](function (e) { + console.error('[SW]:', 'Cache section `additional` failed to load'); + }); + } + + function cacheAssets(section) { + var batch = assets[section]; + + return caches.open(CACHE_NAME).then(function (cache) { + return addAllNormalized(cache, batch, { + bust: params.version, + request: prefetchRequest + }); + }).then(function () { + logGroup('Cached assets: ' + section, batch); + })['catch'](function (e) { + console.error(e); + throw e; + }); + } + + function cacheChanged(section) { + return getLastCache().then(function (args) { + if (!args) { + return cacheAssets(section); + } + + var lastCache = args[0]; + var lastKeys = args[1]; + var lastData = args[2]; + + var lastMap = lastData.hashmap; + var lastVersion = lastData.version; + + if (!lastData.hashmap || lastVersion === params.version) { + return cacheAssets(section); + } + + var lastHashedAssets = Object.keys(lastMap).map(function (hash) { + return lastMap[hash]; + }); + + var lastUrls = lastKeys.map(function (req) { + var url = new URL(req.url); + url.search = ''; + url.hash = ''; + + return url.toString(); + }); + + var sectionAssets = assets[section]; + var moved = []; + var changed = sectionAssets.filter(function (url) { + if (lastUrls.indexOf(url) === -1 || lastHashedAssets.indexOf(url) === -1) { + return true; + } + + return false; + }); + + Object.keys(hashesMap).forEach(function (hash) { + var asset = hashesMap[hash]; + + // Return if not in sectionAssets or in changed or moved array + if (sectionAssets.indexOf(asset) === -1 || changed.indexOf(asset) !== -1 || moved.indexOf(asset) !== -1) return; + + var lastAsset = lastMap[hash]; + + if (lastAsset && lastUrls.indexOf(lastAsset) !== -1) { + moved.push([lastAsset, asset]); + } else { + changed.push(asset); + } + }); + + logGroup('Changed assets: ' + section, changed); + logGroup('Moved assets: ' + section, moved); + + var movedResponses = Promise.all(moved.map(function (pair) { + return lastCache.match(pair[0]).then(function (response) { + return [pair[1], response]; + }); + })); + + return caches.open(CACHE_NAME).then(function (cache) { + var move = movedResponses.then(function (responses) { + return Promise.all(responses.map(function (pair) { + return cache.put(pair[0], pair[1]); + })); + }); + + return Promise.all([move, addAllNormalized(cache, changed, { + bust: params.version, + request: prefetchRequest + })]); + }); + }); + } + + function deleteObsolete() { + return caches.keys().then(function (keys) { + var all = keys.map(function (key) { + if (key.indexOf(CACHE_PREFIX) !== 0 || key.indexOf(CACHE_NAME) === 0) return; + + console.log('[SW]:', 'Delete cache:', key); + return caches['delete'](key); + }); + + return Promise.all(all); + }); + } + + function getLastCache() { + return caches.keys().then(function (keys) { + var index = keys.length; + var key = undefined; + + while (index--) { + key = keys[index]; + + if (key.indexOf(CACHE_PREFIX) === 0) { + break; + } + } + + if (!key) return; + + var cache = undefined; + + return caches.open(key).then(function (_cache) { + cache = _cache; + return _cache.match(new URL(STORED_DATA_KEY, location).toString()); + }).then(function (response) { + if (!response) return; + + return Promise.all([cache, cache.keys(), response.json()]); + }); + }); + } + + function storeCacheData() { + return caches.open(CACHE_NAME).then(function (cache) { + var data = new Response(JSON.stringify({ + version: params.version, + hashmap: hashesMap + })); + + return cache.put(new URL(STORED_DATA_KEY, location).toString(), data); + }); + } + + self.addEventListener('fetch', function (event) { + var url = new URL(event.request.url); + url.hash = ''; + + var urlString = url.toString(); + + // Not external, so search part of the URL should be stripped, + // if it's external URL, the search part should be kept + if (externals.indexOf(urlString) === -1) { + url.search = ''; + urlString = url.toString(); + } + + // Handle only GET requests + var isGET = event.request.method === 'GET'; + var assetMatches = allAssets.indexOf(urlString) !== -1; + var cacheUrl = urlString; + + if (!assetMatches) { + var cacheRewrite = matchCacheMap(event.request); + + if (cacheRewrite) { + cacheUrl = cacheRewrite; + assetMatches = true; + } + } + + if (!assetMatches && isGET) { + // If isn't a cached asset and is a navigation request, + // perform network request and fallback to navigateFallbackURL if available. + // + // Requesting with fetchWithPreload(). + // Preload is used only if navigationPreload is enabled and + // navigationPreload mapping is not used. + if (navigateFallbackURL && isNavigateRequest(event.request)) { + event.respondWith(handleNavigateFallback(fetchWithPreload(event))); + + return; + } + + if (navigationPreload === true) { + event.respondWith(fetchWithPreload(event)); + return; + } + + // Something else, positive, but not `true` + if (navigationPreload) { + var preloadedResponse = retrivePreloadedResponse(event); + + if (preloadedResponse) { + event.respondWith(preloadedResponse); + return; + } + } + + // Logic exists here if no cache match, or no preload + return; + } + + if (!assetMatches || !isGET) { + // Fix for https://twitter.com/wanderview/status/696819243262873600 + if (url.origin !== location.origin && navigator.userAgent.indexOf('Firefox/44.') !== -1) { + event.respondWith(fetch(event.request)); + } + + // Logic exists here if no cache match + return; + } + + // Cache handling/storing/fetching starts here + + var resource = undefined; + + if (responseStrategy === 'network-first') { + resource = networkFirstResponse(event, urlString, cacheUrl); + } + // 'cache-first' otherwise + // (responseStrategy has been validated before) + else { + resource = cacheFirstResponse(event, urlString, cacheUrl); + } + + if (navigateFallbackURL && isNavigateRequest(event.request)) { + resource = handleNavigateFallback(resource); + } + + event.respondWith(resource); + }); + + self.addEventListener('message', function (e) { + var data = e.data; + if (!data) return; + + switch (data.action) { + case 'skipWaiting': + { + if (self.skipWaiting) self.skipWaiting(); + }break; + } + }); + + function cacheFirstResponse(event, urlString, cacheUrl) { + handleNavigationPreload(event); + + return cachesMatch(cacheUrl, CACHE_NAME).then(function (response) { + if (response) { + if (DEBUG) { + console.log('[SW]:', 'URL [' + cacheUrl + '](' + urlString + ') from cache'); + } + + return response; + } + + // Load and cache known assets + var fetching = fetch(event.request).then(function (response) { + if (!response.ok) { + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] wrong response: [' + response.status + '] ' + response.type); + } + + return response; + } + + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] from network'); + } + + if (cacheUrl === urlString) { + (function () { + var responseClone = response.clone(); + var storing = caches.open(CACHE_NAME).then(function (cache) { + return cache.put(urlString, responseClone); + }).then(function () { + console.log('[SW]:', 'Cache asset: ' + urlString); + }); + + event.waitUntil(storing); + })(); + } + + return response; + }); + + return fetching; + }); + } + + function networkFirstResponse(event, urlString, cacheUrl) { + return fetchWithPreload(event).then(function (response) { + if (response.ok) { + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] from network'); + } + + return response; + } + + // Throw to reach the code in the catch below + throw new Error('Response is not ok'); + }) + // This needs to be in a catch() and not just in the then() above + // cause if your network is down, the fetch() will throw + ['catch'](function () { + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] from cache if possible'); + } + + return cachesMatch(cacheUrl, CACHE_NAME); + }); + } + + function handleNavigationPreload(event) { + if (navigationPreload && typeof navigationPreload.map === 'function' && + // Use request.mode === 'navigate' instead of isNavigateRequest + // because everything what supports navigationPreload supports + // 'navigate' request.mode + event.preloadResponse && event.request.mode === 'navigate') { + var mapped = navigationPreload.map(new URL(event.request.url), event.request); + + if (mapped) { + storePreloadedResponse(mapped, event); + } + } + } + + // Temporary in-memory store for faster access + var navigationPreloadStore = new Map(); + + function storePreloadedResponse(_url, event) { + var url = new URL(_url, location); + var preloadResponsePromise = event.preloadResponse; + + navigationPreloadStore.set(preloadResponsePromise, { + url: url, + response: preloadResponsePromise + }); + + var isSamePreload = function isSamePreload() { + return navigationPreloadStore.has(preloadResponsePromise); + }; + + var storing = preloadResponsePromise.then(function (res) { + // Return if preload isn't enabled or hasn't happened + if (!res) return; + + // If navigationPreloadStore already consumed + // or navigationPreloadStore already contains another preload, + // then do not store anything and return + if (!isSamePreload()) { + return; + } + + var clone = res.clone(); + + // Storing the preload response for later consume (hasn't yet been consumed) + return caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + if (!isSamePreload()) return; + + return cache.put(url, clone).then(function () { + if (!isSamePreload()) { + return caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + return cache['delete'](url); + }); + } + }); + }); + }); + + event.waitUntil(storing); + } + + function retriveInMemoryPreloadedResponse(url) { + if (!navigationPreloadStore) { + return; + } + + var foundResponse = undefined; + var foundKey = undefined; + + navigationPreloadStore.forEach(function (store, key) { + if (store.url.href === url.href) { + foundResponse = store.response; + foundKey = key; + } + }); + + if (foundResponse) { + navigationPreloadStore['delete'](foundKey); + return foundResponse; + } + } + + function retrivePreloadedResponse(event) { + var url = new URL(event.request.url); + + if (self.registration.navigationPreload && navigationPreload && navigationPreload.test && navigationPreload.test(url, event.request)) {} else { + return; + } + + var fromMemory = retriveInMemoryPreloadedResponse(url); + var request = event.request; + + if (fromMemory) { + event.waitUntil(caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + return cache['delete'](request); + })); + + return fromMemory; + } + + return cachesMatch(request, PRELOAD_CACHE_NAME).then(function (response) { + if (response) { + event.waitUntil(caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + return cache['delete'](request); + })); + } + + return response || fetch(event.request); + }); + } + + function handleNavigateFallback(fetching) { + return fetching['catch'](function () {}).then(function (response) { + var isOk = response && response.ok; + var isRedirect = response && response.type === 'opaqueredirect'; + + if (isOk || isRedirect && !navigateFallbackForRedirects) { + return response; + } + + if (DEBUG) { + console.log('[SW]:', 'Loading navigation fallback [' + navigateFallbackURL + '] from cache'); + } + + return cachesMatch(navigateFallbackURL, CACHE_NAME); + }); + } + + function mapAssets() { + Object.keys(assets).forEach(function (key) { + assets[key] = assets[key].map(function (path) { + var url = new URL(path, location); + + url.hash = ''; + + if (externals.indexOf(path) === -1) { + url.search = ''; + } + + return url.toString(); + }); + }); + + Object.keys(loadersMap).forEach(function (key) { + loadersMap[key] = loadersMap[key].map(function (path) { + var url = new URL(path, location); + + url.hash = ''; + + if (externals.indexOf(path) === -1) { + url.search = ''; + } + + return url.toString(); + }); + }); + + hashesMap = Object.keys(hashesMap).reduce(function (result, hash) { + var url = new URL(hashesMap[hash], location); + url.search = ''; + url.hash = ''; + + result[hash] = url.toString(); + return result; + }, {}); + + externals = externals.map(function (path) { + var url = new URL(path, location); + url.hash = ''; + + return url.toString(); + }); + } + + function addAllNormalized(cache, requests, options) { + var allowLoaders = options.allowLoaders !== false; + var bustValue = options && options.bust; + var requestInit = options.request || { + credentials: 'omit', + mode: 'cors' + }; + + return Promise.all(requests.map(function (request) { + if (bustValue) { + request = applyCacheBust(request, bustValue); + } + + return fetch(request, requestInit).then(fixRedirectedResponse); + })).then(function (responses) { + if (responses.some(function (response) { + return !response.ok; + })) { + return Promise.reject(new Error('Wrong response status')); + } + + var extracted = []; + var addAll = responses.map(function (response, i) { + if (allowLoaders) { + extracted.push(extractAssetsWithLoaders(requests[i], response)); + } + + return cache.put(requests[i], response); + }); + + if (extracted.length) { + (function () { + var newOptions = copyObject(options); + newOptions.allowLoaders = false; + + var waitAll = addAll; + + addAll = Promise.all(extracted).then(function (all) { + var extractedRequests = [].concat.apply([], all); + + if (requests.length) { + waitAll = waitAll.concat(addAllNormalized(cache, extractedRequests, newOptions)); + } + + return Promise.all(waitAll); + }); + })(); + } else { + addAll = Promise.all(addAll); + } + + return addAll; + }); + } + + function extractAssetsWithLoaders(request, response) { + var all = Object.keys(loadersMap).map(function (key) { + var loader = loadersMap[key]; + + if (loader.indexOf(request) !== -1 && loaders[key]) { + return loaders[key](response.clone()); + } + }).filter(function (a) { + return !!a; + }); + + return Promise.all(all).then(function (all) { + return [].concat.apply([], all); + }); + } + + function matchCacheMap(request) { + var urlString = request.url; + var url = new URL(urlString); + + var requestType = undefined; + + if (isNavigateRequest(request)) { + requestType = 'navigate'; + } else if (url.origin === location.origin) { + requestType = 'same-origin'; + } else { + requestType = 'cross-origin'; + } + + for (var i = 0; i < cacheMaps.length; i++) { + var map = cacheMaps[i]; + + if (!map) continue; + if (map.requestTypes && map.requestTypes.indexOf(requestType) === -1) { + continue; + } + + var newString = undefined; + + if (typeof map.match === 'function') { + newString = map.match(url, request); + } else { + newString = urlString.replace(map.match, map.to); + } + + if (newString && newString !== urlString) { + return newString; + } + } + } + + function fetchWithPreload(event) { + if (!event.preloadResponse || navigationPreload !== true) { + return fetch(event.request); + } + + return event.preloadResponse.then(function (response) { + return response || fetch(event.request); + }); + } +} + +function cachesMatch(request, cacheName) { + return caches.match(request, { + cacheName: cacheName + }).then(function (response) { + if (isNotRedirectedResponse()) { + return response; + } + + // Fix already cached redirected responses + return fixRedirectedResponse(response).then(function (fixedResponse) { + return caches.open(cacheName).then(function (cache) { + return cache.put(request, fixedResponse); + }).then(function () { + return fixedResponse; + }); + }); + }) + // Return void if error happened (cache not found) + ['catch'](function () {}); +} + +function applyCacheBust(asset, key) { + var hasQuery = asset.indexOf('?') !== -1; + return asset + (hasQuery ? '&' : '?') + '__uncache=' + encodeURIComponent(key); +} + +function isNavigateRequest(request) { + return request.mode === 'navigate' || request.headers.get('Upgrade-Insecure-Requests') || (request.headers.get('Accept') || '').indexOf('text/html') !== -1; +} + +function isNotRedirectedResponse(response) { + return !response || !response.redirected || !response.ok || response.type === 'opaqueredirect'; +} + +// Based on https://github.com/GoogleChrome/sw-precache/pull/241/files#diff-3ee9060dc7a312c6a822cac63a8c630bR85 +function fixRedirectedResponse(response) { + if (isNotRedirectedResponse(response)) { + return Promise.resolve(response); + } + + var body = 'body' in response ? Promise.resolve(response.body) : response.blob(); + + return body.then(function (data) { + return new Response(data, { + headers: response.headers, + status: response.status + }); + }); +} + +function copyObject(original) { + return Object.keys(original).reduce(function (result, key) { + result[key] = original[key]; + return result; + }, {}); +} + +function logGroup(title, assets) { + console.groupCollapsed('[SW]:', title); + + assets.forEach(function (asset) { + console.log('Asset:', asset); + }); + + console.groupEnd(); +} + WebpackServiceWorker(__wpo, { +loaders: {}, +cacheMaps: [], +navigationPreload: false, +}); + module.exports = __webpack_require__(1) + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +'MEOW' + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack4/appcache/manifest.appcache b/tests/legacy/fixtures/sw-plugins/__expected/webpack4/appcache/manifest.appcache new file mode 100644 index 00000000..5a7cf4dd --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack4/appcache/manifest.appcache @@ -0,0 +1,8 @@ +CACHE MANIFEST +#ver:da39a3ee5e6b4b0d3255bfef95601890afd80709 + +CACHE: +../external.js + +NETWORK: +* \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack4/appcache/manifest.html b/tests/legacy/fixtures/sw-plugins/__expected/webpack4/appcache/manifest.html new file mode 100644 index 00000000..7e4d9c0b --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack4/appcache/manifest.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack4/main.js b/tests/legacy/fixtures/sw-plugins/__expected/webpack4/main.js new file mode 100644 index 00000000..4bac97c7 --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack4/main.js @@ -0,0 +1,79 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + + + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/__expected/webpack4/sw.js b/tests/legacy/fixtures/sw-plugins/__expected/webpack4/sw.js new file mode 100644 index 00000000..54c9adc1 --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/__expected/webpack4/sw.js @@ -0,0 +1,930 @@ +var __wpo = { + "assets": { + "main": [ + "./external.js" + ], + "additional": [], + "optional": [] + }, + "externals": [ + "./external.js" + ], + "hashesMap": {}, + "strategy": "changed", + "responseStrategy": "cache-first", + "version": "da39a3ee5e6b4b0d3255bfef95601890afd80709", + "name": "webpack-offline", + "relativePaths": true +}; + +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +(function () { + var waitUntil = ExtendableEvent.prototype.waitUntil; + var respondWith = FetchEvent.prototype.respondWith; + var promisesMap = new WeakMap(); + + ExtendableEvent.prototype.waitUntil = function (promise) { + var extendableEvent = this; + var promises = promisesMap.get(extendableEvent); + + if (promises) { + promises.push(Promise.resolve(promise)); + return; + } + + promises = [Promise.resolve(promise)]; + promisesMap.set(extendableEvent, promises); + + // call original method + return waitUntil.call(extendableEvent, Promise.resolve().then(function processPromises() { + var len = promises.length; + + // wait for all to settle + return Promise.all(promises.map(function (p) { + return p["catch"](function () {}); + })).then(function () { + // have new items been added? If so, wait again + if (promises.length != len) return processPromises(); + // we're done! + promisesMap["delete"](extendableEvent); + // reject if one of the promises rejected + return Promise.all(promises); + }); + })); + }; + + FetchEvent.prototype.respondWith = function (promise) { + this.waitUntil(promise); + return respondWith.call(this, promise); + }; +})();; + 'use strict'; + +if (typeof DEBUG === 'undefined') { + var DEBUG = false; +} + +function WebpackServiceWorker(params, helpers) { + var loaders = helpers.loaders; + var cacheMaps = helpers.cacheMaps; + // navigationPreload: true, { map: (URL) => URL, test: (URL) => boolean } + var navigationPreload = helpers.navigationPreload; + + // (update)strategy: changed, all + var strategy = params.strategy; + // responseStrategy: cache-first, network-first + var responseStrategy = params.responseStrategy; + + var assets = params.assets; + var loadersMap = params.loaders || {}; + + var hashesMap = params.hashesMap; + var externals = params.externals; + + var prefetchRequest = params.prefetchRequest || { + credentials: 'same-origin', + mode: 'cors' + }; + + var CACHE_PREFIX = params.name; + var CACHE_TAG = params.version; + var CACHE_NAME = CACHE_PREFIX + ':' + CACHE_TAG; + + var PRELOAD_CACHE_NAME = CACHE_PREFIX + '$preload'; + var STORED_DATA_KEY = '__offline_webpack__data'; + + mapAssets(); + + var allAssets = [].concat(assets.main, assets.additional, assets.optional); + + // Deprecated { + var navigateFallbackURL = params.navigateFallbackURL; + var navigateFallbackForRedirects = params.navigateFallbackForRedirects; + // } + + self.addEventListener('install', function (event) { + console.log('[SW]:', 'Install event'); + + var installing = undefined; + + if (strategy === 'changed') { + installing = cacheChanged('main'); + } else { + installing = cacheAssets('main'); + } + + event.waitUntil(installing); + }); + + self.addEventListener('activate', function (event) { + console.log('[SW]:', 'Activate event'); + + var activation = cacheAdditional(); + + // Delete all assets which name starts with CACHE_PREFIX and + // is not current cache (CACHE_NAME) + activation = activation.then(storeCacheData); + activation = activation.then(deleteObsolete); + activation = activation.then(function () { + if (self.clients && self.clients.claim) { + return self.clients.claim(); + } + }); + + if (navigationPreload && self.registration.navigationPreload) { + activation = Promise.all([activation, self.registration.navigationPreload.enable()]); + } + + event.waitUntil(activation); + }); + + function cacheAdditional() { + if (!assets.additional.length) { + return Promise.resolve(); + } + + if (DEBUG) { + console.log('[SW]:', 'Caching additional'); + } + + var operation = undefined; + + if (strategy === 'changed') { + operation = cacheChanged('additional'); + } else { + operation = cacheAssets('additional'); + } + + // Ignore fail of `additional` cache section + return operation['catch'](function (e) { + console.error('[SW]:', 'Cache section `additional` failed to load'); + }); + } + + function cacheAssets(section) { + var batch = assets[section]; + + return caches.open(CACHE_NAME).then(function (cache) { + return addAllNormalized(cache, batch, { + bust: params.version, + request: prefetchRequest + }); + }).then(function () { + logGroup('Cached assets: ' + section, batch); + })['catch'](function (e) { + console.error(e); + throw e; + }); + } + + function cacheChanged(section) { + return getLastCache().then(function (args) { + if (!args) { + return cacheAssets(section); + } + + var lastCache = args[0]; + var lastKeys = args[1]; + var lastData = args[2]; + + var lastMap = lastData.hashmap; + var lastVersion = lastData.version; + + if (!lastData.hashmap || lastVersion === params.version) { + return cacheAssets(section); + } + + var lastHashedAssets = Object.keys(lastMap).map(function (hash) { + return lastMap[hash]; + }); + + var lastUrls = lastKeys.map(function (req) { + var url = new URL(req.url); + url.search = ''; + url.hash = ''; + + return url.toString(); + }); + + var sectionAssets = assets[section]; + var moved = []; + var changed = sectionAssets.filter(function (url) { + if (lastUrls.indexOf(url) === -1 || lastHashedAssets.indexOf(url) === -1) { + return true; + } + + return false; + }); + + Object.keys(hashesMap).forEach(function (hash) { + var asset = hashesMap[hash]; + + // Return if not in sectionAssets or in changed or moved array + if (sectionAssets.indexOf(asset) === -1 || changed.indexOf(asset) !== -1 || moved.indexOf(asset) !== -1) return; + + var lastAsset = lastMap[hash]; + + if (lastAsset && lastUrls.indexOf(lastAsset) !== -1) { + moved.push([lastAsset, asset]); + } else { + changed.push(asset); + } + }); + + logGroup('Changed assets: ' + section, changed); + logGroup('Moved assets: ' + section, moved); + + var movedResponses = Promise.all(moved.map(function (pair) { + return lastCache.match(pair[0]).then(function (response) { + return [pair[1], response]; + }); + })); + + return caches.open(CACHE_NAME).then(function (cache) { + var move = movedResponses.then(function (responses) { + return Promise.all(responses.map(function (pair) { + return cache.put(pair[0], pair[1]); + })); + }); + + return Promise.all([move, addAllNormalized(cache, changed, { + bust: params.version, + request: prefetchRequest + })]); + }); + }); + } + + function deleteObsolete() { + return caches.keys().then(function (keys) { + var all = keys.map(function (key) { + if (key.indexOf(CACHE_PREFIX) !== 0 || key.indexOf(CACHE_NAME) === 0) return; + + console.log('[SW]:', 'Delete cache:', key); + return caches['delete'](key); + }); + + return Promise.all(all); + }); + } + + function getLastCache() { + return caches.keys().then(function (keys) { + var index = keys.length; + var key = undefined; + + while (index--) { + key = keys[index]; + + if (key.indexOf(CACHE_PREFIX) === 0) { + break; + } + } + + if (!key) return; + + var cache = undefined; + + return caches.open(key).then(function (_cache) { + cache = _cache; + return _cache.match(new URL(STORED_DATA_KEY, location).toString()); + }).then(function (response) { + if (!response) return; + + return Promise.all([cache, cache.keys(), response.json()]); + }); + }); + } + + function storeCacheData() { + return caches.open(CACHE_NAME).then(function (cache) { + var data = new Response(JSON.stringify({ + version: params.version, + hashmap: hashesMap + })); + + return cache.put(new URL(STORED_DATA_KEY, location).toString(), data); + }); + } + + self.addEventListener('fetch', function (event) { + var url = new URL(event.request.url); + url.hash = ''; + + var urlString = url.toString(); + + // Not external, so search part of the URL should be stripped, + // if it's external URL, the search part should be kept + if (externals.indexOf(urlString) === -1) { + url.search = ''; + urlString = url.toString(); + } + + // Handle only GET requests + var isGET = event.request.method === 'GET'; + var assetMatches = allAssets.indexOf(urlString) !== -1; + var cacheUrl = urlString; + + if (!assetMatches) { + var cacheRewrite = matchCacheMap(event.request); + + if (cacheRewrite) { + cacheUrl = cacheRewrite; + assetMatches = true; + } + } + + if (!assetMatches && isGET) { + // If isn't a cached asset and is a navigation request, + // perform network request and fallback to navigateFallbackURL if available. + // + // Requesting with fetchWithPreload(). + // Preload is used only if navigationPreload is enabled and + // navigationPreload mapping is not used. + if (navigateFallbackURL && isNavigateRequest(event.request)) { + event.respondWith(handleNavigateFallback(fetchWithPreload(event))); + + return; + } + + if (navigationPreload === true) { + event.respondWith(fetchWithPreload(event)); + return; + } + + // Something else, positive, but not `true` + if (navigationPreload) { + var preloadedResponse = retrivePreloadedResponse(event); + + if (preloadedResponse) { + event.respondWith(preloadedResponse); + return; + } + } + + // Logic exists here if no cache match, or no preload + return; + } + + if (!assetMatches || !isGET) { + // Fix for https://twitter.com/wanderview/status/696819243262873600 + if (url.origin !== location.origin && navigator.userAgent.indexOf('Firefox/44.') !== -1) { + event.respondWith(fetch(event.request)); + } + + // Logic exists here if no cache match + return; + } + + // Cache handling/storing/fetching starts here + + var resource = undefined; + + if (responseStrategy === 'network-first') { + resource = networkFirstResponse(event, urlString, cacheUrl); + } + // 'cache-first' otherwise + // (responseStrategy has been validated before) + else { + resource = cacheFirstResponse(event, urlString, cacheUrl); + } + + if (navigateFallbackURL && isNavigateRequest(event.request)) { + resource = handleNavigateFallback(resource); + } + + event.respondWith(resource); + }); + + self.addEventListener('message', function (e) { + var data = e.data; + if (!data) return; + + switch (data.action) { + case 'skipWaiting': + { + if (self.skipWaiting) self.skipWaiting(); + }break; + } + }); + + function cacheFirstResponse(event, urlString, cacheUrl) { + handleNavigationPreload(event); + + return cachesMatch(cacheUrl, CACHE_NAME).then(function (response) { + if (response) { + if (DEBUG) { + console.log('[SW]:', 'URL [' + cacheUrl + '](' + urlString + ') from cache'); + } + + return response; + } + + // Load and cache known assets + var fetching = fetch(event.request).then(function (response) { + if (!response.ok) { + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] wrong response: [' + response.status + '] ' + response.type); + } + + return response; + } + + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] from network'); + } + + if (cacheUrl === urlString) { + (function () { + var responseClone = response.clone(); + var storing = caches.open(CACHE_NAME).then(function (cache) { + return cache.put(urlString, responseClone); + }).then(function () { + console.log('[SW]:', 'Cache asset: ' + urlString); + }); + + event.waitUntil(storing); + })(); + } + + return response; + }); + + return fetching; + }); + } + + function networkFirstResponse(event, urlString, cacheUrl) { + return fetchWithPreload(event).then(function (response) { + if (response.ok) { + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] from network'); + } + + return response; + } + + // Throw to reach the code in the catch below + throw new Error('Response is not ok'); + }) + // This needs to be in a catch() and not just in the then() above + // cause if your network is down, the fetch() will throw + ['catch'](function () { + if (DEBUG) { + console.log('[SW]:', 'URL [' + urlString + '] from cache if possible'); + } + + return cachesMatch(cacheUrl, CACHE_NAME); + }); + } + + function handleNavigationPreload(event) { + if (navigationPreload && typeof navigationPreload.map === 'function' && + // Use request.mode === 'navigate' instead of isNavigateRequest + // because everything what supports navigationPreload supports + // 'navigate' request.mode + event.preloadResponse && event.request.mode === 'navigate') { + var mapped = navigationPreload.map(new URL(event.request.url), event.request); + + if (mapped) { + storePreloadedResponse(mapped, event); + } + } + } + + // Temporary in-memory store for faster access + var navigationPreloadStore = new Map(); + + function storePreloadedResponse(_url, event) { + var url = new URL(_url, location); + var preloadResponsePromise = event.preloadResponse; + + navigationPreloadStore.set(preloadResponsePromise, { + url: url, + response: preloadResponsePromise + }); + + var isSamePreload = function isSamePreload() { + return navigationPreloadStore.has(preloadResponsePromise); + }; + + var storing = preloadResponsePromise.then(function (res) { + // Return if preload isn't enabled or hasn't happened + if (!res) return; + + // If navigationPreloadStore already consumed + // or navigationPreloadStore already contains another preload, + // then do not store anything and return + if (!isSamePreload()) { + return; + } + + var clone = res.clone(); + + // Storing the preload response for later consume (hasn't yet been consumed) + return caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + if (!isSamePreload()) return; + + return cache.put(url, clone).then(function () { + if (!isSamePreload()) { + return caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + return cache['delete'](url); + }); + } + }); + }); + }); + + event.waitUntil(storing); + } + + function retriveInMemoryPreloadedResponse(url) { + if (!navigationPreloadStore) { + return; + } + + var foundResponse = undefined; + var foundKey = undefined; + + navigationPreloadStore.forEach(function (store, key) { + if (store.url.href === url.href) { + foundResponse = store.response; + foundKey = key; + } + }); + + if (foundResponse) { + navigationPreloadStore['delete'](foundKey); + return foundResponse; + } + } + + function retrivePreloadedResponse(event) { + var url = new URL(event.request.url); + + if (self.registration.navigationPreload && navigationPreload && navigationPreload.test && navigationPreload.test(url, event.request)) {} else { + return; + } + + var fromMemory = retriveInMemoryPreloadedResponse(url); + var request = event.request; + + if (fromMemory) { + event.waitUntil(caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + return cache['delete'](request); + })); + + return fromMemory; + } + + return cachesMatch(request, PRELOAD_CACHE_NAME).then(function (response) { + if (response) { + event.waitUntil(caches.open(PRELOAD_CACHE_NAME).then(function (cache) { + return cache['delete'](request); + })); + } + + return response || fetch(event.request); + }); + } + + function handleNavigateFallback(fetching) { + return fetching['catch'](function () {}).then(function (response) { + var isOk = response && response.ok; + var isRedirect = response && response.type === 'opaqueredirect'; + + if (isOk || isRedirect && !navigateFallbackForRedirects) { + return response; + } + + if (DEBUG) { + console.log('[SW]:', 'Loading navigation fallback [' + navigateFallbackURL + '] from cache'); + } + + return cachesMatch(navigateFallbackURL, CACHE_NAME); + }); + } + + function mapAssets() { + Object.keys(assets).forEach(function (key) { + assets[key] = assets[key].map(function (path) { + var url = new URL(path, location); + + url.hash = ''; + + if (externals.indexOf(path) === -1) { + url.search = ''; + } + + return url.toString(); + }); + }); + + Object.keys(loadersMap).forEach(function (key) { + loadersMap[key] = loadersMap[key].map(function (path) { + var url = new URL(path, location); + + url.hash = ''; + + if (externals.indexOf(path) === -1) { + url.search = ''; + } + + return url.toString(); + }); + }); + + hashesMap = Object.keys(hashesMap).reduce(function (result, hash) { + var url = new URL(hashesMap[hash], location); + url.search = ''; + url.hash = ''; + + result[hash] = url.toString(); + return result; + }, {}); + + externals = externals.map(function (path) { + var url = new URL(path, location); + url.hash = ''; + + return url.toString(); + }); + } + + function addAllNormalized(cache, requests, options) { + var allowLoaders = options.allowLoaders !== false; + var bustValue = options && options.bust; + var requestInit = options.request || { + credentials: 'omit', + mode: 'cors' + }; + + return Promise.all(requests.map(function (request) { + if (bustValue) { + request = applyCacheBust(request, bustValue); + } + + return fetch(request, requestInit).then(fixRedirectedResponse); + })).then(function (responses) { + if (responses.some(function (response) { + return !response.ok; + })) { + return Promise.reject(new Error('Wrong response status')); + } + + var extracted = []; + var addAll = responses.map(function (response, i) { + if (allowLoaders) { + extracted.push(extractAssetsWithLoaders(requests[i], response)); + } + + return cache.put(requests[i], response); + }); + + if (extracted.length) { + (function () { + var newOptions = copyObject(options); + newOptions.allowLoaders = false; + + var waitAll = addAll; + + addAll = Promise.all(extracted).then(function (all) { + var extractedRequests = [].concat.apply([], all); + + if (requests.length) { + waitAll = waitAll.concat(addAllNormalized(cache, extractedRequests, newOptions)); + } + + return Promise.all(waitAll); + }); + })(); + } else { + addAll = Promise.all(addAll); + } + + return addAll; + }); + } + + function extractAssetsWithLoaders(request, response) { + var all = Object.keys(loadersMap).map(function (key) { + var loader = loadersMap[key]; + + if (loader.indexOf(request) !== -1 && loaders[key]) { + return loaders[key](response.clone()); + } + }).filter(function (a) { + return !!a; + }); + + return Promise.all(all).then(function (all) { + return [].concat.apply([], all); + }); + } + + function matchCacheMap(request) { + var urlString = request.url; + var url = new URL(urlString); + + var requestType = undefined; + + if (isNavigateRequest(request)) { + requestType = 'navigate'; + } else if (url.origin === location.origin) { + requestType = 'same-origin'; + } else { + requestType = 'cross-origin'; + } + + for (var i = 0; i < cacheMaps.length; i++) { + var map = cacheMaps[i]; + + if (!map) continue; + if (map.requestTypes && map.requestTypes.indexOf(requestType) === -1) { + continue; + } + + var newString = undefined; + + if (typeof map.match === 'function') { + newString = map.match(url, request); + } else { + newString = urlString.replace(map.match, map.to); + } + + if (newString && newString !== urlString) { + return newString; + } + } + } + + function fetchWithPreload(event) { + if (!event.preloadResponse || navigationPreload !== true) { + return fetch(event.request); + } + + return event.preloadResponse.then(function (response) { + return response || fetch(event.request); + }); + } +} + +function cachesMatch(request, cacheName) { + return caches.match(request, { + cacheName: cacheName + }).then(function (response) { + if (isNotRedirectedResponse()) { + return response; + } + + // Fix already cached redirected responses + return fixRedirectedResponse(response).then(function (fixedResponse) { + return caches.open(cacheName).then(function (cache) { + return cache.put(request, fixedResponse); + }).then(function () { + return fixedResponse; + }); + }); + }) + // Return void if error happened (cache not found) + ['catch'](function () {}); +} + +function applyCacheBust(asset, key) { + var hasQuery = asset.indexOf('?') !== -1; + return asset + (hasQuery ? '&' : '?') + '__uncache=' + encodeURIComponent(key); +} + +function isNavigateRequest(request) { + return request.mode === 'navigate' || request.headers.get('Upgrade-Insecure-Requests') || (request.headers.get('Accept') || '').indexOf('text/html') !== -1; +} + +function isNotRedirectedResponse(response) { + return !response || !response.redirected || !response.ok || response.type === 'opaqueredirect'; +} + +// Based on https://github.com/GoogleChrome/sw-precache/pull/241/files#diff-3ee9060dc7a312c6a822cac63a8c630bR85 +function fixRedirectedResponse(response) { + if (isNotRedirectedResponse(response)) { + return Promise.resolve(response); + } + + var body = 'body' in response ? Promise.resolve(response.body) : response.blob(); + + return body.then(function (data) { + return new Response(data, { + headers: response.headers, + status: response.status + }); + }); +} + +function copyObject(original) { + return Object.keys(original).reduce(function (result, key) { + result[key] = original[key]; + return result; + }, {}); +} + +function logGroup(title, assets) { + console.groupCollapsed('[SW]:', title); + + assets.forEach(function (asset) { + console.log('Asset:', asset); + }); + + console.groupEnd(); +} + WebpackServiceWorker(__wpo, { +loaders: {}, +cacheMaps: [], +navigationPreload: false, +}); + module.exports = __webpack_require__(1) + + +/***/ }), +/* 1 */ +/***/ (function(module, exports) { + +'MEOW' + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/main.js b/tests/legacy/fixtures/sw-plugins/main.js new file mode 100644 index 00000000..e69de29b diff --git a/tests/legacy/fixtures/sw-plugins/sw-entry.js b/tests/legacy/fixtures/sw-plugins/sw-entry.js new file mode 100644 index 00000000..886fc7c6 --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/sw-entry.js @@ -0,0 +1 @@ +CAT \ No newline at end of file diff --git a/tests/legacy/fixtures/sw-plugins/webpack.config.js b/tests/legacy/fixtures/sw-plugins/webpack.config.js new file mode 100644 index 00000000..9c5ee3b4 --- /dev/null +++ b/tests/legacy/fixtures/sw-plugins/webpack.config.js @@ -0,0 +1,18 @@ +const { DefinePlugin } = require('webpack'); + +module.exports = __CONFIG__({ + caches: { + main: ['external.js', ':rest:'] + }, + + externals: ['external.js'], + excludes: ['main.js'], + version: '[hash]', + + ServiceWorker: { + entry: 'sw-entry.js', + plugins: [new DefinePlugin({ CAT: '\'MEOW\'' })], + } +}, { + swMetadataOnly: false +});