// Copyright 2014 Evan Wallace // Copyright 2018 the Deno authors. All rights reserved. MIT license. // Originated from source-map-support but has been heavily modified for deno. import { SourceMapConsumer, MappedPosition } from "source-map"; import { CallSite, RawSourceMap } from "./types"; import { atob } from "./text_encoding"; const consumers = new Map(); 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; } type GetGeneratedContentsCallback = (fileName: string) => string | RawSourceMap; let getGeneratedContents: GetGeneratedContentsCallback; // @internal export function install(options: Options) { getGeneratedContents = options.getGeneratedContents; if (options.installPrepareStackTrace) { Error.prepareStackTrace = prepareStackTraceWrapper; } } // @internal export function prepareStackTraceWrapper( error: Error, stack: CallSite[] ): string { try { return prepareStackTrace(error, stack); } catch (prepareStackError) { Error.prepareStackTrace = undefined; console.log("=====Error inside of prepareStackTrace===="); console.log(prepareStackError.stack.toString()); console.log("=====Original error======================="); throw error; } } // @internal export function prepareStackTrace(error: Error, stack: CallSite[]): string { const frames = stack.map( frame => `\n at ${wrapCallSite(frame).toString()}` ); return `${error.toString()}${frames.join("")}`; } // @internal 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) { const line = frame.getLineNumber() || 0; const column = (frame.getColumnNumber() || 1) - 1; const position = mapSourcePosition({ source, line, column }); frame = cloneCallSite(frame); Object.assign(frame, { getFileName: () => position.source, getLineNumber: () => position.line, getColumnNumber: () => Number(position.column) + 1, getScriptNameOrSourceURL: () => position.source, toString: () => CallSiteToString(frame) }); return frame; } // Code called using eval() needs special handling let origin = (frame.isEval() && frame.getEvalOrigin()) || undefined; if (origin) { origin = mapEvalOrigin(origin); frame = cloneCallSite(frame); Object.assign(frame, { getEvalOrigin: () => origin, toString: () => CallSiteToString(frame) }); return frame; } // If we get here then we were unable to change the source position return frame; } function cloneCallSite( frame: CallSite // mixin: Partial & { toString: () => string } ): CallSite { const obj = {} as CallSite; const props = Object.getOwnPropertyNames( Object.getPrototypeOf(frame) ) as Array; for (const name of props) { obj[name] = /^(?:is|get)/.test(name) ? () => frame[name].call(frame) : frame[name]; } return obj; } // 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 fileLocation = ""; if (frame.isNative()) { fileLocation = "native"; } else { const fileName = frame.getScriptNameOrSourceURL(); if (!fileName && frame.isEval()) { fileLocation = frame.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 += ""; } const lineNumber = frame.getLineNumber(); if (lineNumber != null) { fileLocation += `:${lineNumber}`; const columnNumber = frame.getColumnNumber(); if (columnNumber) { fileLocation += `:${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 compatible 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 { line += `${typeName}.${methodName || ""}`; } } else if (isConstructor) { line += `new ${functionName || ""}`; } 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,/; export function loadConsumer(source: string): SourceMapConsumer | null { let consumer = consumers.get(source); if (consumer == null) { const code = getGeneratedContents(source); if (!code) { return null; } if (typeof code !== "string") { throw new Error("expected string"); } let sourceMappingURL = retrieveSourceMapURL(code); if (!sourceMappingURL) { throw Error("No source map?"); } let sourceMapData: string | RawSourceMap; if (reSourceMap.test(sourceMappingURL)) { // Support source map URL as a data url const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1); sourceMapData = atob(rawData); sourceMappingURL = source; } else { // TODO Support source map URLs relative to the source URL // sourceMappingURL = supportRelativeURL(source, sourceMappingURL); sourceMapData = getGeneratedContents(sourceMappingURL); } const rawSourceMap = typeof sourceMapData === "string" ? (JSON.parse(sourceMapData) as RawSourceMap) : sourceMapData; consumer = new SourceMapConsumer(rawSourceMap); consumers.set(source, consumer); } return consumer; } // tslint:disable-next-line:max-line-length const sourceMapUrlRe = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^\*]+?)[ \t]*(?:\*\/)[ \t]*$)/gm; function retrieveSourceMapURL(fileData: string): string | null { // Keep executing the search to find the *last* sourceMappingURL to avoid // picking up sourceMappingURLs from comments, strings, etc. let lastMatch, match; while ((match = sourceMapUrlRe.exec(fileData))) { lastMatch = match; } if (!lastMatch) { return null; } return lastMatch[1]; } export function mapSourcePosition(position: Position): MappedPosition { const consumer = loadConsumer(position.source); if (consumer == null) { return position; } return consumer.originalPositionFor(position); } const stackEvalRe = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/; const nestedEvalRe = /^eval at ([^(]+) \((.+)\)$/; // 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 = stackEvalRe.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 = nestedEvalRe.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; }