diff --git a/.gitignore b/.gitignore index 646ac519e..d7dcaedfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store -node_modules/ +dist/ +node_modules/ \ No newline at end of file diff --git a/build.js b/build.js index f9b478616..c35ffb46c 100755 --- a/build.js +++ b/build.js @@ -2,286 +2,23 @@ ;(function() { 'use strict'; - /** The Node filesystem, path, `zlib`, and child process modules */ + /** The Node filesystem and path modules */ var fs = require('fs'), - gzip = require('zlib').gzip, - path = require('path'), - spawn = require('child_process').spawn; + path = require('path'); /** The build directory containing the build scripts */ var buildPath = path.join(__dirname, 'build'); - /** The directory where the Closure Compiler is located */ - var closurePath = path.join(__dirname, 'vendor', 'closure-compiler', 'compiler.jar'); + /** The minify module */ + var Minify = require(path.join(buildPath, 'minify')); - /** The distribution directory */ - var distPath = path.join(__dirname, 'dist'); - - /** Load other modules */ - var preprocess = require(path.join(buildPath, 'pre-compile')), - postprocess = require(path.join(buildPath, 'post-compile')), - uglifyJS = require(path.join(__dirname, 'vendor', 'uglifyjs', 'uglify-js')); - - /** Used to shares values between multiple callbacks */ - var accumulator = { - 'compiled': {}, - 'hybrid': {}, - 'uglified': {} - }; - - /** Closure Compiler command-line options */ - var closureOptions = [ - '--compilation_level=ADVANCED_OPTIMIZATIONS', - '--language_in=ECMASCRIPT5_STRICT', - '--warning_level=QUIET' - ]; - - /** The pre-processed Lo-Dash source */ - var source = preprocess(fs.readFileSync(path.join(__dirname, 'lodash.js'), 'utf8')); + /** The lodash.js source */ + var source = fs.readFileSync(path.join(__dirname, 'lodash.js'), 'utf8'); /*--------------------------------------------------------------------------*/ - /** - * Compresses a `source` string using the Closure Compiler. Yields the - * minified result, and any exceptions encountered, to a `callback` function. - * - * @private - * @param {String} source The JavaScript source to minify. - * @param {String} [message] The message to log. - * @param {Function} callback The function to call once the process completes. - */ - function closureCompile(source, message, callback) { - // the standard error stream, standard output stream, and Closure Compiler process - var error = '', - output = '', - compiler = spawn('java', ['-jar', closurePath].concat(closureOptions)); - - // juggle arguments - if (typeof message == 'function') { - callback = message; - message = null; - } - - console.log(message == null ? 'Compressing lodash.js using the Closure Compiler...' : message); - - compiler.stdout.on('data', function(data) { - // append the data to the output stream - output += data; - }); - - compiler.stderr.on('data', function(data) { - // append the error message to the error stream - error += data; - }); - - compiler.on('exit', function(status) { - var exception = null; - - // `status` contains the process exit code - if (status) { - exception = new Error(error); - exception.status = status; - } - callback(exception, output); - }); - - // proxy the standard input to the Closure Compiler - compiler.stdin.end(source); - } - - /** - * Compresses a `source` string using UglifyJS. Yields the result to a - * `callback` function. This function is synchronous; the `callback` is used - * for symmetry. - * - * @private - * @param {String} source The JavaScript source to minify. - * @param {String} [message] The message to log. - * @param {Function} callback The function to call once the process completes. - */ - function uglify(source, message, callback) { - var exception, - result, - ugly = uglifyJS.uglify; - - // juggle arguments - if (typeof message == 'function') { - callback = message; - message = null; - } - - console.log(message == null ? 'Compressing lodash.js using UglifyJS...' : message); - - try { - result = ugly.gen_code( - // enable unsafe transformations - ugly.ast_squeeze_more( - ugly.ast_squeeze( - // munge variable and function names, excluding the special `define` - // function exposed by AMD loaders - ugly.ast_mangle(uglifyJS.parser.parse(source), { - 'except': ['define'] - } - ))), { - 'ascii_only': true - }); - } catch(e) { - exception = e; - } - // lines are restricted to 500 characters for consistency with the Closure Compiler - callback(exception, result && ugly.split_lines(result, 500)); - } - - /*--------------------------------------------------------------------------*/ - - /** - * The `closureCompile()` callback. - * - * @private - * @param {Object|Undefined} exception The error object. - * @param {String} result The resulting minified source. - */ - function onClosureCompile(exception, result) { - if (exception) { - throw exception; - } - // store the post-processed Closure Compiler result and gzip it - accumulator.compiled.source = result = postprocess(result); - gzip(result, onClosureGzip); - } - - /** - * The Closure Compiler `gzip` callback. - * - * @private - * @param {Object|Undefined} exception The error object. - * @param {Buffer} result The resulting gzipped source. - */ - function onClosureGzip(exception, result) { - if (exception) { - throw exception; - } - // store the gzipped result and report the size - accumulator.compiled.gzip = result; - console.log('Done. Size: %d KB.', result.length); - - // next, minify the source using only UglifyJS - uglify(source, onUglify); - } - - /** - * The `uglify()` callback. - * - * @private - * @param {Object|Undefined} exception The error object. - * @param {String} result The resulting minified source. - */ - function onUglify(exception, result) { - if (exception) { - throw exception; - } - // store the post-processed Uglified result and gzip it - accumulator.uglified.source = result = postprocess(result); - gzip(result, onUglifyGzip); - } - - /** - * The UglifyJS `gzip` callback. - * - * @private - * @param {Object|Undefined} exception The error object. - * @param {Buffer} result The resulting gzipped source. - */ - function onUglifyGzip(exception, result) { - if (exception) { - throw exception; - } - var message = 'Compressing lodash.js combining Closure Compiler and UglifyJS...'; - - // store the gzipped result and report the size - accumulator.uglified.gzip = result; - console.log('Done. Size: %d KB.', result.length); - - // next, minify the Closure Compiler minified source using UglifyJS - uglify(accumulator.compiled.source, message, onHybrid); - } - - /** - * The hybrid `uglify()` callback. - * - * @private - * @param {Object|Undefined} exception The error object. - * @param {String} result The resulting minified source. - */ - function onHybrid(exception, result) { - if (exception) { - throw exception; - } - // store the post-processed Uglified result and gzip it - accumulator.hybrid.source = result = postprocess(result); - gzip(result, onHybridGzip); - } - - /** - * The hybrid `gzip` callback. - * - * @private - * @param {Object|Undefined} exception The error object. - * @param {Buffer} result The resulting gzipped source. - */ - function onHybridGzip(exception, result) { - if (exception) { - throw exception; - } - // store the gzipped result and report the size - accumulator.hybrid.gzip = result; - console.log('Done. Size: %d KB.', result.length); - - // finish by choosing the smallest compressed file - onComplete(); - } - - /** - * The callback executed after JavaScript source is minified and gzipped. - * - * @private - */ - function onComplete() { - var compiled = accumulator.compiled, - hybrid = accumulator.hybrid, - uglified = accumulator.uglified; - - // save the Closure Compiled version to disk - fs.writeFileSync(path.join(distPath, 'lodash.compiler.js'), compiled.source); - fs.writeFileSync(path.join(distPath, 'lodash.compiler.js.gz'), compiled.gzip); - - // save the Uglified version to disk - fs.writeFileSync(path.join(distPath, 'lodash.uglify.js'), uglified.source); - fs.writeFileSync(path.join(distPath, 'lodash.uglify.js.gz'), uglified.gzip); - - // save the hybrid minified version to disk - fs.writeFileSync(path.join(distPath, 'lodash.hybrid.js'), hybrid.source); - fs.writeFileSync(path.join(distPath, 'lodash.hybrid.js.gz'), hybrid.gzip); - - // select the smallest gzipped file and use its minified counterpart as the - // official minified release (ties go to Closure Compiler) - var min = Math.min(compiled.gzip.length, hybrid.gzip.length, uglified.gzip.length); - - fs.writeFileSync(path.join(__dirname, 'lodash.min.js'), - compiled.gzip.length == min - ? compiled.source - : uglified.gzip.length == min - ? uglified.source - : hybrid.source - ); - } - - /*--------------------------------------------------------------------------*/ - - // create the destination directory if it doesn't exist - if (!path.existsSync(distPath)) { - fs.mkdirSync(distPath); - } // begin the minification process - closureCompile(source, onClosureCompile); + new Minify(source, 'lodash.min', function(result) { + fs.writeFileSync(path.join(__dirname, 'lodash.min.js'), result); + }); }()); diff --git a/build/minify.js b/build/minify.js new file mode 100755 index 000000000..356808148 --- /dev/null +++ b/build/minify.js @@ -0,0 +1,327 @@ +#!/usr/bin/env node +;(function() { + 'use strict'; + + /** The Node filesystem, path, `zlib`, and child process modules */ + var fs = require('fs'), + gzip = require('zlib').gzip, + path = require('path'), + spawn = require('child_process').spawn; + + /** The directory that is the base of the repository */ + var basePath = path.join(__dirname, '../'); + + /** The directory where the Closure Compiler is located */ + var closurePath = path.join(basePath, 'vendor', 'closure-compiler', 'compiler.jar'); + + /** The distribution directory */ + var distPath = path.join(basePath, 'dist'); + + /** Load other modules */ + var preprocess = require(path.join(__dirname, 'pre-compile')), + postprocess = require(path.join(__dirname, 'post-compile')), + uglifyJS = require(path.join(basePath, 'vendor', 'uglifyjs', 'uglify-js')); + + /** Closure Compiler command-line options */ + var closureOptions = [ + '--compilation_level=ADVANCED_OPTIMIZATIONS', + '--language_in=ECMASCRIPT5_STRICT', + '--warning_level=QUIET' + ]; + + /** Used to match the file extension of a file path */ + var reExtension = /\.[^.]+$/; + + /*--------------------------------------------------------------------------*/ + + /** + * Minify a given JavaScript `source`. + * + * @param {String} source The source to minify. + * @param {String} workingName The name to give temporary files creates during the minification process. + * @param {Function} onComplete A function called when minification has completed. + */ + function Minify(source, workingName, onComplete) { + // create the destination directory if it doesn't exist + if (!path.existsSync(distPath)) { + fs.mkdirSync(distPath); + } + + this.compiled = {}; + this.hybrid = {}; + this.uglified = {}; + this.onComplete = onComplete; + this.source = source = preprocess(source); + this.workingName = workingName; + + // begin the minification process + closureCompile.call(this, source, onClosureCompile.bind(this)); + } + + /*--------------------------------------------------------------------------*/ + + /** + * Compresses a `source` string using the Closure Compiler. Yields the + * minified result, and any exceptions encountered, to a `callback` function. + * + * @private + * @param {String} source The JavaScript source to minify. + * @param {String} [message] The message to log. + * @param {Function} callback The function to call once the process completes. + */ + function closureCompile(source, message, callback) { + // the standard error stream, standard output stream, and Closure Compiler process + var error = '', + output = '', + compiler = spawn('java', ['-jar', closurePath].concat(closureOptions)); + + // juggle arguments + if (typeof message == 'function') { + callback = message; + message = null; + } + + console.log(message == null + ? 'Compressing ' + this.workingName + ' using the Closure Compiler...' + : message + ); + + compiler.stdout.on('data', function(data) { + // append the data to the output stream + output += data; + }); + + compiler.stderr.on('data', function(data) { + // append the error message to the error stream + error += data; + }); + + compiler.on('exit', function(status) { + var exception = null; + + // `status` contains the process exit code + if (status) { + exception = new Error(error); + exception.status = status; + } + callback(exception, output); + }); + + // proxy the standard input to the Closure Compiler + compiler.stdin.end(source); + } + + /** + * Compresses a `source` string using UglifyJS. Yields the result to a + * `callback` function. This function is synchronous; the `callback` is used + * for symmetry. + * + * @private + * @param {String} source The JavaScript source to minify. + * @param {String} [message] The message to log. + * @param {Function} callback The function to call once the process completes. + */ + function uglify(source, message, callback) { + var exception, + result, + ugly = uglifyJS.uglify; + + // juggle arguments + if (typeof message == 'function') { + callback = message; + message = null; + } + + console.log(message == null + ? 'Compressing ' + this.workingName + ' using UglifyJS...' + : message + ); + + try { + result = ugly.gen_code( + // enable unsafe transformations + ugly.ast_squeeze_more( + ugly.ast_squeeze( + // munge variable and function names, excluding the special `define` + // function exposed by AMD loaders + ugly.ast_mangle(uglifyJS.parser.parse(source), { + 'except': ['define'] + } + ))), { + 'ascii_only': true + }); + } catch(e) { + exception = e; + } + // lines are restricted to 500 characters for consistency with the Closure Compiler + callback(exception, result && ugly.split_lines(result, 500)); + } + + /*--------------------------------------------------------------------------*/ + + /** + * The `closureCompile()` callback. + * + * @private + * @param {Object|Undefined} exception The error object. + * @param {String} result The resulting minified source. + */ + function onClosureCompile(exception, result) { + if (exception) { + throw exception; + } + // store the post-processed Closure Compiler result and gzip it + this.compiled.source = result = postprocess(result); + gzip(result, onClosureGzip.bind(this)); + } + + /** + * The Closure Compiler `gzip` callback. + * + * @private + * @param {Object|Undefined} exception The error object. + * @param {Buffer} result The resulting gzipped source. + */ + function onClosureGzip(exception, result) { + if (exception) { + throw exception; + } + // store the gzipped result and report the size + this.compiled.gzip = result; + console.log('Done. Size: %d KB.', result.length); + + // next, minify the source using only UglifyJS + uglify.call(this, this.source, onUglify.bind(this)); + } + + /** + * The `uglify()` callback. + * + * @private + * @param {Object|Undefined} exception The error object. + * @param {String} result The resulting minified source. + */ + function onUglify(exception, result) { + if (exception) { + throw exception; + } + // store the post-processed Uglified result and gzip it + this.uglified.source = result = postprocess(result); + gzip(result, onUglifyGzip.bind(this)); + } + + /** + * The UglifyJS `gzip` callback. + * + * @private + * @param {Object|Undefined} exception The error object. + * @param {Buffer} result The resulting gzipped source. + */ + function onUglifyGzip(exception, result) { + if (exception) { + throw exception; + } + var message = 'Compressing ' + this.workingName + ' using hybrid minification...'; + + // store the gzipped result and report the size + this.uglified.gzip = result; + console.log('Done. Size: %d KB.', result.length); + + // next, minify the Closure Compiler minified source using UglifyJS + uglify.call(this, this.compiled.source, message, onHybrid.bind(this)); + } + + /** + * The hybrid `uglify()` callback. + * + * @private + * @param {Object|Undefined} exception The error object. + * @param {String} result The resulting minified source. + */ + function onHybrid(exception, result) { + if (exception) { + throw exception; + } + // store the post-processed Uglified result and gzip it + this.hybrid.source = result = postprocess(result); + gzip(result, onHybridGzip.bind(this)); + } + + /** + * The hybrid `gzip` callback. + * + * @private + * @param {Object|Undefined} exception The error object. + * @param {Buffer} result The resulting gzipped source. + */ + function onHybridGzip(exception, result) { + if (exception) { + throw exception; + } + // store the gzipped result and report the size + this.hybrid.gzip = result; + console.log('Done. Size: %d KB.', result.length); + + // finish by choosing the smallest compressed file + onComplete.call(this); + } + + /** + * The callback executed after JavaScript source is minified and gzipped. + * + * @private + */ + function onComplete() { + var compiled = this.compiled, + hybrid = this.hybrid, + name = this.workingName, + uglified = this.uglified; + + // save the Closure Compiled version to disk + fs.writeFileSync(path.join(distPath, name + '.compiler.js'), compiled.source); + fs.writeFileSync(path.join(distPath, name + '.compiler.js.gz'), compiled.gzip); + + // save the Uglified version to disk + fs.writeFileSync(path.join(distPath, name + '.uglify.js'), uglified.source); + fs.writeFileSync(path.join(distPath, name + '.uglify.js.gz'), uglified.gzip); + + // save the hybrid minified version to disk + fs.writeFileSync(path.join(distPath, name + '.hybrid.js'), hybrid.source); + fs.writeFileSync(path.join(distPath, name + '.hybrid.js.gz'), hybrid.gzip); + + // select the smallest gzipped file and use its minified counterpart as the + // official minified release (ties go to Closure Compiler) + var min = Math.min(compiled.gzip.length, hybrid.gzip.length, uglified.gzip.length); + + // pass the minified source to the minify instances "onComplete" callback + this.onComplete( + compiled.gzip.length == min + ? compiled.source + : uglified.gzip.length == min + ? uglified.source + : hybrid.source + ); + } + + /*--------------------------------------------------------------------------*/ + + // expose `Minify` + if (module != require.main) { + module.exports = Minify; + } + else { + // read the JavaScript source file from the first argument if the script + // was invoked directly (e.g. `node minify.js source.js`) and write to + // the same file + (function() { + var filePath = process.argv[2], + dirPath = path.dirname(filePath), + source = fs.readFileSync(filePath, 'utf8'), + workingName = path.basename(filePath, '.js') + '.min'; + + new Minify(source, workingName, function(result) { + fs.writeFileSync(path.join(dirPath, workingName + '.js')); + }); + }()); + } +}()); diff --git a/build/pre-compile.js b/build/pre-compile.js index 43f18ddd7..755919f08 100644 --- a/build/pre-compile.js +++ b/build/pre-compile.js @@ -105,7 +105,6 @@ /** * Pre-process a given JavaScript `source`, preparing it for minification. * - * @private * @param {String} source The source to process. * @returns {String} Returns the processed source. */ @@ -130,7 +129,7 @@ // minify `_.sortBy` internal properties (function() { var properties = ['criteria', 'value'], - snippet = source.match(RegExp('( +)function sortBy[\\s\\S]+?\\n\\1}'))[0], + snippet = source.match(/( +)function sortBy[\s\S]+?\n\1}/)[0], result = snippet; // minify property strings