From d9f3c259c08770ee370ed2b50909c333f9557bdf Mon Sep 17 00:00:00 2001 From: Jasper De Moor Date: Wed, 21 Feb 2018 19:47:32 +0100 Subject: [PATCH] Uglify sourcemaps support (#617) --- src/Bundler.js | 4 +- src/SourceMap.js | 92 ++++++++++++++++++- src/cli.js | 1 + src/transforms/uglify.js | 40 +++++++- test/css.js | 3 + .../sourcemap-nested-minified/index.js | 5 + .../sourcemap-nested-minified/local.js | 4 + .../sourcemap-nested-minified/utils/util.js | 3 + test/sourcemaps.js | 32 +++++++ test/typescript.js | 2 +- 10 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 test/integration/sourcemap-nested-minified/index.js create mode 100644 test/integration/sourcemap-nested-minified/local.js create mode 100644 test/integration/sourcemap-nested-minified/utils/util.js diff --git a/src/Bundler.js b/src/Bundler.js index 3c43288513b..952278675a3 100644 --- a/src/Bundler.js +++ b/src/Bundler.js @@ -92,9 +92,7 @@ class Bundler extends EventEmitter { hmrPort: options.hmrPort || 0, rootDir: Path.dirname(this.mainFile), sourceMaps: - typeof options.sourceMaps === 'boolean' - ? options.sourceMaps - : !isProduction, + typeof options.sourceMaps === 'boolean' ? options.sourceMaps : true, hmrHostname: options.hmrHostname || '', detailedReport: options.detailedReport || false }; diff --git a/src/SourceMap.js b/src/SourceMap.js index 4908ab6abb9..f3a06654a38 100644 --- a/src/SourceMap.js +++ b/src/SourceMap.js @@ -137,12 +137,12 @@ class SourceMap { async extendSourceMap(original, extension) { if (!(extension instanceof SourceMap)) { - throw new Error( - '[SOURCEMAP] Type of extension should be a SourceMap instance!' - ); + extension = await new SourceMap().addMap(extension); + } + if (!(original instanceof SourceMap)) { + original = await this.getConsumer(original); } - original = await this.getConsumer(original); extension.eachMapping(mapping => { let originalMapping = original.originalPositionFor({ line: mapping.original.line, @@ -182,6 +182,90 @@ class SourceMap { return this; } + findClosest(line, column, key = 'original') { + if (line < 1) { + throw new Error('Line numbers must be >= 1'); + } + + if (column < 0) { + throw new Error('Column numbers must be >= 0'); + } + + if (this.mappings.length < 1) { + return undefined; + } + + let startIndex = 0; + let stopIndex = this.mappings.length - 1; + let middleIndex = Math.floor((stopIndex + startIndex) / 2); + + while ( + startIndex < stopIndex && + this.mappings[middleIndex][key].line !== line + ) { + if (line < this.mappings[middleIndex][key].line) { + stopIndex = middleIndex - 1; + } else if (line > this.mappings[middleIndex][key].line) { + startIndex = middleIndex + 1; + } + middleIndex = Math.floor((stopIndex + startIndex) / 2); + } + + let mapping = this.mappings[middleIndex]; + if (!mapping || mapping[key].line !== line) { + return this.mappings.length - 1; + } + + while ( + middleIndex >= 1 && + this.mappings[middleIndex - 1][key].line === line + ) { + middleIndex--; + } + + while ( + middleIndex < this.mappings.length - 1 && + this.mappings[middleIndex + 1][key].line === line && + column > this.mappings[middleIndex][key].column + ) { + middleIndex++; + } + + return middleIndex; + } + + originalPositionFor(generatedPosition) { + let index = this.findClosest( + generatedPosition.line, + generatedPosition.column, + 'generated' + ); + return { + source: this.mappings[index].source, + name: this.mappings[index].name, + line: this.mappings[index].original.line, + column: this.mappings[index].original.column + }; + } + + generatedPositionFor(originalPosition) { + let index = this.findClosest( + originalPosition.line, + originalPosition.column, + 'original' + ); + return { + source: this.mappings[index].source, + name: this.mappings[index].name, + line: this.mappings[index].generated.line, + column: this.mappings[index].generated.column + }; + } + + sourceContentFor(fileName) { + return this.sources[fileName]; + } + offset(lineOffset = 0, columnOffset = 0) { this.mappings.map(mapping => { mapping.generated.line = mapping.generated.line + lineOffset; diff --git a/src/cli.js b/src/cli.js index 5321091d874..e3baf7265a6 100755 --- a/src/cli.js +++ b/src/cli.js @@ -98,6 +98,7 @@ program ) .option('--no-minify', 'disable minification') .option('--no-cache', 'disable the filesystem cache') + .option('--no-source-maps', 'disable sourcemaps') .option( '-t, --target ', 'set the runtime environment, either "node", "browser" or "electron". defaults to "browser"', diff --git a/src/transforms/uglify.js b/src/transforms/uglify.js index 9c89f101f80..49e45e52465 100644 --- a/src/transforms/uglify.js +++ b/src/transforms/uglify.js @@ -1,10 +1,11 @@ const {minify} = require('uglify-es'); +const SourceMap = require('../SourceMap'); module.exports = async function(asset) { await asset.parseIfNeeded(); // Convert AST into JS - let code = (await asset.generate()).js; + let source = (await asset.generate()).js; let customConfig = await asset.getConfig(['.uglifyrc']); let options = { @@ -14,15 +15,50 @@ module.exports = async function(asset) { } }; + let sourceMap; + if (asset.options.sourceMap) { + sourceMap = new SourceMap(); + options.output = { + source_map: { + add(source, gen_line, gen_col, orig_line, orig_col, name) { + sourceMap.addMapping({ + source, + name, + original: { + line: orig_line, + column: orig_col + }, + generated: { + line: gen_line, + column: gen_col + } + }); + } + } + }; + } + if (customConfig) { options = Object.assign(options, customConfig); } - let result = minify(code, options); + let result = minify(source, options); + if (result.error) { throw result.error; } + if (sourceMap) { + if (asset.sourceMap) { + asset.sourceMap = await new SourceMap().extendSourceMap( + asset.sourceMap, + sourceMap + ); + } else { + asset.sourceMap = sourceMap; + } + } + // babel-generator did our code generation for us, so remove the old AST asset.ast = null; asset.outputCode = result.code; diff --git a/test/css.js b/test/css.js index 584a96b42bb..57e39686d8f 100644 --- a/test/css.js +++ b/test/css.js @@ -160,6 +160,9 @@ describe('css', function() { assets: ['index.css'], childBundles: [] }, + { + type: 'map' + }, { type: 'woff2', assets: ['test.woff2'], diff --git a/test/integration/sourcemap-nested-minified/index.js b/test/integration/sourcemap-nested-minified/index.js new file mode 100644 index 00000000000..cff228a7641 --- /dev/null +++ b/test/integration/sourcemap-nested-minified/index.js @@ -0,0 +1,5 @@ +const local = require('./local'); + +module.exports = function() { + return local.a + local.b; +} \ No newline at end of file diff --git a/test/integration/sourcemap-nested-minified/local.js b/test/integration/sourcemap-nested-minified/local.js new file mode 100644 index 00000000000..6b8edab3ae6 --- /dev/null +++ b/test/integration/sourcemap-nested-minified/local.js @@ -0,0 +1,4 @@ +const util = require('./utils/util'); + +exports.a = 5; +exports.b = util.count(4, 5); \ No newline at end of file diff --git a/test/integration/sourcemap-nested-minified/utils/util.js b/test/integration/sourcemap-nested-minified/utils/util.js new file mode 100644 index 00000000000..94e3f35ece9 --- /dev/null +++ b/test/integration/sourcemap-nested-minified/utils/util.js @@ -0,0 +1,3 @@ +exports.count = function(a, b) { + return a + b; +} \ No newline at end of file diff --git a/test/sourcemaps.js b/test/sourcemaps.js index 0c2a8f82472..0d184ab0907 100644 --- a/test/sourcemaps.js +++ b/test/sourcemaps.js @@ -116,4 +116,36 @@ describe('sourcemaps', function() { assert.equal(typeof output, 'function'); assert.equal(output(), 14); }); + + it('should create a valid sourcemap for a minified js bundle with requires', async function() { + let b = await bundle( + __dirname + '/integration/sourcemap-nested-minified/index.js', + { + minify: true + } + ); + + assertBundleTree(b, { + name: 'index.js', + assets: ['index.js', 'local.js', 'util.js'], + childBundles: [ + { + name: 'index.map', + type: 'map' + } + ] + }); + + let raw = fs + .readFileSync(path.join(__dirname, '/dist/index.js')) + .toString(); + let map = fs + .readFileSync(path.join(__dirname, '/dist/index.map')) + .toString(); + mapValidator(raw, map); + + let output = run(b); + assert.equal(typeof output, 'function'); + assert.equal(output(), 14); + }); }); diff --git a/test/typescript.js b/test/typescript.js index 6f12634772b..a0cb3e63695 100644 --- a/test/typescript.js +++ b/test/typescript.js @@ -80,7 +80,7 @@ describe('typescript', function() { ); assert.equal(b.assets.size, 2); - assert.equal(b.childBundles.size, 0); + assert.equal(b.childBundles.size, 1); let output = run(b); assert.equal(typeof output.count, 'function');