Refactoring to support loaders

This commit is contained in:
Jan Nicklas 2015-05-18 14:28:02 +02:00 committed by Jan Nicklas
parent f68b6e3783
commit c8a69255b9
11 changed files with 164 additions and 199 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/node_modules/
/dist/
example/dist

View File

@ -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>

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>

1
example/example.js Executable file
View File

@ -0,0 +1 @@
document.body.innerHTML = 'Hello world!';

BIN
example/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

14
example/webpack.config.js Executable file
View File

@ -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'
})
]
};

View File

@ -0,0 +1,2 @@
// This is just a place holder - the real code lies inside
// html-webpack-plugin-loader.js

266
index.js
View File

@ -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
*/

10
loader.js Normal file
View File

@ -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 + "');";
};

View File

@ -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"
}
}

View File

@ -202,34 +202,6 @@ describe('HtmlWebpackPlugin', function() {
}, ['<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) {
testHtmlPlugin({
entry: {app: path.join(__dirname, 'fixtures/index.js')},
@ -489,21 +461,6 @@ describe('HtmlWebpackPlugin', function() {
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) {
testHtmlPlugin({
entry: {