UNPKG

16.1 kBJavaScriptView Raw
1'use strict';
2
3if (typeof DEBUG === 'undefined') {
4 var DEBUG = false;
5}
6
7function WebpackServiceWorker(params, helpers) {
8 var loaders = helpers.loaders;
9 var cacheMaps = helpers.cacheMaps;
10
11 var strategy = params.strategy;
12 var responseStrategy = params.responseStrategy;
13
14 var assets = params.assets;
15 var loadersMap = params.loaders || {};
16
17 var hashesMap = params.hashesMap;
18 var externals = params.externals;
19
20 // Not used yet
21 // const alwaysRevalidate = params.alwaysRevalidate;
22 // const ignoreSearch = params.ignoreSearch;
23 // const preferOnline = params.preferOnline;
24
25 var CACHE_PREFIX = params.name;
26 var CACHE_TAG = params.version;
27 var CACHE_NAME = CACHE_PREFIX + ':' + CACHE_TAG;
28
29 var STORED_DATA_KEY = '__offline_webpack__data';
30
31 mapAssets();
32
33 var allAssets = [].concat(assets.main, assets.additional, assets.optional);
34 var navigateFallbackURL = params.navigateFallbackURL;
35
36 self.addEventListener('install', function (event) {
37 console.log('[SW]:', 'Install event');
38
39 var installing = undefined;
40
41 if (strategy === 'changed') {
42 installing = cacheChanged('main');
43 } else {
44 installing = cacheAssets('main');
45 }
46
47 event.waitUntil(installing);
48 });
49
50 self.addEventListener('activate', function (event) {
51 console.log('[SW]:', 'Activate event');
52
53 var activation = cacheAdditional();
54
55 // Delete all assets which name starts with CACHE_PREFIX and
56 // is not current cache (CACHE_NAME)
57 activation = activation.then(storeCacheData);
58 activation = activation.then(deleteObsolete);
59 activation = activation.then(function () {
60 if (self.clients && self.clients.claim) {
61 return self.clients.claim();
62 }
63 });
64
65 event.waitUntil(activation);
66 });
67
68 function cacheAdditional() {
69 if (!assets.additional.length) {
70 return Promise.resolve();
71 }
72
73 if (DEBUG) {
74 console.log('[SW]:', 'Caching additional');
75 }
76
77 var operation = undefined;
78
79 if (strategy === 'changed') {
80 operation = cacheChanged('additional');
81 } else {
82 operation = cacheAssets('additional');
83 }
84
85 // Ignore fail of `additional` cache section
86 return operation['catch'](function (e) {
87 console.error('[SW]:', 'Cache section `additional` failed to load');
88 });
89 }
90
91 function cacheAssets(section) {
92 var batch = assets[section];
93
94 return caches.open(CACHE_NAME).then(function (cache) {
95 return addAllNormalized(cache, batch, {
96 bust: params.version,
97 request: params.prefetchRequest
98 });
99 }).then(function () {
100 logGroup('Cached assets: ' + section, batch);
101 })['catch'](function (e) {
102 console.error(e);
103 throw e;
104 });
105 }
106
107 function cacheChanged(section) {
108 return getLastCache().then(function (args) {
109 if (!args) {
110 return cacheAssets(section);
111 }
112
113 var lastCache = args[0];
114 var lastKeys = args[1];
115 var lastData = args[2];
116
117 var lastMap = lastData.hashmap;
118 var lastVersion = lastData.version;
119
120 if (!lastData.hashmap || lastVersion === params.version) {
121 return cacheAssets(section);
122 }
123
124 var lastHashedAssets = Object.keys(lastMap).map(function (hash) {
125 return lastMap[hash];
126 });
127
128 var lastUrls = lastKeys.map(function (req) {
129 var url = new URL(req.url);
130 url.search = '';
131
132 return url.toString();
133 });
134
135 var sectionAssets = assets[section];
136 var moved = [];
137 var changed = sectionAssets.filter(function (url) {
138 if (lastUrls.indexOf(url) === -1 || lastHashedAssets.indexOf(url) === -1) {
139 return true;
140 }
141
142 return false;
143 });
144
145 Object.keys(hashesMap).forEach(function (hash) {
146 var asset = hashesMap[hash];
147
148 // Return if not in sectionAssets or in changed or moved array
149 if (sectionAssets.indexOf(asset) === -1 || changed.indexOf(asset) !== -1 || moved.indexOf(asset) !== -1) return;
150
151 var lastAsset = lastMap[hash];
152
153 if (lastAsset && lastUrls.indexOf(lastAsset) !== -1) {
154 moved.push([lastAsset, asset]);
155 } else {
156 changed.push(asset);
157 }
158 });
159
160 logGroup('Changed assets: ' + section, changed);
161 logGroup('Moved assets: ' + section, moved);
162
163 var movedResponses = Promise.all(moved.map(function (pair) {
164 return lastCache.match(pair[0]).then(function (response) {
165 return [pair[1], response];
166 });
167 }));
168
169 return caches.open(CACHE_NAME).then(function (cache) {
170 var move = movedResponses.then(function (responses) {
171 return Promise.all(responses.map(function (pair) {
172 return cache.put(pair[0], pair[1]);
173 }));
174 });
175
176 return Promise.all([move, addAllNormalized(cache, changed, {
177 bust: params.version,
178 request: params.prefetchRequest
179 })]);
180 });
181 });
182 }
183
184 function deleteObsolete() {
185 return caches.keys().then(function (keys) {
186 var all = keys.map(function (key) {
187 if (key.indexOf(CACHE_PREFIX) !== 0 || key.indexOf(CACHE_NAME) === 0) return;
188
189 console.log('[SW]:', 'Delete cache:', key);
190 return caches['delete'](key);
191 });
192
193 return Promise.all(all);
194 });
195 }
196
197 function getLastCache() {
198 return caches.keys().then(function (keys) {
199 var index = keys.length;
200 var key = undefined;
201
202 while (index--) {
203 key = keys[index];
204
205 if (key.indexOf(CACHE_PREFIX) === 0) {
206 break;
207 }
208 }
209
210 if (!key) return;
211
212 var cache = undefined;
213
214 return caches.open(key).then(function (_cache) {
215 cache = _cache;
216 return _cache.match(new URL(STORED_DATA_KEY, location).toString());
217 }).then(function (response) {
218 if (!response) return;
219
220 return Promise.all([cache, cache.keys(), response.json()]);
221 });
222 });
223 }
224
225 function storeCacheData() {
226 return caches.open(CACHE_NAME).then(function (cache) {
227 var data = new Response(JSON.stringify({
228 version: params.version,
229 hashmap: hashesMap
230 }));
231
232 return cache.put(new URL(STORED_DATA_KEY, location).toString(), data);
233 });
234 }
235
236 self.addEventListener('fetch', function (event) {
237 var requestUrl = event.request.url;
238 var url = new URL(requestUrl);
239 var urlString = undefined;
240
241 if (externals.indexOf(requestUrl) !== -1) {
242 urlString = requestUrl;
243 } else {
244 url.search = '';
245 urlString = url.toString();
246 }
247
248 // Handle only GET requests
249 var isGET = event.request.method === 'GET';
250 var assetMatches = allAssets.indexOf(urlString) !== -1;
251 var cacheUrl = urlString;
252
253 if (!assetMatches) {
254 var cacheRewrite = matchCacheMap(event.request);
255
256 if (cacheRewrite) {
257 cacheUrl = cacheRewrite;
258 assetMatches = true;
259 }
260 }
261
262 if (!assetMatches && isGET) {
263 // If isn't a cached asset and is a navigation request,
264 // fallback to navigateFallbackURL if available
265 if (navigateFallbackURL && isNavigateRequest(event.request)) {
266 event.respondWith(handleNavigateFallback(fetch(event.request)));
267
268 return;
269 }
270 }
271
272 if (!assetMatches || !isGET) {
273 // Fix for https://twitter.com/wanderview/status/696819243262873600
274 if (url.origin !== location.origin && navigator.userAgent.indexOf('Firefox/44.') !== -1) {
275 event.respondWith(fetch(event.request));
276 }
277
278 return;
279 }
280
281 // Logic of caching / fetching is here
282 // * urlString -- url to match from the CACHE_NAME
283 // * event.request -- original Request to perform fetch() if necessary
284 var resource = undefined;
285
286 if (responseStrategy === "network-first") {
287 resource = networkFirstResponse(event, urlString, cacheUrl);
288 }
289 // "cache-first"
290 // (responseStrategy has been validated before)
291 else {
292 resource = cacheFirstResponse(event, urlString, cacheUrl);
293 }
294
295 if (navigateFallbackURL && isNavigateRequest(event.request)) {
296 resource = handleNavigateFallback(resource);
297 }
298
299 event.respondWith(resource);
300 });
301
302 self.addEventListener('message', function (e) {
303 var data = e.data;
304 if (!data) return;
305
306 switch (data.action) {
307 case 'skipWaiting':
308 {
309 if (self.skipWaiting) self.skipWaiting();
310 }break;
311 }
312 });
313
314 function cacheFirstResponse(event, urlString, cacheUrl) {
315 return cachesMatch(cacheUrl, CACHE_NAME).then(function (response) {
316 if (response) {
317 if (DEBUG) {
318 console.log('[SW]:', 'URL [' + cacheUrl + '](' + urlString + ') from cache');
319 }
320
321 return response;
322 }
323
324 // Load and cache known assets
325 var fetching = fetch(event.request).then(function (response) {
326 if (!response.ok) {
327 if (DEBUG) {
328 console.log('[SW]:', 'URL [' + urlString + '] wrong response: [' + response.status + '] ' + response.type);
329 }
330
331 return response;
332 }
333
334 if (DEBUG) {
335 console.log('[SW]:', 'URL [' + urlString + '] from network');
336 }
337
338 if (cacheUrl === urlString) {
339 (function () {
340 var responseClone = response.clone();
341
342 caches.open(CACHE_NAME).then(function (cache) {
343 return cache.put(urlString, responseClone);
344 }).then(function () {
345 console.log('[SW]:', 'Cache asset: ' + urlString);
346 });
347 })();
348 }
349
350 return response;
351 });
352
353 return fetching;
354 });
355 }
356
357 function networkFirstResponse(event, urlString, cacheUrl) {
358 return fetch(event.request).then(function (response) {
359 if (response.ok) {
360 if (DEBUG) {
361 console.log('[SW]:', 'URL [' + urlString + '] from network');
362 }
363
364 return response;
365 }
366
367 // throw to reach the code in the catch below
368 throw new Error("response is not ok");
369 })
370 // this needs to be in a catch() and not just in the then() above
371 // cause if your network is down, the fetch() will throw
372 ['catch'](function () {
373 if (DEBUG) {
374 console.log('[SW]:', 'URL [' + urlString + '] from cache if possible');
375 }
376
377 return cachesMatch(cacheUrl, CACHE_NAME);
378 });
379 }
380
381 function handleNavigateFallback(fetching) {
382 return fetching['catch'](function () {}).then(function (response) {
383 if (!response || !response.ok) {
384 if (DEBUG) {
385 console.log('[SW]:', 'Loading navigation fallback [' + navigateFallbackURL + '] from cache');
386 }
387
388 return cachesMatch(navigateFallbackURL, CACHE_NAME);
389 }
390
391 return response;
392 });
393 }
394
395 function mapAssets() {
396 Object.keys(assets).forEach(function (key) {
397 assets[key] = assets[key].map(function (path) {
398 var url = new URL(path, location);
399
400 if (externals.indexOf(path) === -1) {
401 url.search = '';
402 } else {
403 // Remove hash from possible passed externals
404 url.hash = '';
405 }
406
407 return url.toString();
408 });
409 });
410
411 Object.keys(loadersMap).forEach(function (key) {
412 loadersMap[key] = loadersMap[key].map(function (path) {
413 var url = new URL(path, location);
414
415 if (externals.indexOf(path) === -1) {
416 url.search = '';
417 } else {
418 // Remove hash from possible passed externals
419 url.hash = '';
420 }
421
422 return url.toString();
423 });
424 });
425
426 hashesMap = Object.keys(hashesMap).reduce(function (result, hash) {
427 var url = new URL(hashesMap[hash], location);
428 url.search = '';
429
430 result[hash] = url.toString();
431 return result;
432 }, {});
433
434 externals = externals.map(function (path) {
435 var url = new URL(path, location);
436 url.hash = '';
437
438 return url.toString();
439 });
440 }
441
442 function addAllNormalized(cache, requests, options) {
443 var allowLoaders = options.allowLoaders !== false;
444 var bustValue = options && options.bust;
445 var requestInit = options.request || {
446 credentials: 'omit',
447 mode: 'cors'
448 };
449
450 return Promise.all(requests.map(function (request) {
451 if (bustValue) {
452 request = applyCacheBust(request, bustValue);
453 }
454
455 return fetch(request, requestInit);
456 })).then(function (responses) {
457 if (responses.some(function (response) {
458 return !response.ok;
459 })) {
460 return Promise.reject(new Error('Wrong response status'));
461 }
462
463 var extracted = [];
464 var addAll = responses.map(function (response, i) {
465 if (allowLoaders) {
466 extracted.push(extractAssetsWithLoaders(requests[i], response));
467 }
468
469 return cache.put(requests[i], response);
470 });
471
472 if (extracted.length) {
473 (function () {
474 var newOptions = copyObject(options);
475 newOptions.allowLoaders = false;
476
477 var waitAll = addAll;
478
479 addAll = Promise.all(extracted).then(function (all) {
480 var extractedRequests = [].concat.apply([], all);
481
482 if (requests.length) {
483 waitAll = waitAll.concat(addAllNormalized(cache, extractedRequests, newOptions));
484 }
485
486 return Promise.all(waitAll);
487 });
488 })();
489 } else {
490 addAll = Promise.all(addAll);
491 }
492
493 return addAll;
494 });
495 }
496
497 function extractAssetsWithLoaders(request, response) {
498 var all = Object.keys(loadersMap).map(function (key) {
499 var loader = loadersMap[key];
500
501 if (loader.indexOf(request) !== -1 && loaders[key]) {
502 return loaders[key](response.clone());
503 }
504 }).filter(function (a) {
505 return !!a;
506 });
507
508 return Promise.all(all).then(function (all) {
509 return [].concat.apply([], all);
510 });
511 }
512
513 function matchCacheMap(request) {
514 var urlString = request.url;
515 var url = new URL(urlString);
516
517 var requestType = undefined;
518
519 if (request.mode === 'navigate') {
520 requestType = 'navigate';
521 } else if (url.origin === location.origin) {
522 requestType = 'same-origin';
523 } else {
524 requestType = 'cross-origin';
525 }
526
527 for (var i = 0; i < cacheMaps.length; i++) {
528 var map = cacheMaps[i];
529
530 if (!map) continue;
531 if (map.requestTypes && map.requestTypes.indexOf(requestType) === -1) {
532 continue;
533 }
534
535 var newString = undefined;
536
537 if (typeof map.match === 'function') {
538 newString = map.match(url, request);
539 } else {
540 newString = urlString.replace(map.match, map.to);
541 }
542
543 if (newString && newString !== urlString) {
544 return newString;
545 }
546 }
547 }
548}
549
550function cachesMatch(request, cacheName) {
551 return caches.match(request, {
552 cacheName: cacheName
553 })
554 // Return void if error happened (cache not found)
555 ['catch'](function () {});
556}
557
558function applyCacheBust(asset, key) {
559 var hasQuery = asset.indexOf('?') !== -1;
560 return asset + (hasQuery ? '&' : '?') + '__uncache=' + encodeURIComponent(key);
561}
562
563function getClientsURLs() {
564 if (!self.clients) {
565 return Promise.resolve([]);
566 }
567
568 return self.clients.matchAll({
569 includeUncontrolled: true
570 }).then(function (clients) {
571 if (!clients.length) return [];
572
573 var result = [];
574
575 clients.forEach(function (client) {
576 var url = new URL(client.url);
577 url.search = '';
578 url.hash = '';
579 var urlString = url.toString();
580
581 if (!result.length || result.indexOf(urlString) === -1) {
582 result.push(urlString);
583 }
584 });
585
586 return result;
587 });
588}
589
590function isNavigateRequest(request) {
591 return request.mode === 'navigate' || request.headers.get('Upgrade-Insecure-Requests') || (request.headers.get('Accept') || '').indexOf('text/html') !== -1;
592}
593
594function copyObject(original) {
595 return Object.keys(original).reduce(function (result, key) {
596 result[key] = original[key];
597 return result;
598 }, {});
599}
600
601function logGroup(title, assets) {
602 console.groupCollapsed('[SW]:', title);
603
604 assets.forEach(function (asset) {
605 console.log('Asset:', asset);
606 });
607
608 console.groupEnd();
609}
\No newline at end of file