summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
authorRyan Dahl <ry@tinyclouds.org>2018-07-06 11:20:35 -0400
committerRyan Dahl <ry@tinyclouds.org>2018-07-06 12:22:11 -0400
commitfe404dfce901356dc7a5d38ba70029c72a946f27 (patch)
treeb8ed0d3417e920da1fd4e6278046184d8205a237 /js
parent21e1425656ccebb8d31da95acd83991fb7d728fd (diff)
Import ts file from prototype without change
From commit 559453cf6cc88283bcf8fdeccd387458f5c63165 Excluding v8worker.d.ts, main.ts, and deno.d.ts. Updates tslint.json to be original settings.
Diffstat (limited to 'js')
-rw-r--r--js/console.ts124
-rw-r--r--js/deno.ts6
-rw-r--r--js/dispatch.ts73
-rw-r--r--js/fetch.ts146
-rw-r--r--js/globals.ts32
-rw-r--r--js/main.ts4
-rw-r--r--js/os.ts65
-rw-r--r--js/runtime.ts339
-rw-r--r--js/tests.ts126
-rw-r--r--js/text-encoding.d.ts6
-rw-r--r--js/timers.ts89
-rw-r--r--js/types.ts10
-rw-r--r--js/util.ts53
-rw-r--r--js/v8_source_maps.ts273
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;
+}