diff --git a/CHANGES.md b/CHANGES.md index 19eed1e..2afc018 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ Change History ============== +v1.4.0 +---- +* Add `favicon.ico` option +* Add html minifcation + v1.2.0 ------ * Set charset using HTML5 meta attribute diff --git a/README.md b/README.md index c7765f3..5f775bb 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,6 @@ If you have any css assets in webpack's output (for example, css extracted with the [ExtractTextPlugin](https://github.com/webpack/extract-text-webpack-plugin)) then these will be included with `` tags in the HTML head. - Configuration ------------- You can pass a hash of configuration options to `HtmlWebpackPlugin`. @@ -63,6 +62,11 @@ Allowed values are as follows: - `title`: The title to use for the generated HTML document. - `filename`: The file to write the HTML to. Defaults to `index.html`. You can specify a subdirectory here too (eg: `assets/admin.html`). +- `template`: A html template (supports [blueimp templates](https://github.com/blueimp/JavaScript-Templates)). +- `templateContent`: A html string or a function returning the html (supports [blueimp templates](https://github.com/blueimp/JavaScript-Templates)). +- `inject`: Inject all assets into the given `template` or `templateContent`. +- `favicon`: Adds the given favicon path to the output html. +- `minify`: Set to true or pass a [html-minifier](https://github.com/kangax/html-minifier#options-quick-reference) options object to minify the output. - `hash`: if `true` then append a unique webpack compilation hash to all included scripts and css files. This is useful for cache busting. @@ -108,58 +112,64 @@ once in your plugins array: Writing Your Own Templates -------------------------- If the default generated HTML doesn't meet your needs you can supply -your own [blueimp template](https://github.com/blueimp/JavaScript-Templates). -The [default template](https://github.com/ampedandwired/html-webpack-plugin/blob/master/default_index.html) -is a good starting point for writing your own. +your own template. The easiest way is to use the `inject` option and pass a custom html file. +The html-webpack-plugin will automatically inject all necessary css, js, manifest +and favicon files into the markup. + +```javascript +plugins: [ + new HtmlWebpackPlugin({ + inject: true, + template: 'my-index.html' + }) +] +``` -Let's say for example you wanted to put a webpack bundle into the head of your -HTML as well as the body. Your template might look like this: ```html My App - - ``` -To use this template, configure the plugin like this: -```javascript -{ - entry: 'index.js', - output: { - path: 'dist', - filename: 'index_bundle.js' - }, - plugins: [ - new HtmlWebpackPlugin({ - template: 'src/assets/my_template.html' - }) - ] -} -``` - Alternatively, if you already have your template's content in a String, you can pass it to the plugin using the `templateContent` option: ```javascript plugins: [ new HtmlWebpackPlugin({ + inject: true, templateContent: templateContentString }) ] ``` +You can use the [blueimp template](https://github.com/blueimp/JavaScript-Templates) syntax out of the box. +If the `inject` feature doesn't fit your needs and you want full control over the asset placement use the [default template](https://github.com/ampedandwired/html-webpack-plugin/blob/master/default_index.html) +as a starting point for writing your own. + The `templateContent` option can also be a function to use another template language like jade: ```javascript plugins: [ new HtmlWebpackPlugin({ - templateContent: function(templateParams, webpackCompiler) { + templateContent: function(templateParams, compilation) { // Return your template content synchronously here + return '..'; + } + }) +] +``` +Or the async version: +```javascript +plugins: [ + new HtmlWebpackPlugin({ + templateContent: function(templateParams, compilation, callback) { + // Return your template content asynchronously here + callback(null, '..'); } }) ] diff --git a/default_index.html b/default_index.html index 2dd1749..69d3384 100644 --- a/default_index.html +++ b/default_index.html @@ -3,6 +3,9 @@ {%=o.htmlWebpackPlugin.options.title || 'Webpack App'%} + {% if (o.htmlWebpackPlugin.files.favicon) { %} + + {% } %} {% for (var css in o.htmlWebpackPlugin.files.css) { %} {% } %} diff --git a/index.js b/index.js index e9245da..36ae451 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,8 @@ var fs = require('fs'); var path = require('path'); var _ = require('lodash'); var tmpl = require('blueimp-tmpl').tmpl; +var Promise = require('bluebird'); +Promise.promisifyAll(fs); function HtmlWebpackPlugin(options) { this.options = options || {}; @@ -10,56 +12,128 @@ function HtmlWebpackPlugin(options) { HtmlWebpackPlugin.prototype.apply = function(compiler) { var self = this; - compiler.plugin('emit', function(compilation, callback) { + compiler.plugin('emit', function(compilation, compileCallback) { 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.chunks, self.options.excludeChunks); - 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) { - // Use a special index file to prevent double script / style injection if the `inject` option is truthy - templateFile = path.join(__dirname, self.options.inject ? 'default_inject_index.html' : '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); + Promise.resolve() + // Add the favicon + .then(function(callback) { + if (self.options.favicon) { + return self.addFileToAssets(compilation, self.options.favicon, callback); } - callback(); - }); - } + }) + // Generate the html + .then(function() { + var templateParams = { + webpack: webpackStatsJson, + webpackConfig: compilation.options, + htmlWebpackPlugin: { + files: self.htmlWebpackPluginAssets(compilation, webpackStatsJson, self.options.chunks, self.options.excludeChunks), + options: self.options, + } + }; + // Deprecate templateParams.htmlWebpackPlugin.assets + var assets = self.htmlWebpackPluginLegacyAssets(compilation, webpackStatsJson); + Object.defineProperty(templateParams.htmlWebpackPlugin, 'assets', { + get: function() { + compilation.errors.push('htmlWebpackPlugin.assets is deprecated - please use htmlWebpackPlugin.files instead'); + return assets; + } + }); + + // Get/generate html + return self.getTemplateContent(compilation, templateParams) + .then(function(htmlTemplateContent) { + // Compile and add html to compilation + return self.emitHtml(compilation, htmlTemplateContent, templateParams, outputFilename); + }); + }) + // In case anything went wrong let the user know + .catch(function(err) { + compilation.errors.push(err); + compilation.assets[outputFilename] = { + source: function() { + return err.toString(); + }, + size: function() { + return err.toString().length; + } + }; + }) + // Tell the compiler to proceed + .finally(compileCallback); }); }; +/** + * Retrieves the html source depending on `this.options`. + * Supports: + * + options.fileContent as string + * + options.fileContent as sync function + * + options.fileContent as async function + * + options.template as template path + * Returns a Promise + */ +HtmlWebpackPlugin.prototype.getTemplateContent = function(compilation, templateParams) { + var self = this; + // If config is invalid + if (self.options.templateContent && self.options.template) { + return Promise.reject(new Error('HtmlWebpackPlugin: cannot specify both template and templateContent options')); + } + // If a function is passed + if (typeof self.options.templateContent === 'function') { + return Promise.fromNode(function(callback) { + // allow to specify a sync or an async function to generate the template content + var result = self.options.templateContent(templateParams, compilation, callback); + // if it return a result expect it to be sync + if (result !== undefined) { + callback(null, result); + } + }); + } + // If a string is passed + if (self.options.templateContent) { + return Promise.resolve(self.options.templateContent); + } + // If templateContent is empty use the tempalte option + var templateFile = self.options.template; + if (!templateFile) { + // Use a special index file to prevent double script / style injection if the `inject` option is truthy + templateFile = path.join(__dirname, self.options.inject ? 'default_inject_index.html' : 'default_index.html'); + } + compilation.fileDependencies.push(templateFile); + return fs.readFileAsync(templateFile, 'utf8') + // If the file could not be read log a error + .catch(function() { + return Promise.reject(new Error('HtmlWebpackPlugin: Unable to read HTML template "' + templateFile + '"')); + }); +}; + +/* + * Compile the html template and push the result to the compilation assets + */ HtmlWebpackPlugin.prototype.emitHtml = function(compilation, htmlTemplateContent, templateParams, outputFilename) { var html; + // blueimp-tmpl processing try { - html = tmpl(htmlTemplateContent, templateParams); + html = tmpl(htmlTemplateContent, templateParams); } catch(e) { - compilation.errors.push(new Error('HtmlWebpackPlugin: template error ' + e)); + return Promise.reject(new Error('HtmlWebpackPlugin: template error ' + e)); } + // Inject link and script elements into an existing html file if (this.options.inject) { html = this.injectAssetsIntoHtml(html, templateParams); } + + // Minify the html output + if (this.options.minify) { + var minify = require('html-minifier').minify; + // If `options.minify` is set to true use the default minify options + var minifyOptions = _.isObject(this.options.minify) ? this.options.minify : {}; + html = minify(html, minifyOptions); + } + compilation.assets[outputFilename] = { source: function() { return html; @@ -70,6 +144,29 @@ HtmlWebpackPlugin.prototype.emitHtml = function(compilation, htmlTemplateContent }; }; +/* + * Pushes the content of the given filename to the compilation assets + */ +HtmlWebpackPlugin.prototype.addFileToAssets = function(compilation, filename) { + return Promise.props({ + size: fs.statAsync(filename), + source: fs.readFileAsync(filename) + }) + .catch(function() { + return Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)); + }) + .then(function(results) { + compilation.fileDependencies.push(filename); + compilation.assets[path.basename(filename)] = { + source: function() { + return results.source; + }, + size: function() { + return results.size; + } + }; + }); +}; HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, webpackStatsJson, includedChunks, excludedChunks) { var self = this; @@ -82,6 +179,8 @@ HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, webp js: [], // Will contain all css files css: [], + // Will contain the path to the favicon if it exists + favicon: self.options.favicon ? publicPath + path.basename(self.options.favicon): undefined, // Will contain the html5 appcache manifest files if it exists manifest: Object.keys(compilation.assets).filter(function(assetFile){ return path.extname(assetFile) === '.appcache'; @@ -91,6 +190,7 @@ HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, webp // Append a hash for cache busting if (this.options.hash) { assets.manifest = self.appendHash(assets.manifest, webpackStatsJson.hash); + assets.favicon = self.appendHash(assets.favicon, webpackStatsJson.hash); } var chunks = webpackStatsJson.chunks.sort(function orderEntryLast(a, b) { @@ -173,6 +273,10 @@ HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, templateParams styles = styles.map(function(stylePath) { return ''; }); + // If there is a favicon present, add it above any link-tags + if (assets.favicon) { + styles.unshift(''); + } // Append scripts to body element html = html.replace(/(<\/body>)/i, function (match) { return scripts.join('') + match; diff --git a/package.json b/package.json index 09be008..02cb720 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "html-webpack-plugin", - "version": "1.3.0", + "version": "1.4.0", "description": "Simplifies creation of HTML files to serve your webpack bundles", "main": "index.js", "files": [ @@ -30,13 +30,16 @@ "devDependencies": { "css-loader": "^0.12.0", "extract-text-webpack-plugin": "^0.7.1", + "file-loader": "^0.8.1", "jasmine-node": "^1.14.5", "jshint": "^2.7.0", "rimraf": "^2.3.3", "style-loader": "^0.12.2", + "url-loader": "^0.5.5", "webpack": "^1.8.11" }, "dependencies": { + "bluebird": "^2.9.25", "blueimp-tmpl": "~2.5.4", "html-minifier": "^0.7.2", "lodash": "~3.8.0" diff --git a/spec/HtmlWebpackPluginSpec.js b/spec/HtmlWebpackPluginSpec.js index d6c1a5c..1896fce 100644 --- a/spec/HtmlWebpackPluginSpec.js +++ b/spec/HtmlWebpackPluginSpec.js @@ -8,11 +8,16 @@ var HtmlWebpackPlugin = require('../index.js'); var OUTPUT_DIR = path.join(__dirname, '../dist'); -function testHtmlPlugin(webpackConfig, expectedResults, outputFile, done) { +function testHtmlPlugin(webpackConfig, expectedResults, outputFile, done, expectErrors) { outputFile = outputFile || 'index.html'; webpack(webpackConfig, function(err, stats) { expect(err).toBeFalsy(); - expect(stats.hasErrors()).toBe(false); + var compilationErrors = (stats.compilation.errors || []).join('\n'); + if (expectErrors) { + expect(compilationErrors).not.toBe(''); + } else { + expect(compilationErrors).toBe(''); + } var htmlContent = fs.readFileSync(path.join(OUTPUT_DIR, outputFile)).toString(); for (var i = 0; i < expectedResults.length; i++) { var expectedResult = expectedResults[i]; @@ -167,10 +172,10 @@ describe('HtmlWebpackPlugin', function() { }, plugins: [new HtmlWebpackPlugin({template: path.join(__dirname, 'fixtures/legacy.html')})] }, - ['