From 70e4abd9b8db6b05de557ca6e9204339a21be06b Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Sat, 21 Dec 2013 10:51:00 -0800 Subject: [PATCH] feat(reporter): support source maps (rewrite stack traces) Closes #594 --- lib/reporter.js | 58 +++++++++++++++++++++++++++------- package.json | 3 +- test/unit/reporter.spec.coffee | 34 ++++++++++++++++++-- 3 files changed, 80 insertions(+), 15 deletions(-) diff --git a/lib/reporter.js b/lib/reporter.js index 20a326cb5..7f2fe23ae 100644 --- a/lib/reporter.js +++ b/lib/reporter.js @@ -1,20 +1,58 @@ +var util = require('util'); var log = require('./logger').create('reporter'); var MultiReporter = require('./reporters/multi'); var baseReporterDecoratorFactory = require('./reporters/base').decoratorFactory; +var SourceMapConsumer = require('source-map').SourceMapConsumer; -var createErrorFormatter = function(basePath) { - var URL_REGEXP = new RegExp('http:\\/\\/[^\\/]*' + - '\\/(base|absolute)([^\\?\\s\\:]*)(\\?\\w*)?', 'g'); +var createErrorFormatter = function(basePath, emitter, SourceMapConsumer) { + var lastServedFiles = []; + + emitter.on('file_list_modified', function(filesPromise) { + filesPromise.then(function(files) { + lastServedFiles = files.served; + }); + }); + + var findFile = function(path) { + for (var i = 0; i < lastServedFiles.length; i++) { + if (lastServedFiles[i].path === path) { + return lastServedFiles[i]; + } + } + return null; + }; + + var URL_REGEXP = new RegExp('http:\\/\\/[^\\/]*\\/' + + '(base|absolute)' + // prefix + '([^\\?\\s\\:]*)' + // path + '(\\?\\w*)?' + // sha + '(\\:(\\d+))?' + // line + '(\\:(\\d+))?' + // column + '', 'g'); return function(msg, indentation) { // remove domain and timestamp from source files // and resolve base path / absolute path urls into absolute path - msg = (msg || '').replace(URL_REGEXP, function(full, prefix, path) { + msg = (msg || '').replace(URL_REGEXP, function(_, prefix, path, __, ___, line, ____, column) { + if (prefix === 'base') { - return basePath + path; - } else if (prefix === 'absolute') { - return path; + path = basePath + path; + } + + var file = findFile(path); + + if (file && file.sourceMap) { + line = parseInt(line || '0', 10); + column = parseInt(column || '0', 10); + + var smc = new SourceMapConsumer(file.sourceMap); + var original = smc.originalPositionFor({line: line, column: column}); + + return util.format('%s:%d:%d <- %s:%d:%d', path, line, column, original.source, + original.line, original.column); } + + return path + (line ? ':' + line : '') + (column ? ':' + column : ''); }); // indent every line @@ -26,11 +64,9 @@ var createErrorFormatter = function(basePath) { }; }; -createErrorFormatter.$inject = ['config.basePath']; - var createReporters = function(names, config, emitter, injector) { - var errorFormatter = createErrorFormatter(config.basePath, config.urlRoot); + var errorFormatter = createErrorFormatter(config.basePath, emitter, SourceMapConsumer); var reporters = []; // TODO(vojta): instantiate all reporters through DI @@ -42,7 +78,7 @@ var createReporters = function(names, config, emitter, injector) { var locals = { baseReporterDecorator: ['factory', baseReporterDecoratorFactory], - formatError: ['factory', createErrorFormatter] + formatError: ['value', errorFormatter] }; try { diff --git a/package.json b/package.json index 97ddb84d3..f95623caf 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,8 @@ "log4js": "~0.6.3", "useragent": "~2.0.4", "graceful-fs": "~1.2.1", - "connect": "~2.8.4" + "connect": "~2.8.4", + "source-map": "~0.1.31" }, "devDependencies": { "grunt": "~0.4", diff --git a/test/unit/reporter.spec.coffee b/test/unit/reporter.spec.coffee index 160e663c1..354e22a76 100644 --- a/test/unit/reporter.spec.coffee +++ b/test/unit/reporter.spec.coffee @@ -2,7 +2,10 @@ # lib/reporter.js module #============================================================================== describe 'reporter', -> + EventEmitter = require('events').EventEmitter + File = require('../../lib/file_list').File loadFile = require('mocks').loadFile + q = require 'q' m = null beforeEach -> @@ -13,10 +16,11 @@ describe 'reporter', -> # formatError() [PRIVATE] #============================================================================== describe 'formatError', -> - formatError = null + formatError = emitter = null beforeEach -> - formatError = m.createErrorFormatter '', '/' + emitter = new EventEmitter + formatError = m.createErrorFormatter '', emitter it 'should indent', -> @@ -52,7 +56,7 @@ describe 'reporter', -> it 'should restore base paths', -> - formatError = m.createErrorFormatter '/some/base', '/' + formatError = m.createErrorFormatter '/some/base', emitter expect(formatError 'at http://localhost:123/base/a.js?123').to.equal 'at /some/base/a.js\n' @@ -64,3 +68,27 @@ describe 'reporter', -> it 'should preserve line numbers', -> ERROR = 'at http://local:1233/absolute/usr/path.js?6e31cb249ee5b32d91f37ea516ca0f84bddc5aa9:2' expect(formatError ERROR).to.equal 'at /usr/path.js:2\n' + + + describe 'source maps', -> + + class MockSourceMapConsumer + constructor: (sourceMap) -> + @source = sourceMap.replace 'SOURCE MAP ', '/original/' + originalPositionFor: (position) -> + source: @source + line: position.line + 2 + column: position.column + 2 + + it 'should rewrite stack traces', (done) -> + formatError = m.createErrorFormatter '/some/base', emitter, MockSourceMapConsumer + servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')] + servedFiles[0].sourceMap = 'SOURCE MAP a.js' + servedFiles[1].sourceMap = 'SOURCE MAP b.js' + + emitter.emit 'file_list_modified', q(served: servedFiles) + + scheduleNextTick -> + ERROR = 'at http://localhost:123/base/b.js:2:6' + expect(formatError ERROR).to.equal 'at /some/base/b.js:2:6 <- /original/b.js:4:8\n' + done()