diff --git a/lib/precompiler.js b/lib/precompiler.js index 6ba3800cb..3827510a6 100644 --- a/lib/precompiler.js +++ b/lib/precompiler.js @@ -4,7 +4,17 @@ import fs from 'fs'; import * as Handlebars from './handlebars'; import {basename} from 'path'; import {SourceMapConsumer, SourceNode} from 'source-map'; -import uglify from 'uglify-js'; + +// We are using `require` instead of `import` here, because es6-modules do not allow +// dynamic imports and uglify-js is an optional dependency. +let uglify; +try { + uglify = require('uglify-js'); +} catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + throw err; + } +} module.exports.loadTemplates = function(opts, callback) { loadStrings(opts, function(err, strings) { @@ -244,12 +254,15 @@ module.exports.cli = function(opts) { output.map = output.map + ''; if (opts.min) { - output = uglify.minify(output.code, { - fromString: true, - - outSourceMap: opts.map, - inSourceMap: JSON.parse(output.map) - }); + if (uglify) { + output = uglify.minify(output.code, { + fromString: true, + outSourceMap: opts.map, + inSourceMap: JSON.parse(output.map) + }); + } else { + console.error('Code minimization is disabled due to missing uglify-js dependency'); + } } if (opts.map) { diff --git a/spec/precompiler.js b/spec/precompiler.js index 006a37e39..6b6364e29 100644 --- a/spec/precompiler.js +++ b/spec/precompiler.js @@ -12,6 +12,8 @@ describe('precompiler', function() { var log, logFunction, + errorLog, + errorLogFunction, precompile, minify, @@ -26,16 +28,55 @@ describe('precompiler', function() { content, writeFileSync; + /** + * Mock the Module.prototype.require-function such that an error is thrown, when "uglify-js" is loaded. + * + * The function cleans up its mess when "callback" is finished + * + * @param {Error} loadError the error that should be thrown if uglify is loaded + * @param {function} callback a callback-function to run when the mock is active. + */ + function mockRequireUglify(loadError, callback) { + var Module = require('module'); + var requireFunction = Module.prototype.require; + Module.prototype.require = function(moduleName) { + if (moduleName === 'uglify-js') { + throw loadError; + } + return requireFunction.call(this, moduleName); + }; + delete require.cache[require.resolve('uglify-js')]; + delete require.cache[require.resolve('../dist/cjs/precompiler')]; + try { + callback(); + } finally { + Module.prototype.require = requireFunction; + delete require.cache[require.resolve('uglify-js')]; + delete require.cache[require.resolve('../dist/cjs/precompiler')]; + } + } + + beforeEach(function() { precompile = Handlebars.precompile; minify = uglify.minify; writeFileSync = fs.writeFileSync; + // Mock stdout and stderr logFunction = console.log; log = ''; console.log = function() { log += Array.prototype.join.call(arguments, ''); }; + errorLogFunction = console.error; + errorLog = ''; + console.error = function() { + errorLog += Array.prototype.join.call(arguments, ''); + }; + // Mock module loading, "requireHook" can throw exceptions when loading a module + + + fs.writeFileSync = function(_file, _content) { file = _file; content = _content; @@ -46,6 +87,7 @@ describe('precompiler', function() { uglify.minify = minify; fs.writeFileSync = writeFileSync; console.log = logFunction; + console.error = errorLogFunction; }); it('should output version', function() { @@ -148,6 +190,28 @@ describe('precompiler', function() { equal(log, 'min'); }); + it('should omit minimization gracefully, if uglify-js is missing', function() { + let error = new Error("Cannot find module 'uglify-js'"); + error.code = 'MODULE_NOT_FOUND'; + mockRequireUglify(error, function() { + var Precompiler = require('../dist/cjs/precompiler'); + Handlebars.precompile = function() { return 'amd'; }; + Precompiler.cli({templates: [emptyTemplate], min: true}); + equal(/template\(amd\)/.test(log), true); + equal(/\n/.test(log), true); + equal(/Code minimization is disabled/.test(errorLog), true); + }); + }); + + it('should fail on errors (other than missing module) while loading uglify-js', function() { + mockRequireUglify(new Error('Mock Error'), function() { + shouldThrow(function() { + require('../dist/cjs/precompiler'); + }, Error, 'Mock Error'); + }); + }); + + it('should output map', function() { Precompiler.cli({templates: [emptyTemplate], map: 'foo.js.map'});