Skip to content

Commit

Permalink
Refactoring to support loaders
Browse files Browse the repository at this point in the history
  • Loading branch information
jantimon committed May 18, 2015
1 parent 025d770 commit d5160fe
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 198 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/node_modules/
/dist/
example/dist
15 changes: 3 additions & 12 deletions default_index.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
<!DOCTYPE html>
<html{% if(o.htmlWebpackPlugin.files.manifest) { %} manifest="{%= o.htmlWebpackPlugin.files.manifest %}"{% } %}>
<html>
<head>
<meta charset="UTF-8">
<title>{%=o.htmlWebpackPlugin.options.title || 'Webpack App'%}</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">
{% } %}
<title>{%=o.htmlWebpackPlugin.options.title %}</title>
</head>
<body>
{% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %}
<script src="{%=o.htmlWebpackPlugin.files.chunks[chunk].entry %}"></script>
{% } %}
</body>
</html>
</html>
9 changes: 0 additions & 9 deletions default_inject_index.html

This file was deleted.

1 change: 1 addition & 0 deletions example/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
document.body.innerHTML = 'Hello world!';
Binary file added example/favicon.ico
Binary file not shown.
14 changes: 14 additions & 0 deletions example/webpack.config.js
Original file line number Diff line number Diff line change
@@ -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'
})
]
};
2 changes: 2 additions & 0 deletions html-webpack-plugin-entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This is just a place holder - the real code lies inside
// html-webpack-plugin-loader.js
264 changes: 131 additions & 133 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,153 +1,164 @@
/* 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.errors.push('htmlWebpackPlugin.assets is deprecated - please use htmlWebpackPlugin.files instead');
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'));
}
// 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');
HtmlWebpackPlugin.prototype.evaluateCompilationResult = function(compilationResult) {
if(!compilationResult) {
return Promise.reject('The child compilation didn\'t provide a result');
}
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;
// 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;
},
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)
Expand All @@ -156,15 +167,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;
});
};

Expand All @@ -179,8 +192,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';
Expand Down Expand Up @@ -254,8 +265,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
Expand Down Expand Up @@ -298,18 +308,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
*/
Expand Down
Loading

0 comments on commit d5160fe

Please sign in to comment.