node-source-map-support/source-map-support.js
Vlad GURDIGA af37071383 Handle missing source maps
mapSourcePosition() will try to load the compiled script’s text and
extract its source map. When the source map is found, the module will
cache it, but when the script just doesn’t have any source map at all,
the module doesn’t remember that, and will load the script text again at
every subsequent call.
2015-02-24 17:42:16 +02:00

387 lines
12 KiB
JavaScript

var SourceMapConsumer = require('source-map').SourceMapConsumer;
var path = require('path');
var fs = require('fs');
// Only install once if called multiple times
var alreadyInstalled = false;
// If true, the caches are reset before a stack trace formatting operation
var emptyCacheBetweenOperations = false;
// Maps a file path to a string containing the file contents
var fileContentsCache = {};
// Maps a file path to a source map for that file
var sourceMapCache = {};
function isInBrowser() {
return typeof window !== 'undefined';
}
function retrieveFile(path) {
if (path in fileContentsCache) {
return fileContentsCache[path];
}
try {
// Use SJAX if we are in the browser
if (isInBrowser()) {
var xhr = new XMLHttpRequest();
xhr.open('GET', path, false);
xhr.send(null);
var contents = xhr.readyState === 4 ? xhr.responseText : null;
}
// Otherwise, use the filesystem
else {
var contents = fs.readFileSync(path, 'utf8');
}
} catch (e) {
var contents = null;
}
return fileContentsCache[path] = contents;
}
// 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 retrieveSourceMapURL(source) {
var fileData;
if (isInBrowser()) {
var xhr = new XMLHttpRequest();
xhr.open('GET', source, false);
xhr.send(null);
fileData = xhr.readyState === 4 ? xhr.responseText : null;
// Support providing a sourceMappingURL via the SourceMap header
var sourceMapHeader = xhr.getResponseHeader("SourceMap") ||
xhr.getResponseHeader("X-SourceMap");
if (sourceMapHeader) {
return sourceMapHeader;
}
}
// Get the URL of the source map
fileData = retrieveFile(source);
var match = /\/\/[#@]\s*sourceMappingURL=(.*)\s*$/m.exec(fileData);
if (!match) return null;
return match[1];
};
// Can be overridden by the retrieveSourceMap option to install. Takes a
// generated source filename; returns a {map, optional url} object, or null if
// there is no source map. The map field may be either a string or the parsed
// JSON object (ie, it must be a valid argument to the SourceMapConsumer
// constructor).
function retrieveSourceMap(source) {
var sourceMappingURL = retrieveSourceMapURL(source);
if (!sourceMappingURL) return null;
// Read the contents of the source map
var sourceMapData;
var dataUrlPrefix = "data:application/json;base64,";
if (sourceMappingURL.slice(0, dataUrlPrefix.length).toLowerCase() == dataUrlPrefix) {
// Support source map URL as a data url
sourceMapData = new Buffer(sourceMappingURL.slice(dataUrlPrefix.length), "base64").toString();
sourceMappingURL = null;
} else {
// Support source map URLs relative to the source URL
sourceMappingURL = supportRelativeURL(source, sourceMappingURL);
sourceMapData = retrieveFile(sourceMappingURL, 'utf8');
}
if (!sourceMapData) {
return null;
}
return {
url: sourceMappingURL,
map: sourceMapData
};
}
function mapSourcePosition(position) {
var sourceMap = sourceMapCache[position.source];
if (!sourceMap) {
// Call the (overrideable) retrieveSourceMap function to get the source map.
var urlAndMap = retrieveSourceMap(position.source);
if (urlAndMap) {
sourceMap = sourceMapCache[position.source] = {
url: urlAndMap.url,
map: new SourceMapConsumer(urlAndMap.map)
};
// Load all sources stored inline with the source map into the file cache
// to pretend like they are already loaded. They may not exist on disk.
if (sourceMap.map.sourcesContent) {
sourceMap.map.sources.forEach(function(source, i) {
var contents = sourceMap.map.sourcesContent[i];
if (contents) {
var url = supportRelativeURL(sourceMap.url, source);
fileContentsCache[url] = contents;
}
});
}
} else {
sourceMap = sourceMapCache[position.source] = {
url: null,
map: null
};
}
}
// Resolve the source URL relative to the URL of the source map
if (sourceMap && sourceMap.map) {
var originalPosition = sourceMap.map.originalPositionFor(position);
// Only return the original position if a matching line was found. If no
// matching line is found then we return position instead, which will cause
// the stack trace to print the path and line for the compiled file. It is
// better to give a precise location in the compiled file than a vague
// location in the original file.
if (originalPosition.source !== null) {
originalPosition.source = supportRelativeURL(
sourceMap.url, originalPosition.source);
return originalPosition;
}
}
return position;
}
// Parses code generated by FormatEvalOrigin(), a function inside V8:
// https://code.google.com/p/v8/source/browse/trunk/src/messages.js
function mapEvalOrigin(origin) {
// Most eval() calls are in this format
var match = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(origin);
if (match) {
var position = mapSourcePosition({
source: match[2],
line: match[3],
column: match[4] - 1
});
return 'eval at ' + match[1] + ' (' + position.source + ':' +
position.line + ':' + (position.column + 1) + ')';
}
// Parse nested eval() calls using recursion
match = /^eval at ([^(]+) \((.+)\)$/.exec(origin);
if (match) {
return 'eval at ' + match[1] + ' (' + mapEvalOrigin(match[2]) + ')';
}
// Make sure we still return useful information if we didn't find anything
return origin;
}
// This is copied almost verbatim from the V8 source code at
// https://code.google.com/p/v8/source/browse/trunk/src/messages.js. The
// implementation of wrapCallSite() used to just forward to the actual source
// code of CallSite.prototype.toString but unfortunately a new release of V8
// did something to the prototype chain and broke the shim. The only fix I
// could find was copy/paste.
function CallSiteToString() {
var fileName;
var fileLocation = "";
if (this.isNative()) {
fileLocation = "native";
} else {
fileName = this.getScriptNameOrSourceURL();
if (!fileName && this.isEval()) {
fileLocation = this.getEvalOrigin();
fileLocation += ", "; // Expecting source position to follow.
}
if (fileName) {
fileLocation += fileName;
} else {
// Source code does not originate from a file and is not native, but we
// can still get the source position inside the source string, e.g. in
// an eval string.
fileLocation += "<anonymous>";
}
var lineNumber = this.getLineNumber();
if (lineNumber != null) {
fileLocation += ":" + lineNumber;
var columnNumber = this.getColumnNumber();
if (columnNumber) {
fileLocation += ":" + columnNumber;
}
}
}
var line = "";
var functionName = this.getFunctionName();
var addSuffix = true;
var isConstructor = this.isConstructor();
var isMethodCall = !(this.isToplevel() || isConstructor);
if (isMethodCall) {
var typeName = this.getTypeName();
var methodName = this.getMethodName();
if (functionName) {
if (typeName && functionName.indexOf(typeName) != 0) {
line += typeName + ".";
}
line += functionName;
if (methodName && functionName.indexOf("." + methodName) != functionName.length - methodName.length - 1) {
line += " [as " + methodName + "]";
}
} else {
line += typeName + "." + (methodName || "<anonymous>");
}
} else if (isConstructor) {
line += "new " + (functionName || "<anonymous>");
} else if (functionName) {
line += functionName;
} else {
line += fileLocation;
addSuffix = false;
}
if (addSuffix) {
line += " (" + fileLocation + ")";
}
return line;
}
function cloneCallSite(frame) {
var object = {};
Object.getOwnPropertyNames(Object.getPrototypeOf(frame)).forEach(function(name) {
object[name] = /^(?:is|get)/.test(name) ? function() { return frame[name].call(frame); } : frame[name];
});
object.toString = CallSiteToString;
return object;
}
function wrapCallSite(frame) {
// Most call sites will return the source file from getFileName(), but code
// passed to eval() ending in "//# sourceURL=..." will return the source file
// from getScriptNameOrSourceURL() instead
var source = frame.getFileName() || frame.getScriptNameOrSourceURL();
if (source) {
var position = mapSourcePosition({
source: source,
line: frame.getLineNumber(),
column: frame.getColumnNumber() - 1
});
frame = cloneCallSite(frame);
frame.getFileName = function() { return position.source; };
frame.getLineNumber = function() { return position.line; };
frame.getColumnNumber = function() { return position.column + 1; };
frame.getScriptNameOrSourceURL = function() { return position.source; };
return frame;
}
// Code called using eval() needs special handling
var origin = frame.isEval() && frame.getEvalOrigin();
if (origin) {
origin = mapEvalOrigin(origin);
frame = cloneCallSite(frame);
frame.getEvalOrigin = function() { return origin; };
return frame;
}
// If we get here then we were unable to change the source position
return frame;
}
// This function is part of the V8 stack trace API, for more info see:
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function prepareStackTrace(error, stack) {
if (emptyCacheBetweenOperations) {
fileContentsCache = {};
sourceMapCache = {};
}
return error + stack.map(function(frame) {
return '\n at ' + wrapCallSite(frame);
}).join('');
}
// Generate position and snippet of original source with pointer
function getErrorSource(error) {
var match = /\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(error.stack);
if (match) {
var source = match[1];
var line = +match[2];
var column = +match[3];
// Support the inline sourceContents inside the source map
var contents = fileContentsCache[source];
// Support files on disk
if (!contents && fs.existsSync(source)) {
contents = fs.readFileSync(source, 'utf8');
}
// Format the line from the original source code like node does
if (contents) {
var code = contents.split(/(?:\r\n|\r|\n)/)[line - 1];
if (code) {
return '\n' + source + ':' + line + '\n' + code + '\n' +
new Array(column).join(' ') + '^';
}
}
}
return null;
}
// Mimic node's stack trace printing when an exception escapes the process
function handleUncaughtExceptions(error) {
if (!error || !error.stack) {
console.log('Uncaught exception:', error);
} else {
var source = getErrorSource(error);
if (source !== null) console.log(source);
console.log(error.stack);
}
process.exit(1);
}
exports.wrapCallSite = wrapCallSite;
exports.getErrorSource = getErrorSource;
exports.mapSourcePosition = mapSourcePosition;
exports.retrieveSourceMap = retrieveSourceMap;
exports.install = function(options) {
if (!alreadyInstalled) {
alreadyInstalled = true;
Error.prepareStackTrace = prepareStackTrace;
// Configure options
options = options || {};
var installHandler = 'handleUncaughtExceptions' in options ?
options.handleUncaughtExceptions : true;
emptyCacheBetweenOperations = 'emptyCacheBetweenOperations' in options ?
options.emptyCacheBetweenOperations : false;
// Allow sources to be found by methods other than reading the files
// directly from disk.
if (options.retrieveFile)
retrieveFile = options.retrieveFile;
// Allow source maps to be found by methods other than reading the files
// directly from disk.
if (options.retrieveSourceMap)
retrieveSourceMap = options.retrieveSourceMap;
// Provide the option to not install the uncaught exception handler. This is
// to support other uncaught exception handlers (in test frameworks, for
// example). If this handler is not installed and there are no other uncaught
// exception handlers, uncaught exceptions will be caught by node's built-in
// exception handler and the process will still be terminated. However, the
// generated JavaScript code will be shown above the stack trace instead of
// the original source code.
if (installHandler && !isInBrowser()) {
process.on('uncaughtException', handleUncaughtExceptions);
}
}
};