3
var _ = require('lodash'),
4
spawn = require('child_process').spawn,
5
zlib = require('zlib');
7
var util = require('./util'),
8
preprocess = require('./preprocess'),
9
postprocess = require('./postprocess');
14
/** Used to specify the default minifer modes. */
15
var DEFAULT_MODES = ['simple', 'advanced', 'hybrid'];
17
/** The minimum version of Java required for the Closure Compiler. */
18
var JAVA_MIN_VERSION = '1.7.0';
20
/** Native method shortcut. */
21
var push = Array.prototype.push;
23
/** Used to extract version numbers. */
24
var reDigits = /^[.\d]+/;
26
/** The Closure Compiler optimization modes. */
27
var optimizationModes = {
28
'simple': 'simple_optimizations',
29
'advanced': 'advanced_optimizations'
32
/*----------------------------------------------------------------------------*/
35
* Minifies a given lodash `source` and invokes the `options.onComplete`
36
* callback when finished. The `onComplete` callback is invoked with one
37
* argument: (outputSource).
39
* @param {string|string[]} [source=''] The source to minify or array of commands.
40
* -o, --output - Write output to a given path/filename.
41
* -s, --silent - Skip status updates normally logged to the console.
42
* -t, --template - Applies template specific minifier options.
44
* @param {Object} [options={}] The options object.
45
* @param {string} [options.outputPath] Write output to a given path/filename.
46
* @param {boolean} [options.isSilent] Skip status updates normally logged to the console.
47
* @param {boolean} [options.isTemplate] Applies template specific minifier options.
48
* @param {Function} [options.onComplete] The function called once minification has finished.
50
function minify(source, options) {
51
// Convert commands to an options object.
52
if (_.isArray(source)) {
55
// Used to specify the source map URL.
58
// Used to report invalid command-line arguments.
59
var invalidArgs = _.reject(options, function(value, index, options) {
60
if (/^(?:-o|--output)$/.test(options[index - 1]) ||
61
/^modes=.*$/.test(value)) {
64
var result = _.includes([
71
if (!result && /^(?:-m|--source-map)$/.test(options[index - 1])) {
78
// Report invalid arguments.
79
if (!_.isEmpty(invalidArgs)) {
80
console.log('\nInvalid argument' + (_.size(invalidArgs) > 1 ? 's' : '') + ' passed:', invalidArgs.join(', '));
83
var filePath = path.normalize(_.last(options)),
84
outputPath = path.join(path.dirname(filePath), path.basename(filePath, '.js') + '.min.js');
86
outputPath = _.reduce(options, function(result, value, index) {
87
if (/-o|--output/.test(value)) {
88
result = path.normalize(options[index + 1]);
89
var dirname = path.dirname(result);
90
fs.mkdirpSync(dirname);
91
result = path.join(fs.realpathSync(dirname), path.basename(result));
98
'isMapped': getOption(options, '-m') || getOption(options, '--source-map'),
99
'isSilent': getOption(options, '-s') || getOption(options, '--silent'),
100
'isTemplate': getOption(options, '-t') || getOption(options, '--template'),
101
'modes': getOption(options, 'modes', DEFAULT_MODES),
102
'outputPath': outputPath,
103
'sourceMapURL': sourceMapURL
106
source = fs.readFileSync(filePath, 'utf8');
109
options = _.cloneDeep(options);
111
source = _.toString(source);
113
options.filePath = path.normalize(options.filePath);
114
options.modes = _.get(options, 'modes', DEFAULT_MODES);
115
options.outputPath = path.normalize(options.outputPath);
117
if (options.isMapped) {
118
_.pull(options.modes, 'advanced', 'hybrid');
120
if (options.isTemplate) {
121
_.pull(options.modes, 'advanced');
123
new Minify(source, options);
127
* The Minify constructor used to keep state of each `minify` invocation.
131
* @param {string} source The source to minify.
132
* @param {Object} options The options object.
133
* @param {string} [options.outputPath=''] Write output to a given path/filename.
134
* @param {boolean} [options.isMapped] Specify creating a source map for the minified source.
135
* @param {boolean} [options.isSilent] Skip status updates normally logged to the console.
136
* @param {boolean} [options.isTemplate] Applies template specific minifier options.
137
* @param {Function} [options.onComplete] The function called once minification has finished.
139
function Minify(source, options) {
141
if (_.isObject(source)) {
143
source = options.source || '';
145
var modes = options.modes;
147
this.compiled = { 'simple': {}, 'advanced': {} };
148
this.hybrid = { 'simple': {}, 'advanced': {} };
151
this.filePath = options.filePath;
152
this.isMapped = !!options.isMapped;
153
this.isSilent = !!options.isSilent;
154
this.isTemplate = !!options.isTemplate;
156
this.outputPath = options.outputPath;
157
this.source = source;
158
this.sourceMapURL = options.sourceMapURL;
160
this.onComplete = options.onComplete || function(data) {
161
var outputPath = this.outputPath,
162
sourceMap = data.sourceMap;
164
fs.writeFileSync(outputPath, data.source, 'utf8');
166
fs.writeFileSync(getMapPath(outputPath), sourceMap, 'utf8');
170
// Begin the minification process.
172
uglify.call(this, source, onUglify.bind(this));
173
} else if (_.includes(modes, 'simple')) {
174
closureCompiler.call(this, source, 'simple', onClosureSimpleCompile.bind(this));
175
} else if (_.includes(modes, 'advanced')) {
176
onClosureSimpleGzip.call(this);
178
onClosureAdvancedGzip.call(this);
182
/*----------------------------------------------------------------------------*/
185
* Asynchronously checks if Java 1.7 is installed. The callback is invoked
186
* with one argument: (success).
190
* @param {Function} callback The function called once the status is resolved.
192
var checkJava = (function() {
194
return function(callback) {
195
if (result != null) {
196
_.defer(callback, result);
199
var java = spawn('java', ['-version']);
201
java.stderr.on('data', function(data) {
202
java.stderr.removeAllListeners('data');
204
var version = _.get(/(?:java|openjdk) version "(.+)"/.exec(data.toString()), 1, '');
205
result = compareVersion(version, JAVA_MIN_VERSION) > -1;
213
java.on('error', function() {
221
* Compares two version strings to determine if the first is greater than,
222
* equal to, or less then the second.
225
* @param {string} [version=''] The version string to compare to `other`.
226
* @param {string} [other=''] The version string to compare to `version`.
227
* @returns {number} Returns `1` if greater then, `0` if equal to, or `-1` if
228
* less than the second version string.
230
function compareVersion(version, other) {
231
version = splitVersion(version);
232
other = splitVersion(other);
235
verLength = version.length,
236
othLength = other.length,
237
maxLength = Math.max(verLength, othLength),
238
diff = Math.abs(verLength - othLength);
240
push.apply(verLength == maxLength ? other : version, _.range(0, diff, 0));
241
while (++index < maxLength) {
242
var verValue = version[index],
243
othValue = other[index];
245
if (verValue > othValue) {
248
if (verValue < othValue) {
256
* Resolves the source map path from the given output path.
259
* @param {string} outputPath The output path.
260
* @returns {string} Returns the source map path.
262
function getMapPath(outputPath) {
263
return path.join(path.dirname(outputPath), path.basename(outputPath, '.js') + '.map');
267
* Gets the value of a given name from the `options` array. If no value is
268
* available the `defaultValue` is returned.
271
* @param {Array} options The options array to inspect.
272
* @param {string} name The name of the option.
273
* @param {*} defaultValue The default option value.
274
* @returns {*} Returns the option value.
276
function getOption(options, name, defaultValue) {
277
var isArr = _.isArray(defaultValue);
278
return _.reduce(options, function(result, value) {
280
value = optionToArray(name, value);
281
return _.isEmpty(value) ? result : value;
283
value = optionToValue(name, value);
284
return value == null ? result : value;
289
* Compress a source with Gzip. Yields the gzip buffer and any exceptions
290
* encountered to a callback function.
293
* @param {string} source The source to gzip.
294
* @param {Function} callback The function called once the process has completed.
295
* @param {Buffer} result The gzipped source buffer.
297
function gzip(source, callback) {
298
return _.size(zlib.gzip) > 2
299
? zlib.gzip(source, { 'level': zlib.Z_BEST_COMPRESSION } , callback)
300
: zlib.gzip(source, callback);
304
* Converts a comma separated option value into an array.
307
* @param {string} name The name of the option to inspect.
308
* @param {string} string The options string.
309
* @returns {Array} Returns the new converted array.
311
function optionToArray(name, string) {
312
return _.compact(_.invokeMap((optionToValue(name, string) || '').split(/, */), 'trim'));
316
* Extracts the option value from an option string.
319
* @param {string} name The name of the option to inspect.
320
* @param {string} string The options string.
321
* @returns {string|undefined} Returns the option value, else `undefined`.
323
function optionToValue(name, string) {
324
var result = (result = string.match(RegExp('^' + name + '(?:=([\\s\\S]+))?$'))) && (result[1] ? result[1].trim() : true);
325
if (result === 'false') {
328
return result || undefined;
332
* Splits a version string by its decimal points into its numeric components.
335
* @param {string} [version=''] The version string to split.
336
* @returns {number[]} Returns the array of numeric components.
338
function splitVersion(version) {
339
if (version == null) {
342
var result = String(version).match(reDigits);
343
return result ? _.map(result[0].split('.'), Number) : [];
346
/*----------------------------------------------------------------------------*/
349
* Compress a source using the Closure Compiler. Yields the minified result
350
* and any exceptions encountered to a callback function.
353
* @param {string} source The JavaScript source to minify.
354
* @param {string} mode The optimization mode.
355
* @param {string} [label] The label to log.
356
* @param {Function} callback The function called once the process has completed.
358
function closureCompiler(source, mode, label, callback) {
359
var compiler = require('closure-compiler'),
360
isSilent = this.isSilent,
361
isTemplate = this.isTemplate,
363
outputPath = this.outputPath;
366
'charset': isTemplate ? 'utf8': 'ascii',
367
'compilation_level': optimizationModes[mode],
368
'warning_level': 'quiet'
371
if (callback == null && typeof label == 'function') {
376
label = 'the Closure Compiler (' + mode + ')';
378
checkJava(function(success) {
379
// Skip using the Closure Compiler if Java is not installed.
382
console.warn('The Closure Compiler requires Java %s. Skipping...', JAVA_MIN_VERSION);
384
_.pull(modes, 'advanced', 'hybrid');
388
source = preprocess(source, { 'isTemplate': isTemplate });
390
// Remove the copyright header to make other modifications easier.
391
var license = _.get(/^(?:\s*\/\/.*|\s*\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)*\s*/.exec(source), 0, '');
392
source = source.replace(license, '');
394
var hasIIFE = /^;?\(function[^{]+{/.test(source),
395
isStrict = /^;?\(function[^{]+{\s*["']use strict["']/.test(source);
397
// To avoid stripping the IIFE, convert it to a function call.
400
.replace(/\(function/, '__iife__$&')
401
.replace(/\.call\(this\)\)([\s;]*(?:\n\/\/.+)?)$/, ', this)$1');
404
console.log('Compressing %s using %s...', path.basename(outputPath, '.js'), label);
406
compiler.compile(source, options, function(error, output) {
411
// Restore IIFE and move exposed vars inside the IIFE.
414
.replace(/\b__iife__\b/, '')
415
.replace(/^((?:var (?:[$\w]+=(?:!0|!1|null)[,;])+)?)([\s\S]*?function[^{]+{)/, '$2$1')
416
.replace(/,\s*this\)([\s;]*(?:\n\/\/.+)?)$/, '.call(this))$1');
418
// Inject "use strict" directive.
420
output = output.replace(/^[\s\S]*?function[^{]+{/, '$&"use strict";');
422
// Restore copyright header.
424
output = license + output;
426
callback(error, output);
432
* Compress a source using UglifyJS. Yields the minified result and any
433
* exceptions encountered to a callback function.
436
* @param {string} source The JavaScript source to minify.
437
* @param {string} [label] The label to log.
438
* @param {Function} callback The function called once the process has completed.
440
function uglify(source, label, callback) {
441
var uglifyJS = require('uglify-js'),
442
sourceMapURL = this.isMapped && (this.sourceMapURL || path.basename(getMapPath(this.outputPath)));
444
if (callback == null && typeof label == 'function') {
451
if (!this.isSilent) {
452
console.log('Compressing %s using %s...', path.basename(this.outputPath, '.js'), label);
455
source = preprocess(source, { 'isTemplate': this.isTemplate });
457
var result = uglifyJS.minify(source, {
459
'outSourceMap': sourceMapURL,
461
'collapse_vars': true,
462
'negate_iife': false,
463
'pure_getters': true,
468
'ascii_only': !this.isTemplate,
470
}, sourceMapURL ? {} : {
471
'comments': /@license/
478
result.map = !sourceMapURL ? null : JSON.stringify(_.assign(JSON.parse(result.map), {
479
'file': path.basename(this.outputPath),
480
'sources': [path.basename(this.filePath)]
483
_.defer(callback, error, result.code, result.map);
486
/*----------------------------------------------------------------------------*/
489
* The Closure Compiler callback for simple optimizations.
492
* @param {Object} [error] The error object.
493
* @param {string} result The minified source.
495
function onClosureSimpleCompile(error, result) {
499
if (result != null) {
500
result = postprocess(result);
501
this.compiled.simple.source = result;
502
gzip(result, onClosureSimpleGzip.bind(this));
505
onClosureSimpleGzip.call(this);
510
* The Closure Compiler `gzip` callback for simple optimizations.
513
* @param {Object} [error] The error object.
514
* @param {Buffer} result The gzipped source buffer.
516
function onClosureSimpleGzip(error, result) {
520
if (result != null) {
521
if (!this.isSilent) {
522
console.log('Done. Size: %d bytes.', _.size(result));
524
this.compiled.simple.gzip = result;
526
if (_.includes(this.modes, 'advanced')) {
527
closureCompiler.call(this, this.source, 'advanced', onClosureAdvancedCompile.bind(this));
529
onClosureAdvancedGzip.call(this);
534
* The Closure Compiler callback for advanced optimizations.
537
* @param {Object} [error] The error object.
538
* @param {string} result The minified source.
540
function onClosureAdvancedCompile(error, result) {
544
if (result != null) {
545
result = postprocess(result);
546
this.compiled.advanced.source = result;
547
gzip(result, onClosureAdvancedGzip.bind(this));
550
onClosureAdvancedGzip.call(this);
555
* The Closure Compiler `gzip` callback for advanced optimizations.
558
* @param {Object} [error] The error object.
559
* @param {Buffer} result The gzipped source buffer.
561
function onClosureAdvancedGzip(error, result) {
565
if (result != null) {
566
if (!this.isSilent) {
567
console.log('Done. Size: %d bytes.', _.size(result));
569
this.compiled.advanced.gzip = result;
571
uglify.call(this, this.source, onUglify.bind(this));
575
* The UglifyJS callback.
578
* @param {Object} [error] The error object.
579
* @param {string} result The minified source.
580
* @param {string} map The source map output.
582
function onUglify(error, result, map) {
586
result = postprocess(result, !!map);
587
_.assign(this.uglified, { 'source': result, 'sourceMap': map });
588
gzip(result, onUglifyGzip.bind(this));
592
* The UglifyJS `gzip` callback.
595
* @param {Object} [error] The error object.
596
* @param {Buffer} result The gzipped source buffer.
598
function onUglifyGzip(error, result) {
602
if (result != null) {
603
if (!this.isSilent) {
604
console.log('Done. Size: %d bytes.', _.size(result));
606
this.uglified.gzip = result;
608
// Minify the already Closure Compiler simple optimized source using UglifyJS.
609
var modes = this.modes;
610
if (_.includes(modes, 'hybrid')) {
611
if (_.includes(modes, 'simple')) {
612
uglify.call(this, this.compiled.simple.source, 'hybrid (simple)', onSimpleHybrid.bind(this));
613
} else if (_.includes(modes, 'advanced')) {
614
onSimpleHybridGzip.call(this);
617
onComplete.call(this);
622
* The hybrid callback for simple optimizations.
625
* @param {Object} [error] The error object.
626
* @param {string} result The minified source.
628
function onSimpleHybrid(error, result) {
632
result = postprocess(result);
633
this.hybrid.simple.source = result;
634
gzip(result, onSimpleHybridGzip.bind(this));
638
* The hybrid `gzip` callback for simple optimizations.
641
* @param {Object} [error] The error object.
642
* @param {Buffer} result The gzipped source buffer.
644
function onSimpleHybridGzip(error, result) {
648
if (result != null) {
649
if (!this.isSilent) {
650
console.log('Done. Size: %d bytes.', _.size(result));
652
this.hybrid.simple.gzip = result;
654
// Minify the already Closure Compiler advance optimized source using UglifyJS.
655
if (_.includes(this.modes, 'advanced')) {
656
uglify.call(this, this.compiled.advanced.source, 'hybrid (advanced)', onAdvancedHybrid.bind(this));
658
onComplete.call(this);
663
* The hybrid callback for advanced optimizations.
666
* @param {Object} [error] The error object.
667
* @param {string} result The minified source.
669
function onAdvancedHybrid(error, result) {
673
result = postprocess(result);
674
this.hybrid.advanced.source = result;
675
gzip(result, onAdvancedHybridGzip.bind(this));
679
* The hybrid `gzip` callback for advanced optimizations.
682
* @param {Object} [error] The error object.
683
* @param {Buffer} result The gzipped source buffer.
685
function onAdvancedHybridGzip(error, result) {
689
if (result != null) {
690
if (!this.isSilent) {
691
console.log('Done. Size: %d bytes.', _.size(result));
693
this.hybrid.advanced.gzip = result;
695
// Finish by choosing the smallest compressed file.
696
onComplete.call(this);
700
* The callback executed after the source is minified and gzipped.
704
function onComplete() {
706
this.compiled.simple,
707
this.compiled.advanced,
713
var gzips = _.compact(_.map(objects, 'gzip'));
715
// Select the smallest gzipped file and use its minified counterpart as the
716
// official minified release (ties go to the Closure Compiler).
717
var min = _.size(_.minBy(gzips, 'length'));
719
// Pass the minified source to the "onComplete" callback.
720
_.each(objects, _.bind(function(data) {
721
if (_.size(data.gzip) == min) {
722
data.outputPath = this.outputPath;
723
this.onComplete(data);
729
module.exports = minify;