diff --git a/.gitignore b/.gitignore index 71daf201c..84d84d0b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.sublime-project *.sublime-workspace +*.code-workspace node_modules example/dist/ .ntvs_analysis.dat diff --git a/CHANGELOG.md b/CHANGELOG.md index 004d25530..25e0cf0b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ ### Fixed - Your improvement here... +## [1.7.2] - 2019-01-09 + +### Fixed +- Added `processSource` option to run handlebars template through html minifier for production optimizations (#175) + ## [1.7.1] - 2018-12-18 ### Fixed diff --git a/README.md b/README.md index 381a16946..2c71e6ffc 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ The following query (or config) options are supported: - *ignorePartials*: Prevents partial references from being fetched and bundled. Useful for manually loading partials at runtime. - *ignoreHelpers*: Prevents helper references from being fetched and bundled. Useful for manually loading helpers at runtime. - *precompileOptions*: Options passed to handlebars precompile. See the Handlebars.js [documentation](http://handlebarsjs.com/reference.html#base-precompile) for more information. + - *processSource*: Runs the handlebars templates through an html minifier to remove whitespace and reduce the size of the compiled templates. - *config*: Tells the loader where to look in the webpack config for configurations for this loader. Defaults to `handlebarsLoader`. - *config.partialResolver* You can specify a function to use for resolving partials. To do so, add to your webpack config: ```js diff --git a/index.js b/index.js index b02466800..407da8087 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ var path = require("path"); var assign = require("object-assign"); var fastreplace = require('./lib/fastreplace'); var findNestedRequires = require('./lib/findNestedRequires'); +var minify = require('html-minifier').minify; function versionCheck(hbCompiler, hbRuntime) { return hbCompiler.COMPILER_REVISION === (hbRuntime["default"] || hbRuntime).COMPILER_REVISION; @@ -26,7 +27,23 @@ function getLoaderConfig(loaderContext) { return assign({}, config, query); } -module.exports = function(source) { +/** + * Returns the template source with newlines and multiple spaces removed + * based on the processSource config option. + * + * @param {String} source + * @returns {String} + */ +var processSource = function (source) { + return minify(source, { + collapseWhitespace: true, + conservativeCollapse: true, + removeComments: true, + removeAttributeQuotes: true + }); +} + +module.exports = function (source) { if (this.cacheable) this.cacheable(); var loaderApi = this; var query = getLoaderConfig(loaderApi); @@ -42,8 +59,7 @@ module.exports = function(source) { var extensions = query.extensions; if (!extensions) { extensions = [".handlebars", ".hbs", ""]; - } - else if (!Array.isArray(extensions)) { + } else if (!Array.isArray(extensions)) { extensions = extensions.split(/[ ,;]/g); } @@ -57,7 +73,7 @@ module.exports = function(source) { var foundUnclearStuff = {}; var knownHelpers = {}; - [].concat(query.knownHelpers, precompileOptions.knownHelpers).forEach(function(k) { + [].concat(query.knownHelpers, precompileOptions.knownHelpers).forEach(function (k) { if (k && typeof k === 'string') { knownHelpers[k] = true; } @@ -77,12 +93,13 @@ module.exports = function(source) { var hb = handlebars.create(); var JavaScriptCompiler = hb.JavaScriptCompiler; + function MyJavaScriptCompiler() { JavaScriptCompiler.apply(this, arguments); } MyJavaScriptCompiler.prototype = Object.create(JavaScriptCompiler.prototype); MyJavaScriptCompiler.prototype.compiler = MyJavaScriptCompiler; - MyJavaScriptCompiler.prototype.nameLookup = function(parent, name, type) { + MyJavaScriptCompiler.prototype.nameLookup = function (parent, name, type) { if (debug) { console.log("nameLookup %s %s %s", parent, name, type); } @@ -96,26 +113,23 @@ module.exports = function(source) { } foundPartials["$" + name] = null; return JavaScriptCompiler.prototype.nameLookup.apply(this, arguments); - } - else if (type === "helper") { + } else if (type === "helper") { if (foundHelpers["$" + name]) { return "__default(require(" + loaderUtils.stringifyRequest(loaderApi, foundHelpers["$" + name]) + "))"; } foundHelpers["$" + name] = null; return JavaScriptCompiler.prototype.nameLookup.apply(this, arguments); - } - else if (type === "context") { + } else if (type === "context") { // This could be a helper too, save it to check it later if (!foundUnclearStuff["$" + name]) foundUnclearStuff["$" + name] = false; return JavaScriptCompiler.prototype.nameLookup.apply(this, arguments); - } - else { + } else { return JavaScriptCompiler.prototype.nameLookup.apply(this, arguments); } }; if (inlineRequires) { - MyJavaScriptCompiler.prototype.pushString = function(value) { + MyJavaScriptCompiler.prototype.pushString = function (value) { if (inlineRequires.test(value)) { this.pushLiteral("require(" + loaderUtils.stringifyRequest(loaderApi, value) + ")"); } else { @@ -138,17 +152,18 @@ module.exports = function(source) { // Define custom visitor for further template AST parsing var Visitor = handlebars.Visitor; + function InternalBlocksVisitor() { this.partialBlocks = []; this.inlineBlocks = []; } InternalBlocksVisitor.prototype = new Visitor(); - InternalBlocksVisitor.prototype.PartialBlockStatement = function(partial) { + InternalBlocksVisitor.prototype.PartialBlockStatement = function (partial) { this.partialBlocks.push(partial.name.original); Visitor.prototype.PartialBlockStatement.call(this, partial); }; - InternalBlocksVisitor.prototype.DecoratorBlock = function(partial) { + InternalBlocksVisitor.prototype.DecoratorBlock = function (partial) { if (partial.path.original === 'inline') { this.inlineBlocks.push(partial.params[0].value); } @@ -202,6 +217,9 @@ module.exports = function(source) { try { if (source) { + if (!!query.processSource) { + source = processSource(source); + } ast = hb.parse(source, opts); template = hb.precompile(ast, opts); } @@ -209,7 +227,7 @@ module.exports = function(source) { return loaderAsyncCallback(err); } - var resolve = function(request, type, callback) { + var resolve = function (request, type, callback) { var contexts = [loaderApi.context]; // Any additional helper dirs will be added to the searchable contexts @@ -222,7 +240,7 @@ module.exports = function(source) { contexts = contexts.concat(query.partialDirs); } - var resolveWithContexts = function() { + var resolveWithContexts = function () { var context = contexts.shift(); var traceMsg; @@ -232,28 +250,25 @@ module.exports = function(source) { console.log("request=%s", request); } - var next = function(err) { + var next = function (err) { if (contexts.length > 0) { resolveWithContexts(); - } - else { + } else { if (debug) console.log("Failed to resolve %s %s", type, traceMsg); return callback(err); } }; - loaderApi.resolve(context, request, function(err, result) { + loaderApi.resolve(context, request, function (err, result) { if (!err && result) { if (exclude && exclude.test(result)) { if (debug) console.log("Excluding %s %s", type, traceMsg); return next(); - } - else { + } else { if (debug) console.log("Resolved %s %s", type, traceMsg); return callback(err, result); } - } - else { + } else { return next(err); } }); @@ -262,14 +277,14 @@ module.exports = function(source) { resolveWithContexts(); }; - var resolveUnclearStuffIterator = function(stuff, unclearStuffCallback) { + var resolveUnclearStuffIterator = function (stuff, unclearStuffCallback) { if (foundUnclearStuff[stuff]) return unclearStuffCallback(); var request = referenceToRequest(stuff.substr(1), 'unclearStuff'); if (query.ignoreHelpers) { unclearStuffCallback(); } else { - resolve(request, 'unclearStuff', function(err, result) { + resolve(request, 'unclearStuff', function (err, result) { if (!err && result) { knownHelpers[stuff.substr(1)] = true; foundHelpers[stuff] = result; @@ -281,7 +296,7 @@ module.exports = function(source) { } }; - var defaultPartialResolver = function defaultPartialResolver(request, callback){ + var defaultPartialResolver = function defaultPartialResolver(request, callback) { request = referenceToRequest(request, 'partial'); // Try every extension for partials var i = 0; @@ -292,7 +307,7 @@ module.exports = function(source) { } var extension = extensions[i++]; - resolve(request + extension, 'partial', function(err, result) { + resolve(request + extension, 'partial', function (err, result) { if (!err && result) { return callback(null, result); } @@ -301,18 +316,18 @@ module.exports = function(source) { }()); }; - var resolvePartialsIterator = function(partial, partialCallback) { + var resolvePartialsIterator = function (partial, partialCallback) { if (foundPartials[partial]) return partialCallback(); // Strip the # off of the partial name var request = partial.substr(1); var partialResolver = query.partialResolver || defaultPartialResolver; - if(query.ignorePartials) { + if (query.ignorePartials) { return partialCallback(); } else { - partialResolver(request, function(err, resolved){ - if(err) { + partialResolver(request, function (err, resolved) { + if (err) { var visitor = new InternalBlocksVisitor(); visitor.accept(ast); @@ -334,20 +349,20 @@ module.exports = function(source) { } }; - var resolveHelpersIterator = function(helper, helperCallback) { + var resolveHelpersIterator = function (helper, helperCallback) { if (foundHelpers[helper]) return helperCallback(); var request = referenceToRequest(helper.substr(1), 'helper'); if (query.ignoreHelpers) { helperCallback(); } else { - var defaultHelperResolver = function(request, callback){ + var defaultHelperResolver = function (request, callback) { return resolve(request, 'helper', callback); }; var helperResolver = query.helperResolver || defaultHelperResolver; - helperResolver(request, function(err, result) { + helperResolver(request, function (err, result) { if (!err && result) { knownHelpers[helper.substr(1)] = true; foundHelpers[helper] = result; @@ -363,7 +378,7 @@ module.exports = function(source) { } }; - var doneResolving = function(err) { + var doneResolving = function (err) { if (err) return loaderAsyncCallback(err); // Do another compiler pass if not everything was resolved @@ -374,15 +389,15 @@ module.exports = function(source) { // export as module if template is not blank var slug = template ? - 'var Handlebars = require(' + loaderUtils.stringifyRequest(loaderApi, runtimePath) + ');\n' - + 'function __default(obj) { return obj && (obj.__esModule ? obj["default"] : obj); }\n' - + 'module.exports = (Handlebars["default"] || Handlebars).template(' + template + ');' : + 'var Handlebars = require(' + loaderUtils.stringifyRequest(loaderApi, runtimePath) + ');\n' + + 'function __default(obj) { return obj && (obj.__esModule ? obj["default"] : obj); }\n' + + 'module.exports = (Handlebars["default"] || Handlebars).template(' + template + ');' : 'module.exports = function(){return "";};'; loaderAsyncCallback(null, slug); }; - var resolveItems = function(err, type, items, iterator, callback) { + var resolveItems = function (err, type, items, iterator, callback) { if (err) return callback(err); var itemKeys = Object.keys(items); @@ -395,18 +410,18 @@ module.exports = function(source) { async.each(itemKeys, iterator, callback); }; - var resolvePartials = function(err) { + var resolvePartials = function (err) { resolveItems(err, 'partials', foundPartials, resolvePartialsIterator, doneResolving); }; - var resolveUnclearStuff = function(err) { + var resolveUnclearStuff = function (err) { resolveItems(err, 'unclearStuff', foundUnclearStuff, resolveUnclearStuffIterator, resolvePartials); }; - var resolveHelpers = function(err) { + var resolveHelpers = function (err) { resolveItems(err, 'helpers', foundHelpers, resolveHelpersIterator, resolveUnclearStuff); }; resolveHelpers(); }()); -}; +}; \ No newline at end of file diff --git a/package.json b/package.json index 8f87c0f8f..4865b0f6c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "contributors": [ "Alan @altano", "Tobias Koppers @sokra", - "Jason Anderson @diurnalist" + "Jason Anderson @diurnalist", + "Patrick Szczypinski @pizatski" ], "maintainers": [ "Paul Carduner @pcardune" @@ -12,6 +13,7 @@ "dependencies": { "async": "~0.2.10", "fastparse": "^1.0.0", + "html-minifier": "^3.5.21", "loader-utils": "1.0.x", "object-assign": "^4.1.0" }, @@ -28,7 +30,7 @@ "type": "git", "url": "git://github.com/pcardune/handlebars-loader.git" }, - "version": "1.7.1", + "version": "1.7.2", "devDependencies": { "coveralls": "^2.11.8", "eslint": "^2.8.0", diff --git a/test/test.js b/test/test.js index 683d5906b..dc105812f 100644 --- a/test/test.js +++ b/test/test.js @@ -556,4 +556,20 @@ describe('handlebars-loader', function () { }); }); -}); +/* + * This uses an html-minifier with 'conservative whitespace stripping' on. This means you'll still get a single space around each + * var in the handlebars template which can result in two spaces between tags. + */ + it('should output with fewer than three spaces and no newlines when processSource option is set', function (done) { + testTemplate(loader, './simple.handlebars', { + query: '?processSource=true', + data: TEST_TEMPLATE_DATA + }, function (err, output, require) { + assert.ok(output, 'generated output'); + assert.ok(output.indexOf('\n') === -1); + assert.ok(output.indexOf(' ') === -1); + done(); + }); + }); + +}); \ No newline at end of file