Skip to content

Commit d5caa56

Browse files
committed
Gracefully handle missing uglify-js dependency
closes #1391 uglify-js is an optional dependency and should be treated as such. This commit gracefully handles MODULE_NOT_FOUND errors while loading uglify. - Check for existing uglify-js (and load uglify-js) only if minification was activated - Use "require.resolve" to check if uglify exists. Otherwise, a missing dependency of uglify-js would cause the same behavior as missing uglify-js. (Only a warning, no error) - The code to load and run uglify is put into a single for readability purposes - Tests use a mockup Module._resolveFilename to simulate the missing module. This function is used by both "require" and "require.resolve", so both are mocked equally.
1 parent ce3cd8a commit d5caa56

2 files changed

Lines changed: 93 additions & 8 deletions

File tree

lib/precompiler.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import fs from 'fs';
44
import * as Handlebars from './handlebars';
55
import {basename} from 'path';
66
import {SourceMapConsumer, SourceNode} from 'source-map';
7-
import uglify from 'uglify-js';
7+
88

99
module.exports.loadTemplates = function(opts, callback) {
1010
loadStrings(opts, function(err, strings) {
@@ -235,7 +235,6 @@ module.exports.cli = function(opts) {
235235
}
236236
}
237237

238-
239238
if (opts.map) {
240239
output.add('\n//# sourceMappingURL=' + opts.map + '\n');
241240
}
@@ -244,12 +243,7 @@ module.exports.cli = function(opts) {
244243
output.map = output.map + '';
245244

246245
if (opts.min) {
247-
output = uglify.minify(output.code, {
248-
fromString: true,
249-
250-
outSourceMap: opts.map,
251-
inSourceMap: JSON.parse(output.map)
252-
});
246+
output = minify(output, opts.map);
253247
}
254248

255249
if (opts.map) {
@@ -271,3 +265,33 @@ function arrayCast(value) {
271265
}
272266
return value;
273267
}
268+
269+
/**
270+
* Run uglify to minify the compiled template, if uglify exists in the dependencies.
271+
*
272+
* We are using `require` instead of `import` here, because es6-modules do not allow
273+
* dynamic imports and uglify-js is an optional dependency. Since we are inside NodeJS here, this
274+
* should not be a problem.
275+
*
276+
* @param {string} output the compiled template
277+
* @param {string} sourceMapFile the file to write the source map to.
278+
*/
279+
function minify(output, sourceMapFile) {
280+
try {
281+
// Try to resolve uglify-js in order to see if it does exist
282+
require.resolve('uglify-js');
283+
} catch (e) {
284+
if (e.code !== 'MODULE_NOT_FOUND') {
285+
// Something else seems to be wrong
286+
throw e;
287+
}
288+
// it does not exist!
289+
console.error('Code minimization is disabled due to missing uglify-js dependency');
290+
return output;
291+
}
292+
return require('uglify-js').minify(output.code, {
293+
fromString: true,
294+
outSourceMap: sourceMapFile,
295+
inSourceMap: JSON.parse(output.map)
296+
});
297+
}

spec/precompiler.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ describe('precompiler', function() {
1212

1313
var log,
1414
logFunction,
15+
errorLog,
16+
errorLogFunction,
1517

1618
precompile,
1719
minify,
@@ -26,16 +28,51 @@ describe('precompiler', function() {
2628
content,
2729
writeFileSync;
2830

31+
/**
32+
* Mock the Module.prototype.require-function such that an error is thrown, when "uglify-js" is loaded.
33+
*
34+
* The function cleans up its mess when "callback" is finished
35+
*
36+
* @param {Error} loadError the error that should be thrown if uglify is loaded
37+
* @param {function} callback a callback-function to run when the mock is active.
38+
*/
39+
function mockRequireUglify(loadError, callback) {
40+
var Module = require('module');
41+
var _resolveFilename = Module._resolveFilename;
42+
delete require.cache[require.resolve('uglify-js')];
43+
delete require.cache[require.resolve('../dist/cjs/precompiler')];
44+
Module._resolveFilename = function(request, mod) {
45+
if (request === 'uglify-js') {
46+
throw loadError;
47+
}
48+
return _resolveFilename.call(this, request, mod);
49+
};
50+
try {
51+
callback();
52+
} finally {
53+
Module._resolveFilename = _resolveFilename;
54+
delete require.cache[require.resolve('uglify-js')];
55+
delete require.cache[require.resolve('../dist/cjs/precompiler')];
56+
}
57+
}
58+
2959
beforeEach(function() {
3060
precompile = Handlebars.precompile;
3161
minify = uglify.minify;
3262
writeFileSync = fs.writeFileSync;
3363

64+
// Mock stdout and stderr
3465
logFunction = console.log;
3566
log = '';
3667
console.log = function() {
3768
log += Array.prototype.join.call(arguments, '');
3869
};
70+
errorLogFunction = console.error;
71+
errorLog = '';
72+
console.error = function() {
73+
errorLog += Array.prototype.join.call(arguments, '');
74+
};
75+
3976
fs.writeFileSync = function(_file, _content) {
4077
file = _file;
4178
content = _content;
@@ -46,6 +83,7 @@ describe('precompiler', function() {
4683
uglify.minify = minify;
4784
fs.writeFileSync = writeFileSync;
4885
console.log = logFunction;
86+
console.error = errorLogFunction;
4987
});
5088

5189
it('should output version', function() {
@@ -148,6 +186,29 @@ describe('precompiler', function() {
148186
equal(log, 'min');
149187
});
150188

189+
it('should omit minimization gracefully, if uglify-js is missing', function() {
190+
var error = new Error("Cannot find module 'uglify-js'");
191+
error.code = 'MODULE_NOT_FOUND';
192+
mockRequireUglify(error, function() {
193+
var Precompiler = require('../dist/cjs/precompiler');
194+
Handlebars.precompile = function() { return 'amd'; };
195+
Precompiler.cli({templates: [emptyTemplate], min: true});
196+
equal(/template\(amd\)/.test(log), true);
197+
equal(/\n/.test(log), true);
198+
equal(/Code minimization is disabled/.test(errorLog), true);
199+
});
200+
});
201+
202+
it('should fail on errors (other than missing module) while loading uglify-js', function() {
203+
mockRequireUglify(new Error('Mock Error'), function() {
204+
shouldThrow(function() {
205+
var Precompiler = require('../dist/cjs/precompiler');
206+
Handlebars.precompile = function() { return 'amd'; };
207+
Precompiler.cli({templates: [emptyTemplate], min: true});
208+
}, Error, 'Mock Error');
209+
});
210+
});
211+
151212
it('should output map', function() {
152213
Precompiler.cli({templates: [emptyTemplate], map: 'foo.js.map'});
153214

0 commit comments

Comments
 (0)