From b52d9d1bdd4b2fd1c80a5358f9cf36a042a006db Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Fri, 18 Jan 2013 02:37:22 -0800 Subject: [PATCH] Add `--source-map` build option. [closes #161] Former-commit-id: e0cac11fda86671d944de5c157d3df3146d6def1 --- README.md | 15 +++--- build.js | 61 ++++++++++++++++-------- build/minify.js | 106 +++++++++++++++++++++++++++++++++--------- build/post-compile.js | 2 +- 4 files changed, 134 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index ec971bab9..3b2553960 100644 --- a/README.md +++ b/README.md @@ -154,13 +154,14 @@ Unless specified by `-o` or `--output`, all files created are saved to the curre The following options are also supported: - * `-c`, `--stdout`     Write output to standard output - * `-d`, `--debug`       Write only the debug output - * `-h`, `--help`         Display help information - * `-m`, `--minify`     Write only the minified output - * `-o`, `--output`     Write output to a given path/filename - * `-s`, `--silent`     Skip status updates normally logged to the console - * `-V`, `--version`   Output current version of Lo-Dash + * `-c`, `--stdout`          Write output to standard output + * `-d`, `--debug`            Write only the debug output + * `-h`, `--help`              Display help information + * `-m`, `--minify`          Write only the minified output + * `-o`, `--output`          Write output to a given path/filename + * `-p`, `--source-map`   Generate a source map for the minified output + * `-s`, `--silent`          Skip status updates normally logged to the console + * `-V`, `--version`        Output current version of Lo-Dash The `lodash` command-line utility is available when Lo-Dash is installed as a global package (i.e. `npm install -g lodash`). diff --git a/build.js b/build.js index be622a684..9bd5f3ab7 100755 --- a/build.js +++ b/build.js @@ -580,13 +580,14 @@ '', ' Options:', '', - ' -c, --stdout Write output to standard output', - ' -d, --debug Write only the debug output', - ' -h, --help Display help information', - ' -m, --minify Write only the minified output', - ' -o, --output Write output to a given path/filename', - ' -s, --silent Skip status updates normally logged to the console', - ' -V, --version Output current version of Lo-Dash', + ' -c, --stdout Write output to standard output', + ' -d, --debug Write only the debug output', + ' -h, --help Display help information', + ' -m, --minify Write only the minified output', + ' -o, --output Write output to a given path/filename', + ' -p, --source-map Generate a source map for the minified output', + ' -s, --silent Skip status updates normally logged to the console', + ' -V, --version Output current version of Lo-Dash', '' ].join('\n')); } @@ -1182,7 +1183,7 @@ // used to report invalid command-line arguments var invalidArgs = _.reject(options.slice(options[0] == 'node' ? 2 : 0), function(value, index, options) { if (/^(?:-o|--output)$/.test(options[index - 1]) || - /^(?:category|exclude|exports|iife|include|moduleId|minus|plus|settings|template)=.*$/i.test(value)) { + /^(?:category|exclude|exports|iife|include|moduleId|minus|plus|settings|template)=.*$/.test(value)) { return true; } return [ @@ -1198,6 +1199,7 @@ '-h', '--help', '-m', '--minify', '-o', '--output', + '-p', '--source-map', '-s', '--silent', '-V', '--version' ].indexOf(value) > -1; @@ -1241,6 +1243,9 @@ return match ? match[1] : result; }, null); + // the path to the source file + var filePath = path.join(__dirname, 'lodash.js'); + // flag used to specify a Backbone build var isBackbone = options.indexOf('backbone') > -1; @@ -1256,8 +1261,11 @@ // flag used to specify an Underscore build var isUnderscore = options.indexOf('underscore') > -1; + // flag used to specify creating a source map for the minified source + var isMapped = options.indexOf('-p') > -1 || options.indexOf('--source-map') > -1; + // flag used to specify only creating the minified build - var isMinify = !isDebug && options.indexOf('-m') > -1 || options.indexOf('--minify')> -1; + var isMinify = options.indexOf('-m') > -1 || options.indexOf('--minify') > -1; // flag used to specify a mobile build var isMobile = !isLegacy && (isCSP || isUnderscore || options.indexOf('mobile') > -1); @@ -1325,7 +1333,7 @@ var isTemplate = !!templatePattern; // the lodash.js source - var source = fs.readFileSync(path.join(__dirname, 'lodash.js'), 'utf8'); + var source = fs.readFileSync(filePath, 'utf8'); // flag used to specify replacing Lo-Dash's `_.clone` with Underscore's var useUnderscoreClone = isUnderscore; @@ -2123,7 +2131,7 @@ /*------------------------------------------------------------------------*/ // used to specify creating a custom build - var isCustom = isBackbone || isLegacy || isMobile || isStrict || isUnderscore || + var isCustom = isBackbone || isLegacy || isMapped || isMobile || isStrict || isUnderscore || /(?:category|exclude|exports|iife|include|minus|plus)=/.test(options) || !_.isEqual(exportsOptions, exportsAll); @@ -2144,7 +2152,10 @@ stdout.write(debugSource); callback(debugSource); } else if (!isStdOut) { - callback(debugSource, (isDebug && outputPath) || path.join(cwd, basename + '.js')); + callback({ + 'source': debugSource, + 'outputPath': (isDebug && outputPath) || path.join(cwd, basename + '.js') + }); } } // begin the minification process @@ -2152,22 +2163,24 @@ outputPath || (outputPath = path.join(cwd, basename + '.min.js')); minify(source, { + 'filePath': filePath, + 'isMapped': isMapped, 'isSilent': isSilent, 'isTemplate': isTemplate, 'outputPath': outputPath, - 'onComplete': function(source) { + 'onComplete': function(data) { // inject "use strict" directive if (isStrict) { - source = source.replace(/^([\s\S]*?function[^{]+{)([^"'])/, '$1"use strict";$2'); + data.source = data.source.replace(/^([\s\S]*?function[^{]+{)([^"'])/, '$1"use strict";$2'); } if (isCustom) { - source = addCommandsToHeader(source, options); + data.source = addCommandsToHeader(data.source, options); } if (isStdOut) { - stdout.write(source); - callback(source); + stdout.write(data.source); + callback(data); } else { - callback(source, outputPath); + callback(data); } } }); @@ -2182,8 +2195,16 @@ } else { // or invoked directly - build(process.argv, function(source, filePath) { - filePath && fs.writeFileSync(filePath, source, 'utf8'); + build(process.argv, function(data) { + var outputPath = data.outputPath, + sourceMap = data.sourceMap; + + if (outputPath) { + fs.writeFileSync(outputPath, data.source, 'utf8'); + if (sourceMap) { + fs.writeFileSync(path.join(path.dirname(outputPath), path.basename(outputPath, '.js') + '.map'), sourceMap, 'utf8'); + } + } }); } }()); diff --git a/build/minify.js b/build/minify.js index 543dfaabb..dcdf6b2f1 100755 --- a/build/minify.js +++ b/build/minify.js @@ -89,6 +89,7 @@ options = source; var filePath = options[options.length - 1], + isMapped = options.indexOf('-p') > -1 || options.indexOf('--source-map') > -1, isSilent = options.indexOf('-s') > -1 || options.indexOf('--silent') > -1, isTemplate = options.indexOf('-t') > -1 || options.indexOf('--template') > -1, outputPath = path.join(path.dirname(filePath), path.basename(filePath, '.js') + '.min.js'); @@ -102,6 +103,8 @@ }, outputPath); options = { + 'filePath': filePath, + 'isMapped': isMapped, 'isSilent': isSilent, 'isTemplate': isTemplate, 'outputPath': outputPath @@ -157,6 +160,8 @@ this.hybrid = { 'simple': {}, 'advanced': {} }; this.uglified = {}; + this.filePath = options.filePath; + this.isMapped = !!options.isMapped; this.isSilent = !!options.isSilent; this.isTemplate = !!options.isTemplate; this.outputPath = options.outputPath; @@ -164,8 +169,14 @@ source = preprocess(source, options); this.source = source; - this.onComplete = options.onComplete || function(source) { - fs.writeFileSync(this.outputPath, source, 'utf8'); + this.onComplete = options.onComplete || function(data) { + var outputPath = this.outputPath, + sourceMap = data.sourceMap; + + fs.writeFileSync(outputPath, data.source, 'utf8'); + if (sourceMap) { + fs.writeFileSync(getMapPath(outputPath), sourceMap, 'utf8'); + } }; // begin the minification process @@ -182,8 +193,7 @@ * @private * @param {Object} options The options object. * id - The Git object ID of the `.tar.gz` file. - * onComplete - The function, invoked with one argument (exception), - * called once the extraction has finished. + * onComplete - The function called once the extraction has finished. * path - The path of the extraction directory. * title - The dependency's title used in status updates logged to the console. */ @@ -242,6 +252,17 @@ }); } + /** + * Resolves the source map path from the given output path. + * + * @private + * @param {String} outputPath The output path. + * @returns {String} Returns the source map path. + */ + function getMapPath(outputPath) { + return path.join(path.dirname(outputPath), path.basename(outputPath, '.js') + '.map'); + } + /*--------------------------------------------------------------------------*/ /** @@ -254,17 +275,26 @@ * @param {Function} callback The function called once the process has completed. */ function closureCompile(source, mode, callback) { + var filePath = this.filePath, + outputPath = this.outputPath, + isMapped = this.isMapped, + mapPath = getMapPath(outputPath), + options = closureOptions.slice(); + // use simple optimizations when minifying template files - var options = closureOptions.slice(); options.push('--compilation_level=' + optimizationModes[this.isTemplate ? 'simple' : mode]); + if (isMapped) { + options.push('--create_source_map=' + mapPath, '--source_map_format=V3'); + } + // the standard error stream, standard output stream, and the Closure Compiler process var error = '', output = '', compiler = spawn('java', ['-jar', closurePath].concat(options)); if (!this.isSilent) { - console.log('Compressing ' + path.basename(this.outputPath, '.js') + ' using the Closure Compiler (' + mode + ')...'); + console.log('Compressing ' + path.basename(outputPath, '.js') + ' using the Closure Compiler (' + mode + ')...'); } compiler.stdout.on('data', function(data) { // append the data to the output stream @@ -282,7 +312,18 @@ var exception = new Error(error); exception.status = status; } - callback(exception, output); + if (isMapped) { + var mapOutput = fs.readFileSync(mapPath, 'utf8'); + fs.unlinkSync(mapPath); + + output = output + .replace(/[\s;]*$/, '\n//@ sourceMappingURL=' + path.basename(mapPath)); + + mapOutput = mapOutput + .replace(/("file":)""/, '$1"' + path.basename(outputPath) + '"') + .replace(/("sources":)\["stdin"\]/, '$1["' + path.basename(filePath) + '"]'); + } + callback(exception, output, mapOutput); }); // proxy the standard input to the Closure Compiler @@ -350,13 +391,17 @@ * @private * @param {Object|Undefined} exception The error object. * @param {String} result The resulting minified source. + * @param {String} map The source map output. */ - function onClosureSimpleCompile(exception, result) { + function onClosureSimpleCompile(exception, result, map) { if (exception) { throw exception; } result = postprocess(result); - this.compiled.simple.source = result; + + var simple = this.compiled.simple; + simple.source = result; + simple.sourceMap = map; zlib.gzip(result, onClosureSimpleGzip.bind(this)); } @@ -386,13 +431,17 @@ * @private * @param {Object|Undefined} exception The error object. * @param {String} result The resulting minified source. + * @param {String} map The source map output. */ - function onClosureAdvancedCompile(exception, result) { + function onClosureAdvancedCompile(exception, result, map) { if (exception) { throw exception; } result = postprocess(result); - this.compiled.advanced.source = result; + + var advanced = this.compiled.advanced; + advanced.source = result; + advanced.sourceMap = map; zlib.gzip(result, onClosureAdvancedGzip.bind(this)); } @@ -412,8 +461,14 @@ } this.compiled.advanced.gzip = result; - // next, minify the source using only UglifyJS - uglify.call(this, this.source, 'UglifyJS', onUglify.bind(this)); + // if mapped, finish by choosing the smallest compressed file + if (this.isMapped) { + onComplete.call(this); + } + // else, minify the source using UglifyJS + else { + uglify.call(this, this.source, 'UglifyJS', onUglify.bind(this)); + } } /** @@ -538,18 +593,25 @@ // select the smallest gzipped file and use its minified counterpart as the // official minified release (ties go to the Closure Compiler) - var min = Math.min( - compiledSimple.gzip.length, - compiledAdvanced.gzip.length, - uglified.gzip.length, - hybridSimple.gzip.length, - hybridAdvanced.gzip.length - ); + var min = this.isMapped + ? Math.min( + compiledSimple.gzip.length, + compiledAdvanced.gzip.length + ) + : Math.min( + compiledSimple.gzip.length, + compiledAdvanced.gzip.length, + uglified.gzip.length, + hybridSimple.gzip.length, + hybridAdvanced.gzip.length + ); // pass the minified source to the "onComplete" callback [compiledSimple, compiledAdvanced, uglified, hybridSimple, hybridAdvanced].some(function(data) { - if (data.gzip.length == min) { - this.onComplete(data.source); + var gzip = data.gzip; + if (gzip && gzip.length == min) { + data.outputPath = this.outputPath; + this.onComplete(data); } }, this); } diff --git a/build/post-compile.js b/build/post-compile.js index 6cf17fadd..be0624953 100644 --- a/build/post-compile.js +++ b/build/post-compile.js @@ -49,7 +49,7 @@ // add trailing semicolon if (source) { - source = source.replace(/[\s;]*$/, ';'); + source = source.replace(/[\s;]*(\n\/\/.+)?$/, ';$1'); } // exit early if version snippet isn't found var snippet = /VERSION\s*[=:]\s*([\'"])(.*?)\1/.exec(source);