UNPKG

19.7 kBJavaScriptView Raw
1'use strict';
2
3if (typeof DEBUG === 'undefined') {
4 var DEBUG = false;
5}
6
7function WebpackServiceWorker(params, helpers) {
8 var cacheMaps = helpers.cacheMaps;
9 // navigationPreload: true, { map: (URL) => URL, test: (URL) => boolean }
10 var navigationPreload = helpers.navigationPreload;
11
12 // (update)strategy: changed, all
13 var strategy = params.strategy;
14 // responseStrategy: cache-first, network-first
15 var responseStrategy = params.responseStrategy;
16
17 var assets = params.assets;
18
19 var hashesMap = params.hashesMap;
20 var externals = params.externals;
21
22 var prefetchRequest = params.prefetchRequest || {
23 credentials: 'same-origin',
24 mode: 'cors'
25 };
26
27 var CACHE_PREFIX = params.name;
28 var CACHE_TAG = params.version;
29 var CACHE_NAME = CACHE_PREFIX + ':' + CACHE_TAG;
30
31 var PRELOAD_CACHE_NAME = CACHE_PREFIX + '$preload';
32 var STORED_DATA_KEY = '__offline_webpack__data';
33
34 mapAssets();
35
36 var allAssets = [].concat(assets.main, assets.additional, assets.optional);
37
38 self.addEventListener('install', function (event) {
39 console.log('[SW]:', 'Install event');
40
41 var installing = undefined;
42
43 if (strategy === 'changed') {
44 installing = cacheChanged('main');
45 } else {
46 installing = cacheAssets('main');
47 }
48
49 event.waitUntil(installing);
50 });
51
52 self.addEventListener('activate', function (event) {
53 console.log('[SW]:', 'Activate event');
54
55 var activation = cacheAdditional();
56
57 // Delete all assets which name starts with CACHE_PREFIX and
58 // is not current cache (CACHE_NAME)
59 activation = activation.then(storeCacheData);
60 activation = activation.then(deleteObsolete);
61 activation = activation.then(function () {
62 if (self.clients && self.clients.claim) {
63 return self.clients.claim();
64 }
65 });
66
67 if (navigationPreload && self.registration.navigationPreload) {
68 activation = Promise.all([activation, self.registration.navigationPreload.enable()]);
69 }
70
71 event.waitUntil(activation);
72 });
73
74 function cacheAdditional() {
75 if (!assets.additional.length) {
76 return Promise.resolve();
77 }
78
79 if (DEBUG) {
80 console.log('[SW]:', 'Caching additional');
81 }
82
83 var operation = undefined;
84
85 if (strategy === 'changed') {
86 operation = cacheChanged('additional');
87 } else {
88 operation = cacheAssets('additional');
89 }
90
91 // Ignore fail of `additional` cache section
92 return operation['catch'](function (e) {
93 console.error('[SW]:', 'Cache section `additional` failed to load');
94 });
95 }
96
97 function cacheAssets(section) {
98 var batch = assets[section];
99
100 return caches.open(CACHE_NAME).then(function (cache) {
101 return addAllNormalized(cache, batch, {
102 bust: params.version,
103 request: prefetchRequest,
104 failAll: section === 'main'
105 });
106 }).then(function () {
107 logGroup('Cached assets: ' + section, batch);
108 })['catch'](function (e) {
109 console.error(e);
110 throw e;
111 });
112 }
113
114 function cacheChanged(section) {
115 return getLastCache().then(function (args) {
116 if (!args) {
117 return cacheAssets(section);
118 }
119
120 var lastCache = args[0];
121 var lastKeys = args[1];
122 var lastData = args[2];
123
124 var lastMap = lastData.hashmap;
125 var lastVersion = lastData.version;
126
127 if (!lastData.hashmap || lastVersion === params.version) {
128 return cacheAssets(section);
129 }
130
131 var lastHashedAssets = Object.keys(lastMap).map(function (hash) {
132 return lastMap[hash];
133 });
134
135 var lastUrls = lastKeys.map(function (req) {
136 var url = new URL(req.url);
137 url.search = '';
138 url.hash = '';
139
140 return url.toString();
141 });
142
143 var sectionAssets = assets[section];
144 var moved = [];
145 var changed = sectionAssets.filter(function (url) {
146 if (lastUrls.indexOf(url) === -1 || lastHashedAssets.indexOf(url) === -1) {
147 return true;
148 }
149
150 return false;
151 });
152
153 Object.keys(hashesMap).forEach(function (hash) {
154 var asset = hashesMap[hash];
155
156 // Return if not in sectionAssets or in changed or moved array
157 if (sectionAssets.indexOf(asset) === -1 || changed.indexOf(asset) !== -1 || moved.indexOf(asset) !== -1) return;
158
159 var lastAsset = lastMap[hash];
160
161 if (lastAsset && lastUrls.indexOf(lastAsset) !== -1) {
162 moved.push([lastAsset, asset]);
163 } else {
164 changed.push(asset);
165 }
166 });
167
168 logGroup('Changed assets: ' + section, changed);
169 logGroup('Moved assets: ' + section, moved);
170
171 var movedResponses = Promise.all(moved.map(function (pair) {
172 return lastCache.match(pair[0]).then(function (response) {
173 return [pair[1], response];
174 });
175 }));
176
177 return caches.open(CACHE_NAME).then(function (cache) {
178 var move = movedResponses.then(function (responses) {
179 return Promise.all(responses.map(function (pair) {
180 return cache.put(pair[0], pair[1]);
181 }));
182 });
183
184 return Promise.all([move, addAllNormalized(cache, changed, {
185 bust: params.version,
186 request: prefetchRequest,
187 failAll: section === 'main',
188 deleteFirst: section !== 'main'
189 })]);
190 });
191 });
192 }
193
194 function deleteObsolete() {
195 return caches.keys().then(function (keys) {
196 var all = keys.map(function (key) {
197 if (key.indexOf(CACHE_PREFIX) !== 0 || key.indexOf(CACHE_NAME) === 0) return;
198
199 console.log('[SW]:', 'Delete cache:', key);
200 return caches['delete'](key);
201 });
202
203 return Promise.all(all);
204 });
205 }
206
207 function getLastCache() {
208 return caches.keys().then(function (keys) {
209 var index = keys.length;
210 var key = undefined;
211
212 while (index--) {
213 key = keys[index];
214
215 if (key.indexOf(CACHE_PREFIX) === 0) {
216 break;
217 }
218 }
219
220 if (!key) return;
221
222 var cache = undefined;
223
224 return caches.open(key).then(function (_cache) {
225 cache = _cache;
226 return _cache.match(new URL(STORED_DATA_KEY, location).toString());
227 }).then(function (response) {
228 if (!response) return;
229
230 return Promise.all([cache, cache.keys(), response.json()]);
231 });
232 });
233 }
234
235 function storeCacheData() {
236 return caches.open(CACHE_NAME).then(function (cache) {
237 var data = new Response(JSON.stringify({
238 version: params.version,
239 hashmap: hashesMap
240 }));
241
242 return cache.put(new URL(STORED_DATA_KEY, location).toString(), data);
243 });
244 }
245
246 self.addEventListener('fetch', function (event) {
247 // Handle only GET requests
248 if (event.request.method !== 'GET') {
249 return;
250 }
251
252 var url = new URL(event.request.url);
253 url.hash = '';
254
255 var urlString = url.toString();
256
257 // Not external, so search part of the URL should be stripped,
258 // if it's external URL, the search part should be kept
259 if (externals.indexOf(urlString) === -1) {
260 url.search = '';
261 urlString = url.toString();
262 }
263
264 var assetMatches = allAssets.indexOf(urlString) !== -1;
265 var cacheUrl = urlString;
266
267 if (!assetMatches) {
268 var cacheRewrite = matchCacheMap(event.request);
269
270 if (cacheRewrite) {
271 cacheUrl = cacheRewrite;
272 assetMatches = true;
273 }
274 }
275
276 if (!assetMatches) {
277 // Use request.mode === 'navigate' instead of isNavigateRequest
278 // because everything what supports navigationPreload supports
279 // 'navigate' request.mode
280 if (event.request.mode === 'navigate') {
281 // Requesting with fetchWithPreload().
282 // Preload is used only if navigationPreload is enabled and
283 // navigationPreload mapping is not used.
284 if (navigationPreload === true) {
285 event.respondWith(fetchWithPreload(event));
286 return;
287 }
288 }
289
290 // Something else, positive, but not `true`
291 if (navigationPreload) {
292 var preloadedResponse = retrivePreloadedResponse(event);
293
294 if (preloadedResponse) {
295 event.respondWith(preloadedResponse);
296 return;
297 }
298 }
299
300 // Logic exists here if no cache match
301 return;
302 }
303
304 // Cache handling/storing/fetching starts here
305 var resource = undefined;
306
307 if (responseStrategy === 'network-first') {
308 resource = networkFirstResponse(event, urlString, cacheUrl);
309 }
310 // 'cache-first' otherwise
311 // (responseStrategy has been validated before)
312 else {
313 resource = cacheFirstResponse(event, urlString, cacheUrl);
314 }
315
316 event.respondWith(resource);
317 });
318
319 self.addEventListener('message', function (e) {
320 var data = e.data;
321 if (!data) return;
322
323 switch (data.action) {
324 case 'skipWaiting':
325 {
326 if (self.skipWaiting) self.skipWaiting();
327 }break;
328 }
329 });
330
331 function cacheFirstResponse(event, urlString, cacheUrl) {
332 handleNavigationPreload(event);
333
334 return cachesMatch(cacheUrl, CACHE_NAME).then(function (response) {
335 if (response) {
336 if (DEBUG) {
337 console.log('[SW]:', 'URL [' + cacheUrl + '](' + urlString + ') from cache');
338 }
339
340 return response;
341 }
342
343 // Load and cache known assets
344 var fetching = fetch(event.request).then(function (response) {
345 if (!response.ok) {
346 if (DEBUG) {
347 console.log('[SW]:', 'URL [' + urlString + '] wrong response: [' + response.status + '] ' + response.type);
348 }
349
350 return response;
351 }
352
353 if (DEBUG) {
354 console.log('[SW]:', 'URL [' + urlString + '] from network');
355 }
356
357 if (cacheUrl === urlString) {
358 (function () {
359 var responseClone = response.clone();
360 var storing = caches.open(CACHE_NAME).then(function (cache) {
361 return cache.put(urlString, responseClone);
362 }).then(function () {
363 console.log('[SW]:', 'Cache asset: ' + urlString);
364 });
365
366 event.waitUntil(storing);
367 })();
368 }
369
370 return response;
371 });
372
373 return fetching;
374 });
375 }
376
377 function networkFirstResponse(event, urlString, cacheUrl) {
378 return fetchWithPreload(event).then(function (response) {
379 if (response.ok) {
380 if (DEBUG) {
381 console.log('[SW]:', 'URL [' + urlString + '] from network');
382 }
383
384 return response;
385 }
386
387 // Throw to reach the code in the catch below
388 throw response;
389 })
390 // This needs to be in a catch() and not just in the then() above
391 // cause if your network is down, the fetch() will throw
392 ['catch'](function (erroredResponse) {
393 if (DEBUG) {
394 console.log('[SW]:', 'URL [' + urlString + '] from cache if possible');
395 }
396
397 return cachesMatch(cacheUrl, CACHE_NAME).then(function (response) {
398 if (response) {
399 return response;
400 }
401
402 if (erroredResponse instanceof Response) {
403 return erroredResponse;
404 }
405
406 // Not a response at this point, some other error
407 throw erroredResponse;
408 // return Response.error();
409 });
410 });
411 }
412
413 function handleNavigationPreload(event) {
414 if (navigationPreload && typeof navigationPreload.map === 'function' &&
415 // Use request.mode === 'navigate' instead of isNavigateRequest
416 // because everything what supports navigationPreload supports
417 // 'navigate' request.mode
418 event.preloadResponse && event.request.mode === 'navigate') {
419 var mapped = navigationPreload.map(new URL(event.request.url), event.request);
420
421 if (mapped) {
422 storePreloadedResponse(mapped, event);
423 }
424 }
425 }
426
427 // Temporary in-memory store for faster access
428 var navigationPreloadStore = new Map();
429
430 function storePreloadedResponse(_url, event) {
431 var url = new URL(_url, location);
432 var preloadResponsePromise = event.preloadResponse;
433
434 navigationPreloadStore.set(preloadResponsePromise, {
435 url: url,
436 response: preloadResponsePromise
437 });
438
439 var isSamePreload = function isSamePreload() {
440 return navigationPreloadStore.has(preloadResponsePromise);
441 };
442
443 var storing = preloadResponsePromise.then(function (res) {
444 // Return if preload isn't enabled or hasn't happened
445 if (!res) return;
446
447 // If navigationPreloadStore already consumed
448 // or navigationPreloadStore already contains another preload,
449 // then do not store anything and return
450 if (!isSamePreload()) {
451 return;
452 }
453
454 var clone = res.clone();
455
456 // Storing the preload response for later consume (hasn't yet been consumed)
457 return caches.open(PRELOAD_CACHE_NAME).then(function (cache) {
458 if (!isSamePreload()) return;
459
460 return cache.put(url, clone).then(function () {
461 if (!isSamePreload()) {
462 return caches.open(PRELOAD_CACHE_NAME).then(function (cache) {
463 return cache['delete'](url);
464 });
465 }
466 });
467 });
468 });
469
470 event.waitUntil(storing);
471 }
472
473 function retriveInMemoryPreloadedResponse(url) {
474 if (!navigationPreloadStore) {
475 return;
476 }
477
478 var foundResponse = undefined;
479 var foundKey = undefined;
480
481 navigationPreloadStore.forEach(function (store, key) {
482 if (store.url.href === url.href) {
483 foundResponse = store.response;
484 foundKey = key;
485 }
486 });
487
488 if (foundResponse) {
489 navigationPreloadStore['delete'](foundKey);
490 return foundResponse;
491 }
492 }
493
494 function retrivePreloadedResponse(event) {
495 var url = new URL(event.request.url);
496
497 if (self.registration.navigationPreload && navigationPreload && navigationPreload.test && navigationPreload.test(url, event.request)) {} else {
498 return;
499 }
500
501 var fromMemory = retriveInMemoryPreloadedResponse(url);
502 var request = event.request;
503
504 if (fromMemory) {
505 event.waitUntil(caches.open(PRELOAD_CACHE_NAME).then(function (cache) {
506 return cache['delete'](request);
507 }));
508
509 return fromMemory;
510 }
511
512 return cachesMatch(request, PRELOAD_CACHE_NAME).then(function (response) {
513 if (response) {
514 event.waitUntil(caches.open(PRELOAD_CACHE_NAME).then(function (cache) {
515 return cache['delete'](request);
516 }));
517 }
518
519 return response || fetch(event.request);
520 });
521 }
522
523 function mapAssets() {
524 Object.keys(assets).forEach(function (key) {
525 assets[key] = assets[key].map(function (path) {
526 var url = new URL(path, location);
527
528 url.hash = '';
529
530 if (externals.indexOf(path) === -1) {
531 url.search = '';
532 }
533
534 return url.toString();
535 });
536 });
537
538 hashesMap = Object.keys(hashesMap).reduce(function (result, hash) {
539 var url = new URL(hashesMap[hash], location);
540 url.search = '';
541 url.hash = '';
542
543 result[hash] = url.toString();
544 return result;
545 }, {});
546
547 externals = externals.map(function (path) {
548 var url = new URL(path, location);
549 url.hash = '';
550
551 return url.toString();
552 });
553 }
554
555 function addAllNormalized(cache, requests, options) {
556 var bustValue = options.bust;
557 var failAll = options.failAll !== false;
558 var deleteFirst = options.deleteFirst === true;
559 var requestInit = options.request || {
560 credentials: 'omit',
561 mode: 'cors'
562 };
563
564 var deleting = Promise.resolve();
565
566 if (deleteFirst) {
567 deleting = Promise.all(requests.map(function (request) {
568 return cache['delete'](request)['catch'](function () {});
569 }));
570 }
571
572 return Promise.all(requests.map(function (request) {
573 if (bustValue) {
574 request = applyCacheBust(request, bustValue);
575 }
576
577 return fetch(request, requestInit).then(fixRedirectedResponse).then(function (response) {
578 if (!response.ok) {
579 return { error: true };
580 }
581
582 return { response: response };
583 }, function () {
584 return { error: true };
585 });
586 })).then(function (responses) {
587 if (failAll && responses.some(function (data) {
588 return data.error;
589 })) {
590 return Promise.reject(new Error('Wrong response status'));
591 }
592
593 if (!failAll) {
594 responses = responses.filter(function (data) {
595 return !data.error;
596 });
597 }
598
599 return deleting.then(function () {
600 var addAll = responses.map(function (_ref, i) {
601 var response = _ref.response;
602
603 return cache.put(requests[i], response);
604 });
605
606 return Promise.all(addAll);
607 });
608 });
609 }
610
611 function matchCacheMap(request) {
612 var urlString = request.url;
613 var url = new URL(urlString);
614
615 var requestType = undefined;
616
617 if (isNavigateRequest(request)) {
618 requestType = 'navigate';
619 } else if (url.origin === location.origin) {
620 requestType = 'same-origin';
621 } else {
622 requestType = 'cross-origin';
623 }
624
625 for (var i = 0; i < cacheMaps.length; i++) {
626 var map = cacheMaps[i];
627
628 if (!map) continue;
629 if (map.requestTypes && map.requestTypes.indexOf(requestType) === -1) {
630 continue;
631 }
632
633 var newString = undefined;
634
635 if (typeof map.match === 'function') {
636 newString = map.match(url, request);
637 } else {
638 newString = urlString.replace(map.match, map.to);
639 }
640
641 if (newString && newString !== urlString) {
642 return newString;
643 }
644 }
645 }
646
647 function fetchWithPreload(event) {
648 if (!event.preloadResponse || navigationPreload !== true) {
649 return fetch(event.request);
650 }
651
652 return event.preloadResponse.then(function (response) {
653 return response || fetch(event.request);
654 });
655 }
656}
657
658function cachesMatch(request, cacheName) {
659 return caches.match(request, {
660 cacheName: cacheName
661 }).then(function (response) {
662 if (isNotRedirectedResponse(response)) {
663 return response;
664 }
665
666 // Fix already cached redirected responses
667 return fixRedirectedResponse(response).then(function (fixedResponse) {
668 return caches.open(cacheName).then(function (cache) {
669 return cache.put(request, fixedResponse);
670 }).then(function () {
671 return fixedResponse;
672 });
673 });
674 })
675 // Return void if error happened (cache not found)
676 ['catch'](function () {});
677}
678
679function applyCacheBust(asset, key) {
680 var hasQuery = asset.indexOf('?') !== -1;
681 return asset + (hasQuery ? '&' : '?') + '__uncache=' + encodeURIComponent(key);
682}
683
684function isNavigateRequest(request) {
685 return request.mode === 'navigate' || request.headers.get('Upgrade-Insecure-Requests') || (request.headers.get('Accept') || '').indexOf('text/html') !== -1;
686}
687
688function isNotRedirectedResponse(response) {
689 return !response || !response.redirected || !response.ok || response.type === 'opaqueredirect';
690}
691
692// Based on https://github.com/GoogleChrome/sw-precache/pull/241/files#diff-3ee9060dc7a312c6a822cac63a8c630bR85
693function fixRedirectedResponse(response) {
694 if (isNotRedirectedResponse(response)) {
695 return Promise.resolve(response);
696 }
697
698 var body = 'body' in response ? Promise.resolve(response.body) : response.blob();
699
700 return body.then(function (data) {
701 return new Response(data, {
702 headers: response.headers,
703 status: response.status
704 });
705 });
706}
707
708function copyObject(original) {
709 return Object.keys(original).reduce(function (result, key) {
710 result[key] = original[key];
711 return result;
712 }, {});
713}
714
715function logGroup(title, assets) {
716 console.groupCollapsed('[SW]:', title);
717
718 assets.forEach(function (asset) {
719 console.log('Asset:', asset);
720 });
721
722 console.groupEnd();
723}
\No newline at end of file