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')})]
},
- ['