diff options
author | Ryan Dahl <ry@tinyclouds.org> | 2018-12-06 23:05:36 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-06 23:05:36 -0500 |
commit | c113df1bb8a0c7d0c560ad32c0291c918c7da7b4 (patch) | |
tree | 0d15de448be602c22aecb2ec65ac7667c437a209 /js | |
parent | 568ac0c9026b6f4012e2511a026bb5eb31a06020 (diff) |
Process source maps in Rust instead of JS (#1280)
- Improves speed and binary size significantly.
- Makes deno_last_exception() output a JSON structure.
- Isolate::execute and Isolate::event_loop now return
structured, mapped JSError objects on errors.
- Removes libdeno functions:
libdeno.setGlobalErrorHandler()
libdeno.setPromiseRejectHandler()
libdeno.setPromiseErrorExaminer()
In collaboration with Ryan Dahl.
Diffstat (limited to 'js')
-rw-r--r-- | js/compiler.ts | 48 | ||||
-rw-r--r-- | js/libdeno.ts | 4 | ||||
-rw-r--r-- | js/main.ts | 33 | ||||
-rw-r--r-- | js/promise_util.ts | 50 | ||||
-rw-r--r-- | js/unit_tests.ts | 1 | ||||
-rw-r--r-- | js/v8_source_maps.ts | 285 | ||||
-rw-r--r-- | js/v8_source_maps_test.ts | 17 |
7 files changed, 9 insertions, 429 deletions
diff --git a/js/compiler.ts b/js/compiler.ts index c9fa4611c..e0bfb77bb 100644 --- a/js/compiler.ts +++ b/js/compiler.ts @@ -3,10 +3,8 @@ import * as ts from "typescript"; import { MediaType } from "gen/msg_generated"; import { assetSourceCode } from "./assets"; -import { libdeno } from "./libdeno"; import * as os from "./os"; import { CodeProvider } from "./runner"; -import { RawSourceMap } from "./types"; import { assert, log, notImplemented } from "./util"; const EOL = "\n"; @@ -71,7 +69,7 @@ export class ModuleMetaData implements ts.IScriptSnapshot { public readonly mediaType: MediaType, public readonly sourceCode: SourceCode = "", public outputCode: OutputCode = "", - public sourceMap: SourceMap | RawSourceMap = "" + public sourceMap: SourceMap = "" ) { if (outputCode !== "" || fileName.endsWith(".d.ts")) { this.scriptVersion = "1"; @@ -131,8 +129,6 @@ export class Compiler ContainingFile, Map<ModuleSpecifier, ModuleFileName> >(); - // 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 @@ -369,19 +365,15 @@ export class Compiler moduleMetaData.outputCode = `${ outputFile.text }\n//# sourceURL=${fileName}`; - moduleMetaData.sourceMap = JSON.parse(sourceMapFile.text); + moduleMetaData.sourceMap = sourceMapFile.text; } moduleMetaData.scriptVersion = "1"; - const sourceMap = - moduleMetaData.sourceMap === "string" - ? moduleMetaData.sourceMap - : JSON.stringify(moduleMetaData.sourceMap); this._os.codeCache( fileName, sourceCode, moduleMetaData.outputCode, - sourceMap + moduleMetaData.sourceMap ); return moduleMetaData.outputCode; } @@ -398,36 +390,10 @@ export class Compiler } /** 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}"`); - } - } - }; + getGeneratedSourceMap(fileName: string): string { + const moduleMetaData = this._moduleMetaDataMap.get(fileName); + return moduleMetaData ? moduleMetaData.sourceMap : ""; + } /** Get the output code for a module based on its filename. A call to * `.getFilename()` should occur before attempting to get the output code as diff --git a/js/libdeno.ts b/js/libdeno.ts index c61027324..5448165a1 100644 --- a/js/libdeno.ts +++ b/js/libdeno.ts @@ -1,5 +1,4 @@ // Copyright 2018 the Deno authors. All rights reserved. MIT license. -import { RawSourceMap } from "./types"; import { globalEval } from "./global_eval"; // The libdeno functions are moved so that users can't access them. @@ -39,9 +38,6 @@ interface Libdeno { ) => void; setPromiseErrorExaminer: (handler: () => boolean) => void; - - mainSource: string; - mainSourceMap: RawSourceMap; } const window = globalEval("this"); diff --git a/js/main.ts b/js/main.ts index 241e8d7d1..818713593 100644 --- a/js/main.ts +++ b/js/main.ts @@ -11,22 +11,9 @@ import { Runner } from "./runner"; import { libdeno } from "./libdeno"; 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"; -// Install the source maps handler and do some pre-calculations so all of it is -// available in the snapshot -const compiler = Compiler.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(); msg.Start.startStart(builder); @@ -39,27 +26,9 @@ function sendStart(): msg.StartRes { return startRes; } -function onGlobalError( - message: string, - source: string, - lineno: number, - colno: number, - error: any // tslint:disable-line:no-any -) { - if (error instanceof Error) { - console.log(error.stack); - } else { - console.log(`Thrown: ${String(error)}`); - } - os.exit(1); -} - /* tslint:disable-next-line:no-default-export */ export default function denoMain() { libdeno.recv(handleAsyncMsgFromRust); - libdeno.setGlobalErrorHandler(onGlobalError); - libdeno.setPromiseRejectHandler(promiseRejectHandler); - libdeno.setPromiseErrorExaminer(promiseErrorExaminer); // First we send an empty "Start" message to let the privileged side know we // are ready. The response should be a "StartRes" message containing the CLI @@ -68,6 +37,8 @@ export default function denoMain() { setLogDebug(startResMsg.debugFlag()); + const compiler = Compiler.instance(); + // handle `--types` if (startResMsg.typesFlag()) { const defaultLibFileName = compiler.getDefaultLibFileName(); diff --git a/js/promise_util.ts b/js/promise_util.ts deleted file mode 100644 index 3c789124d..000000000 --- a/js/promise_util.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2018 the Deno authors. All rights reserved. MIT license. -import { PromiseRejectEvent } from "./libdeno"; - -/* tslint:disable-next-line:no-any */ -const rejectMap = new Map<Promise<any>, string>(); -// For uncaught promise rejection errors - -/* tslint:disable-next-line:no-any */ -const otherErrorMap = new Map<Promise<any>, string>(); -// For reject after resolve / resolve after resolve errors - -export function promiseRejectHandler( - error: Error | string, - event: PromiseRejectEvent, - /* tslint:disable-next-line:no-any */ - promise: Promise<any> -) { - switch (event) { - case "RejectWithNoHandler": - rejectMap.set(promise, (error as Error).stack || "RejectWithNoHandler"); - break; - case "HandlerAddedAfterReject": - rejectMap.delete(promise); - break; - case "ResolveAfterResolved": - // Should not warn. See #1272 - break; - default: - // error is string here - otherErrorMap.set(promise, `Promise warning: ${error as string}`); - } -} - -// Return true when continue, false to die on uncaught promise reject -export function promiseErrorExaminer(): boolean { - if (otherErrorMap.size > 0) { - for (const msg of otherErrorMap.values()) { - console.log(msg); - } - otherErrorMap.clear(); - } - if (rejectMap.size > 0) { - for (const msg of rejectMap.values()) { - console.log(msg); - } - rejectMap.clear(); - return false; - } - return true; -} diff --git a/js/unit_tests.ts b/js/unit_tests.ts index 4863b7ac4..55877091e 100644 --- a/js/unit_tests.ts +++ b/js/unit_tests.ts @@ -35,5 +35,4 @@ import "./text_encoding_test.ts"; import "./timers_test.ts"; import "./truncate_test.ts"; import "./url_search_params_test.ts"; -import "./v8_source_maps_test.ts"; import "./write_file_test.ts"; diff --git a/js/v8_source_maps.ts b/js/v8_source_maps.ts deleted file mode 100644 index dcdde16c7..000000000 --- a/js/v8_source_maps.ts +++ /dev/null @@ -1,285 +0,0 @@ -// 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<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; -} - -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<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; -} - -// 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 += "<anonymous>"; - } - 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 || "<anonymous>"}`; - } - } else if (isConstructor) { - line += `new ${functionName || "<anonymous>"}`; - } 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; -} diff --git a/js/v8_source_maps_test.ts b/js/v8_source_maps_test.ts deleted file mode 100644 index 90c123431..000000000 --- a/js/v8_source_maps_test.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2018 the Deno authors. All rights reserved. MIT license. -import { test, assert, assertEqual } from "./test_util.ts"; - -// This test demonstrates a bug: -// https://github.com/denoland/deno/issues/808 -test(function evalErrorFormatted() { - let err; - try { - eval("boom"); - } catch (e) { - err = e; - } - assert(!!err); - // tslint:disable-next-line:no-unused-expression - err.stack; // This would crash if err.stack is malformed - assertEqual(err.name, "ReferenceError"); -}); |