2018-07-06 11:20:35 -04:00
|
|
|
// Copyright 2014 Evan Wallace
|
2018-07-23 14:46:30 -04:00
|
|
|
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
|
2018-07-06 11:20:35 -04:00
|
|
|
// Originated from source-map-support but has been heavily modified for deno.
|
2018-08-02 13:13:32 -04:00
|
|
|
|
2018-07-06 11:20:35 -04:00
|
|
|
import { SourceMapConsumer, MappedPosition } from "source-map";
|
|
|
|
import * as base64 from "base64-js";
|
|
|
|
import { arrayToStr } from "./util";
|
2018-08-06 18:37:32 -04:00
|
|
|
import { CallSite, RawSourceMap } from "./types";
|
2018-07-06 11:20:35 -04:00
|
|
|
|
|
|
|
const consumers = new Map<string, SourceMapConsumer>();
|
|
|
|
|
|
|
|
interface Options {
|
|
|
|
// A callback the returns generated file contents.
|
|
|
|
getGeneratedContents: GetGeneratedContentsCallback;
|
|
|
|
// Usually set the following to true. Set to false for testing.
|
|
|
|
installPrepareStackTrace: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Position {
|
|
|
|
source: string; // Filename
|
|
|
|
column: number;
|
|
|
|
line: number;
|
|
|
|
}
|
|
|
|
|
2018-08-02 13:13:32 -04:00
|
|
|
type GetGeneratedContentsCallback = (fileName: string) => string | RawSourceMap;
|
2018-07-06 11:20:35 -04:00
|
|
|
|
|
|
|
let getGeneratedContents: GetGeneratedContentsCallback;
|
|
|
|
|
2018-09-04 15:23:38 -04:00
|
|
|
// @internal
|
2018-07-06 11:20:35 -04:00
|
|
|
export function install(options: Options) {
|
|
|
|
getGeneratedContents = options.getGeneratedContents;
|
|
|
|
if (options.installPrepareStackTrace) {
|
|
|
|
Error.prepareStackTrace = prepareStackTraceWrapper;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-04 15:23:38 -04:00
|
|
|
// @internal
|
2018-07-06 11:20:35 -04:00
|
|
|
export function prepareStackTraceWrapper(
|
|
|
|
error: Error,
|
|
|
|
stack: CallSite[]
|
|
|
|
): string {
|
|
|
|
try {
|
|
|
|
return prepareStackTrace(error, stack);
|
|
|
|
} catch (prepareStackError) {
|
2018-08-15 12:40:30 -04:00
|
|
|
Error.prepareStackTrace = undefined;
|
2018-07-06 11:20:35 -04:00
|
|
|
console.log("=====Error inside of prepareStackTrace====");
|
|
|
|
console.log(prepareStackError.stack.toString());
|
|
|
|
console.log("=====Original error=======================");
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-04 15:23:38 -04:00
|
|
|
// @internal
|
2018-07-06 11:20:35 -04:00
|
|
|
export function prepareStackTrace(error: Error, stack: CallSite[]): string {
|
|
|
|
const frames = stack.map(
|
|
|
|
(frame: CallSite) => `\n at ${wrapCallSite(frame).toString()}`
|
|
|
|
);
|
|
|
|
return error.toString() + frames.join("");
|
|
|
|
}
|
|
|
|
|
2018-09-04 15:23:38 -04:00
|
|
|
// @internal
|
2018-07-06 11:20:35 -04:00
|
|
|
export function wrapCallSite(frame: CallSite): CallSite {
|
|
|
|
if (frame.isNative()) {
|
|
|
|
return 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
|
|
|
|
const source = frame.getFileName() || frame.getScriptNameOrSourceURL();
|
|
|
|
|
|
|
|
if (source) {
|
2018-08-15 12:40:30 -04:00
|
|
|
const line = frame.getLineNumber() || 0;
|
|
|
|
const column = (frame.getColumnNumber() || 1) - 1;
|
2018-07-06 11:20:35 -04:00
|
|
|
const position = mapSourcePosition({ source, line, column });
|
|
|
|
frame = cloneCallSite(frame);
|
|
|
|
frame.getFileName = () => position.source;
|
|
|
|
frame.getLineNumber = () => position.line;
|
|
|
|
frame.getColumnNumber = () => Number(position.column) + 1;
|
|
|
|
frame.getScriptNameOrSourceURL = () => position.source;
|
|
|
|
frame.toString = () => CallSiteToString(frame);
|
|
|
|
return frame;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Code called using eval() needs special handling
|
2018-08-15 12:40:30 -04:00
|
|
|
let origin = (frame.isEval() && frame.getEvalOrigin()) || undefined;
|
2018-07-06 11:20:35 -04:00
|
|
|
if (origin) {
|
|
|
|
origin = mapEvalOrigin(origin);
|
|
|
|
frame = cloneCallSite(frame);
|
|
|
|
frame.getEvalOrigin = () => origin;
|
|
|
|
return frame;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we get here then we were unable to change the source position
|
|
|
|
return frame;
|
|
|
|
}
|
|
|
|
|
|
|
|
function cloneCallSite(frame: CallSite): CallSite {
|
|
|
|
// tslint:disable:no-any
|
|
|
|
const obj: any = {};
|
|
|
|
const frame_ = frame as any;
|
|
|
|
const props = Object.getOwnPropertyNames(Object.getPrototypeOf(frame));
|
|
|
|
props.forEach(name => {
|
|
|
|
obj[name] = /^(?:is|get)/.test(name)
|
|
|
|
? () => frame_[name].call(frame)
|
|
|
|
: frame_[name];
|
|
|
|
});
|
|
|
|
return (obj as any) as CallSite;
|
|
|
|
// tslint:enable:no-any
|
|
|
|
}
|
|
|
|
|
|
|
|
// Taken from source-map-support, original copied from V8's messages.js
|
|
|
|
// MIT License. Copyright (c) 2014 Evan Wallace
|
|
|
|
function CallSiteToString(frame: CallSite): string {
|
|
|
|
let fileName;
|
|
|
|
let fileLocation = "";
|
|
|
|
if (frame.isNative()) {
|
|
|
|
fileLocation = "native";
|
|
|
|
} else {
|
|
|
|
fileName = frame.getScriptNameOrSourceURL();
|
|
|
|
if (!fileName && frame.isEval()) {
|
2018-08-15 12:40:30 -04:00
|
|
|
fileLocation = frame.getEvalOrigin() || "";
|
2018-07-06 11:20:35 -04:00
|
|
|
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>";
|
|
|
|
}
|
|
|
|
const lineNumber = frame.getLineNumber();
|
|
|
|
if (lineNumber != null) {
|
|
|
|
fileLocation += ":" + String(lineNumber);
|
|
|
|
const columnNumber = frame.getColumnNumber();
|
|
|
|
if (columnNumber) {
|
|
|
|
fileLocation += ":" + String(columnNumber);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let line = "";
|
|
|
|
const functionName = frame.getFunctionName();
|
|
|
|
let addSuffix = true;
|
|
|
|
const isConstructor = frame.isConstructor();
|
|
|
|
const isMethodCall = !(frame.isToplevel() || isConstructor);
|
|
|
|
if (isMethodCall) {
|
|
|
|
let typeName = frame.getTypeName();
|
|
|
|
// Fixes shim to be backward compatable with Node v0 to v4
|
|
|
|
if (typeName === "[object Object]") {
|
|
|
|
typeName = "null";
|
|
|
|
}
|
|
|
|
const methodName = frame.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 {
|
2018-08-15 12:40:30 -04:00
|
|
|
line += `${typeName}.${methodName || "<anonymous>"}`;
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|
|
|
|
} else if (isConstructor) {
|
2018-09-01 13:52:48 -04:00
|
|
|
line += `new ${functionName || "<anonymous>"}`;
|
2018-07-06 11:20:35 -04:00
|
|
|
} else if (functionName) {
|
|
|
|
line += functionName;
|
|
|
|
} else {
|
|
|
|
line += fileLocation;
|
|
|
|
addSuffix = false;
|
|
|
|
}
|
|
|
|
if (addSuffix) {
|
|
|
|
line += ` (${fileLocation})`;
|
|
|
|
}
|
|
|
|
return line;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Regex for detecting source maps
|
|
|
|
const reSourceMap = /^data:application\/json[^,]+base64,/;
|
|
|
|
|
2018-08-15 12:40:30 -04:00
|
|
|
function loadConsumer(source: string): SourceMapConsumer | null {
|
2018-07-06 11:20:35 -04:00
|
|
|
let consumer = consumers.get(source);
|
|
|
|
if (consumer == null) {
|
|
|
|
const code = getGeneratedContents(source);
|
|
|
|
if (!code) {
|
|
|
|
return null;
|
|
|
|
}
|
2018-08-02 13:13:32 -04:00
|
|
|
if (typeof code !== "string") {
|
|
|
|
throw new Error("expected string");
|
|
|
|
}
|
2018-07-06 11:20:35 -04:00
|
|
|
|
|
|
|
let sourceMappingURL = retrieveSourceMapURL(code);
|
|
|
|
if (!sourceMappingURL) {
|
|
|
|
throw Error("No source map?");
|
|
|
|
}
|
|
|
|
|
2018-08-02 13:13:32 -04:00
|
|
|
let sourceMapData: string | RawSourceMap;
|
2018-07-06 11:20:35 -04:00
|
|
|
if (reSourceMap.test(sourceMappingURL)) {
|
|
|
|
// Support source map URL as a data url
|
|
|
|
const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1);
|
|
|
|
const ui8 = base64.toByteArray(rawData);
|
|
|
|
sourceMapData = arrayToStr(ui8);
|
|
|
|
sourceMappingURL = source;
|
|
|
|
} else {
|
|
|
|
// Support source map URLs relative to the source URL
|
|
|
|
//sourceMappingURL = supportRelativeURL(source, sourceMappingURL);
|
|
|
|
sourceMapData = getGeneratedContents(sourceMappingURL);
|
|
|
|
}
|
|
|
|
|
2018-08-02 13:13:32 -04:00
|
|
|
const rawSourceMap =
|
|
|
|
typeof sourceMapData === "string"
|
|
|
|
? JSON.parse(sourceMapData)
|
|
|
|
: sourceMapData;
|
2018-07-06 11:20:35 -04:00
|
|
|
//console.log("sourceMapData", sourceMapData);
|
|
|
|
consumer = new SourceMapConsumer(rawSourceMap);
|
|
|
|
consumers.set(source, consumer);
|
|
|
|
}
|
|
|
|
return consumer;
|
|
|
|
}
|
|
|
|
|
2018-08-15 12:40:30 -04:00
|
|
|
function retrieveSourceMapURL(fileData: string): string | null {
|
2018-07-06 11:20:35 -04:00
|
|
|
// Get the URL of the source map
|
|
|
|
// tslint:disable-next-line:max-line-length
|
|
|
|
const re = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^\*]+?)[ \t]*(?:\*\/)[ \t]*$)/gm;
|
|
|
|
// Keep executing the search to find the *last* sourceMappingURL to avoid
|
|
|
|
// picking up sourceMappingURLs from comments, strings, etc.
|
|
|
|
let lastMatch, match;
|
|
|
|
while ((match = re.exec(fileData))) {
|
|
|
|
lastMatch = match;
|
|
|
|
}
|
|
|
|
if (!lastMatch) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return lastMatch[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
function mapSourcePosition(position: Position): MappedPosition {
|
|
|
|
const consumer = loadConsumer(position.source);
|
|
|
|
if (consumer == null) {
|
|
|
|
return position;
|
|
|
|
}
|
2018-09-01 18:49:47 -04:00
|
|
|
return consumer.originalPositionFor(position);
|
2018-07-06 11:20:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parses code generated by FormatEvalOrigin(), a function inside V8:
|
|
|
|
// https://code.google.com/p/v8/source/browse/trunk/src/messages.js
|
|
|
|
function mapEvalOrigin(origin: string): string {
|
|
|
|
// Most eval() calls are in this format
|
|
|
|
let match = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(origin);
|
|
|
|
if (match) {
|
|
|
|
const position = mapSourcePosition({
|
|
|
|
source: match[2],
|
|
|
|
line: Number(match[3]),
|
|
|
|
column: Number(match[4]) - 1
|
|
|
|
});
|
|
|
|
const pos = [
|
|
|
|
position.source,
|
|
|
|
position.line,
|
|
|
|
Number(position.column) + 1
|
|
|
|
].join(":");
|
|
|
|
return `eval at ${match[1]} (${pos})`;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|