diff --git a/.gitignore b/.gitignore
index b0a5c34..e89a4ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/node_modules/
/dist/
+example/dist
diff --git a/default_index.html b/default_index.html
index 69d3384..5ba181b 100644
--- a/default_index.html
+++ b/default_index.html
@@ -1,18 +1,9 @@
-
+
- {%=o.htmlWebpackPlugin.options.title || 'Webpack App'%}
- {% if (o.htmlWebpackPlugin.files.favicon) { %}
-
- {% } %}
- {% for (var css in o.htmlWebpackPlugin.files.css) { %}
-
- {% } %}
+ {%=o.htmlWebpackPlugin.options.title %}
- {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %}
-
- {% } %}
-
+
\ No newline at end of file
diff --git a/default_inject_index.html b/default_inject_index.html
deleted file mode 100644
index 6967979..0000000
--- a/default_inject_index.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
- {%=o.htmlWebpackPlugin.options.title || 'Webpack App'%}
-
-
-
-
diff --git a/example/example.js b/example/example.js
new file mode 100755
index 0000000..80372b0
--- /dev/null
+++ b/example/example.js
@@ -0,0 +1 @@
+document.body.innerHTML = 'Hello world!';
\ No newline at end of file
diff --git a/example/favicon.ico b/example/favicon.ico
new file mode 100644
index 0000000..be74abd
Binary files /dev/null and b/example/favicon.ico differ
diff --git a/example/webpack.config.js b/example/webpack.config.js
new file mode 100755
index 0000000..e090443
--- /dev/null
+++ b/example/webpack.config.js
@@ -0,0 +1,14 @@
+var HtmlWebpackPlugin = require('..');
+module.exports = {
+ entry: './example.js',
+ output: {
+ path: __dirname + "/dist",
+ publicPath: '/',
+ filename: "bundle.js"
+ },
+ plugins: [
+ new HtmlWebpackPlugin({
+ favicon: 'favicon.ico'
+ })
+ ]
+};
\ No newline at end of file
diff --git a/html-webpack-plugin-entry.js b/html-webpack-plugin-entry.js
new file mode 100644
index 0000000..8d0de2b
--- /dev/null
+++ b/html-webpack-plugin-entry.js
@@ -0,0 +1,2 @@
+// This is just a place holder - the real code lies inside
+// html-webpack-plugin-loader.js
\ No newline at end of file
diff --git a/index.js b/index.js
index f014fc8..bb974f6 100644
--- a/index.js
+++ b/index.js
@@ -1,152 +1,163 @@
+/* global escape */
'use strict';
+var vm = require('vm');
var fs = require('fs');
-var path = require('path');
var _ = require('lodash');
var tmpl = require('blueimp-tmpl').tmpl;
var Promise = require('bluebird');
+var path = require('path');
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) {
- this.options = options || {};
+ // Default options
+ this.options = _.extend({
+ template: __dirname + '/default_index.html',
+ filename: 'index.html',
+ hash: false,
+ inject: true,
+ compile: true,
+ favicon: false,
+ chunks: 'all',
+ excludeChunks: [],
+ title: 'Webpack App'
+ }, options);
+ // If the template doesn't use a loader use the raw loader
+ if(this.options.template.indexOf('!') === -1) {
+ this.options.template = 'raw!' + this.options.template;
+ }
}
HtmlWebpackPlugin.prototype.apply = function(compiler) {
var self = this;
- compiler.plugin('emit', function(compilation, compileCallback) {
- var webpackStatsJson = compilation.getStats().toJson();
- var outputFilename = self.options.filename || 'index.html';
- Promise.resolve()
- // Add the favicon
- .then(function(callback) {
- if (self.options.favicon) {
- return self.addFileToAssets(compilation, self.options.favicon, callback);
+ compiler.plugin('make', function(compilation, compilerCallback) {
+ // The entry file is just an empty helper as the dynamic template
+ // require is added in "loader.js"
+ var entryFilename = path.resolve(__dirname, 'html-webpack-plugin-entry.js');
+ var entryRequest = require.resolve('./loader.js') + '?' + escape(JSON.stringify(self.options.template)) + '!' + entryFilename;
+ var outputOptions = {
+ filename: self.options.filename,
+ 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 childCompiler = compilation.createChildCompiler('html-webpack-plugin', outputOptions);
+ childCompiler.apply(new NodeTemplatePlugin(outputOptions));
+ childCompiler.apply(new LibraryTemplatePlugin('result', 'var'));
+ childCompiler.apply(new NodeTargetPlugin());
+ childCompiler.apply(new SingleEntryPlugin(this.context, entryRequest));
+ // Create a subCache (copied from https://github.com/SanderSpies/extract-text-webpack-plugin/blob/master/loader.js)
+ var subCache = 'HtmlWebpackPlugin-' + self.options.filename;
+ childCompiler.plugin('compilation', function(compilation) {
+ if(compilation.cache) {
+ if(!compilation.cache[subCache]) {
+ compilation.cache[subCache] = {};
}
- })
- // 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.warnings.push(new Error('HtmlWebPackPlugin: htmlWebpackPlugin.assets is deprecated - please use inject or htmlWebpackPlugin.files instead' +
- '\nsee: https://github.com/ampedandwired/html-webpack-plugin/issues/52'));
- return assets;
- }
- });
+ compilation.cache = compilation.cache[subCache];
+ }
+ });
+ childCompiler.runAsChild(compilerCallback);
+ });
- // Get/generate html
- return self.getTemplateContent(compilation, templateParams)
- .then(function(htmlTemplateContent) {
- // Compile and add html to compilation
- return self.emitHtml(compilation, htmlTemplateContent, templateParams, outputFilename);
- });
+ // Once everything is compiled we evaluate the html factory
+ // and replace it with its content
+ compiler.plugin('emit', function(compilation, callback) {
+ self.evaluateCompilationResult(compilation.assets[self.options.filename])
+ .then(function(html) {
+ // Add the assets to the resulting html
+ return self.postProcessHtml(html, compilation);
})
- // In case anything went wrong let the user know
.catch(function(err) {
- compilation.errors.push(err);
- compilation.assets[outputFilename] = {
+ // In case anything went wrong the promise is resolved
+ // 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() {
- return err.toString();
+ return html;
},
size: function() {
- return err.toString().length;
+ return html.length;
}
};
- })
- // Tell the compiler to proceed
- .finally(compileCallback);
- });
+ callback();
+ });
+ });
};
/**
- * 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
+ * Evaluates the child compilation result
+ * 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'));
+HtmlWebpackPlugin.prototype.evaluateCompilationResult = function(compilationResult) {
+ if(!compilationResult) {
+ return Promise.reject('The child compilation didn\'t provide a result');
}
- // 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 returns 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 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 + '"'));
- });
+ // Strip the leading 'var '
+ var source = compilationResult.source().substr(3);
+ // Evaluate code and cast to string
+ var newSource = vm.runInThisContext(source);
+ return typeof newSource === 'string' ?
+ Promise.resolve(newSource) :
+ Promise.reject('The loader "' + this.options.template + '" didn\'t return html.');
};
-/*
- * 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) {
- var html;
- // blueimp-tmpl processing
- try {
- html = tmpl(htmlTemplateContent, templateParams);
- } catch(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;
- html = minify(html, this.options.minify);
- }
-
- compilation.assets[outputFilename] = {
- source: function() {
- return html;
- },
- size: function() {
- return html.length;
- }
- };
+HtmlWebpackPlugin.prototype.postProcessHtml = function(html, compilation) {
+ var self = this;
+ var webpackStatsJson = compilation.getStats().toJson();
+ var assets = self.htmlWebpackPluginAssets(compilation, webpackStatsJson, self.options.chunks, self.options.excludeChunks);
+ return Promise.resolve()
+ // Favicon
+ .then(function() {
+ if (self.options.favicon) {
+ return self.addFileToAssets(self.options.favicon, compilation)
+ .then(function(faviconBasename){
+ assets.favicon = faviconBasename;
+ });
+ }
+ })
+ // Template processing
+ .then(function() {
+ var templateParams = {
+ webpack: webpackStatsJson,
+ webpackConfig: compilation.options,
+ htmlWebpackPlugin: {
+ files: assets,
+ options: self.options,
+ }
+ };
+ if (self.options.compile === true) {
+ html = tmpl(html, templateParams);
+ }
+ })
+ // Inject
+ .then(function() {
+ if (self.options.inject) {
+ return self.injectAssetsIntoHtml(html, assets);
+ } else {
+ return html;
+ }
+ });
};
/*
* 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({
size: fs.statAsync(filename),
source: fs.readFileAsync(filename)
@@ -155,15 +166,17 @@ HtmlWebpackPlugin.prototype.addFileToAssets = function(compilation, filename) {
return Promise.reject(new Error('HtmlWebpackPlugin: could not load file ' + filename));
})
.then(function(results) {
+ var basename = path.basename(filename);
compilation.fileDependencies.push(filename);
- compilation.assets[path.basename(filename)] = {
+ compilation.assets[basename] = {
source: function() {
return results.source;
},
size: function() {
- return results.size;
+ return results.size.size;
}
};
+ return basename;
});
};
@@ -186,8 +199,6 @@ 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';
@@ -267,8 +278,7 @@ HtmlWebpackPlugin.prototype.htmlWebpackPluginAssets = function(compilation, webp
/**
* Injects the assets into the given html string
*/
-HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, templateParams) {
- var assets = templateParams.htmlWebpackPlugin.files;
+HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets) {
var chunks = Object.keys(assets.chunks);
// Gather all css and script files
@@ -298,7 +308,7 @@ HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, templateParams
head = head.concat(styles);
// Add scripts to body or head
if (this.options.inject === 'head') {
- head = head.concat(scripts);
+ head = body.concat(scripts);
} else {
body = body.concat(scripts);
}
@@ -323,18 +333,6 @@ HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, templateParams
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
*/
diff --git a/loader.js b/loader.js
new file mode 100644
index 0000000..8250579
--- /dev/null
+++ b/loader.js
@@ -0,0 +1,10 @@
+'use strict';
+
+module.exports = function() {
+ if (this.cacheable) {
+ this.cacheable();
+ }
+ var template = JSON.parse(decodeURIComponent(this.query.substr(1)));
+ return "module.exports = require('" + template + "');";
+};
+
diff --git a/package.json b/package.json
index e587ebb..fc97e49 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,6 @@
"bluebird": "^2.9.34",
"blueimp-tmpl": "^2.5.4",
"html-minifier": "^0.7.2",
- "lodash": "^3.10.0"
+ "lodash": "~3.9.3"
}
}
diff --git a/spec/HtmlWebpackPluginSpec.js b/spec/HtmlWebpackPluginSpec.js
index a8bde34..ccbe3bd 100644
--- a/spec/HtmlWebpackPluginSpec.js
+++ b/spec/HtmlWebpackPluginSpec.js
@@ -202,34 +202,6 @@ describe('HtmlWebpackPlugin', function() {
}, ['