'use strict'; var fs = require('fs'); var path = require('path'); var _ = require('lodash'); var tmpl = require('blueimp-tmpl').tmpl; function HtmlWebpackPlugin(options) { this.options = options || {}; } HtmlWebpackPlugin.prototype.apply = function(compiler) { var self = this; compiler.plugin('emit', function(compilation, callback) { var webpackStatsJson = compilation.getStats().toJson(); var templateParams = {}; templateParams.webpack = webpackStatsJson; templateParams.htmlWebpackPlugin = {}; templateParams.htmlWebpackPlugin.assets = self.htmlWebpackPluginLegacyAssets(compilation, webpackStatsJson); templateParams.htmlWebpackPlugin.files = self.htmlWebpackPluginAssets(compilation, webpackStatsJson, self.options.hash); templateParams.htmlWebpackPlugin.options = self.options; templateParams.webpackConfig = compilation.options; var outputFilename = self.options.filename || 'index.html'; if (self.options.templateContent && self.options.template) { compilation.errors.push(new Error('HtmlWebpackPlugin: cannot specify both template and templateContent options')); callback(); } else if (self.options.templateContent) { var templateContent = typeof self.options.templateContent === 'function' ? self.options.templateContent(templateParams, compiler) : self.options.templateContent; self.emitHtml(compilation, templateContent, templateParams, outputFilename); callback(); } else { var templateFile = self.options.template; if (!templateFile) { templateFile = path.join(__dirname, 'default_index.html'); } compilation.fileDependencies.push(templateFile); fs.readFile(templateFile, 'utf8', function(err, htmlTemplateContent) { if (err) { compilation.errors.push(new Error('HtmlWebpackPlugin: Unable to read HTML template "' + templateFile + '"')); } else { self.emitHtml(compilation, htmlTemplateContent, templateParams, outputFilename); } callback(); }); } }); }; HtmlWebpackPlugin.prototype.emitHtml = function(compilation, htmlTemplateContent, templateParams, outputFilename) { var html; try { html = tmpl(htmlTemplateContent, templateParams); } catch(e) { compilation.errors.push(new Error('HtmlWebpackPlugin: template error ' + e)); } // Append/Inject link and script elements into an existing html file if (this.options.append) { html = this.appendAssetsToHtml(html, templateParams, this.options.append); } compilation.assets[outputFilename] = { source: function() { return html; }, size: function() { return html.length; } }; }; HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, webpackStatsJson, appendHash) { var publicPath = compilation.options.output.publicPath || ''; var queryString = appendHash ? '?' + webpackStatsJson.hash : ''; var assets = { // Will contain all js & css files by chunk chunks: {}, // Will contain all js files js: [], // Will contain all css files css: [], // Will contain the html5 appcache manifest files if it exists manifest: Object.keys(compilation.assets).filter(function(assetFile){ return path.extname(assetFile) === '.appcache'; }).map(function(assetFile) { return assetFile + queryString; })[0] }; var chunks = webpackStatsJson.chunks.sort(function orderEntryLast(a, b) { if (a.entry !== b.entry) { return b.entry ? 1 : -1; } else { return b.id - a.id; } }); for (var i = 0; i < chunks.length; i++) { var chunk = chunks[i]; var chunkName = chunk.names[0]; assets.chunks[chunkName] = {}; // Prepend the public path to all chunk files var chunkFiles = [].concat(chunk.files).map(function(chunkFile) { return publicPath + chunkFile + queryString; }); // Webpack outputs an array for each chunk when using sourcemaps // But we need only the entry file var entry = chunkFiles[0]; assets.chunks[chunkName].entry = entry; assets.js.push(entry); // Gather all css files var css = chunkFiles.filter(function(chunkFile){ // Some chunks may contain content hash in their names, for ex. 'main.css?1e7cac4e4d8b52fd5ccd2541146ef03f'. // We must proper handle such cases, so we use regexp testing here return /^.css/.test(path.extname(chunkFile)); }); assets.chunks[chunkName].css = css; assets.css = assets.css.concat(css); } // Duplicate css assets can occur on occasion if more than one chunk // requires the same css. assets.css = _.uniq(assets.css); return assets; }; /** * Inject the assets into the given html string */ HtmlWebpackPlugin.prototype.appendAssetsToHtml = function(html, templateParams, chunks) { var assets = templateParams.htmlWebpackPlugin.files; // If chunks is set to true append all chunks if (chunks === true) { chunks = Object.keys(assets.chunks); } // Gather all css and script files var styles = []; var scripts = []; chunks.forEach(function(chunkName) { styles = styles.concat(assets.chunks[chunkName].css); scripts.push(assets.chunks[chunkName].entry); }); // Turn script files into script tags scripts = scripts.map(function(scriptPath) { return ''; }); // Turn css files into link tags styles = styles.map(function(stylePath) { return ''; }); // Append scripts html = html.replace(/(<\/body>)/i, function (match, start) { return scripts.join('') + match; }); // Append styles html = html.replace(/(<\/head>)/i, function (match, start) { return styles.join('') + match; }); // Append manifest if (assets.manifest) { html = html.replace(/()/i, function (match, start, end) { // Don't append a manifest if a manifest was already specified if (match.test(/\smanifest\s*=/)) { return match; } return start + ' manifest="' + assets.manifest + '?' + templateParams.hash + '"' + end; }); } return html; }; /** * A helper to support the templates written for html-webpack-plugin <= 1.1.0 */ HtmlWebpackPlugin.prototype.htmlWebpackPluginLegacyAssets = function(compilation, webpackStatsJson) { var assets = this.htmlWebpackPluginAssets(compilation, webpackStatsJson); var legacyAssets = {}; Object.keys(assets.chunks).forEach(function(chunkName){ legacyAssets[chunkName] = assets.chunks[chunkName].entry; }); return legacyAssets; }; module.exports = HtmlWebpackPlugin;