Move compiler to its own file

Improve error messages
Fix global HTML_WEBPACK_PLUGIN variable
This commit is contained in:
Jan Nicklas 2016-01-11 22:00:47 +01:00 committed by Jan Nicklas
parent 54528a5b41
commit 1622c7d2e2
11 changed files with 155 additions and 98 deletions

View File

@ -68,6 +68,8 @@ Allowed values are as follows:
- `minify`: `{...} | false` 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
included scripts and css files. This is useful for cache busting.
- `cache`: `true | false` if `true` (default) try to emit the file only if it was changed.
- `showErrors`: `true | false` if `true` (default) errors details will be written into the html page.
- `chunks`: Allows you to add only some chunks (e.g. only the unit-test chunk)
- `chunksSortMode`: Allows to controll how chunks should be sorted before they are included to the html. Allowed values: 'none' | 'default' | {function} - default: 'auto'
- `excludeChunks`: Allows you to skip some chunks (e.g. don't add the unit-test chunk)

View File

@ -14,6 +14,7 @@ module.exports = {
{ test: /\.jade$/, loader: 'jade'}
]
},
devtool: 'eval',
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',

View File

@ -1,3 +1,5 @@
# isomorphic javascript example
This example shows how to generate a template on the fly using javascript.
This example shows how to generate a template on the fly using javascript.
The best way to debug the compilation result is `devTool:eval`

View File

@ -1,5 +1,10 @@
// This file is used for frontend and backend
'use strict';
// If compiled by the html-webpack-plugin
// HTML_WEBPACK_PLUGIN is set to true:
var backend = typeof HTML_WEBPACK_PLUGIN !== 'undefined';
module.exports = function() {
return "Hello World";
return 'Hello World from ' + (backend ? 'backend' : 'frontend');
};

View File

