diff options
Diffstat (limited to 'js')
-rw-r--r-- | js/console.ts | 124 | ||||
-rw-r--r-- | js/deno.ts | 6 | ||||
-rw-r--r-- | js/dispatch.ts | 73 | ||||
-rw-r--r-- | js/fetch.ts | 146 | ||||
-rw-r--r-- | js/globals.ts | 32 | ||||
-rw-r--r-- | js/main.ts | 4 | ||||
-rw-r--r-- | js/os.ts | 65 | ||||
-rw-r--r-- | js/runtime.ts | 339 | ||||
-rw-r--r-- | js/tests.ts | 126 | ||||
-rw-r--r-- | js/text-encoding.d.ts | 6 | ||||
-rw-r--r-- | js/timers.ts | 89 | ||||
-rw-r--r-- | js/types.ts | 10 | ||||
-rw-r--r-- | js/util.ts | 53 | ||||
-rw-r--r-- | js/v8_source_maps.ts | 273 |
14 files changed, 1344 insertions, 2 deletions
diff --git a/js/console.ts b/js/console.ts new file mode 100644 index 000000000..af92c8871 --- /dev/null +++ b/js/console.ts @@ -0,0 +1,124 @@ +const print = V8Worker2.print; + +// tslint:disable-next-line:no-any +type ConsoleContext = Set<any>; + +// tslint:disable-next-line:no-any +function getClassInstanceName(instance: any): string { + if (typeof instance !== "object") { + return ""; + } + if (instance && instance.__proto__ && instance.__proto__.constructor) { + return instance.__proto__.constructor.name; // could be "Object" or "Array" + } + return ""; +} + +// tslint:disable-next-line:no-any +function stringify(ctx: ConsoleContext, value: any): string { + switch (typeof value) { + case "string": + return value; + case "number": + case "boolean": + case "undefined": + case "symbol": + return String(value); + case "function": + if (value.name && value.name !== "anonymous") { + // from MDN spec + return `[Function: ${value.name}]`; + } + return "[Function]"; + case "object": + if (value === null) { + return "null"; + } + + if (ctx.has(value)) { + return "[Circular]"; + } + + ctx.add(value); + const entries: string[] = []; + + if (Array.isArray(value)) { + for (const el of value) { + entries.push(stringify(ctx, el)); + } + + ctx.delete(value); + + if (entries.length === 0) { + return "[]"; + } + return `[ ${entries.join(", ")} ]`; + } else { + let baseString = ""; + + const className = getClassInstanceName(value); + let shouldShowClassName = false; + if (className && className !== "Object" && className !== "anonymous") { + shouldShowClassName = true; + } + + for (const key of Object.keys(value)) { + entries.push(`${key}: ${stringify(ctx, value[key])}`); + } + + ctx.delete(value); + + if (entries.length === 0) { + baseString = "{}"; + } else { + baseString = `{ ${entries.join(", ")} }`; + } + + if (shouldShowClassName) { + baseString = `${className} ${baseString}`; + } + + return baseString; + } + default: + return "[Not Implemented]"; + } +} + +// tslint:disable-next-line:no-any +function stringifyArgs(args: any[]): string { + const out: string[] = []; + for (const a of args) { + if (typeof a === "string") { + out.push(a); + } else { + // tslint:disable-next-line:no-any + out.push(stringify(new Set<any>(), a)); + } + } + return out.join(" "); +} + +export class Console { + // tslint:disable-next-line:no-any + log(...args: any[]): void { + print(stringifyArgs(args)); + } + + debug = this.log; + info = this.log; + + // tslint:disable-next-line:no-any + warn(...args: any[]): void { + print(`ERROR: ${stringifyArgs(args)}`); + } + + error = this.warn; + + // tslint:disable-next-line:no-any + assert(condition: boolean, ...args: any[]): void { + if (!condition) { + throw new Error(`Assertion failed: ${stringifyArgs(args)}`); + } + } +} diff --git a/js/deno.ts b/js/deno.ts new file mode 100644 index 000000000..595d87709 --- /dev/null +++ b/js/deno.ts @@ -0,0 +1,6 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +// Public deno module. +// TODO get rid of deno.d.ts +export { pub, sub } from "./dispatch"; +export { readFileSync, writeFileSync } from "./os"; diff --git a/js/dispatch.ts b/js/dispatch.ts new file mode 100644 index 000000000..a83e5a0e5 --- /dev/null +++ b/js/dispatch.ts @@ -0,0 +1,73 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +import { typedArrayToArrayBuffer } from "./util"; +import { _global } from "./globals"; +import { deno as pb } from "./msg.pb"; + +export type MessageCallback = (msg: Uint8Array) => void; +//type MessageStructCallback = (msg: pb.IMsg) => void; + +const send = V8Worker2.send; +const channels = new Map<string, MessageCallback[]>(); + +export function sub(channel: string, cb: MessageCallback): void { + let subscribers = channels.get(channel); + if (!subscribers) { + subscribers = []; + channels.set(channel, subscribers); + } + subscribers.push(cb); +} + +/* +export function subMsg(channel: string, cb: MessageStructCallback): void { + sub(channel, (payload: Uint8Array) => { + const msg = pb.Msg.decode(payload); + if (msg.error != null) { + f.onError(new Error(msg.error)); + } else { + cb(msg); + } + }); +} + */ + +export function pub(channel: string, payload: Uint8Array): null | ArrayBuffer { + const msg = pb.BaseMsg.fromObject({ channel, payload }); + const ui8 = pb.BaseMsg.encode(msg).finish(); + const ab = typedArrayToArrayBuffer(ui8); + return send(ab); +} + +// Internal version of "pub". +// TODO add internal version of "sub" +export function pubInternal(channel: string, obj: pb.IMsg): null | pb.Msg { + const msg = pb.Msg.fromObject(obj); + const ui8 = pb.Msg.encode(msg).finish(); + const resBuf = pub(channel, ui8); + if (resBuf != null && resBuf.byteLength > 0) { + const res = pb.Msg.decode(new Uint8Array(resBuf)); + if (res != null && res.error != null && res.error.length > 0) { + throw Error(res.error); + } + return res; + } else { + return null; + } +} + +V8Worker2.recv((ab: ArrayBuffer) => { + const msg = pb.BaseMsg.decode(new Uint8Array(ab)); + const subscribers = channels.get(msg.channel); + if (subscribers == null) { + throw Error(`No subscribers for channel "${msg.channel}".`); + } + + for (const subscriber of subscribers) { + subscriber(msg.payload); + } +}); + +// Delete the V8Worker2 from the global object, so that no one else can receive +// messages. +_global["V8Worker2"] = null; diff --git a/js/fetch.ts b/js/fetch.ts new file mode 100644 index 000000000..c59d41bf5 --- /dev/null +++ b/js/fetch.ts @@ -0,0 +1,146 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +import { + assert, + log, + createResolvable, + Resolvable, + typedArrayToArrayBuffer +} from "./util"; +import { pubInternal, sub } from "./dispatch"; +import { deno as pb } from "./msg.pb"; + +export function initFetch() { + sub("fetch", (payload: Uint8Array) => { + const msg = pb.Msg.decode(payload); + assert(msg.command === pb.Msg.Command.FETCH_RES); + const id = msg.fetchResId; + const f = fetchRequests.get(id); + assert(f != null, `Couldn't find FetchRequest id ${id}`); + + f.onMsg(msg); + }); +} + +const fetchRequests = new Map<number, FetchRequest>(); + +class FetchResponse implements Response { + readonly url: string; + body: null; + bodyUsed = false; // TODO + status: number; + statusText = "FIXME"; // TODO + readonly type = "basic"; // TODO + redirected = false; // TODO + headers: null; // TODO + //private bodyChunks: Uint8Array[] = []; + private first = true; + + constructor(readonly req: FetchRequest) { + this.url = req.url; + } + + bodyWaiter: Resolvable<ArrayBuffer>; + arrayBuffer(): Promise<ArrayBuffer> { + this.bodyWaiter = createResolvable(); + return this.bodyWaiter; + } + + blob(): Promise<Blob> { + throw Error("not implemented"); + } + + formData(): Promise<FormData> { + throw Error("not implemented"); + } + + async json(): Promise<object> { + const text = await this.text(); + return JSON.parse(text); + } + + async text(): Promise<string> { + const ab = await this.arrayBuffer(); + const decoder = new TextDecoder("utf-8"); + return decoder.decode(ab); + } + + get ok(): boolean { + return 200 <= this.status && this.status < 300; + } + + clone(): Response { + throw Error("not implemented"); + } + + onHeader: (res: Response) => void; + onError: (error: Error) => void; + + onMsg(msg: pb.Msg) { + if (msg.error !== null && msg.error !== "") { + //throw new Error(msg.error) + this.onError(new Error(msg.error)); + return; + } + + if (this.first) { + this.first = false; + this.status = msg.fetchResStatus; + this.onHeader(this); + } else { + // Body message. Assuming it all comes in one message now. + const ab = typedArrayToArrayBuffer(msg.fetchResBody); + this.bodyWaiter.resolve(ab); + } + } +} + +let nextFetchId = 0; +//TODO implements Request +class FetchRequest { + private readonly id: number; + response: FetchResponse; + constructor(readonly url: string) { + this.id = nextFetchId++; + fetchRequests.set(this.id, this); + this.response = new FetchResponse(this); + } + + onMsg(msg: pb.Msg) { + this.response.onMsg(msg); + } + + destroy() { + fetchRequests.delete(this.id); + } + + start() { + log("dispatch FETCH_REQ", this.id, this.url); + const res = pubInternal("fetch", { + command: pb.Msg.Command.FETCH_REQ, + fetchReqId: this.id, + fetchReqUrl: this.url + }); + assert(res == null); + } +} + +export function fetch( + input?: Request | string, + init?: RequestInit +): Promise<Response> { + const fetchReq = new FetchRequest(input as string); + const response = fetchReq.response; + return new Promise((resolve, reject) => { + // tslint:disable-next-line:no-any + response.onHeader = (response: any) => { + log("onHeader"); + resolve(response); + }; + response.onError = (error: Error) => { + log("onError", error); + reject(error); + }; + fetchReq.start(); + }); +} diff --git a/js/globals.ts b/js/globals.ts new file mode 100644 index 000000000..cca72d172 --- /dev/null +++ b/js/globals.ts @@ -0,0 +1,32 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +import * as timer from "./timers"; + +// If you use the eval function indirectly, by invoking it via a reference +// other than eval, as of ECMAScript 5 it works in the global scope rather than +// the local scope. This means, for instance, that function declarations create +// global functions, and that the code being evaluated doesn't have access to +// local variables within the scope where it's being called. +export const globalEval = eval; + +// A reference to the global object. +// TODO The underscore is because it's conflicting with @types/node. +export const _global = globalEval("this"); + +_global["window"] = _global; // Create a window object. +import "./url"; + +_global["setTimeout"] = timer.setTimeout; +_global["setInterval"] = timer.setInterval; +_global["clearTimeout"] = timer.clearTimer; +_global["clearInterval"] = timer.clearTimer; + +import { Console } from "./console"; +_global["console"] = new Console(); + +import { fetch } from "./fetch"; +_global["fetch"] = fetch; + +import { TextEncoder, TextDecoder } from "text-encoding"; +_global["TextEncoder"] = TextEncoder; +_global["TextDecoder"] = TextDecoder; diff --git a/js/main.ts b/js/main.ts index 3613f3c89..43bd0e03f 100644 --- a/js/main.ts +++ b/js/main.ts @@ -25,9 +25,9 @@ window["denoMain"] = () => { const argv: string[] = []; for (let i = 0; i < msg.startArgvLength(); i++) { - const arg = msg.startArgv(i); - deno.print(`argv[${i}] ${arg}`); + argv.push(msg.startArgv(i)); } + deno.print(`argv ${argv}`); }; function typedArrayToArrayBuffer(ta: Uint8Array): ArrayBuffer { diff --git a/js/os.ts b/js/os.ts new file mode 100644 index 000000000..a51c6ec5d --- /dev/null +++ b/js/os.ts @@ -0,0 +1,65 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +import { ModuleInfo } from "./types"; +import { pubInternal } from "./dispatch"; +import { deno as pb } from "./msg.pb"; +import { assert } from "./util"; + +export function exit(exitCode = 0): void { + pubInternal("os", { + command: pb.Msg.Command.EXIT, + exitCode + }); +} + +export function codeFetch( + moduleSpecifier: string, + containingFile: string +): ModuleInfo { + const res = pubInternal("os", { + command: pb.Msg.Command.CODE_FETCH, + codeFetchModuleSpecifier: moduleSpecifier, + codeFetchContainingFile: containingFile + }); + assert(res.command === pb.Msg.Command.CODE_FETCH_RES); + return { + moduleName: res.codeFetchResModuleName, + filename: res.codeFetchResFilename, + sourceCode: res.codeFetchResSourceCode, + outputCode: res.codeFetchResOutputCode + }; +} + +export function codeCache( + filename: string, + sourceCode: string, + outputCode: string +): void { + pubInternal("os", { + command: pb.Msg.Command.CODE_CACHE, + codeCacheFilename: filename, + codeCacheSourceCode: sourceCode, + codeCacheOutputCode: outputCode + }); +} + +export function readFileSync(filename: string): Uint8Array { + const res = pubInternal("os", { + command: pb.Msg.Command.READ_FILE_SYNC, + readFileSyncFilename: filename + }); + return res.readFileSyncData; +} + +export function writeFileSync( + filename: string, + data: Uint8Array, + perm: number +): void { + pubInternal("os", { + command: pb.Msg.Command.WRITE_FILE_SYNC, + writeFileSyncFilename: filename, + writeFileSyncData: data, + writeFileSyncPerm: perm + }); +} diff --git a/js/runtime.ts b/js/runtime.ts new file mode 100644 index 000000000..46538c80f --- /dev/null +++ b/js/runtime.ts @@ -0,0 +1,339 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +// Glossary +// outputCode = generated javascript code +// sourceCode = typescript code (or input javascript code) +// moduleName = a resolved module name +// fileName = an unresolved raw fileName. +// for http modules , its the path to the locally downloaded +// version. + +import * as ts from "typescript"; +import * as util from "./util"; +import { log } from "./util"; +import * as os from "./os"; +import * as sourceMaps from "./v8_source_maps"; +import { _global, globalEval } from "./globals"; +import * as deno from "./deno"; + +const EOL = "\n"; + +// tslint:disable-next-line:no-any +export type AmdFactory = (...args: any[]) => undefined | object; +export type AmdDefine = (deps: string[], factory: AmdFactory) => void; + +// Uncaught exceptions are sent to window.onerror by v8worker2. +// https://git.io/vhOsf +window.onerror = (message, source, lineno, colno, error) => { + // TODO Currently there is a bug in v8_source_maps.ts that causes a segfault + // if it is used within window.onerror. To workaround we uninstall the + // Error.prepareStackTrace handler. Users will get unmapped stack traces on + // uncaught exceptions until this issue is fixed. + Error.prepareStackTrace = null; + console.log(error.message, error.stack); + os.exit(1); +}; + +export function setup(mainJs: string, mainMap: string): void { + sourceMaps.install({ + installPrepareStackTrace: true, + getGeneratedContents: (filename: string): string => { + if (filename === "/main.js") { + return mainJs; + } else if (filename === "/main.map") { + return mainMap; + } else { + const mod = FileModule.load(filename); + if (!mod) { + console.error("getGeneratedContents cannot find", filename); + } + return mod.outputCode; + } + } + }); +} + +// This class represents a module. We call it FileModule to make it explicit +// that each module represents a single file. +// Access to FileModule instances should only be done thru the static method +// FileModule.load(). FileModules are NOT executed upon first load, only when +// compileAndRun is called. +export class FileModule { + scriptVersion: string; + readonly exports = {}; + + private static readonly map = new Map<string, FileModule>(); + constructor( + readonly fileName: string, + readonly sourceCode = "", + public outputCode = "" + ) { + util.assert( + !FileModule.map.has(fileName), + `FileModule.map already has ${fileName}` + ); + FileModule.map.set(fileName, this); + if (outputCode !== "") { + this.scriptVersion = "1"; + } + } + + compileAndRun(): void { + if (!this.outputCode) { + // If there is no cached outputCode, then compile the code. + util.assert( + this.sourceCode != null && this.sourceCode.length > 0, + `Have no source code from ${this.fileName}` + ); + const compiler = Compiler.instance(); + this.outputCode = compiler.compile(this.fileName); + os.codeCache(this.fileName, this.sourceCode, this.outputCode); + } + util.log("compileAndRun", this.sourceCode); + execute(this.fileName, this.outputCode); + } + + static load(fileName: string): FileModule { + return this.map.get(fileName); + } + + static getScriptsWithSourceCode(): string[] { + const out = []; + for (const fn of this.map.keys()) { + const m = this.map.get(fn); + if (m.sourceCode) { + out.push(fn); + } + } + return out; + } +} + +export function makeDefine(fileName: string): AmdDefine { + const localDefine = (deps: string[], factory: AmdFactory): void => { + const localRequire = (x: string) => { + log("localRequire", x); + }; + const currentModule = FileModule.load(fileName); + const localExports = currentModule.exports; + log("localDefine", fileName, deps, localExports); + const args = deps.map(dep => { + if (dep === "require") { + return localRequire; + } else if (dep === "exports") { + return localExports; + } else if (dep === "typescript") { + return ts; + } else if (dep === "deno") { + return deno; + } else { + const resolved = resolveModuleName(dep, fileName); + const depModule = FileModule.load(resolved); + depModule.compileAndRun(); + return depModule.exports; + } + }); + factory(...args); + }; + return localDefine; +} + +export function resolveModule( + moduleSpecifier: string, + containingFile: string +): null | FileModule { + //util.log("resolveModule", { moduleSpecifier, containingFile }); + util.assert(moduleSpecifier != null && moduleSpecifier.length > 0); + // We ask golang to sourceCodeFetch. It will load the sourceCode and if + // there is any outputCode cached, it will return that as well. + let fetchResponse; + try { + fetchResponse = os.codeFetch(moduleSpecifier, containingFile); + } catch (e) { + // TODO Only catch "no such file or directory" errors. Need error codes. + return null; + } + const { filename, sourceCode, outputCode } = fetchResponse; + if (sourceCode.length === 0) { + return null; + } + util.log("resolveModule sourceCode length ", sourceCode.length); + const m = FileModule.load(filename); + if (m != null) { + return m; + } else { + return new FileModule(filename, sourceCode, outputCode); + } +} + +function resolveModuleName( + moduleSpecifier: string, + containingFile: string +): string | undefined { + const mod = resolveModule(moduleSpecifier, containingFile); + if (mod) { + return mod.fileName; + } else { + return undefined; + } +} + +function execute(fileName: string, outputCode: string): void { + util.assert(outputCode && outputCode.length > 0); + _global["define"] = makeDefine(fileName); + outputCode += `\n//# sourceURL=${fileName}`; + globalEval(outputCode); + _global["define"] = null; +} + +// This is a singleton class. Use Compiler.instance() to access. +class Compiler { + options: ts.CompilerOptions = { + allowJs: true, + module: ts.ModuleKind.AMD, + outDir: "$deno$", + inlineSourceMap: true, + lib: ["es2017"], + inlineSources: true, + target: ts.ScriptTarget.ES2017 + }; + /* + allowJs: true, + module: ts.ModuleKind.AMD, + noEmit: false, + outDir: '$deno$', + */ + private service: ts.LanguageService; + + private constructor() { + const host = new TypeScriptHost(this.options); + this.service = ts.createLanguageService(host); + } + + private static _instance: Compiler; + static instance(): Compiler { + return this._instance || (this._instance = new this()); + } + + compile(fileName: string): string { + const output = this.service.getEmitOutput(fileName); + + // Get the relevant diagnostics - this is 3x faster than + // `getPreEmitDiagnostics`. + const diagnostics = this.service + .getCompilerOptionsDiagnostics() + .concat(this.service.getSyntacticDiagnostics(fileName)) + .concat(this.service.getSemanticDiagnostics(fileName)); + if (diagnostics.length > 0) { + const errMsg = ts.formatDiagnosticsWithColorAndContext( + diagnostics, + formatDiagnosticsHost + ); + console.log(errMsg); + os.exit(1); + } + + util.assert(!output.emitSkipped); + + const outputCode = output.outputFiles[0].text; + // let sourceMapCode = output.outputFiles[0].text; + return outputCode; + } +} + +// Create the compiler host for type checking. +class TypeScriptHost implements ts.LanguageServiceHost { + constructor(readonly options: ts.CompilerOptions) {} + + getScriptFileNames(): string[] { + const keys = FileModule.getScriptsWithSourceCode(); + util.log("getScriptFileNames", keys); + return keys; + } + + getScriptVersion(fileName: string): string { + util.log("getScriptVersion", fileName); + const m = FileModule.load(fileName); + return m.scriptVersion; + } + + getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { + util.log("getScriptSnapshot", fileName); + const m = resolveModule(fileName, "."); + if (m == null) { + util.log("getScriptSnapshot", fileName, "NOT FOUND"); + return undefined; + } + //const m = resolveModule(fileName, "."); + util.assert(m.sourceCode.length > 0); + return ts.ScriptSnapshot.fromString(m.sourceCode); + } + + fileExists(fileName: string): boolean { + const m = resolveModule(fileName, "."); + const exists = m != null; + util.log("fileExist", fileName, exists); + return exists; + } + + readFile(path: string, encoding?: string): string | undefined { + util.log("readFile", path); + throw Error("not implemented"); + } + + getNewLine() { + return EOL; + } + + getCurrentDirectory() { + util.log("getCurrentDirectory"); + return "."; + } + + getCompilationSettings() { + util.log("getCompilationSettings"); + return this.options; + } + + getDefaultLibFileName(options: ts.CompilerOptions): string { + const fn = ts.getDefaultLibFileName(options); + util.log("getDefaultLibFileName", fn); + const m = resolveModule(fn, "/$asset$/"); + return m.fileName; + } + + resolveModuleNames( + moduleNames: string[], + containingFile: string, + reusedNames?: string[] + ): Array<ts.ResolvedModule | undefined> { + //util.log("resolveModuleNames", { moduleNames, reusedNames }); + return moduleNames.map((name: string) => { + let resolvedFileName; + if (name === "deno") { + resolvedFileName = resolveModuleName("deno.d.ts", "/$asset$/"); + } else if (name === "typescript") { + resolvedFileName = resolveModuleName("typescript.d.ts", "/$asset$/"); + } else { + resolvedFileName = resolveModuleName(name, containingFile); + if (resolvedFileName == null) { + return undefined; + } + } + const isExternalLibraryImport = false; + return { resolvedFileName, isExternalLibraryImport }; + }); + } +} + +const formatDiagnosticsHost: ts.FormatDiagnosticsHost = { + getCurrentDirectory(): string { + return "."; + }, + getCanonicalFileName(fileName: string): string { + return fileName; + }, + getNewLine(): string { + return EOL; + } +}; diff --git a/js/tests.ts b/js/tests.ts new file mode 100644 index 000000000..49e6aa9b3 --- /dev/null +++ b/js/tests.ts @@ -0,0 +1,126 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +// This test is executed as part of integration_test.go +// But it can also be run manually: +// ./deno tests.ts +// There must also be a static file http server running on localhost:4545 +// serving the deno project directory. Try this: +// http-server -p 4545 --cors . +import { test, assert, assertEqual } from "./testing/testing.ts"; +import { readFileSync, writeFileSync } from "deno"; + +test(async function tests_test() { + assert(true); +}); + +test(async function tests_fetch() { + const response = await fetch("http://localhost:4545/package.json"); + const json = await response.json(); + assertEqual(json.name, "deno"); +}); + +test(function tests_console_assert() { + console.assert(true); + + let hasThrown = false; + try { + console.assert(false); + } catch { + hasThrown = true; + } + assertEqual(hasThrown, true); +}); + +test(async function tests_readFileSync() { + const data = readFileSync("package.json"); + if (!data.byteLength) { + throw Error( + `Expected positive value for data.byteLength ${data.byteLength}` + ); + } + const decoder = new TextDecoder("utf-8"); + const json = decoder.decode(data); + const pkg = JSON.parse(json); + assertEqual(pkg.name, "deno"); +}); + +test(async function tests_writeFileSync() { + const enc = new TextEncoder(); + const data = enc.encode("Hello"); + // TODO need ability to get tmp dir. + // const fn = "/tmp/test.txt"; + writeFileSync("/tmp/test.txt", data, 0o666); + const dataRead = readFileSync("/tmp/test.txt"); + const dec = new TextDecoder("utf-8"); + const actual = dec.decode(dataRead); + assertEqual("Hello", actual); +}); + +test(function tests_console_assert() { + console.assert(true); + + let hasThrown = false; + try { + console.assert(false); + } catch { + hasThrown = true; + } + assertEqual(hasThrown, true); +}); + +test(function tests_console_stringify_circular() { + class Base { + a = 1; + m1() {} + } + + class Extended extends Base { + b = 2; + m2() {} + } + + // tslint:disable-next-line:no-any + const nestedObj: any = { + num: 1, + bool: true, + str: "a", + method() {}, + un: undefined, + nu: null, + arrowFunc: () => {}, + extendedClass: new Extended(), + nFunc: new Function(), + extendedCstr: Extended + }; + + const circularObj = { + num: 2, + bool: false, + str: "b", + method() {}, + un: undefined, + nu: null, + nested: nestedObj, + emptyObj: {}, + arr: [1, "s", false, null, nestedObj], + baseClass: new Base() + }; + + nestedObj.o = circularObj; + + try { + console.log(1); + console.log("s"); + console.log(false); + console.log(Symbol(1)); + console.log(null); + console.log(undefined); + console.log(new Extended()); + console.log(function f() {}); + console.log(nestedObj); + console.log(JSON); + console.log(console); + } catch { + throw new Error("Expected no crash on circular object"); + } +}); diff --git a/js/text-encoding.d.ts b/js/text-encoding.d.ts new file mode 100644 index 000000000..6feadad9c --- /dev/null +++ b/js/text-encoding.d.ts @@ -0,0 +1,6 @@ +// Remove and depend on @types/text-encoding once this PR is merged +// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/26141 +declare module "text-encoding" { + export const TextEncoder: TextEncoder; + export const TextDecoder: TextDecoder; +} diff --git a/js/timers.ts b/js/timers.ts new file mode 100644 index 000000000..da2cccd89 --- /dev/null +++ b/js/timers.ts @@ -0,0 +1,89 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +import { deno as pb } from "./msg.pb"; +import { pubInternal, sub } from "./dispatch"; +import { assert } from "./util"; + +let nextTimerId = 1; + +// tslint:disable-next-line:no-any +export type TimerCallback = (...args: any[]) => void; + +interface Timer { + id: number; + cb: TimerCallback; + interval: boolean; + // tslint:disable-next-line:no-any + args: any[]; + delay: number; // milliseconds +} + +const timers = new Map<number, Timer>(); + +export function initTimers() { + sub("timers", onMessage); +} + +function onMessage(payload: Uint8Array) { + const msg = pb.Msg.decode(payload); + assert(msg.command === pb.Msg.Command.TIMER_READY); + const { timerReadyId, timerReadyDone } = msg; + const timer = timers.get(timerReadyId); + if (!timer) { + return; + } + timer.cb(...timer.args); + if (timerReadyDone) { + timers.delete(timerReadyId); + } +} + +function setTimer( + cb: TimerCallback, + delay: number, + interval: boolean, + // tslint:disable-next-line:no-any + args: any[] +): number { + const timer = { + id: nextTimerId++, + interval, + delay, + args, + cb + }; + timers.set(timer.id, timer); + pubInternal("timers", { + command: pb.Msg.Command.TIMER_START, + timerStartId: timer.id, + timerStartInterval: timer.interval, + timerStartDelay: timer.delay + }); + return timer.id; +} + +export function setTimeout( + cb: TimerCallback, + delay: number, + // tslint:disable-next-line:no-any + ...args: any[] +): number { + return setTimer(cb, delay, false, args); +} + +export function setInterval( + cb: TimerCallback, + delay: number, + // tslint:disable-next-line:no-any + ...args: any[] +): number { + return setTimer(cb, delay, true, args); +} + +export function clearTimer(id: number) { + timers.delete(id); + pubInternal("timers", { + command: pb.Msg.Command.TIMER_CLEAR, + timerClearId: id + }); +} diff --git a/js/types.ts b/js/types.ts new file mode 100644 index 000000000..d32d9f5a6 --- /dev/null +++ b/js/types.ts @@ -0,0 +1,10 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +export type TypedArray = Uint8Array | Float32Array | Int32Array; + +export interface ModuleInfo { + moduleName?: string; + filename?: string; + sourceCode?: string; + outputCode?: string; +} diff --git a/js/util.ts b/js/util.ts new file mode 100644 index 000000000..70cb79a55 --- /dev/null +++ b/js/util.ts @@ -0,0 +1,53 @@ +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +import { debug } from "./main"; +import { TypedArray } from "./types"; + +// Internal logging for deno. Use the "debug" variable above to control +// output. +// tslint:disable-next-line:no-any +export function log(...args: any[]): void { + if (debug) { + console.log(...args); + } +} + +export function assert(cond: boolean, msg = "") { + if (!cond) { + throw Error(`Assert fail. ${msg}`); + } +} + +export function typedArrayToArrayBuffer(ta: TypedArray): ArrayBuffer { + const ab = ta.buffer.slice(ta.byteOffset, ta.byteOffset + ta.byteLength); + return ab as ArrayBuffer; +} + +export function arrayToStr(ui8: Uint8Array): string { + return String.fromCharCode(...ui8); +} + +// A `Resolvable` is a Promise with the `reject` and `resolve` functions +// placed as methods on the promise object itself. It allows you to do: +// +// const p = createResolvable<number>(); +// ... +// p.resolve(42); +// +// It'd be prettier to make Resolvable a class that inherits from Promise, +// rather than an interface. This is possible in ES2016, however typescript +// produces broken code when targeting ES5 code. +// See https://github.com/Microsoft/TypeScript/issues/15202 +// At the time of writing, the github issue is closed but the problem remains. +export interface Resolvable<T> extends Promise<T> { + resolve: (value?: T | PromiseLike<T>) => void; + // tslint:disable-next-line:no-any + reject: (reason?: any) => void; +} +export function createResolvable<T>(): Resolvable<T> { + let methods; + const promise = new Promise<T>((resolve, reject) => { + methods = { resolve, reject }; + }); + return Object.assign(promise, methods) as Resolvable<T>; +} diff --git a/js/v8_source_maps.ts b/js/v8_source_maps.ts new file mode 100644 index 000000000..2384f34dc --- /dev/null +++ b/js/v8_source_maps.ts @@ -0,0 +1,273 @@ +// Copyright 2014 Evan Wallace +// Copyright 2018 Ryan Dahl <ry@tinyclouds.org> +// All rights reserved. MIT License. +// 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"; + +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 CallSite extends NodeJS.CallSite { + getScriptNameOrSourceURL(): string; +} + +interface Position { + source: string; // Filename + column: number; + line: number; +} + +type GetGeneratedContentsCallback = (fileName: string) => string; + +let getGeneratedContents: GetGeneratedContentsCallback; + +export function install(options: Options) { + getGeneratedContents = options.getGeneratedContents; + if (options.installPrepareStackTrace) { + Error.prepareStackTrace = prepareStackTraceWrapper; + } +} + +export function prepareStackTraceWrapper( + error: Error, + stack: CallSite[] +): string { + try { + return prepareStackTrace(error, stack); + } catch (prepareStackError) { + Error.prepareStackTrace = null; + console.log("=====Error inside of prepareStackTrace===="); + console.log(prepareStackError.stack.toString()); + console.log("=====Original error======================="); + throw error; + } +} + +export function prepareStackTrace(error: Error, stack: CallSite[]): string { + const frames = stack.map( + (frame: CallSite) => `\n at ${wrapCallSite(frame).toString()}` + ); + return error.toString() + frames.join(""); +} + +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(); + const column = frame.getColumnNumber() - 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); + return frame; + } + + // Code called using eval() needs special handling + let origin = frame.isEval() && frame.getEvalOrigin(); + if (origin) { + origin = mapEvalOrigin(origin); + frame = cloneCallSite(frame); + frame.getEvalOrigin = () => origin; + return frame; + } + + // If we get here then we were unable to change the source position + 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 => { + obj[name] = /^(?:is|get)/.test(name) + ? () => frame_[name].call(frame) + : frame_[name]; + }); + return (obj as any) as CallSite; + // tslint:enable:no-any +} + +// 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(); + 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 += ":" + String(lineNumber); + const columnNumber = frame.getColumnNumber(); + if (columnNumber) { + fileLocation += ":" + String(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 compatable 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,/; + +function loadConsumer(source: string): SourceMapConsumer { + let consumer = consumers.get(source); + if (consumer == null) { + const code = getGeneratedContents(source); + if (!code) { + return null; + } + + let sourceMappingURL = retrieveSourceMapURL(code); + if (!sourceMappingURL) { + throw Error("No source map?"); + } + + let sourceMapData: string; + 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); + sourceMappingURL = source; + } else { + // Support source map URLs relative to the source URL + //sourceMappingURL = supportRelativeURL(source, sourceMappingURL); + sourceMapData = getGeneratedContents(sourceMappingURL); + } + + //console.log("sourceMapData", sourceMapData); + const rawSourceMap = JSON.parse(sourceMapData); + consumer = new SourceMapConsumer(rawSourceMap); + consumers.set(source, consumer); + } + return consumer; +} + +function retrieveSourceMapURL(fileData: string): string { + // 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))) { + lastMatch = match; + } + if (!lastMatch) { + return null; + } + return lastMatch[1]; +} + +function mapSourcePosition(position: Position): MappedPosition { + const consumer = loadConsumer(position.source); + if (consumer == null) { + return position; + } + const mapped = consumer.originalPositionFor(position); + return mapped; +} + +// 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); + 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 = /^eval at ([^(]+) \((.+)\)$/.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; +} |