diff --git a/.gitignore b/.gitignore index d7dcaedfe..399d268a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store dist/ -node_modules/ \ No newline at end of file +node_modules/ +lodash.custom* \ No newline at end of file diff --git a/build.js b/build.js index c25df0346..40e054b86 100755 --- a/build.js +++ b/build.js @@ -6,16 +6,404 @@ var fs = require('fs'), path = require('path'); - /** The minify module */ - var Minify = require(path.join(__dirname, 'build', 'minify')); + /** Load other modules */ + var lodash = require(path.join(__dirname, 'lodash')), + minify = require(path.join(__dirname, 'build', 'minify')); + + /** Flag used to specify a custom build */ + var isCustom = false; /** The lodash.js source */ var source = fs.readFileSync(path.join(__dirname, 'lodash.js'), 'utf8'); + /** Used to associate aliases with their real names */ + var aliasToRealMap = { + 'all': 'every', + 'any': 'some', + 'collect': 'map', + 'detect': 'find', + 'each': 'forEach', + 'foldl': 'reduce', + 'foldr': 'reduceRight', + 'head': 'first', + 'include': 'contains', + 'inject': 'reduce', + 'intersect': 'intersection', + 'methods': 'functions', + 'select': 'filter', + 'tail': 'rest', + 'take': 'first', + 'unique': 'uniq' + }; + + /** Used to associate real names with their aliases */ + var realToAliasMap = { + 'contains': ['include'], + 'every': ['all'], + 'filter': ['select'], + 'find': ['detect'], + 'first': ['head', 'take'], + 'forEach': ['each'], + 'functions': ['methods'], + 'intersection': ['intersect'], + 'map': ['collect'], + 'reduce': ['foldl', 'inject'], + 'reduceRight': ['foldr'], + 'rest': ['tail'], + 'some': ['any'], + 'uniq': ['unique'] + }; + + /** Used to track function dependencies */ + var dependencyMap = { + 'after': [], + 'bind': [], + 'bindAll': ['bind'], + 'chain': [], + 'clone': ['extend', 'isArray'], + 'compact': [], + 'compose': [], + 'contains': ['createIterator'], + 'createIterator': ['template'], + 'debounce': [], + 'defaults': ['createIterator'], + 'defer': [], + 'delay': [], + 'difference': ['indexOf'], + 'escape': [], + 'every': ['bind', 'createIterator', 'identity'], + 'extend': ['createIterator'], + 'filter': ['bind', 'createIterator', 'identity'], + 'find': ['createIterator'], + 'first': [], + 'flatten': ['isArray'], + 'forEach': ['bind', 'createIterator'], + 'functions': ['createIterator'], + 'groupBy': ['bind', 'createIterator'], + 'has': [], + 'identity': [], + 'indexOf': ['sortedIndex'], + 'initial': [], + 'intersection': ['every', 'indexOf'], + 'invoke': [], + 'isArguments': [], + 'isArray': [], + 'isBoolean': [], + 'isDate': [], + 'isElement': [], + 'isEmpty': ['createIterator'], + 'isEqual': [], + 'isFinite': [], + 'isFunction': [], + 'isNaN': [], + 'isNull': [], + 'isNumber': [], + 'isObject': [], + 'isRegExp': [], + 'isString': [], + 'isUndefined': [], + 'keys': ['createIterator'], + 'last': [], + 'lastIndexOf': [], + 'map': ['bind', 'createIterator', 'identity'], + 'max': ['bind'], + 'memoize': [], + 'min': ['bind'], + 'mixin': ['forEach'], + 'noConflict': [], + 'once': [], + 'partial': [], + 'pick': [], + 'pluck': ['createIterator'], + 'range': [], + 'reduce': ['bind', 'createIterator'], + 'reduceRight': ['bind', 'keys'], + 'reject': ['bind', 'createIterator', 'identity'], + 'rest': [], + 'result': [], + 'shuffle': [], + 'size': ['keys'], + 'some': ['bind', 'createIterator', 'identity'], + 'sortBy': ['bind', 'map', 'pluck'], + 'sortedIndex': [], + 'tap': [], + 'template': ['escape'], + 'throttle': [], + 'times': ['bind'], + 'toArray': ['values'], + 'union': ['indexOf'], + 'uniq': ['indexOf'], + 'uniqueId': [], + 'values': ['createIterator'], + 'without': ['indexOf'], + 'wrap': [], + 'zip': ['max', 'pluck'] + }; + + /** Used to indicate core functions */ + var coreFuncs = ['extend', 'forEach', 'mixin']; + + /** Used to determine the remaining functions in the source */ + var funcNames = Object.keys(dependencyMap); + /*--------------------------------------------------------------------------*/ - // begin the minification process - new Minify(source, 'lodash.min', function(result) { - fs.writeFileSync(path.join(__dirname, 'lodash.min.js'), result); + /** + * Gets the aliases associated with a given `funcName`. + * + * @private + * @param {String} funcName The name of the function to get aliases for. + * @returns {Array} Returns an array of aliases. + */ + function getAliases(funcName) { + return realToAliasMap[funcName] || []; + } + + /** + * Gets an array of depenants for a function by the given `funcName`. + * + * @private + * @param {String} funcName The name of the function to query. + * @returns {Array} Returns an array of function dependants. + */ + function getDependants(funcName) { + // iterate over `dependencyMap`, adding the names of functions that + // have `funcName` as a dependency + return lodash.reduce(dependencyMap, function(result, dependencies, otherName) { + if (dependencies.indexOf(funcName) > -1) { + result.push(otherName); + } + return result; + }, []); + } + + /** + * Gets an array of dependencies for a function of the given `funcName`. + * + * @private + * @param {String} funcName The name of the function to query. + * @returns {Array} Returns an array of function dependencies. + */ + function getDependencies(funcName) { + var dependencies = dependencyMap[funcName], + result = []; + + if (!dependencies) { + return result; + } + // recursively accumulate the dependencies of the `funcName` function, and + // the dependencies of its dependencies, and so on. + return dependencies.reduce(function(result, otherName) { + result.push.apply(result, getDependencies(otherName).concat(otherName)); + return result; + }, result); + } + + /** + * Gets the real name, not alias, of a given `funcName`. + * + * @private + * @param {String} funcName The name of the function to resolve. + * @returns {String} Returns the real name. + */ + function getRealName(funcName) { + return aliasToRealMap[funcName] || funcName; + } + + /** + * Determines if all functions of the given names have been removed. + * + * @private + * @param {String} [funcName1, funcName2, ...] The names of functions to check. + * @returns {Boolean} Returns `true` if all functions have been removed, else `false`. + */ + function isRemoved() { + return !lodash.intersection(funcNames, arguments).length; + } + + /** + * Removes a function and associated code from the `source`. + * + * @private + * @param {String} source The source to process. + * @param {String} funcName The name of the function to remove. + * @returns {String} Returns the source with the function removed. + */ + function removeFunction(source, funcName) { + // remove function + source = source.replace(RegExp( + // match multi-line comment block (could be on a single line) + '\\n +/\\*[^*]*\\*+(?:[^/][^*]*\\*+)*/\\n' + + // begin non-capturing group + '(?:' + + // match a function declaration + '( +)function ' + funcName + '\\b[\\s\\S]+?\\n\\1}|' + + // match a variable declaration with `createIerator` + ' +var ' + funcName + ' *= *(?:[a-zA-Z]+ *\\|\\| *)?createIterator\\((?:{|[a-zA-Z])[\\s\\S]+?\\);|' + + // match a variable declaration with function expression + '( +)var ' + funcName + ' *= *(?:[a-zA-Z]+ *\\|\\| *)?function[\\s\\S]+?\\n\\2};' + + // end non-capturing group + ')\\n' + ), ''); + + // exit early if function is already removed + var found = funcNames.indexOf(funcName); + if (found < 0) { + return source; + } + + // grab `lodash` method assignments snippet + var assignmentSnippet = source.match(/( +)extend\(lodash,(?:[\s\S]+?\},)?([\s\S]+?\n\1}\))/)[2]; + + // remove `funcName` from method assignments + var modifiedSnippet = getAliases(funcName).concat(funcName).reduce(function(result, otherName) { + result = result.replace(RegExp(" *'" + otherName + "'[^\\n]+\\n"), ''); + return result; + }, assignmentSnippet) + + // remove any trailing commas and comments from the method assignments + modifiedSnippet = modifiedSnippet.replace(/,(?:\s*\/\/[^\n]*)?(\s*}\))/, '$1'); + + // replace method assignments snippet with the modified snippet + source = source.replace(assignmentSnippet, modifiedSnippet); + + // remove from remaining function names + funcNames.splice(found, 1); + + // remove associated code snippets + switch(funcName) { + case 'isArguments': + // remove `isArguments` if-statement + source = source.replace(/ +(?:\/\/[^\n]*\s+)?if *\(!isArguments[^)]+\)[\s\S]+?};?\s*}\n/, ''); + break; + + case 'template': + // remove associated functions + ['detokenize', 'escapeChar', 'tokenizeEscape', 'tokenizeInterpolate', 'tokenizeEvaluate'].forEach(function(otherName) { + source = removeFunction(source, otherName); + }); + // remove associated variables + ['escapes', 'iteratorTemplate', 'reEscapeDelimiter', 'reEvaluateDelimiter', 'reInterpolateDelimiter', 'reToken', 'reUnescaped', 'token', 'tokenized'].forEach(function(varName) { + source = removeVar(source, varName); + }); + // remove `templateSettings` assignment + source = source.replace(/\n +\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/\n( +)'templateSettings'[\s\S]+?},\n/, ''); + break; + + case 'uniqueId': + source = removeVar(source, 'idCounter'); + } + return source; + } + + /** + * Removes a given variable from the `source`. + * + * @private + * @param {String} source The source to process. + * @param {String} varName The name of the variable to remove. + * @returns {String} Returns the source with the variable removed. + */ + function removeVar(source, varName) { + return source.replace(RegExp( + // match multi-line comment block + '\\n +/\\*[^*]*\\*+(?:[^/][^*]*\\*+)*/\\n' + + // match a variable declaration + '( +)var ' + varName + ' *= *(?:.*?;|[\\s\\S]+?\\n\\1[^\\n]+;)\\n' + ), ''); + } + + /*--------------------------------------------------------------------------*/ + + // custom build + process.argv.some(function(arg) { + // exit early if not the "exclude" or "include" command option + var pair = arg.match(/^(exclude|include)=(.+)$/); + if (!pair) { + return false; + } + + var filterType = pair[1], + filterNames = pair[2].split(','); + + // set custom build flag + isCustom = true; + + // remove the specified functions and their dependants + if (filterType == 'exclude') { + filterNames.forEach(function(funcName) { + funcName = getRealName(funcName); + var otherNames = getDependants(funcName).concat(funcName); + + // skip removal if `funcName` is a required core function + if (otherNames.some(function(otherName) { + return coreFuncs.indexOf(otherName) > -1; + })) { + return; + } + otherNames.forEach(function(otherName) { + source = removeFunction(source, otherName); + }); + }); + } + // else remove all but the specified functions and their dependencies + else { + filterNames = lodash.uniq(filterNames.concat(coreFuncs).reduce(function(result, funcName) { + funcName = getRealName(funcName); + result.push.apply(result, getDependencies(funcName).concat(funcName)); + return result; + }, [])); + + lodash.each(dependencyMap, function(dependencies, otherName) { + if (filterNames.indexOf(otherName) < 0) { + source = removeFunction(source, otherName); + } + }); + } + + // remove shared variables + if (isRemoved('createIterator', 'isEqual')) { + source = removeVar(source, 'hasDontEnumBug'); + } + if (isRemoved('every', 'filter', 'find', 'forEach', 'groupBy', 'map', 'reject', 'some')) { + source = removeVar(source, 'baseIteratorOptions'); + } + if (isRemoved('every', 'some')) { + source = removeVar(source, 'everyIteratorOptions'); + } + if (isRemoved('defaults', 'extend')) { + source = removeVar(source, 'extendIteratorOptions'); + } + if (isRemoved('filter', 'reject')) { + source = removeVar(source, 'filterIteratorOptions'); + } + if (isRemoved('map', 'pluck', 'values')) { + source = removeVar(source, 'mapIteratorOptions'); + } + if (isRemoved('max', 'min')) { + // remove varaible and associated try-catch + source = removeVar(source, 'argsLimit'); + source = source.replace(/\n *try\s*\{\s*\(function[\s\S]+?catch[^}]+}\n/, ''); + } + + // consolidate consecutive horizontal rule comment separators + source = source.replace(/(?:\s*\/\*-+\*\/\s*){2,}/g, function(separators) { + return separators.match(/^\s*/)[0] + separators.slice(separators.lastIndexOf('/*')); + }); + + return true; }); + + // begin the minification process + if (isCustom) { + minify(source, 'lodash.custom.min', function(result) { + fs.writeFileSync(path.join(__dirname, 'lodash.custom.js'), source); + fs.writeFileSync(path.join(__dirname, 'lodash.custom.min.js'), result); + }); + } + else { + minify(source, 'lodash.min', function(result) { + fs.writeFileSync(path.join(__dirname, 'lodash.min.js'), result); + }); + } }());