0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 17:34:47 -05:00

Improve preparing stack traces

This commit is contained in:
Kitson Kelly 2018-11-06 15:08:25 +11:00 committed by Ryan Dahl
parent 7a17e2aec6
commit f477b45a0a
4 changed files with 97 additions and 97 deletions

View file

@ -10,7 +10,6 @@ import { window } from "./globals";
import * as os from "./os"; import * as os from "./os";
import { RawSourceMap } from "./types"; import { RawSourceMap } from "./types";
import { assert, log, notImplemented } from "./util"; import { assert, log, notImplemented } from "./util";
import * as sourceMaps from "./v8_source_maps";
const EOL = "\n"; const EOL = "\n";
const ASSETS = "$asset$"; const ASSETS = "$asset$";
@ -91,7 +90,7 @@ export class ModuleMetaData implements ts.IScriptSnapshot {
public readonly mediaType: MediaType, public readonly mediaType: MediaType,
public readonly sourceCode: SourceCode = "", public readonly sourceCode: SourceCode = "",
public outputCode: OutputCode = "", public outputCode: OutputCode = "",
public sourceMap: SourceMap = "" public sourceMap: SourceMap | RawSourceMap = ""
) { ) {
if (outputCode !== "" || fileName.endsWith(".d.ts")) { if (outputCode !== "" || fileName.endsWith(".d.ts")) {
this.scriptVersion = "1"; this.scriptVersion = "1";
@ -153,6 +152,8 @@ export class DenoCompiler
>(); >();
// A reference to global eval, so it can be monkey patched during testing // A reference to global eval, so it can be monkey patched during testing
private _globalEval = globalEval; private _globalEval = globalEval;
// Keep track of state of the last module requested via `getGeneratedContents`
private _lastModule: ModuleMetaData | undefined;
// A reference to the log utility, so it can be monkey patched during testing // A reference to the log utility, so it can be monkey patched during testing
private _log = log; private _log = log;
// A map of module file names to module meta data // A map of module file names to module meta data
@ -376,63 +377,11 @@ export class DenoCompiler
innerMap.set(moduleSpecifier, fileName); innerMap.set(moduleSpecifier, fileName);
} }
/** Setup being able to map back source references back to their source
*
* TODO is this the best place for this? It is tightly coupled to how the
* compiler works, but it is also tightly coupled to how the whole runtime
* environment is bootstrapped. It also needs efficient access to the
* `outputCode` of the module information, which exists inside of the
* compiler instance.
*/
private _setupSourceMaps(): void {
let lastModule: ModuleMetaData | undefined;
sourceMaps.install({
installPrepareStackTrace: true,
getGeneratedContents: (fileName: string): string | RawSourceMap => {
this._log("compiler.getGeneratedContents", fileName);
if (fileName === "gen/bundle/main.js") {
assert(libdeno.mainSource.length > 0);
return libdeno.mainSource;
} else if (fileName === "main.js.map") {
return libdeno.mainSourceMap;
} else if (fileName === "deno_main.js") {
return "";
} else if (!fileName.endsWith(".map")) {
const moduleMetaData = this._moduleMetaDataMap.get(fileName);
if (!moduleMetaData) {
lastModule = undefined;
return "";
}
lastModule = moduleMetaData;
return moduleMetaData.outputCode;
} else {
if (lastModule && lastModule.sourceMap) {
// Assuming the the map will always be asked for after the source
// code.
const { sourceMap } = lastModule;
lastModule = undefined;
return sourceMap;
} else {
// Errors thrown here are caught by source-map.
throw new Error(`Unable to find source map: "${fileName}"`);
}
}
}
});
// Pre-compute source maps for main.js.map. This will happen at compile-time
// as long as Compiler is instanciated before the snapshot is created..
const consumer = sourceMaps.loadConsumer("gen/bundle/main.js");
assert(consumer != null);
consumer!.computeColumnSpans();
}
private constructor() { private constructor() {
if (DenoCompiler._instance) { if (DenoCompiler._instance) {
throw new TypeError("Attempt to create an additional compiler."); throw new TypeError("Attempt to create an additional compiler.");
} }
this._service = this._ts.createLanguageService(this); this._service = this._ts.createLanguageService(this);
this._setupSourceMaps();
} }
// Deno specific compiler API // Deno specific compiler API
@ -503,19 +452,55 @@ export class DenoCompiler
moduleMetaData.outputCode = `${ moduleMetaData.outputCode = `${
outputFile.text outputFile.text
}\n//# sourceURL=${fileName}`; }\n//# sourceURL=${fileName}`;
moduleMetaData.sourceMap = sourceMapFile.text; moduleMetaData.sourceMap = JSON.parse(sourceMapFile.text);
} }
moduleMetaData.scriptVersion = "1"; moduleMetaData.scriptVersion = "1";
const sourceMap =
moduleMetaData.sourceMap === "string"
? moduleMetaData.sourceMap
: JSON.stringify(moduleMetaData.sourceMap);
this._os.codeCache( this._os.codeCache(
fileName, fileName,
sourceCode, sourceCode,
moduleMetaData.outputCode, moduleMetaData.outputCode,
moduleMetaData.sourceMap sourceMap
); );
return moduleMetaData.outputCode; return moduleMetaData.outputCode;
} }
/** Given a fileName, return what was generated by the compiler. */
getGeneratedContents = (fileName: string): string | RawSourceMap => {
this._log("compiler.getGeneratedContents", fileName);
if (fileName === "gen/bundle/main.js") {
assert(libdeno.mainSource.length > 0);
return libdeno.mainSource;
} else if (fileName === "main.js.map") {
return libdeno.mainSourceMap;
} else if (fileName === "deno_main.js") {
return "";
} else if (!fileName.endsWith(".map")) {
const moduleMetaData = this._moduleMetaDataMap.get(fileName);
if (!moduleMetaData) {
this._lastModule = undefined;
return "";
}
this._lastModule = moduleMetaData;
return moduleMetaData.outputCode;
} else {
if (this._lastModule && this._lastModule.sourceMap) {
// Assuming the the map will always be asked for after the source
// code.
const { sourceMap } = this._lastModule;
this._lastModule = undefined;
return sourceMap;
} else {
// Errors thrown here are caught by source-map.
throw new Error(`Unable to find source map: "${fileName}"`);
}
}
};
/** For a given module specifier and containing file, return a list of /** For a given module specifier and containing file, return a list of
* absolute identifiers for dependent modules that are required by this * absolute identifiers for dependent modules that are required by this
* module. * module.
@ -586,7 +571,8 @@ export class DenoCompiler
mediaType = fetchResponse.mediaType; mediaType = fetchResponse.mediaType;
sourceCode = fetchResponse.sourceCode; sourceCode = fetchResponse.sourceCode;
outputCode = fetchResponse.outputCode; outputCode = fetchResponse.outputCode;
sourceMap = fetchResponse.sourceMap; sourceMap =
fetchResponse.sourceMap && JSON.parse(fetchResponse.sourceMap);
} }
assert(moduleId != null, "No module ID."); assert(moduleId != null, "No module ID.");
assert(fileName != null, "No file name."); assert(fileName != null, "No file name.");

View file

@ -438,7 +438,7 @@ test(function compilerRun() {
assert(moduleMetaData.hasRun); assert(moduleMetaData.hasRun);
assertEqual(moduleMetaData.sourceCode, fooBarTsSource); assertEqual(moduleMetaData.sourceCode, fooBarTsSource);
assertEqual(moduleMetaData.outputCode, fooBarTsOutput); assertEqual(moduleMetaData.outputCode, fooBarTsOutput);
assertEqual(moduleMetaData.sourceMap, fooBarTsSourcemap); assertEqual(JSON.stringify(moduleMetaData.sourceMap), fooBarTsSourcemap);
assertEqual(moduleMetaData.exports, { foo: "bar" }); assertEqual(moduleMetaData.exports, { foo: "bar" });
assertEqual( assertEqual(
@ -550,7 +550,7 @@ test(function compilerResolveModule() {
); );
assertEqual(moduleMetaData.sourceCode, fooBazTsSource); assertEqual(moduleMetaData.sourceCode, fooBazTsSource);
assertEqual(moduleMetaData.outputCode, fooBazTsOutput); assertEqual(moduleMetaData.outputCode, fooBazTsOutput);
assertEqual(moduleMetaData.sourceMap, fooBazTsSourcemap); assertEqual(JSON.stringify(moduleMetaData.sourceMap), fooBazTsSourcemap);
assert(!moduleMetaData.hasRun); assert(!moduleMetaData.hasRun);
assert(!moduleMetaData.deps); assert(!moduleMetaData.deps);
assertEqual(moduleMetaData.exports, {}); assertEqual(moduleMetaData.exports, {});

View file

@ -9,11 +9,19 @@ import { args } from "./deno";
import { sendSync, handleAsyncMsgFromRust } from "./dispatch"; import { sendSync, handleAsyncMsgFromRust } from "./dispatch";
import { promiseErrorExaminer, promiseRejectHandler } from "./promise_util"; import { promiseErrorExaminer, promiseRejectHandler } from "./promise_util";
import { replLoop } from "./repl"; import { replLoop } from "./repl";
import * as sourceMaps from "./v8_source_maps";
import { version } from "typescript"; import { version } from "typescript";
// Instantiate compiler at the top-level so it decodes source maps for the main // Install the source maps handler and do some pre-calculations so all of it is
// bundle during snapshot. // available in the snapshot
const compiler = DenoCompiler.instance(); const compiler = DenoCompiler.instance();
sourceMaps.install({
installPrepareStackTrace: true,
getGeneratedContents: compiler.getGeneratedContents
});
const consumer = sourceMaps.loadConsumer("gen/bundle/main.js");
assert(consumer != null);
consumer!.computeColumnSpans();
function sendStart(): msg.StartRes { function sendStart(): msg.StartRes {
const builder = flatbuffers.createBuilder(); const builder = flatbuffers.createBuilder();

View file

@ -3,9 +3,8 @@
// Originated from source-map-support but has been heavily modified for deno. // Originated from source-map-support but has been heavily modified for deno.
import { SourceMapConsumer, MappedPosition } from "source-map"; import { SourceMapConsumer, MappedPosition } from "source-map";
import * as base64 from "base64-js";
import { arrayToStr } from "./util";
import { CallSite, RawSourceMap } from "./types"; import { CallSite, RawSourceMap } from "./types";
import { atob } from "./text_encoding";
const consumers = new Map<string, SourceMapConsumer>(); const consumers = new Map<string, SourceMapConsumer>();
@ -53,9 +52,9 @@ export function prepareStackTraceWrapper(
// @internal // @internal
export function prepareStackTrace(error: Error, stack: CallSite[]): string { export function prepareStackTrace(error: Error, stack: CallSite[]): string {
const frames = stack.map( const frames = stack.map(
(frame: CallSite) => `\n at ${wrapCallSite(frame).toString()}` frame => `\n at ${wrapCallSite(frame).toString()}`
); );
return error.toString() + frames.join(""); return `${error.toString()}${frames.join("")}`;
} }
// @internal // @internal
@ -74,11 +73,13 @@ export function wrapCallSite(frame: CallSite): CallSite {
const column = (frame.getColumnNumber() || 1) - 1; const column = (frame.getColumnNumber() || 1) - 1;
const position = mapSourcePosition({ source, line, column }); const position = mapSourcePosition({ source, line, column });
frame = cloneCallSite(frame); frame = cloneCallSite(frame);
frame.getFileName = () => position.source; Object.assign(frame, {
frame.getLineNumber = () => position.line; getFileName: () => position.source,
frame.getColumnNumber = () => Number(position.column) + 1; getLineNumber: () => position.line,
frame.getScriptNameOrSourceURL = () => position.source; getColumnNumber: () => Number(position.column) + 1,
frame.toString = () => CallSiteToString(frame); getScriptNameOrSourceURL: () => position.source,
toString: () => CallSiteToString(frame)
});
return frame; return frame;
} }
@ -87,8 +88,10 @@ export function wrapCallSite(frame: CallSite): CallSite {
if (origin) { if (origin) {
origin = mapEvalOrigin(origin); origin = mapEvalOrigin(origin);
frame = cloneCallSite(frame); frame = cloneCallSite(frame);
frame.getEvalOrigin = () => origin; Object.assign(frame, {
frame.toString = () => CallSiteToString(frame); getEvalOrigin: () => origin,
toString: () => CallSiteToString(frame)
});
return frame; return frame;
} }
@ -96,29 +99,30 @@ export function wrapCallSite(frame: CallSite): CallSite {
return frame; return frame;
} }
function cloneCallSite(frame: CallSite): CallSite { function cloneCallSite(
// tslint:disable:no-any frame: CallSite
const obj: any = {}; // mixin: Partial<CallSite> & { toString: () => string }
const frame_ = frame as any; ): CallSite {
const props = Object.getOwnPropertyNames(Object.getPrototypeOf(frame)); const obj = {} as CallSite;
props.forEach(name => { const props = Object.getOwnPropertyNames(
Object.getPrototypeOf(frame)
) as Array<keyof CallSite>;
for (const name of props) {
obj[name] = /^(?:is|get)/.test(name) obj[name] = /^(?:is|get)/.test(name)
? () => frame_[name].call(frame) ? () => frame[name].call(frame)
: frame_[name]; : frame[name];
}); }
return (obj as any) as CallSite; return obj;
// tslint:enable:no-any
} }
// Taken from source-map-support, original copied from V8's messages.js // Taken from source-map-support, original copied from V8's messages.js
// MIT License. Copyright (c) 2014 Evan Wallace // MIT License. Copyright (c) 2014 Evan Wallace
function CallSiteToString(frame: CallSite): string { function CallSiteToString(frame: CallSite): string {
let fileName;
let fileLocation = ""; let fileLocation = "";
if (frame.isNative()) { if (frame.isNative()) {
fileLocation = "native"; fileLocation = "native";
} else { } else {
fileName = frame.getScriptNameOrSourceURL(); const fileName = frame.getScriptNameOrSourceURL();
if (!fileName && frame.isEval()) { if (!fileName && frame.isEval()) {
fileLocation = frame.getEvalOrigin() || ""; fileLocation = frame.getEvalOrigin() || "";
fileLocation += ", "; // Expecting source position to follow. fileLocation += ", "; // Expecting source position to follow.
@ -134,10 +138,10 @@ function CallSiteToString(frame: CallSite): string {
} }
const lineNumber = frame.getLineNumber(); const lineNumber = frame.getLineNumber();
if (lineNumber != null) { if (lineNumber != null) {
fileLocation += ":" + String(lineNumber); fileLocation += `:${lineNumber}`;
const columnNumber = frame.getColumnNumber(); const columnNumber = frame.getColumnNumber();
if (columnNumber) { if (columnNumber) {
fileLocation += ":" + String(columnNumber); fileLocation += `:${columnNumber}`;
} }
} }
} }
@ -156,7 +160,7 @@ function CallSiteToString(frame: CallSite): string {
const methodName = frame.getMethodName(); const methodName = frame.getMethodName();
if (functionName) { if (functionName) {
if (typeName && functionName.indexOf(typeName) !== 0) { if (typeName && functionName.indexOf(typeName) !== 0) {
line += typeName + "."; line += `${typeName}.`;
} }
line += functionName; line += functionName;
if ( if (
@ -206,8 +210,7 @@ export function loadConsumer(source: string): SourceMapConsumer | null {
if (reSourceMap.test(sourceMappingURL)) { if (reSourceMap.test(sourceMappingURL)) {
// Support source map URL as a data url // Support source map URL as a data url
const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1); const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1);
const ui8 = base64.toByteArray(rawData); sourceMapData = atob(rawData);
sourceMapData = arrayToStr(ui8);
sourceMappingURL = source; sourceMappingURL = source;
} else { } else {
// TODO Support source map URLs relative to the source URL // TODO Support source map URLs relative to the source URL
@ -217,7 +220,7 @@ export function loadConsumer(source: string): SourceMapConsumer | null {
const rawSourceMap = const rawSourceMap =
typeof sourceMapData === "string" typeof sourceMapData === "string"
? JSON.parse(sourceMapData) ? (JSON.parse(sourceMapData) as RawSourceMap)
: sourceMapData; : sourceMapData;
consumer = new SourceMapConsumer(rawSourceMap); consumer = new SourceMapConsumer(rawSourceMap);
consumers.set(source, consumer); consumers.set(source, consumer);
@ -225,14 +228,14 @@ export function loadConsumer(source: string): SourceMapConsumer | null {
return 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 { function retrieveSourceMapURL(fileData: string): string | null {
// 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 // Keep executing the search to find the *last* sourceMappingURL to avoid
// picking up sourceMappingURLs from comments, strings, etc. // picking up sourceMappingURLs from comments, strings, etc.
let lastMatch, match; let lastMatch, match;
while ((match = re.exec(fileData))) { while ((match = sourceMapUrlRe.exec(fileData))) {
lastMatch = match; lastMatch = match;
} }
if (!lastMatch) { if (!lastMatch) {
@ -249,11 +252,14 @@ export function mapSourcePosition(position: Position): MappedPosition {
return consumer.originalPositionFor(position); return consumer.originalPositionFor(position);
} }
const stackEvalRe = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/;
const nestedEvalRe = /^eval at ([^(]+) \((.+)\)$/;
// Parses code generated by FormatEvalOrigin(), a function inside V8: // Parses code generated by FormatEvalOrigin(), a function inside V8:
// https://code.google.com/p/v8/source/browse/trunk/src/messages.js // https://code.google.com/p/v8/source/browse/trunk/src/messages.js
function mapEvalOrigin(origin: string): string { function mapEvalOrigin(origin: string): string {
// Most eval() calls are in this format // Most eval() calls are in this format
let match = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(origin); let match = stackEvalRe.exec(origin);
if (match) { if (match) {
const position = mapSourcePosition({ const position = mapSourcePosition({
source: match[2], source: match[2],
@ -269,7 +275,7 @@ function mapEvalOrigin(origin: string): string {
} }
// Parse nested eval() calls using recursion // Parse nested eval() calls using recursion
match = /^eval at ([^(]+) \((.+)\)$/.exec(origin); match = nestedEvalRe.exec(origin);
if (match) { if (match) {
return `eval at ${match[1]} (${mapEvalOrigin(match[2])})`; return `eval at ${match[1]} (${mapEvalOrigin(match[2])})`;
} }