minify.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. #!/usr/bin/env node
  2. ;(function() {
  3. 'use strict';
  4. /** The Node filesystem, path, `zlib`, and child process modules */
  5. var fs = require('fs'),
  6. gzip = require('zlib').gzip,
  7. path = require('path'),
  8. spawn = require('child_process').spawn;
  9. /** The directory that is the base of the repository */
  10. var basePath = path.join(__dirname, '../');
  11. /** The directory where the Closure Compiler is located */
  12. var closurePath = path.join(basePath, 'vendor', 'closure-compiler', 'compiler.jar');
  13. /** The distribution directory */
  14. var distPath = path.join(basePath, 'dist');
  15. /** Load other modules */
  16. var preprocess = require(path.join(__dirname, 'pre-compile')),
  17. postprocess = require(path.join(__dirname, 'post-compile')),
  18. uglifyJS = require(path.join(basePath, 'vendor', 'uglifyjs', 'uglify-js'));
  19. /** Closure Compiler command-line options */
  20. var closureOptions = [
  21. '--compilation_level=ADVANCED_OPTIMIZATIONS',
  22. '--warning_level=QUIET'
  23. ];
  24. /** Reassign `existsSync` for older versions of Node */
  25. fs.existsSync || (fs.existsSync = path.existsSync);
  26. /*--------------------------------------------------------------------------*/
  27. /**
  28. * The exposed `minify` function minifies a given Lo-Dash `source` and invokes
  29. * the `onComplete` callback when finished.
  30. *
  31. * @param {Array|String} source The array of command-line arguments or the
  32. * source to minify.
  33. * @param {Object} options The options object containing `onComplete`,
  34. * `silent`, and `workingName`.
  35. */
  36. function minify(source, options) {
  37. options || (options = {});
  38. if (Array.isArray(source)) {
  39. // convert the command-line arguments to an options object
  40. options = source;
  41. var filePath = options[options.length - 1],
  42. dirPath = path.dirname(filePath),
  43. workingName = path.basename(filePath, '.js') + '.min',
  44. outputPath = path.join(dirPath, workingName + '.js'),
  45. isSilent = options.indexOf('-s') > -1 || options.indexOf('--silent') > -1;
  46. source = fs.readFileSync(filePath, 'utf8');
  47. options = {
  48. 'silent': isSilent,
  49. 'workingName': workingName,
  50. 'onComplete': function(source) {
  51. fs.writeFileSync(outputPath, source, 'utf8');
  52. }
  53. };
  54. }
  55. new Minify(source, options);
  56. }
  57. /**
  58. * The Minify constructor used to keep state of each `minify` invocation.
  59. *
  60. * @private
  61. * @constructor
  62. * @param {String} source The source to minify.
  63. * @param {Object} options The options object containing `onComplete`,
  64. * `silent`, and `workingName`.
  65. */
  66. function Minify(source, options) {
  67. source || (source = '');
  68. options || (options = {});
  69. if (typeof source != 'string') {
  70. options = source || options;
  71. source = options.source || '';
  72. }
  73. // create the destination directory if it doesn't exist
  74. if (!fs.existsSync(distPath)) {
  75. // avoid errors when called as a npm executable
  76. try {
  77. fs.mkdirSync(distPath);
  78. } catch(e) { }
  79. }
  80. this.compiled = {};
  81. this.hybrid = {};
  82. this.uglified = {};
  83. this.isSilent = !!options.silent;
  84. this.onComplete = options.onComplete || function() {};
  85. this.workingName = options.workingName || 'temp';
  86. source = preprocess(source);
  87. this.source = source;
  88. // begin the minification process
  89. closureCompile.call(this, source, onClosureCompile.bind(this));
  90. }
  91. /*--------------------------------------------------------------------------*/
  92. /**
  93. * Compresses a `source` string using the Closure Compiler. Yields the
  94. * minified result, and any exceptions encountered, to a `callback` function.
  95. *
  96. * @private
  97. * @param {String} source The JavaScript source to minify.
  98. * @param {String} [message] The message to log.
  99. * @param {Function} callback The function to call once the process completes.
  100. */
  101. function closureCompile(source, message, callback) {
  102. // the standard error stream, standard output stream, and Closure Compiler process
  103. var error = '',
  104. output = '',
  105. compiler = spawn('java', ['-jar', closurePath].concat(closureOptions));
  106. // juggle arguments
  107. if (typeof message == 'function') {
  108. callback = message;
  109. message = null;
  110. }
  111. if (!this.isSilent) {
  112. console.log(message == null
  113. ? 'Compressing ' + this.workingName + ' using the Closure Compiler...'
  114. : message
  115. );
  116. }
  117. compiler.stdout.on('data', function(data) {
  118. // append the data to the output stream
  119. output += data;
  120. });
  121. compiler.stderr.on('data', function(data) {
  122. // append the error message to the error stream
  123. error += data;
  124. });
  125. compiler.on('exit', function(status) {
  126. var exception = null;
  127. // `status` contains the process exit code
  128. if (status) {
  129. exception = new Error(error);
  130. exception.status = status;
  131. }
  132. callback(exception, output);
  133. });
  134. // proxy the standard input to the Closure Compiler
  135. compiler.stdin.end(source);
  136. }
  137. /**
  138. * Compresses a `source` string using UglifyJS. Yields the result to a
  139. * `callback` function. This function is synchronous; the `callback` is used
  140. * for symmetry.
  141. *
  142. * @private
  143. * @param {String} source The JavaScript source to minify.
  144. * @param {String} [message] The message to log.
  145. * @param {Function} callback The function to call once the process completes.
  146. */
  147. function uglify(source, message, callback) {
  148. var exception,
  149. result,
  150. ugly = uglifyJS.uglify;
  151. // juggle arguments
  152. if (typeof message == 'function') {
  153. callback = message;
  154. message = null;
  155. }
  156. if (!this.isSilent) {
  157. console.log(message == null
  158. ? 'Compressing ' + this.workingName + ' using UglifyJS...'
  159. : message
  160. );
  161. }
  162. try {
  163. result = ugly.gen_code(
  164. // enable unsafe transformations
  165. ugly.ast_squeeze_more(
  166. ugly.ast_squeeze(
  167. // munge variable and function names, excluding the special `define`
  168. // function exposed by AMD loaders
  169. ugly.ast_mangle(uglifyJS.parser.parse(source), {
  170. 'except': ['define']
  171. }
  172. ))), {
  173. 'ascii_only': true
  174. });
  175. } catch(e) {
  176. exception = e;
  177. }
  178. // lines are restricted to 500 characters for consistency with the Closure Compiler
  179. callback(exception, result && ugly.split_lines(result, 500));
  180. }
  181. /*--------------------------------------------------------------------------*/
  182. /**
  183. * The `closureCompile()` callback.
  184. *
  185. * @private
  186. * @param {Object|Undefined} exception The error object.
  187. * @param {String} result The resulting minified source.
  188. */
  189. function onClosureCompile(exception, result) {
  190. if (exception) {
  191. throw exception;
  192. }
  193. // store the post-processed Closure Compiler result and gzip it
  194. this.compiled.source = result = postprocess(result);
  195. gzip(result, onClosureGzip.bind(this));
  196. }
  197. /**
  198. * The Closure Compiler `gzip` callback.
  199. *
  200. * @private
  201. * @param {Object|Undefined} exception The error object.
  202. * @param {Buffer} result The resulting gzipped source.
  203. */
  204. function onClosureGzip(exception, result) {
  205. if (exception) {
  206. throw exception;
  207. }
  208. if (!this.isSilent) {
  209. console.log('Done. Size: %d bytes.', result.length);
  210. }
  211. // store the gzipped result and report the size
  212. this.compiled.gzip = result;
  213. // next, minify the source using only UglifyJS
  214. uglify.call(this, this.source, onUglify.bind(this));
  215. }
  216. /**
  217. * The `uglify()` callback.
  218. *
  219. * @private
  220. * @param {Object|Undefined} exception The error object.
  221. * @param {String} result The resulting minified source.
  222. */
  223. function onUglify(exception, result) {
  224. if (exception) {
  225. throw exception;
  226. }
  227. // store the post-processed Uglified result and gzip it
  228. this.uglified.source = result = postprocess(result);
  229. gzip(result, onUglifyGzip.bind(this));
  230. }
  231. /**
  232. * The UglifyJS `gzip` callback.
  233. *
  234. * @private
  235. * @param {Object|Undefined} exception The error object.
  236. * @param {Buffer} result The resulting gzipped source.
  237. */
  238. function onUglifyGzip(exception, result) {
  239. if (exception) {
  240. throw exception;
  241. }
  242. if (!this.isSilent) {
  243. console.log('Done. Size: %d bytes.', result.length);
  244. }
  245. var message = 'Compressing ' + this.workingName + ' using hybrid minification...';
  246. // store the gzipped result and report the size
  247. this.uglified.gzip = result;
  248. // next, minify the Closure Compiler minified source using UglifyJS
  249. uglify.call(this, this.compiled.source, message, onHybrid.bind(this));
  250. }
  251. /**
  252. * The hybrid `uglify()` callback.
  253. *
  254. * @private
  255. * @param {Object|Undefined} exception The error object.
  256. * @param {String} result The resulting minified source.
  257. */
  258. function onHybrid(exception, result) {
  259. if (exception) {
  260. throw exception;
  261. }
  262. // store the post-processed Uglified result and gzip it
  263. this.hybrid.source = result = postprocess(result);
  264. gzip(result, onHybridGzip.bind(this));
  265. }
  266. /**
  267. * The hybrid `gzip` callback.
  268. *
  269. * @private
  270. * @param {Object|Undefined} exception The error object.
  271. * @param {Buffer} result The resulting gzipped source.
  272. */
  273. function onHybridGzip(exception, result) {
  274. if (exception) {
  275. throw exception;
  276. }
  277. if (!this.isSilent) {
  278. console.log('Done. Size: %d bytes.', result.length);
  279. }
  280. // store the gzipped result and report the size
  281. this.hybrid.gzip = result;
  282. // finish by choosing the smallest compressed file
  283. onComplete.call(this);
  284. }
  285. /**
  286. * The callback executed after JavaScript source is minified and gzipped.
  287. *
  288. * @private
  289. */
  290. function onComplete() {
  291. var compiled = this.compiled,
  292. hybrid = this.hybrid,
  293. name = this.workingName,
  294. uglified = this.uglified;
  295. // avoid errors when called as a npm executable
  296. try {
  297. // save the Closure Compiled version to disk
  298. fs.writeFileSync(path.join(distPath, name + '.compiler.js'), compiled.source);
  299. fs.writeFileSync(path.join(distPath, name + '.compiler.js.gz'), compiled.gzip);
  300. // save the Uglified version to disk
  301. fs.writeFileSync(path.join(distPath, name + '.uglify.js'), uglified.source);
  302. fs.writeFileSync(path.join(distPath, name + '.uglify.js.gz'), uglified.gzip);
  303. // save the hybrid minified version to disk
  304. fs.writeFileSync(path.join(distPath, name + '.hybrid.js'), hybrid.source);
  305. fs.writeFileSync(path.join(distPath, name + '.hybrid.js.gz'), hybrid.gzip);
  306. } catch(e) { }
  307. // select the smallest gzipped file and use its minified counterpart as the
  308. // official minified release (ties go to Closure Compiler)
  309. var min = Math.min(compiled.gzip.length, hybrid.gzip.length, uglified.gzip.length);
  310. // pass the minified source to the minify instances "onComplete" callback
  311. this.onComplete(
  312. compiled.gzip.length == min
  313. ? compiled.source
  314. : uglified.gzip.length == min
  315. ? uglified.source
  316. : hybrid.source
  317. );
  318. }
  319. /*--------------------------------------------------------------------------*/
  320. // expose `minify`
  321. if (module != require.main) {
  322. module.exports = minify;
  323. }
  324. else {
  325. // read the Lo-Dash source file from the first argument if the script
  326. // was invoked directly (e.g. `node minify.js source.js`) and write to
  327. // `<filename>.min.js`
  328. (function() {
  329. var options = process.argv;
  330. if (options.length < 3) {
  331. return;
  332. }
  333. minify(options);
  334. }());
  335. }
  336. }());