UNPKG

22.9 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', {
4 value: true
5});
6
7var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
8
9function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
10
11function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
12
13var _appCache = require('./app-cache');
14
15var _appCache2 = _interopRequireDefault(_appCache);
16
17var _serviceWorker = require('./service-worker');
18
19var _serviceWorker2 = _interopRequireDefault(_serviceWorker);
20
21var _path2 = require('path');
22
23var _path3 = _interopRequireDefault(_path2);
24
25var _url = require('url');
26
27var _url2 = _interopRequireDefault(_url);
28
29var _deepExtend = require('deep-extend');
30
31var _deepExtend2 = _interopRequireDefault(_deepExtend);
32
33var _minimatch = require('minimatch');
34
35var _minimatch2 = _interopRequireDefault(_minimatch);
36
37var _miscUtils = require('./misc/utils');
38
39var _loaderUtils = require('loader-utils');
40
41var _loaderUtils2 = _interopRequireDefault(_loaderUtils);
42
43var _slash = require('slash');
44
45var _slash2 = _interopRequireDefault(_slash);
46
47var _require = require('../package.json');
48
49var pluginVersion = _require.version;
50
51var AUTO_UPDATE_INTERVAL = 3600000;
52
53var hasOwn = ({}).hasOwnProperty;
54var updateStrategies = ['all', 'hash', 'changed'];
55var defaultOptions = {
56 caches: 'all',
57 publicPath: void 0,
58 updateStrategy: 'changed',
59 responseStrategy: 'cache-first',
60 externals: [],
61 excludes: ['**/.*', '**/*.map'],
62 // Hack to have intermediate value, e.g. default one, true and false
63 relativePaths: ':relativePaths:',
64 version: null,
65 autoUpdate: false,
66
67 rewrites: function rewrites(asset) {
68 return asset.replace(/^([\s\S]*?)index.htm(l?)$/, function (match, dir) {
69 if ((0, _miscUtils.isAbsoluteURL)(match)) {
70 return match;
71 }
72
73 return dir || './';
74 });
75 },
76
77 cacheMaps: null,
78
79 ServiceWorker: {
80 output: 'sw.js',
81 entry: _path3['default'].join(__dirname, '../empty-entry.js'),
82 scope: null,
83 events: false,
84 prefetchRequest: {
85 credentials: 'omit',
86 headers: void 0,
87 mode: 'cors',
88 cache: void 0
89 }
90 },
91
92 AppCache: {
93 NETWORK: '*',
94 FALLBACK: null,
95 directory: 'appcache/',
96 caches: ['main'],
97 events: false,
98 disableInstall: false,
99 includeCrossOrigin: false
100 },
101
102 // Needed for testing
103 __tests: {
104 swMetadataOnly: false,
105 ignoreRuntime: false,
106 noVersionDump: false
107 },
108
109 // Not yet used
110 alwaysRevalidate: void 0,
111 preferOnline: void 0,
112 ignoreSearch: ['**']
113};
114
115var OfflinePlugin = (function () {
116 function OfflinePlugin(options) {
117 var _this = this;
118
119 _classCallCheck(this, OfflinePlugin);
120
121 this.options = (0, _deepExtend2['default'])({}, defaultOptions, options);
122 this.hash = null;
123 this.assets = null;
124 this.hashesMap = null;
125 this.externals = null;
126 this.publicPath = this.options.publicPath;
127 this.strategy = this.options.updateStrategy;
128 this.responseStrategy = this.options.responseStrategy;
129 this.relativePaths = this.options.relativePaths;
130 this.pluginVersion = pluginVersion;
131 this.loaders = {};
132 this.warnings = [];
133 this.errors = [];
134
135 this.__tests = this.options.__tests;
136 this.flags = {};
137
138 if (this.__tests.pluginVersion) {
139 this.pluginVersion = this.__tests.pluginVersion;
140 }
141
142 var autoUpdate = this.options.autoUpdate;
143
144 if (autoUpdate === true) {
145 this.autoUpdate = AUTO_UPDATE_INTERVAL;
146 } else if (typeof autoUpdate === 'number' && autoUpdate) {
147 this.autoUpdate = autoUpdate;
148 }
149
150 if (this.options.responseStrategy !== "cache-first" && this.options.responseStrategy !== "network-first") {
151 throw new Error('OfflinePlugin: `responseStrategy` option must use ' + '`cache-first` or `network-first` (or be undefined).');
152 }
153
154 if (typeof this.publicPath !== 'string') {
155 this.publicPath = null;
156 }
157
158 if (updateStrategies.indexOf(this.strategy) === -1) {
159 throw new Error('Update strategy must be one of [' + updateStrategies + ']');
160 } else if (this.strategy === 'hash') {
161 this.warnings.push(new Error('OfflinePlugin: `hash` update strategy is deprecated, use `all` strategy and { version: "[hash]" } instead'));
162
163 this.strategy = 'all';
164 this.options.version = '[hash]';
165 }
166
167 if (!Array.isArray(this.options.externals)) {
168 this.options.externals = [];
169 }
170
171 var rewrites = this.options.rewrites || defaultOptions.rewrites;
172
173 if (typeof rewrites === 'function') {
174 this.rewrite = function (asset) {
175 if (asset.indexOf(_this.entryPrefix) === 0) {
176 return '';
177 }
178
179 return rewrites(asset);
180 };
181 } else {
182 this.rewrite = function (asset) {
183 if (asset.indexOf(_this.entryPrefix) === 0) {
184 return '';
185 }
186
187 if (!hasOwn.call(rewrites, asset)) {
188 return asset;
189 }
190
191 return rewrites[asset];
192 };
193 }
194
195 this.cacheMaps = this.stringifyCacheMaps(this.options.cacheMaps);
196
197 this.REST_KEY = ':rest:';
198 this.EXTERNALS_KEY = ':externals:';
199 this.entryPrefix = '__offline_';
200 this.tools = {};
201
202 this.addTool(_serviceWorker2['default'], 'ServiceWorker');
203 this.addTool(_appCache2['default'], 'AppCache');
204
205 if (!Object.keys(this.tools).length) {
206 throw new Error('You should have at least one cache service to be specified');
207 }
208 }
209
210 _createClass(OfflinePlugin, [{
211 key: 'apply',
212 value: function apply(compiler) {
213 var _this2 = this;
214
215 var runtimePath = _path3['default'].resolve(__dirname, '../runtime.js');
216 var compilerOptions = compiler.options;
217
218 this.options.externals = this.extractLoaders(this.options.externals);
219
220 if (this.relativePaths === true) {
221 this.publicPath = null;
222 }
223
224 if (typeof this.publicPath !== 'string' && compilerOptions && compilerOptions.output && compilerOptions.output.publicPath && this.relativePaths !== true) {
225 this.publicPath = compilerOptions.output.publicPath;
226 this.relativePaths = false;
227 }
228
229 if (this.publicPath) {
230 this.publicPath = this.publicPath.replace(/\/$/, '') + '/';
231 }
232
233 if (this.relativePaths === true && this.publicPath) {
234 this.errors.push(new Error('OfflinePlugin: `publicPath` is used in conjunction with `relativePaths`,\n' + 'choose one of it'));
235
236 this.relativePaths = false;
237 }
238
239 if (this.relativePaths === defaultOptions.relativePaths) {
240 this.relativePaths = !this.publicPath;
241 }
242
243 this.useTools(function (tool, key) {
244 _this2.resolveToolPaths(tool, key, compiler);
245 });
246
247 compiler.plugin('normal-module-factory', function (nmf) {
248 nmf.plugin('after-resolve', function (result, callback) {
249 var resource = _path3['default'].resolve(compiler.context, result.resource);
250
251 if (resource !== runtimePath) {
252 return callback(null, result);
253 }
254
255 var data = {
256 autoUpdate: _this2.autoUpdate
257 };
258
259 _this2.useTools(function (tool, key) {
260 data[key] = tool.getConfig(_this2);
261 });
262
263 result.loaders.push(_path3['default'].join(__dirname, 'misc/runtime-loader.js') + '?' + JSON.stringify(data));
264
265 callback(null, result);
266 });
267 });
268
269 compiler.plugin('make', function (compilation, callback) {
270 if (_this2.warnings.length) {
271 [].push.apply(compilation.warnings, _this2.warnings);
272 }
273
274 if (_this2.errors.length) {
275 [].push.apply(compilation.errors, _this2.errors);
276 }
277
278 _this2.useTools(function (tool) {
279 return tool.addEntry(_this2, compilation, compiler);
280 }).then(function () {
281 callback();
282 }, function () {
283 throw new Error('Something went wrong');
284 });
285 });
286
287 compiler.plugin('emit', function (compilation, callback) {
288 var runtimeTemplatePath = _path3['default'].resolve(__dirname, '../tpls/runtime-template.js');
289
290 if (compilation.fileDependencies.indexOf(runtimeTemplatePath) === -1 && !_this2.__tests.ignoreRuntime) {
291 compilation.errors.push(new Error('OfflinePlugin: Plugin\'s runtime wasn\'t added to one of your bundle entries. See this https://goo.gl/YwewYp for details.'));
292 callback();
293 return;
294 }
295
296 var stats = compilation.getStats().toJson();
297
298 // By some reason errors raised here are not fatal,
299 // so we need manually try..catch and exit with error
300 try {
301 _this2.setAssets(compilation);
302 _this2.setHashesMap(compilation);
303
304 // Generate bundle hash manually (from what we have)
305 _this2.hash = _loaderUtils2['default'].getHashDigest(Object.keys(_this2.hashesMap).join(''), 'sha1');
306
307 // Not used yet
308 // this.setNetworkOptions();
309 } catch (e) {
310 callback(e);
311 return;
312 }
313
314 _this2.useTools(function (tool) {
315 return tool.apply(_this2, compilation, compiler);
316 }).then(function () {
317 callback();
318 }, function () {
319 callback(new Error('Something went wrong'));
320 });
321 });
322 }
323 }, {
324 key: 'setAssets',
325 value: function setAssets(compilation) {
326 var _this3 = this;
327
328 var caches = this.options.caches || defaultOptions.caches;
329
330 if (this.options.safeToUseOptionalCaches !== true && (caches.additional && caches.additional.length || caches.optional && caches.optional.length)) {
331 compilation.warnings.push(new Error('OfflinePlugin: Cache sections `additional` and `optional` could be used ' + 'only when each asset passed to it has unique name (e.g. hash or version in it) and ' + 'is permanently available for given URL. If you think that it\' your case, ' + 'set `safeToUseOptionalCaches` option to `true`, to remove this warning.'));
332 }
333
334 var excludes = this.options.excludes;
335 var assets = Object.keys(compilation.assets);
336 var externals = this.options.externals;
337
338 if (Array.isArray(excludes) && excludes.length) {
339 assets = assets.filter(function (asset) {
340 if (excludes.some(function (glob) {
341 if ((0, _minimatch2['default'])(asset, glob)) {
342 return true;
343 }
344 })) {
345 return false;
346 }
347
348 return true;
349 });
350 }
351
352 this.externals = this.validatePaths(externals);
353
354 if (caches === 'all') {
355 this.assets = this.validatePaths(assets).concat(this.externals);
356 this.caches = {
357 main: this.assets.concat()
358 };
359 } else {
360 (function () {
361 var restSection = undefined;
362 var externalsSection = undefined;
363
364 var handledCaches = ['main', 'additional', 'optional'].reduce(function (result, key) {
365 var cache = Array.isArray(caches[key]) ? caches[key] : [];
366
367 if (!cache.length) {
368 result[key] = cache;
369 return result;
370 }
371
372 var cacheResult = [];
373
374 cache.some(function (cacheKey) {
375 if (cacheKey === _this3.REST_KEY) {
376 if (restSection) {
377 throw new Error('The ' + _this3.REST_KEY + ' keyword can be used only once');
378 }
379
380 restSection = key;
381 return;
382 }
383
384 if (cacheKey === _this3.EXTERNALS_KEY) {
385 if (externalsSection) {
386 throw new Error('The ' + _this3.EXTERNALS_KEY + ' keyword can be used only once');
387 }
388
389 externalsSection = key;
390 return;
391 }
392
393 var magic = undefined;
394
395 if (!(0, _miscUtils.isAbsoluteURL)(cacheKey) && cacheKey[0] !== '/' && cacheKey.indexOf('./') !== 0 && (magic = (0, _miscUtils.hasMagic)(cacheKey))) {
396 var matched = undefined;
397
398 for (var i = 0, len = assets.length; i < len; i++) {
399 if (!magic.match(assets[i])) continue;
400
401 matched = true;
402 cacheResult.push(assets[i]);
403 assets.splice(i, 1);
404 i--, len--;
405 }
406
407 if (!matched) {
408 compilation.warnings.push(new Error('OfflinePlugin: Cache pattern [' + cacheKey + '] did not match any assets'));
409 }
410
411 return;
412 }
413
414 var index = assets.indexOf(cacheKey);
415
416 __EXTERNALS_CHECK: if (index === -1) {
417 var externalsIndex = externals.indexOf(cacheKey);
418
419 if (externalsIndex !== -1) {
420 externals.splice(externalsIndex, 1);
421 break __EXTERNALS_CHECK;
422 }
423
424 compilation.warnings.push(new Error('OfflinePlugin: Cache asset [' + cacheKey + '] is not found in the output assets,' + 'if it\'s an external asset, put it to the |externals| option to remove this warning'));
425 } else {
426 assets.splice(index, 1);
427 }
428
429 cacheResult.push(cacheKey);
430 });
431
432 result[key] = _this3.validatePaths(cacheResult);
433
434 return result;
435 }, {});
436
437 if (restSection && assets.length) {
438 handledCaches[restSection] = handledCaches[restSection].concat(_this3.validatePaths(assets));
439 }
440
441 if (externalsSection && externals.length) {
442 handledCaches[externalsSection] = handledCaches[externalsSection].concat(_this3.validatePaths(externals));
443 }
444
445 _this3.caches = handledCaches;
446 _this3.assets = [].concat(_this3.caches.main, _this3.caches.additional, _this3.caches.optional);
447 })();
448 }
449
450 Object.keys(this.loaders).forEach(function (loader) {
451 _this3.loaders[loader] = _this3.validatePaths(_this3.loaders[loader]);
452 });
453 }
454 }, {
455 key: 'setHashesMap',
456 value: function setHashesMap(compilation) {
457 var _this4 = this;
458
459 this.hashesMap = {};
460
461 Object.keys(compilation.assets).forEach(function (key) {
462 var validatedPath = _this4.validatePaths([key])[0];
463
464 if (typeof validatedPath !== 'string' || _this4.assets.indexOf(validatedPath) === -1) return;
465
466 var hash = _loaderUtils2['default'].getHashDigest(compilation.assets[key].source(), 'sha1');
467
468 _this4.hashesMap[hash] = validatedPath;
469 });
470 }
471 }, {
472 key: 'setNetworkOptions',
473 value: function setNetworkOptions() {
474 var alwaysRevalidate = this.options.alwaysRevalidate;
475 var preferOnline = this.options.preferOnline;
476 var ignoreSearch = this.options.ignoreSearch;
477
478 var assets = this.assets;
479
480 // Disable temporarily
481 if (Array.isArray(alwaysRevalidate) && alwaysRevalidate.length) {
482 alwaysRevalidate = assets.filter(function (asset) {
483 if (alwaysRevalidate.some(function (glob) {
484 if ((0, _minimatch2['default'])(asset, glob)) {
485 return true;
486 }
487 })) {
488 return true;
489 }
490
491 return false;
492 });
493
494 if (alwaysRevalidate.length) {
495 this.alwaysRevalidate = alwaysRevalidate;
496 }
497 }
498
499 if (Array.isArray(ignoreSearch) && ignoreSearch.length) {
500 ignoreSearch = assets.filter(function (asset) {
501 if (ignoreSearch.some(function (glob) {
502 if ((0, _minimatch2['default'])(asset, glob)) {
503 return true;
504 }
505 })) {
506 return true;
507 }
508
509 return false;
510 });
511
512 if (ignoreSearch.length) {
513 this.ignoreSearch = ignoreSearch;
514 }
515 }
516
517 if (Array.isArray(preferOnline) && preferOnline.length) {
518 preferOnline = assets.filter(function (asset) {
519 if (preferOnline.some(function (glob) {
520 if ((0, _minimatch2['default'])(asset, glob)) {
521 return true;
522 }
523 })) {
524 return true;
525 }
526
527 return false;
528 });
529
530 if (preferOnline.length) {
531 this.preferOnline = preferOnline;
532 }
533 }
534 }
535 }, {
536 key: 'extractLoaders',
537 value: function extractLoaders(assets) {
538 var _this5 = this;
539
540 var R_LOADER = /^([^\s]+?):(\/\/)?/;
541
542 return assets.map(function (asset) {
543 var loaderMatch = asset.match(R_LOADER);
544
545 if (loaderMatch && !loaderMatch[2]) {
546 asset = asset.slice(loaderMatch[0].length);
547
548 var loaderName = loaderMatch[1];
549 var loader = _this5.loaders[loaderName];
550
551 if (!loader) {
552 loader = _this5.loaders[loaderName] = [];
553 }
554
555 loader.push(asset);
556 }
557
558 return asset;
559 });
560 }
561 }, {
562 key: 'stringifyCacheMaps',
563 value: function stringifyCacheMaps(cacheMaps) {
564 if (!cacheMaps) {
565 return [];
566 }
567
568 return cacheMaps.map(function (map) {
569 if (map.to != null && typeof map.to !== 'string' && typeof map.to !== 'function') {
570 throw new Error('cacheMaps `to` property must either string, function, undefined or null');
571 }
572
573 if (map.requestTypes != null) {
574 if (Array.isArray(map.requestTypes)) {
575 var types = map.requestTypes.filter(function (item) {
576 if (item === 'navigate' || item === 'same-origin' || item === 'cross-origin') {
577 return false;
578 }
579
580 return true;
581 });
582
583 if (types.length) {
584 throw new Error("cacheMaps `requestTypes` array values could be only: 'navigate', 'same-origin' or 'cross-origin'");
585 }
586 } else {
587 throw new Error('cacheMaps `requestTypes` property must either array, undefined or null');
588 }
589 }
590
591 var to = undefined;
592
593 if (typeof map.to === 'function') {
594 to = map.to + '';
595 } else {
596 to = map.to ? JSON.stringify(map.to) : null;
597 }
598
599 return {
600 match: map.match + '',
601 to: to,
602 requestTypes: map.requestTypes || null
603 };
604 });
605 }
606 }, {
607 key: 'resolveToolPaths',
608 value: function resolveToolPaths(tool, key, compiler) {
609 // Tool much implement:
610 //
611 // tool.output
612 // tool.publicPath
613 // tool.basePath
614 // tool.location
615 // tool.pathRewrite
616
617 if (!this.relativePaths && !this.publicPath) {
618 throw new Error('OfflinePlugin: Cannot generate base path for ' + key);
619 }
620
621 var isDirectory = tool.output[tool.output.length - 1] === '/';
622
623 if (this.relativePaths) {
624 var compilerOutput = (compiler.options.output || { path: process.cwd() }).path;
625 var absoluteOutput = _path3['default'].resolve(compilerOutput, tool.output);
626
627 var relativeBase = undefined;
628
629 if (isDirectory) {
630 relativeBase = _path3['default'].relative(absoluteOutput, compilerOutput);
631 } else {
632 relativeBase = _path3['default'].relative(_path3['default'].dirname(absoluteOutput), compilerOutput);
633 }
634
635 relativeBase = (0, _slash2['default'])(relativeBase);
636 relativeBase = relativeBase.replace(/\/$/, '');
637
638 if (relativeBase) {
639 relativeBase = relativeBase + '/';
640 }
641
642 tool.basePath = relativeBase[0] === '.' ? relativeBase : './' + relativeBase;
643 } else if (this.publicPath) {
644 tool.basePath = this.publicPath.replace(/\/$/, '') + '/';
645 }
646
647 if (this.relativePaths) {
648 tool.location = tool.output;
649 } else if (this.publicPath && tool.publicPath) {
650 tool.location = tool.publicPath;
651 } else if (this.publicPath) {
652 var publicUrl = _url2['default'].parse(this.publicPath);
653 var publicPath = publicUrl.pathname;
654
655 publicUrl.pathname = _path3['default'].join(publicPath, tool.output);
656 var outerPathname = _path3['default'].join('/outer/', publicPath, tool.output);
657
658 if (outerPathname.indexOf('/outer/') !== 0) {
659 new Error('OfflinePlugin: Wrong ' + key + '.output value. Final ' + key + '.location URL path bounds are outside of publicPath');
660 }
661
662 tool.location = _url2['default'].format(publicUrl);
663 }
664
665 if (this.relativePaths) {
666 tool.pathRewrite = function (_path) {
667 if ((0, _miscUtils.isAbsoluteURL)(_path) || _path[0] === '/') {
668 return _path;
669 }
670
671 return tool.basePath + _path;
672 };
673 } else {
674 tool.pathRewrite = function (path) {
675 return path;
676 };
677 }
678 }
679 }, {
680 key: 'validatePaths',
681 value: function validatePaths(assets) {
682 var _this6 = this;
683
684 return assets.map(this.rewrite).filter(function (asset) {
685 return !!asset;
686 }).map(function (key) {
687 // If absolute url, use it as is
688 if ((0, _miscUtils.isAbsoluteURL)(key)) {
689 return key;
690 }
691
692 if (_this6.relativePaths) {
693 return key.replace(/^\.\//, '');
694 }
695
696 // Absolute path, use it as is
697 if (key[0] === '/') {
698 return key;
699 }
700
701 return _this6.publicPath + key.replace(/^\.?\//, '');
702 });
703 }
704 }, {
705 key: 'useTools',
706 value: function useTools(fn) {
707 var _this7 = this;
708
709 var tools = Object.keys(this.tools).map(function (tool) {
710 return fn(_this7.tools[tool], tool);
711 });
712
713 return Promise.all(tools);
714 }
715 }, {
716 key: 'addTool',
717 value: function addTool(Tool, name) {
718 var options = this.options[name];
719
720 if (options === null || options === false) {
721 // tool is not needed
722 return;
723 }
724
725 this.tools[name] = new Tool(options);
726 }
727 }, {
728 key: 'version',
729 get: function get() {
730 var version = this.options.version;
731 var hash = this.hash;
732
733 if (version == null) {
734 return new Date().toLocaleString();
735 }
736
737 if (typeof version === 'function') {
738 return version(this);
739 }
740
741 return (0, _miscUtils.interpolateString)(version, { hash: hash });
742 }
743 }]);
744
745 return OfflinePlugin;
746})();
747
748exports['default'] = OfflinePlugin;
749
750OfflinePlugin.defaultOptions = defaultOptions;
751module.exports = exports['default'];
\No newline at end of file