diff --git a/README.md b/README.md index 100b65a..5858bfb 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/examples/jade-loader/webpack.config.js b/examples/jade-loader/webpack.config.js index 3786711..e2df6c2 100755 --- a/examples/jade-loader/webpack.config.js +++ b/examples/jade-loader/webpack.config.js @@ -14,6 +14,7 @@ module.exports = { { test: /\.jade$/, loader: 'jade'} ] }, + devtool: 'eval', plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', diff --git a/examples/javascript/readme.md b/examples/javascript/readme.md index 5866653..06f9bcc 100644 --- a/examples/javascript/readme.md +++ b/examples/javascript/readme.md @@ -1,3 +1,5 @@ # isomorphic javascript example -This example shows how to generate a template on the fly using javascript. \ No newline at end of file +This example shows how to generate a template on the fly using javascript. + +The best way to debug the compilation result is `devTool:eval` \ No newline at end of file diff --git a/examples/javascript/universial.js b/examples/javascript/universial.js index 58b8872..1e9f132 100644 --- a/examples/javascript/universial.js +++ b/examples/javascript/universial.js @@ -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'); }; \ No newline at end of file diff --git a/examples/javascript/webpack.config.js b/examples/javascript/webpack.config.js index 8df7eec..e468e7b 100644 --- a/examples/javascript/webpack.config.js +++ b/examples/javascript/webpack.config.js @@ -14,6 +14,7 @@ module.exports = { { test: /\.html$/, loader: 'html-loader' } ] }, + devtool: 'eval', plugins: [ new HtmlWebpackPlugin({ template: 'template.js' diff --git a/index.js b/index.js index dff2462..37aded4 100644 --- a/index.js +++ b/index.js @@ -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()) diff --git a/lib/compiler.js b/lib/compiler.js new file mode 100644 index 0000000..4e2c5b3 --- /dev/null +++ b/lib/compiler.js @@ -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) + '"'; +} diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..43a9464 --- /dev/null +++ b/lib/errors.js @@ -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
\n' + this.toString() + ''; + }, + toJsonHtml: function() { + return JSON.stringify(this.toHtml()); + }, + toString: function() { + return prettyError.render(err).replace(/webpack:\/\/\/\./g, context); + } + }; +}; \ No newline at end of file diff --git a/loader.js b/lib/loader.js similarity index 89% rename from loader.js rename to lib/loader.js index 2b8cce7..d73fc3a 100644 --- a/loader.js +++ b/lib/loader.js @@ -1,3 +1,4 @@ +/* This loader renders the template with underscore if no other loader was found */ 'use strict'; var _ = require('lodash'); diff --git a/package.json b/package.json index 377f938..ed70267 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/spec/HtmlWebpackPluginSpec.js b/spec/HtmlWebpackPluginSpec.js index a4a00cb..57c1cb3 100644 --- a/spec/HtmlWebpackPluginSpec.js +++ b/spec/HtmlWebpackPluginSpec.js @@ -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) {