UNPKG

22.1 kBJavaScriptView Raw
1'use strict';
2var vm = require('vm');
3var fs = require('fs');
4var _ = require('lodash');
5var Promise = require('bluebird');
6var path = require('path');
7var childCompiler = require('./lib/compiler.js');
8var prettyError = require('./lib/errors.js');
9var chunkSorter = require('./lib/chunksorter.js');
10Promise.promisifyAll(fs);
11
12function HtmlWebpackPlugin (options) {
13 // Default options
14 this.options = _.extend({
15 template: path.join(__dirname, 'default_index.ejs'),
16 filename: 'index.html',
17 hash: false,
18 inject: true,
19 compile: true,
20 favicon: false,
21 minify: false,
22 cache: true,
23 showErrors: true,
24 chunks: 'all',
25 excludeChunks: [],
26 title: 'Webpack App',
27 xhtml: false
28 }, options);
29}
30
31HtmlWebpackPlugin.prototype.apply = function (compiler) {
32 var self = this;
33 var isCompilationCached = false;
34 var compilationPromise;
35
36 this.options.template = this.getFullTemplatePath(this.options.template, compiler.context);
37
38 // convert absolute filename into relative so that webpack can
39 // generate it at correct location
40 var filename = this.options.filename;
41 if (path.resolve(filename) === path.normalize(filename)) {
42 this.options.filename = path.relative(compiler.options.output.path, filename);
43 }
44
45 compiler.plugin('make', function (compilation, callback) {
46 // Compile the template (queued)
47 compilationPromise = childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation)
48 .catch(function (err) {
49 compilation.errors.push(prettyError(err, compiler.context).toString());
50 return {
51 content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR',
52 outputName: self.options.filename
53 };
54 })
55 .then(function (compilationResult) {
56 // If the compilation change didnt change the cache is valid
57 isCompilationCached = compilationResult.hash && self.childCompilerHash === compilationResult.hash;
58 self.childCompilerHash = compilationResult.hash;
59 self.childCompilationOutputName = compilationResult.outputName;
60 callback();
61 return compilationResult.content;
62 });
63 });
64
65 compiler.plugin('emit', function (compilation, callback) {
66 var applyPluginsAsyncWaterfall = self.applyPluginsAsyncWaterfall(compilation);
67 // Get all chunks
68 var chunks = self.filterChunks(compilation.getStats().toJson(), self.options.chunks, self.options.excludeChunks);
69 // Sort chunks
70 chunks = self.sortChunks(chunks, self.options.chunksSortMode);
71 // Let plugins alter the chunks and the chunk sorting
72 chunks = compilation.applyPluginsWaterfall('html-webpack-plugin-alter-chunks', chunks, { plugin: self });
73 // Get assets
74 var assets = self.htmlWebpackPluginAssets(compilation, chunks);
75 // If this is a hot update compilation, move on!
76 // This solves a problem where an `index.html` file is generated for hot-update js files
77 // It only happens in Webpack 2, where hot updates are emitted separately before the full bundle
78 if (self.isHotUpdateCompilation(assets)) {
79 return callback();
80 }
81
82 // If the template and the assets did not change we don't have to emit the html
83 var assetJson = JSON.stringify(self.getAssetFiles(assets));
84 if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
85 return callback();
86 } else {
87 self.assetJson = assetJson;
88 }
89
90 Promise.resolve()
91 // Favicon
92 .then(function () {
93 if (self.options.favicon) {
94 return self.addFileToAssets(self.options.favicon, compilation)
95 .then(function (faviconBasename) {
96 var publicPath = compilation.mainTemplate.getPublicPath({hash: compilation.hash}) || '';
97 if (publicPath && publicPath.substr(-1) !== '/') {
98 publicPath += '/';
99 }
100 assets.favicon = publicPath + faviconBasename;
101 });
102 }
103 })
104 // Wait for the compilation to finish
105 .then(function () {
106 return compilationPromise;
107 })
108 .then(function (compiledTemplate) {
109 // Allow to use a custom function / string instead
110 if (self.options.templateContent !== undefined) {
111 return self.options.templateContent;
112 }
113 // Once everything is compiled evaluate the html factory
114 // and replace it with its content
115 return self.evaluateCompilationResult(compilation, compiledTemplate);
116 })
117 // Allow plugins to make changes to the assets before invoking the template
118 // This only makes sense to use if `inject` is `false`
119 .then(function (compilationResult) {
120 return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-generation', false, {
121 assets: assets,
122 outputName: self.childCompilationOutputName,
123 plugin: self
124 })
125 .then(function () {
126 return compilationResult;
127 });
128 })
129 // Execute the template
130 .then(function (compilationResult) {
131 // If the loader result is a function execute it to retrieve the html
132 // otherwise use the returned html
133 return typeof compilationResult !== 'function'
134 ? compilationResult
135 : self.executeTemplate(compilationResult, chunks, assets, compilation);
136 })
137 // Allow plugins to change the html before assets are injected
138 .then(function (html) {
139 var pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
140 return applyPluginsAsyncWaterfall('html-webpack-plugin-before-html-processing', true, pluginArgs);
141 })
142 .then(function (result) {
143 var html = result.html;
144 var assets = result.assets;
145 var chunks = result.chunks;
146 // Prepare script and link tags
147 var assetTags = self.generateAssetTags(assets);
148 var pluginArgs = {head: assetTags.head, body: assetTags.body, plugin: self, chunks: chunks, outputName: self.childCompilationOutputName};
149 // Allow plugins to change the assetTag definitions
150 return applyPluginsAsyncWaterfall('html-webpack-plugin-alter-asset-tags', true, pluginArgs)
151 .then(function (result) {
152 // Add the stylesheets, scripts and so on to the resulting html
153 return self.postProcessHtml(html, assets, { body: result.body, head: result.head })
154 .then(function (html) {
155 return _.extend(result, {html: html, assets: assets});
156 });
157 });
158 })
159 // Allow plugins to change the html after assets are injected
160 .then(function (result) {
161 var html = result.html;
162 var assets = result.assets;
163 var pluginArgs = {html: html, assets: assets, plugin: self, outputName: self.childCompilationOutputName};
164 return applyPluginsAsyncWaterfall('html-webpack-plugin-after-html-processing', true, pluginArgs)
165 .then(function (result) {
166 return result.html;
167 });
168 })
169 .catch(function (err) {
170 // In case anything went wrong the promise is resolved
171 // with the error message and an error is logged
172 compilation.errors.push(prettyError(err, compiler.context).toString());
173 // Prevent caching
174 self.hash = null;
175 return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
176 })
177 .then(function (html) {
178 // Replace the compilation result with the evaluated html code
179 compilation.assets[self.childCompilationOutputName] = {
180 source: function () {
181 return html;
182 },
183 size: function () {
184 return html.length;
185 }
186 };
187 })
188 .then(function () {
189 // Let other plugins know that we are done:
190 return applyPluginsAsyncWaterfall('html-webpack-plugin-after-emit', false, {
191 html: compilation.assets[self.childCompilationOutputName],
192 outputName: self.childCompilationOutputName,
193 plugin: self
194 }).catch(function (err) {
195 console.error(err);
196 return null;
197 }).then(function () {
198 return null;
199 });
200 })
201 // Let webpack continue with it
202 .finally(function () {
203 callback();
204 // Tell blue bird that we don't want to wait for callback.
205 // Fixes "Warning: a promise was created in a handler but none were returned from it"
206 // https://github.com/petkaantonov/bluebird/blob/master/docs/docs/warning-explanations.md#warning-a-promise-was-created-in-a-handler-but-none-were-returned-from-it
207 return null;
208 });
209 });
210};
211
212/**
213 * Evaluates the child compilation result
214 * Returns a promise
215 */
216HtmlWebpackPlugin.prototype.evaluateCompilationResult = function (compilation, source) {
217 if (!source) {
218 return Promise.reject('The child compilation didn\'t provide a result');
219 }
220
221 // The LibraryTemplatePlugin stores the template result in a local variable.
222 // To extract the result during the evaluation this part has to be removed.
223 source = source.replace('var HTML_WEBPACK_PLUGIN_RESULT =', '');
224 var template = this.options.template.replace(/^.+!/, '').replace(/\?.+$/, '');
225 var vmContext = vm.createContext(_.extend({HTML_WEBPACK_PLUGIN: true, require: require}, global));
226 var vmScript = new vm.Script(source, {filename: template});
227 // Evaluate code and cast to string
228 var newSource;
229 try {
230 newSource = vmScript.runInContext(vmContext);
231 } catch (e) {
232 return Promise.reject(e);
233 }
234 return typeof newSource === 'string' || typeof newSource === 'function'
235 ? Promise.resolve(newSource)
236 : Promise.reject('The loader "' + this.options.template + '" didn\'t return html.');
237};
238
239/**
240 * Html post processing
241 *
242 * Returns a promise
243 */
244HtmlWebpackPlugin.prototype.executeTemplate = function (templateFunction, chunks, assets, compilation) {
245 var self = this;
246 return Promise.resolve()
247 // Template processing
248 .then(function () {
249 var templateParams = {
250 compilation: compilation,
251 webpack: compilation.getStats().toJson(),
252 webpackConfig: compilation.options,
253 htmlWebpackPlugin: {
254 files: assets,
255 options: self.options
256 }
257 };
258 var html = '';
259 try {
260 html = templateFunction(templateParams);
261 } catch (e) {
262 compilation.errors.push(new Error('Template execution failed: ' + e));
263 return Promise.reject(e);
264 }
265 return html;
266 });
267};
268
269/**
270 * Html post processing
271 *
272 * Returns a promise
273 */
274HtmlWebpackPlugin.prototype.postProcessHtml = function (html, assets, assetTags) {
275 var self = this;
276 if (typeof html !== 'string') {
277 return Promise.reject('Expected html to be a string but got ' + JSON.stringify(html));
278 }
279 return Promise.resolve()
280 // Inject
281 .then(function () {
282 if (self.options.inject) {
283 return self.injectAssetsIntoHtml(html, assets, assetTags);
284 } else {
285 return html;
286 }
287 })
288 // Minify
289 .then(function (html) {
290 if (self.options.minify) {
291 var minify = require('html-minifier').minify;
292 return minify(html, self.options.minify);
293 }
294 return html;
295 });
296};
297
298/*
299 * Pushes the content of the given filename to the compilation assets
300 */
301HtmlWebpackPlugin.prototype.addFileToAssets = function (filename, compilation) {
302 filename = path.resolve(compilation.compiler.context, filename);
303 return Promise.props({
304 size: fs.statAsync(filename),
305 source: fs.readFileAsync(filename)
306 })
307 .catch(function () {
308 return Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename));
309 })
310 .then(function (results) {
311 var basename = path.basename(filename);
312 compilation.fileDependencies.push(filename);
313 compilation.assets[basename] = {
314 source: function () {
315 return results.source;
316 },
317 size: function () {
318 return results.size.size;
319 }
320 };
321 return basename;
322 });
323};
324
325/**
326 * Helper to sort chunks
327 */
328HtmlWebpackPlugin.prototype.sortChunks = function (chunks, sortMode) {
329 // Sort mode auto by default:
330 if (typeof sortMode === 'undefined') {
331 sortMode = 'auto';
332 }
333 // Custom function
334 if (typeof sortMode === 'function') {
335 return chunks.sort(sortMode);
336 }
337 // Disabled sorting:
338 if (sortMode === 'none') {
339 return chunkSorter.none(chunks);
340 }
341 // Check if the given sort mode is a valid chunkSorter sort mode
342 if (typeof chunkSorter[sortMode] !== 'undefined') {
343 return chunkSorter[sortMode](chunks);
344 }
345 throw new Error('"' + sortMode + '" is not a valid chunk sort mode');
346};
347
348/**
349 * Return all chunks from the compilation result which match the exclude and include filters
350 */
351HtmlWebpackPlugin.prototype.filterChunks = function (webpackStatsJson, includedChunks, excludedChunks) {
352 return webpackStatsJson.chunks.filter(function (chunk) {
353 var chunkName = chunk.names[0];
354 // This chunk doesn't have a name. This script can't handled it.
355 if (chunkName === undefined) {
356 return false;
357 }
358 // Skip if the chunk should be lazy loaded
359 if (!chunk.initial) {
360 return false;
361 }
362 // Skip if the chunks should be filtered and the given chunk was not added explicity
363 if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
364 return false;
365 }
366 // Skip if the chunks should be filtered and the given chunk was excluded explicity
367 if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
368 return false;
369 }
370 // Add otherwise
371 return true;
372 });
373};
374
375HtmlWebpackPlugin.prototype.isHotUpdateCompilation = function (assets) {
376 return assets.js.length && assets.js.every(function (name) {
377 return /\.hot-update\.js$/.test(name);
378 });
379};
380
381HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function (compilation, chunks) {
382 var self = this;
383 var webpackStatsJson = compilation.getStats().toJson();
384
385 // Use the configured public path or build a relative path
386 var publicPath = typeof compilation.options.output.publicPath !== 'undefined'
387 // If a hard coded public path exists use it
388 ? compilation.mainTemplate.getPublicPath({hash: webpackStatsJson.hash})
389 // If no public path was set get a relative url path
390 : path.relative(path.resolve(compilation.options.output.path, path.dirname(self.childCompilationOutputName)), compilation.options.output.path)
391 .split(path.sep).join('/');
392
393 if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
394 publicPath += '/';
395 }
396
397 var assets = {
398 // The public path
399 publicPath: publicPath,
400 // Will contain all js & css files by chunk
401 chunks: {},
402 // Will contain all js files
403 js: [],
404 // Will contain all css files
405 css: [],
406 // Will contain the html5 appcache manifest files if it exists
407 manifest: Object.keys(compilation.assets).filter(function (assetFile) {
408 return path.extname(assetFile) === '.appcache';
409 })[0]
410 };
411
412 // Append a hash for cache busting
413 if (this.options.hash) {
414 assets.manifest = self.appendHash(assets.manifest, webpackStatsJson.hash);
415 assets.favicon = self.appendHash(assets.favicon, webpackStatsJson.hash);
416 }
417
418 for (var i = 0; i < chunks.length; i++) {
419 var chunk = chunks[i];
420 var chunkName = chunk.names[0];
421
422 assets.chunks[chunkName] = {};
423
424 // Prepend the public path to all chunk files
425 var chunkFiles = [].concat(chunk.files).map(function (chunkFile) {
426 return publicPath + chunkFile;
427 });
428
429 // Append a hash for cache busting
430 if (this.options.hash) {
431 chunkFiles = chunkFiles.map(function (chunkFile) {
432 return self.appendHash(chunkFile, webpackStatsJson.hash);
433 });
434 }
435
436 // Webpack outputs an array for each chunk when using sourcemaps
437 // But we need only the entry file
438 var entry = chunkFiles[0];
439 assets.chunks[chunkName].size = chunk.size;
440 assets.chunks[chunkName].entry = entry;
441 assets.chunks[chunkName].hash = chunk.hash;
442 assets.js.push(entry);
443
444 // Gather all css files
445 var css = chunkFiles.filter(function (chunkFile) {
446 // Some chunks may contain content hash in their names, for ex. 'main.css?1e7cac4e4d8b52fd5ccd2541146ef03f'.
447 // We must proper handle such cases, so we use regexp testing here
448 return /.css($|\?)/.test(chunkFile);
449 });
450 assets.chunks[chunkName].css = css;
451 assets.css = assets.css.concat(css);
452 }
453
454 // Duplicate css assets can occur on occasion if more than one chunk
455 // requires the same css.
456 assets.css = _.uniq(assets.css);
457
458 return assets;
459};
460
461/**
462 * Injects the assets into the given html string
463 */
464HtmlWebpackPlugin.prototype.generateAssetTags = function (assets) {
465 // Turn script files into script tags
466 var scripts = assets.js.map(function (scriptPath) {
467 return {
468 tagName: 'script',
469 closeTag: true,
470 attributes: {
471 type: 'text/javascript',
472 src: scriptPath
473 }
474 };
475 });
476 // Make tags self-closing in case of xhtml
477 var selfClosingTag = !!this.options.xhtml;
478 // Turn css files into link tags
479 var styles = assets.css.map(function (stylePath) {
480 return {
481 tagName: 'link',
482 selfClosingTag: selfClosingTag,
483 attributes: {
484 href: stylePath,
485 rel: 'stylesheet'
486 }
487 };
488 });
489 // Injection targets
490 var head = [];
491 var body = [];
492
493 // If there is a favicon present, add it to the head
494 if (assets.favicon) {
495 head.push({
496 tagName: 'link',
497 selfClosingTag: selfClosingTag,
498 attributes: {
499 rel: 'shortcut icon',
500 href: assets.favicon
501 }
502 });
503 }
504 // Add styles to the head
505 head = head.concat(styles);
506 // Add scripts to body or head
507 if (this.options.inject === 'head') {
508 head = head.concat(scripts);
509 } else {
510 body = body.concat(scripts);
511 }
512 return {head: head, body: body};
513};
514
515/**
516 * Injects the assets into the given html string
517 */
518HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function (html, assets, assetTags) {
519 var htmlRegExp = /(<html[^>]*>)/i;
520 var headRegExp = /(<\/head>)/i;
521 var bodyRegExp = /(<\/body>)/i;
522 var body = assetTags.body.map(this.createHtmlTag);
523 var head = assetTags.head.map(this.createHtmlTag);
524
525 if (body.length) {
526 if (bodyRegExp.test(html)) {
527 // Append assets to body element
528 html = html.replace(bodyRegExp, function (match) {
529 return body.join('') + match;
530 });
531 } else {
532 // Append scripts to the end of the file if no <body> element exists:
533 html += body.join('');
534 }
535 }
536
537 if (head.length) {
538 // Create a head tag if none exists
539 if (!headRegExp.test(html)) {
540 if (!htmlRegExp.test(html)) {
541 html = '<head></head>' + html;
542 } else {
543 html = html.replace(htmlRegExp, function (match) {
544 return match + '<head></head>';
545 });
546 }
547 }
548
549 // Append assets to head element
550 html = html.replace(headRegExp, function (match) {
551 return head.join('') + match;
552 });
553 }
554
555 // Inject manifest into the opening html tag
556 if (assets.manifest) {
557 html = html.replace(/(<html[^>]*)(>)/i, function (match, start, end) {
558 // Append the manifest only if no manifest was specified
559 if (/\smanifest\s*=/.test(match)) {
560 return match;
561 }
562 return start + ' manifest="' + assets.manifest + '"' + end;
563 });
564 }
565 return html;
566};
567
568/**
569 * Appends a cache busting hash
570 */
571HtmlWebpackPlugin.prototype.appendHash = function (url, hash) {
572 if (!url) {
573 return url;
574 }
575 return url + (url.indexOf('?') === -1 ? '?' : '&') + hash;
576};
577
578/**
579 * Turn a tag definition into a html string
580 */
581HtmlWebpackPlugin.prototype.createHtmlTag = function (tagDefinition) {
582 var attributes = Object.keys(tagDefinition.attributes || {}).map(function (attributeName) {
583 return attributeName + '="' + tagDefinition.attributes[attributeName] + '"';
584 });
585 return '<' + [tagDefinition.tagName].concat(attributes).join(' ') + (tagDefinition.selfClosingTag ? '/' : '') + '>' +
586 (tagDefinition.innerHTML || '') +
587 (tagDefinition.closeTag ? '</' + tagDefinition.tagName + '>' : '');
588};
589
590/**
591 * Helper to return the absolute template path with a fallback loader
592 */
593HtmlWebpackPlugin.prototype.getFullTemplatePath = function (template, context) {
594 // If the template doesn't use a loader use the lodash template loader
595 if (template.indexOf('!') === -1) {
596 template = require.resolve('./lib/loader.js') + '!' + path.resolve(context, template);
597 }
598 // Resolve template path
599 return template.replace(
600 /([!])([^\/\\][^!\?]+|[^\/\\!?])($|\?.+$)/,
601 function (match, prefix, filepath, postfix) {
602 return prefix + path.resolve(filepath) + postfix;
603 });
604};
605
606/**
607 * Helper to return a sorted unique array of all asset files out of the
608 * asset object
609 */
610HtmlWebpackPlugin.prototype.getAssetFiles = function (assets) {
611 var files = _.uniq(Object.keys(assets).filter(function (assetType) {
612 return assetType !== 'chunks' && assets[assetType];
613 }).reduce(function (files, assetType) {
614 return files.concat(assets[assetType]);
615 }, []));
616 files.sort();
617 return files;
618};
619
620/**
621 * Helper to promisify compilation.applyPluginsAsyncWaterfall that returns
622 * a function that helps to merge given plugin arguments with processed ones
623 */
624HtmlWebpackPlugin.prototype.applyPluginsAsyncWaterfall = function (compilation) {
625 var promisedApplyPluginsAsyncWaterfall = Promise.promisify(compilation.applyPluginsAsyncWaterfall, {context: compilation});
626 return function (eventName, requiresResult, pluginArgs) {
627 return promisedApplyPluginsAsyncWaterfall(eventName, pluginArgs)
628 .then(function (result) {
629 if (requiresResult && !result) {
630 compilation.warnings.push(new Error('Using ' + eventName + ' without returning a result is deprecated.'));
631 }
632 return _.extend(pluginArgs, result);
633 });
634 };
635};
636
637module.exports = HtmlWebpackPlugin;