@ -14,6 +14,7 @@ module.exports = {
{ test: /\.html$/, loader: 'html-loader' }
]
},
devtool: 'eval',
plugins: [
new HtmlWebpackPlugin({
template: 'template.js'

115
index.js
View File

@ -4,15 +4,10 @@ var fs = require('fs');
var _ = require('lodash');
var Promise = require('bluebird');
var path = require('path');
var childCompiler = require('./lib/compiler.js');
var prettyError = require('./lib/errors.js');
Promise.promisifyAll(fs);
var webpack = require('webpack');
var NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
var LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
var LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
function HtmlWebpackPlugin(options) {
// Default options
this.options = _.extend({
@ -24,13 +19,14 @@ function HtmlWebpackPlugin(options) {
favicon: false,
minify: false,
cache: true,
showErrors: true,
chunks: 'all',
excludeChunks: [],
title: 'Webpack App'
}, options);
// If the template doesn't use a loader use the lodash template loader
if(this.options.template.indexOf('!') === -1) {
this.options.template = require.resolve('./loader.js') + '!' + path.resolve(this.options.template);
this.options.template = require.resolve('./lib/loader.js') + '!' + path.resolve(this.options.template);
}
// Resolve template path
this.options.template = this.options.template.replace(
@ -42,17 +38,26 @@ function HtmlWebpackPlugin(options) {
HtmlWebpackPlugin.prototype.apply = function(compiler) {
var self = this;
var isCompilationCached = false;
var compilationPromise;
self.context = compiler.context;
compiler.plugin('make', function(compilation, callback) {
// Compile the template (queued)
compilationPromise = getNextCompilationSlot(compiler, function() {
return self.compileTemplate(self.options.template, self.options.filename, compilation)
return childCompiler.compileTemplate(self.options.template, compiler.context, self.options.filename, compilation)
.catch(function(err) {
return new Error(err);
compilation.errors.push(prettyError(err, compiler.context).toString());
return {
content: self.options.showErrors ? prettyError(err, compiler.context).toJsonHtml() : 'ERROR',
};
})
.finally(callback);
.then(function(compilationResult) {
// If the compilation change didnt change the cache is valid
isCompilationCached = compilationResult.hash && self.hash === compilationResult.hash;
self.hash = compilation.hash;
callback();
return compilationResult.content;
});
});
});
@ -73,7 +78,7 @@ HtmlWebpackPlugin.prototype.apply = function(compiler) {
// If the template and the assets did not change we don't have to emit the html
var assetJson = JSON.stringify(assets);
if (self.options.cache && !self.built && assetJson === self.assetJson) {
if (isCompilationCached && self.options.cache && assetJson === self.assetJson) {
return callback();
} else {
self.assetJson = assetJson;
@ -94,9 +99,6 @@ HtmlWebpackPlugin.prototype.apply = function(compiler) {
return compilationPromise;
})
.then(function(compiledTemplate) {
if (compiledTemplate instanceof Error) {
return Promise.reject(compiledTemplate);
}
// Allow to use a custom function / string instead
if (self.options.templateContent) {
return self.options.templateContent;
@ -135,9 +137,8 @@ HtmlWebpackPlugin.prototype.apply = function(compiler) {
.catch(function(err) {
// In case anything went wrong the promise is resolved
// with the error message and an error is logged
var errorMessage = "HtmlWebpackPlugin " + err;
compilation.errors.push(new Error(errorMessage));
return errorMessage;
compilation.errors.push(prettyError(err, compiler.context).toString());
return self.options.showErrors ? prettyError(err, compiler.context).toHtml() : 'ERROR';
})
.then(function(html) {
// Replace the compilation result with the evaluated html code
@ -168,74 +169,15 @@ HtmlWebpackPlugin.prototype.apply = function(compiler) {
});
};
/**
* 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
};
var cachedAsset = compilation.assets[outputOptions.filename];
// 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),
new NodeTargetPlugin(),
new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var'),
new SingleEntryPlugin(this.context, template),
new LoaderTargetPlugin('node'),
new webpack.DefinePlugin({ HTML_WEBPACK_PLUGIN : 'true' })
);
// Compile and return a promise
return new Promise(function (resolve, reject) {
childCompiler.runAsChild(function(err, entries, childCompilation) {
compilation.assets[outputOptions.filename] = cachedAsset;
if (cachedAsset === undefined) {
delete compilation.assets[outputOptions.filename];
}
// Resolve / reject the promise
if (childCompilation.errors && childCompilation.errors.length) {
var errorDetails = childCompilation.errors.map(function(error) {
return error.message + (error.error ? ':\n' + error.error: '');
}).join('\n');
reject('Child compilation failed:\n' + errorDetails);
} else {
this.built = this.hash !== entries[0].hash;
this.hash = entries[0].hash;
resolve(childCompilation.assets[outputOptions.filename]);
}
}.bind(this));
}.bind(this));
};
/**
* Evaluates the child compilation result
* Returns a promise
*/
HtmlWebpackPlugin.prototype.evaluateCompilationResult = function(compilation, compilationResult) {
if(!compilationResult) {
HtmlWebpackPlugin.prototype.evaluateCompilationResult = function(compilation, source) {
if (!source) {
return Promise.reject('The child compilation didn\'t provide a result');
}
var source = compilationResult.source();
// The LibraryTemplatePlugin stores the template result in a local variable.
// To extract the result during the evaluation this part has to be removed.
source = source.replace('var HTML_WEBPACK_PLUGIN_RESULT =', '');
@ -243,15 +185,8 @@ HtmlWebpackPlugin.prototype.evaluateCompilationResult = function(compilation, co
// Evaluate code and cast to string
var newSource;
try {
newSource = vm.runInThisContext(source);
newSource = vm.runInNewContext(source, {HTML_WEBPACK_PLUGIN: true}, {filename: 'html-plugin-evaluation'});
} catch (e) {
// Log syntax error
var syntaxError = require('syntax-error')(source);
var errorMessage = 'Template compilation failed: ' + e +
(syntaxError ? '\n' + syntaxError + '\n\n\n' + source.split('\n').map(function(row, i) {
return (1 + i) + ' - ' + row;
}).join('\n') : '');
compilation.errors.push(new Error(errorMessage));
return Promise.reject(e);
}
return typeof newSource === 'string' || typeof newSource === 'function' ?
@ -556,8 +491,8 @@ HtmlWebpackPlugin.prototype.appendHash = function (url, hash) {
};
/**
* Helper to prevent compilation in parallel
* Fixes an issue where incomplete cache modules were used
* Helper to prevent html-plugin compilation in parallel
* Fixes "No source available" where incomplete cache modules were used
*/
function getNextCompilationSlot(compiler, newEntry) {
compiler.HtmlWebpackPluginQueue = (compiler.HtmlWebpackPluginQueue || Promise.resolve())

87
lib/compiler.js Normal file
View File

@ -0,0 +1,87 @@
/*
* This file uses webpack to compile a template with a child compiler.
*
* [TEMPLATE] -> [JAVASCRIPT]
*
*/
'use strict';
var Promise = require('bluebird');
var path = require('path');
var webpack = require('webpack');
var NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin');
var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
var LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
var LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin');
var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
/**
* Compiles the template into a nodejs factory, adds its to the compilation.assets
* and returns a promise of the result asset object.
*
* @param template relative path to the template file
* @param context path context
* @param outputFilename the file name
* @param compilation The webpack compilation object
*
* Returns an object:
* {
* hash: {String} - Base64 hash of the file
* content: {String} - Javascript executable code of the template
* }
*
*/
module.exports.compileTemplate = function compileTemplate(template, context, 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
};
var cachedAsset = compilation.assets[outputOptions.filename];
// 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 = getCompilerName(context, outputFilename);
var childCompiler = compilation.createChildCompiler(compilerName, outputOptions);
childCompiler.apply(
new NodeTemplatePlugin(outputOptions),
new NodeTargetPlugin(),
new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var'),
new SingleEntryPlugin(this.context, template),
new LoaderTargetPlugin('node')
);
// Compile and return a promise
return new Promise(function (resolve, reject) {
childCompiler.runAsChild(function(err, entries, childCompilation) {
compilation.assets[outputOptions.filename] = cachedAsset;
if (cachedAsset === undefined) {
delete compilation.assets[outputOptions.filename];
}
// Resolve / reject the promise
if (childCompilation.errors && childCompilation.errors.length) {
var errorDetails = childCompilation.errors.map(function(error) {
return error.message + (error.error ? ':\n' + error.error: '');
}).join('\n');
reject(new Error('Child compilation failed:\n' + errorDetails));
} else {
resolve({
// Hash of the template entry point
hash: entries[0].hash,
// Compiled code
content: childCompilation.assets[outputOptions.filename].source()
});
}
});
});
};
/**
* Returns the child compiler name e.g. 'html-webpack-plugin for "index.html"'
*/
function getCompilerName (context, filename) {
var absolutePath = path.resolve(context, filename);
var relativePath = path.relative(context, absolutePath);
return 'html-webpack-plugin for "' + (absolutePath.length < relativePath.length ? absolutePath : relativePath) + '"';
}

23
lib/errors.js Normal file
View File

@ -0,0 +1,23 @@
'use strict';
var PrettyError = require('pretty-error');
var prettyError = new PrettyError();
prettyError.withoutColors();
prettyError.skipPackage(['html-plugin-evaluation']);
prettyError.skipNodeFiles();
prettyError.skip(function(traceLine) {
return traceLine.path === 'html-plugin-evaluation';
});
module.exports = function(err, context) {
return {
toHtml: function() {
return 'Html Webpack Plugin:\n<pre>\n' + this.toString() + '</pre>';
},
toJsonHtml: function() {
return JSON.stringify(this.toHtml());
},
toString: function() {
return prettyError.render(err).replace(/webpack:\/\/\/\./g, context);
}
};
};

View File

@ -1,3 +1,4 @@
/* This loader renders the template with underscore if no other loader was found */
'use strict';
var _ = require('lodash');

View File

@ -1,6 +1,6 @@
{
"name": "html-webpack-plugin",
"version": "2.5.0",
"version": "2.6.0",
"description": "Simplifies creation of HTML files to serve your webpack bundles",
"main": "index.js",
"files": [
@ -45,10 +45,10 @@
"webpack": "^1.12.10"
},
"dependencies": {
"loader-utils": "^0.2.12",
"syntax-error": "^1.1.4",
"bluebird": "^3.1.1",
"html-minifier": "^1.1.1",
"lodash": "^3.10.1"
"loader-utils": "^0.2.12",
"lodash": "^3.10.1",
"pretty-error": "^2.0.0"
}
}

View File

@ -112,7 +112,7 @@ describe('HtmlWebpackPlugin', function() {
template: path.join(__dirname, 'fixtures/invalid.html')
})]
},
['HtmlWebpackPlugin ReferenceError: foo is not defined'], null, done, true);
['ReferenceError: foo is not defined'], null, done, true);
});
it('uses a custom loader from webpacks config', function(done) {
@ -840,7 +840,7 @@ describe('HtmlWebpackPlugin', function() {
template: path.join(__dirname, 'fixtures/non-existing-template.html')
})
]
}, ["HtmlWebpackPlugin Error: Child compilation failed:\nEntry module not found: Error: Cannot resolve 'file' or 'directory'"], null, done, true);
}, ["Child compilation failed:\n Entry module not found: Error: Cannot resolve 'file' or 'directory'"], null, done, true);
});
it('should short the chunks', function(done) {