Compare commits

...

18 Commits

Author SHA1 Message Date
Jan Nicklas 74909e4c94 Pre release 2.0 2015-06-30 11:12:24 +02:00
Jan Nicklas 571407f960 Fix demos 2015-06-29 17:15:41 +02:00
Jan Nicklas 4f1ab08827 Cache loader result 2015-06-29 17:04:36 +02:00
Jan Nicklas 8e89ea23de Use lodash to solve dependency issues for the fallback loader 2015-06-29 16:52:50 +02:00
Jan Nicklas 7f63ba8988 Fix correct autoloading 2015-06-29 16:09:39 +02:00
Jan Nicklas 9e313ef6d1 Fix rebase 2015-06-29 16:08:54 +02:00
Jan Nicklas a5b10196be Fix build 2015-06-29 16:06:51 +02:00
Jan Nicklas 58a3414a36 Refactor examples 2015-06-29 16:06:51 +02:00
Jan Nicklas 2fb24b29be Use blueimp loader instead of blueimp 2015-06-29 16:06:51 +02:00
Jan Nicklas a1442f2a54 Fix webpack-dev-server child compilation on template change 2015-06-29 16:05:24 +02:00
Jan Nicklas d91c306c81 Fix chunk filtering 2015-06-29 16:05:24 +02:00
Jan Nicklas 3066de978e Refactor template loading 2015-06-29 16:04:22 +02:00
Kenny Tran 94fc6c98af Fix typo 2015-06-29 16:02:52 +02:00
Jan Nicklas e56b2fb600 Move compilation into a new function 2015-06-29 16:02:52 +02:00
Jan Nicklas c5033f9d95 Fix image example 2015-06-29 16:02:52 +02:00
Jan Nicklas d793886a5e Fix to resolve path of modules 2015-06-29 16:02:52 +02:00
Jan Nicklas 998ef87cf7 Add minification again 2015-06-29 16:02:52 +02:00
Jan Nicklas 79b0e692f0 Refactoring to support loaders 2015-06-29 16:02:52 +02:00
35 changed files with 478 additions and 292 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/node_modules/ /node_modules/
/dist/ /dist/
examples/*/dist

View File

@ -1,11 +1,11 @@
HTML Webpack Plugin HTML Webpack Plugin
=================== ===================
[![npm version](https://badge.fury.io/js/html-webpack-plugin.svg)](http://badge.fury.io/js/html-webpack-plugin) [![Dependency Status](https://david-dm.org/ampedandwired/html-webpack-plugin.svg)](https://david-dm.org/ampedandwired/html-webpack-plugin) [![bitHound Score](https://www.bithound.io/github/ampedandwired/html-webpack-plugin/badges/score.svg)](https://www.bithound.io/github/ampedandwired/html-webpack-plugin) [![Build status](https://travis-ci.org/ampedandwired/html-webpack-plugin.svg)](https://travis-ci.org/ampedandwired/html-webpack-plugin) [![npm version](https://badge.fury.io/js/html-webpack-plugin.svg)](http://badge.fury.io/js/html-webpack-plugin) [![Dependency Status](https://david-dm.org/ampedandwired/html-webpack-plugin.svg)](https://david-dm.org/ampedandwired/html-webpack-plugin) [![bitHound Score](https://www.bithound.io/github/ampedandwired/html-webpack-plugin/badges/score.svg)](https://www.bithound.io/github/ampedandwired/html-webpack-plugin) [![Build status](https://travis-ci.org/ampedandwired/html-webpack-plugin.svg)](https://travis-ci.org/ampedandwired/html-webpack-plugin)
This is a [webpack](http://webpack.github.io/) plugin that simplifies creation of HTML files to serve your This is a [webpack](http://webpack.github.io/) plugin that simplifies creation of HTML files to serve your
webpack bundles. This is especially useful for webpack bundles that include webpack bundles. This is especially useful for webpack bundles that include
a hash in the filename which changes every compilation. You can either let the plugin generate an HTML file for you or supply a hash in the filename which changes every compilation. You can either let the plugin generate an HTML file for you or supply
your own template (using [blueimp templates](https://github.com/blueimp/JavaScript-Templates)). your own template (using lodash/ejs templates.
Installation Installation
------------ ------------
@ -63,15 +63,14 @@ Allowed values are as follows:
- `title`: The title to use for the generated HTML document. - `title`: The title to use for the generated HTML document.
- `filename`: The file to write the HTML to. Defaults to `index.html`. - `filename`: The file to write the HTML to. Defaults to `index.html`.
You can specify a subdirectory here too (eg: `assets/admin.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)). - `template`: Path to the template. Supports loaders e.g. `html!./index.html`.
- `templateContent`: A html string or a function returning the html (supports [blueimp templates](https://github.com/blueimp/JavaScript-Templates)).
- `inject`: `true | 'head' | 'body' | false` Inject all assets into the given `template` or `templateContent` - When passing `true` or `'body'` all javascript resources will be placed at the bottom of the body element. `'head'` will place the scripts in the head element. - `inject`: `true | 'head' | 'body' | false` Inject all assets into the given `template` or `templateContent` - When passing `true` or `'body'` all javascript resources will be placed at the bottom of the body element. `'head'` will place the scripts in the head element.
- `favicon`: Adds the given favicon path to the output html. - `favicon`: Adds the given favicon path to the output html.
- `minify`: `true | {...} | false` Set to true or pass a [html-minifier](https://github.com/kangax/html-minifier#options-quick-reference) options object to minify the output. - `minify`: `true | {...} | false` Set to true or pass a [html-minifier](https://github.com/kangax/html-minifier#options-quick-reference) options object to minify the output.
- `hash`: `true | false` if `true` then append a unique webpack compilation hash to all - `hash`: `true | false` if `true` then append a unique webpack compilation hash to all
included scripts and css files. This is useful for cache busting. included scripts and css files. This is useful for cache busting.
- `chunks`: Allows you to add only some chunks (e.g. only the unit-test chunk) - `chunks`: Allows you to add only some chunks (e.g. only the unit-test chunk)
- `excludeChunks`: Allows you to skip some chunks (e.g. don't add the unit-test chunk) - `excludeChunks`: Allows you to skip some chunks (e.g. don't add the unit-test chunk)
Here's an example webpack config illustrating how to use these options: Here's an example webpack config illustrating how to use these options:
```javascript ```javascript
@ -122,7 +121,7 @@ and favicon files into the markup.
```javascript ```javascript
plugins: [ plugins: [
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
title: 'Custom template', title: 'Custom template',
template: 'my-index.html', // Load a custom template template: 'my-index.html', // Load a custom template
inject: 'body' // Inject all scripts into the body inject: 'body' // Inject all scripts into the body
}) })
@ -136,7 +135,7 @@ plugins: [
<html> <html>
<head> <head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/> <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
<title>{%=o.htmlWebpackPlugin.options.title}</title> <title><%= htmlWebpackPlugin.options.title %></title>
</head> </head>
<body> <body>
</body> </body>
@ -154,7 +153,7 @@ plugins: [
] ]
``` ```
You can use the [blueimp template](https://github.com/blueimp/JavaScript-Templates) syntax out of the box. You can use the lodash/ejs 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) 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. as a starting point for writing your own.

View File

@ -1,18 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html{% if(o.htmlWebpackPlugin.files.manifest) { %} manifest="{%= o.htmlWebpackPlugin.files.manifest %}"{% } %}> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{%=o.htmlWebpackPlugin.options.title || 'Webpack App'%}</title> <title><%= htmlWebpackPlugin.options.title %></title>
{% if (o.htmlWebpackPlugin.files.favicon) { %}
<link rel="shortcut icon" href="{%=o.htmlWebpackPlugin.files.favicon%}">
{% } %}
{% for (var css in o.htmlWebpackPlugin.files.css) { %}
<link href="{%=o.htmlWebpackPlugin.files.css[css] %}" rel="stylesheet">
{% } %}
</head> </head>
<body> <body>
{% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %}
<script src="{%=o.htmlWebpackPlugin.files.chunks[chunk].entry %}"></script>
{% } %}
</body> </body>
</html> </html>

View File

@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{%=o.htmlWebpackPlugin.options.title || 'Webpack App'%}</title>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,4 @@
require('./main.css');
var h1 = document.createElement('h1');
h1.innerHTML = 'Hello world!';
document.body.appendChild(h1);

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -0,0 +1,3 @@
body {
background: snow;
}

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example template</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<img src="logo.png">
</body>
</html>

View File

@ -0,0 +1,22 @@
var HtmlWebpackPlugin = require('..');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './example.js',
output: {
path: __dirname + '/dist',
publicPath: '',
filename: 'bundle.js'
},
module: {
loaders: [
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') },
{ test: /\.png$/, loader: 'file-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'template.html'
}),
new ExtractTextPlugin('styles.css')
]
};

4
examples/default/example.js Executable file
View File

@ -0,0 +1,4 @@
require('./main.css');
var h1 = document.createElement('h1');
h1.innerHTML = 'Hello world!';
document.body.appendChild(h1);

View File

@ -0,0 +1,3 @@
body {
background: snow;
}

View File

@ -0,0 +1,18 @@
var HtmlWebpackPlugin = require('..');
module.exports = {
entry: './example.js',
output: {
path: __dirname + '/dist',
publicPath: '',
filename: 'bundle.js'
},
module: {
loaders: [
{ test: /\.css$/, loader: 'style-loader!css-loader' },
{ test: /\.png$/, loader: 'file-loader' }
]
},
plugins: [
new HtmlWebpackPlugin()
]
};

4
examples/favicon/example.js Executable file
View File

@ -0,0 +1,4 @@
require('./main.css');
var h1 = document.createElement('h1');
h1.innerHTML = 'Hello world!';
document.body.appendChild(h1);

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

View File

@ -0,0 +1,3 @@
body {
background: snow;
}

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example template</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<img src="logo.png">
</body>
</html>

View File

@ -0,0 +1,24 @@
var HtmlWebpackPlugin = require('..');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './example.js',
output: {
path: __dirname + '/dist',
publicPath: '',
filename: 'bundle.js'
},
module: {
loaders: [
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') },
{ test: /\.png$/, loader: 'file-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'HtmlWebpackPlugin example',
favicon: 'favicon.ico',
filename: 'favicon.html'
}),
new ExtractTextPlugin('styles.css')
]
};

View File

@ -0,0 +1,4 @@
require('./main.css');
var h1 = document.createElement('h1');
h1.innerHTML = 'Hello world!';
document.body.appendChild(h1);

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -0,0 +1,3 @@
body {
background: snow;
}

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example template</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<img src="logo.png">
</body>
</html>

View File

@ -0,0 +1,24 @@
var HtmlWebpackPlugin = require('..');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './example.js',
output: {
path: __dirname + '/dist',
publicPath: '',
filename: 'bundle.js'
},
module: {
loaders: [
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') },
{ test: /\.png$/, loader: 'file-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'html-loader.html',
favicon: 'favicon.ico',
template: 'html!./template.html'
}),
new ExtractTextPlugin('styles.css')
]
};

View File

@ -0,0 +1,4 @@
require('./main.css');
var h1 = document.createElement('h1');
h1.innerHTML = 'Hello world!';
document.body.appendChild(h1);

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -0,0 +1,3 @@
body {
background: snow;
}

View File

@ -0,0 +1,2 @@
<h2>Partial</h2>
<img src="logo.png">

View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example template</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<%- include(require('html!./partial.html'), {}); %>
</body>
</html>

View File

@ -0,0 +1,22 @@
var HtmlWebpackPlugin = require('../..');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './example.js',
output: {
path: __dirname + '/dist',
publicPath: '',
filename: 'bundle.js'
},
module: {
loaders: [
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') },
{ test: /\.png$/, loader: 'file-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'template.html'
}),
new ExtractTextPlugin('styles.css')
]
};

399
index.js
View File

@ -1,154 +1,249 @@
'use strict'; 'use strict';
var vm = require('vm');
var fs = require('fs'); var fs = require('fs');
var path = require('path');
var _ = require('lodash'); var _ = require('lodash');
var tmpl = require('blueimp-tmpl').tmpl;
var Promise = require('bluebird'); var Promise = require('bluebird');
var path = require('path');
Promise.promisifyAll(fs); Promise.promisifyAll(fs);
var NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
var LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
function HtmlWebpackPlugin(options) { function HtmlWebpackPlugin(options) {
this.options = options || {}; // Default options
this.options = _.extend({
template: __dirname + '/default_index.html',
filename: 'index.html',
hash: false,
inject: true,
compile: true,
favicon: false,
minify: false,
chunks: 'all',
excludeChunks: [],
title: 'Webpack App'
}, options);
// If the template doesn't use a loader use the blueimp template loader
if(this.options.template.indexOf('!') === -1) {
this.options.template = require.resolve('./loader.js') + '!' + path.resolve(this.options.template);
}
// Resolve template path
this.options.template = this.options.template.replace(
/(\!)([^\/\\][^\!\?]+|[^\/\\!?])($|\?.+$)/,
function(match, prefix, filepath, postfix) {
return prefix + path.resolve(filepath) + postfix;
});
} }
HtmlWebpackPlugin.prototype.apply = function(compiler) { HtmlWebpackPlugin.prototype.apply = function(compiler) {
var self = this; var self = this;
compiler.plugin('emit', function(compilation, compileCallback) { var compilationPromise;
var webpackStatsJson = compilation.getStats().toJson(); self.context = compiler.context;
var outputFilename = self.options.filename || 'index.html';
compiler.plugin('make', function(compilation, callback) {
// Compile the template
compilationPromise = self.compileTemplate(self.options.template, self.options.filename, compilation);
compilationPromise.finally(callback);
});
compiler.plugin('emit', function(compilation, callback) {
// Get all chunks
var chunks = self.filterChunks(compilation.getStats().toJson(), self.options.chunks, self.options.excludeChunks);
// Get assets
var assets = self.htmlWebpackPluginAssets(compilation, chunks);
Promise.resolve() Promise.resolve()
// Add the favicon // Favicon
.then(function(callback) { .then(function() {
if (self.options.favicon) { if (self.options.favicon) {
return self.addFileToAssets(compilation, self.options.favicon, callback); return self.addFileToAssets(self.options.favicon, compilation)
.then(function(faviconBasename){
assets.favicon = faviconBasename;
});
} }
}) })
// Generate the html // Wait for the compilation to finish
.then(function() { .then(function() {
var templateParams = { return compilationPromise;
webpack: webpackStatsJson, })
webpackConfig: compilation.options, .then(function(resultAsset) {
htmlWebpackPlugin: { // Once everything is compiled evaluate the html factory
files: self.htmlWebpackPluginAssets(compilation, webpackStatsJson, self.options.chunks, self.options.excludeChunks), // and replace it with its content
options: self.options, return self.evaluateCompilationResult(compilation, resultAsset);
} })
}; // Execute the template
// Deprecate templateParams.htmlWebpackPlugin.assets .then(function(compilationResult) {
var assets = self.htmlWebpackPluginLegacyAssets(compilation, webpackStatsJson); // If the loader result is a function execute it to retreive the html
Object.defineProperty(templateParams.htmlWebpackPlugin, 'assets', { // otherwise use the returned html
get: function() { return typeof compilationResult !== 'function' ? compilationResult :
compilation.warnings.push(new Error('HtmlWebPackPlugin: htmlWebpackPlugin.assets is deprecated - please use inject or htmlWebpackPlugin.files instead' + self.executeTemplate(compilationResult, chunks, assets, compilation);
'\nsee: https://github.com/ampedandwired/html-webpack-plugin/issues/52')); })
return assets; .then(function(html) {
} // Add the stylesheets, scripts and so on to the resulting html
}); return self.postProcessHtml(html, 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) { .catch(function(err) {
compilation.errors.push(err); // In case anything went wrong the promise is resolved
compilation.assets[outputFilename] = { // with the error message and an error is logged
var errorMessage = "HtmlWebpackPlugin Error: " + err;
compilation.errors.push(new Error(errorMessage));
return errorMessage;
})
.then(function(html) {
// Replace the compilation result with the evaluated html code
compilation.assets[self.options.filename] = {
source: function() { source: function() {
return err.toString(); return html;
}, },
size: function() { size: function() {
return err.toString().length; return html.length;
} }
}; };
}) callback();
// Tell the compiler to proceed });
.finally(compileCallback); });
};
/**
* Returns the child compiler name
*/
HtmlWebpackPlugin.prototype.getCompilerName = function() {
var absolutePath = path.resolve(this.context, this.options.filename);
var relativePath = path.relative(this.context, absolutePath);
return 'html-webpack-plugin for "' + (absolutePath.length < relativePath.length ? absolutePath : relativePath) + '"';
};
/**
* Compiles the template into a nodejs factory, adds its to the compilation.assets
* and returns a promise of the result asset object.
*/
HtmlWebpackPlugin.prototype.compileTemplate = function(template, outputFilename, compilation) {
// The entry file is just an empty helper as the dynamic template
// require is added in "loader.js"
var outputOptions = {
filename: outputFilename,
publicPath: compilation.outputOptions.publicPath
};
// Create an additional child compiler which takes the template
// and turns it into an Node.JS html factory.
// This allows us to use loaders during the compilation
var compilerName = this.getCompilerName();
var childCompiler = compilation.createChildCompiler(compilerName, outputOptions);
childCompiler.apply(new NodeTemplatePlugin(outputOptions));
childCompiler.apply(new LibraryTemplatePlugin('result', 'var'));
childCompiler.apply(new NodeTargetPlugin());
childCompiler.apply(new SingleEntryPlugin(this.context, template));
// Create a subCache (copied from https://github.com/SanderSpies/extract-text-webpack-plugin/blob/master/loader.js)
childCompiler.plugin('compilation', function(compilation) {
if(compilation.cache) {
if(!compilation.cache[compilerName]) {
compilation.cache[compilerName] = {};
}
compilation.cache = compilation.cache[compilerName];
}
});
// Compile and return a promise
return new Promise(function (resolve, reject) {
childCompiler.runAsChild(function(err) {
// Resolve / reject the promise
if (err) {
reject(err);
} else {
resolve(compilation.assets[outputFilename]);
}
});
}); });
}; };
/** /**
* Retrieves the html source depending on `this.options`. * Evaluates the child compilation result
* Supports: * Returns a promise
* + 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) { HtmlWebpackPlugin.prototype.evaluateCompilationResult = function(compilation, compilationResult) {
if(!compilationResult) {
return Promise.reject('The child compilation didn\'t provide a result');
}
// Strip the leading 'var '
var source = compilationResult.source().substr(3);
// Evaluate code and cast to string
var newSource;
try {
newSource = vm.runInThisContext(source);
} catch (e) {
compilation.errors.push(new Error('Template compilation failed: ' + e));
return Promise.reject(e);
}
return typeof newSource === 'string' || typeof newSource === 'function' ?
Promise.resolve(newSource) :
Promise.reject('The loader "' + this.options.template + '" didn\'t return html.');
};
/**
* Html post processing
*
* Returns a promise
*/
HtmlWebpackPlugin.prototype.executeTemplate = function(templateFunction, chunks, assets, compilation) {
var self = this; var self = this;
// If config is invalid return Promise.resolve()
if (self.options.templateContent && self.options.template) { // Template processing
return Promise.reject(new Error('HtmlWebpackPlugin: cannot specify both template and templateContent options')); .then(function() {
} var templateParams = {
// If a function is passed webpack: compilation.getStats().toJson(),
if (typeof self.options.templateContent === 'function') { webpackConfig: compilation.options,
return Promise.fromNode(function(callback) { htmlWebpackPlugin: {
// allow to specify a sync or an async function to generate the template content files: assets,
var result = self.options.templateContent(templateParams, compilation, callback); options: self.options,
// if it returns a result expect it to be sync }
if (result !== undefined) { };
callback(null, result); var html = '';
try {
html = templateFunction(templateParams);
} catch (e) {
compilation.errors.push(new Error('Template execution failed: ' + e));
return Promise.reject(e);
} }
}); return html;
}
// If a string is passed
if (self.options.templateContent) {
return Promise.resolve(self.options.templateContent);
}
// If templateContent is empty use the template 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 * Html post processing
*
* Returns a promise
*/ */
HtmlWebpackPlugin.prototype.emitHtml = function(compilation, htmlTemplateContent, templateParams, outputFilename) { HtmlWebpackPlugin.prototype.postProcessHtml = function(html, assets) {
var html; var self = this;
// blueimp-tmpl processing return Promise.resolve()
try { // Inject
html = tmpl(htmlTemplateContent, templateParams); .then(function() {
} catch(e) { if (self.options.inject) {
return Promise.reject(new Error('HtmlWebpackPlugin: template error ' + e)); return self.injectAssetsIntoHtml(html, assets);
} } else {
return html;
// Inject link and script elements into an existing html file }
if (this.options.inject) { })
html = this.injectAssetsIntoHtml(html, templateParams); // Minify
} .then(function(html) {
if (self.options.minify) {
// Minify the html output var minify = require('html-minifier').minify;
if (this.options.minify) { // If `options.minify` is set to true use the default minify options
var minify = require('html-minifier').minify; var minifyOptions = _.isObject(self.options.minify) ? self.options.minify : {};
// If `options.minify` is set to true use the default minify options try {
var minifyOptions = _.isObject(this.options.minify) ? this.options.minify : {}; minify(html, minifyOptions);
html = minify(html, minifyOptions); } catch(e) {
} Promise.reject(e);
}
compilation.assets[outputFilename] = { }
source: function() {
return html; return html;
}, });
size: function() {
return html.length;
}
};
}; };
/* /*
* Pushes the content of the given filename to the compilation assets * Pushes the content of the given filename to the compilation assets
*/ */
HtmlWebpackPlugin.prototype.addFileToAssets = function(compilation, filename) { HtmlWebpackPlugin.prototype.addFileToAssets = function(filename, compilation) {
return Promise.props({ return Promise.props({
size: fs.statAsync(filename), size: fs.statAsync(filename),
source: fs.readFileAsync(filename) source: fs.readFileAsync(filename)
@ -157,24 +252,57 @@ HtmlWebpackPlugin.prototype.addFileToAssets = function(compilation, filename) {
return Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename)); return Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename));
}) })
.then(function(results) { .then(function(results) {
var basename = path.basename(filename);
compilation.fileDependencies.push(filename); compilation.fileDependencies.push(filename);
compilation.assets[path.basename(filename)] = { compilation.assets[basename] = {
source: function() { source: function() {
return results.source; return results.source;
}, },
size: function() { size: function() {
return results.size; return results.size.size;
} }
}; };
return basename;
}); });
}; };
HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, webpackStatsJson, includedChunks, excludedChunks) { /**
* Return all chunks from the compilation result which match the exclude and include filters
*/
HtmlWebpackPlugin.prototype.filterChunks = function (webpackStatsJson, includedChunks, excludedChunks) {
var chunks = webpackStatsJson.chunks.filter(function(chunk){
var chunkName = chunk.names[0];
// This chunk doesn't have a name. This script can't handled it.
if (chunkName === undefined) {
return false;
}
// Skip if the chunks should be filtered and the given chunk was not added explicity
if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
return false;
}
// Skip if the chunks should be filtered and the given chunk was excluded explicity
if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
return false;
}
// Add otherwise
return true;
});
return chunks.sort(function orderEntryLast(a, b) {
if (a.entry !== b.entry) {
return b.entry ? 1 : -1;
} else {
return b.id - a.id;
}
});
};
HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, chunks) {
var self = this; var self = this;
var webpackStatsJson = compilation.getStats().toJson();
// Use the configured public path or build a relative path // Use the configured public path or build a relative path
var publicPath = typeof compilation.options.output.publicPath !== 'undefined' ? var publicPath = typeof compilation.options.output.publicPath !== 'undefined' ?
compilation.options.output.publicPath : compilation.options.output.publicPath :
path.relative(path.dirname(self.options.filename), '.'); path.relative(path.dirname(self.options.filename), '.');
if (publicPath.length && publicPath.substr(-1, 1) !== '/') { if (publicPath.length && publicPath.substr(-1, 1) !== '/') {
@ -188,8 +316,6 @@ HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, webp
js: [], js: [],
// Will contain all css files // Will contain all css files
css: [], 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 // Will contain the html5 appcache manifest files if it exists
manifest: Object.keys(compilation.assets).filter(function(assetFile){ manifest: Object.keys(compilation.assets).filter(function(assetFile){
return path.extname(assetFile) === '.appcache'; return path.extname(assetFile) === '.appcache';
@ -202,32 +328,10 @@ HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, webp
assets.favicon = self.appendHash(assets.favicon, webpackStatsJson.hash); assets.favicon = self.appendHash(assets.favicon, webpackStatsJson.hash);
} }
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++) { for (var i = 0; i < chunks.length; i++) {
var chunk = chunks[i]; var chunk = chunks[i];
var chunkName = chunk.names[0]; var chunkName = chunk.names[0];
// This chunk doesn't have a name. This script can't handled it.
if(chunkName === undefined) {
continue;
}
// Skip if the chunks should be filtered and the given chunk was not added explicity
if (Array.isArray(includedChunks) && includedChunks.indexOf(chunkName) === -1) {
continue;
}
// Skip if the chunks should be filtered and the given chunk was excluded explicity
if (Array.isArray(excludedChunks) && excludedChunks.indexOf(chunkName) !== -1) {
continue;
}
assets.chunks[chunkName] = {}; assets.chunks[chunkName] = {};
// Prepend the public path to all chunk files // Prepend the public path to all chunk files
@ -268,8 +372,7 @@ HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, webp
/** /**
* Injects the assets into the given html string * Injects the assets into the given html string
*/ */
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, templateParams) { HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets) {
var assets = templateParams.htmlWebpackPlugin.files;
var chunks = Object.keys(assets.chunks); var chunks = Object.keys(assets.chunks);
// Gather all css and script files // Gather all css and script files
@ -324,18 +427,6 @@ HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, templateParams
return html; 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;
};
/** /**
* Appends a cache busting hash * Appends a cache busting hash
*/ */

20
loader.js Normal file
View File

@ -0,0 +1,20 @@
var _ = require('lodash');
var loaderUtils = require('loader-utils');
module.exports = function (source) {
'use strict';
if (this.cacheable) {
this.cacheable();
}
var allLoadersButThisOne = this.loaders.filter(function(loader) {
return loader.module !== module.exports;
});
// This loader shouldn't kick in if there is any other loader
if (allLoadersButThisOne.length > 0) {
return source;
}
// Use underscore for a minimalistic loader
var options = loaderUtils.parseQuery(this.query);
var template = _.template(source, options);
return 'module.exports = ' + template;
};

View File

@ -1,10 +1,11 @@
{ {
"name": "html-webpack-plugin", "name": "html-webpack-plugin",
"version": "1.5.2", "version": "2.0.0",
"description": "Simplifies creation of HTML files to serve your webpack bundles", "description": "Simplifies creation of HTML files to serve your webpack bundles",
"main": "index.js", "main": "index.js",
"files": [ "files": [
"index.js", "index.js",
"loader.js",
"default_index.html", "default_index.html",
"default_inject_index.html" "default_inject_index.html"
], ],
@ -32,6 +33,7 @@
"css-loader": "^0.12.0", "css-loader": "^0.12.0",
"extract-text-webpack-plugin": "^0.7.1", "extract-text-webpack-plugin": "^0.7.1",
"file-loader": "^0.8.1", "file-loader": "^0.8.1",
"html-loader": "^0.3.0",
"jasmine-node": "^1.14.5", "jasmine-node": "^1.14.5",
"jshint": "^2.7.0", "jshint": "^2.7.0",
"rimraf": "^2.3.3", "rimraf": "^2.3.3",
@ -41,8 +43,8 @@
}, },
"dependencies": { "dependencies": {
"bluebird": "^2.9.25", "bluebird": "^2.9.25",
"blueimp-tmpl": "~2.5.4",
"html-minifier": "^0.7.2", "html-minifier": "^0.7.2",
"loader-utils": "^0.2.10",
"lodash": "~3.9.3" "lodash": "~3.9.3"
} }
} }

View File

@ -24,6 +24,11 @@ function testHtmlPlugin(webpackConfig, expectedResults, outputFile, done, expect
} else { } else {
expect(compilationWarnings).toBe(''); expect(compilationWarnings).toBe('');
} }
var outputFileExists = fs.existsSync(path.join(OUTPUT_DIR, outputFile));
expect(outputFileExists).toBe(true);
if(!outputFileExists) {
return done();
}
var htmlContent = fs.readFileSync(path.join(OUTPUT_DIR, outputFile)).toString(); var htmlContent = fs.readFileSync(path.join(OUTPUT_DIR, outputFile)).toString();
for (var i = 0; i < expectedResults.length; i++) { for (var i = 0; i < expectedResults.length; i++) {
@ -83,20 +88,6 @@ describe('HtmlWebpackPlugin', function() {
['<script src="app_bundle.js', 'Some unique text'], null, done); ['<script src="app_bundle.js', 'Some unique text'], null, done);
}); });
it('allows you to specify your own HTML template string', function(done) {
testHtmlPlugin({
entry: {app: path.join(__dirname, 'fixtures/index.js')},
output: {
path: OUTPUT_DIR,
filename: 'app_bundle.js'
},
plugins: [new HtmlWebpackPlugin({
templateContent: fs.readFileSync(path.join(__dirname, 'fixtures/test.html'), 'utf8')
})]
},
['<script src="app_bundle.js'], null, done);
});
it('allows you to inject the assets into a given html file', function (done) { it('allows you to inject the assets into a given html file', function (done) {
testHtmlPlugin({ testHtmlPlugin({
entry: { entry: {
@ -148,24 +139,6 @@ describe('HtmlWebpackPlugin', function() {
}, ['<script src="util_bundle.js"', '<script src="app_bundle.js"'], null, done); }, ['<script src="util_bundle.js"', '<script src="app_bundle.js"'], null, done);
}); });
it('allows you to inject the assets into a html string', function (done) {
testHtmlPlugin({
entry: {
util: path.join(__dirname, 'fixtures/util.js'),
app: path.join(__dirname, 'fixtures/index.js')
},
output: {
path: OUTPUT_DIR,
filename: '[name]_bundle.js'
},
plugins: [new HtmlWebpackPlugin({
inject: true,
chunks: ['util', 'app'],
templateContent: fs.readFileSync(path.join(__dirname, 'fixtures/plain.html'), 'utf8')
})]
}, ['<script src="util_bundle.js"', '<script src="app_bundle.js"'], null, done);
});
it('allows you to inject a specified asset into a given html file', function (done) { it('allows you to inject a specified asset into a given html file', function (done) {
testHtmlPlugin({ testHtmlPlugin({
entry: { entry: {
@ -202,34 +175,6 @@ describe('HtmlWebpackPlugin', function() {
}, ['<script src="app_bundle.js"'], null, done); }, ['<script src="app_bundle.js"'], null, done);
}); });
it('allows you to use the deprecated assets object', function (done) {
testHtmlPlugin({
entry: {
app: path.join(__dirname, 'fixtures/index.js')
},
output: {
path: OUTPUT_DIR,
filename: '[name]_bundle.js'
},
plugins: [new HtmlWebpackPlugin({template: path.join(__dirname, 'fixtures/legacy.html')})]
},
['<script src="app_bundle.js', 'Some unique text'], null, done, false, true);
});
it('allows you to use a deprecated legacy_index template', function (done) {
testHtmlPlugin({
entry: {
app: path.join(__dirname, 'fixtures/index.js')
},
output: {
path: OUTPUT_DIR,
filename: '[name]_bundle.js'
},
plugins: [new HtmlWebpackPlugin({template: path.join(__dirname, 'fixtures/legacy_default_index.html')})]
},
['<script src="app_bundle.js'], null, done, false, true);
});
it('allows you to specify your own HTML template function', function(done) { it('allows you to specify your own HTML template function', function(done) {
testHtmlPlugin({ testHtmlPlugin({
entry: {app: path.join(__dirname, 'fixtures/index.js')}, entry: {app: path.join(__dirname, 'fixtures/index.js')},
@ -246,24 +191,6 @@ describe('HtmlWebpackPlugin', function() {
['<script src="app_bundle.js"'], null, done); ['<script src="app_bundle.js"'], null, done);
}); });
it('registers a webpack error both template and template content are specified', function(done) {
webpack({
entry: path.join(__dirname, 'fixtures/index.js'),
output: {
path: OUTPUT_DIR,
filename: 'index_bundle.js'
},
plugins: [new HtmlWebpackPlugin({
template: path.join(__dirname, 'fixtures/test.html'),
templateContent: 'whatever'
})]
}, function(err, stats) {
expect(stats.hasErrors()).toBe(true);
expect(stats.toJson().errors[0]).toContain('HtmlWebpackPlugin');
done();
});
});
it('works with source maps', function(done) { it('works with source maps', function(done) {
testHtmlPlugin({ testHtmlPlugin({
devtool: 'sourcemap', devtool: 'sourcemap',
@ -489,21 +416,6 @@ describe('HtmlWebpackPlugin', function() {
expect(fs.existsSync(path.join(__dirname, 'fixtures/test.html'))).toBe(true); expect(fs.existsSync(path.join(__dirname, 'fixtures/test.html'))).toBe(true);
}); });
it('registers a webpack error if the template cannot be opened', function(done) {
webpack({
entry: path.join(__dirname, 'fixtures/index.js'),
output: {
path: OUTPUT_DIR,
filename: 'index_bundle.js'
},
plugins: [new HtmlWebpackPlugin({template: 'fixtures/does_not_exist.html'})]
}, function(err, stats) {
expect(stats.hasErrors()).toBe(true);
expect(stats.toJson().errors[0]).toContain('HtmlWebpackPlugin');
done();
});
});
it('exposes the webpack configuration to templates', function(done) { it('exposes the webpack configuration to templates', function(done) {
testHtmlPlugin({ testHtmlPlugin({
entry: { entry: {

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{%=o.htmlWebpackPlugin.options.title || 'Webpack App'%}</title>
</head>
<body>
{% for (var chunk in o.htmlWebpackPlugin.assets) { %}
<script src="{%=o.htmlWebpackPlugin.assets[chunk]%}"></script>
{% } %}
</body>
</html>

View File

@ -5,7 +5,7 @@
<title>Test</title> <title>Test</title>
</head> </head>
<body> <body>
<p>Public path is {%=o.webpackConfig.output.publicPath%}</p> <p>Public path is <%= webpackConfig.output.publicPath %></p>
<script src="{%=o.htmlWebpackPlugin.files.chunks.app.entry%}"></script> <script src="<%= htmlWebpackPlugin.files.chunks.app.entry %>"></script>
</body> </body>
</html> </html>