diff options
author | Kitson Kelly <me@kitsonkelly.com> | 2018-11-06 15:08:25 +1100 |
---|---|---|
committer | Ryan Dahl <ry@tinyclouds.org> | 2018-11-06 06:37:16 -0800 |
commit | f477b45a0a398e379ecafd2525c460f2793a43c2 (patch) | |
tree | 5a1782f818ad11fe0aa09fcc0d904b3a8ef7549f | |
parent | 7a17e2aec6307d37b7fe3bd9c7af0beb15ec924b (diff) |
Improve preparing stack traces
-rw-r--r-- | js/compiler.ts | 100 | ||||
-rw-r--r-- | js/compiler_test.ts | 4 | ||||
-rw-r--r-- | js/main.ts | 12 | ||||
-rw-r--r-- | js/v8_source_maps.ts | 78 |
4 files changed, 97 insertions, 97 deletions
diff --git a/js/compiler.ts b/js/compiler.ts index a6b6e6970..046233442 100644 --- a/js/compiler.ts +++ b/js/compiler.ts @@ -10,7 +10,6 @@ import { window } from "./globals"; import * as os from "./os"; import { RawSourceMap } from "./types"; import { assert, log, notImplemented } from "./util"; -import * as sourceMaps from "./v8_source_maps"; const EOL = "\n"; const ASSETS = "$asset$"; @@ -91,7 +90,7 @@ export class ModuleMetaData implements ts.IScriptSnapshot { public readonly mediaType: MediaType, public readonly sourceCode: SourceCode = "", public outputCode: OutputCode = "", - public sourceMap: SourceMap = "" + public sourceMap: SourceMap | RawSourceMap = "" ) { if (outputCode !== "" || fileName.endsWith(".d.ts")) { this.scriptVersion = "1"; @@ -153,6 +152,8 @@ export class DenoCompiler >(); // A reference to global eval, so it can be monkey patched during testing 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 private _log = log; // A map of module file names to module meta data @@ -376,63 +377,11 @@ export class DenoCompiler 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() { if (DenoCompiler._instance) { throw new TypeError("Attempt to create an additional compiler."); } this._service = this._ts.createLanguageService(this); - this._setupSourceMaps(); } // Deno specific compiler API @@ -503,19 +452,55 @@ export class DenoCompiler moduleMetaData.outputCode = `${ outputFile.text }\n//# sourceURL=${fileName}`; - moduleMetaData.sourceMap = sourceMapFile.text; + moduleMetaData.sourceMap = JSON.parse(sourceMapFile.text); } moduleMetaData.scriptVersion = "1"; + const sourceMap = + moduleMetaData.sourceMap === "string" + ? moduleMetaData.sourceMap + : JSON.stringify(moduleMetaData.sourceMap); this._os.codeCache( fileName, sourceCode, moduleMetaData.outputCode, - moduleMetaData.sourceMap + sourceMap ); 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 * absolute identifiers for dependent modules that are required by this * module. @@ -586,7 +571,8 @@ export class DenoCompiler mediaType = fetchResponse.mediaType; sourceCode = fetchResponse.sourceCode; outputCode = fetchResponse.outputCode; - sourceMap = fetchResponse.sourceMap; + sourceMap = + fetchResponse.sourceMap && JSON.parse(fetchResponse.sourceMap); } assert(moduleId != null, "No module ID."); assert(fileName != null, "No file name."); diff --git a/js/compiler_test.ts b/js/compiler_test.ts index 729d6b4a7..d2a3a2cf2 100644 --- a/js/compiler_test.ts +++ b/js/compiler_test.ts @@ -438,7 +438,7 @@ test(function compilerRun() { assert(moduleMetaData.hasRun); assertEqual(moduleMetaData.sourceCode, fooBarTsSource); assertEqual(moduleMetaData.outputCode, fooBarTsOutput); - assertEqual(moduleMetaData.sourceMap, fooBarTsSourcemap); + assertEqual(JSON.stringify(moduleMetaData.sourceMap), fooBarTsSourcemap); assertEqual(moduleMetaData.exports, { foo: "bar" }); assertEqual( @@ -550,7 +550,7 @@ test(function compilerResolveModule() { ); assertEqual(moduleMetaData.sourceCode, fooBazTsSource); assertEqual(moduleMetaData.outputCode, fooBazTsOutput); - assertEqual(moduleMetaData.sourceMap, fooBazTsSourcemap); + assertEqual(JSON.stringify(moduleMetaData.sourceMap), fooBazTsSourcemap); assert(!moduleMetaData.hasRun); assert(!moduleMetaData.deps); assertEqual(moduleMetaData.exports, {}); diff --git a/js/main.ts b/js/main.ts index 02e5dcb4d..176f098c7 100644 --- a/js/main.ts +++ b/js/main.ts @@ -9,11 +9,19 @@ import { args } from "./deno"; import { sendSync, handleAsyncMsgFromRust } from "./dispatch"; import { promiseErrorExaminer, promiseRejectHandler } from "./promise_util"; import { replLoop } from "./repl"; +import * as sourceMaps from "./v8_source_maps"; import { version } from "typescript"; -// Instantiate compiler at the top-level so it decodes source maps for the main -// bundle during snapshot. +// Install the source maps handler and do some pre-calculations so all of it is +// available in the snapshot 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 { const builder = flatbuffers.createBuilder(); diff --git a/js/v8_source_maps.ts b/js/v8_source_maps.ts index cd61955d2..dcdde16c7 100644 --- a/js/v8_source_maps.ts +++ b/js/v8_source_maps.ts @@ -3,9 +3,8 @@ // Originated from source-map-support but has been heavily modified for deno. import { SourceMapConsumer, MappedPosition } from "source-map"; -import * as base64 from "base64-js"; -import { arrayToStr } from "./util"; import { CallSite, RawSourceMap } from "./types"; +import { atob } from "./text_encoding"; const consumers = new Map<string, SourceMapConsumer>(); @@ -53,9 +52,9 @@ export function prepareStackTraceWrapper( // @internal export function prepareStackTrace(error: Error, stack: CallSite[]): string { 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 @@ -74,11 +73,13 @@ export function wrapCallSite(frame: CallSite): CallSite { const column = (frame.getColumnNumber() || 1) - 1; 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); + Object.assign(frame, { + getFileName: () => position.source, + getLineNumber: () => position.line, + getColumnNumber: () => Number(position.column) + 1, + getScriptNameOrSourceURL: () => position.source, + toString: () => CallSiteToString(frame) + }); return frame; } @@ -87,8 +88,10 @@ export function wrapCallSite(frame: CallSite): CallSite { if (origin) { origin = mapEvalOrigin(origin); frame = cloneCallSite(frame); - frame.getEvalOrigin = () => origin; - frame.toString = () => CallSiteToString(frame); + Object.assign(frame, { + getEvalOrigin: () => origin, + toString: () => CallSiteToString(frame) + }); return frame; } @@ -96,29 +99,30 @@ export function wrapCallSite(frame: CallSite): CallSite { 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 => { +function cloneCallSite( + frame: CallSite + // mixin: Partial<CallSite> & { toString: () => string } +): CallSite { + const obj = {} as CallSite; + const props = Object.getOwnPropertyNames( + Object.getPrototypeOf(frame) + ) as Array<keyof CallSite>; + for (const name of props) { obj[name] = /^(?:is|get)/.test(name) - ? () => frame_[name].call(frame) - : frame_[name]; - }); - return (obj as any) as CallSite; - // tslint:enable:no-any + ? () => 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 fileName; let fileLocation = ""; if (frame.isNative()) { fileLocation = "native"; } else { - fileName = frame.getScriptNameOrSourceURL(); + const fileName = frame.getScriptNameOrSourceURL(); if (!fileName && frame.isEval()) { fileLocation = frame.getEvalOrigin() || ""; fileLocation += ", "; // Expecting source position to follow. @@ -134,10 +138,10 @@ function CallSiteToString(frame: CallSite): string { } const lineNumber = frame.getLineNumber(); if (lineNumber != null) { - fileLocation += ":" + String(lineNumber); + fileLocation += `:${lineNumber}`; const columnNumber = frame.getColumnNumber(); if (columnNumber) { - fileLocation += ":" + String(columnNumber); + fileLocation += `:${columnNumber}`; } } } @@ -156,7 +160,7 @@ function CallSiteToString(frame: CallSite): string { const methodName = frame.getMethodName(); if (functionName) { if (typeName && functionName.indexOf(typeName) !== 0) { - line += typeName + "."; + line += `${typeName}.`; } line += functionName; if ( @@ -206,8 +210,7 @@ export function loadConsumer(source: string): SourceMapConsumer | null { 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); + sourceMapData = atob(rawData); sourceMappingURL = source; } else { // TODO Support source map URLs relative to the source URL @@ -217,7 +220,7 @@ export function loadConsumer(source: string): SourceMapConsumer | null { const rawSourceMap = typeof sourceMapData === "string" - ? JSON.parse(sourceMapData) + ? (JSON.parse(sourceMapData) as RawSourceMap) : sourceMapData; consumer = new SourceMapConsumer(rawSourceMap); consumers.set(source, consumer); @@ -225,14 +228,14 @@ export function loadConsumer(source: string): SourceMapConsumer | null { 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 { - // 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))) { + while ((match = sourceMapUrlRe.exec(fileData))) { lastMatch = match; } if (!lastMatch) { @@ -249,11 +252,14 @@ export function mapSourcePosition(position: Position): MappedPosition { 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 = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(origin); + let match = stackEvalRe.exec(origin); if (match) { const position = mapSourcePosition({ source: match[2], @@ -269,7 +275,7 @@ function mapEvalOrigin(origin: string): string { } // Parse nested eval() calls using recursion - match = /^eval at ([^(]+) \((.+)\)$/.exec(origin); + match = nestedEvalRe.exec(origin); if (match) { return `eval at ${match[1]} (${mapEvalOrigin(match[2])})`; } |