// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. // Some of the code here is adapted directly from V8 and licensed under a BSD // style license available here: https://github.com/v8/v8/blob/24886f2d1c565287d33d71e4109a53bf0b54b75c/LICENSE.v8 import * as msg from "gen/cli/msg_generated"; import * as flatbuffers from "./flatbuffers"; import * as dispatch from "./dispatch"; import { assert } from "./util"; export interface Location { /** The full url for the module, e.g. `file://some/file.ts` or * `https://some/file.ts`. */ filename: string; /** The line number in the file. It is assumed to be 1-indexed. */ line: number; /** The column number in the file. It is assumed to be 1-indexed. */ column: number; } function req( filename: string, line: number, column: number ): [flatbuffers.Builder, msg.Any.ApplySourceMap, flatbuffers.Offset] { const builder = flatbuffers.createBuilder(); const filename_ = builder.createString(filename); const inner = msg.ApplySourceMap.createApplySourceMap( builder, filename_, // On this side, line/column are 1 based, but in the source maps, they are // 0 based, so we have to convert back and forth line - 1, column - 1 ); return [builder, msg.Any.ApplySourceMap, inner]; } function res(baseRes: msg.Base | null): Location { assert(baseRes != null); assert(baseRes!.innerType() === msg.Any.ApplySourceMap); const res = new msg.ApplySourceMap(); assert(baseRes!.inner(res) != null); const filename = res.filename()!; assert(filename != null); return { filename, // On this side, line/column are 1 based, but in the source maps, they are // 0 based, so we have to convert back and forth line: res.line() + 1, column: res.column() + 1 }; } /** Given a current location in a module, lookup the source location and * return it. * * When Deno transpiles code, it keep source maps of the transpiled code. This * function can be used to lookup the original location. This is automatically * done when accessing the `.stack` of an error, or when an uncaught error is * logged. This function can be used to perform the lookup for creating better * error handling. * * **Note:** `line` and `column` are 1 indexed, which matches display * expectations, but is not typical of most index numbers in Deno. * * An example: * * const orig = Deno.applySourceMap({ * location: "file://my/module.ts", * line: 5, * column: 15 * }); * console.log(`${orig.filename}:${orig.line}:${orig.column}`); * */ export function applySourceMap(location: Location): Location { const { filename, line, column } = location; return res(dispatch.sendSync(...req(filename, line, column))); } /** Mutate the call site so that it returns the location, instead of its * original location. */ function patchCallSite(callSite: CallSite, location: Location): CallSite { return { getThis(): unknown { return callSite.getThis(); }, getTypeName(): string { return callSite.getTypeName(); }, getFunction(): Function { return callSite.getFunction(); }, getFunctionName(): string { return callSite.getFunctionName(); }, getMethodName(): string { return callSite.getMethodName(); }, getFileName(): string { return location.filename; }, getLineNumber(): number { return location.line; }, getColumnNumber(): number { return location.column; }, getEvalOrigin(): string | null { return callSite.getEvalOrigin(); }, isToplevel(): boolean { return callSite.isToplevel(); }, isEval(): boolean { return callSite.isEval(); }, isNative(): boolean { return callSite.isNative(); }, isConstructor(): boolean { return callSite.isConstructor(); }, isAsync(): boolean { return callSite.isAsync(); }, isPromiseAll(): boolean { return callSite.isPromiseAll(); }, getPromiseIndex(): number | null { return callSite.getPromiseIndex(); } }; } /** Return a string representations of a CallSite's method call name * * This is adapted directly from V8. */ function getMethodCall(callSite: CallSite): string { let result = ""; const typeName = callSite.getTypeName(); const methodName = callSite.getMethodName(); const functionName = callSite.getFunctionName(); if (functionName) { if (typeName) { const startsWithTypeName = functionName.startsWith(typeName); if (!startsWithTypeName) { result += `${typeName}.`; } } result += functionName; if (methodName) { if (!functionName.endsWith(methodName)) { result += ` [as ${methodName}]`; } } } else { if (typeName) { result += `${typeName}.`; } if (methodName) { result += methodName; } else { result += ""; } } return result; } /** Return a string representations of a CallSite's file location * * This is adapted directly from V8. */ function getFileLocation(callSite: CallSite): string { if (callSite.isNative()) { return "native"; } let result = ""; const fileName = callSite.getFileName(); if (!fileName && callSite.isEval()) { const evalOrigin = callSite.getEvalOrigin(); assert(evalOrigin != null); result += `${evalOrigin}, `; } if (fileName) { result += fileName; } else { result += ""; } const lineNumber = callSite.getLineNumber(); if (lineNumber != null) { result += `:${lineNumber}`; const columnNumber = callSite.getColumnNumber(); if (columnNumber != null) { result += `:${columnNumber}`; } } return result; } /** Convert a CallSite to a string. * * This is adapted directly from V8. */ function callSiteToString(callSite: CallSite): string { let result = ""; const functionName = callSite.getFunctionName(); const isTopLevel = callSite.isToplevel(); const isAsync = callSite.isAsync(); const isPromiseAll = callSite.isPromiseAll(); const isConstructor = callSite.isConstructor(); const isMethodCall = !(isTopLevel || isConstructor); if (isAsync) { result += "async "; } if (isPromiseAll) { result += `Promise.all (index ${callSite.getPromiseIndex})`; return result; } if (isMethodCall) { result += getMethodCall(callSite); } else if (isConstructor) { result += "new "; if (functionName) { result += functionName; } else { result += ""; } } else if (functionName) { result += functionName; } else { result += getFileLocation(callSite); return result; } result += ` (${getFileLocation(callSite)})`; return result; } /** A replacement for the default stack trace preparer which will op into Rust * to apply source maps to individual sites */ function prepareStackTrace( error: Error, structuredStackTrace: CallSite[] ): string { return ( `${error.name}: ${error.message}\n` + structuredStackTrace .map( (callSite): CallSite => { const filename = callSite.getFileName(); const line = callSite.getLineNumber(); const column = callSite.getColumnNumber(); if (filename && line != null && column != null) { return patchCallSite( callSite, applySourceMap({ filename, line, column }) ); } return callSite; } ) .map((callSite): string => ` at ${callSiteToString(callSite)}`) .join("\n") ); } /** Sets the `prepareStackTrace` method on the Error constructor which will * op into Rust to remap source code for caught errors where the `.stack` is * being accessed. * * See: https://v8.dev/docs/stack-trace-api */ // @internal export function setPrepareStackTrace(ErrorConstructor: typeof Error): void { ErrorConstructor.prepareStackTrace = prepareStackTrace; }