diff --git a/.gitignore b/.gitignore index 19c622f..fc481ed 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ browserify-test/script.js browserify-test/script.map header-test/script.js header-test/script.map +browser-test/script.js +browser-test/script.map \ No newline at end of file diff --git a/browser-test/script.js b/browser-test/script.js deleted file mode 100644 index e16d414..0000000 --- a/browser-test/script.js +++ /dev/null @@ -1,25 +0,0 @@ -// Generated by CoffeeScript 1.7.1 -(function() { - var e, foo; - - sourceMapSupport.install(); - - foo = function() { - throw new Error('foo'); - }; - - try { - foo(); - } catch (_error) { - e = _error; - if (/\bscript\.coffee\b/.test(e.stack)) { - document.body.appendChild(document.createTextNode('Test passed')); - } else { - document.body.appendChild(document.createTextNode('Test failed')); - console.log(e.stack); - } - } - -}).call(this); - -//# sourceMappingURL=script.map diff --git a/browser-test/script.map b/browser-test/script.map deleted file mode 100644 index 127a2b6..0000000 --- a/browser-test/script.map +++ /dev/null @@ -1,10 +0,0 @@ -{ - "version": 3, - "file": "script.js", - "sourceRoot": "..", - "sources": [ - "browser-test/script.coffee" - ], - "names": [], - "mappings": ";AAAA;AAAA,MAAA,MAAA;;AAAA,EAAA,gBAAgB,CAAC,OAAjB,CAAA,CAAA,CAAA;;AAAA,EAEA,GAAA,GAAM,SAAA,GAAA;AAAG,UAAU,IAAA,KAAA,CAAM,KAAN,CAAV,CAAH;EAAA,CAFN,CAAA;;AAIA;AACE,IAAA,GAAA,CAAA,CAAA,CADF;GAAA,cAAA;AAGE,IADI,UACJ,CAAA;AAAA,IAAA,IAAG,oBAAoB,CAAC,IAArB,CAA0B,CAAC,CAAC,KAA5B,CAAH;AACE,MAAA,QAAQ,CAAC,IAAI,CAAC,WAAd,CAA0B,QAAQ,CAAC,cAAT,CAAwB,aAAxB,CAA1B,CAAA,CADF;KAAA,MAAA;AAGE,MAAA,QAAQ,CAAC,IAAI,CAAC,WAAd,CAA0B,QAAQ,CAAC,cAAT,CAAwB,aAAxB,CAA1B,CAAA,CAAA;AAAA,MACA,OAAO,CAAC,GAAR,CAAY,CAAC,CAAC,KAAd,CADA,CAHF;KAHF;GAJA;AAAA" -} \ No newline at end of file diff --git a/source-map-support.js b/source-map-support.js index 49ed72d..353189d 100644 --- a/source-map-support.js +++ b/source-map-support.js @@ -1,6 +1,7 @@ var SourceMapConsumer = require('source-map').SourceMapConsumer; var path = require('path'); var fs = require('fs'); +var url = require('url'); // Only install once if called multiple times var errorFormatterInstalled = false; @@ -72,7 +73,7 @@ retrieveFileHandlers.push(function(path) { // Otherwise, use the filesystem else { - var contents = fs.readFileSync(path, 'utf8'); + var contents = fs.readFileSync(toLocalPath(path), 'utf8'); } } catch (e) { var contents = null; @@ -81,14 +82,85 @@ retrieveFileHandlers.push(function(path) { return fileContentsCache[path] = contents; }); +// Determines whether a file path is a URI using the following rules: +// - A path is a URI if it starts with an alpha character followed by +// one or more of either an alpha character, digit, '+', '-', or '-', +// followed by a colon. For example: +// http://tempuri.org/path - Web URI +// file://server/share - File URI for UNC path +// file:///c:/path - File URI for DOS path +// urn:custom - Other URI +// +// NOTE: A path is NOT a URI if it starts with a single alpha character +// and a colon is, instead it is treated as a DOS path. For example: +// c:/path - DOS path +// c:\path - DOS path +// +// - Any other sequence of characters is not considered a URI. +function isURI(file) { + return /^[a-z][a-z0-9+.\-]+:/i.test(file); +} + +// Tries to convert a URI to a local path. If the URI is a +// file URI (file://), it is converted into a local path format +// that NodeJS understands using the following rules: +// - A file URI with a host name is treated as a UNC path/NTFS +// long path: +// file://server/share -> \\host\path +// +// - A file URI without a host name, whose first path segment is +// a single alpha character followed by either a colon (':') or +// pipe ('|') is treated as a rooted DOS path: +// file:///c:/path -> c:\path +// file:///c|/path -> c:\path +// +// - A file URI without a hostname that is not treated as a DOS +// path is treated as a rooted POSIX path: +// file:///etc/path -> /etc/path +// +// - Querystring (?) and fragments (#) are removed. +function toLocalPath(uri) { + if (isURI(uri)) { + var parsed = url.parse(uri); + if (parsed.protocol === 'file:') { + if (parsed.hostname) { + // A file URI with a hostname is a UNC path. + return '\\\\' + parsed.hostname + decodeURIComponent(parsed.path).replace(/\//g, '\\'); + } + + if (parsed.pathname) { + var path = decodeURIComponent(parsed.pathname); + if (/^[\\/][a-z][:|]/i.test(path)) { + // DOS path + return path.slice(1, 2) + ':' + path.slice(3).replace(/\//g, '\\'); + } + else { + // POSIX path + return path.replace(/\\/g, '/'); + } + } + } + } + + // Unhandled URI format. + return uri; +} + // Support URLs relative to a directory, but be careful about a protocol prefix // in case we are in the browser (i.e. directories may start with "http://") -function supportRelativeURL(file, url) { - if (!file) return url; - var dir = path.dirname(file); - var match = /^\w+:\/\/[^\/]*/.exec(dir); - var protocol = match ? match[0] : ''; - return protocol + path.resolve(dir.slice(protocol.length), url); +function supportRelativeURL(base, relative) { + if (!base || isURI(relative)) { + // no base or relative is absolute URI + return relative; + } + else if (isURI(base)) { + // base is a url, use url to combine. + return url.resolve(base, relative); + } + else { + var dir = path.dirname(base); + return path.resolve(dir, relative); + } } function retrieveSourceMapURL(source) { @@ -420,6 +492,7 @@ exports.wrapCallSite = wrapCallSite; exports.getErrorSource = getErrorSource; exports.mapSourcePosition = mapSourcePosition; exports.retrieveSourceMap = retrieveSourceMap; +exports.toLocalPath = toLocalPath; exports.install = function(options) { options = options || {}; diff --git a/test.js b/test.js index 1315c9f..8acf5d0 100644 --- a/test.js +++ b/test.js @@ -6,6 +6,7 @@ var SourceMapGenerator = require('source-map').SourceMapGenerator; var child_process = require('child_process'); var assert = require('assert'); var fs = require('fs'); +var path = require('path'); function compareLines(actual, expected) { assert(actual.length >= expected.length, 'got ' + actual.length + ' lines but expected at least ' + expected.length + ' lines'); @@ -73,8 +74,31 @@ function createMultiLineSourceMapWithSourcesContent() { return sourceMap; } -function compareStackTrace(sourceMap, source, expected) { - // Check once with a separate source map +function createMultiLineSourceMapWithFileUrl() { + var sourceMap = new SourceMapGenerator({ + file: '.generated.js', + sourceRoot: fileURL('.') + }); + for (var i = 1; i <= 100; i++) { + sourceMap.addMapping({ + generated: { line: i, column: 0 }, + original: { line: 1000 + i, column: 99 + i }, + source: 'line' + i + '.js' + }); + } + return sourceMap; +} + + +function fileURL(file) { + file = path.resolve(file); + file = file.replace(/\\/g, '/'); + file = file[0] !== '/' ? '/' + file : file; + return encodeURI('file://' + file); +} + +function compareStackTrace(sourceMap, source, expected, useFileUrl) { + // Check once with a separate source map using a relative url fs.writeFileSync('.generated.js.map', sourceMap); fs.writeFileSync('.generated.js', 'exports.test = function() {' + source.join('\n') + '};//@ sourceMappingURL=.generated.js.map'); @@ -86,6 +110,19 @@ function compareStackTrace(sourceMap, source, expected) { } fs.unlinkSync('.generated.js'); fs.unlinkSync('.generated.js.map'); + + // Check again with a source map using a relative url + fs.writeFileSync('.generated.js.map', sourceMap); + fs.writeFileSync('.generated.js', 'exports.test = function() {' + + source.join('\n') + '};//@ sourceMappingURL=' + fileURL('.generated.js.map')); + try { + delete require.cache[require.resolve('./.generated')]; + require('./.generated').test(); + } catch (e) { + compareLines(e.stack.split('\n'), expected); + } + fs.unlinkSync('.generated.js'); + fs.unlinkSync('.generated.js.map'); // Check again with an inline source map (in a data URL) fs.writeFileSync('.generated.js', 'exports.test = function() {' + @@ -110,7 +147,7 @@ function compareStdout(done, sourceMap, source, expected) { compareLines( (stdout + stderr) .trim() - .split('\n') + .split(/\r\n|\r|\n/g) .filter(function (line) { return line !== '' }), // Empty lines are not relevant. expected ); @@ -129,7 +166,7 @@ it('normal throw', function() { 'throw new Error("test");' ], [ 'Error: test', - /^ at Object\.exports\.test \(.*\/line1\.js:1001:101\)$/ + /^ at Object\.exports\.test \(.*[\\/]line1\.js:1001:101\)$/ ]); }); @@ -141,8 +178,8 @@ it('throw inside function', function() { 'foo();' ], [ 'Error: test', - /^ at foo \(.*\/line2\.js:1002:102\)$/, - /^ at Object\.exports\.test \(.*\/line4\.js:1004:104\)$/ + /^ at foo \(.*[\\/]line2\.js:1002:102\)$/, + /^ at Object\.exports\.test \(.*[\\/]line4\.js:1004:104\)$/ ]); }); @@ -157,9 +194,9 @@ it('throw inside function inside function', function() { 'foo();' ], [ 'Error: test', - /^ at bar \(.*\/line3\.js:1003:103\)$/, - /^ at foo \(.*\/line5\.js:1005:105\)$/, - /^ at Object\.exports\.test \(.*\/line7\.js:1007:107\)$/ + /^ at bar \(.*[\\/]line3\.js:1003:103\)$/, + /^ at foo \(.*[\\/]line5\.js:1005:105\)$/, + /^ at Object\.exports\.test \(.*[\\/]line7\.js:1007:107\)$/ ]); }); @@ -170,9 +207,9 @@ it('eval', function() { 'Error: test', // Before Node 4, `Object.eval`, after just `eval`. - /^ at (?:Object\.)?eval \(eval at \(.*\/line1\.js:1001:101\)/, + /^ at (?:Object\.)?eval \(eval at \(.*[\\/]line1\.js:1001:101\)/, - /^ at Object\.exports\.test \(.*\/line1\.js:1001:101\)$/ + /^ at Object\.exports\.test \(.*[\\/]line1\.js:1001:101\)$/ ]); }); @@ -181,9 +218,9 @@ it('eval inside eval', function() { 'eval("eval(\'throw new Error(\\"test\\")\')");' ], [ 'Error: test', - /^ at (?:Object\.)?eval \(eval at \(eval at \(.*\/line1\.js:1001:101\)/, - /^ at (?:Object\.)?eval \(eval at \(.*\/line1\.js:1001:101\)/, - /^ at Object\.exports\.test \(.*\/line1\.js:1001:101\)$/ + /^ at (?:Object\.)?eval \(eval at \(eval at \(.*[\\/]line1\.js:1001:101\)/, + /^ at (?:Object\.)?eval \(eval at \(.*[\\/]line1\.js:1001:101\)/, + /^ at Object\.exports\.test \(.*[\\/]line1\.js:1001:101\)$/ ]); }); @@ -195,9 +232,9 @@ it('eval inside function', function() { 'foo();' ], [ 'Error: test', - /^ at eval \(eval at foo \(.*\/line2\.js:1002:102\)/, - /^ at foo \(.*\/line2\.js:1002:102\)/, - /^ at Object\.exports\.test \(.*\/line4\.js:1004:104\)$/ + /^ at eval \(eval at foo \(.*[\\/]line2\.js:1002:102\)/, + /^ at foo \(.*[\\/]line2\.js:1002:102\)/, + /^ at Object\.exports\.test \(.*[\\/]line4\.js:1004:104\)$/ ]); }); @@ -207,7 +244,7 @@ it('eval with sourceURL', function() { ], [ 'Error: test', /^ at (?:Object\.)?eval \(sourceURL\.js:1:7\)$/, - /^ at Object\.exports\.test \(.*\/line1\.js:1001:101\)$/ + /^ at Object\.exports\.test \(.*[\\/]line1\.js:1001:101\)$/ ]); }); @@ -217,8 +254,8 @@ it('eval with sourceURL inside eval', function() { ], [ 'Error: test', /^ at (?:Object\.)?eval \(sourceURL\.js:1:7\)$/, - /^ at (?:Object\.)?eval \(eval at \(.*\/line1\.js:1001:101\)/, - /^ at Object\.exports\.test \(.*\/line1\.js:1001:101\)$/ + /^ at (?:Object\.)?eval \(eval at \(.*[\\/]line1\.js:1001:101\)/, + /^ at Object\.exports\.test \(.*[\\/]line1\.js:1001:101\)$/ ]); }); @@ -228,7 +265,7 @@ it('function constructor', function() { ], [ 'SyntaxError: Unexpected token )', /^ at (?:Object\.)?Function \((?:unknown source||native)\)$/, - /^ at Object\.exports\.test \(.*\/line1\.js:1001:101\)$/, + /^ at Object\.exports\.test \(.*[\\/]line1\.js:1001:101\)$/, ]); }); @@ -237,7 +274,7 @@ it('throw with empty source map', function() { 'throw new Error("test");' ], [ 'Error: test', - /^ at Object\.exports\.test \(.*\/.generated.js:1:34\)$/ + /^ at Object\.exports\.test \(.*[\\/].generated.js:1:34\)$/ ]); }); @@ -246,7 +283,7 @@ it('throw with source map with gap', function() { 'throw new Error("test");' ], [ 'Error: test', - /^ at Object\.exports\.test \(.*\/.generated.js:1:34\)$/ + /^ at Object\.exports\.test \(.*[\\/].generated.js:1:34\)$/ ]); }); @@ -255,7 +292,16 @@ it('sourcesContent with data URL', function() { 'throw new Error("test");' ], [ 'Error: test', - /^ at Object\.exports\.test \(.*\/original.js:1001:5\)$/ + /^ at Object\.exports\.test \(.*[\\/]original.js:1001:5\)$/ + ]); +}); + +it('support file urls', function () { + compareStackTrace(createMultiLineSourceMapWithFileUrl(), [ + 'throw new Error("test");' + ], [ + 'Error: test', + /^ at Object\.exports\.test \(.*[\\/]line1\.js:1001:101\)$/ ]); }); @@ -265,7 +311,7 @@ it('finds the last sourceMappingURL', function() { 'throw new Error("test");' ], [ 'Error: test', - /^ at Object\.exports\.test \(.*\/original.js:1002:5\)$/ + /^ at Object\.exports\.test \(.*[\\/]original.js:1002:5\)$/ ]); }); @@ -277,11 +323,11 @@ it('default options', function(done) { 'process.nextTick(foo);', 'process.nextTick(function() { process.exit(1); });' ], [ - /\/.original\.js:1$/, + /[\\/].original\.js:1$/, 'this is the original code', '^', 'Error: this is the error', - /^ at foo \(.*\/.original\.js:1:1\)$/ + /^ at foo \(.*[\\/].original\.js:1:1\)$/ ]); }); @@ -292,11 +338,11 @@ it('handleUncaughtExceptions is true', function(done) { 'require("./source-map-support").install({ handleUncaughtExceptions: true });', 'process.nextTick(foo);' ], [ - /\/.original\.js:1$/, + /[\\/].original\.js:1$/, 'this is the original code', '^', 'Error: this is the error', - /^ at foo \(.*\/.original\.js:1:1\)$/ + /^ at foo \(.*[\\/].original\.js:1:1\)$/ ]); }); @@ -307,7 +353,7 @@ it('handleUncaughtExceptions is false', function(done) { 'require("./source-map-support").install({ handleUncaughtExceptions: false });', 'process.nextTick(foo);' ], [ - /\/.generated.js:2$/, + /[\\/].generated.js:2$/, 'function foo() { throw new Error("this is the error"); }', // Before Node 4, the arrow points on the `new`, after on the @@ -315,7 +361,7 @@ it('handleUncaughtExceptions is false', function(done) { /^ (?: )?\^$/, 'Error: this is the error', - /^ at foo \(.*\/.original\.js:1:1\)$/ + /^ at foo \(.*[\\/].original\.js:1:1\)$/ ]); }); @@ -326,11 +372,11 @@ it('default options with empty source map', function(done) { 'require("./source-map-support").install();', 'process.nextTick(foo);' ], [ - /\/.generated.js:2$/, + /[\\/].generated.js:2$/, 'function foo() { throw new Error("this is the error"); }', /^ (?: )?\^$/, 'Error: this is the error', - /^ at foo \(.*\/.generated.js:2:24\)$/ + /^ at foo \(.*[\\/].generated.js:2:24\)$/ ]); }); @@ -341,11 +387,11 @@ it('default options with source map with gap', function(done) { 'require("./source-map-support").install();', 'process.nextTick(foo);' ], [ - /\/.generated.js:2$/, + /[\\/].generated.js:2$/, 'function foo() { throw new Error("this is the error"); }', /^ (?: )?\^$/, 'Error: this is the error', - /^ at foo \(.*\/.generated.js:2:24\)$/ + /^ at foo \(.*[\\/].generated.js:2:24\)$/ ]); }); @@ -358,7 +404,7 @@ it('specifically requested error source', function(done) { 'process.on("uncaughtException", function (e) { console.log("SRC:" + sms.getErrorSource(e)); });', 'process.nextTick(foo);' ], [ - /^SRC:.*\/.original.js:1$/, + /^SRC:.*[\\/].original.js:1$/, 'this is the original code', '^' ]); @@ -372,11 +418,11 @@ it('sourcesContent', function(done) { 'process.nextTick(foo);', 'process.nextTick(function() { process.exit(1); });' ], [ - /\/original\.js:1002$/, + /[\\/]original\.js:1002$/, ' line 2', ' ^', 'Error: this is the error', - /^ at foo \(.*\/original\.js:1002:5\)$/ + /^ at foo \(.*[\\/]original\.js:1002:5\)$/ ]); }); @@ -399,9 +445,9 @@ it('missing source maps should also be cached', function(done) { 'process.nextTick(function() { console.log(count); });', ], [ 'Error: this is the error', - /^ at foo \(.*\/.generated.js:4:15\)$/, + /^ at foo \(.*[\\/].generated.js:4:15\)$/, 'Error: this is the error', - /^ at foo \(.*\/.generated.js:4:15\)$/, + /^ at foo \(.*[\\/].generated.js:4:15\)$/, '1', // The retrieval should only be attempted once ]); }); @@ -432,9 +478,9 @@ it('should consult all retrieve source map providers', function(done) { 'process.nextTick(function() { console.log(count); });', ], [ 'Error: this is the error', - /^ at foo \(.*\/original.js:1004:5\)$/, + /^ at foo \(.*[\\/]original.js:1004:5\)$/, 'Error: this is the error', - /^ at foo \(.*\/original.js:1004:5\)$/, + /^ at foo \(.*[\\/]original.js:1004:5\)$/, '1', // The retrieval should only be attempted once ]); }); @@ -472,7 +518,7 @@ it('allows code/comments after sourceMappingURL', function() { var source = [ 'throw new Error("test");' ]; var expected = [ 'Error: test', - /^ at Object\.exports\.test \(.*\/line1\.js:1001:101\)$/ + /^ at Object\.exports\.test \(.*[\\/]line1\.js:1001:101\)$/ ]; fs.writeFileSync('.generated.js', 'exports.test = function() {' +