diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..332eb2ba2 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +BUILD_DIR = ${CURDIR}/build +DIST_DIR = ${CURDIR}/dist +VENDOR_DIR = ${CURDIR}/vendor + +JS_ENGINE ?= `which node nodejs 2>/dev/null` + +LODASH_TMP = ${BUILD_DIR}/lodash.tmp.js +LODASH_COMPILER = ${DIST_DIR}/lodash.compiler.js +LODASH_UGLIFY = ${DIST_DIR}/lodash.uglify.js + +UGLIFY = ${VENDOR_DIR}/uglifyjs/bin/uglifyjs +CLOSURE_COMPILER = java -jar ${VENDOR_DIR}/closure-compiler/compiler.jar + +PRE_COMPILER = ${JS_ENGINE} ${BUILD_DIR}/pre-compile.js +POST_COMPILER = ${JS_ENGINE} ${BUILD_DIR}/post-compile.js + +core: + mkdir -p ${DIST_DIR} + cp ${CURDIR}/lodash.js ${LODASH_TMP} + ${PRE_COMPILER} ${LODASH_TMP} + + ${CLOSURE_COMPILER} \ + --compilation_level=ADVANCED_OPTIMIZATIONS \ + --language_in=ECMASCRIPT5_STRICT \ + --warning_level=QUIET \ + --js ${LODASH_TMP} \ + --js_output_file ${LODASH_COMPILER} + ${POST_COMPILER} ${LODASH_COMPILER} + + ${UGLIFY} \ + --unsafe \ + --max-line-len 500 \ + -o ${LODASH_UGLIFY} \ + ${LODASH_TMP} + ${POST_COMPILER} ${LODASH_UGLIFY} + + rm -f ${LODASH_TMP} diff --git a/build/post-compile.js b/build/post-compile.js new file mode 100644 index 000000000..e1b6a9a0f --- /dev/null +++ b/build/post-compile.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node +;(function() { + 'use strict'; + + /** The filesystem module */ + var fs = require('fs'); + + /** The JavaScript source */ + var src = fs.readFileSync(process.argv[2], 'utf8'); + + /** The minimal license/copyright header */ + var license = + '/*!\n' + + ' Lo-Dash @VERSION github.com/bestiejs/lodash/blob/master/LICENSE.txt\n' + + ' Underscore.js 1.3.3 github.com/documentcloud/underscore/blob/master/LICENSE\n' + + '*/'; + + /*--------------------------------------------------------------------------*/ + + // set the version + license = license.replace('@VERSION', /VERSION=([\'"])(.*?)\1/.exec(src).pop()); + + // move vars exposed by Closure Compiler into the IIFE + src = src.replace(/^([^(\n]+)\s*(\(function[^)]+\){)/, '$2$1'); + + // use double quotes consistently + src = src.replace(/'use strict'/, '"use strict"'); + + // add license + src = license + '\n;' + src; + + // write to the same file + fs.writeFileSync(process.argv[2], src, 'utf8'); +}()); diff --git a/build/pre-compile.js b/build/pre-compile.js new file mode 100644 index 000000000..e87f4061c --- /dev/null +++ b/build/pre-compile.js @@ -0,0 +1,205 @@ +#!/usr/bin/env node +;(function() { + 'use strict'; + + /** The filesystem module */ + var fs = require('fs'); + + /** The JavaScript source */ + var src = fs.readFileSync(process.argv[2], 'utf8'); + + /** Used to minify variables embedded in compiled strings */ + var compiledVars = [ + 'accumulator', + 'array', + 'arrayClass', + 'bind', + 'callback', + 'className', + 'collection', + 'computed', + 'concat', + 'current', + 'false', + 'funcClass', + 'hasOwnProperty', + 'identity', + 'index', + 'indexOf', + 'Infinity', + 'initial', + 'isArray', + 'isEmpty', + 'length', + 'object', + 'Math', + 'property', + 'result', + 'slice', + 'source', + 'stringClass', + 'target', + 'thisArg', + 'toString', + 'true', + 'undefined', + 'value', + 'values' + ]; + + /** Used to minify string values embedded in compiled strings */ + var compiledValues = [ + 'arrays', + 'objects' + ]; + + /** Used to minify `iterationFactory` option properties */ + var iterationFactoryOptions = [ + 'afterLoop', + 'args', + 'array', + 'beforeLoop', + 'bottom', + 'exits', + 'inLoop', + 'init', + 'iterate', + 'loopExp', + 'object', + 'returns', + 'top', + 'useHas' + ]; + + /** Used to minify variables and string values to a single character */ + var minNames = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); + + /** Used protect the specified properties from getting minified */ + var propWhitelist = [ + '_', + 'VERSION', + 'amd', + 'chain', + 'clearInterval', + 'criteria', + 'escape', + 'evaluate', + 'interpolate', + 'isEqual', + 'isFinite', + 'lodash', + 'setTimeout', + 'templateSettings', + 'toArray', + 'value', + 'variable' + ]; + + /*--------------------------------------------------------------------------*/ + + /** + * Remove copyright to add later in post-compile.js + */ + src = src.replace(/\/\*![\s\S]+?\*\//, ''); + + /** + * Correct JSDoc tags for Closure Compiler. + */ + src = src.replace(/@(?:alias|category)[^\n]*/g, ''); + + /** + * Add brackets to whitelisted properties so Closure Compiler won't mung them. + * http://code.google.com/closure/compiler/docs/api-tutorial3.html#export + */ + src = src.replace(RegExp('\\.(' + iterationFactoryOptions.concat(propWhitelist).join('|') + ')\\b', 'g'), "['$1']"); + + /** + * Minify `sortBy` and `template` methods. + */ + ['sortBy', 'template'].forEach(function(methodName) { + var properties = ['criteria', 'value'], + snippet = src.match(RegExp('(\\n\\s*)function ' + methodName + '[\\s\\S]+?\\1}'))[0], + result = snippet; + + // minify property strings + properties.forEach(function(property, index) { + result = result.replace(RegExp("'" + property + "'", 'g'), "'" + minNames[index] + "'"); + }); + + // remove escaped newlines in strings + result = result.replace(/\\n/g, ''); + + // replace with modified snippet + src = src.replace(snippet, result); + }); + + /*--------------------------------------------------------------------------*/ + + /** + * Minify all `iterationFactory` related snippets. + */ + src.match( + RegExp([ + // match variables storing `iterationFactory` options + 'var [a-zA-Z]+FactoryOptions\\s*=\\s*\\{[\\s\\S]+?};\\n', + // match the the `iterationFactory` function + '(\\n\\s*)function iterationFactory[\\s\\S]+?\\1}', + // match methods created by `iterationFactor` calls + 'iterationFactory\\((?:[\'{]|[a-zA-Z]+,)[\\s\\S]+?\\);\\n' + ].join('|'), 'g') + ) + .forEach(function(snippet, index) { + var result = snippet; + + // add `true` and `false` arguments to be minified + if (/function iterationFactory/.test(snippet)) { + result = result + .replace(/(Function\('[\w\s,]+)undefined/, '$1true,false,undefined') + .replace(/\)\([^)]+/, '$&,true,false'); + + // replace with modified snippet early and clip snippet + src = src.replace(snippet, result); + snippet = result = result.replace(/\)\([\s\S]+$/, ''); + } + + // minify snippet variables/arguments + compiledVars.forEach(function(variable, index) { + result = result.replace(RegExp('([^.]\\b|\\\\n)' + variable + '\\b(?!\'\\s*[\\]:])', 'g'), '$1' + minNames[index]); + // correct `typeof x == 'object'` + if (variable == 'object') { + result = result.replace(RegExp("(typeof [^']+')" + minNames[index] + "'", 'g'), "$1object'"); + } + // correct boolean literals + if (variable == 'true' || variable == 'false') { + result = result + .replace(RegExp(':\\s*' + minNames[index] + '\\s*,', 'g'), ':' + variable + ',') + .replace(RegExp('\\s*' + minNames[index] + '\\s*;', 'g'), variable + ';'); + } + }); + + // minify snippet values + compiledValues.forEach(function(value, index) { + result = result.replace(RegExp("'" + value + "'", 'g'), "'" + minNames[index] + "'"); + }); + + // minify iterationFactory option property strings + iterationFactoryOptions.forEach(function(property, index) { + if (property == 'array' || property == 'object') { + result = result.replace(RegExp("'" + property + "'(\\s*[\\]:])", 'g'), "'" + minNames[index] + "'$1"); + } else { + result = result.replace(RegExp("'" + property + "'", 'g'), "'" + minNames[index] + "'"); + } + }); + + // remove escaped newlines in strings + result = result.replace(/\\n/g, ''); + + // replace with modified snippet + src = src.replace(snippet, result); + }); + + /*--------------------------------------------------------------------------*/ + + // write to the same file + fs.writeFileSync(process.argv[2], src, 'utf8'); +}());