summaryrefslogtreecommitdiff
path: root/cli/js
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js')
-rw-r--r--cli/js/base64.ts150
-rw-r--r--cli/js/blob.ts178
-rw-r--r--cli/js/blob_test.ts62
-rw-r--r--cli/js/body.ts272
-rw-r--r--cli/js/body_test.ts68
-rw-r--r--cli/js/buffer.ts294
-rw-r--r--cli/js/buffer_test.ts277
-rw-r--r--cli/js/build.ts27
-rw-r--r--cli/js/build_test.ts10
-rw-r--r--cli/js/chmod.ts20
-rw-r--r--cli/js/chmod_test.ts142
-rw-r--r--cli/js/chown.ts27
-rw-r--r--cli/js/chown_test.ts145
-rw-r--r--cli/js/colors.ts40
-rw-r--r--cli/js/compiler.ts667
-rw-r--r--cli/js/console.ts790
-rw-r--r--cli/js/console_table.ts94
-rw-r--r--cli/js/console_test.ts698
-rw-r--r--cli/js/copy_file.ts30
-rw-r--r--cli/js/copy_file_test.ts163
-rw-r--r--cli/js/core.ts6
-rw-r--r--cli/js/custom_event.ts48
-rw-r--r--cli/js/custom_event_test.ts27
-rw-r--r--cli/js/deno.ts119
-rw-r--r--cli/js/diagnostics.ts217
-rw-r--r--cli/js/dir.ts22
-rw-r--r--cli/js/dir_test.ts54
-rw-r--r--cli/js/dispatch.ts110
-rw-r--r--cli/js/dispatch_json.ts86
-rw-r--r--cli/js/dispatch_json_test.ts19
-rw-r--r--cli/js/dispatch_minimal.ts80
-rw-r--r--cli/js/dom_file.ts24
-rw-r--r--cli/js/dom_types.ts625
-rw-r--r--cli/js/dom_util.ts85
-rw-r--r--cli/js/error_stack.ts273
-rw-r--r--cli/js/error_stack_test.ts108
-rw-r--r--cli/js/errors.ts79
-rw-r--r--cli/js/event.ts348
-rw-r--r--cli/js/event_target.ts503
-rw-r--r--cli/js/event_target_test.ts142
-rw-r--r--cli/js/event_test.ts95
-rw-r--r--cli/js/fetch.ts478
-rw-r--r--cli/js/fetch_test.ts357
-rw-r--r--cli/js/file_info.ts91
-rw-r--r--cli/js/file_test.ts103
-rw-r--r--cli/js/files.ts235
-rw-r--r--cli/js/files_test.ts329
-rw-r--r--cli/js/form_data.ts149
-rw-r--r--cli/js/form_data_test.ts179
-rw-r--r--cli/js/format_error.ts9
-rw-r--r--cli/js/get_random_values.ts31
-rw-r--r--cli/js/get_random_values_test.ts51
-rw-r--r--cli/js/globals.ts207
-rw-r--r--cli/js/globals_test.ts104
-rw-r--r--cli/js/headers.ts139
-rw-r--r--cli/js/headers_test.ts331
-rw-r--r--cli/js/io.ts170
-rw-r--r--cli/js/lib.deno_runtime.d.ts2800
-rw-r--r--cli/js/lib.web_assembly.d.ts173
-rw-r--r--cli/js/link.ts19
-rw-r--r--cli/js/link_test.ts115
-rw-r--r--cli/js/location.ts52
-rw-r--r--cli/js/location_test.ts8
-rw-r--r--cli/js/main.ts41
-rw-r--r--cli/js/make_temp_dir.ts35
-rw-r--r--cli/js/make_temp_dir_test.ts66
-rw-r--r--cli/js/metrics.ts28
-rw-r--r--cli/js/metrics_test.ts46
-rw-r--r--cli/js/mixins/dom_iterable.ts82
-rw-r--r--cli/js/mixins/dom_iterable_test.ts79
-rw-r--r--cli/js/mkdir.ts33
-rw-r--r--cli/js/mkdir_test.ts66
-rw-r--r--cli/js/mock_builtin.js2
-rw-r--r--cli/js/net.ts205
-rw-r--r--cli/js/net_test.ts229
-rw-r--r--cli/js/os.ts151
-rw-r--r--cli/js/os_test.ts165
-rw-r--r--cli/js/performance.ts22
-rw-r--r--cli/js/performance_test.ts10
-rw-r--r--cli/js/permissions.ts39
-rw-r--r--cli/js/permissions_test.ts28
-rw-r--r--cli/js/process.ts307
-rw-r--r--cli/js/process_test.ts377
-rw-r--r--cli/js/read_dir.ts34
-rw-r--r--cli/js/read_dir_test.ts84
-rw-r--r--cli/js/read_file.ts29
-rw-r--r--cli/js/read_file_test.ts57
-rw-r--r--cli/js/read_link.ts19
-rw-r--r--cli/js/read_link_test.ts69
-rw-r--r--cli/js/remove.ts32
-rw-r--r--cli/js/remove_test.ts335
-rw-r--r--cli/js/rename.ts24
-rw-r--r--cli/js/rename_test.ts74
-rw-r--r--cli/js/repl.ts197
-rw-r--r--cli/js/request.ts151
-rw-r--r--cli/js/request_test.ts17
-rw-r--r--cli/js/resources.ts19
-rw-r--r--cli/js/resources_test.ts48
-rw-r--r--cli/js/stat.ts73
-rw-r--r--cli/js/stat_test.ts172
-rw-r--r--cli/js/symlink.ts39
-rw-r--r--cli/js/symlink_test.ts80
-rw-r--r--cli/js/test_util.ts262
-rw-r--r--cli/js/text_encoding.ts554
-rw-r--r--cli/js/text_encoding_test.ts193
-rw-r--r--cli/js/timers.ts280
-rw-r--r--cli/js/timers_test.ts291
-rw-r--r--cli/js/tls.ts21
-rw-r--r--cli/js/tls_test.ts25
-rw-r--r--cli/js/truncate.ts34
-rw-r--r--cli/js/truncate_test.ts74
-rw-r--r--cli/js/ts_global.d.ts19
-rw-r--r--cli/js/type_directives.ts91
-rw-r--r--cli/js/types.ts2
-rwxr-xr-xcli/js/unit_test_runner.ts107
-rw-r--r--cli/js/unit_tests.ts65
-rw-r--r--cli/js/url.ts376
-rw-r--r--cli/js/url_search_params.ts297
-rw-r--r--cli/js/url_search_params_test.ts238
-rw-r--r--cli/js/url_test.ts181
-rw-r--r--cli/js/util.ts225
-rw-r--r--cli/js/utime.ts45
-rw-r--r--cli/js/utime_test.ts181
-rw-r--r--cli/js/version.ts28
-rw-r--r--cli/js/version_test.ts8
-rw-r--r--cli/js/window.ts9
-rw-r--r--cli/js/workers.ts193
-rw-r--r--cli/js/write_file.ts76
-rw-r--r--cli/js/write_file_test.ts219
129 files changed, 21012 insertions, 0 deletions
diff --git a/cli/js/base64.ts b/cli/js/base64.ts
new file mode 100644
index 000000000..4d30e00f1
--- /dev/null
+++ b/cli/js/base64.ts
@@ -0,0 +1,150 @@
+// Forked from https://github.com/beatgammit/base64-js
+// Copyright (c) 2014 Jameson Little. MIT License.
+
+const lookup: string[] = [];
+const revLookup: number[] = [];
+
+const code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+for (let i = 0, len = code.length; i < len; ++i) {
+ lookup[i] = code[i];
+ revLookup[code.charCodeAt(i)] = i;
+}
+
+// Support decoding URL-safe base64 strings, as Node.js does.
+// See: https://en.wikipedia.org/wiki/Base64#URL_applications
+revLookup["-".charCodeAt(0)] = 62;
+revLookup["_".charCodeAt(0)] = 63;
+
+function getLens(b64: string): [number, number] {
+ const len = b64.length;
+
+ if (len % 4 > 0) {
+ throw new Error("Invalid string. Length must be a multiple of 4");
+ }
+
+ // Trim off extra bytes after placeholder bytes are found
+ // See: https://github.com/beatgammit/base64-js/issues/42
+ let validLen = b64.indexOf("=");
+ if (validLen === -1) validLen = len;
+
+ const placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4);
+
+ return [validLen, placeHoldersLen];
+}
+
+// base64 is 4/3 + up to two characters of the original data
+export function byteLength(b64: string): number {
+ const lens = getLens(b64);
+ const validLen = lens[0];
+ const placeHoldersLen = lens[1];
+ return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen;
+}
+
+function _byteLength(
+ b64: string,
+ validLen: number,
+ placeHoldersLen: number
+): number {
+ return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen;
+}
+
+export function toByteArray(b64: string): Uint8Array {
+ let tmp;
+ const lens = getLens(b64);
+ const validLen = lens[0];
+ const placeHoldersLen = lens[1];
+
+ const arr = new Uint8Array(_byteLength(b64, validLen, placeHoldersLen));
+
+ let curByte = 0;
+
+ // if there are placeholders, only get up to the last complete 4 chars
+ const len = placeHoldersLen > 0 ? validLen - 4 : validLen;
+
+ let i;
+ for (i = 0; i < len; i += 4) {
+ tmp =
+ (revLookup[b64.charCodeAt(i)] << 18) |
+ (revLookup[b64.charCodeAt(i + 1)] << 12) |
+ (revLookup[b64.charCodeAt(i + 2)] << 6) |
+ revLookup[b64.charCodeAt(i + 3)];
+ arr[curByte++] = (tmp >> 16) & 0xff;
+ arr[curByte++] = (tmp >> 8) & 0xff;
+ arr[curByte++] = tmp & 0xff;
+ }
+
+ if (placeHoldersLen === 2) {
+ tmp =
+ (revLookup[b64.charCodeAt(i)] << 2) |
+ (revLookup[b64.charCodeAt(i + 1)] >> 4);
+ arr[curByte++] = tmp & 0xff;
+ }
+
+ if (placeHoldersLen === 1) {
+ tmp =
+ (revLookup[b64.charCodeAt(i)] << 10) |
+ (revLookup[b64.charCodeAt(i + 1)] << 4) |
+ (revLookup[b64.charCodeAt(i + 2)] >> 2);
+ arr[curByte++] = (tmp >> 8) & 0xff;
+ arr[curByte++] = tmp & 0xff;
+ }
+
+ return arr;
+}
+
+function tripletToBase64(num: number): string {
+ return (
+ lookup[(num >> 18) & 0x3f] +
+ lookup[(num >> 12) & 0x3f] +
+ lookup[(num >> 6) & 0x3f] +
+ lookup[num & 0x3f]
+ );
+}
+
+function encodeChunk(uint8: Uint8Array, start: number, end: number): string {
+ let tmp;
+ const output = [];
+ for (let i = start; i < end; i += 3) {
+ tmp =
+ ((uint8[i] << 16) & 0xff0000) +
+ ((uint8[i + 1] << 8) & 0xff00) +
+ (uint8[i + 2] & 0xff);
+ output.push(tripletToBase64(tmp));
+ }
+ return output.join("");
+}
+
+export function fromByteArray(uint8: Uint8Array): string {
+ let tmp;
+ const len = uint8.length;
+ const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes
+ const parts = [];
+ const maxChunkLength = 16383; // must be multiple of 3
+
+ // go through the array every three bytes, we'll deal with trailing stuff later
+ for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
+ parts.push(
+ encodeChunk(
+ uint8,
+ i,
+ i + maxChunkLength > len2 ? len2 : i + maxChunkLength
+ )
+ );
+ }
+
+ // pad the end with zeros, but make sure to not forget the extra bytes
+ if (extraBytes === 1) {
+ tmp = uint8[len - 1];
+ parts.push(lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f] + "==");
+ } else if (extraBytes === 2) {
+ tmp = (uint8[len - 2] << 8) + uint8[len - 1];
+ parts.push(
+ lookup[tmp >> 10] +
+ lookup[(tmp >> 4) & 0x3f] +
+ lookup[(tmp << 2) & 0x3f] +
+ "="
+ );
+ }
+
+ return parts.join("");
+}
diff --git a/cli/js/blob.ts b/cli/js/blob.ts
new file mode 100644
index 000000000..50ab7f374
--- /dev/null
+++ b/cli/js/blob.ts
@@ -0,0 +1,178 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types.ts";
+import { containsOnlyASCII, hasOwnProperty } from "./util.ts";
+import { TextEncoder } from "./text_encoding.ts";
+import { build } from "./build.ts";
+
+export const bytesSymbol = Symbol("bytes");
+
+function convertLineEndingsToNative(s: string): string {
+ const nativeLineEnd = build.os == "win" ? "\r\n" : "\n";
+
+ let position = 0;
+
+ let collectionResult = collectSequenceNotCRLF(s, position);
+
+ let token = collectionResult.collected;
+ position = collectionResult.newPosition;
+
+ let result = token;
+
+ while (position < s.length) {
+ const c = s.charAt(position);
+ if (c == "\r") {
+ result += nativeLineEnd;
+ position++;
+ if (position < s.length && s.charAt(position) == "\n") {
+ position++;
+ }
+ } else if (c == "\n") {
+ position++;
+ result += nativeLineEnd;
+ }
+
+ collectionResult = collectSequenceNotCRLF(s, position);
+
+ token = collectionResult.collected;
+ position = collectionResult.newPosition;
+
+ result += token;
+ }
+
+ return result;
+}
+
+function collectSequenceNotCRLF(
+ s: string,
+ position: number
+): { collected: string; newPosition: number } {
+ const start = position;
+ for (
+ let c = s.charAt(position);
+ position < s.length && !(c == "\r" || c == "\n");
+ c = s.charAt(++position)
+ );
+ return { collected: s.slice(start, position), newPosition: position };
+}
+
+function toUint8Arrays(
+ blobParts: domTypes.BlobPart[],
+ doNormalizeLineEndingsToNative: boolean
+): Uint8Array[] {
+ const ret: Uint8Array[] = [];
+ const enc = new TextEncoder();
+ for (const element of blobParts) {
+ if (typeof element === "string") {
+ let str = element;
+ if (doNormalizeLineEndingsToNative) {
+ str = convertLineEndingsToNative(element);
+ }
+ ret.push(enc.encode(str));
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ } else if (element instanceof DenoBlob) {
+ ret.push(element[bytesSymbol]);
+ } else if (element instanceof Uint8Array) {
+ ret.push(element);
+ } else if (element instanceof Uint16Array) {
+ const uint8 = new Uint8Array(element.buffer);
+ ret.push(uint8);
+ } else if (element instanceof Uint32Array) {
+ const uint8 = new Uint8Array(element.buffer);
+ ret.push(uint8);
+ } else if (ArrayBuffer.isView(element)) {
+ // Convert view to Uint8Array.
+ const uint8 = new Uint8Array(element.buffer);
+ ret.push(uint8);
+ } else if (element instanceof ArrayBuffer) {
+ // Create a new Uint8Array view for the given ArrayBuffer.
+ const uint8 = new Uint8Array(element);
+ ret.push(uint8);
+ } else {
+ ret.push(enc.encode(String(element)));
+ }
+ }
+ return ret;
+}
+
+function processBlobParts(
+ blobParts: domTypes.BlobPart[],
+ options: domTypes.BlobPropertyBag
+): Uint8Array {
+ const normalizeLineEndingsToNative = options.ending === "native";
+ // ArrayBuffer.transfer is not yet implemented in V8, so we just have to
+ // pre compute size of the array buffer and do some sort of static allocation
+ // instead of dynamic allocation.
+ const uint8Arrays = toUint8Arrays(blobParts, normalizeLineEndingsToNative);
+ const byteLength = uint8Arrays
+ .map((u8): number => u8.byteLength)
+ .reduce((a, b): number => a + b, 0);
+ const ab = new ArrayBuffer(byteLength);
+ const bytes = new Uint8Array(ab);
+
+ let courser = 0;
+ for (const u8 of uint8Arrays) {
+ bytes.set(u8, courser);
+ courser += u8.byteLength;
+ }
+
+ return bytes;
+}
+
+// A WeakMap holding blob to byte array mapping.
+// Ensures it does not impact garbage collection.
+export const blobBytesWeakMap = new WeakMap<domTypes.Blob, Uint8Array>();
+
+export class DenoBlob implements domTypes.Blob {
+ private readonly [bytesSymbol]: Uint8Array;
+ readonly size: number = 0;
+ readonly type: string = "";
+
+ /** A blob object represents a file-like object of immutable, raw data. */
+ constructor(
+ blobParts?: domTypes.BlobPart[],
+ options?: domTypes.BlobPropertyBag
+ ) {
+ if (arguments.length === 0) {
+ this[bytesSymbol] = new Uint8Array();
+ return;
+ }
+
+ options = options || {};
+ // Set ending property's default value to "transparent".
+ if (!hasOwnProperty(options, "ending")) {
+ options.ending = "transparent";
+ }
+
+ if (options.type && !containsOnlyASCII(options.type)) {
+ const errMsg = "The 'type' property must consist of ASCII characters.";
+ throw new SyntaxError(errMsg);
+ }
+
+ const bytes = processBlobParts(blobParts!, options);
+ // Normalize options.type.
+ let type = options.type ? options.type : "";
+ if (type.length) {
+ for (let i = 0; i < type.length; ++i) {
+ const char = type[i];
+ if (char < "\u0020" || char > "\u007E") {
+ type = "";
+ break;
+ }
+ }
+ type = type.toLowerCase();
+ }
+ // Set Blob object's properties.
+ this[bytesSymbol] = bytes;
+ this.size = bytes.byteLength;
+ this.type = type;
+
+ // Register bytes for internal private use.
+ blobBytesWeakMap.set(this, bytes);
+ }
+
+ slice(start?: number, end?: number, contentType?: string): DenoBlob {
+ return new DenoBlob([this[bytesSymbol].slice(start, end)], {
+ type: contentType || this.type
+ });
+ }
+}
diff --git a/cli/js/blob_test.ts b/cli/js/blob_test.ts
new file mode 100644
index 000000000..afa1182a9
--- /dev/null
+++ b/cli/js/blob_test.ts
@@ -0,0 +1,62 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEquals } from "./test_util.ts";
+
+test(function blobString(): void {
+ const b1 = new Blob(["Hello World"]);
+ const str = "Test";
+ const b2 = new Blob([b1, str]);
+ assertEquals(b2.size, b1.size + str.length);
+});
+
+test(function blobBuffer(): void {
+ const buffer = new ArrayBuffer(12);
+ const u8 = new Uint8Array(buffer);
+ const f1 = new Float32Array(buffer);
+ const b1 = new Blob([buffer, u8]);
+ assertEquals(b1.size, 2 * u8.length);
+ const b2 = new Blob([b1, f1]);
+ assertEquals(b2.size, 3 * u8.length);
+});
+
+test(function blobSlice(): void {
+ const blob = new Blob(["Deno", "Foo"]);
+ const b1 = blob.slice(0, 3, "Text/HTML");
+ assert(b1 instanceof Blob);
+ assertEquals(b1.size, 3);
+ assertEquals(b1.type, "text/html");
+ const b2 = blob.slice(-1, 3);
+ assertEquals(b2.size, 0);
+ const b3 = blob.slice(100, 3);
+ assertEquals(b3.size, 0);
+ const b4 = blob.slice(0, 10);
+ assertEquals(b4.size, blob.size);
+});
+
+test(function blobShouldNotThrowError(): void {
+ let hasThrown = false;
+
+ try {
+ const options1: object = {
+ ending: "utf8",
+ hasOwnProperty: "hasOwnProperty"
+ };
+ const options2: object = Object.create(null);
+ new Blob(["Hello World"], options1);
+ new Blob(["Hello World"], options2);
+ } catch {
+ hasThrown = true;
+ }
+
+ assertEquals(hasThrown, false);
+});
+
+test(function nativeEndLine(): void {
+ const options: object = {
+ ending: "native"
+ };
+ const blob = new Blob(["Hello\nWorld"], options);
+
+ assertEquals(blob.size, Deno.build.os === "win" ? 12 : 11);
+});
+
+// TODO(qti3e) Test the stored data in a Blob after implementing FileReader API.
diff --git a/cli/js/body.ts b/cli/js/body.ts
new file mode 100644
index 000000000..6567b1934
--- /dev/null
+++ b/cli/js/body.ts
@@ -0,0 +1,272 @@
+import * as formData from "./form_data.ts";
+import * as blob from "./blob.ts";
+import * as encoding from "./text_encoding.ts";
+import * as headers from "./headers.ts";
+import * as domTypes from "./dom_types.ts";
+
+const { Headers } = headers;
+
+// only namespace imports work for now, plucking out what we need
+const { FormData } = formData;
+const { TextEncoder, TextDecoder } = encoding;
+const Blob = blob.DenoBlob;
+const DenoBlob = blob.DenoBlob;
+
+export type BodySource =
+ | domTypes.Blob
+ | domTypes.BufferSource
+ | domTypes.FormData
+ | domTypes.URLSearchParams
+ | domTypes.ReadableStream
+ | string;
+
+function validateBodyType(owner: Body, bodySource: BodySource): boolean {
+ if (
+ bodySource instanceof Int8Array ||
+ bodySource instanceof Int16Array ||
+ bodySource instanceof Int32Array ||
+ bodySource instanceof Uint8Array ||
+ bodySource instanceof Uint16Array ||
+ bodySource instanceof Uint32Array ||
+ bodySource instanceof Uint8ClampedArray ||
+ bodySource instanceof Float32Array ||
+ bodySource instanceof Float64Array
+ ) {
+ return true;
+ } else if (bodySource instanceof ArrayBuffer) {
+ return true;
+ } else if (typeof bodySource === "string") {
+ return true;
+ } else if (bodySource instanceof FormData) {
+ return true;
+ } else if (!bodySource) {
+ return true; // null body is fine
+ }
+ throw new Error(
+ `Bad ${owner.constructor.name} body type: ${bodySource.constructor.name}`
+ );
+}
+
+function getHeaderValueParams(value: string): Map<string, string> {
+ const params = new Map();
+ // Forced to do so for some Map constructor param mismatch
+ value
+ .split(";")
+ .slice(1)
+ .map((s): string[] => s.trim().split("="))
+ .filter((arr): boolean => arr.length > 1)
+ .map(([k, v]): [string, string] => [k, v.replace(/^"([^"]*)"$/, "$1")])
+ .forEach(([k, v]): Map<string, string> => params.set(k, v));
+ return params;
+}
+
+function hasHeaderValueOf(s: string, value: string): boolean {
+ return new RegExp(`^${value}[\t\s]*;?`).test(s);
+}
+
+export const BodyUsedError =
+ "Failed to execute 'clone' on 'Body': body is already used";
+
+export class Body implements domTypes.Body {
+ protected _stream: domTypes.ReadableStream | null;
+
+ constructor(protected _bodySource: BodySource, readonly contentType: string) {
+ validateBodyType(this, _bodySource);
+ this._bodySource = _bodySource;
+ this.contentType = contentType;
+ this._stream = null;
+ }
+
+ get body(): domTypes.ReadableStream | null {
+ if (this._stream) {
+ return this._stream;
+ }
+ if (typeof this._bodySource === "string") {
+ throw Error("not implemented");
+ }
+ return this._stream;
+ }
+
+ get bodyUsed(): boolean {
+ if (this.body && this.body.locked) {
+ return true;
+ }
+ return false;
+ }
+
+ public async blob(): Promise<domTypes.Blob> {
+ return new Blob([await this.arrayBuffer()]);
+ }
+
+ // ref: https://fetch.spec.whatwg.org/#body-mixin
+ public async formData(): Promise<domTypes.FormData> {
+ const formData = new FormData();
+ const enc = new TextEncoder();
+ if (hasHeaderValueOf(this.contentType, "multipart/form-data")) {
+ const params = getHeaderValueParams(this.contentType);
+ if (!params.has("boundary")) {
+ // TypeError is required by spec
+ throw new TypeError("multipart/form-data must provide a boundary");
+ }
+ // ref: https://tools.ietf.org/html/rfc2046#section-5.1
+ const boundary = params.get("boundary")!;
+ const dashBoundary = `--${boundary}`;
+ const delimiter = `\r\n${dashBoundary}`;
+ const closeDelimiter = `${delimiter}--`;
+
+ const body = await this.text();
+ let bodyParts: string[];
+ const bodyEpilogueSplit = body.split(closeDelimiter);
+ if (bodyEpilogueSplit.length < 2) {
+ bodyParts = [];
+ } else {
+ // discard epilogue
+ const bodyEpilogueTrimmed = bodyEpilogueSplit[0];
+ // first boundary treated special due to optional prefixed \r\n
+ const firstBoundaryIndex = bodyEpilogueTrimmed.indexOf(dashBoundary);
+ if (firstBoundaryIndex < 0) {
+ throw new TypeError("Invalid boundary");
+ }
+ const bodyPreambleTrimmed = bodyEpilogueTrimmed
+ .slice(firstBoundaryIndex + dashBoundary.length)
+ .replace(/^[\s\r\n\t]+/, ""); // remove transport-padding CRLF
+ // trimStart might not be available
+ // Be careful! body-part allows trailing \r\n!
+ // (as long as it is not part of `delimiter`)
+ bodyParts = bodyPreambleTrimmed
+ .split(delimiter)
+ .map((s): string => s.replace(/^[\s\r\n\t]+/, ""));
+ // TODO: LWSP definition is actually trickier,
+ // but should be fine in our case since without headers
+ // we should just discard the part
+ }
+ for (const bodyPart of bodyParts) {
+ const headers = new Headers();
+ const headerOctetSeperatorIndex = bodyPart.indexOf("\r\n\r\n");
+ if (headerOctetSeperatorIndex < 0) {
+ continue; // Skip unknown part
+ }
+ const headerText = bodyPart.slice(0, headerOctetSeperatorIndex);
+ const octets = bodyPart.slice(headerOctetSeperatorIndex + 4);
+
+ // TODO: use textproto.readMIMEHeader from deno_std
+ const rawHeaders = headerText.split("\r\n");
+ for (const rawHeader of rawHeaders) {
+ const sepIndex = rawHeader.indexOf(":");
+ if (sepIndex < 0) {
+ continue; // Skip this header
+ }
+ const key = rawHeader.slice(0, sepIndex);
+ const value = rawHeader.slice(sepIndex + 1);
+ headers.set(key, value);
+ }
+ if (!headers.has("content-disposition")) {
+ continue; // Skip unknown part
+ }
+ // Content-Transfer-Encoding Deprecated
+ const contentDisposition = headers.get("content-disposition")!;
+ const partContentType = headers.get("content-type") || "text/plain";
+ // TODO: custom charset encoding (needs TextEncoder support)
+ // const contentTypeCharset =
+ // getHeaderValueParams(partContentType).get("charset") || "";
+ if (!hasHeaderValueOf(contentDisposition, "form-data")) {
+ continue; // Skip, might not be form-data
+ }
+ const dispositionParams = getHeaderValueParams(contentDisposition);
+ if (!dispositionParams.has("name")) {
+ continue; // Skip, unknown name
+ }
+ const dispositionName = dispositionParams.get("name")!;
+ if (dispositionParams.has("filename")) {
+ const filename = dispositionParams.get("filename")!;
+ const blob = new DenoBlob([enc.encode(octets)], {
+ type: partContentType
+ });
+ // TODO: based on spec
+ // https://xhr.spec.whatwg.org/#dom-formdata-append
+ // https://xhr.spec.whatwg.org/#create-an-entry
+ // Currently it does not mention how I could pass content-type
+ // to the internally created file object...
+ formData.append(dispositionName, blob, filename);
+ } else {
+ formData.append(dispositionName, octets);
+ }
+ }
+ return formData;
+ } else if (
+ hasHeaderValueOf(this.contentType, "application/x-www-form-urlencoded")
+ ) {
+ // From https://github.com/github/fetch/blob/master/fetch.js
+ // Copyright (c) 2014-2016 GitHub, Inc. MIT License
+ const body = await this.text();
+ try {
+ body
+ .trim()
+ .split("&")
+ .forEach(
+ (bytes): void => {
+ if (bytes) {
+ const split = bytes.split("=");
+ const name = split.shift()!.replace(/\+/g, " ");
+ const value = split.join("=").replace(/\+/g, " ");
+ formData.append(
+ decodeURIComponent(name),
+ decodeURIComponent(value)
+ );
+ }
+ }
+ );
+ } catch (e) {
+ throw new TypeError("Invalid form urlencoded format");
+ }
+ return formData;
+ } else {
+ throw new TypeError("Invalid form data");
+ }
+ }
+
+ public async text(): Promise<string> {
+ if (typeof this._bodySource === "string") {
+ return this._bodySource;
+ }
+
+ const ab = await this.arrayBuffer();
+ const decoder = new TextDecoder("utf-8");
+ return decoder.decode(ab);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public async json(): Promise<any> {
+ const raw = await this.text();
+ return JSON.parse(raw);
+ }
+
+ public async arrayBuffer(): Promise<ArrayBuffer> {
+ if (
+ this._bodySource instanceof Int8Array ||
+ this._bodySource instanceof Int16Array ||
+ this._bodySource instanceof Int32Array ||
+ this._bodySource instanceof Uint8Array ||
+ this._bodySource instanceof Uint16Array ||
+ this._bodySource instanceof Uint32Array ||
+ this._bodySource instanceof Uint8ClampedArray ||
+ this._bodySource instanceof Float32Array ||
+ this._bodySource instanceof Float64Array
+ ) {
+ return this._bodySource.buffer as ArrayBuffer;
+ } else if (this._bodySource instanceof ArrayBuffer) {
+ return this._bodySource;
+ } else if (typeof this._bodySource === "string") {
+ const enc = new TextEncoder();
+ return enc.encode(this._bodySource).buffer as ArrayBuffer;
+ } else if (this._bodySource instanceof FormData) {
+ const enc = new TextEncoder();
+ return enc.encode(this._bodySource.toString()).buffer as ArrayBuffer;
+ } else if (!this._bodySource) {
+ return new ArrayBuffer(0);
+ }
+ throw new Error(
+ `Body type not yet implemented: ${this._bodySource.constructor.name}`
+ );
+ }
+}
diff --git a/cli/js/body_test.ts b/cli/js/body_test.ts
new file mode 100644
index 000000000..ec76e9072
--- /dev/null
+++ b/cli/js/body_test.ts
@@ -0,0 +1,68 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, testPerm, assertEquals, assert } from "./test_util.ts";
+
+// just a hack to get a body object
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function buildBody(body: any): Body {
+ const stub = new Request("", {
+ body: body
+ });
+ return stub as Body;
+}
+
+const intArrays = [
+ Int8Array,
+ Int16Array,
+ Int32Array,
+ Uint8Array,
+ Uint16Array,
+ Uint32Array,
+ Uint8ClampedArray,
+ Float32Array,
+ Float64Array
+];
+test(async function arrayBufferFromByteArrays(): Promise<void> {
+ const buffer = new TextEncoder().encode("ahoyhoy8").buffer;
+
+ for (const type of intArrays) {
+ const body = buildBody(new type(buffer));
+ const text = new TextDecoder("utf-8").decode(await body.arrayBuffer());
+ assertEquals(text, "ahoyhoy8");
+ }
+});
+
+//FormData
+testPerm({ net: true }, async function bodyMultipartFormData(): Promise<void> {
+ const response = await fetch(
+ "http://localhost:4545/tests/subdir/multipart_form_data.txt"
+ );
+ const text = await response.text();
+
+ const body = buildBody(text);
+
+ // @ts-ignore
+ body.contentType = "multipart/form-data;boundary=boundary";
+
+ const formData = await body.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1").toString(), "value_1 \r\n");
+ assert(formData.has("field_2"));
+});
+
+testPerm({ net: true }, async function bodyURLEncodedFormData(): Promise<void> {
+ const response = await fetch(
+ "http://localhost:4545/tests/subdir/form_urlencoded.txt"
+ );
+ const text = await response.text();
+
+ const body = buildBody(text);
+
+ // @ts-ignore
+ body.contentType = "application/x-www-form-urlencoded";
+
+ const formData = await body.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1").toString(), "Hi");
+ assert(formData.has("field_2"));
+ assertEquals(formData.get("field_2").toString(), "<Deno>");
+});
diff --git a/cli/js/buffer.ts b/cli/js/buffer.ts
new file mode 100644
index 000000000..dc73b7e60
--- /dev/null
+++ b/cli/js/buffer.ts
@@ -0,0 +1,294 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+// This code has been ported almost directly from Go's src/bytes/buffer.go
+// Copyright 2009 The Go Authors. All rights reserved. BSD license.
+// https://github.com/golang/go/blob/master/LICENSE
+
+import { Reader, Writer, EOF, SyncReader, SyncWriter } from "./io.ts";
+import { assert } from "./util.ts";
+import { TextDecoder } from "./text_encoding.ts";
+import { DenoError, ErrorKind } from "./errors.ts";
+
+// MIN_READ is the minimum ArrayBuffer size passed to a read call by
+// buffer.ReadFrom. As long as the Buffer has at least MIN_READ bytes beyond
+// what is required to hold the contents of r, readFrom() will not grow the
+// underlying buffer.
+const MIN_READ = 512;
+const MAX_SIZE = 2 ** 32 - 2;
+
+// `off` is the offset into `dst` where it will at which to begin writing values
+// from `src`.
+// Returns the number of bytes copied.
+function copyBytes(dst: Uint8Array, src: Uint8Array, off = 0): number {
+ const r = dst.byteLength - off;
+ if (src.byteLength > r) {
+ src = src.subarray(0, r);
+ }
+ dst.set(src, off);
+ return src.byteLength;
+}
+
+/** A Buffer is a variable-sized buffer of bytes with read() and write()
+ * methods. Based on https://golang.org/pkg/bytes/#Buffer
+ */
+export class Buffer implements Reader, SyncReader, Writer, SyncWriter {
+ private buf: Uint8Array; // contents are the bytes buf[off : len(buf)]
+ private off = 0; // read at buf[off], write at buf[buf.byteLength]
+
+ constructor(ab?: ArrayBuffer) {
+ if (ab == null) {
+ this.buf = new Uint8Array(0);
+ return;
+ }
+
+ this.buf = new Uint8Array(ab);
+ }
+
+ /** bytes() returns a slice holding the unread portion of the buffer.
+ * The slice is valid for use only until the next buffer modification (that
+ * is, only until the next call to a method like read(), write(), reset(), or
+ * truncate()). The slice aliases the buffer content at least until the next
+ * buffer modification, so immediate changes to the slice will affect the
+ * result of future reads.
+ */
+ bytes(): Uint8Array {
+ return this.buf.subarray(this.off);
+ }
+
+ /** toString() returns the contents of the unread portion of the buffer
+ * as a string. Warning - if multibyte characters are present when data is
+ * flowing through the buffer, this method may result in incorrect strings
+ * due to a character being split.
+ */
+ toString(): string {
+ const decoder = new TextDecoder();
+ return decoder.decode(this.buf.subarray(this.off));
+ }
+
+ /** empty() returns whether the unread portion of the buffer is empty. */
+ empty(): boolean {
+ return this.buf.byteLength <= this.off;
+ }
+
+ /** length is a getter that returns the number of bytes of the unread
+ * portion of the buffer
+ */
+ get length(): number {
+ return this.buf.byteLength - this.off;
+ }
+
+ /** Returns the capacity of the buffer's underlying byte slice, that is,
+ * the total space allocated for the buffer's data.
+ */
+ get capacity(): number {
+ return this.buf.buffer.byteLength;
+ }
+
+ /** truncate() discards all but the first n unread bytes from the buffer but
+ * continues to use the same allocated storage. It throws if n is negative or
+ * greater than the length of the buffer.
+ */
+ truncate(n: number): void {
+ if (n === 0) {
+ this.reset();
+ return;
+ }
+ if (n < 0 || n > this.length) {
+ throw Error("bytes.Buffer: truncation out of range");
+ }
+ this._reslice(this.off + n);
+ }
+
+ /** reset() resets the buffer to be empty, but it retains the underlying
+ * storage for use by future writes. reset() is the same as truncate(0)
+ */
+ reset(): void {
+ this._reslice(0);
+ this.off = 0;
+ }
+
+ /** _tryGrowByReslice() is a version of grow for the fast-case
+ * where the internal buffer only needs to be resliced. It returns the index
+ * where bytes should be written and whether it succeeded.
+ * It returns -1 if a reslice was not needed.
+ */
+ private _tryGrowByReslice(n: number): number {
+ const l = this.buf.byteLength;
+ if (n <= this.capacity - l) {
+ this._reslice(l + n);
+ return l;
+ }
+ return -1;
+ }
+
+ private _reslice(len: number): void {
+ assert(len <= this.buf.buffer.byteLength);
+ this.buf = new Uint8Array(this.buf.buffer, 0, len);
+ }
+
+ /** readSync() reads the next len(p) bytes from the buffer or until the buffer
+ * is drained. The return value n is the number of bytes read. If the
+ * buffer has no data to return, eof in the response will be true.
+ */
+ readSync(p: Uint8Array): number | EOF {
+ if (this.empty()) {
+ // Buffer is empty, reset to recover space.
+ this.reset();
+ if (p.byteLength === 0) {
+ // this edge case is tested in 'bufferReadEmptyAtEOF' test
+ return 0;
+ }
+ return EOF;
+ }
+ const nread = copyBytes(p, this.buf.subarray(this.off));
+ this.off += nread;
+ return nread;
+ }
+
+ async read(p: Uint8Array): Promise<number | EOF> {
+ const rr = this.readSync(p);
+ return Promise.resolve(rr);
+ }
+
+ writeSync(p: Uint8Array): number {
+ const m = this._grow(p.byteLength);
+ return copyBytes(this.buf, p, m);
+ }
+
+ async write(p: Uint8Array): Promise<number> {
+ const n = this.writeSync(p);
+ return Promise.resolve(n);
+ }
+
+ /** _grow() grows the buffer to guarantee space for n more bytes.
+ * It returns the index where bytes should be written.
+ * If the buffer can't grow it will throw with ErrTooLarge.
+ */
+ private _grow(n: number): number {
+ const m = this.length;
+ // If buffer is empty, reset to recover space.
+ if (m === 0 && this.off !== 0) {
+ this.reset();
+ }
+ // Fast: Try to grow by means of a reslice.
+ const i = this._tryGrowByReslice(n);
+ if (i >= 0) {
+ return i;
+ }
+ const c = this.capacity;
+ if (n <= Math.floor(c / 2) - m) {
+ // We can slide things down instead of allocating a new
+ // ArrayBuffer. We only need m+n <= c to slide, but
+ // we instead let capacity get twice as large so we
+ // don't spend all our time copying.
+ copyBytes(this.buf, this.buf.subarray(this.off));
+ } else if (c > MAX_SIZE - c - n) {
+ throw new DenoError(
+ ErrorKind.TooLarge,
+ "The buffer cannot be grown beyond the maximum size."
+ );
+ } else {
+ // Not enough space anywhere, we need to allocate.
+ const buf = new Uint8Array(2 * c + n);
+ copyBytes(buf, this.buf.subarray(this.off));
+ this.buf = buf;
+ }
+ // Restore this.off and len(this.buf).
+ this.off = 0;
+ this._reslice(m + n);
+ return m;
+ }
+
+ /** grow() grows the buffer's capacity, if necessary, to guarantee space for
+ * another n bytes. After grow(n), at least n bytes can be written to the
+ * buffer without another allocation. If n is negative, grow() will panic. If
+ * the buffer can't grow it will throw ErrTooLarge.
+ * Based on https://golang.org/pkg/bytes/#Buffer.Grow
+ */
+ grow(n: number): void {
+ if (n < 0) {
+ throw Error("Buffer.grow: negative count");
+ }
+ const m = this._grow(n);
+ this._reslice(m);
+ }
+
+ /** readFrom() reads data from r until EOF and appends it to the buffer,
+ * growing the buffer as needed. It returns the number of bytes read. If the
+ * buffer becomes too large, readFrom will panic with ErrTooLarge.
+ * Based on https://golang.org/pkg/bytes/#Buffer.ReadFrom
+ */
+ async readFrom(r: Reader): Promise<number> {
+ let n = 0;
+ while (true) {
+ try {
+ const i = this._grow(MIN_READ);
+ this._reslice(i);
+ const fub = new Uint8Array(this.buf.buffer, i);
+ const nread = await r.read(fub);
+ if (nread === EOF) {
+ return n;
+ }
+ this._reslice(i + nread);
+ n += nread;
+ } catch (e) {
+ return n;
+ }
+ }
+ }
+
+ /** Sync version of `readFrom`
+ */
+ readFromSync(r: SyncReader): number {
+ let n = 0;
+ while (true) {
+ try {
+ const i = this._grow(MIN_READ);
+ this._reslice(i);
+ const fub = new Uint8Array(this.buf.buffer, i);
+ const nread = r.readSync(fub);
+ if (nread === EOF) {
+ return n;
+ }
+ this._reslice(i + nread);
+ n += nread;
+ } catch (e) {
+ return n;
+ }
+ }
+ }
+}
+
+/** Read `r` until EOF and return the content as `Uint8Array`.
+ */
+export async function readAll(r: Reader): Promise<Uint8Array> {
+ const buf = new Buffer();
+ await buf.readFrom(r);
+ return buf.bytes();
+}
+
+/** Read synchronously `r` until EOF and return the content as `Uint8Array`.
+ */
+export function readAllSync(r: SyncReader): Uint8Array {
+ const buf = new Buffer();
+ buf.readFromSync(r);
+ return buf.bytes();
+}
+
+/** Write all the content of `arr` to `w`.
+ */
+export async function writeAll(w: Writer, arr: Uint8Array): Promise<void> {
+ let nwritten = 0;
+ while (nwritten < arr.length) {
+ nwritten += await w.write(arr.subarray(nwritten));
+ }
+}
+
+/** Write synchronously all the content of `arr` to `w`.
+ */
+export function writeAllSync(w: SyncWriter, arr: Uint8Array): void {
+ let nwritten = 0;
+ while (nwritten < arr.length) {
+ nwritten += w.writeSync(arr.subarray(nwritten));
+ }
+}
diff --git a/cli/js/buffer_test.ts b/cli/js/buffer_test.ts
new file mode 100644
index 000000000..a157b927e
--- /dev/null
+++ b/cli/js/buffer_test.ts
@@ -0,0 +1,277 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+// This code has been ported almost directly from Go's src/bytes/buffer_test.go
+// Copyright 2009 The Go Authors. All rights reserved. BSD license.
+// https://github.com/golang/go/blob/master/LICENSE
+import { assertEquals, test } from "./test_util.ts";
+
+const { Buffer, readAll, readAllSync, writeAll, writeAllSync } = Deno;
+type Buffer = Deno.Buffer;
+
+// N controls how many iterations of certain checks are performed.
+const N = 100;
+let testBytes: Uint8Array | null;
+let testString: string | null;
+
+function init(): void {
+ if (testBytes == null) {
+ testBytes = new Uint8Array(N);
+ for (let i = 0; i < N; i++) {
+ testBytes[i] = "a".charCodeAt(0) + (i % 26);
+ }
+ const decoder = new TextDecoder();
+ testString = decoder.decode(testBytes);
+ }
+}
+
+function check(buf: Deno.Buffer, s: string): void {
+ const bytes = buf.bytes();
+ assertEquals(buf.length, bytes.byteLength);
+ const decoder = new TextDecoder();
+ const bytesStr = decoder.decode(bytes);
+ assertEquals(bytesStr, s);
+ assertEquals(buf.length, buf.toString().length);
+ assertEquals(buf.length, s.length);
+}
+
+// Fill buf through n writes of byte slice fub.
+// The initial contents of buf corresponds to the string s;
+// the result is the final contents of buf returned as a string.
+async function fillBytes(
+ buf: Buffer,
+ s: string,
+ n: number,
+ fub: Uint8Array
+): Promise<string> {
+ check(buf, s);
+ for (; n > 0; n--) {
+ const m = await buf.write(fub);
+ assertEquals(m, fub.byteLength);
+ const decoder = new TextDecoder();
+ s += decoder.decode(fub);
+ check(buf, s);
+ }
+ return s;
+}
+
+// Empty buf through repeated reads into fub.
+// The initial contents of buf corresponds to the string s.
+async function empty(buf: Buffer, s: string, fub: Uint8Array): Promise<void> {
+ check(buf, s);
+ while (true) {
+ const r = await buf.read(fub);
+ if (r === Deno.EOF) {
+ break;
+ }
+ s = s.slice(r);
+ check(buf, s);
+ }
+ check(buf, "");
+}
+
+function repeat(c: string, bytes: number): Uint8Array {
+ assertEquals(c.length, 1);
+ const ui8 = new Uint8Array(bytes);
+ ui8.fill(c.charCodeAt(0));
+ return ui8;
+}
+
+test(function bufferNewBuffer(): void {
+ init();
+ const buf = new Buffer(testBytes.buffer as ArrayBuffer);
+ check(buf, testString);
+});
+
+test(async function bufferBasicOperations(): Promise<void> {
+ init();
+ const buf = new Buffer();
+ for (let i = 0; i < 5; i++) {
+ check(buf, "");
+
+ buf.reset();
+ check(buf, "");
+
+ buf.truncate(0);
+ check(buf, "");
+
+ let n = await buf.write(testBytes.subarray(0, 1));
+ assertEquals(n, 1);
+ check(buf, "a");
+
+ n = await buf.write(testBytes.subarray(1, 2));
+ assertEquals(n, 1);
+ check(buf, "ab");
+
+ n = await buf.write(testBytes.subarray(2, 26));
+ assertEquals(n, 24);
+ check(buf, testString.slice(0, 26));
+
+ buf.truncate(26);
+ check(buf, testString.slice(0, 26));
+
+ buf.truncate(20);
+ check(buf, testString.slice(0, 20));
+
+ await empty(buf, testString.slice(0, 20), new Uint8Array(5));
+ await empty(buf, "", new Uint8Array(100));
+
+ // TODO buf.writeByte()
+ // TODO buf.readByte()
+ }
+});
+
+test(async function bufferReadEmptyAtEOF(): Promise<void> {
+ // check that EOF of 'buf' is not reached (even though it's empty) if
+ // results are written to buffer that has 0 length (ie. it can't store any data)
+ const buf = new Buffer();
+ const zeroLengthTmp = new Uint8Array(0);
+ const result = await buf.read(zeroLengthTmp);
+ assertEquals(result, 0);
+});
+
+test(async function bufferLargeByteWrites(): Promise<void> {
+ init();
+ const buf = new Buffer();
+ const limit = 9;
+ for (let i = 3; i < limit; i += 3) {
+ const s = await fillBytes(buf, "", 5, testBytes);
+ await empty(buf, s, new Uint8Array(Math.floor(testString.length / i)));
+ }
+ check(buf, "");
+});
+
+test(async function bufferTooLargeByteWrites(): Promise<void> {
+ init();
+ const tmp = new Uint8Array(72);
+ const growLen = Number.MAX_VALUE;
+ const xBytes = repeat("x", 0);
+ const buf = new Buffer(xBytes.buffer as ArrayBuffer);
+ await buf.read(tmp);
+
+ let err;
+ try {
+ buf.grow(growLen);
+ } catch (e) {
+ err = e;
+ }
+
+ assertEquals(err.kind, Deno.ErrorKind.TooLarge);
+ assertEquals(err.name, "TooLarge");
+});
+
+test(async function bufferLargeByteReads(): Promise<void> {
+ init();
+ const buf = new Buffer();
+ for (let i = 3; i < 30; i += 3) {
+ const n = Math.floor(testBytes.byteLength / i);
+ const s = await fillBytes(buf, "", 5, testBytes.subarray(0, n));
+ await empty(buf, s, new Uint8Array(testString.length));
+ }
+ check(buf, "");
+});
+
+test(function bufferCapWithPreallocatedSlice(): void {
+ const buf = new Buffer(new ArrayBuffer(10));
+ assertEquals(buf.capacity, 10);
+});
+
+test(async function bufferReadFrom(): Promise<void> {
+ init();
+ const buf = new Buffer();
+ for (let i = 3; i < 30; i += 3) {
+ const s = await fillBytes(
+ buf,
+ "",
+ 5,
+ testBytes.subarray(0, Math.floor(testBytes.byteLength / i))
+ );
+ const b = new Buffer();
+ await b.readFrom(buf);
+ const fub = new Uint8Array(testString.length);
+ await empty(b, s, fub);
+ }
+});
+
+test(async function bufferReadFromSync(): Promise<void> {
+ init();
+ const buf = new Buffer();
+ for (let i = 3; i < 30; i += 3) {
+ const s = await fillBytes(
+ buf,
+ "",
+ 5,
+ testBytes.subarray(0, Math.floor(testBytes.byteLength / i))
+ );
+ const b = new Buffer();
+ b.readFromSync(buf);
+ const fub = new Uint8Array(testString.length);
+ await empty(b, s, fub);
+ }
+});
+
+test(async function bufferTestGrow(): Promise<void> {
+ const tmp = new Uint8Array(72);
+ for (const startLen of [0, 100, 1000, 10000, 100000]) {
+ const xBytes = repeat("x", startLen);
+ for (const growLen of [0, 100, 1000, 10000, 100000]) {
+ const buf = new Buffer(xBytes.buffer as ArrayBuffer);
+ // If we read, this affects buf.off, which is good to test.
+ const result = await buf.read(tmp);
+ const nread = result === Deno.EOF ? 0 : result;
+ buf.grow(growLen);
+ const yBytes = repeat("y", growLen);
+ await buf.write(yBytes);
+ // Check that buffer has correct data.
+ assertEquals(
+ buf.bytes().subarray(0, startLen - nread),
+ xBytes.subarray(nread)
+ );
+ assertEquals(
+ buf.bytes().subarray(startLen - nread, startLen - nread + growLen),
+ yBytes
+ );
+ }
+ }
+});
+
+test(async function testReadAll(): Promise<void> {
+ init();
+ const reader = new Buffer(testBytes.buffer as ArrayBuffer);
+ const actualBytes = await readAll(reader);
+ assertEquals(testBytes.byteLength, actualBytes.byteLength);
+ for (let i = 0; i < testBytes.length; ++i) {
+ assertEquals(testBytes[i], actualBytes[i]);
+ }
+});
+
+test(function testReadAllSync(): void {
+ init();
+ const reader = new Buffer(testBytes.buffer as ArrayBuffer);
+ const actualBytes = readAllSync(reader);
+ assertEquals(testBytes.byteLength, actualBytes.byteLength);
+ for (let i = 0; i < testBytes.length; ++i) {
+ assertEquals(testBytes[i], actualBytes[i]);
+ }
+});
+
+test(async function testWriteAll(): Promise<void> {
+ init();
+ const writer = new Buffer();
+ await writeAll(writer, testBytes);
+ const actualBytes = writer.bytes();
+ assertEquals(testBytes.byteLength, actualBytes.byteLength);
+ for (let i = 0; i < testBytes.length; ++i) {
+ assertEquals(testBytes[i], actualBytes[i]);
+ }
+});
+
+test(function testWriteAllSync(): void {
+ init();
+ const writer = new Buffer();
+ writeAllSync(writer, testBytes);
+ const actualBytes = writer.bytes();
+ assertEquals(testBytes.byteLength, actualBytes.byteLength);
+ for (let i = 0; i < testBytes.length; ++i) {
+ assertEquals(testBytes[i], actualBytes[i]);
+ }
+});
diff --git a/cli/js/build.ts b/cli/js/build.ts
new file mode 100644
index 000000000..942f57458
--- /dev/null
+++ b/cli/js/build.ts
@@ -0,0 +1,27 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+export type OperatingSystem = "mac" | "win" | "linux";
+
+export type Arch = "x64" | "arm64";
+
+// Do not add unsupported platforms.
+/** Build related information */
+export interface BuildInfo {
+ /** The CPU architecture. */
+ arch: Arch;
+
+ /** The operating system. */
+ os: OperatingSystem;
+}
+
+export const build: BuildInfo = {
+ arch: "" as Arch,
+ os: "" as OperatingSystem
+};
+
+export function setBuildInfo(os: OperatingSystem, arch: Arch): void {
+ build.os = os;
+ build.arch = arch;
+
+ Object.freeze(build);
+}
diff --git a/cli/js/build_test.ts b/cli/js/build_test.ts
new file mode 100644
index 000000000..4423de338
--- /dev/null
+++ b/cli/js/build_test.ts
@@ -0,0 +1,10 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert } from "./test_util.ts";
+
+test(function buildInfo(): void {
+ // Deno.build is injected by rollup at compile time. Here
+ // we check it has been properly transformed.
+ const { arch, os } = Deno.build;
+ assert(arch === "x64");
+ assert(os === "mac" || os === "win" || os === "linux");
+});
diff --git a/cli/js/chmod.ts b/cli/js/chmod.ts
new file mode 100644
index 000000000..7bf54cc5b
--- /dev/null
+++ b/cli/js/chmod.ts
@@ -0,0 +1,20 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+/** Changes the permission of a specific file/directory of specified path
+ * synchronously.
+ *
+ * Deno.chmodSync("/path/to/file", 0o666);
+ */
+export function chmodSync(path: string, mode: number): void {
+ sendSync(dispatch.OP_CHMOD, { path, mode });
+}
+
+/** Changes the permission of a specific file/directory of specified path.
+ *
+ * await Deno.chmod("/path/to/file", 0o666);
+ */
+export async function chmod(path: string, mode: number): Promise<void> {
+ await sendAsync(dispatch.OP_CHMOD, { path, mode });
+}
diff --git a/cli/js/chmod_test.ts b/cli/js/chmod_test.ts
new file mode 100644
index 000000000..420f4f313
--- /dev/null
+++ b/cli/js/chmod_test.ts
@@ -0,0 +1,142 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assertEquals } from "./test_util.ts";
+
+const isNotWindows = Deno.build.os !== "win";
+
+testPerm({ read: true, write: true }, function chmodSyncSuccess(): void {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "/test.txt";
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+
+ // On windows no effect, but should not crash
+ Deno.chmodSync(filename, 0o777);
+
+ // Check success when not on windows
+ if (isNotWindows) {
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+ }
+});
+
+// Check symlink when not on windows
+if (isNotWindows) {
+ testPerm(
+ { read: true, write: true },
+ function chmodSyncSymlinkSuccess(): void {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+
+ const filename = tempDir + "/test.txt";
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+ const symlinkName = tempDir + "/test_symlink.txt";
+ Deno.symlinkSync(filename, symlinkName);
+
+ let symlinkInfo = Deno.lstatSync(symlinkName);
+ const symlinkMode = symlinkInfo.mode & 0o777; // platform dependent
+
+ Deno.chmodSync(symlinkName, 0o777);
+
+ // Change actual file mode, not symlink
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+ symlinkInfo = Deno.lstatSync(symlinkName);
+ assertEquals(symlinkInfo.mode & 0o777, symlinkMode);
+ }
+ );
+}
+
+testPerm({ write: true }, function chmodSyncFailure(): void {
+ let err;
+ try {
+ const filename = "/badfile.txt";
+ Deno.chmodSync(filename, 0o777);
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: false }, function chmodSyncPerm(): void {
+ let err;
+ try {
+ Deno.chmodSync("/somefile.txt", 0o777);
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ read: true, write: true }, async function chmodSuccess(): Promise<
+ void
+> {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "/test.txt";
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+
+ // On windows no effect, but should not crash
+ await Deno.chmod(filename, 0o777);
+
+ // Check success when not on windows
+ if (isNotWindows) {
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+ }
+});
+
+// Check symlink when not on windows
+if (isNotWindows) {
+ testPerm(
+ { read: true, write: true },
+ async function chmodSymlinkSuccess(): Promise<void> {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const tempDir = Deno.makeTempDirSync();
+
+ const filename = tempDir + "/test.txt";
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+ const symlinkName = tempDir + "/test_symlink.txt";
+ Deno.symlinkSync(filename, symlinkName);
+
+ let symlinkInfo = Deno.lstatSync(symlinkName);
+ const symlinkMode = symlinkInfo.mode & 0o777; // platform dependent
+
+ await Deno.chmod(symlinkName, 0o777);
+
+ // Just change actual file mode, not symlink
+ const fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.mode & 0o777, 0o777);
+ symlinkInfo = Deno.lstatSync(symlinkName);
+ assertEquals(symlinkInfo.mode & 0o777, symlinkMode);
+ }
+ );
+}
+
+testPerm({ write: true }, async function chmodFailure(): Promise<void> {
+ let err;
+ try {
+ const filename = "/badfile.txt";
+ await Deno.chmod(filename, 0o777);
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: false }, async function chmodPerm(): Promise<void> {
+ let err;
+ try {
+ await Deno.chmod("/somefile.txt", 0o777);
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
diff --git a/cli/js/chown.ts b/cli/js/chown.ts
new file mode 100644
index 000000000..a8bad1193
--- /dev/null
+++ b/cli/js/chown.ts
@@ -0,0 +1,27 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+/**
+ * Change owner of a regular file or directory synchronously. Unix only at the moment.
+ * @param path path to the file
+ * @param uid user id of the new owner
+ * @param gid group id of the new owner
+ */
+export function chownSync(path: string, uid: number, gid: number): void {
+ sendSync(dispatch.OP_CHOWN, { path, uid, gid });
+}
+
+/**
+ * Change owner of a regular file or directory asynchronously. Unix only at the moment.
+ * @param path path to the file
+ * @param uid user id of the new owner
+ * @param gid group id of the new owner
+ */
+export async function chown(
+ path: string,
+ uid: number,
+ gid: number
+): Promise<void> {
+ await sendAsync(dispatch.OP_CHOWN, { path, uid, gid });
+}
diff --git a/cli/js/chown_test.ts b/cli/js/chown_test.ts
new file mode 100644
index 000000000..84106d545
--- /dev/null
+++ b/cli/js/chown_test.ts
@@ -0,0 +1,145 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assertEquals } from "./test_util.ts";
+
+// chown on Windows is noop for now, so ignore its testing on Windows
+if (Deno.build.os !== "win") {
+ async function getUidAndGid(): Promise<{ uid: number; gid: number }> {
+ // get the user ID and group ID of the current process
+ const uidProc = Deno.run({
+ stdout: "piped",
+ args: ["python", "-c", "import os; print(os.getuid())"]
+ });
+ const gidProc = Deno.run({
+ stdout: "piped",
+ args: ["python", "-c", "import os; print(os.getgid())"]
+ });
+
+ assertEquals((await uidProc.status()).code, 0);
+ assertEquals((await gidProc.status()).code, 0);
+ const uid = parseInt(
+ new TextDecoder("utf-8").decode(await uidProc.output())
+ );
+ const gid = parseInt(
+ new TextDecoder("utf-8").decode(await gidProc.output())
+ );
+
+ return { uid, gid };
+ }
+
+ testPerm({}, async function chownNoWritePermission(): Promise<void> {
+ const filePath = "chown_test_file.txt";
+ try {
+ await Deno.chown(filePath, 1000, 1000);
+ } catch (e) {
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ });
+
+ testPerm(
+ { run: true, write: true },
+ async function chownSyncFileNotExist(): Promise<void> {
+ const { uid, gid } = await getUidAndGid();
+ const filePath = Deno.makeTempDirSync() + "/chown_test_file.txt";
+
+ try {
+ Deno.chownSync(filePath, uid, gid);
+ } catch (e) {
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ assertEquals(e.name, "NotFound");
+ }
+ }
+ );
+
+ testPerm(
+ { run: true, write: true },
+ async function chownFileNotExist(): Promise<void> {
+ const { uid, gid } = await getUidAndGid();
+ const filePath = (await Deno.makeTempDir()) + "/chown_test_file.txt";
+
+ try {
+ await Deno.chown(filePath, uid, gid);
+ } catch (e) {
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ assertEquals(e.name, "NotFound");
+ }
+ }
+ );
+
+ testPerm({ write: true }, function chownSyncPermissionDenied(): void {
+ const enc = new TextEncoder();
+ const dirPath = Deno.makeTempDirSync();
+ const filePath = dirPath + "/chown_test_file.txt";
+ const fileData = enc.encode("Hello");
+ Deno.writeFileSync(filePath, fileData);
+
+ try {
+ // try changing the file's owner to root
+ Deno.chownSync(filePath, 0, 0);
+ } catch (e) {
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ Deno.removeSync(dirPath, { recursive: true });
+ });
+
+ testPerm({ write: true }, async function chownPermissionDenied(): Promise<
+ void
+ > {
+ const enc = new TextEncoder();
+ const dirPath = await Deno.makeTempDir();
+ const filePath = dirPath + "/chown_test_file.txt";
+ const fileData = enc.encode("Hello");
+ await Deno.writeFile(filePath, fileData);
+
+ try {
+ // try changing the file's owner to root
+ await Deno.chown(filePath, 0, 0);
+ } catch (e) {
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ await Deno.remove(dirPath, { recursive: true });
+ });
+
+ testPerm(
+ { run: true, write: true },
+ async function chownSyncSucceed(): Promise<void> {
+ // TODO: when a file's owner is actually being changed,
+ // chown only succeeds if run under priviledged user (root)
+ // The test script has no such priviledge, so need to find a better way to test this case
+ const { uid, gid } = await getUidAndGid();
+
+ const enc = new TextEncoder();
+ const dirPath = Deno.makeTempDirSync();
+ const filePath = dirPath + "/chown_test_file.txt";
+ const fileData = enc.encode("Hello");
+ Deno.writeFileSync(filePath, fileData);
+
+ // the test script creates this file with the same uid and gid,
+ // here chown is a noop so it succeeds under non-priviledged user
+ Deno.chownSync(filePath, uid, gid);
+
+ Deno.removeSync(dirPath, { recursive: true });
+ }
+ );
+
+ testPerm({ run: true, write: true }, async function chownSucceed(): Promise<
+ void
+ > {
+ // TODO: same as chownSyncSucceed
+ const { uid, gid } = await getUidAndGid();
+
+ const enc = new TextEncoder();
+ const dirPath = await Deno.makeTempDir();
+ const filePath = dirPath + "/chown_test_file.txt";
+ const fileData = enc.encode("Hello");
+ await Deno.writeFile(filePath, fileData);
+
+ // the test script creates this file with the same uid and gid,
+ // here chown is a noop so it succeeds under non-priviledged user
+ await Deno.chown(filePath, uid, gid);
+
+ Deno.removeSync(dirPath, { recursive: true });
+ });
+}
diff --git a/cli/js/colors.ts b/cli/js/colors.ts
new file mode 100644
index 000000000..9937bdb57
--- /dev/null
+++ b/cli/js/colors.ts
@@ -0,0 +1,40 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+// TODO(kitsonk) Replace with `deno_std/colors/mod.ts` when we can load modules
+// which end in `.ts`.
+
+import { noColor } from "./deno.ts";
+
+interface Code {
+ open: string;
+ close: string;
+ regexp: RegExp;
+}
+
+const enabled = !noColor;
+
+function code(open: number, close: number): Code {
+ return {
+ open: `\x1b[${open}m`,
+ close: `\x1b[${close}m`,
+ regexp: new RegExp(`\\x1b\\[${close}m`, "g")
+ };
+}
+
+function run(str: string, code: Code): string {
+ return enabled
+ ? `${code.open}${str.replace(code.regexp, code.open)}${code.close}`
+ : str;
+}
+
+export function bold(str: string): string {
+ return run(str, code(1, 22));
+}
+
+export function yellow(str: string): string {
+ return run(str, code(33, 39));
+}
+
+export function cyan(str: string): string {
+ return run(str, code(36, 39));
+}
diff --git a/cli/js/compiler.ts b/cli/js/compiler.ts
new file mode 100644
index 000000000..57e5e3a47
--- /dev/null
+++ b/cli/js/compiler.ts
@@ -0,0 +1,667 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+// TODO(ry) Combine this implementation with //deno_typescript/compiler_main.js
+
+import "./globals.ts";
+import "./ts_global.d.ts";
+
+import { bold, cyan, yellow } from "./colors.ts";
+import { Console } from "./console.ts";
+import { core } from "./core.ts";
+import { Diagnostic, fromTypeScriptDiagnostic } from "./diagnostics.ts";
+import { cwd } from "./dir.ts";
+import * as dispatch from "./dispatch.ts";
+import { sendAsync, sendSync } from "./dispatch_json.ts";
+import * as os from "./os.ts";
+import { TextEncoder } from "./text_encoding.ts";
+import { getMappedModuleName, parseTypeDirectives } from "./type_directives.ts";
+import { assert, notImplemented } from "./util.ts";
+import * as util from "./util.ts";
+import { window } from "./window.ts";
+import { postMessage, workerClose, workerMain } from "./workers.ts";
+import { writeFileSync } from "./write_file.ts";
+
+// Warning! The values in this enum are duplicated in cli/msg.rs
+// Update carefully!
+enum MediaType {
+ JavaScript = 0,
+ JSX = 1,
+ TypeScript = 2,
+ TSX = 3,
+ Json = 4,
+ Unknown = 5
+}
+
+// Startup boilerplate. This is necessary because the compiler has its own
+// snapshot. (It would be great if we could remove these things or centralize
+// them somewhere else.)
+const console = new Console(core.print);
+window.console = console;
+window.workerMain = workerMain;
+function denoMain(): void {
+ os.start(true, "TS");
+}
+window["denoMain"] = denoMain;
+
+const ASSETS = "$asset$";
+const OUT_DIR = "$deno$";
+
+/** The format of the work message payload coming from the privileged side */
+interface CompilerReq {
+ rootNames: string[];
+ bundle?: string;
+ // TODO(ry) add compiler config to this interface.
+ // options: ts.CompilerOptions;
+ configPath?: string;
+ config?: string;
+}
+
+interface ConfigureResponse {
+ ignoredOptions?: string[];
+ diagnostics?: ts.Diagnostic[];
+}
+
+/** Options that either do nothing in Deno, or would cause undesired behavior
+ * if modified. */
+const ignoredCompilerOptions: readonly string[] = [
+ "allowSyntheticDefaultImports",
+ "baseUrl",
+ "build",
+ "composite",
+ "declaration",
+ "declarationDir",
+ "declarationMap",
+ "diagnostics",
+ "downlevelIteration",
+ "emitBOM",
+ "emitDeclarationOnly",
+ "esModuleInterop",
+ "extendedDiagnostics",
+ "forceConsistentCasingInFileNames",
+ "help",
+ "importHelpers",
+ "incremental",
+ "inlineSourceMap",
+ "inlineSources",
+ "init",
+ "isolatedModules",
+ "lib",
+ "listEmittedFiles",
+ "listFiles",
+ "mapRoot",
+ "maxNodeModuleJsDepth",
+ "module",
+ "moduleResolution",
+ "newLine",
+ "noEmit",
+ "noEmitHelpers",
+ "noEmitOnError",
+ "noLib",
+ "noResolve",
+ "out",
+ "outDir",
+ "outFile",
+ "paths",
+ "preserveSymlinks",
+ "preserveWatchOutput",
+ "pretty",
+ "rootDir",
+ "rootDirs",
+ "showConfig",
+ "skipDefaultLibCheck",
+ "skipLibCheck",
+ "sourceMap",
+ "sourceRoot",
+ "stripInternal",
+ "target",
+ "traceResolution",
+ "tsBuildInfoFile",
+ "types",
+ "typeRoots",
+ "version",
+ "watch"
+];
+
+/** The shape of the SourceFile that comes from the privileged side */
+interface SourceFileJson {
+ url: string;
+ filename: string;
+ mediaType: MediaType;
+ sourceCode: string;
+}
+
+/** A self registering abstraction of source files. */
+class SourceFile {
+ extension!: ts.Extension;
+ filename!: string;
+
+ /** An array of tuples which represent the imports for the source file. The
+ * first element is the one that will be requested at compile time, the
+ * second is the one that should be actually resolved. This provides the
+ * feature of type directives for Deno. */
+ importedFiles?: Array<[string, string]>;
+
+ mediaType!: MediaType;
+ processed = false;
+ sourceCode!: string;
+ tsSourceFile?: ts.SourceFile;
+ url!: string;
+
+ constructor(json: SourceFileJson) {
+ if (SourceFile._moduleCache.has(json.url)) {
+ throw new TypeError("SourceFile already exists");
+ }
+ Object.assign(this, json);
+ this.extension = getExtension(this.url, this.mediaType);
+ SourceFile._moduleCache.set(this.url, this);
+ }
+
+ /** Cache the source file to be able to be retrieved by `moduleSpecifier` and
+ * `containingFile`. */
+ cache(moduleSpecifier: string, containingFile: string): void {
+ let innerCache = SourceFile._specifierCache.get(containingFile);
+ if (!innerCache) {
+ innerCache = new Map();
+ SourceFile._specifierCache.set(containingFile, innerCache);
+ }
+ innerCache.set(moduleSpecifier, this);
+ }
+
+ /** Process the imports for the file and return them. */
+ imports(): Array<[string, string]> {
+ if (this.processed) {
+ throw new Error("SourceFile has already been processed.");
+ }
+ assert(this.sourceCode != null);
+ const preProcessedFileInfo = ts.preProcessFile(
+ this.sourceCode!,
+ true,
+ true
+ );
+ this.processed = true;
+ const files = (this.importedFiles = [] as Array<[string, string]>);
+
+ function process(references: ts.FileReference[]): void {
+ for (const { fileName } of references) {
+ files.push([fileName, fileName]);
+ }
+ }
+
+ const {
+ importedFiles,
+ referencedFiles,
+ libReferenceDirectives,
+ typeReferenceDirectives
+ } = preProcessedFileInfo;
+ const typeDirectives = parseTypeDirectives(this.sourceCode);
+ if (typeDirectives) {
+ for (const importedFile of importedFiles) {
+ files.push([
+ importedFile.fileName,
+ getMappedModuleName(importedFile, typeDirectives)
+ ]);
+ }
+ } else {
+ process(importedFiles);
+ }
+ process(referencedFiles);
+ process(libReferenceDirectives);
+ process(typeReferenceDirectives);
+ return files;
+ }
+
+ /** A cache of all the source files which have been loaded indexed by the
+ * url. */
+ private static _moduleCache: Map<string, SourceFile> = new Map();
+
+ /** A cache of source files based on module specifiers and containing files
+ * which is used by the TypeScript compiler to resolve the url */
+ private static _specifierCache: Map<
+ string,
+ Map<string, SourceFile>
+ > = new Map();
+
+ /** Retrieve a `SourceFile` based on a `moduleSpecifier` and `containingFile`
+ * or return `undefined` if not preset. */
+ static getUrl(
+ moduleSpecifier: string,
+ containingFile: string
+ ): string | undefined {
+ const containingCache = this._specifierCache.get(containingFile);
+ if (containingCache) {
+ const sourceFile = containingCache.get(moduleSpecifier);
+ return sourceFile && sourceFile.url;
+ }
+ return undefined;
+ }
+
+ /** Retrieve a `SourceFile` based on a `url` */
+ static get(url: string): SourceFile | undefined {
+ return this._moduleCache.get(url);
+ }
+}
+
+interface EmitResult {
+ emitSkipped: boolean;
+ diagnostics?: Diagnostic;
+}
+
+/** Ops to Rust to resolve special static assets. */
+function fetchAsset(name: string): string {
+ return sendSync(dispatch.OP_FETCH_ASSET, { name });
+}
+
+/** Ops to Rust to resolve and fetch modules meta data. */
+function fetchSourceFiles(
+ specifiers: string[],
+ referrer: string
+): Promise<SourceFileJson[]> {
+ util.log("compiler::fetchSourceFiles", { specifiers, referrer });
+ return sendAsync(dispatch.OP_FETCH_SOURCE_FILES, {
+ specifiers,
+ referrer
+ });
+}
+
+/** Recursively process the imports of modules, generating `SourceFile`s of any
+ * imported files.
+ *
+ * Specifiers are supplied in an array of tupples where the first is the
+ * specifier that will be requested in the code and the second is the specifier
+ * that should be actually resolved. */
+async function processImports(
+ specifiers: Array<[string, string]>,
+ referrer = ""
+): Promise<void> {
+ if (!specifiers.length) {
+ return;
+ }
+ const sources = specifiers.map(([, moduleSpecifier]) => moduleSpecifier);
+ const sourceFiles = await fetchSourceFiles(sources, referrer);
+ assert(sourceFiles.length === specifiers.length);
+ for (let i = 0; i < sourceFiles.length; i++) {
+ const sourceFileJson = sourceFiles[i];
+ const sourceFile =
+ SourceFile.get(sourceFileJson.url) || new SourceFile(sourceFileJson);
+ sourceFile.cache(specifiers[i][0], referrer);
+ if (!sourceFile.processed) {
+ await processImports(sourceFile.imports(), sourceFile.url);
+ }
+ }
+}
+
+/** Utility function to turn the number of bytes into a human readable
+ * unit */
+function humanFileSize(bytes: number): string {
+ const thresh = 1000;
+ if (Math.abs(bytes) < thresh) {
+ return bytes + " B";
+ }
+ const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
+ let u = -1;
+ do {
+ bytes /= thresh;
+ ++u;
+ } while (Math.abs(bytes) >= thresh && u < units.length - 1);
+ return `${bytes.toFixed(1)} ${units[u]}`;
+}
+
+/** Ops to rest for caching source map and compiled js */
+function cache(extension: string, moduleId: string, contents: string): void {
+ util.log("compiler::cache", { extension, moduleId });
+ sendSync(dispatch.OP_CACHE, { extension, moduleId, contents });
+}
+
+const encoder = new TextEncoder();
+
+/** Given a fileName and the data, emit the file to the file system. */
+function emitBundle(fileName: string, data: string): void {
+ // For internal purposes, when trying to emit to `$deno$` just no-op
+ if (fileName.startsWith("$deno$")) {
+ console.warn("skipping emitBundle", fileName);
+ return;
+ }
+ const encodedData = encoder.encode(data);
+ console.log(`Emitting bundle to "${fileName}"`);
+ writeFileSync(fileName, encodedData);
+ console.log(`${humanFileSize(encodedData.length)} emitted.`);
+}
+
+/** Returns the TypeScript Extension enum for a given media type. */
+function getExtension(fileName: string, mediaType: MediaType): ts.Extension {
+ switch (mediaType) {
+ case MediaType.JavaScript:
+ return ts.Extension.Js;
+ case MediaType.JSX:
+ return ts.Extension.Jsx;
+ case MediaType.TypeScript:
+ return fileName.endsWith(".d.ts") ? ts.Extension.Dts : ts.Extension.Ts;
+ case MediaType.TSX:
+ return ts.Extension.Tsx;
+ case MediaType.Json:
+ return ts.Extension.Json;
+ case MediaType.Unknown:
+ default:
+ throw TypeError("Cannot resolve extension.");
+ }
+}
+
+class Host implements ts.CompilerHost {
+ private readonly _options: ts.CompilerOptions = {
+ allowJs: true,
+ allowNonTsExtensions: true,
+ checkJs: false,
+ esModuleInterop: true,
+ module: ts.ModuleKind.ESNext,
+ outDir: OUT_DIR,
+ resolveJsonModule: true,
+ sourceMap: true,
+ stripComments: true,
+ target: ts.ScriptTarget.ESNext,
+ jsx: ts.JsxEmit.React
+ };
+
+ private _getAsset(filename: string): SourceFile {
+ const sourceFile = SourceFile.get(filename);
+ if (sourceFile) {
+ return sourceFile;
+ }
+ const url = filename.split("/").pop()!;
+ const assetName = url.includes(".") ? url : `${url}.d.ts`;
+ const sourceCode = fetchAsset(assetName);
+ return new SourceFile({
+ url,
+ filename,
+ mediaType: MediaType.TypeScript,
+ sourceCode
+ });
+ }
+
+ /* Deno specific APIs */
+
+ /** Provides the `ts.HostCompiler` interface for Deno.
+ *
+ * @param _bundle Set to a string value to configure the host to write out a
+ * bundle instead of caching individual files.
+ */
+ constructor(private _bundle?: string) {
+ if (this._bundle) {
+ // options we need to change when we are generating a bundle
+ const bundlerOptions: ts.CompilerOptions = {
+ module: ts.ModuleKind.AMD,
+ inlineSourceMap: true,
+ outDir: undefined,
+ outFile: `${OUT_DIR}/bundle.js`,
+ sourceMap: false
+ };
+ Object.assign(this._options, bundlerOptions);
+ }
+ }
+
+ /** Take a configuration string, parse it, and use it to merge with the
+ * compiler's configuration options. The method returns an array of compiler
+ * options which were ignored, or `undefined`. */
+ configure(path: string, configurationText: string): ConfigureResponse {
+ util.log("compiler::host.configure", path);
+ const { config, error } = ts.parseConfigFileTextToJson(
+ path,
+ configurationText
+ );
+ if (error) {
+ return { diagnostics: [error] };
+ }
+ const { options, errors } = ts.convertCompilerOptionsFromJson(
+ config.compilerOptions,
+ cwd()
+ );
+ const ignoredOptions: string[] = [];
+ for (const key of Object.keys(options)) {
+ if (
+ ignoredCompilerOptions.includes(key) &&
+ (!(key in this._options) || options[key] !== this._options[key])
+ ) {
+ ignoredOptions.push(key);
+ delete options[key];
+ }
+ }
+ Object.assign(this._options, options);
+ return {
+ ignoredOptions: ignoredOptions.length ? ignoredOptions : undefined,
+ diagnostics: errors.length ? errors : undefined
+ };
+ }
+
+ /* TypeScript CompilerHost APIs */
+
+ fileExists(_fileName: string): boolean {
+ return notImplemented();
+ }
+
+ getCanonicalFileName(fileName: string): string {
+ return fileName;
+ }
+
+ getCompilationSettings(): ts.CompilerOptions {
+ util.log("compiler::host.getCompilationSettings()");
+ return this._options;
+ }
+
+ getCurrentDirectory(): string {
+ return "";
+ }
+
+ getDefaultLibFileName(_options: ts.CompilerOptions): string {
+ return ASSETS + "/lib.deno_runtime.d.ts";
+ }
+
+ getNewLine(): string {
+ return "\n";
+ }
+
+ getSourceFile(
+ fileName: string,
+ languageVersion: ts.ScriptTarget,
+ onError?: (message: string) => void,
+ shouldCreateNewSourceFile?: boolean
+ ): ts.SourceFile | undefined {
+ util.log("compiler::host.getSourceFile", fileName);
+ try {
+ assert(!shouldCreateNewSourceFile);
+ const sourceFile = fileName.startsWith(ASSETS)
+ ? this._getAsset(fileName)
+ : SourceFile.get(fileName);
+ assert(sourceFile != null);
+ if (!sourceFile!.tsSourceFile) {
+ sourceFile!.tsSourceFile = ts.createSourceFile(
+ fileName,
+ sourceFile!.sourceCode,
+ languageVersion
+ );
+ }
+ return sourceFile!.tsSourceFile;
+ } catch (e) {
+ if (onError) {
+ onError(String(e));
+ } else {
+ throw e;
+ }
+ return undefined;
+ }
+ }
+
+ readFile(_fileName: string): string | undefined {
+ return notImplemented();
+ }
+
+ resolveModuleNames(
+ moduleNames: string[],
+ containingFile: string
+ ): Array<ts.ResolvedModuleFull | undefined> {
+ util.log("compiler::host.resolveModuleNames", {
+ moduleNames,
+ containingFile
+ });
+ return moduleNames.map(specifier => {
+ const url = SourceFile.getUrl(specifier, containingFile);
+ const sourceFile = specifier.startsWith(ASSETS)
+ ? this._getAsset(specifier)
+ : url
+ ? SourceFile.get(url)
+ : undefined;
+ if (!sourceFile) {
+ return undefined;
+ }
+ return {
+ resolvedFileName: sourceFile.url,
+ isExternalLibraryImport: specifier.startsWith(ASSETS),
+ extension: sourceFile.extension
+ };
+ });
+ }
+
+ useCaseSensitiveFileNames(): boolean {
+ return true;
+ }
+
+ writeFile(
+ fileName: string,
+ data: string,
+ _writeByteOrderMark: boolean,
+ onError?: (message: string) => void,
+ sourceFiles?: readonly ts.SourceFile[]
+ ): void {
+ util.log("compiler::host.writeFile", fileName);
+ try {
+ if (this._bundle) {
+ emitBundle(this._bundle, data);
+ } else {
+ assert(sourceFiles != null && sourceFiles.length == 1);
+ const url = sourceFiles![0].fileName;
+ const sourceFile = SourceFile.get(url);
+
+ if (sourceFile) {
+ // NOTE: If it's a `.json` file we don't want to write it to disk.
+ // JSON files are loaded and used by TS compiler to check types, but we don't want
+ // to emit them to disk because output file is the same as input file.
+ if (sourceFile.extension === ts.Extension.Json) {
+ return;
+ }
+
+ // NOTE: JavaScript files are only emitted to disk if `checkJs` option in on
+ if (
+ sourceFile.extension === ts.Extension.Js &&
+ !this._options.checkJs
+ ) {
+ return;
+ }
+ }
+
+ if (fileName.endsWith(".map")) {
+ // Source Map
+ cache(".map", url, data);
+ } else if (fileName.endsWith(".js") || fileName.endsWith(".json")) {
+ // Compiled JavaScript
+ cache(".js", url, data);
+ } else {
+ assert(false, "Trying to cache unhandled file type " + fileName);
+ }
+ }
+ } catch (e) {
+ if (onError) {
+ onError(String(e));
+ } else {
+ throw e;
+ }
+ }
+ }
+}
+
+// provide the "main" function that will be called by the privileged side when
+// lazy instantiating the compiler web worker
+window.compilerMain = function compilerMain(): void {
+ // workerMain should have already been called since a compiler is a worker.
+ window.onmessage = async ({ data }: { data: CompilerReq }): Promise<void> => {
+ const { rootNames, configPath, config, bundle } = data;
+ util.log(">>> compile start", { rootNames, bundle });
+
+ // This will recursively analyse all the code for other imports, requesting
+ // those from the privileged side, populating the in memory cache which
+ // will be used by the host, before resolving.
+ await processImports(rootNames.map(rootName => [rootName, rootName]));
+
+ const host = new Host(bundle);
+ let emitSkipped = true;
+ let diagnostics: ts.Diagnostic[] | undefined;
+
+ // if there is a configuration supplied, we need to parse that
+ if (config && config.length && configPath) {
+ const configResult = host.configure(configPath, config);
+ const ignoredOptions = configResult.ignoredOptions;
+ diagnostics = configResult.diagnostics;
+ if (ignoredOptions) {
+ console.warn(
+ yellow(`Unsupported compiler options in "${configPath}"\n`) +
+ cyan(` The following options were ignored:\n`) +
+ ` ${ignoredOptions
+ .map((value): string => bold(value))
+ .join(", ")}`
+ );
+ }
+ }
+
+ // if there was a configuration and no diagnostics with it, we will continue
+ // to generate the program and possibly emit it.
+ if (!diagnostics || (diagnostics && diagnostics.length === 0)) {
+ const options = host.getCompilationSettings();
+ const program = ts.createProgram(rootNames, options, host);
+
+ diagnostics = ts.getPreEmitDiagnostics(program).filter(
+ ({ code }): boolean => {
+ // TS1308: 'await' expression is only allowed within an async
+ // function.
+ if (code === 1308) return false;
+ // TS2691: An import path cannot end with a '.ts' extension. Consider
+ // importing 'bad-module' instead.
+ if (code === 2691) return false;
+ // TS5009: Cannot find the common subdirectory path for the input files.
+ if (code === 5009) return false;
+ // TS5055: Cannot write file
+ // 'http://localhost:4545/tests/subdir/mt_application_x_javascript.j4.js'
+ // because it would overwrite input file.
+ if (code === 5055) return false;
+ // TypeScript is overly opinionated that only CommonJS modules kinds can
+ // support JSON imports. Allegedly this was fixed in
+ // Microsoft/TypeScript#26825 but that doesn't seem to be working here,
+ // so we will ignore complaints about this compiler setting.
+ if (code === 5070) return false;
+ return true;
+ }
+ );
+
+ // We will only proceed with the emit if there are no diagnostics.
+ if (diagnostics && diagnostics.length === 0) {
+ if (bundle) {
+ console.log(`Bundling "${bundle}"`);
+ }
+ const emitResult = program.emit();
+ emitSkipped = emitResult.emitSkipped;
+ // emitResult.diagnostics is `readonly` in TS3.5+ and can't be assigned
+ // without casting.
+ diagnostics = emitResult.diagnostics as ts.Diagnostic[];
+ }
+ }
+
+ const result: EmitResult = {
+ emitSkipped,
+ diagnostics: diagnostics.length
+ ? fromTypeScriptDiagnostic(diagnostics)
+ : undefined
+ };
+
+ postMessage(result);
+
+ util.log("<<< compile end", { rootNames, bundle });
+
+ // The compiler isolate exits after a single message.
+ workerClose();
+ };
+};
diff --git a/cli/js/console.ts b/cli/js/console.ts
new file mode 100644
index 000000000..9f0ce4bd6
--- /dev/null
+++ b/cli/js/console.ts
@@ -0,0 +1,790 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { isTypedArray } from "./util.ts";
+import { TypedArray } from "./types.ts";
+import { TextEncoder } from "./text_encoding.ts";
+import { File, stdout } from "./files.ts";
+import { cliTable } from "./console_table.ts";
+
+type ConsoleContext = Set<unknown>;
+type ConsoleOptions = Partial<{
+ showHidden: boolean;
+ depth: number;
+ colors: boolean;
+ indentLevel: number;
+}>;
+
+// Default depth of logging nested objects
+const DEFAULT_MAX_DEPTH = 4;
+
+// Number of elements an object must have before it's displayed in appreviated
+// form.
+const OBJ_ABBREVIATE_SIZE = 5;
+
+const STR_ABBREVIATE_SIZE = 100;
+
+// Char codes
+const CHAR_PERCENT = 37; /* % */
+const CHAR_LOWERCASE_S = 115; /* s */
+const CHAR_LOWERCASE_D = 100; /* d */
+const CHAR_LOWERCASE_I = 105; /* i */
+const CHAR_LOWERCASE_F = 102; /* f */
+const CHAR_LOWERCASE_O = 111; /* o */
+const CHAR_UPPERCASE_O = 79; /* O */
+const CHAR_LOWERCASE_C = 99; /* c */
+export class CSI {
+ static kClear = "\x1b[1;1H";
+ static kClearScreenDown = "\x1b[0J";
+}
+
+/* eslint-disable @typescript-eslint/no-use-before-define */
+
+function cursorTo(stream: File, _x: number, _y?: number): void {
+ const uint8 = new TextEncoder().encode(CSI.kClear);
+ stream.write(uint8);
+}
+
+function clearScreenDown(stream: File): void {
+ const uint8 = new TextEncoder().encode(CSI.kClearScreenDown);
+ stream.write(uint8);
+}
+
+function getClassInstanceName(instance: unknown): string {
+ if (typeof instance !== "object") {
+ return "";
+ }
+ if (!instance) {
+ return "";
+ }
+
+ const proto = Object.getPrototypeOf(instance);
+ if (proto && proto.constructor) {
+ return proto.constructor.name; // could be "Object" or "Array"
+ }
+
+ return "";
+}
+
+function createFunctionString(value: Function, _ctx: ConsoleContext): string {
+ // Might be Function/AsyncFunction/GeneratorFunction
+ const cstrName = Object.getPrototypeOf(value).constructor.name;
+ if (value.name && value.name !== "anonymous") {
+ // from MDN spec
+ return `[${cstrName}: ${value.name}]`;
+ }
+ return `[${cstrName}]`;
+}
+
+interface IterablePrintConfig<T> {
+ typeName: string;
+ displayName: string;
+ delims: [string, string];
+ entryHandler: (
+ entry: T,
+ ctx: ConsoleContext,
+ level: number,
+ maxLevel: number
+ ) => string;
+}
+
+function createIterableString<T>(
+ value: Iterable<T>,
+ ctx: ConsoleContext,
+ level: number,
+ maxLevel: number,
+ config: IterablePrintConfig<T>
+): string {
+ if (level >= maxLevel) {
+ return `[${config.typeName}]`;
+ }
+ ctx.add(value);
+
+ const entries: string[] = [];
+ // In cases e.g. Uint8Array.prototype
+ try {
+ for (const el of value) {
+ entries.push(config.entryHandler(el, ctx, level + 1, maxLevel));
+ }
+ } catch (e) {}
+ ctx.delete(value);
+ const iPrefix = `${config.displayName ? config.displayName + " " : ""}`;
+ const iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `;
+ return `${iPrefix}${config.delims[0]}${iContent}${config.delims[1]}`;
+}
+
+function stringify(
+ value: unknown,
+ ctx: ConsoleContext,
+ level: number,
+ maxLevel: number
+): string {
+ switch (typeof value) {
+ case "string":
+ return value;
+ case "number":
+ // Special handling of -0
+ return Object.is(value, -0) ? "-0" : `${value}`;
+ case "boolean":
+ case "undefined":
+ case "symbol":
+ return String(value);
+ case "bigint":
+ return `${value}n`;
+ case "function":
+ return createFunctionString(value as Function, ctx);
+ case "object":
+ if (value === null) {
+ return "null";
+ }
+
+ if (ctx.has(value)) {
+ return "[Circular]";
+ }
+
+ return createObjectString(value, ctx, level, maxLevel);
+ default:
+ return "[Not Implemented]";
+ }
+}
+
+// Print strings when they are inside of arrays or objects with quotes
+function stringifyWithQuotes(
+ value: unknown,
+ ctx: ConsoleContext,
+ level: number,
+ maxLevel: number
+): string {
+ switch (typeof value) {
+ case "string":
+ const trunc =
+ value.length > STR_ABBREVIATE_SIZE
+ ? value.slice(0, STR_ABBREVIATE_SIZE) + "..."
+ : value;
+ return JSON.stringify(trunc);
+ default:
+ return stringify(value, ctx, level, maxLevel);
+ }
+}
+
+function createArrayString(
+ value: unknown[],
+ ctx: ConsoleContext,
+ level: number,
+ maxLevel: number
+): string {
+ const printConfig: IterablePrintConfig<unknown> = {
+ typeName: "Array",
+ displayName: "",
+ delims: ["[", "]"],
+ entryHandler: (el, ctx, level, maxLevel): string =>
+ stringifyWithQuotes(el, ctx, level + 1, maxLevel)
+ };
+ return createIterableString(value, ctx, level, maxLevel, printConfig);
+}
+
+function createTypedArrayString(
+ typedArrayName: string,
+ value: TypedArray,
+ ctx: ConsoleContext,
+ level: number,
+ maxLevel: number
+): string {
+ const printConfig: IterablePrintConfig<unknown> = {
+ typeName: typedArrayName,
+ displayName: typedArrayName,
+ delims: ["[", "]"],
+ entryHandler: (el, ctx, level, maxLevel): string =>
+ stringifyWithQuotes(el, ctx, level + 1, maxLevel)
+ };
+ return createIterableString(value, ctx, level, maxLevel, printConfig);
+}
+
+function createSetString(
+ value: Set<unknown>,
+ ctx: ConsoleContext,
+ level: number,
+ maxLevel: number
+): string {
+ const printConfig: IterablePrintConfig<unknown> = {
+ typeName: "Set",
+ displayName: "Set",
+ delims: ["{", "}"],
+ entryHandler: (el, ctx, level, maxLevel): string =>
+ stringifyWithQuotes(el, ctx, level + 1, maxLevel)
+ };
+ return createIterableString(value, ctx, level, maxLevel, printConfig);
+}
+
+function createMapString(
+ value: Map<unknown, unknown>,
+ ctx: ConsoleContext,
+ level: number,
+ maxLevel: number
+): string {
+ const printConfig: IterablePrintConfig<[unknown, unknown]> = {
+ typeName: "Map",
+ displayName: "Map",
+ delims: ["{", "}"],
+ entryHandler: (el, ctx, level, maxLevel): string => {
+ const [key, val] = el;
+ return `${stringifyWithQuotes(
+ key,
+ ctx,
+ level + 1,
+ maxLevel
+ )} => ${stringifyWithQuotes(val, ctx, level + 1, maxLevel)}`;
+ }
+ };
+ return createIterableString(value, ctx, level, maxLevel, printConfig);
+}
+
+function createWeakSetString(): string {
+ return "WeakSet { [items unknown] }"; // as seen in Node
+}
+
+function createWeakMapString(): string {
+ return "WeakMap { [items unknown] }"; // as seen in Node
+}
+
+function createDateString(value: Date): string {
+ // without quotes, ISO format
+ return value.toISOString();
+}
+
+function createRegExpString(value: RegExp): string {
+ return value.toString();
+}
+
+/* eslint-disable @typescript-eslint/ban-types */
+
+function createStringWrapperString(value: String): string {
+ return `[String: "${value.toString()}"]`;
+}
+
+function createBooleanWrapperString(value: Boolean): string {
+ return `[Boolean: ${value.toString()}]`;
+}
+
+function createNumberWrapperString(value: Number): string {
+ return `[Number: ${value.toString()}]`;
+}
+
+/* eslint-enable @typescript-eslint/ban-types */
+
+// TODO: Promise, requires v8 bindings to get info
+// TODO: Proxy
+
+function createRawObjectString(
+ value: { [key: string]: unknown },
+ ctx: ConsoleContext,
+ level: number,
+ maxLevel: number
+): string {
+ if (level >= maxLevel) {
+ return "[Object]";
+ }
+ ctx.add(value);
+
+ let baseString = "";
+
+ const className = getClassInstanceName(value);
+ let shouldShowClassName = false;
+ if (className && className !== "Object" && className !== "anonymous") {
+ shouldShowClassName = true;
+ }
+ const keys = Object.keys(value);
+ const entries: string[] = keys.map(
+ (key): string => {
+ if (keys.length > OBJ_ABBREVIATE_SIZE) {
+ return key;
+ } else {
+ return `${key}: ${stringifyWithQuotes(
+ value[key],
+ ctx,
+ level + 1,
+ maxLevel
+ )}`;
+ }
+ }
+ );
+
+ ctx.delete(value);
+
+ if (entries.length === 0) {
+ baseString = "{}";
+ } else {
+ baseString = `{ ${entries.join(", ")} }`;
+ }
+
+ if (shouldShowClassName) {
+ baseString = `${className} ${baseString}`;
+ }
+
+ return baseString;
+}
+
+function createObjectString(
+ value: {},
+ ...args: [ConsoleContext, number, number]
+): string {
+ if (customInspect in value && typeof value[customInspect] === "function") {
+ return String(value[customInspect]!());
+ } else if (value instanceof Error) {
+ return String(value.stack);
+ } else if (Array.isArray(value)) {
+ return createArrayString(value, ...args);
+ } else if (value instanceof Number) {
+ return createNumberWrapperString(value);
+ } else if (value instanceof Boolean) {
+ return createBooleanWrapperString(value);
+ } else if (value instanceof String) {
+ return createStringWrapperString(value);
+ } else if (value instanceof RegExp) {
+ return createRegExpString(value);
+ } else if (value instanceof Date) {
+ return createDateString(value);
+ } else if (value instanceof Set) {
+ return createSetString(value, ...args);
+ } else if (value instanceof Map) {
+ return createMapString(value, ...args);
+ } else if (value instanceof WeakSet) {
+ return createWeakSetString();
+ } else if (value instanceof WeakMap) {
+ return createWeakMapString();
+ } else if (isTypedArray(value)) {
+ return createTypedArrayString(
+ Object.getPrototypeOf(value).constructor.name,
+ value,
+ ...args
+ );
+ } else {
+ // Otherwise, default object formatting
+ return createRawObjectString(value, ...args);
+ }
+}
+
+/** TODO Do not expose this from "deno" namespace.
+ * @internal
+ */
+export function stringifyArgs(
+ args: unknown[],
+ options: ConsoleOptions = {}
+): string {
+ const first = args[0];
+ let a = 0;
+ let str = "";
+ let join = "";
+
+ if (typeof first === "string") {
+ let tempStr: string;
+ let lastPos = 0;
+
+ for (let i = 0; i < first.length - 1; i++) {
+ if (first.charCodeAt(i) === CHAR_PERCENT) {
+ const nextChar = first.charCodeAt(++i);
+ if (a + 1 !== args.length) {
+ switch (nextChar) {
+ case CHAR_LOWERCASE_S:
+ // format as a string
+ tempStr = String(args[++a]);
+ break;
+ case CHAR_LOWERCASE_D:
+ case CHAR_LOWERCASE_I:
+ // format as an integer
+ const tempInteger = args[++a];
+ if (typeof tempInteger === "bigint") {
+ tempStr = `${tempInteger}n`;
+ } else if (typeof tempInteger === "symbol") {
+ tempStr = "NaN";
+ } else {
+ tempStr = `${parseInt(String(tempInteger), 10)}`;
+ }
+ break;
+ case CHAR_LOWERCASE_F:
+ // format as a floating point value
+ const tempFloat = args[++a];
+ if (typeof tempFloat === "symbol") {
+ tempStr = "NaN";
+ } else {
+ tempStr = `${parseFloat(String(tempFloat))}`;
+ }
+ break;
+ case CHAR_LOWERCASE_O:
+ case CHAR_UPPERCASE_O:
+ // format as an object
+ tempStr = stringify(
+ args[++a],
+ new Set<unknown>(),
+ 0,
+ options.depth != undefined ? options.depth : DEFAULT_MAX_DEPTH
+ );
+ break;
+ case CHAR_PERCENT:
+ str += first.slice(lastPos, i);
+ lastPos = i + 1;
+ continue;
+ case CHAR_LOWERCASE_C:
+ // TODO: applies CSS style rules to the output string as specified
+ continue;
+ default:
+ // any other character is not a correct placeholder
+ continue;
+ }
+
+ if (lastPos !== i - 1) {
+ str += first.slice(lastPos, i - 1);
+ }
+
+ str += tempStr;
+ lastPos = i + 1;
+ } else if (nextChar === CHAR_PERCENT) {
+ str += first.slice(lastPos, i);
+ lastPos = i + 1;
+ }
+ }
+ }
+
+ if (lastPos !== 0) {
+ a++;
+ join = " ";
+ if (lastPos < first.length) {
+ str += first.slice(lastPos);
+ }
+ }
+ }
+
+ while (a < args.length) {
+ const value = args[a];
+ str += join;
+ if (typeof value === "string") {
+ str += value;
+ } else {
+ // use default maximum depth for null or undefined argument
+ str += stringify(
+ value,
+ new Set<unknown>(),
+ 0,
+ options.depth != undefined ? options.depth : DEFAULT_MAX_DEPTH
+ );
+ }
+ join = " ";
+ a++;
+ }
+
+ const { indentLevel } = options;
+ if (indentLevel != null && indentLevel > 0) {
+ const groupIndent = " ".repeat(indentLevel);
+ if (str.indexOf("\n") !== -1) {
+ str = str.replace(/\n/g, `\n${groupIndent}`);
+ }
+ str = groupIndent + str;
+ }
+
+ return str;
+}
+
+type PrintFunc = (x: string, isErr?: boolean) => void;
+
+const countMap = new Map<string, number>();
+const timerMap = new Map<string, number>();
+const isConsoleInstance = Symbol("isConsoleInstance");
+
+export class Console {
+ indentLevel: number;
+ [isConsoleInstance] = false;
+
+ /** @internal */
+ constructor(private printFunc: PrintFunc) {
+ this.indentLevel = 0;
+ this[isConsoleInstance] = true;
+
+ // ref https://console.spec.whatwg.org/#console-namespace
+ // For historical web-compatibility reasons, the namespace object for
+ // console must have as its [[Prototype]] an empty object, created as if
+ // by ObjectCreate(%ObjectPrototype%), instead of %ObjectPrototype%.
+ const console = Object.create({}) as Console;
+ Object.assign(console, this);
+ return console;
+ }
+
+ /** Writes the arguments to stdout */
+ log = (...args: unknown[]): void => {
+ this.printFunc(
+ stringifyArgs(args, {
+ indentLevel: this.indentLevel
+ }) + "\n",
+ false
+ );
+ };
+
+ /** Writes the arguments to stdout */
+ debug = this.log;
+ /** Writes the arguments to stdout */
+ info = this.log;
+
+ /** Writes the properties of the supplied `obj` to stdout */
+ dir = (obj: unknown, options: ConsoleOptions = {}): void => {
+ this.printFunc(stringifyArgs([obj], options) + "\n", false);
+ };
+
+ /** From MDN:
+ * Displays an interactive tree of the descendant elements of
+ * the specified XML/HTML element. If it is not possible to display
+ * as an element the JavaScript Object view is shown instead.
+ * The output is presented as a hierarchical listing of expandable
+ * nodes that let you see the contents of child nodes.
+ *
+ * Since we write to stdout, we can't display anything interactive
+ * we just fall back to `console.dir`.
+ */
+ dirxml = this.dir;
+
+ /** Writes the arguments to stdout */
+ warn = (...args: unknown[]): void => {
+ this.printFunc(
+ stringifyArgs(args, {
+ indentLevel: this.indentLevel
+ }) + "\n",
+ true
+ );
+ };
+
+ /** Writes the arguments to stdout */
+ error = this.warn;
+
+ /** Writes an error message to stdout if the assertion is `false`. If the
+ * assertion is `true`, nothing happens.
+ *
+ * ref: https://console.spec.whatwg.org/#assert
+ */
+ assert = (condition = false, ...args: unknown[]): void => {
+ if (condition) {
+ return;
+ }
+
+ if (args.length === 0) {
+ this.error("Assertion failed");
+ return;
+ }
+
+ const [first, ...rest] = args;
+
+ if (typeof first === "string") {
+ this.error(`Assertion failed: ${first}`, ...rest);
+ return;
+ }
+
+ this.error(`Assertion failed:`, ...args);
+ };
+
+ count = (label = "default"): void => {
+ label = String(label);
+
+ if (countMap.has(label)) {
+ const current = countMap.get(label) || 0;
+ countMap.set(label, current + 1);
+ } else {
+ countMap.set(label, 1);
+ }
+
+ this.info(`${label}: ${countMap.get(label)}`);
+ };
+
+ countReset = (label = "default"): void => {
+ label = String(label);
+
+ if (countMap.has(label)) {
+ countMap.set(label, 0);
+ } else {
+ this.warn(`Count for '${label}' does not exist`);
+ }
+ };
+
+ table = (data: unknown, properties?: string[]): void => {
+ if (properties !== undefined && !Array.isArray(properties)) {
+ throw new Error(
+ "The 'properties' argument must be of type Array. " +
+ "Received type string"
+ );
+ }
+
+ if (data === null || typeof data !== "object") {
+ return this.log(data);
+ }
+
+ const objectValues: { [key: string]: string[] } = {};
+ const indexKeys: string[] = [];
+ const values: string[] = [];
+
+ const stringifyValue = (value: unknown): string =>
+ stringifyWithQuotes(value, new Set<unknown>(), 0, 1);
+ const toTable = (header: string[], body: string[][]): void =>
+ this.log(cliTable(header, body));
+ const createColumn = (value: unknown, shift?: number): string[] => [
+ ...(shift ? [...new Array(shift)].map((): string => "") : []),
+ stringifyValue(value)
+ ];
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let resultData: any;
+ const isSet = data instanceof Set;
+ const isMap = data instanceof Map;
+ const valuesKey = "Values";
+ const indexKey = isSet || isMap ? "(iteration index)" : "(index)";
+
+ if (data instanceof Set) {
+ resultData = [...data];
+ } else if (data instanceof Map) {
+ let idx = 0;
+ resultData = {};
+
+ data.forEach(
+ (v: unknown, k: unknown): void => {
+ resultData[idx] = { Key: k, Values: v };
+ idx++;
+ }
+ );
+ } else {
+ resultData = data!;
+ }
+
+ Object.keys(resultData).forEach(
+ (k, idx): void => {
+ const value: unknown = resultData[k]!;
+
+ if (value !== null && typeof value === "object") {
+ Object.entries(value as { [key: string]: unknown }).forEach(
+ ([k, v]): void => {
+ if (properties && !properties.includes(k)) {
+ return;
+ }
+
+ if (objectValues[k]) {
+ objectValues[k].push(stringifyValue(v));
+ } else {
+ objectValues[k] = createColumn(v, idx);
+ }
+ }
+ );
+
+ values.push("");
+ } else {
+ values.push(stringifyValue(value));
+ }
+
+ indexKeys.push(k);
+ }
+ );
+
+ const headerKeys = Object.keys(objectValues);
+ const bodyValues = Object.values(objectValues);
+ const header = [
+ indexKey,
+ ...(properties || [
+ ...headerKeys,
+ !isMap && values.length > 0 && valuesKey
+ ])
+ ].filter(Boolean) as string[];
+ const body = [indexKeys, ...bodyValues, values];
+
+ toTable(header, body);
+ };
+
+ time = (label = "default"): void => {
+ label = String(label);
+
+ if (timerMap.has(label)) {
+ this.warn(`Timer '${label}' already exists`);
+ return;
+ }
+
+ timerMap.set(label, Date.now());
+ };
+
+ timeLog = (label = "default", ...args: unknown[]): void => {
+ label = String(label);
+
+ if (!timerMap.has(label)) {
+ this.warn(`Timer '${label}' does not exists`);
+ return;
+ }
+
+ const startTime = timerMap.get(label) as number;
+ const duration = Date.now() - startTime;
+
+ this.info(`${label}: ${duration}ms`, ...args);
+ };
+
+ timeEnd = (label = "default"): void => {
+ label = String(label);
+
+ if (!timerMap.has(label)) {
+ this.warn(`Timer '${label}' does not exists`);
+ return;
+ }
+
+ const startTime = timerMap.get(label) as number;
+ timerMap.delete(label);
+ const duration = Date.now() - startTime;
+
+ this.info(`${label}: ${duration}ms`);
+ };
+
+ group = (...label: unknown[]): void => {
+ if (label.length > 0) {
+ this.log(...label);
+ }
+ this.indentLevel += 2;
+ };
+
+ groupCollapsed = this.group;
+
+ groupEnd = (): void => {
+ if (this.indentLevel > 0) {
+ this.indentLevel -= 2;
+ }
+ };
+
+ clear = (): void => {
+ this.indentLevel = 0;
+ cursorTo(stdout, 0, 0);
+ clearScreenDown(stdout);
+ };
+
+ trace = (...args: unknown[]): void => {
+ const message = stringifyArgs(args, { indentLevel: 0 });
+ const err = {
+ name: "Trace",
+ message
+ };
+ // @ts-ignore
+ Error.captureStackTrace(err, this.trace);
+ this.error((err as Error).stack);
+ };
+
+ static [Symbol.hasInstance](instance: Console): boolean {
+ return instance[isConsoleInstance];
+ }
+}
+
+/** A symbol which can be used as a key for a custom method which will be called
+ * when `Deno.inspect()` is called, or when the object is logged to the console.
+ */
+export const customInspect = Symbol.for("Deno.customInspect");
+
+/**
+ * `inspect()` converts input into string that has the same format
+ * as printed by `console.log(...)`;
+ */
+export function inspect(value: unknown, options?: ConsoleOptions): string {
+ const opts = options || {};
+ if (typeof value === "string") {
+ return value;
+ } else {
+ return stringify(
+ value,
+ new Set<unknown>(),
+ 0,
+ opts.depth != undefined ? opts.depth : DEFAULT_MAX_DEPTH
+ );
+ }
+}
diff --git a/cli/js/console_table.ts b/cli/js/console_table.ts
new file mode 100644
index 000000000..d74dc0127
--- /dev/null
+++ b/cli/js/console_table.ts
@@ -0,0 +1,94 @@
+// Copyright Joyent, Inc. and other Node contributors. MIT license.
+// Forked from Node's lib/internal/cli_table.js
+
+import { TextEncoder } from "./text_encoding.ts";
+import { hasOwnProperty } from "./util.ts";
+
+const encoder = new TextEncoder();
+
+const tableChars = {
+ middleMiddle: "─",
+ rowMiddle: "┼",
+ topRight: "┐",
+ topLeft: "┌",
+ leftMiddle: "├",
+ topMiddle: "┬",
+ bottomRight: "┘",
+ bottomLeft: "└",
+ bottomMiddle: "┴",
+ rightMiddle: "┤",
+ left: "│ ",
+ right: " │",
+ middle: " │ "
+};
+
+const colorRegExp = /\u001b\[\d\d?m/g;
+
+function removeColors(str: string): string {
+ return str.replace(colorRegExp, "");
+}
+
+function countBytes(str: string): number {
+ const normalized = removeColors(String(str)).normalize("NFC");
+
+ return encoder.encode(normalized).byteLength;
+}
+
+function renderRow(row: string[], columnWidths: number[]): string {
+ let out = tableChars.left;
+ for (let i = 0; i < row.length; i++) {
+ const cell = row[i];
+ const len = countBytes(cell);
+ const needed = (columnWidths[i] - len) / 2;
+ // round(needed) + ceil(needed) will always add up to the amount
+ // of spaces we need while also left justifying the output.
+ out += `${" ".repeat(needed)}${cell}${" ".repeat(Math.ceil(needed))}`;
+ if (i !== row.length - 1) {
+ out += tableChars.middle;
+ }
+ }
+ out += tableChars.right;
+ return out;
+}
+
+export function cliTable(head: string[], columns: string[][]): string {
+ const rows: string[][] = [];
+ const columnWidths = head.map((h: string): number => countBytes(h));
+ const longestColumn = columns.reduce(
+ (n: number, a: string[]): number => Math.max(n, a.length),
+ 0
+ );
+
+ for (let i = 0; i < head.length; i++) {
+ const column = columns[i];
+ for (let j = 0; j < longestColumn; j++) {
+ if (rows[j] === undefined) {
+ rows[j] = [];
+ }
+ const value = (rows[j][i] = hasOwnProperty(column, j) ? column[j] : "");
+ const width = columnWidths[i] || 0;
+ const counted = countBytes(value);
+ columnWidths[i] = Math.max(width, counted);
+ }
+ }
+
+ const divider = columnWidths.map(
+ (i: number): string => tableChars.middleMiddle.repeat(i + 2)
+ );
+
+ let result =
+ `${tableChars.topLeft}${divider.join(tableChars.topMiddle)}` +
+ `${tableChars.topRight}\n${renderRow(head, columnWidths)}\n` +
+ `${tableChars.leftMiddle}${divider.join(tableChars.rowMiddle)}` +
+ `${tableChars.rightMiddle}\n`;
+
+ for (const row of rows) {
+ result += `${renderRow(row, columnWidths)}\n`;
+ }
+
+ result +=
+ `${tableChars.bottomLeft}${divider.join(tableChars.bottomMiddle)}` +
+ tableChars.bottomRight;
+
+ return result;
+}
diff --git a/cli/js/console_test.ts b/cli/js/console_test.ts
new file mode 100644
index 000000000..903e65a82
--- /dev/null
+++ b/cli/js/console_test.ts
@@ -0,0 +1,698 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals, test } from "./test_util.ts";
+
+// Some of these APIs aren't exposed in the types and so we have to cast to any
+// in order to "trick" TypeScript.
+const {
+ Console,
+ customInspect,
+ stringifyArgs,
+ inspect,
+ write,
+ stdout
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+} = Deno as any;
+
+function stringify(...args: unknown[]): string {
+ return stringifyArgs(args).replace(/\n$/, "");
+}
+
+// test cases from web-platform-tests
+// via https://github.com/web-platform-tests/wpt/blob/master/console/console-is-a-namespace.any.js
+test(function consoleShouldBeANamespace(): void {
+ const prototype1 = Object.getPrototypeOf(console);
+ const prototype2 = Object.getPrototypeOf(prototype1);
+
+ assertEquals(Object.getOwnPropertyNames(prototype1).length, 0);
+ assertEquals(prototype2, Object.prototype);
+});
+
+test(function consoleHasRightInstance(): void {
+ assert(console instanceof Console);
+ assertEquals({} instanceof Console, false);
+});
+
+test(function consoleTestAssertShouldNotThrowError(): void {
+ console.assert(true);
+
+ let hasThrown = undefined;
+ try {
+ console.assert(false);
+ hasThrown = false;
+ } catch {
+ hasThrown = true;
+ }
+ assertEquals(hasThrown, false);
+});
+
+test(function consoleTestStringifyComplexObjects(): void {
+ assertEquals(stringify("foo"), "foo");
+ assertEquals(stringify(["foo", "bar"]), `[ "foo", "bar" ]`);
+ assertEquals(stringify({ foo: "bar" }), `{ foo: "bar" }`);
+});
+
+test(function consoleTestStringifyLongStrings(): void {
+ const veryLongString = "a".repeat(200);
+ // If we stringify an object containing the long string, it gets abbreviated.
+ let actual = stringify({ veryLongString });
+ assert(actual.includes("..."));
+ assert(actual.length < 200);
+ // However if we stringify the string itself, we get it exactly.
+ actual = stringify(veryLongString);
+ assertEquals(actual, veryLongString);
+});
+
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+test(function consoleTestStringifyCircular(): void {
+ class Base {
+ a = 1;
+ m1() {}
+ }
+
+ class Extended extends Base {
+ b = 2;
+ m2() {}
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const nestedObj: any = {
+ num: 1,
+ bool: true,
+ str: "a",
+ method() {},
+ async asyncMethod() {},
+ *generatorMethod() {},
+ 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;
+ const nestedObjExpected = `{ num, bool, str, method, asyncMethod, generatorMethod, un, nu, arrowFunc, extendedClass, nFunc, extendedCstr, o }`;
+
+ assertEquals(stringify(1), "1");
+ assertEquals(stringify(-0), "-0");
+ assertEquals(stringify(1n), "1n");
+ assertEquals(stringify("s"), "s");
+ assertEquals(stringify(false), "false");
+ assertEquals(stringify(new Number(1)), "[Number: 1]");
+ assertEquals(stringify(new Boolean(true)), "[Boolean: true]");
+ assertEquals(stringify(new String("deno")), `[String: "deno"]`);
+ assertEquals(stringify(/[0-9]*/), "/[0-9]*/");
+ assertEquals(
+ stringify(new Date("2018-12-10T02:26:59.002Z")),
+ "2018-12-10T02:26:59.002Z"
+ );
+ assertEquals(stringify(new Set([1, 2, 3])), "Set { 1, 2, 3 }");
+ assertEquals(
+ stringify(new Map([[1, "one"], [2, "two"]])),
+ `Map { 1 => "one", 2 => "two" }`
+ );
+ assertEquals(stringify(new WeakSet()), "WeakSet { [items unknown] }");
+ assertEquals(stringify(new WeakMap()), "WeakMap { [items unknown] }");
+ assertEquals(stringify(Symbol(1)), "Symbol(1)");
+ assertEquals(stringify(null), "null");
+ assertEquals(stringify(undefined), "undefined");
+ assertEquals(stringify(new Extended()), "Extended { a: 1, b: 2 }");
+ assertEquals(stringify(function f(): void {}), "[Function: f]");
+ assertEquals(
+ stringify(async function af(): Promise<void> {}),
+ "[AsyncFunction: af]"
+ );
+ assertEquals(stringify(function* gf() {}), "[GeneratorFunction: gf]");
+ assertEquals(
+ stringify(async function* agf() {}),
+ "[AsyncGeneratorFunction: agf]"
+ );
+ assertEquals(stringify(new Uint8Array([1, 2, 3])), "Uint8Array [ 1, 2, 3 ]");
+ assertEquals(stringify(Uint8Array.prototype), "TypedArray []");
+ assertEquals(
+ stringify({ a: { b: { c: { d: new Set([1]) } } } }),
+ "{ a: { b: { c: { d: [Set] } } } }"
+ );
+ assertEquals(stringify(nestedObj), nestedObjExpected);
+ assertEquals(stringify(JSON), "{}");
+ assertEquals(
+ stringify(console),
+ "{ printFunc, log, debug, info, dir, dirxml, warn, error, assert, count, countReset, table, time, timeLog, timeEnd, group, groupCollapsed, groupEnd, clear, trace, indentLevel }"
+ );
+ // test inspect is working the same
+ assertEquals(inspect(nestedObj), nestedObjExpected);
+});
+/* eslint-enable @typescript-eslint/explicit-function-return-type */
+
+test(function consoleTestStringifyWithDepth(): void {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const nestedObj: any = { a: { b: { c: { d: { e: { f: 42 } } } } } };
+ assertEquals(
+ stringifyArgs([nestedObj], { depth: 3 }),
+ "{ a: { b: { c: [Object] } } }"
+ );
+ assertEquals(
+ stringifyArgs([nestedObj], { depth: 4 }),
+ "{ a: { b: { c: { d: [Object] } } } }"
+ );
+ assertEquals(stringifyArgs([nestedObj], { depth: 0 }), "[Object]");
+ assertEquals(
+ stringifyArgs([nestedObj], { depth: null }),
+ "{ a: { b: { c: { d: [Object] } } } }"
+ );
+ // test inspect is working the same way
+ assertEquals(
+ inspect(nestedObj, { depth: 4 }),
+ "{ a: { b: { c: { d: [Object] } } } }"
+ );
+});
+
+test(function consoleTestWithCustomInspector(): void {
+ class A {
+ [customInspect](): string {
+ return "b";
+ }
+ }
+
+ assertEquals(stringify(new A()), "b");
+});
+
+test(function consoleTestWithIntegerFormatSpecifier(): void {
+ assertEquals(stringify("%i"), "%i");
+ assertEquals(stringify("%i", 42.0), "42");
+ assertEquals(stringify("%i", 42), "42");
+ assertEquals(stringify("%i", "42"), "42");
+ assertEquals(stringify("%i", "42.0"), "42");
+ assertEquals(stringify("%i", 1.5), "1");
+ assertEquals(stringify("%i", -0.5), "0");
+ assertEquals(stringify("%i", ""), "NaN");
+ assertEquals(stringify("%i", Symbol()), "NaN");
+ assertEquals(stringify("%i %d", 42, 43), "42 43");
+ assertEquals(stringify("%d %i", 42), "42 %i");
+ assertEquals(stringify("%d", 12345678901234567890123), "1");
+ assertEquals(
+ stringify("%i", 12345678901234567890123n),
+ "12345678901234567890123n"
+ );
+});
+
+test(function consoleTestWithFloatFormatSpecifier(): void {
+ assertEquals(stringify("%f"), "%f");
+ assertEquals(stringify("%f", 42.0), "42");
+ assertEquals(stringify("%f", 42), "42");
+ assertEquals(stringify("%f", "42"), "42");
+ assertEquals(stringify("%f", "42.0"), "42");
+ assertEquals(stringify("%f", 1.5), "1.5");
+ assertEquals(stringify("%f", -0.5), "-0.5");
+ assertEquals(stringify("%f", Math.PI), "3.141592653589793");
+ assertEquals(stringify("%f", ""), "NaN");
+ assertEquals(stringify("%f", Symbol("foo")), "NaN");
+ assertEquals(stringify("%f", 5n), "5");
+ assertEquals(stringify("%f %f", 42, 43), "42 43");
+ assertEquals(stringify("%f %f", 42), "42 %f");
+});
+
+test(function consoleTestWithStringFormatSpecifier(): void {
+ assertEquals(stringify("%s"), "%s");
+ assertEquals(stringify("%s", undefined), "undefined");
+ assertEquals(stringify("%s", "foo"), "foo");
+ assertEquals(stringify("%s", 42), "42");
+ assertEquals(stringify("%s", "42"), "42");
+ assertEquals(stringify("%s %s", 42, 43), "42 43");
+ assertEquals(stringify("%s %s", 42), "42 %s");
+ assertEquals(stringify("%s", Symbol("foo")), "Symbol(foo)");
+});
+
+test(function consoleTestWithObjectFormatSpecifier(): void {
+ assertEquals(stringify("%o"), "%o");
+ assertEquals(stringify("%o", 42), "42");
+ assertEquals(stringify("%o", "foo"), "foo");
+ assertEquals(stringify("o: %o, a: %O", {}, []), "o: {}, a: []");
+ assertEquals(stringify("%o", { a: 42 }), "{ a: 42 }");
+ assertEquals(
+ stringify("%o", { a: { b: { c: { d: new Set([1]) } } } }),
+ "{ a: { b: { c: { d: [Set] } } } }"
+ );
+});
+
+test(function consoleTestWithVariousOrInvalidFormatSpecifier(): void {
+ assertEquals(stringify("%s:%s"), "%s:%s");
+ assertEquals(stringify("%i:%i"), "%i:%i");
+ assertEquals(stringify("%d:%d"), "%d:%d");
+ assertEquals(stringify("%%s%s", "foo"), "%sfoo");
+ assertEquals(stringify("%s:%s", undefined), "undefined:%s");
+ assertEquals(stringify("%s:%s", "foo", "bar"), "foo:bar");
+ assertEquals(stringify("%s:%s", "foo", "bar", "baz"), "foo:bar baz");
+ assertEquals(stringify("%%%s%%", "hi"), "%hi%");
+ assertEquals(stringify("%d:%d", 12), "12:%d");
+ assertEquals(stringify("%i:%i", 12), "12:%i");
+ assertEquals(stringify("%f:%f", 12), "12:%f");
+ assertEquals(stringify("o: %o, a: %o", {}), "o: {}, a: %o");
+ assertEquals(stringify("abc%", 1), "abc% 1");
+});
+
+test(function consoleTestCallToStringOnLabel(): void {
+ const methods = ["count", "countReset", "time", "timeLog", "timeEnd"];
+
+ for (const method of methods) {
+ let hasCalled = false;
+
+ console[method]({
+ toString(): void {
+ hasCalled = true;
+ }
+ });
+
+ assertEquals(hasCalled, true);
+ }
+});
+
+test(function consoleTestError(): void {
+ class MyError extends Error {
+ constructor(errStr: string) {
+ super(errStr);
+ this.name = "MyError";
+ }
+ }
+ try {
+ throw new MyError("This is an error");
+ } catch (e) {
+ assert(
+ stringify(e)
+ .split("\n")[0] // error has been caught
+ .includes("MyError: This is an error")
+ );
+ }
+});
+
+test(function consoleTestClear(): void {
+ const stdoutWrite = stdout.write;
+ const uint8 = new TextEncoder().encode("\x1b[1;1H" + "\x1b[0J");
+ let buffer = new Uint8Array(0);
+
+ stdout.write = async (u8: Uint8Array): Promise<number> => {
+ const tmp = new Uint8Array(buffer.length + u8.length);
+ tmp.set(buffer, 0);
+ tmp.set(u8, buffer.length);
+ buffer = tmp;
+
+ return await write(stdout.rid, u8);
+ };
+ console.clear();
+ stdout.write = stdoutWrite;
+ assertEquals(buffer, uint8);
+});
+
+// Test bound this issue
+test(function consoleDetachedLog(): void {
+ const log = console.log;
+ const dir = console.dir;
+ const dirxml = console.dirxml;
+ const debug = console.debug;
+ const info = console.info;
+ const warn = console.warn;
+ const error = console.error;
+ const consoleAssert = console.assert;
+ const consoleCount = console.count;
+ const consoleCountReset = console.countReset;
+ const consoleTable = console.table;
+ const consoleTime = console.time;
+ const consoleTimeLog = console.timeLog;
+ const consoleTimeEnd = console.timeEnd;
+ const consoleGroup = console.group;
+ const consoleGroupEnd = console.groupEnd;
+ const consoleClear = console.clear;
+ log("Hello world");
+ dir("Hello world");
+ dirxml("Hello world");
+ debug("Hello world");
+ info("Hello world");
+ warn("Hello world");
+ error("Hello world");
+ consoleAssert(true);
+ consoleCount("Hello world");
+ consoleCountReset("Hello world");
+ consoleTable({ test: "Hello world" });
+ consoleTime("Hello world");
+ consoleTimeLog("Hello world");
+ consoleTimeEnd("Hello world");
+ consoleGroup("Hello world");
+ consoleGroupEnd();
+ consoleClear();
+});
+
+class StringBuffer {
+ chunks: string[] = [];
+ add(x: string): void {
+ this.chunks.push(x);
+ }
+ toString(): string {
+ return this.chunks.join("");
+ }
+}
+
+type ConsoleExamineFunc = (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ csl: any,
+ out: StringBuffer,
+ err?: StringBuffer,
+ both?: StringBuffer
+) => void;
+
+function mockConsole(f: ConsoleExamineFunc): void {
+ const out = new StringBuffer();
+ const err = new StringBuffer();
+ const both = new StringBuffer();
+ const csl = new Console(
+ (x: string, isErr: boolean, printsNewLine: boolean): void => {
+ const content = x + (printsNewLine ? "\n" : "");
+ const buf = isErr ? err : out;
+ buf.add(content);
+ both.add(content);
+ }
+ );
+ f(csl, out, err, both);
+}
+
+// console.group test
+test(function consoleGroup(): void {
+ mockConsole(
+ (console, out): void => {
+ console.group("1");
+ console.log("2");
+ console.group("3");
+ console.log("4");
+ console.groupEnd();
+ console.groupEnd();
+ console.log("5");
+ console.log("6");
+
+ assertEquals(
+ out.toString(),
+ `1
+ 2
+ 3
+ 4
+5
+6
+`
+ );
+ }
+ );
+});
+
+// console.group with console.warn test
+test(function consoleGroupWarn(): void {
+ mockConsole(
+ (console, _out, _err, both): void => {
+ console.warn("1");
+ console.group();
+ console.warn("2");
+ console.group();
+ console.warn("3");
+ console.groupEnd();
+ console.warn("4");
+ console.groupEnd();
+ console.warn("5");
+
+ console.warn("6");
+ console.warn("7");
+ assertEquals(
+ both.toString(),
+ `1
+ 2
+ 3
+ 4
+5
+6
+7
+`
+ );
+ }
+ );
+});
+
+// console.table test
+test(function consoleTable(): void {
+ mockConsole(
+ (console, out): void => {
+ console.table({ a: "test", b: 1 });
+ assertEquals(
+ out.toString(),
+ `┌─────────┬────────┐
+│ (index) │ Values │
+├─────────┼────────┤
+│ a │ "test" │
+│ b │ 1 │
+└─────────┴────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table({ a: { b: 10 }, b: { b: 20, c: 30 } }, ["c"]);
+ assertEquals(
+ out.toString(),
+ `┌─────────┬────┐
+│ (index) │ c │
+├─────────┼────┤
+│ a │ │
+│ b │ 30 │
+└─────────┴────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table([1, 2, [3, [4]], [5, 6], [[7], [8]]]);
+ assertEquals(
+ out.toString(),
+ `┌─────────┬───────┬───────┬────────┐
+│ (index) │ 0 │ 1 │ Values │
+├─────────┼───────┼───────┼────────┤
+│ 0 │ │ │ 1 │
+│ 1 │ │ │ 2 │
+│ 2 │ 3 │ [ 4 ] │ │
+│ 3 │ 5 │ 6 │ │
+│ 4 │ [ 7 ] │ [ 8 ] │ │
+└─────────┴───────┴───────┴────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table(new Set([1, 2, 3, "test"]));
+ assertEquals(
+ out.toString(),
+ `┌───────────────────┬────────┐
+│ (iteration index) │ Values │
+├───────────────────┼────────┤
+│ 0 │ 1 │
+│ 1 │ 2 │
+│ 2 │ 3 │
+│ 3 │ "test" │
+└───────────────────┴────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table(new Map([[1, "one"], [2, "two"]]));
+ assertEquals(
+ out.toString(),
+ `┌───────────────────┬─────┬────────┐
+│ (iteration index) │ Key │ Values │
+├───────────────────┼─────┼────────┤
+│ 0 │ 1 │ "one" │
+│ 1 │ 2 │ "two" │
+└───────────────────┴─────┴────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table({
+ a: true,
+ b: { c: { d: 10 }, e: [1, 2, [5, 6]] },
+ f: "test",
+ g: new Set([1, 2, 3, "test"]),
+ h: new Map([[1, "one"]])
+ });
+ assertEquals(
+ out.toString(),
+ `┌─────────┬───────────┬───────────────────┬────────┐
+│ (index) │ c │ e │ Values │
+├─────────┼───────────┼───────────────────┼────────┤
+│ a │ │ │ true │
+│ b │ { d: 10 } │ [ 1, 2, [Array] ] │ │
+│ f │ │ │ "test" │
+│ g │ │ │ │
+│ h │ │ │ │
+└─────────┴───────────┴───────────────────┴────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table([
+ 1,
+ "test",
+ false,
+ { a: 10 },
+ ["test", { b: 20, c: "test" }]
+ ]);
+ assertEquals(
+ out.toString(),
+ `┌─────────┬────────┬──────────────────────┬────┬────────┐
+│ (index) │ 0 │ 1 │ a │ Values │
+├─────────┼────────┼──────────────────────┼────┼────────┤
+│ 0 │ │ │ │ 1 │
+│ 1 │ │ │ │ "test" │
+│ 2 │ │ │ │ false │
+│ 3 │ │ │ 10 │ │
+│ 4 │ "test" │ { b: 20, c: "test" } │ │ │
+└─────────┴────────┴──────────────────────┴────┴────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table([]);
+ assertEquals(
+ out.toString(),
+ `┌─────────┐
+│ (index) │
+├─────────┤
+└─────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table({});
+ assertEquals(
+ out.toString(),
+ `┌─────────┐
+│ (index) │
+├─────────┤
+└─────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table(new Set());
+ assertEquals(
+ out.toString(),
+ `┌───────────────────┐
+│ (iteration index) │
+├───────────────────┤
+└───────────────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table(new Map());
+ assertEquals(
+ out.toString(),
+ `┌───────────────────┐
+│ (iteration index) │
+├───────────────────┤
+└───────────────────┘
+`
+ );
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.table("test");
+ assertEquals(out.toString(), "test\n");
+ }
+ );
+});
+
+// console.log(Error) test
+test(function consoleLogShouldNotThrowError(): void {
+ let result = 0;
+ try {
+ console.log(new Error("foo"));
+ result = 1;
+ } catch (e) {
+ result = 2;
+ }
+ assertEquals(result, 1);
+
+ // output errors to the console should not include "Uncaught"
+ mockConsole(
+ (console, out): void => {
+ console.log(new Error("foo"));
+ assertEquals(out.toString().includes("Uncaught"), false);
+ }
+ );
+});
+
+// console.dir test
+test(function consoleDir(): void {
+ mockConsole(
+ (console, out): void => {
+ console.dir("DIR");
+ assertEquals(out.toString(), "DIR\n");
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.dir("DIR", { indentLevel: 2 });
+ assertEquals(out.toString(), " DIR\n");
+ }
+ );
+});
+
+// console.dir test
+test(function consoleDirXml(): void {
+ mockConsole(
+ (console, out): void => {
+ console.dirxml("DIRXML");
+ assertEquals(out.toString(), "DIRXML\n");
+ }
+ );
+ mockConsole(
+ (console, out): void => {
+ console.dirxml("DIRXML", { indentLevel: 2 });
+ assertEquals(out.toString(), " DIRXML\n");
+ }
+ );
+});
+
+// console.trace test
+test(function consoleTrace(): void {
+ mockConsole(
+ (console, _out, err): void => {
+ console.trace("%s", "custom message");
+ assert(err.toString().includes("Trace: custom message"));
+ }
+ );
+});
diff --git a/cli/js/copy_file.ts b/cli/js/copy_file.ts
new file mode 100644
index 000000000..94d2b63db
--- /dev/null
+++ b/cli/js/copy_file.ts
@@ -0,0 +1,30 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+/** Copies the contents of a file to another by name synchronously.
+ * Creates a new file if target does not exists, and if target exists,
+ * overwrites original content of the target file.
+ *
+ * It would also copy the permission of the original file
+ * to the destination.
+ *
+ * Deno.copyFileSync("from.txt", "to.txt");
+ */
+export function copyFileSync(from: string, to: string): void {
+ sendSync(dispatch.OP_COPY_FILE, { from, to });
+}
+
+/** Copies the contents of a file to another by name.
+ *
+ * Creates a new file if target does not exists, and if target exists,
+ * overwrites original content of the target file.
+ *
+ * It would also copy the permission of the original file
+ * to the destination.
+ *
+ * await Deno.copyFile("from.txt", "to.txt");
+ */
+export async function copyFile(from: string, to: string): Promise<void> {
+ await sendAsync(dispatch.OP_COPY_FILE, { from, to });
+}
diff --git a/cli/js/copy_file_test.ts b/cli/js/copy_file_test.ts
new file mode 100644
index 000000000..72ae43f3e
--- /dev/null
+++ b/cli/js/copy_file_test.ts
@@ -0,0 +1,163 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+function readFileString(filename: string): string {
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ return dec.decode(dataRead);
+}
+
+function writeFileString(filename: string, s: string): void {
+ const enc = new TextEncoder();
+ const data = enc.encode(s);
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+}
+
+function assertSameContent(filename1: string, filename2: string): void {
+ const data1 = Deno.readFileSync(filename1);
+ const data2 = Deno.readFileSync(filename2);
+ assertEquals(data1, data2);
+}
+
+testPerm({ read: true, write: true }, function copyFileSyncSuccess(): void {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ writeFileString(fromFilename, "Hello world!");
+ Deno.copyFileSync(fromFilename, toFilename);
+ // No change to original file
+ assertEquals(readFileString(fromFilename), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromFilename, toFilename);
+});
+
+testPerm({ write: true, read: true }, function copyFileSyncFailure(): void {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ // We skip initial writing here, from.txt does not exist
+ let err;
+ try {
+ Deno.copyFileSync(fromFilename, toFilename);
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: true, read: false }, function copyFileSyncPerm1(): void {
+ let caughtError = false;
+ try {
+ Deno.copyFileSync("/from.txt", "/to.txt");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ write: false, read: true }, function copyFileSyncPerm2(): void {
+ let caughtError = false;
+ try {
+ Deno.copyFileSync("/from.txt", "/to.txt");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true, write: true }, function copyFileSyncOverwrite(): void {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ writeFileString(fromFilename, "Hello world!");
+ // Make Dest exist and have different content
+ writeFileString(toFilename, "Goodbye!");
+ Deno.copyFileSync(fromFilename, toFilename);
+ // No change to original file
+ assertEquals(readFileString(fromFilename), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromFilename, toFilename);
+});
+
+testPerm({ read: true, write: true }, async function copyFileSuccess(): Promise<
+ void
+> {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ writeFileString(fromFilename, "Hello world!");
+ await Deno.copyFile(fromFilename, toFilename);
+ // No change to original file
+ assertEquals(readFileString(fromFilename), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromFilename, toFilename);
+});
+
+testPerm({ read: true, write: true }, async function copyFileFailure(): Promise<
+ void
+> {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ // We skip initial writing here, from.txt does not exist
+ let err;
+ try {
+ await Deno.copyFile(fromFilename, toFilename);
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm(
+ { read: true, write: true },
+ async function copyFileOverwrite(): Promise<void> {
+ const tempDir = Deno.makeTempDirSync();
+ const fromFilename = tempDir + "/from.txt";
+ const toFilename = tempDir + "/to.txt";
+ writeFileString(fromFilename, "Hello world!");
+ // Make Dest exist and have different content
+ writeFileString(toFilename, "Goodbye!");
+ await Deno.copyFile(fromFilename, toFilename);
+ // No change to original file
+ assertEquals(readFileString(fromFilename), "Hello world!");
+ // Original == Dest
+ assertSameContent(fromFilename, toFilename);
+ }
+);
+
+testPerm({ read: false, write: true }, async function copyFilePerm1(): Promise<
+ void
+> {
+ let caughtError = false;
+ try {
+ await Deno.copyFile("/from.txt", "/to.txt");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true, write: false }, async function copyFilePerm2(): Promise<
+ void
+> {
+ let caughtError = false;
+ try {
+ await Deno.copyFile("/from.txt", "/to.txt");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
diff --git a/cli/js/core.ts b/cli/js/core.ts
new file mode 100644
index 000000000..d394d822f
--- /dev/null
+++ b/cli/js/core.ts
@@ -0,0 +1,6 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { window } from "./window.ts";
+
+// This allows us to access core in API even if we
+// dispose window.Deno
+export const core = window.Deno.core as DenoCore;
diff --git a/cli/js/custom_event.ts b/cli/js/custom_event.ts
new file mode 100644
index 000000000..922abd4b1
--- /dev/null
+++ b/cli/js/custom_event.ts
@@ -0,0 +1,48 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types.ts";
+import * as event from "./event.ts";
+import { getPrivateValue, requiredArguments } from "./util.ts";
+
+// WeakMaps are recommended for private attributes (see MDN link below)
+// https://developer.mozilla.org/en-US/docs/Archive/Add-ons/Add-on_SDK/Guides/Contributor_s_Guide/Private_Properties#Using_WeakMaps
+export const customEventAttributes = new WeakMap();
+
+export class CustomEvent extends event.Event implements domTypes.CustomEvent {
+ constructor(
+ type: string,
+ customEventInitDict: domTypes.CustomEventInit = {}
+ ) {
+ requiredArguments("CustomEvent", arguments.length, 1);
+ super(type, customEventInitDict);
+ const { detail = null } = customEventInitDict;
+ customEventAttributes.set(this, { detail });
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ get detail(): any {
+ return getPrivateValue(this, customEventAttributes, "detail");
+ }
+
+ initCustomEvent(
+ type: string,
+ bubbles?: boolean,
+ cancelable?: boolean,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ detail?: any
+ ): void {
+ if (this.dispatched) {
+ return;
+ }
+
+ customEventAttributes.set(this, { detail });
+ }
+
+ get [Symbol.toStringTag](): string {
+ return "CustomEvent";
+ }
+}
+
+/** Built-in objects providing `get` methods for our
+ * interceptable JavaScript operations.
+ */
+Reflect.defineProperty(CustomEvent.prototype, "detail", { enumerable: true });
diff --git a/cli/js/custom_event_test.ts b/cli/js/custom_event_test.ts
new file mode 100644
index 000000000..4d2eb2c16
--- /dev/null
+++ b/cli/js/custom_event_test.ts
@@ -0,0 +1,27 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import { test, assertEquals } from "./test_util.ts";
+
+test(function customEventInitializedWithDetail(): void {
+ const type = "touchstart";
+ const detail = { message: "hello" };
+ const customEventInit = {
+ bubbles: true,
+ cancelable: true,
+ detail
+ } as CustomEventInit;
+ const event = new CustomEvent(type, customEventInit);
+
+ assertEquals(event.bubbles, true);
+ assertEquals(event.cancelable, true);
+ assertEquals(event.currentTarget, null);
+ assertEquals(event.detail, detail);
+ assertEquals(event.isTrusted, false);
+ assertEquals(event.target, null);
+ assertEquals(event.type, type);
+});
+
+test(function toStringShouldBeWebCompatibility(): void {
+ const type = "touchstart";
+ const event = new CustomEvent(type, {});
+ assertEquals(event.toString(), "[object CustomEvent]");
+});
diff --git a/cli/js/deno.ts b/cli/js/deno.ts
new file mode 100644
index 000000000..511e4f0ec
--- /dev/null
+++ b/cli/js/deno.ts
@@ -0,0 +1,119 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+// Public deno module.
+export { env, exit, isTTY, execPath, homeDir, hostname } from "./os.ts";
+export { chdir, cwd } from "./dir.ts";
+export {
+ File,
+ open,
+ openSync,
+ stdin,
+ stdout,
+ stderr,
+ read,
+ readSync,
+ write,
+ writeSync,
+ seek,
+ seekSync,
+ close,
+ OpenMode
+} from "./files.ts";
+export {
+ EOF,
+ copy,
+ toAsyncIterator,
+ SeekMode,
+ Reader,
+ SyncReader,
+ Writer,
+ SyncWriter,
+ Closer,
+ Seeker,
+ SyncSeeker,
+ ReadCloser,
+ WriteCloser,
+ ReadSeeker,
+ WriteSeeker,
+ ReadWriteCloser,
+ ReadWriteSeeker
+} from "./io.ts";
+export {
+ Buffer,
+ readAll,
+ readAllSync,
+ writeAll,
+ writeAllSync
+} from "./buffer.ts";
+export { mkdirSync, mkdir } from "./mkdir.ts";
+export {
+ makeTempDirSync,
+ makeTempDir,
+ MakeTempDirOptions
+} from "./make_temp_dir.ts";
+export { chmodSync, chmod } from "./chmod.ts";
+export { chownSync, chown } from "./chown.ts";
+export { utimeSync, utime } from "./utime.ts";
+export { removeSync, remove, RemoveOption } from "./remove.ts";
+export { renameSync, rename } from "./rename.ts";
+export { readFileSync, readFile } from "./read_file.ts";
+export { readDirSync, readDir } from "./read_dir.ts";
+export { copyFileSync, copyFile } from "./copy_file.ts";
+export { readlinkSync, readlink } from "./read_link.ts";
+export { statSync, lstatSync, stat, lstat } from "./stat.ts";
+export { linkSync, link } from "./link.ts";
+export { symlinkSync, symlink } from "./symlink.ts";
+export { writeFileSync, writeFile, WriteFileOptions } from "./write_file.ts";
+export { applySourceMap } from "./error_stack.ts";
+export { ErrorKind, DenoError } from "./errors.ts";
+export {
+ permissions,
+ revokePermission,
+ Permission,
+ Permissions
+} from "./permissions.ts";
+export { truncateSync, truncate } from "./truncate.ts";
+export { FileInfo } from "./file_info.ts";
+export { connect, dial, listen, Listener, Conn } from "./net.ts";
+export { dialTLS } from "./tls.ts";
+export { metrics, Metrics } from "./metrics.ts";
+export { resources } from "./resources.ts";
+export {
+ kill,
+ run,
+ RunOptions,
+ Process,
+ ProcessStatus,
+ Signal
+} from "./process.ts";
+export { inspect, customInspect } from "./console.ts";
+export { build, OperatingSystem, Arch } from "./build.ts";
+export { version } from "./version.ts";
+export const args: string[] = [];
+
+// These are internal Deno APIs. We are marking them as internal so they do not
+// appear in the runtime type library.
+/** @internal */
+export { core } from "./core.ts";
+
+/** @internal */
+export { setPrepareStackTrace } from "./error_stack.ts";
+
+// TODO Don't expose Console nor stringifyArgs.
+/** @internal */
+export { Console, stringifyArgs } from "./console.ts";
+// TODO Don't expose DomIterableMixin.
+/** @internal */
+export { DomIterableMixin } from "./mixins/dom_iterable.ts";
+
+/** The current process id of the runtime. */
+export let pid: number;
+
+/** Reflects the NO_COLOR environment variable: https://no-color.org/ */
+export let noColor: boolean;
+
+// TODO(ry) This should not be exposed to Deno.
+export function _setGlobals(pid_: number, noColor_: boolean): void {
+ pid = pid_;
+ noColor = noColor_;
+}
diff --git a/cli/js/diagnostics.ts b/cli/js/diagnostics.ts
new file mode 100644
index 000000000..7cdb154b9
--- /dev/null
+++ b/cli/js/diagnostics.ts
@@ -0,0 +1,217 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+// Diagnostic provides an abstraction for advice/errors received from a
+// compiler, which is strongly influenced by the format of TypeScript
+// diagnostics.
+
+/** The log category for a diagnostic message */
+export enum DiagnosticCategory {
+ Log = 0,
+ Debug = 1,
+ Info = 2,
+ Error = 3,
+ Warning = 4,
+ Suggestion = 5
+}
+
+export interface DiagnosticMessageChain {
+ message: string;
+ category: DiagnosticCategory;
+ code: number;
+ next?: DiagnosticMessageChain[];
+}
+
+export interface DiagnosticItem {
+ /** A string message summarizing the diagnostic. */
+ message: string;
+
+ /** An ordered array of further diagnostics. */
+ messageChain?: DiagnosticMessageChain;
+
+ /** Information related to the diagnostic. This is present when there is a
+ * suggestion or other additional diagnostic information */
+ relatedInformation?: DiagnosticItem[];
+
+ /** The text of the source line related to the diagnostic */
+ sourceLine?: string;
+
+ /** The line number that is related to the diagnostic */
+ lineNumber?: number;
+
+ /** The name of the script resource related to the diagnostic */
+ scriptResourceName?: string;
+
+ /** The start position related to the diagnostic */
+ startPosition?: number;
+
+ /** The end position related to the diagnostic */
+ endPosition?: number;
+
+ /** The category of the diagnostic */
+ category: DiagnosticCategory;
+
+ /** A number identifier */
+ code: number;
+
+ /** The the start column of the sourceLine related to the diagnostic */
+ startColumn?: number;
+
+ /** The end column of the sourceLine related to the diagnostic */
+ endColumn?: number;
+}
+
+export interface Diagnostic {
+ /** An array of diagnostic items. */
+ items: DiagnosticItem[];
+}
+
+interface SourceInformation {
+ sourceLine: string;
+ lineNumber: number;
+ scriptResourceName: string;
+ startColumn: number;
+ endColumn: number;
+}
+
+function fromDiagnosticCategory(
+ category: ts.DiagnosticCategory
+): DiagnosticCategory {
+ switch (category) {
+ case ts.DiagnosticCategory.Error:
+ return DiagnosticCategory.Error;
+ case ts.DiagnosticCategory.Message:
+ return DiagnosticCategory.Info;
+ case ts.DiagnosticCategory.Suggestion:
+ return DiagnosticCategory.Suggestion;
+ case ts.DiagnosticCategory.Warning:
+ return DiagnosticCategory.Warning;
+ default:
+ throw new Error(
+ `Unexpected DiagnosticCategory: "${category}"/"${
+ ts.DiagnosticCategory[category]
+ }"`
+ );
+ }
+}
+
+function getSourceInformation(
+ sourceFile: ts.SourceFile,
+ start: number,
+ length: number
+): SourceInformation {
+ const scriptResourceName = sourceFile.fileName;
+ const {
+ line: lineNumber,
+ character: startColumn
+ } = sourceFile.getLineAndCharacterOfPosition(start);
+ const endPosition = sourceFile.getLineAndCharacterOfPosition(start + length);
+ const endColumn =
+ lineNumber === endPosition.line ? endPosition.character : startColumn;
+ const lastLineInFile = sourceFile.getLineAndCharacterOfPosition(
+ sourceFile.text.length
+ ).line;
+ const lineStart = sourceFile.getPositionOfLineAndCharacter(lineNumber, 0);
+ const lineEnd =
+ lineNumber < lastLineInFile
+ ? sourceFile.getPositionOfLineAndCharacter(lineNumber + 1, 0)
+ : sourceFile.text.length;
+ const sourceLine = sourceFile.text
+ .slice(lineStart, lineEnd)
+ .replace(/\s+$/g, "")
+ .replace("\t", " ");
+ return {
+ sourceLine,
+ lineNumber,
+ scriptResourceName,
+ startColumn,
+ endColumn
+ };
+}
+
+/** Converts a TypeScript diagnostic message chain to a Deno one. */
+function fromDiagnosticMessageChain(
+ messageChain: ts.DiagnosticMessageChain[] | undefined
+): DiagnosticMessageChain[] | undefined {
+ if (!messageChain) {
+ return undefined;
+ }
+
+ return messageChain.map(({ messageText: message, code, category, next }) => {
+ return {
+ message,
+ code,
+ category: fromDiagnosticCategory(category),
+ next: fromDiagnosticMessageChain(next)
+ };
+ });
+}
+
+/** Parse out information from a TypeScript diagnostic structure. */
+function parseDiagnostic(
+ item: ts.Diagnostic | ts.DiagnosticRelatedInformation
+): DiagnosticItem {
+ const {
+ messageText,
+ category: sourceCategory,
+ code,
+ file,
+ start: startPosition,
+ length
+ } = item;
+ const sourceInfo =
+ file && startPosition && length
+ ? getSourceInformation(file, startPosition, length)
+ : undefined;
+ const endPosition =
+ startPosition && length ? startPosition + length : undefined;
+ const category = fromDiagnosticCategory(sourceCategory);
+
+ let message: string;
+ let messageChain: DiagnosticMessageChain | undefined;
+ if (typeof messageText === "string") {
+ message = messageText;
+ } else {
+ message = messageText.messageText;
+ messageChain = fromDiagnosticMessageChain([messageText])![0];
+ }
+
+ const base = {
+ message,
+ messageChain,
+ code,
+ category,
+ startPosition,
+ endPosition
+ };
+
+ return sourceInfo ? { ...base, ...sourceInfo } : base;
+}
+
+/** Convert a diagnostic related information array into a Deno diagnostic
+ * array. */
+function parseRelatedInformation(
+ relatedInformation: readonly ts.DiagnosticRelatedInformation[]
+): DiagnosticItem[] {
+ const result: DiagnosticItem[] = [];
+ for (const item of relatedInformation) {
+ result.push(parseDiagnostic(item));
+ }
+ return result;
+}
+
+/** Convert TypeScript diagnostics to Deno diagnostics. */
+export function fromTypeScriptDiagnostic(
+ diagnostics: readonly ts.Diagnostic[]
+): Diagnostic {
+ const items: DiagnosticItem[] = [];
+ for (const sourceDiagnostic of diagnostics) {
+ const item: DiagnosticItem = parseDiagnostic(sourceDiagnostic);
+ if (sourceDiagnostic.relatedInformation) {
+ item.relatedInformation = parseRelatedInformation(
+ sourceDiagnostic.relatedInformation
+ );
+ }
+ items.push(item);
+ }
+ return { items };
+}
diff --git a/cli/js/dir.ts b/cli/js/dir.ts
new file mode 100644
index 000000000..ef1111555
--- /dev/null
+++ b/cli/js/dir.ts
@@ -0,0 +1,22 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+/**
+ * `cwd()` Return a string representing the current working directory.
+ * If the current directory can be reached via multiple paths
+ * (due to symbolic links), `cwd()` may return
+ * any one of them.
+ * throws `NotFound` exception if directory not available
+ */
+export function cwd(): string {
+ return sendSync(dispatch.OP_CWD);
+}
+
+/**
+ * `chdir()` Change the current working directory to path.
+ * throws `NotFound` exception if directory not available
+ */
+export function chdir(directory: string): void {
+ sendSync(dispatch.OP_CHDIR, { directory });
+}
diff --git a/cli/js/dir_test.ts b/cli/js/dir_test.ts
new file mode 100644
index 000000000..6c4e36d7a
--- /dev/null
+++ b/cli/js/dir_test.ts
@@ -0,0 +1,54 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, testPerm, assert, assertEquals } from "./test_util.ts";
+
+test(function dirCwdNotNull(): void {
+ assert(Deno.cwd() != null);
+});
+
+testPerm({ write: true }, function dirCwdChdirSuccess(): void {
+ const initialdir = Deno.cwd();
+ const path = Deno.makeTempDirSync();
+ Deno.chdir(path);
+ const current = Deno.cwd();
+ if (Deno.build.os === "mac") {
+ assertEquals(current, "/private" + path);
+ } else {
+ assertEquals(current, path);
+ }
+ Deno.chdir(initialdir);
+});
+
+testPerm({ write: true }, function dirCwdError(): void {
+ // excluding windows since it throws resource busy, while removeSync
+ if (["linux", "mac"].includes(Deno.build.os)) {
+ const initialdir = Deno.cwd();
+ const path = Deno.makeTempDirSync();
+ Deno.chdir(path);
+ Deno.removeSync(path);
+ try {
+ Deno.cwd();
+ throw Error("current directory removed, should throw error");
+ } catch (err) {
+ if (err instanceof Deno.DenoError) {
+ console.log(err.name === "NotFound");
+ } else {
+ throw Error("raised different exception");
+ }
+ }
+ Deno.chdir(initialdir);
+ }
+});
+
+testPerm({ write: true }, function dirChdirError(): void {
+ const path = Deno.makeTempDirSync() + "test";
+ try {
+ Deno.chdir(path);
+ throw Error("directory not available, should throw error");
+ } catch (err) {
+ if (err instanceof Deno.DenoError) {
+ console.log(err.name === "NotFound");
+ } else {
+ throw Error("raised different exception");
+ }
+ }
+});
diff --git a/cli/js/dispatch.ts b/cli/js/dispatch.ts
new file mode 100644
index 000000000..bff4d0f5b
--- /dev/null
+++ b/cli/js/dispatch.ts
@@ -0,0 +1,110 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as minimal from "./dispatch_minimal.ts";
+import * as json from "./dispatch_json.ts";
+
+// These consts are shared with Rust. Update with care.
+export let OP_READ: number;
+export let OP_WRITE: number;
+export let OP_EXIT: number;
+export let OP_IS_TTY: number;
+export let OP_ENV: number;
+export let OP_EXEC_PATH: number;
+export let OP_UTIME: number;
+export let OP_SET_ENV: number;
+export let OP_GET_ENV: number;
+export let OP_HOME_DIR: number;
+export let OP_START: number;
+export let OP_APPLY_SOURCE_MAP: number;
+export let OP_FORMAT_ERROR: number;
+export let OP_CACHE: number;
+export let OP_FETCH_SOURCE_FILES: number;
+export let OP_OPEN: number;
+export let OP_CLOSE: number;
+export let OP_SEEK: number;
+export let OP_FETCH: number;
+export let OP_METRICS: number;
+export let OP_REPL_START: number;
+export let OP_REPL_READLINE: number;
+export let OP_ACCEPT: number;
+export let OP_DIAL: number;
+export let OP_SHUTDOWN: number;
+export let OP_LISTEN: number;
+export let OP_RESOURCES: number;
+export let OP_GET_RANDOM_VALUES: number;
+export let OP_GLOBAL_TIMER_STOP: number;
+export let OP_GLOBAL_TIMER: number;
+export let OP_NOW: number;
+export let OP_PERMISSIONS: number;
+export let OP_REVOKE_PERMISSION: number;
+export let OP_CREATE_WORKER: number;
+export let OP_HOST_GET_WORKER_CLOSED: number;
+export let OP_HOST_POST_MESSAGE: number;
+export let OP_HOST_GET_MESSAGE: number;
+export let OP_WORKER_POST_MESSAGE: number;
+export let OP_WORKER_GET_MESSAGE: number;
+export let OP_RUN: number;
+export let OP_RUN_STATUS: number;
+export let OP_KILL: number;
+export let OP_CHDIR: number;
+export let OP_MKDIR: number;
+export let OP_CHMOD: number;
+export let OP_CHOWN: number;
+export let OP_REMOVE: number;
+export let OP_COPY_FILE: number;
+export let OP_STAT: number;
+export let OP_READ_DIR: number;
+export let OP_RENAME: number;
+export let OP_LINK: number;
+export let OP_SYMLINK: number;
+export let OP_READ_LINK: number;
+export let OP_TRUNCATE: number;
+export let OP_MAKE_TEMP_DIR: number;
+export let OP_CWD: number;
+export let OP_FETCH_ASSET: number;
+export let OP_DIAL_TLS: number;
+export let OP_HOSTNAME: number;
+
+export function asyncMsgFromRust(opId: number, ui8: Uint8Array): void {
+ switch (opId) {
+ case OP_WRITE:
+ case OP_READ:
+ minimal.asyncMsgFromRust(opId, ui8);
+ break;
+ case OP_EXIT:
+ case OP_IS_TTY:
+ case OP_ENV:
+ case OP_EXEC_PATH:
+ case OP_UTIME:
+ case OP_OPEN:
+ case OP_SEEK:
+ case OP_FETCH:
+ case OP_REPL_START:
+ case OP_REPL_READLINE:
+ case OP_ACCEPT:
+ case OP_DIAL:
+ case OP_GLOBAL_TIMER:
+ case OP_HOST_GET_WORKER_CLOSED:
+ case OP_HOST_GET_MESSAGE:
+ case OP_WORKER_GET_MESSAGE:
+ case OP_RUN_STATUS:
+ case OP_MKDIR:
+ case OP_CHMOD:
+ case OP_CHOWN:
+ case OP_REMOVE:
+ case OP_COPY_FILE:
+ case OP_STAT:
+ case OP_READ_DIR:
+ case OP_RENAME:
+ case OP_LINK:
+ case OP_SYMLINK:
+ case OP_READ_LINK:
+ case OP_TRUNCATE:
+ case OP_MAKE_TEMP_DIR:
+ case OP_DIAL_TLS:
+ case OP_FETCH_SOURCE_FILES:
+ json.asyncMsgFromRust(opId, ui8);
+ break;
+ default:
+ throw Error("bad async opId");
+ }
+}
diff --git a/cli/js/dispatch_json.ts b/cli/js/dispatch_json.ts
new file mode 100644
index 000000000..572ec855a
--- /dev/null
+++ b/cli/js/dispatch_json.ts
@@ -0,0 +1,86 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as util from "./util.ts";
+import { TextEncoder, TextDecoder } from "./text_encoding.ts";
+import { core } from "./core.ts";
+import { ErrorKind, DenoError } from "./errors.ts";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type Ok = any;
+
+interface JsonError {
+ kind: ErrorKind;
+ message: string;
+}
+
+interface JsonResponse {
+ ok?: Ok;
+ err?: JsonError;
+ promiseId?: number; // Only present in async messages.
+}
+
+const promiseTable = new Map<number, util.Resolvable<JsonResponse>>();
+let _nextPromiseId = 1;
+
+function nextPromiseId(): number {
+ return _nextPromiseId++;
+}
+
+function decode(ui8: Uint8Array): JsonResponse {
+ const s = new TextDecoder().decode(ui8);
+ return JSON.parse(s) as JsonResponse;
+}
+
+function encode(args: object): Uint8Array {
+ const s = JSON.stringify(args);
+ return new TextEncoder().encode(s);
+}
+
+function unwrapResponse(res: JsonResponse): Ok {
+ if (res.err != null) {
+ throw new DenoError(res.err!.kind, res.err!.message);
+ }
+ util.assert(res.ok != null);
+ return res.ok!;
+}
+
+export function asyncMsgFromRust(opId: number, resUi8: Uint8Array): void {
+ const res = decode(resUi8);
+ util.assert(res.promiseId != null);
+
+ const promise = promiseTable.get(res.promiseId!);
+ util.assert(promise != null);
+ promiseTable.delete(res.promiseId!);
+ promise!.resolve(res);
+}
+
+export function sendSync(
+ opId: number,
+ args: object = {},
+ zeroCopy?: Uint8Array
+): Ok {
+ const argsUi8 = encode(args);
+ const resUi8 = core.dispatch(opId, argsUi8, zeroCopy);
+ util.assert(resUi8 != null);
+
+ const res = decode(resUi8!);
+ util.assert(res.promiseId == null);
+ return unwrapResponse(res);
+}
+
+export async function sendAsync(
+ opId: number,
+ args: object = {},
+ zeroCopy?: Uint8Array
+): Promise<Ok> {
+ const promiseId = nextPromiseId();
+ args = Object.assign(args, { promiseId });
+ const promise = util.createResolvable<Ok>();
+ promiseTable.set(promiseId, promise);
+
+ const argsUi8 = encode(args);
+ const resUi8 = core.dispatch(opId, argsUi8, zeroCopy);
+ util.assert(resUi8 == null);
+
+ const res = await promise;
+ return unwrapResponse(res);
+}
diff --git a/cli/js/dispatch_json_test.ts b/cli/js/dispatch_json_test.ts
new file mode 100644
index 000000000..11dadc620
--- /dev/null
+++ b/cli/js/dispatch_json_test.ts
@@ -0,0 +1,19 @@
+import { testPerm, assertMatch, unreachable } from "./test_util.ts";
+
+const openErrorStackPattern = new RegExp(
+ `^.*
+ at unwrapResponse \\(.*dispatch_json\\.ts:.*\\)
+ at Object.sendAsync \\(.*dispatch_json\\.ts:.*\\)
+ at async Object\\.open \\(.*files\\.ts:.*\\).*$`,
+ "ms"
+);
+
+testPerm({ read: true }, async function sendAsyncStackTrace(): Promise<void> {
+ await Deno.open("nonexistent.txt")
+ .then(unreachable)
+ .catch(
+ (error): void => {
+ assertMatch(error.stack, openErrorStackPattern);
+ }
+ );
+});
diff --git a/cli/js/dispatch_minimal.ts b/cli/js/dispatch_minimal.ts
new file mode 100644
index 000000000..98636f85b
--- /dev/null
+++ b/cli/js/dispatch_minimal.ts
@@ -0,0 +1,80 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as util from "./util.ts";
+import { core } from "./core.ts";
+
+const promiseTableMin = new Map<number, util.Resolvable<number>>();
+// Note it's important that promiseId starts at 1 instead of 0, because sync
+// messages are indicated with promiseId 0. If we ever add wrap around logic for
+// overflows, this should be taken into account.
+let _nextPromiseId = 1;
+
+function nextPromiseId(): number {
+ return _nextPromiseId++;
+}
+
+export interface RecordMinimal {
+ promiseId: number;
+ opId: number; // Maybe better called dispatchId
+ arg: number;
+ result: number;
+}
+
+export function recordFromBufMinimal(
+ opId: number,
+ buf32: Int32Array
+): RecordMinimal {
+ if (buf32.length != 3) {
+ throw Error("Bad message");
+ }
+ return {
+ promiseId: buf32[0],
+ opId,
+ arg: buf32[1],
+ result: buf32[2]
+ };
+}
+
+const scratch32 = new Int32Array(3);
+const scratchBytes = new Uint8Array(
+ scratch32.buffer,
+ scratch32.byteOffset,
+ scratch32.byteLength
+);
+util.assert(scratchBytes.byteLength === scratch32.length * 4);
+
+export function asyncMsgFromRust(opId: number, ui8: Uint8Array): void {
+ const buf32 = new Int32Array(ui8.buffer, ui8.byteOffset, ui8.byteLength / 4);
+ const record = recordFromBufMinimal(opId, buf32);
+ const { promiseId, result } = record;
+ const promise = promiseTableMin.get(promiseId);
+ promiseTableMin.delete(promiseId);
+ promise!.resolve(result);
+}
+
+export function sendAsyncMinimal(
+ opId: number,
+ arg: number,
+ zeroCopy: Uint8Array
+): Promise<number> {
+ const promiseId = nextPromiseId(); // AKA cmdId
+ scratch32[0] = promiseId;
+ scratch32[1] = arg;
+ scratch32[2] = 0; // result
+ const promise = util.createResolvable<number>();
+ promiseTableMin.set(promiseId, promise);
+ core.dispatch(opId, scratchBytes, zeroCopy);
+ return promise;
+}
+
+export function sendSyncMinimal(
+ opId: number,
+ arg: number,
+ zeroCopy: Uint8Array
+): number {
+ scratch32[0] = 0; // promiseId 0 indicates sync
+ scratch32[1] = arg;
+ const res = core.dispatch(opId, scratchBytes, zeroCopy)!;
+ const res32 = new Int32Array(res.buffer, res.byteOffset, 3);
+ const resRecord = recordFromBufMinimal(opId, res32);
+ return resRecord.result;
+}
diff --git a/cli/js/dom_file.ts b/cli/js/dom_file.ts
new file mode 100644
index 000000000..1f9bf93a5
--- /dev/null
+++ b/cli/js/dom_file.ts
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types.ts";
+import * as blob from "./blob.ts";
+
+export class DomFileImpl extends blob.DenoBlob implements domTypes.DomFile {
+ lastModified: number;
+ name: string;
+
+ constructor(
+ fileBits: domTypes.BlobPart[],
+ fileName: string,
+ options?: domTypes.FilePropertyBag
+ ) {
+ options = options || {};
+ super(fileBits, options);
+
+ // 4.1.2.1 Replace any "/" character (U+002F SOLIDUS)
+ // with a ":" (U + 003A COLON)
+ this.name = String(fileName).replace(/\u002F/g, "\u003A");
+ // 4.1.3.3 If lastModified is not provided, set lastModified to the current
+ // date and time represented in number of milliseconds since the Unix Epoch.
+ this.lastModified = options.lastModified || Date.now();
+ }
+}
diff --git a/cli/js/dom_types.ts b/cli/js/dom_types.ts
new file mode 100644
index 000000000..308505cf5
--- /dev/null
+++ b/cli/js/dom_types.ts
@@ -0,0 +1,625 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+/*! ****************************************************************************
+Copyright (c) Microsoft Corporation. All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at http://www.apache.org/licenses/LICENSE-2.0
+
+THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+MERCHANTABLITY OR NON-INFRINGEMENT.
+
+See the Apache Version 2.0 License for specific language governing permissions
+and limitations under the License.
+*******************************************************************************/
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+export type BufferSource = ArrayBufferView | ArrayBuffer;
+
+export type HeadersInit =
+ | Headers
+ | Array<[string, string]>
+ | Record<string, string>;
+export type URLSearchParamsInit = string | string[][] | Record<string, string>;
+type BodyInit =
+ | Blob
+ | BufferSource
+ | FormData
+ | URLSearchParams
+ | ReadableStream
+ | string;
+export type RequestInfo = Request | string;
+type ReferrerPolicy =
+ | ""
+ | "no-referrer"
+ | "no-referrer-when-downgrade"
+ | "origin-only"
+ | "origin-when-cross-origin"
+ | "unsafe-url";
+export type BlobPart = BufferSource | Blob | string;
+export type FormDataEntryValue = DomFile | string;
+
+export interface DomIterable<K, V> {
+ keys(): IterableIterator<K>;
+ values(): IterableIterator<V>;
+ entries(): IterableIterator<[K, V]>;
+ [Symbol.iterator](): IterableIterator<[K, V]>;
+ forEach(
+ callback: (value: V, key: K, parent: this) => void,
+ thisArg?: any
+ ): void;
+}
+
+type EndingType = "transparent" | "native";
+
+export interface BlobPropertyBag {
+ type?: string;
+ ending?: EndingType;
+}
+
+interface AbortSignalEventMap {
+ abort: ProgressEvent;
+}
+
+// https://dom.spec.whatwg.org/#node
+export enum NodeType {
+ ELEMENT_NODE = 1,
+ TEXT_NODE = 3,
+ DOCUMENT_FRAGMENT_NODE = 11
+}
+
+export const eventTargetHost: unique symbol = Symbol();
+export const eventTargetListeners: unique symbol = Symbol();
+export const eventTargetMode: unique symbol = Symbol();
+export const eventTargetNodeType: unique symbol = Symbol();
+
+export interface EventTarget {
+ [eventTargetHost]: EventTarget | null;
+ [eventTargetListeners]: { [type in string]: EventListener[] };
+ [eventTargetMode]: string;
+ [eventTargetNodeType]: NodeType;
+ addEventListener(
+ type: string,
+ callback: (event: Event) => void | null,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+ dispatchEvent(event: Event): boolean;
+ removeEventListener(
+ type: string,
+ callback?: (event: Event) => void | null,
+ options?: EventListenerOptions | boolean
+ ): void;
+}
+
+export interface ProgressEventInit extends EventInit {
+ lengthComputable?: boolean;
+ loaded?: number;
+ total?: number;
+}
+
+export interface URLSearchParams extends DomIterable<string, string> {
+ /**
+ * Appends a specified key/value pair as a new search parameter.
+ */
+ append(name: string, value: string): void;
+ /**
+ * Deletes the given search parameter, and its associated value,
+ * from the list of all search parameters.
+ */
+ delete(name: string): void;
+ /**
+ * Returns the first value associated to the given search parameter.
+ */
+ get(name: string): string | null;
+ /**
+ * Returns all the values association with a given search parameter.
+ */
+ getAll(name: string): string[];
+ /**
+ * Returns a Boolean indicating if such a search parameter exists.
+ */
+ has(name: string): boolean;
+ /**
+ * Sets the value associated to a given search parameter to the given value.
+ * If there were several values, delete the others.
+ */
+ set(name: string, value: string): void;
+ /**
+ * Sort all key/value pairs contained in this object in place
+ * and return undefined. The sort order is according to Unicode
+ * code points of the keys.
+ */
+ sort(): void;
+ /**
+ * Returns a query string suitable for use in a URL.
+ */
+ toString(): string;
+ /**
+ * Iterates over each name-value pair in the query
+ * and invokes the given function.
+ */
+ forEach(
+ callbackfn: (value: string, key: string, parent: this) => void,
+ thisArg?: any
+ ): void;
+}
+
+export interface EventListener {
+ handleEvent(event: Event): void;
+ readonly callback: (event: Event) => void | null;
+ readonly options: boolean | AddEventListenerOptions;
+}
+
+export interface EventInit {
+ bubbles?: boolean;
+ cancelable?: boolean;
+ composed?: boolean;
+}
+
+export interface CustomEventInit extends EventInit {
+ detail?: any;
+}
+
+export enum EventPhase {
+ NONE = 0,
+ CAPTURING_PHASE = 1,
+ AT_TARGET = 2,
+ BUBBLING_PHASE = 3
+}
+
+export interface EventPath {
+ item: EventTarget;
+ itemInShadowTree: boolean;
+ relatedTarget: EventTarget | null;
+ rootOfClosedTree: boolean;
+ slotInClosedTree: boolean;
+ target: EventTarget | null;
+ touchTargetList: EventTarget[];
+}
+
+export interface Event {
+ readonly type: string;
+ target: EventTarget | null;
+ currentTarget: EventTarget | null;
+ composedPath(): EventPath[];
+
+ eventPhase: number;
+
+ stopPropagation(): void;
+ stopImmediatePropagation(): void;
+
+ readonly bubbles: boolean;
+ readonly cancelable: boolean;
+ preventDefault(): void;
+ readonly defaultPrevented: boolean;
+ readonly composed: boolean;
+
+ isTrusted: boolean;
+ readonly timeStamp: Date;
+
+ dispatched: boolean;
+ readonly initialized: boolean;
+ inPassiveListener: boolean;
+ cancelBubble: boolean;
+ cancelBubbleImmediately: boolean;
+ path: EventPath[];
+ relatedTarget: EventTarget | null;
+}
+
+export interface CustomEvent extends Event {
+ readonly detail: any;
+ initCustomEvent(
+ type: string,
+ bubbles?: boolean,
+ cancelable?: boolean,
+ detail?: any | null
+ ): void;
+}
+
+export interface DomFile extends Blob {
+ readonly lastModified: number;
+ readonly name: string;
+}
+
+export interface DomFileConstructor {
+ new (bits: BlobPart[], filename: string, options?: FilePropertyBag): DomFile;
+ prototype: DomFile;
+}
+
+export interface FilePropertyBag extends BlobPropertyBag {
+ lastModified?: number;
+}
+
+interface ProgressEvent extends Event {
+ readonly lengthComputable: boolean;
+ readonly loaded: number;
+ readonly total: number;
+}
+
+export interface EventListenerOptions {
+ capture: boolean;
+}
+
+export interface AddEventListenerOptions extends EventListenerOptions {
+ once: boolean;
+ passive: boolean;
+}
+
+interface AbortSignal extends EventTarget {
+ readonly aborted: boolean;
+ onabort: ((this: AbortSignal, ev: ProgressEvent) => any) | null;
+ addEventListener<K extends keyof AbortSignalEventMap>(
+ type: K,
+ listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+ addEventListener(
+ type: string,
+ listener: EventListener,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+ removeEventListener<K extends keyof AbortSignalEventMap>(
+ type: K,
+ listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any,
+ options?: boolean | EventListenerOptions
+ ): void;
+ removeEventListener(
+ type: string,
+ listener: EventListener,
+ options?: boolean | EventListenerOptions
+ ): void;
+}
+
+export interface ReadableStream {
+ readonly locked: boolean;
+ cancel(): Promise<void>;
+ getReader(): ReadableStreamReader;
+ tee(): [ReadableStream, ReadableStream];
+}
+
+export interface ReadableStreamReader {
+ cancel(): Promise<void>;
+ read(): Promise<any>;
+ releaseLock(): void;
+}
+
+export interface FormData extends DomIterable<string, FormDataEntryValue> {
+ append(name: string, value: string | Blob, fileName?: string): void;
+ delete(name: string): void;
+ get(name: string): FormDataEntryValue | null;
+ getAll(name: string): FormDataEntryValue[];
+ has(name: string): boolean;
+ set(name: string, value: string | Blob, fileName?: string): void;
+}
+
+export interface FormDataConstructor {
+ new (): FormData;
+ prototype: FormData;
+}
+
+/** A blob object represents a file-like object of immutable, raw data. */
+export interface Blob {
+ /** The size, in bytes, of the data contained in the `Blob` object. */
+ readonly size: number;
+ /** A string indicating the media type of the data contained in the `Blob`.
+ * If the type is unknown, this string is empty.
+ */
+ readonly type: string;
+ /** Returns a new `Blob` object containing the data in the specified range of
+ * bytes of the source `Blob`.
+ */
+ slice(start?: number, end?: number, contentType?: string): Blob;
+}
+
+export interface Body {
+ /** A simple getter used to expose a `ReadableStream` of the body contents. */
+ readonly body: ReadableStream | null;
+ /** Stores a `Boolean` that declares whether the body has been used in a
+ * response yet.
+ */
+ readonly bodyUsed: boolean;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with an `ArrayBuffer`.
+ */
+ arrayBuffer(): Promise<ArrayBuffer>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `Blob`.
+ */
+ blob(): Promise<Blob>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `FormData` object.
+ */
+ formData(): Promise<FormData>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with the result of parsing the body text as JSON.
+ */
+ json(): Promise<any>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `USVString` (text).
+ */
+ text(): Promise<string>;
+}
+
+export interface Headers extends DomIterable<string, string> {
+ /** Appends a new value onto an existing header inside a `Headers` object, or
+ * adds the header if it does not already exist.
+ */
+ append(name: string, value: string): void;
+ /** Deletes a header from a `Headers` object. */
+ delete(name: string): void;
+ /** Returns an iterator allowing to go through all key/value pairs
+ * contained in this Headers object. The both the key and value of each pairs
+ * are ByteString objects.
+ */
+ entries(): IterableIterator<[string, string]>;
+ /** Returns a `ByteString` sequence of all the values of a header within a
+ * `Headers` object with a given name.
+ */
+ get(name: string): string | null;
+ /** Returns a boolean stating whether a `Headers` object contains a certain
+ * header.
+ */
+ has(name: string): boolean;
+ /** Returns an iterator allowing to go through all keys contained in
+ * this Headers object. The keys are ByteString objects.
+ */
+ keys(): IterableIterator<string>;
+ /** Sets a new value for an existing header inside a Headers object, or adds
+ * the header if it does not already exist.
+ */
+ set(name: string, value: string): void;
+ /** Returns an iterator allowing to go through all values contained in
+ * this Headers object. The values are ByteString objects.
+ */
+ values(): IterableIterator<string>;
+ forEach(
+ callbackfn: (value: string, key: string, parent: this) => void,
+ thisArg?: any
+ ): void;
+ /** The Symbol.iterator well-known symbol specifies the default
+ * iterator for this Headers object
+ */
+ [Symbol.iterator](): IterableIterator<[string, string]>;
+}
+
+export interface HeadersConstructor {
+ new (init?: HeadersInit): Headers;
+ prototype: Headers;
+}
+
+type RequestCache =
+ | "default"
+ | "no-store"
+ | "reload"
+ | "no-cache"
+ | "force-cache"
+ | "only-if-cached";
+type RequestCredentials = "omit" | "same-origin" | "include";
+type RequestDestination =
+ | ""
+ | "audio"
+ | "audioworklet"
+ | "document"
+ | "embed"
+ | "font"
+ | "image"
+ | "manifest"
+ | "object"
+ | "paintworklet"
+ | "report"
+ | "script"
+ | "sharedworker"
+ | "style"
+ | "track"
+ | "video"
+ | "worker"
+ | "xslt";
+type RequestMode = "navigate" | "same-origin" | "no-cors" | "cors";
+type RequestRedirect = "follow" | "error" | "manual";
+type ResponseType =
+ | "basic"
+ | "cors"
+ | "default"
+ | "error"
+ | "opaque"
+ | "opaqueredirect";
+
+export interface RequestInit {
+ body?: BodyInit | null;
+ cache?: RequestCache;
+ credentials?: RequestCredentials;
+ headers?: HeadersInit;
+ integrity?: string;
+ keepalive?: boolean;
+ method?: string;
+ mode?: RequestMode;
+ redirect?: RequestRedirect;
+ referrer?: string;
+ referrerPolicy?: ReferrerPolicy;
+ signal?: AbortSignal | null;
+ window?: any;
+}
+
+export interface ResponseInit {
+ headers?: HeadersInit;
+ status?: number;
+ statusText?: string;
+}
+
+export interface RequestConstructor {
+ new (input: RequestInfo, init?: RequestInit): Request;
+ prototype: Request;
+}
+
+export interface Request extends Body {
+ /** Returns the cache mode associated with request, which is a string
+ * indicating how the the request will interact with the browser's cache when
+ * fetching.
+ */
+ readonly cache?: RequestCache;
+ /** Returns the credentials mode associated with request, which is a string
+ * indicating whether credentials will be sent with the request always, never,
+ * or only when sent to a same-origin URL.
+ */
+ readonly credentials?: RequestCredentials;
+ /** Returns the kind of resource requested by request, (e.g., `document` or
+ * `script`).
+ */
+ readonly destination?: RequestDestination;
+ /** Returns a Headers object consisting of the headers associated with
+ * request.
+ *
+ * Note that headers added in the network layer by the user agent
+ * will not be accounted for in this object, (e.g., the `Host` header).
+ */
+ readonly headers: Headers;
+ /** Returns request's subresource integrity metadata, which is a cryptographic
+ * hash of the resource being fetched. Its value consists of multiple hashes
+ * separated by whitespace. [SRI]
+ */
+ readonly integrity?: string;
+ /** Returns a boolean indicating whether or not request is for a history
+ * navigation (a.k.a. back-forward navigation).
+ */
+ readonly isHistoryNavigation?: boolean;
+ /** Returns a boolean indicating whether or not request is for a reload
+ * navigation.
+ */
+ readonly isReloadNavigation?: boolean;
+ /** Returns a boolean indicating whether or not request can outlive the global
+ * in which it was created.
+ */
+ readonly keepalive?: boolean;
+ /** Returns request's HTTP method, which is `GET` by default. */
+ readonly method: string;
+ /** Returns the mode associated with request, which is a string indicating
+ * whether the request will use CORS, or will be restricted to same-origin
+ * URLs.
+ */
+ readonly mode?: RequestMode;
+ /** Returns the redirect mode associated with request, which is a string
+ * indicating how redirects for the request will be handled during fetching.
+ *
+ * A request will follow redirects by default.
+ */
+ readonly redirect?: RequestRedirect;
+ /** Returns the referrer of request. Its value can be a same-origin URL if
+ * explicitly set in init, the empty string to indicate no referrer, and
+ * `about:client` when defaulting to the global's default.
+ *
+ * This is used during fetching to determine the value of the `Referer`
+ * header of the request being made.
+ */
+ readonly referrer?: string;
+ /** Returns the referrer policy associated with request. This is used during
+ * fetching to compute the value of the request's referrer.
+ */
+ readonly referrerPolicy?: ReferrerPolicy;
+ /** Returns the signal associated with request, which is an AbortSignal object
+ * indicating whether or not request has been aborted, and its abort event
+ * handler.
+ */
+ readonly signal?: AbortSignal;
+ /** Returns the URL of request as a string. */
+ readonly url: string;
+ clone(): Request;
+}
+
+export interface Response extends Body {
+ /** Contains the `Headers` object associated with the response. */
+ readonly headers: Headers;
+ /** Contains a boolean stating whether the response was successful (status in
+ * the range 200-299) or not.
+ */
+ readonly ok: boolean;
+ /** Indicates whether or not the response is the result of a redirect; that
+ * is, its URL list has more than one entry.
+ */
+ readonly redirected: boolean;
+ /** Contains the status code of the response (e.g., `200` for a success). */
+ readonly status: number;
+ /** Contains the status message corresponding to the status code (e.g., `OK`
+ * for `200`).
+ */
+ readonly statusText: string;
+ readonly trailer: Promise<Headers>;
+ /** Contains the type of the response (e.g., `basic`, `cors`). */
+ readonly type: ResponseType;
+ /** Contains the URL of the response. */
+ readonly url: string;
+ /** Creates a clone of a `Response` object. */
+ clone(): Response;
+}
+
+export interface Location {
+ /**
+ * Returns a DOMStringList object listing the origins of the ancestor browsing
+ * contexts, from the parent browsing context to the top-level browsing
+ * context.
+ */
+ readonly ancestorOrigins: string[];
+ /**
+ * Returns the Location object's URL's fragment (includes leading "#" if
+ * non-empty).
+ * Can be set, to navigate to the same URL with a changed fragment (ignores
+ * leading "#").
+ */
+ hash: string;
+ /**
+ * Returns the Location object's URL's host and port (if different from the
+ * default port for the scheme). Can be set, to navigate to the same URL with
+ * a changed host and port.
+ */
+ host: string;
+ /**
+ * Returns the Location object's URL's host. Can be set, to navigate to the
+ * same URL with a changed host.
+ */
+ hostname: string;
+ /**
+ * Returns the Location object's URL. Can be set, to navigate to the given
+ * URL.
+ */
+ href: string;
+ /** Returns the Location object's URL's origin. */
+ readonly origin: string;
+ /**
+ * Returns the Location object's URL's path.
+ * Can be set, to navigate to the same URL with a changed path.
+ */
+ pathname: string;
+ /**
+ * Returns the Location object's URL's port.
+ * Can be set, to navigate to the same URL with a changed port.
+ */
+ port: string;
+ /**
+ * Returns the Location object's URL's scheme.
+ * Can be set, to navigate to the same URL with a changed scheme.
+ */
+ protocol: string;
+ /**
+ * Returns the Location object's URL's query (includes leading "?" if
+ * non-empty). Can be set, to navigate to the same URL with a changed query
+ * (ignores leading "?").
+ */
+ search: string;
+ /**
+ * Navigates to the given URL.
+ */
+ assign(url: string): void;
+ /**
+ * Reloads the current page.
+ */
+ reload(): void;
+ /** @deprecated */
+ reload(forcedReload: boolean): void;
+ /**
+ * Removes the current page from the session history and navigates to the
+ * given URL.
+ */
+ replace(url: string): void;
+}
diff --git a/cli/js/dom_util.ts b/cli/js/dom_util.ts
new file mode 100644
index 000000000..725a35aaf
--- /dev/null
+++ b/cli/js/dom_util.ts
@@ -0,0 +1,85 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+// Utility functions for DOM nodes
+import * as domTypes from "./dom_types.ts";
+
+export function isNode(nodeImpl: domTypes.EventTarget | null): boolean {
+ return Boolean(nodeImpl && "nodeType" in nodeImpl);
+}
+
+export function isShadowRoot(nodeImpl: domTypes.EventTarget | null): boolean {
+ return Boolean(
+ nodeImpl &&
+ nodeImpl[domTypes.eventTargetNodeType] ===
+ domTypes.NodeType.DOCUMENT_FRAGMENT_NODE &&
+ nodeImpl[domTypes.eventTargetHost] != null
+ );
+}
+
+export function isSlotable(nodeImpl: domTypes.EventTarget | null): boolean {
+ return Boolean(
+ nodeImpl &&
+ (nodeImpl[domTypes.eventTargetNodeType] ===
+ domTypes.NodeType.ELEMENT_NODE ||
+ nodeImpl[domTypes.eventTargetNodeType] === domTypes.NodeType.TEXT_NODE)
+ );
+}
+
+// https://dom.spec.whatwg.org/#node-trees
+// const domSymbolTree = Symbol("DOM Symbol Tree");
+
+// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor
+export function isShadowInclusiveAncestor(
+ ancestor: domTypes.EventTarget | null,
+ node: domTypes.EventTarget | null
+): boolean {
+ while (isNode(node)) {
+ if (node === ancestor) {
+ return true;
+ }
+
+ if (isShadowRoot(node)) {
+ node = node && node[domTypes.eventTargetHost];
+ } else {
+ node = null; // domSymbolTree.parent(node);
+ }
+ }
+
+ return false;
+}
+
+export function getRoot(
+ node: domTypes.EventTarget | null
+): domTypes.EventTarget | null {
+ const root = node;
+
+ // for (const ancestor of domSymbolTree.ancestorsIterator(node)) {
+ // root = ancestor;
+ // }
+
+ return root;
+}
+
+// https://dom.spec.whatwg.org/#retarget
+export function retarget(
+ a: domTypes.EventTarget | null,
+ b: domTypes.EventTarget
+): domTypes.EventTarget | null {
+ while (true) {
+ if (!isNode(a)) {
+ return a;
+ }
+
+ const aRoot = getRoot(a);
+
+ if (aRoot) {
+ if (
+ !isShadowRoot(aRoot) ||
+ (isNode(b) && isShadowInclusiveAncestor(aRoot, b))
+ ) {
+ return a;
+ }
+
+ a = aRoot[domTypes.eventTargetHost];
+ }
+ }
+}
diff --git a/cli/js/error_stack.ts b/cli/js/error_stack.ts
new file mode 100644
index 000000000..98b0b02d4
--- /dev/null
+++ b/cli/js/error_stack.ts
@@ -0,0 +1,273 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+// Some of the code here is adapted directly from V8 and licensed under a BSD
+// style license available here: https://github.com/v8/v8/blob/24886f2d1c565287d33d71e4109a53bf0b54b75c/LICENSE.v8
+import * as dispatch from "./dispatch.ts";
+import { sendSync } from "./dispatch_json.ts";
+import { assert } from "./util.ts";
+
+export interface Location {
+ /** The full url for the module, e.g. `file://some/file.ts` or
+ * `https://some/file.ts`. */
+ filename: string;
+
+ /** The line number in the file. It is assumed to be 1-indexed. */
+ line: number;
+
+ /** The column number in the file. It is assumed to be 1-indexed. */
+ column: number;
+}
+
+/** Given a current location in a module, lookup the source location and
+ * return it.
+ *
+ * When Deno transpiles code, it keep source maps of the transpiled code. This
+ * function can be used to lookup the original location. This is automatically
+ * done when accessing the `.stack` of an error, or when an uncaught error is
+ * logged. This function can be used to perform the lookup for creating better
+ * error handling.
+ *
+ * **Note:** `line` and `column` are 1 indexed, which matches display
+ * expectations, but is not typical of most index numbers in Deno.
+ *
+ * An example:
+ *
+ * const orig = Deno.applySourceMap({
+ * location: "file://my/module.ts",
+ * line: 5,
+ * column: 15
+ * });
+ * console.log(`${orig.filename}:${orig.line}:${orig.column}`);
+ *
+ */
+export function applySourceMap(location: Location): Location {
+ const { filename, line, column } = location;
+ // On this side, line/column are 1 based, but in the source maps, they are
+ // 0 based, so we have to convert back and forth
+ const res = sendSync(dispatch.OP_APPLY_SOURCE_MAP, {
+ filename,
+ line: line - 1,
+ column: column - 1
+ });
+ return {
+ filename: res.filename,
+ line: res.line + 1,
+ column: res.column + 1
+ };
+}
+
+/** Mutate the call site so that it returns the location, instead of its
+ * original location.
+ */
+function patchCallSite(callSite: CallSite, location: Location): CallSite {
+ return {
+ getThis(): unknown {
+ return callSite.getThis();
+ },
+ getTypeName(): string {
+ return callSite.getTypeName();
+ },
+ getFunction(): Function {
+ return callSite.getFunction();
+ },
+ getFunctionName(): string {
+ return callSite.getFunctionName();
+ },
+ getMethodName(): string {
+ return callSite.getMethodName();
+ },
+ getFileName(): string {
+ return location.filename;
+ },
+ getLineNumber(): number {
+ return location.line;
+ },
+ getColumnNumber(): number {
+ return location.column;
+ },
+ getEvalOrigin(): string | null {
+ return callSite.getEvalOrigin();
+ },
+ isToplevel(): boolean {
+ return callSite.isToplevel();
+ },
+ isEval(): boolean {
+ return callSite.isEval();
+ },
+ isNative(): boolean {
+ return callSite.isNative();
+ },
+ isConstructor(): boolean {
+ return callSite.isConstructor();
+ },
+ isAsync(): boolean {
+ return callSite.isAsync();
+ },
+ isPromiseAll(): boolean {
+ return callSite.isPromiseAll();
+ },
+ getPromiseIndex(): number | null {
+ return callSite.getPromiseIndex();
+ }
+ };
+}
+
+/** Return a string representations of a CallSite's method call name
+ *
+ * This is adapted directly from V8.
+ */
+function getMethodCall(callSite: CallSite): string {
+ let result = "";
+
+ const typeName = callSite.getTypeName();
+ const methodName = callSite.getMethodName();
+ const functionName = callSite.getFunctionName();
+
+ if (functionName) {
+ if (typeName) {
+ const startsWithTypeName = functionName.startsWith(typeName);
+ if (!startsWithTypeName) {
+ result += `${typeName}.`;
+ }
+ }
+ result += functionName;
+
+ if (methodName) {
+ if (!functionName.endsWith(methodName)) {
+ result += ` [as ${methodName}]`;
+ }
+ }
+ } else {
+ if (typeName) {
+ result += `${typeName}.`;
+ }
+ if (methodName) {
+ result += methodName;
+ } else {
+ result += "<anonymous>";
+ }
+ }
+
+ return result;
+}
+
+/** Return a string representations of a CallSite's file location
+ *
+ * This is adapted directly from V8.
+ */
+function getFileLocation(callSite: CallSite): string {
+ if (callSite.isNative()) {
+ return "native";
+ }
+
+ let result = "";
+
+ const fileName = callSite.getFileName();
+ if (!fileName && callSite.isEval()) {
+ const evalOrigin = callSite.getEvalOrigin();
+ assert(evalOrigin != null);
+ result += `${evalOrigin}, `;
+ }
+
+ if (fileName) {
+ result += fileName;
+ } else {
+ result += "<anonymous>";
+ }
+
+ const lineNumber = callSite.getLineNumber();
+ if (lineNumber != null) {
+ result += `:${lineNumber}`;
+
+ const columnNumber = callSite.getColumnNumber();
+ if (columnNumber != null) {
+ result += `:${columnNumber}`;
+ }
+ }
+
+ return result;
+}
+
+/** Convert a CallSite to a string.
+ *
+ * This is adapted directly from V8.
+ */
+function callSiteToString(callSite: CallSite): string {
+ let result = "";
+ const functionName = callSite.getFunctionName();
+
+ const isTopLevel = callSite.isToplevel();
+ const isAsync = callSite.isAsync();
+ const isPromiseAll = callSite.isPromiseAll();
+ const isConstructor = callSite.isConstructor();
+ const isMethodCall = !(isTopLevel || isConstructor);
+
+ if (isAsync) {
+ result += "async ";
+ }
+ if (isPromiseAll) {
+ result += `Promise.all (index ${callSite.getPromiseIndex})`;
+ return result;
+ }
+ if (isMethodCall) {
+ result += getMethodCall(callSite);
+ } else if (isConstructor) {
+ result += "new ";
+ if (functionName) {
+ result += functionName;
+ } else {
+ result += "<anonymous>";
+ }
+ } else if (functionName) {
+ result += functionName;
+ } else {
+ result += getFileLocation(callSite);
+ return result;
+ }
+
+ result += ` (${getFileLocation(callSite)})`;
+ return result;
+}
+
+/** A replacement for the default stack trace preparer which will op into Rust
+ * to apply source maps to individual sites
+ */
+function prepareStackTrace(
+ error: Error,
+ structuredStackTrace: CallSite[]
+): string {
+ return (
+ `${error.name}: ${error.message}\n` +
+ structuredStackTrace
+ .map(
+ (callSite): CallSite => {
+ const filename = callSite.getFileName();
+ const line = callSite.getLineNumber();
+ const column = callSite.getColumnNumber();
+ if (filename && line != null && column != null) {
+ return patchCallSite(
+ callSite,
+ applySourceMap({
+ filename,
+ line,
+ column
+ })
+ );
+ }
+ return callSite;
+ }
+ )
+ .map((callSite): string => ` at ${callSiteToString(callSite)}`)
+ .join("\n")
+ );
+}
+
+/** Sets the `prepareStackTrace` method on the Error constructor which will
+ * op into Rust to remap source code for caught errors where the `.stack` is
+ * being accessed.
+ *
+ * See: https://v8.dev/docs/stack-trace-api
+ */
+// @internal
+export function setPrepareStackTrace(ErrorConstructor: typeof Error): void {
+ ErrorConstructor.prepareStackTrace = prepareStackTrace;
+}
diff --git a/cli/js/error_stack_test.ts b/cli/js/error_stack_test.ts
new file mode 100644
index 000000000..4c7edb2fd
--- /dev/null
+++ b/cli/js/error_stack_test.ts
@@ -0,0 +1,108 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert } from "./test_util.ts";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const { setPrepareStackTrace } = Deno as any;
+
+interface CallSite {
+ getThis(): unknown;
+ getTypeName(): string;
+ getFunction(): Function;
+ getFunctionName(): string;
+ getMethodName(): string;
+ getFileName(): string;
+ getLineNumber(): number | null;
+ getColumnNumber(): number | null;
+ getEvalOrigin(): string | null;
+ isToplevel(): boolean;
+ isEval(): boolean;
+ isNative(): boolean;
+ isConstructor(): boolean;
+ isAsync(): boolean;
+ isPromiseAll(): boolean;
+ getPromiseIndex(): number | null;
+}
+
+function getMockCallSite(
+ filename: string,
+ line: number | null,
+ column: number | null
+): CallSite {
+ return {
+ getThis(): unknown {
+ return undefined;
+ },
+ getTypeName(): string {
+ return "";
+ },
+ getFunction(): Function {
+ return (): void => {};
+ },
+ getFunctionName(): string {
+ return "";
+ },
+ getMethodName(): string {
+ return "";
+ },
+ getFileName(): string {
+ return filename;
+ },
+ getLineNumber(): number | null {
+ return line;
+ },
+ getColumnNumber(): number | null {
+ return column;
+ },
+ getEvalOrigin(): null {
+ return null;
+ },
+ isToplevel(): false {
+ return false;
+ },
+ isEval(): false {
+ return false;
+ },
+ isNative(): false {
+ return false;
+ },
+ isConstructor(): false {
+ return false;
+ },
+ isAsync(): false {
+ return false;
+ },
+ isPromiseAll(): false {
+ return false;
+ },
+ getPromiseIndex(): null {
+ return null;
+ }
+ };
+}
+
+test(function prepareStackTrace(): void {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const MockError = {} as any;
+ setPrepareStackTrace(MockError);
+ assert(typeof MockError.prepareStackTrace === "function");
+ const prepareStackTrace: (
+ error: Error,
+ structuredStackTrace: CallSite[]
+ ) => string = MockError.prepareStackTrace;
+ const result = prepareStackTrace(new Error("foo"), [
+ getMockCallSite("CLI_SNAPSHOT.js", 23, 0)
+ ]);
+ assert(result.startsWith("Error: foo\n"));
+ assert(result.includes(".ts:"), "should remap to something in 'js/'");
+});
+
+test(function applySourceMap(): void {
+ const result = Deno.applySourceMap({
+ filename: "CLI_SNAPSHOT.js",
+ line: 23,
+ column: 0
+ });
+ assert(result.filename.endsWith(".ts"));
+ assert(result.line != null);
+ assert(result.column != null);
+});
diff --git a/cli/js/errors.ts b/cli/js/errors.ts
new file mode 100644
index 000000000..02ddfa2f2
--- /dev/null
+++ b/cli/js/errors.ts
@@ -0,0 +1,79 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+/** A Deno specific error. The `kind` property is set to a specific error code
+ * which can be used to in application logic.
+ *
+ * try {
+ * somethingThatMightThrow();
+ * } catch (e) {
+ * if (
+ * e instanceof Deno.DenoError &&
+ * e.kind === Deno.ErrorKind.Overflow
+ * ) {
+ * console.error("Overflow error!");
+ * }
+ * }
+ *
+ */
+export class DenoError<T extends ErrorKind> extends Error {
+ constructor(readonly kind: T, msg: string) {
+ super(msg);
+ this.name = ErrorKind[kind];
+ }
+}
+
+// Warning! The values in this enum are duplicated in cli/msg.rs
+// Update carefully!
+export enum ErrorKind {
+ NoError = 0,
+ NotFound = 1,
+ PermissionDenied = 2,
+ ConnectionRefused = 3,
+ ConnectionReset = 4,
+ ConnectionAborted = 5,
+ NotConnected = 6,
+ AddrInUse = 7,
+ AddrNotAvailable = 8,
+ BrokenPipe = 9,
+ AlreadyExists = 10,
+ WouldBlock = 11,
+ InvalidInput = 12,
+ InvalidData = 13,
+ TimedOut = 14,
+ Interrupted = 15,
+ WriteZero = 16,
+ Other = 17,
+ UnexpectedEof = 18,
+ BadResource = 19,
+ CommandFailed = 20,
+ EmptyHost = 21,
+ IdnaError = 22,
+ InvalidPort = 23,
+ InvalidIpv4Address = 24,
+ InvalidIpv6Address = 25,
+ InvalidDomainCharacter = 26,
+ RelativeUrlWithoutBase = 27,
+ RelativeUrlWithCannotBeABaseBase = 28,
+ SetHostOnCannotBeABaseUrl = 29,
+ Overflow = 30,
+ HttpUser = 31,
+ HttpClosed = 32,
+ HttpCanceled = 33,
+ HttpParse = 34,
+ HttpOther = 35,
+ TooLarge = 36,
+ InvalidUri = 37,
+ InvalidSeekMode = 38,
+ OpNotAvailable = 39,
+ WorkerInitFailed = 40,
+ UnixError = 41,
+ NoAsyncSupport = 42,
+ NoSyncSupport = 43,
+ ImportMapError = 44,
+ InvalidPath = 45,
+ ImportPrefixMissing = 46,
+ UnsupportedFetchScheme = 47,
+ TooManyRedirects = 48,
+ Diagnostic = 49,
+ JSError = 50
+}
diff --git a/cli/js/event.ts b/cli/js/event.ts
new file mode 100644
index 000000000..3efc1c517
--- /dev/null
+++ b/cli/js/event.ts
@@ -0,0 +1,348 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types.ts";
+import { getPrivateValue, requiredArguments } from "./util.ts";
+
+// WeakMaps are recommended for private attributes (see MDN link below)
+// https://developer.mozilla.org/en-US/docs/Archive/Add-ons/Add-on_SDK/Guides/Contributor_s_Guide/Private_Properties#Using_WeakMaps
+export const eventAttributes = new WeakMap();
+
+function isTrusted(this: Event): boolean {
+ return getPrivateValue(this, eventAttributes, "isTrusted");
+}
+
+export class Event implements domTypes.Event {
+ // The default value is `false`.
+ // Use `defineProperty` to define on each instance, NOT on the prototype.
+ isTrusted!: boolean;
+ // Each event has the following associated flags
+ private _canceledFlag = false;
+ private _dispatchedFlag = false;
+ private _initializedFlag = false;
+ private _inPassiveListenerFlag = false;
+ private _stopImmediatePropagationFlag = false;
+ private _stopPropagationFlag = false;
+
+ // Property for objects on which listeners will be invoked
+ private _path: domTypes.EventPath[] = [];
+
+ constructor(type: string, eventInitDict: domTypes.EventInit = {}) {
+ requiredArguments("Event", arguments.length, 1);
+ type = String(type);
+ this._initializedFlag = true;
+ eventAttributes.set(this, {
+ type,
+ bubbles: eventInitDict.bubbles || false,
+ cancelable: eventInitDict.cancelable || false,
+ composed: eventInitDict.composed || false,
+ currentTarget: null,
+ eventPhase: domTypes.EventPhase.NONE,
+ isTrusted: false,
+ relatedTarget: null,
+ target: null,
+ timeStamp: Date.now()
+ });
+ Reflect.defineProperty(this, "isTrusted", {
+ enumerable: true,
+ get: isTrusted
+ });
+ }
+
+ get bubbles(): boolean {
+ return getPrivateValue(this, eventAttributes, "bubbles");
+ }
+
+ get cancelBubble(): boolean {
+ return this._stopPropagationFlag;
+ }
+
+ set cancelBubble(value: boolean) {
+ this._stopPropagationFlag = value;
+ }
+
+ get cancelBubbleImmediately(): boolean {
+ return this._stopImmediatePropagationFlag;
+ }
+
+ set cancelBubbleImmediately(value: boolean) {
+ this._stopImmediatePropagationFlag = value;
+ }
+
+ get cancelable(): boolean {
+ return getPrivateValue(this, eventAttributes, "cancelable");
+ }
+
+ get composed(): boolean {
+ return getPrivateValue(this, eventAttributes, "composed");
+ }
+
+ get currentTarget(): domTypes.EventTarget {
+ return getPrivateValue(this, eventAttributes, "currentTarget");
+ }
+
+ set currentTarget(value: domTypes.EventTarget) {
+ eventAttributes.set(this, {
+ type: this.type,
+ bubbles: this.bubbles,
+ cancelable: this.cancelable,
+ composed: this.composed,
+ currentTarget: value,
+ eventPhase: this.eventPhase,
+ isTrusted: this.isTrusted,
+ relatedTarget: this.relatedTarget,
+ target: this.target,
+ timeStamp: this.timeStamp
+ });
+ }
+
+ get defaultPrevented(): boolean {
+ return this._canceledFlag;
+ }
+
+ get dispatched(): boolean {
+ return this._dispatchedFlag;
+ }
+
+ set dispatched(value: boolean) {
+ this._dispatchedFlag = value;
+ }
+
+ get eventPhase(): number {
+ return getPrivateValue(this, eventAttributes, "eventPhase");
+ }
+
+ set eventPhase(value: number) {
+ eventAttributes.set(this, {
+ type: this.type,
+ bubbles: this.bubbles,
+ cancelable: this.cancelable,
+ composed: this.composed,
+ currentTarget: this.currentTarget,
+ eventPhase: value,
+ isTrusted: this.isTrusted,
+ relatedTarget: this.relatedTarget,
+ target: this.target,
+ timeStamp: this.timeStamp
+ });
+ }
+
+ get initialized(): boolean {
+ return this._initializedFlag;
+ }
+
+ set inPassiveListener(value: boolean) {
+ this._inPassiveListenerFlag = value;
+ }
+
+ get path(): domTypes.EventPath[] {
+ return this._path;
+ }
+
+ set path(value: domTypes.EventPath[]) {
+ this._path = value;
+ }
+
+ get relatedTarget(): domTypes.EventTarget {
+ return getPrivateValue(this, eventAttributes, "relatedTarget");
+ }
+
+ set relatedTarget(value: domTypes.EventTarget) {
+ eventAttributes.set(this, {
+ type: this.type,
+ bubbles: this.bubbles,
+ cancelable: this.cancelable,
+ composed: this.composed,
+ currentTarget: this.currentTarget,
+ eventPhase: this.eventPhase,
+ isTrusted: this.isTrusted,
+ relatedTarget: value,
+ target: this.target,
+ timeStamp: this.timeStamp
+ });
+ }
+
+ get target(): domTypes.EventTarget {
+ return getPrivateValue(this, eventAttributes, "target");
+ }
+
+ set target(value: domTypes.EventTarget) {
+ eventAttributes.set(this, {
+ type: this.type,
+ bubbles: this.bubbles,
+ cancelable: this.cancelable,
+ composed: this.composed,
+ currentTarget: this.currentTarget,
+ eventPhase: this.eventPhase,
+ isTrusted: this.isTrusted,
+ relatedTarget: this.relatedTarget,
+ target: value,
+ timeStamp: this.timeStamp
+ });
+ }
+
+ get timeStamp(): Date {
+ return getPrivateValue(this, eventAttributes, "timeStamp");
+ }
+
+ get type(): string {
+ return getPrivateValue(this, eventAttributes, "type");
+ }
+
+ /** Returns the event’s path (objects on which listeners will be
+ * invoked). This does not include nodes in shadow trees if the
+ * shadow root was created with its ShadowRoot.mode closed.
+ *
+ * event.composedPath();
+ */
+ composedPath(): domTypes.EventPath[] {
+ if (this._path.length === 0) {
+ return [];
+ }
+
+ const composedPath: domTypes.EventPath[] = [
+ {
+ item: this.currentTarget,
+ itemInShadowTree: false,
+ relatedTarget: null,
+ rootOfClosedTree: false,
+ slotInClosedTree: false,
+ target: null,
+ touchTargetList: []
+ }
+ ];
+
+ let currentTargetIndex = 0;
+ let currentTargetHiddenSubtreeLevel = 0;
+
+ for (let index = this._path.length - 1; index >= 0; index--) {
+ const { item, rootOfClosedTree, slotInClosedTree } = this._path[index];
+
+ if (rootOfClosedTree) {
+ currentTargetHiddenSubtreeLevel++;
+ }
+
+ if (item === this.currentTarget) {
+ currentTargetIndex = index;
+ break;
+ }
+
+ if (slotInClosedTree) {
+ currentTargetHiddenSubtreeLevel--;
+ }
+ }
+
+ let currentHiddenLevel = currentTargetHiddenSubtreeLevel;
+ let maxHiddenLevel = currentTargetHiddenSubtreeLevel;
+
+ for (let i = currentTargetIndex - 1; i >= 0; i--) {
+ const { item, rootOfClosedTree, slotInClosedTree } = this._path[i];
+
+ if (rootOfClosedTree) {
+ currentHiddenLevel++;
+ }
+
+ if (currentHiddenLevel <= maxHiddenLevel) {
+ composedPath.unshift({
+ item,
+ itemInShadowTree: false,
+ relatedTarget: null,
+ rootOfClosedTree: false,
+ slotInClosedTree: false,
+ target: null,
+ touchTargetList: []
+ });
+ }
+
+ if (slotInClosedTree) {
+ currentHiddenLevel--;
+
+ if (currentHiddenLevel < maxHiddenLevel) {
+ maxHiddenLevel = currentHiddenLevel;
+ }
+ }
+ }
+
+ currentHiddenLevel = currentTargetHiddenSubtreeLevel;
+ maxHiddenLevel = currentTargetHiddenSubtreeLevel;
+
+ for (
+ let index = currentTargetIndex + 1;
+ index < this._path.length;
+ index++
+ ) {
+ const { item, rootOfClosedTree, slotInClosedTree } = this._path[index];
+
+ if (slotInClosedTree) {
+ currentHiddenLevel++;
+ }
+
+ if (currentHiddenLevel <= maxHiddenLevel) {
+ composedPath.push({
+ item,
+ itemInShadowTree: false,
+ relatedTarget: null,
+ rootOfClosedTree: false,
+ slotInClosedTree: false,
+ target: null,
+ touchTargetList: []
+ });
+ }
+
+ if (rootOfClosedTree) {
+ currentHiddenLevel--;
+
+ if (currentHiddenLevel < maxHiddenLevel) {
+ maxHiddenLevel = currentHiddenLevel;
+ }
+ }
+ }
+
+ return composedPath;
+ }
+
+ /** Cancels the event (if it is cancelable).
+ * See https://dom.spec.whatwg.org/#set-the-canceled-flag
+ *
+ * event.preventDefault();
+ */
+ preventDefault(): void {
+ if (this.cancelable && !this._inPassiveListenerFlag) {
+ this._canceledFlag = true;
+ }
+ }
+
+ /** Stops the propagation of events further along in the DOM.
+ *
+ * event.stopPropagation();
+ */
+ stopPropagation(): void {
+ this._stopPropagationFlag = true;
+ }
+
+ /** For this particular event, no other listener will be called.
+ * Neither those attached on the same element, nor those attached
+ * on elements which will be traversed later (in capture phase,
+ * for instance).
+ *
+ * event.stopImmediatePropagation();
+ */
+ stopImmediatePropagation(): void {
+ this._stopPropagationFlag = true;
+ this._stopImmediatePropagationFlag = true;
+ }
+}
+
+/** Built-in objects providing `get` methods for our
+ * interceptable JavaScript operations.
+ */
+Reflect.defineProperty(Event.prototype, "bubbles", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "cancelable", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "composed", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "currentTarget", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "defaultPrevented", {
+ enumerable: true
+});
+Reflect.defineProperty(Event.prototype, "dispatched", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "eventPhase", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "target", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "timeStamp", { enumerable: true });
+Reflect.defineProperty(Event.prototype, "type", { enumerable: true });
diff --git a/cli/js/event_target.ts b/cli/js/event_target.ts
new file mode 100644
index 000000000..08c39544c
--- /dev/null
+++ b/cli/js/event_target.ts
@@ -0,0 +1,503 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types.ts";
+import { DenoError, ErrorKind } from "./errors.ts";
+import { hasOwnProperty, requiredArguments } from "./util.ts";
+import {
+ getRoot,
+ isNode,
+ isShadowRoot,
+ isShadowInclusiveAncestor,
+ isSlotable,
+ retarget
+} from "./dom_util.ts";
+import { window } from "./window.ts";
+
+// https://dom.spec.whatwg.org/#get-the-parent
+// Note: Nodes, shadow roots, and documents override this algorithm so we set it to null.
+function getEventTargetParent(
+ _eventTarget: domTypes.EventTarget,
+ _event: domTypes.Event
+): null {
+ return null;
+}
+
+export const eventTargetAssignedSlot: unique symbol = Symbol();
+export const eventTargetHasActivationBehavior: unique symbol = Symbol();
+
+export class EventTarget implements domTypes.EventTarget {
+ public [domTypes.eventTargetHost]: domTypes.EventTarget | null = null;
+ public [domTypes.eventTargetListeners]: {
+ [type in string]: domTypes.EventListener[]
+ } = {};
+ public [domTypes.eventTargetMode] = "";
+ public [domTypes.eventTargetNodeType]: domTypes.NodeType =
+ domTypes.NodeType.DOCUMENT_FRAGMENT_NODE;
+ private [eventTargetAssignedSlot] = false;
+ private [eventTargetHasActivationBehavior] = false;
+
+ public addEventListener(
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: domTypes.AddEventListenerOptions | boolean
+ ): void {
+ const this_ = this || window;
+
+ requiredArguments("EventTarget.addEventListener", arguments.length, 2);
+ const normalizedOptions: domTypes.AddEventListenerOptions = eventTargetHelpers.normalizeAddEventHandlerOptions(
+ options
+ );
+
+ if (callback === null) {
+ return;
+ }
+
+ const listeners = this_[domTypes.eventTargetListeners];
+
+ if (!hasOwnProperty(listeners, type)) {
+ listeners[type] = [];
+ }
+
+ for (let i = 0; i < listeners[type].length; ++i) {
+ const listener = listeners[type][i];
+ if (
+ ((typeof listener.options === "boolean" &&
+ listener.options === normalizedOptions.capture) ||
+ (typeof listener.options === "object" &&
+ listener.options.capture === normalizedOptions.capture)) &&
+ listener.callback === callback
+ ) {
+ return;
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const eventTarget = this;
+ listeners[type].push({
+ callback,
+ options: normalizedOptions,
+ handleEvent(event: domTypes.Event): void {
+ this.callback.call(eventTarget, event);
+ }
+ } as domTypes.EventListener);
+ }
+
+ public removeEventListener(
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: domTypes.EventListenerOptions | boolean
+ ): void {
+ const this_ = this || window;
+
+ requiredArguments("EventTarget.removeEventListener", arguments.length, 2);
+ const listeners = this_[domTypes.eventTargetListeners];
+ if (hasOwnProperty(listeners, type) && callback !== null) {
+ listeners[type] = listeners[type].filter(
+ (listener): boolean => listener.callback !== callback
+ );
+ }
+
+ const normalizedOptions: domTypes.EventListenerOptions = eventTargetHelpers.normalizeEventHandlerOptions(
+ options
+ );
+
+ if (callback === null) {
+ // Optimization, not in the spec.
+ return;
+ }
+
+ if (!listeners[type]) {
+ return;
+ }
+
+ for (let i = 0; i < listeners[type].length; ++i) {
+ const listener = listeners[type][i];
+
+ if (
+ ((typeof listener.options === "boolean" &&
+ listener.options === normalizedOptions.capture) ||
+ (typeof listener.options === "object" &&
+ listener.options.capture === normalizedOptions.capture)) &&
+ listener.callback === callback
+ ) {
+ listeners[type].splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ public dispatchEvent(event: domTypes.Event): boolean {
+ const this_ = this || window;
+
+ requiredArguments("EventTarget.dispatchEvent", arguments.length, 1);
+ const listeners = this_[domTypes.eventTargetListeners];
+ if (!hasOwnProperty(listeners, event.type)) {
+ return true;
+ }
+
+ if (event.dispatched || !event.initialized) {
+ throw new DenoError(
+ ErrorKind.InvalidData,
+ "Tried to dispatch an uninitialized event"
+ );
+ }
+
+ if (event.eventPhase !== domTypes.EventPhase.NONE) {
+ throw new DenoError(
+ ErrorKind.InvalidData,
+ "Tried to dispatch a dispatching event"
+ );
+ }
+
+ return eventTargetHelpers.dispatch(this_, event);
+ }
+
+ get [Symbol.toStringTag](): string {
+ return "EventTarget";
+ }
+}
+
+const eventTargetHelpers = {
+ // https://dom.spec.whatwg.org/#concept-event-dispatch
+ dispatch(
+ targetImpl: EventTarget,
+ eventImpl: domTypes.Event,
+ targetOverride?: domTypes.EventTarget
+ ): boolean {
+ let clearTargets = false;
+ let activationTarget = null;
+
+ eventImpl.dispatched = true;
+
+ targetOverride = targetOverride || targetImpl;
+ let relatedTarget = retarget(eventImpl.relatedTarget, targetImpl);
+
+ if (
+ targetImpl !== relatedTarget ||
+ targetImpl === eventImpl.relatedTarget
+ ) {
+ const touchTargets: domTypes.EventTarget[] = [];
+
+ eventTargetHelpers.appendToEventPath(
+ eventImpl,
+ targetImpl,
+ targetOverride,
+ relatedTarget,
+ touchTargets,
+ false
+ );
+
+ const isActivationEvent = eventImpl.type === "click";
+
+ if (isActivationEvent && targetImpl[eventTargetHasActivationBehavior]) {
+ activationTarget = targetImpl;
+ }
+
+ let slotInClosedTree = false;
+ let slotable =
+ isSlotable(targetImpl) && targetImpl[eventTargetAssignedSlot]
+ ? targetImpl
+ : null;
+ let parent = getEventTargetParent(targetImpl, eventImpl);
+
+ // Populate event path
+ // https://dom.spec.whatwg.org/#event-path
+ while (parent !== null) {
+ if (slotable !== null) {
+ slotable = null;
+
+ const parentRoot = getRoot(parent);
+ if (
+ isShadowRoot(parentRoot) &&
+ parentRoot &&
+ parentRoot[domTypes.eventTargetMode] === "closed"
+ ) {
+ slotInClosedTree = true;
+ }
+ }
+
+ relatedTarget = retarget(eventImpl.relatedTarget, parent);
+
+ if (
+ isNode(parent) &&
+ isShadowInclusiveAncestor(getRoot(targetImpl), parent)
+ ) {
+ eventTargetHelpers.appendToEventPath(
+ eventImpl,
+ parent,
+ null,
+ relatedTarget,
+ touchTargets,
+ slotInClosedTree
+ );
+ } else if (parent === relatedTarget) {
+ parent = null;
+ } else {
+ targetImpl = parent;
+
+ if (
+ isActivationEvent &&
+ activationTarget === null &&
+ targetImpl[eventTargetHasActivationBehavior]
+ ) {
+ activationTarget = targetImpl;
+ }
+
+ eventTargetHelpers.appendToEventPath(
+ eventImpl,
+ parent,
+ targetImpl,
+ relatedTarget,
+ touchTargets,
+ slotInClosedTree
+ );
+ }
+
+ if (parent !== null) {
+ parent = getEventTargetParent(parent, eventImpl);
+ }
+
+ slotInClosedTree = false;
+ }
+
+ let clearTargetsTupleIndex = -1;
+ for (
+ let i = eventImpl.path.length - 1;
+ i >= 0 && clearTargetsTupleIndex === -1;
+ i--
+ ) {
+ if (eventImpl.path[i].target !== null) {
+ clearTargetsTupleIndex = i;
+ }
+ }
+ const clearTargetsTuple = eventImpl.path[clearTargetsTupleIndex];
+
+ clearTargets =
+ (isNode(clearTargetsTuple.target) &&
+ isShadowRoot(getRoot(clearTargetsTuple.target))) ||
+ (isNode(clearTargetsTuple.relatedTarget) &&
+ isShadowRoot(getRoot(clearTargetsTuple.relatedTarget)));
+
+ eventImpl.eventPhase = domTypes.EventPhase.CAPTURING_PHASE;
+
+ for (let i = eventImpl.path.length - 1; i >= 0; --i) {
+ const tuple = eventImpl.path[i];
+
+ if (tuple.target === null) {
+ eventTargetHelpers.invokeEventListeners(targetImpl, tuple, eventImpl);
+ }
+ }
+
+ for (let i = 0; i < eventImpl.path.length; i++) {
+ const tuple = eventImpl.path[i];
+
+ if (tuple.target !== null) {
+ eventImpl.eventPhase = domTypes.EventPhase.AT_TARGET;
+ } else {
+ eventImpl.eventPhase = domTypes.EventPhase.BUBBLING_PHASE;
+ }
+
+ if (
+ (eventImpl.eventPhase === domTypes.EventPhase.BUBBLING_PHASE &&
+ eventImpl.bubbles) ||
+ eventImpl.eventPhase === domTypes.EventPhase.AT_TARGET
+ ) {
+ eventTargetHelpers.invokeEventListeners(targetImpl, tuple, eventImpl);
+ }
+ }
+ }
+
+ eventImpl.eventPhase = domTypes.EventPhase.NONE;
+
+ eventImpl.currentTarget = null;
+ eventImpl.path = [];
+ eventImpl.dispatched = false;
+ eventImpl.cancelBubble = false;
+ eventImpl.cancelBubbleImmediately = false;
+
+ if (clearTargets) {
+ eventImpl.target = null;
+ eventImpl.relatedTarget = null;
+ }
+
+ // TODO: invoke activation targets if HTML nodes will be implemented
+ // if (activationTarget !== null) {
+ // if (!eventImpl.defaultPrevented) {
+ // activationTarget._activationBehavior();
+ // }
+ // }
+
+ return !eventImpl.defaultPrevented;
+ },
+
+ // https://dom.spec.whatwg.org/#concept-event-listener-invoke
+ invokeEventListeners(
+ targetImpl: EventTarget,
+ tuple: domTypes.EventPath,
+ eventImpl: domTypes.Event
+ ): void {
+ const tupleIndex = eventImpl.path.indexOf(tuple);
+ for (let i = tupleIndex; i >= 0; i--) {
+ const t = eventImpl.path[i];
+ if (t.target) {
+ eventImpl.target = t.target;
+ break;
+ }
+ }
+
+ eventImpl.relatedTarget = tuple.relatedTarget;
+
+ if (eventImpl.cancelBubble) {
+ return;
+ }
+
+ eventImpl.currentTarget = tuple.item;
+
+ eventTargetHelpers.innerInvokeEventListeners(
+ targetImpl,
+ eventImpl,
+ tuple.item[domTypes.eventTargetListeners]
+ );
+ },
+
+ // https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke
+ innerInvokeEventListeners(
+ targetImpl: EventTarget,
+ eventImpl: domTypes.Event,
+ targetListeners: { [type in string]: domTypes.EventListener[] }
+ ): boolean {
+ let found = false;
+
+ const { type } = eventImpl;
+
+ if (!targetListeners || !targetListeners[type]) {
+ return found;
+ }
+
+ // Copy event listeners before iterating since the list can be modified during the iteration.
+ const handlers = targetListeners[type].slice();
+
+ for (let i = 0; i < handlers.length; i++) {
+ const listener = handlers[i];
+
+ let capture, once, passive;
+ if (typeof listener.options === "boolean") {
+ capture = listener.options;
+ once = false;
+ passive = false;
+ } else {
+ capture = listener.options.capture;
+ once = listener.options.once;
+ passive = listener.options.passive;
+ }
+
+ // Check if the event listener has been removed since the listeners has been cloned.
+ if (!targetListeners[type].includes(listener)) {
+ continue;
+ }
+
+ found = true;
+
+ if (
+ (eventImpl.eventPhase === domTypes.EventPhase.CAPTURING_PHASE &&
+ !capture) ||
+ (eventImpl.eventPhase === domTypes.EventPhase.BUBBLING_PHASE && capture)
+ ) {
+ continue;
+ }
+
+ if (once) {
+ targetListeners[type].splice(
+ targetListeners[type].indexOf(listener),
+ 1
+ );
+ }
+
+ if (passive) {
+ eventImpl.inPassiveListener = true;
+ }
+
+ try {
+ if (listener.callback) {
+ listener.handleEvent(eventImpl);
+ }
+ } catch (error) {
+ throw new DenoError(ErrorKind.Interrupted, error.message);
+ }
+
+ eventImpl.inPassiveListener = false;
+
+ if (eventImpl.cancelBubbleImmediately) {
+ return found;
+ }
+ }
+
+ return found;
+ },
+
+ normalizeAddEventHandlerOptions(
+ options: boolean | domTypes.AddEventListenerOptions | undefined
+ ): domTypes.AddEventListenerOptions {
+ if (typeof options === "boolean" || typeof options === "undefined") {
+ const returnValue: domTypes.AddEventListenerOptions = {
+ capture: Boolean(options),
+ once: false,
+ passive: false
+ };
+
+ return returnValue;
+ } else {
+ return options;
+ }
+ },
+
+ normalizeEventHandlerOptions(
+ options: boolean | domTypes.EventListenerOptions | undefined
+ ): domTypes.EventListenerOptions {
+ if (typeof options === "boolean" || typeof options === "undefined") {
+ const returnValue: domTypes.EventListenerOptions = {
+ capture: Boolean(options)
+ };
+
+ return returnValue;
+ } else {
+ return options;
+ }
+ },
+
+ // https://dom.spec.whatwg.org/#concept-event-path-append
+ appendToEventPath(
+ eventImpl: domTypes.Event,
+ target: domTypes.EventTarget,
+ targetOverride: domTypes.EventTarget | null,
+ relatedTarget: domTypes.EventTarget | null,
+ touchTargets: domTypes.EventTarget[],
+ slotInClosedTree: boolean
+ ): void {
+ const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target));
+ const rootOfClosedTree =
+ isShadowRoot(target) && target[domTypes.eventTargetMode] === "closed";
+
+ eventImpl.path.push({
+ item: target,
+ itemInShadowTree,
+ target: targetOverride,
+ relatedTarget,
+ touchTargetList: touchTargets,
+ rootOfClosedTree,
+ slotInClosedTree
+ });
+ }
+};
+
+/** Built-in objects providing `get` methods for our
+ * interceptable JavaScript operations.
+ */
+Reflect.defineProperty(EventTarget.prototype, "addEventListener", {
+ enumerable: true
+});
+Reflect.defineProperty(EventTarget.prototype, "removeEventListener", {
+ enumerable: true
+});
+Reflect.defineProperty(EventTarget.prototype, "dispatchEvent", {
+ enumerable: true
+});
diff --git a/cli/js/event_target_test.ts b/cli/js/event_target_test.ts
new file mode 100644
index 000000000..9d7e7974c
--- /dev/null
+++ b/cli/js/event_target_test.ts
@@ -0,0 +1,142 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assertEquals } from "./test_util.ts";
+
+test(function addEventListenerTest(): void {
+ const document = new EventTarget();
+
+ assertEquals(document.addEventListener("x", null, false), undefined);
+ assertEquals(document.addEventListener("x", null, true), undefined);
+ assertEquals(document.addEventListener("x", null), undefined);
+});
+
+test(function constructedEventTargetCanBeUsedAsExpected(): void {
+ const target = new EventTarget();
+ const event = new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ const listener = (e): void => {
+ assertEquals(e, event);
+ ++callCount;
+ };
+
+ target.addEventListener("foo", listener);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 1);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+
+ target.removeEventListener("foo", listener);
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+});
+
+test(function anEventTargetCanBeSubclassed(): void {
+ class NicerEventTarget extends EventTarget {
+ on(type, callback?, options?): void {
+ this.addEventListener(type, callback, options);
+ }
+
+ off(type, callback?, options?): void {
+ this.removeEventListener(type, callback, options);
+ }
+ }
+
+ const target = new NicerEventTarget();
+ new Event("foo", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ const listener = (): void => {
+ ++callCount;
+ };
+
+ target.on("foo", listener);
+ assertEquals(callCount, 0);
+
+ target.off("foo", listener);
+ assertEquals(callCount, 0);
+});
+
+test(function removingNullEventListenerShouldSucceed(): void {
+ const document = new EventTarget();
+ assertEquals(document.removeEventListener("x", null, false), undefined);
+ assertEquals(document.removeEventListener("x", null, true), undefined);
+ assertEquals(document.removeEventListener("x", null), undefined);
+});
+
+test(function constructedEventTargetUseObjectPrototype(): void {
+ const target = new EventTarget();
+ const event = new Event("toString", { bubbles: true, cancelable: false });
+ let callCount = 0;
+
+ const listener = (e): void => {
+ assertEquals(e, event);
+ ++callCount;
+ };
+
+ target.addEventListener("toString", listener);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 1);
+
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+
+ target.removeEventListener("toString", listener);
+ target.dispatchEvent(event);
+ assertEquals(callCount, 2);
+});
+
+test(function toStringShouldBeWebCompatible(): void {
+ const target = new EventTarget();
+ assertEquals(target.toString(), "[object EventTarget]");
+});
+
+test(function dispatchEventShouldNotThrowError(): void {
+ let hasThrown = false;
+
+ try {
+ const target = new EventTarget();
+ const event = new Event("hasOwnProperty", {
+ bubbles: true,
+ cancelable: false
+ });
+ const listener = (): void => {};
+ target.addEventListener("hasOwnProperty", listener);
+ target.dispatchEvent(event);
+ } catch {
+ hasThrown = true;
+ }
+
+ assertEquals(hasThrown, false);
+});
+
+test(function eventTargetThisShouldDefaultToWindow(): void {
+ const {
+ addEventListener,
+ dispatchEvent,
+ removeEventListener
+ } = EventTarget.prototype;
+ let n = 1;
+ const event = new Event("hello");
+ const listener = (): void => {
+ n = 2;
+ };
+
+ addEventListener("hello", listener);
+ window.dispatchEvent(event);
+ assertEquals(n, 2);
+ n = 1;
+ removeEventListener("hello", listener);
+ window.dispatchEvent(event);
+ assertEquals(n, 1);
+
+ window.addEventListener("hello", listener);
+ dispatchEvent(event);
+ assertEquals(n, 2);
+ n = 1;
+ window.removeEventListener("hello", listener);
+ dispatchEvent(event);
+ assertEquals(n, 1);
+});
diff --git a/cli/js/event_test.ts b/cli/js/event_test.ts
new file mode 100644
index 000000000..72f4f5855
--- /dev/null
+++ b/cli/js/event_test.ts
@@ -0,0 +1,95 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assertEquals, assertNotEquals } from "./test_util.ts";
+
+test(function eventInitializedWithType(): void {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEquals(event.isTrusted, false);
+ assertEquals(event.target, null);
+ assertEquals(event.currentTarget, null);
+ assertEquals(event.type, "click");
+ assertEquals(event.bubbles, false);
+ assertEquals(event.cancelable, false);
+});
+
+test(function eventInitializedWithTypeAndDict(): void {
+ const init = "submit";
+ const eventInit = { bubbles: true, cancelable: true } as EventInit;
+ const event = new Event(init, eventInit);
+
+ assertEquals(event.isTrusted, false);
+ assertEquals(event.target, null);
+ assertEquals(event.currentTarget, null);
+ assertEquals(event.type, "submit");
+ assertEquals(event.bubbles, true);
+ assertEquals(event.cancelable, true);
+});
+
+test(function eventComposedPathSuccess(): void {
+ const type = "click";
+ const event = new Event(type);
+ const composedPath = event.composedPath();
+
+ assertEquals(composedPath, []);
+});
+
+test(function eventStopPropagationSuccess(): void {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEquals(event.cancelBubble, false);
+ event.stopPropagation();
+ assertEquals(event.cancelBubble, true);
+});
+
+test(function eventStopImmediatePropagationSuccess(): void {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEquals(event.cancelBubble, false);
+ assertEquals(event.cancelBubbleImmediately, false);
+ event.stopImmediatePropagation();
+ assertEquals(event.cancelBubble, true);
+ assertEquals(event.cancelBubbleImmediately, true);
+});
+
+test(function eventPreventDefaultSuccess(): void {
+ const type = "click";
+ const event = new Event(type);
+
+ assertEquals(event.defaultPrevented, false);
+ event.preventDefault();
+ assertEquals(event.defaultPrevented, false);
+
+ const eventInit = { bubbles: true, cancelable: true } as EventInit;
+ const cancelableEvent = new Event(type, eventInit);
+ assertEquals(cancelableEvent.defaultPrevented, false);
+ cancelableEvent.preventDefault();
+ assertEquals(cancelableEvent.defaultPrevented, true);
+});
+
+test(function eventInitializedWithNonStringType(): void {
+ const type = undefined;
+ const event = new Event(type);
+
+ assertEquals(event.isTrusted, false);
+ assertEquals(event.target, null);
+ assertEquals(event.currentTarget, null);
+ assertEquals(event.type, "undefined");
+ assertEquals(event.bubbles, false);
+ assertEquals(event.cancelable, false);
+});
+
+// ref https://github.com/web-platform-tests/wpt/blob/master/dom/events/Event-isTrusted.any.js
+test(function eventIsTrusted(): void {
+ const desc1 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted");
+ assertNotEquals(desc1, undefined);
+ assertEquals(typeof desc1.get, "function");
+
+ const desc2 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted");
+ assertNotEquals(desc2, undefined);
+ assertEquals(typeof desc2.get, "function");
+
+ assertEquals(desc1.get, desc2.get);
+});
diff --git a/cli/js/fetch.ts b/cli/js/fetch.ts
new file mode 100644
index 000000000..0a5f793a8
--- /dev/null
+++ b/cli/js/fetch.ts
@@ -0,0 +1,478 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ createResolvable,
+ notImplemented,
+ isTypedArray
+} from "./util.ts";
+import * as domTypes from "./dom_types.ts";
+import { TextDecoder, TextEncoder } from "./text_encoding.ts";
+import { DenoBlob, bytesSymbol as blobBytesSymbol } from "./blob.ts";
+import { Headers } from "./headers.ts";
+import * as io from "./io.ts";
+import { read, close } from "./files.ts";
+import { Buffer } from "./buffer.ts";
+import { FormData } from "./form_data.ts";
+import { URLSearchParams } from "./url_search_params.ts";
+import * as dispatch from "./dispatch.ts";
+import { sendAsync } from "./dispatch_json.ts";
+
+function getHeaderValueParams(value: string): Map<string, string> {
+ const params = new Map();
+ // Forced to do so for some Map constructor param mismatch
+ value
+ .split(";")
+ .slice(1)
+ .map((s): string[] => s.trim().split("="))
+ .filter((arr): boolean => arr.length > 1)
+ .map(([k, v]): [string, string] => [k, v.replace(/^"([^"]*)"$/, "$1")])
+ .forEach(([k, v]): Map<string, string> => params.set(k, v));
+ return params;
+}
+
+function hasHeaderValueOf(s: string, value: string): boolean {
+ return new RegExp(`^${value}[\t\s]*;?`).test(s);
+}
+
+class Body implements domTypes.Body, domTypes.ReadableStream, io.ReadCloser {
+ private _bodyUsed = false;
+ private _bodyPromise: null | Promise<ArrayBuffer> = null;
+ private _data: ArrayBuffer | null = null;
+ readonly locked: boolean = false; // TODO
+ readonly body: null | Body = this;
+
+ constructor(private rid: number, readonly contentType: string) {}
+
+ private async _bodyBuffer(): Promise<ArrayBuffer> {
+ assert(this._bodyPromise == null);
+ const buf = new Buffer();
+ try {
+ const nread = await buf.readFrom(this);
+ const ui8 = buf.bytes();
+ assert(ui8.byteLength === nread);
+ this._data = ui8.buffer.slice(
+ ui8.byteOffset,
+ ui8.byteOffset + nread
+ ) as ArrayBuffer;
+ assert(this._data.byteLength === nread);
+ } finally {
+ this.close();
+ }
+
+ return this._data;
+ }
+
+ async arrayBuffer(): Promise<ArrayBuffer> {
+ // If we've already bufferred the response, just return it.
+ if (this._data != null) {
+ return this._data;
+ }
+
+ // If there is no _bodyPromise yet, start it.
+ if (this._bodyPromise == null) {
+ this._bodyPromise = this._bodyBuffer();
+ }
+
+ return this._bodyPromise;
+ }
+
+ async blob(): Promise<domTypes.Blob> {
+ const arrayBuffer = await this.arrayBuffer();
+ return new DenoBlob([arrayBuffer], {
+ type: this.contentType
+ });
+ }
+
+ // ref: https://fetch.spec.whatwg.org/#body-mixin
+ async formData(): Promise<domTypes.FormData> {
+ const formData = new FormData();
+ const enc = new TextEncoder();
+ if (hasHeaderValueOf(this.contentType, "multipart/form-data")) {
+ const params = getHeaderValueParams(this.contentType);
+ if (!params.has("boundary")) {
+ // TypeError is required by spec
+ throw new TypeError("multipart/form-data must provide a boundary");
+ }
+ // ref: https://tools.ietf.org/html/rfc2046#section-5.1
+ const boundary = params.get("boundary")!;
+ const dashBoundary = `--${boundary}`;
+ const delimiter = `\r\n${dashBoundary}`;
+ const closeDelimiter = `${delimiter}--`;
+
+ const body = await this.text();
+ let bodyParts: string[];
+ const bodyEpilogueSplit = body.split(closeDelimiter);
+ if (bodyEpilogueSplit.length < 2) {
+ bodyParts = [];
+ } else {
+ // discard epilogue
+ const bodyEpilogueTrimmed = bodyEpilogueSplit[0];
+ // first boundary treated special due to optional prefixed \r\n
+ const firstBoundaryIndex = bodyEpilogueTrimmed.indexOf(dashBoundary);
+ if (firstBoundaryIndex < 0) {
+ throw new TypeError("Invalid boundary");
+ }
+ const bodyPreambleTrimmed = bodyEpilogueTrimmed
+ .slice(firstBoundaryIndex + dashBoundary.length)
+ .replace(/^[\s\r\n\t]+/, ""); // remove transport-padding CRLF
+ // trimStart might not be available
+ // Be careful! body-part allows trailing \r\n!
+ // (as long as it is not part of `delimiter`)
+ bodyParts = bodyPreambleTrimmed
+ .split(delimiter)
+ .map((s): string => s.replace(/^[\s\r\n\t]+/, ""));
+ // TODO: LWSP definition is actually trickier,
+ // but should be fine in our case since without headers
+ // we should just discard the part
+ }
+ for (const bodyPart of bodyParts) {
+ const headers = new Headers();
+ const headerOctetSeperatorIndex = bodyPart.indexOf("\r\n\r\n");
+ if (headerOctetSeperatorIndex < 0) {
+ continue; // Skip unknown part
+ }
+ const headerText = bodyPart.slice(0, headerOctetSeperatorIndex);
+ const octets = bodyPart.slice(headerOctetSeperatorIndex + 4);
+
+ // TODO: use textproto.readMIMEHeader from deno_std
+ const rawHeaders = headerText.split("\r\n");
+ for (const rawHeader of rawHeaders) {
+ const sepIndex = rawHeader.indexOf(":");
+ if (sepIndex < 0) {
+ continue; // Skip this header
+ }
+ const key = rawHeader.slice(0, sepIndex);
+ const value = rawHeader.slice(sepIndex + 1);
+ headers.set(key, value);
+ }
+ if (!headers.has("content-disposition")) {
+ continue; // Skip unknown part
+ }
+ // Content-Transfer-Encoding Deprecated
+ const contentDisposition = headers.get("content-disposition")!;
+ const partContentType = headers.get("content-type") || "text/plain";
+ // TODO: custom charset encoding (needs TextEncoder support)
+ // const contentTypeCharset =
+ // getHeaderValueParams(partContentType).get("charset") || "";
+ if (!hasHeaderValueOf(contentDisposition, "form-data")) {
+ continue; // Skip, might not be form-data
+ }
+ const dispositionParams = getHeaderValueParams(contentDisposition);
+ if (!dispositionParams.has("name")) {
+ continue; // Skip, unknown name
+ }
+ const dispositionName = dispositionParams.get("name")!;
+ if (dispositionParams.has("filename")) {
+ const filename = dispositionParams.get("filename")!;
+ const blob = new DenoBlob([enc.encode(octets)], {
+ type: partContentType
+ });
+ // TODO: based on spec
+ // https://xhr.spec.whatwg.org/#dom-formdata-append
+ // https://xhr.spec.whatwg.org/#create-an-entry
+ // Currently it does not mention how I could pass content-type
+ // to the internally created file object...
+ formData.append(dispositionName, blob, filename);
+ } else {
+ formData.append(dispositionName, octets);
+ }
+ }
+ return formData;
+ } else if (
+ hasHeaderValueOf(this.contentType, "application/x-www-form-urlencoded")
+ ) {
+ // From https://github.com/github/fetch/blob/master/fetch.js
+ // Copyright (c) 2014-2016 GitHub, Inc. MIT License
+ const body = await this.text();
+ try {
+ body
+ .trim()
+ .split("&")
+ .forEach(
+ (bytes): void => {
+ if (bytes) {
+ const split = bytes.split("=");
+ const name = split.shift()!.replace(/\+/g, " ");
+ const value = split.join("=").replace(/\+/g, " ");
+ formData.append(
+ decodeURIComponent(name),
+ decodeURIComponent(value)
+ );
+ }
+ }
+ );
+ } catch (e) {
+ throw new TypeError("Invalid form urlencoded format");
+ }
+ return formData;
+ } else {
+ throw new TypeError("Invalid form data");
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ async json(): Promise<any> {
+ 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);
+ }
+
+ read(p: Uint8Array): Promise<number | io.EOF> {
+ this._bodyUsed = true;
+ return read(this.rid, p);
+ }
+
+ close(): void {
+ close(this.rid);
+ }
+
+ async cancel(): Promise<void> {
+ return notImplemented();
+ }
+
+ getReader(): domTypes.ReadableStreamReader {
+ return notImplemented();
+ }
+
+ tee(): [domTypes.ReadableStream, domTypes.ReadableStream] {
+ return notImplemented();
+ }
+
+ [Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array> {
+ return io.toAsyncIterator(this);
+ }
+
+ get bodyUsed(): boolean {
+ return this._bodyUsed;
+ }
+}
+
+export class Response implements domTypes.Response {
+ readonly type = "basic"; // TODO
+ readonly redirected: boolean;
+ headers: domTypes.Headers;
+ readonly trailer: Promise<domTypes.Headers>;
+ readonly body: Body;
+
+ constructor(
+ readonly url: string,
+ readonly status: number,
+ readonly statusText: string,
+ headersList: Array<[string, string]>,
+ rid: number,
+ redirected_: boolean,
+ body_: null | Body = null
+ ) {
+ this.trailer = createResolvable();
+ this.headers = new Headers(headersList);
+ const contentType = this.headers.get("content-type") || "";
+
+ if (body_ == null) {
+ this.body = new Body(rid, contentType);
+ } else {
+ this.body = body_;
+ }
+
+ this.redirected = redirected_;
+ }
+
+ async arrayBuffer(): Promise<ArrayBuffer> {
+ return this.body.arrayBuffer();
+ }
+
+ async blob(): Promise<domTypes.Blob> {
+ return this.body.blob();
+ }
+
+ async formData(): Promise<domTypes.FormData> {
+ return this.body.formData();
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ async json(): Promise<any> {
+ return this.body.json();
+ }
+
+ async text(): Promise<string> {
+ return this.body.text();
+ }
+
+ get ok(): boolean {
+ return 200 <= this.status && this.status < 300;
+ }
+
+ get bodyUsed(): boolean {
+ return this.body.bodyUsed;
+ }
+
+ clone(): domTypes.Response {
+ if (this.bodyUsed) {
+ throw new TypeError(
+ "Failed to execute 'clone' on 'Response': Response body is already used"
+ );
+ }
+
+ const iterators = this.headers.entries();
+ const headersList: Array<[string, string]> = [];
+ for (const header of iterators) {
+ headersList.push(header);
+ }
+
+ return new Response(
+ this.url,
+ this.status,
+ this.statusText,
+ headersList,
+ -1,
+ this.redirected,
+ this.body
+ );
+ }
+}
+
+interface FetchResponse {
+ bodyRid: number;
+ status: number;
+ statusText: string;
+ headers: Array<[string, string]>;
+}
+
+async function sendFetchReq(
+ url: string,
+ method: string | null,
+ headers: domTypes.Headers | null,
+ body: ArrayBufferView | undefined
+): Promise<FetchResponse> {
+ let headerArray: Array<[string, string]> = [];
+ if (headers) {
+ headerArray = Array.from(headers.entries());
+ }
+
+ let zeroCopy = undefined;
+ if (body) {
+ zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
+ }
+
+ const args = {
+ method,
+ url,
+ headers: headerArray
+ };
+
+ return (await sendAsync(dispatch.OP_FETCH, args, zeroCopy)) as FetchResponse;
+}
+
+/** Fetch a resource from the network. */
+export async function fetch(
+ input: domTypes.Request | string,
+ init?: domTypes.RequestInit
+): Promise<Response> {
+ let url: string;
+ let method: string | null = null;
+ let headers: domTypes.Headers | null = null;
+ let body: ArrayBufferView | undefined;
+ let redirected = false;
+ let remRedirectCount = 20; // TODO: use a better way to handle
+
+ if (typeof input === "string") {
+ url = input;
+ if (init != null) {
+ method = init.method || null;
+ if (init.headers) {
+ headers =
+ init.headers instanceof Headers
+ ? init.headers
+ : new Headers(init.headers);
+ } else {
+ headers = null;
+ }
+
+ // ref: https://fetch.spec.whatwg.org/#body-mixin
+ // Body should have been a mixin
+ // but we are treating it as a separate class
+ if (init.body) {
+ if (!headers) {
+ headers = new Headers();
+ }
+ let contentType = "";
+ if (typeof init.body === "string") {
+ body = new TextEncoder().encode(init.body);
+ contentType = "text/plain;charset=UTF-8";
+ } else if (isTypedArray(init.body)) {
+ body = init.body;
+ } else if (init.body instanceof URLSearchParams) {
+ body = new TextEncoder().encode(init.body.toString());
+ contentType = "application/x-www-form-urlencoded;charset=UTF-8";
+ } else if (init.body instanceof DenoBlob) {
+ body = init.body[blobBytesSymbol];
+ contentType = init.body.type;
+ } else {
+ // TODO: FormData, ReadableStream
+ notImplemented();
+ }
+ if (contentType && !headers.has("content-type")) {
+ headers.set("content-type", contentType);
+ }
+ }
+ }
+ } else {
+ url = input.url;
+ method = input.method;
+ headers = input.headers;
+
+ //@ts-ignore
+ if (input._bodySource) {
+ body = new DataView(await input.arrayBuffer());
+ }
+ }
+
+ while (remRedirectCount) {
+ const fetchResponse = await sendFetchReq(url, method, headers, body);
+
+ const response = new Response(
+ url,
+ fetchResponse.status,
+ fetchResponse.statusText,
+ fetchResponse.headers,
+ fetchResponse.bodyRid,
+ redirected
+ );
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
+ // We're in a redirect status
+ switch ((init && init.redirect) || "follow") {
+ case "error":
+ throw notImplemented();
+ case "manual":
+ throw notImplemented();
+ case "follow":
+ default:
+ let redirectUrl = response.headers.get("Location");
+ if (redirectUrl == null) {
+ return response; // Unspecified
+ }
+ if (
+ !redirectUrl.startsWith("http://") &&
+ !redirectUrl.startsWith("https://")
+ ) {
+ redirectUrl =
+ url.split("//")[0] +
+ "//" +
+ url.split("//")[1].split("/")[0] +
+ redirectUrl; // TODO: handle relative redirection more gracefully
+ }
+ url = redirectUrl;
+ redirected = true;
+ remRedirectCount--;
+ }
+ } else {
+ return response;
+ }
+ }
+ // Return a network error due to too many redirections
+ throw notImplemented();
+}
diff --git a/cli/js/fetch_test.ts b/cli/js/fetch_test.ts
new file mode 100644
index 000000000..56c693681
--- /dev/null
+++ b/cli/js/fetch_test.ts
@@ -0,0 +1,357 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import {
+ test,
+ testPerm,
+ assert,
+ assertEquals,
+ assertStrContains,
+ assertThrows
+} from "./test_util.ts";
+
+testPerm({ net: true }, async function fetchConnectionError(): Promise<void> {
+ let err;
+ try {
+ await fetch("http://localhost:4000");
+ } catch (err_) {
+ err = err_;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.HttpOther);
+ assertEquals(err.name, "HttpOther");
+ assertStrContains(err.message, "error trying to connect");
+});
+
+testPerm({ net: true }, async function fetchJsonSuccess(): Promise<void> {
+ const response = await fetch("http://localhost:4545/package.json");
+ const json = await response.json();
+ assertEquals(json.name, "deno");
+});
+
+test(async function fetchPerm(): Promise<void> {
+ let err;
+ try {
+ await fetch("http://localhost:4545/package.json");
+ } catch (err_) {
+ err = err_;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ net: true }, async function fetchUrl(): Promise<void> {
+ const response = await fetch("http://localhost:4545/package.json");
+ assertEquals(response.url, "http://localhost:4545/package.json");
+});
+
+testPerm({ net: true }, async function fetchHeaders(): Promise<void> {
+ const response = await fetch("http://localhost:4545/package.json");
+ const headers = response.headers;
+ assertEquals(headers.get("Content-Type"), "application/json");
+ assert(headers.get("Server").startsWith("SimpleHTTP"));
+});
+
+testPerm({ net: true }, async function fetchBlob(): Promise<void> {
+ const response = await fetch("http://localhost:4545/package.json");
+ const headers = response.headers;
+ const blob = await response.blob();
+ assertEquals(blob.type, headers.get("Content-Type"));
+ assertEquals(blob.size, Number(headers.get("Content-Length")));
+});
+
+testPerm({ net: true }, async function fetchBodyUsed(): Promise<void> {
+ const response = await fetch("http://localhost:4545/package.json");
+ assertEquals(response.bodyUsed, false);
+ assertThrows(
+ (): void => {
+ // Assigning to read-only property throws in the strict mode.
+ response.bodyUsed = true;
+ }
+ );
+ await response.blob();
+ assertEquals(response.bodyUsed, true);
+});
+
+testPerm({ net: true }, async function fetchAsyncIterator(): Promise<void> {
+ const response = await fetch("http://localhost:4545/package.json");
+ const headers = response.headers;
+ let total = 0;
+ for await (const chunk of response.body) {
+ total += chunk.length;
+ }
+
+ assertEquals(total, Number(headers.get("Content-Length")));
+});
+
+testPerm({ net: true }, async function responseClone(): Promise<void> {
+ const response = await fetch("http://localhost:4545/package.json");
+ const response1 = response.clone();
+ assert(response !== response1);
+ assertEquals(response.status, response1.status);
+ assertEquals(response.statusText, response1.statusText);
+ const ab = await response.arrayBuffer();
+ const ab1 = await response1.arrayBuffer();
+ for (let i = 0; i < ab.byteLength; i++) {
+ assertEquals(ab[i], ab1[i]);
+ }
+});
+
+testPerm({ net: true }, async function fetchEmptyInvalid(): Promise<void> {
+ let err;
+ try {
+ await fetch("");
+ } catch (err_) {
+ err = err_;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.RelativeUrlWithoutBase);
+ assertEquals(err.name, "RelativeUrlWithoutBase");
+});
+
+testPerm({ net: true }, async function fetchMultipartFormDataSuccess(): Promise<
+ void
+> {
+ const response = await fetch(
+ "http://localhost:4545/tests/subdir/multipart_form_data.txt"
+ );
+ const formData = await response.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1").toString(), "value_1 \r\n");
+ assert(formData.has("field_2"));
+ /* TODO(ry) Re-enable this test once we bring back the global File type.
+ const file = formData.get("field_2") as File;
+ assertEquals(file.name, "file.js");
+ */
+ // Currently we cannot read from file...
+});
+
+testPerm(
+ { net: true },
+ async function fetchURLEncodedFormDataSuccess(): Promise<void> {
+ const response = await fetch(
+ "http://localhost:4545/tests/subdir/form_urlencoded.txt"
+ );
+ const formData = await response.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1").toString(), "Hi");
+ assert(formData.has("field_2"));
+ assertEquals(formData.get("field_2").toString(), "<Deno>");
+ }
+);
+
+testPerm({ net: true }, async function fetchWithRedirection(): Promise<void> {
+ const response = await fetch("http://localhost:4546/"); // will redirect to http://localhost:4545/
+ assertEquals(response.status, 200);
+ assertEquals(response.statusText, "OK");
+ assertEquals(response.url, "http://localhost:4545/");
+ const body = await response.text();
+ assert(body.includes("<title>Directory listing for /</title>"));
+});
+
+testPerm({ net: true }, async function fetchWithRelativeRedirection(): Promise<
+ void
+> {
+ const response = await fetch("http://localhost:4545/tests"); // will redirect to /tests/
+ assertEquals(response.status, 200);
+ assertEquals(response.statusText, "OK");
+ const body = await response.text();
+ assert(body.includes("<title>Directory listing for /tests/</title>"));
+});
+
+// The feature below is not implemented, but the test should work after implementation
+/*
+testPerm({ net: true }, async function fetchWithInfRedirection(): Promise<
+ void
+> {
+ const response = await fetch("http://localhost:4549/tests"); // will redirect to the same place
+ assertEquals(response.status, 0); // network error
+});
+*/
+
+testPerm({ net: true }, async function fetchInitStringBody(): Promise<void> {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ assert(response.headers.get("content-type").startsWith("text/plain"));
+});
+
+testPerm({ net: true }, async function fetchRequestInitStringBody(): Promise<
+ void
+> {
+ const data = "Hello World";
+ const req = new Request("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data
+ });
+ const response = await fetch(req);
+ const text = await response.text();
+ assertEquals(text, data);
+});
+
+testPerm({ net: true }, async function fetchInitTypedArrayBody(): Promise<
+ void
+> {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: new TextEncoder().encode(data)
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+});
+
+testPerm({ net: true }, async function fetchInitURLSearchParamsBody(): Promise<
+ void
+> {
+ const data = "param1=value1&param2=value2";
+ const params = new URLSearchParams(data);
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: params
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ assert(
+ response.headers
+ .get("content-type")
+ .startsWith("application/x-www-form-urlencoded")
+ );
+});
+
+testPerm({ net: true }, async function fetchInitBlobBody(): Promise<void> {
+ const data = "const a = 1";
+ const blob = new Blob([data], {
+ type: "text/javascript"
+ });
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: blob
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ assert(response.headers.get("content-type").startsWith("text/javascript"));
+});
+
+testPerm({ net: true }, async function fetchUserAgent(): Promise<void> {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: new TextEncoder().encode(data)
+ });
+ assertEquals(response.headers.get("user-agent"), `Deno/${Deno.version.deno}`);
+ await response.text();
+});
+
+// TODO(ry) The following tests work but are flaky. There's a race condition
+// somewhere. Here is what one of these flaky failures looks like:
+//
+// test fetchPostBodyString_permW0N1E0R0
+// assertEquals failed. actual = expected = POST /blah HTTP/1.1
+// hello: World
+// foo: Bar
+// host: 127.0.0.1:4502
+// content-length: 11
+// hello world
+// Error: actual: expected: POST /blah HTTP/1.1
+// hello: World
+// foo: Bar
+// host: 127.0.0.1:4502
+// content-length: 11
+// hello world
+// at Object.assertEquals (file:///C:/deno/js/testing/util.ts:29:11)
+// at fetchPostBodyString (file
+
+/*
+function bufferServer(addr: string): Deno.Buffer {
+ const listener = Deno.listen(addr);
+ const buf = new Deno.Buffer();
+ listener.accept().then(async conn => {
+ const p1 = buf.readFrom(conn);
+ const p2 = conn.write(
+ new TextEncoder().encode(
+ "HTTP/1.0 404 Not Found\r\nContent-Length: 2\r\n\r\nNF"
+ )
+ );
+ // Wait for both an EOF on the read side of the socket and for the write to
+ // complete before closing it. Due to keep-alive, the EOF won't be sent
+ // until the Connection close (HTTP/1.0) response, so readFrom() can't
+ // proceed write. Conversely, if readFrom() is async, waiting for the
+ // write() to complete is not a guarantee that we've read the incoming
+ // request.
+ await Promise.all([p1, p2]);
+ conn.close();
+ listener.close();
+ });
+ return buf;
+}
+
+testPerm({ net: true }, async function fetchRequest():Promise<void> {
+ const addr = "127.0.0.1:4501";
+ const buf = bufferServer(addr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [["Hello", "World"], ["Foo", "Bar"]]
+ });
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode(buf.bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ `host: ${addr}\r\n\r\n`
+ ].join("");
+ assertEquals(actual, expected);
+});
+
+testPerm({ net: true }, async function fetchPostBodyString():Promise<void> {
+ const addr = "127.0.0.1:4502";
+ const buf = bufferServer(addr);
+ const body = "hello world";
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [["Hello", "World"], ["Foo", "Bar"]],
+ body
+ });
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode(buf.bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ `host: ${addr}\r\n`,
+ `content-length: ${body.length}\r\n\r\n`,
+ body
+ ].join("");
+ assertEquals(actual, expected);
+});
+
+testPerm({ net: true }, async function fetchPostBodyTypedArray():Promise<void> {
+ const addr = "127.0.0.1:4503";
+ const buf = bufferServer(addr);
+ const bodyStr = "hello world";
+ const body = new TextEncoder().encode(bodyStr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [["Hello", "World"], ["Foo", "Bar"]],
+ body
+ });
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode(buf.bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ `host: ${addr}\r\n`,
+ `content-length: ${body.byteLength}\r\n\r\n`,
+ bodyStr
+ ].join("");
+ assertEquals(actual, expected);
+});
+*/
diff --git a/cli/js/file_info.ts b/cli/js/file_info.ts
new file mode 100644
index 000000000..a98989e79
--- /dev/null
+++ b/cli/js/file_info.ts
@@ -0,0 +1,91 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { StatResponse } from "./stat.ts";
+
+/** A FileInfo describes a file and is returned by `stat`, `lstat`,
+ * `statSync`, `lstatSync`.
+ */
+export interface FileInfo {
+ /** The size of the file, in bytes. */
+ len: number;
+ /** The last modification time of the file. This corresponds to the `mtime`
+ * field from `stat` on Unix and `ftLastWriteTime` on Windows. This may not
+ * be available on all platforms.
+ */
+ modified: number | null;
+ /** The last access time of the file. This corresponds to the `atime`
+ * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not
+ * be available on all platforms.
+ */
+ accessed: number | null;
+ /** The last access time of the file. This corresponds to the `birthtime`
+ * field from `stat` on Unix and `ftCreationTime` on Windows. This may not
+ * be available on all platforms.
+ */
+ created: number | null;
+ /** The underlying raw st_mode bits that contain the standard Unix permissions
+ * for this file/directory. TODO Match behavior with Go on windows for mode.
+ */
+ mode: number | null;
+
+ /** The file or directory name. */
+ name: string | null;
+
+ /** Returns whether this is info for a regular file. This result is mutually
+ * exclusive to `FileInfo.isDirectory` and `FileInfo.isSymlink`.
+ */
+ isFile(): boolean;
+
+ /** Returns whether this is info for a regular directory. This result is
+ * mutually exclusive to `FileInfo.isFile` and `FileInfo.isSymlink`.
+ */
+ isDirectory(): boolean;
+
+ /** Returns whether this is info for a symlink. This result is
+ * mutually exclusive to `FileInfo.isFile` and `FileInfo.isDirectory`.
+ */
+ isSymlink(): boolean;
+}
+
+// @internal
+export class FileInfoImpl implements FileInfo {
+ private readonly _isFile: boolean;
+ private readonly _isSymlink: boolean;
+ len: number;
+ modified: number | null;
+ accessed: number | null;
+ created: number | null;
+ mode: number | null;
+ name: string | null;
+
+ /* @internal */
+ constructor(private _res: StatResponse) {
+ const modified = this._res.modified;
+ const accessed = this._res.accessed;
+ const created = this._res.created;
+ const hasMode = this._res.hasMode;
+ const mode = this._res.mode; // negative for invalid mode (Windows)
+ const name = this._res.name;
+
+ this._isFile = this._res.isFile;
+ this._isSymlink = this._res.isSymlink;
+ this.len = this._res.len;
+ this.modified = modified ? modified : null;
+ this.accessed = accessed ? accessed : null;
+ this.created = created ? created : null;
+ // null on Windows
+ this.mode = hasMode ? mode : null;
+ this.name = name ? name : null;
+ }
+
+ isFile(): boolean {
+ return this._isFile;
+ }
+
+ isDirectory(): boolean {
+ return !this._isFile && !this._isSymlink;
+ }
+
+ isSymlink(): boolean {
+ return this._isSymlink;
+ }
+}
diff --git a/cli/js/file_test.ts b/cli/js/file_test.ts
new file mode 100644
index 000000000..345dcd8fe
--- /dev/null
+++ b/cli/js/file_test.ts
@@ -0,0 +1,103 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEquals } from "./test_util.ts";
+
+function testFirstArgument(arg1, expectedSize): void {
+ const file = new File(arg1, "name");
+ assert(file instanceof File);
+ assertEquals(file.name, "name");
+ assertEquals(file.size, expectedSize);
+ assertEquals(file.type, "");
+}
+
+test(function fileEmptyFileBits(): void {
+ testFirstArgument([], 0);
+});
+
+test(function fileStringFileBits(): void {
+ testFirstArgument(["bits"], 4);
+});
+
+test(function fileUnicodeStringFileBits(): void {
+ testFirstArgument(["𝓽𝓮𝔁𝓽"], 16);
+});
+
+test(function fileStringObjectFileBits(): void {
+ testFirstArgument([new String("string object")], 13);
+});
+
+test(function fileEmptyBlobFileBits(): void {
+ testFirstArgument([new Blob()], 0);
+});
+
+test(function fileBlobFileBits(): void {
+ testFirstArgument([new Blob(["bits"])], 4);
+});
+
+test(function fileEmptyFileFileBits(): void {
+ testFirstArgument([new File([], "world.txt")], 0);
+});
+
+test(function fileFileFileBits(): void {
+ testFirstArgument([new File(["bits"], "world.txt")], 4);
+});
+
+test(function fileArrayBufferFileBits(): void {
+ testFirstArgument([new ArrayBuffer(8)], 8);
+});
+
+test(function fileTypedArrayFileBits(): void {
+ testFirstArgument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4);
+});
+
+test(function fileVariousFileBits(): void {
+ testFirstArgument(
+ [
+ "bits",
+ new Blob(["bits"]),
+ new Blob(),
+ new Uint8Array([0x50, 0x41]),
+ new Uint16Array([0x5353]),
+ new Uint32Array([0x53534150])
+ ],
+ 16
+ );
+});
+
+test(function fileNumberInFileBits(): void {
+ testFirstArgument([12], 2);
+});
+
+test(function fileArrayInFileBits(): void {
+ testFirstArgument([[1, 2, 3]], 5);
+});
+
+test(function fileObjectInFileBits(): void {
+ // "[object Object]"
+ testFirstArgument([{}], 15);
+});
+
+function testSecondArgument(arg2, expectedFileName): void {
+ const file = new File(["bits"], arg2);
+ assert(file instanceof File);
+ assertEquals(file.name, expectedFileName);
+}
+
+test(function fileUsingFileName(): void {
+ testSecondArgument("dummy", "dummy");
+});
+
+test(function fileUsingSpecialCharacterInFileName(): void {
+ testSecondArgument("dummy/foo", "dummy:foo");
+});
+
+test(function fileUsingNullFileName(): void {
+ testSecondArgument(null, "null");
+});
+
+test(function fileUsingNumberFileName(): void {
+ testSecondArgument(1, "1");
+});
+
+test(function fileUsingEmptyStringFileName(): void {
+ testSecondArgument("", "");
+});
diff --git a/cli/js/files.ts b/cli/js/files.ts
new file mode 100644
index 000000000..b83a147e1
--- /dev/null
+++ b/cli/js/files.ts
@@ -0,0 +1,235 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import {
+ EOF,
+ Reader,
+ Writer,
+ Seeker,
+ Closer,
+ SeekMode,
+ SyncReader,
+ SyncWriter,
+ SyncSeeker
+} from "./io.ts";
+import { sendAsyncMinimal, sendSyncMinimal } from "./dispatch_minimal.ts";
+import * as dispatch from "./dispatch.ts";
+import {
+ sendSync as sendSyncJson,
+ sendAsync as sendAsyncJson
+} from "./dispatch_json.ts";
+
+/** Open a file and return an instance of the `File` object
+ * synchronously.
+ *
+ * const file = Deno.openSync("/foo/bar.txt");
+ */
+export function openSync(filename: string, mode: OpenMode = "r"): File {
+ const rid = sendSyncJson(dispatch.OP_OPEN, { filename, mode });
+ return new File(rid);
+}
+
+/** Open a file and return an instance of the `File` object.
+ *
+ * (async () => {
+ * const file = await Deno.open("/foo/bar.txt");
+ * })();
+ */
+export async function open(
+ filename: string,
+ mode: OpenMode = "r"
+): Promise<File> {
+ const rid = await sendAsyncJson(dispatch.OP_OPEN, { filename, mode });
+ return new File(rid);
+}
+
+/** Read synchronously from a file ID into an array buffer.
+ *
+ * Return `number | EOF` for the operation.
+ *
+ * const file = Deno.openSync("/foo/bar.txt");
+ * const buf = new Uint8Array(100);
+ * const nread = Deno.readSync(file.rid, buf);
+ * const text = new TextDecoder().decode(buf);
+ *
+ */
+export function readSync(rid: number, p: Uint8Array): number | EOF {
+ const nread = sendSyncMinimal(dispatch.OP_READ, rid, p);
+ if (nread < 0) {
+ throw new Error("read error");
+ } else if (nread == 0) {
+ return EOF;
+ } else {
+ return nread;
+ }
+}
+
+/** Read from a file ID into an array buffer.
+ *
+ * Resolves with the `number | EOF` for the operation.
+ *
+ * (async () => {
+ * const file = await Deno.open("/foo/bar.txt");
+ * const buf = new Uint8Array(100);
+ * const nread = await Deno.read(file.rid, buf);
+ * const text = new TextDecoder().decode(buf);
+ * })();
+ */
+export async function read(rid: number, p: Uint8Array): Promise<number | EOF> {
+ const nread = await sendAsyncMinimal(dispatch.OP_READ, rid, p);
+ if (nread < 0) {
+ throw new Error("read error");
+ } else if (nread == 0) {
+ return EOF;
+ } else {
+ return nread;
+ }
+}
+
+/** Write synchronously to the file ID the contents of the array buffer.
+ *
+ * Resolves with the number of bytes written.
+ *
+ * const encoder = new TextEncoder();
+ * const data = encoder.encode("Hello world\n");
+ * const file = Deno.openSync("/foo/bar.txt");
+ * Deno.writeSync(file.rid, data);
+ */
+export function writeSync(rid: number, p: Uint8Array): number {
+ const result = sendSyncMinimal(dispatch.OP_WRITE, rid, p);
+ if (result < 0) {
+ throw new Error("write error");
+ } else {
+ return result;
+ }
+}
+
+/** Write to the file ID the contents of the array buffer.
+ *
+ * Resolves with the number of bytes written.
+ *
+ * (async () => {
+ * const encoder = new TextEncoder();
+ * const data = encoder.encode("Hello world\n");
+ * const file = await Deno.open("/foo/bar.txt");
+ * await Deno.write(file.rid, data);
+ * })();
+ *
+ */
+export async function write(rid: number, p: Uint8Array): Promise<number> {
+ const result = await sendAsyncMinimal(dispatch.OP_WRITE, rid, p);
+ if (result < 0) {
+ throw new Error("write error");
+ } else {
+ return result;
+ }
+}
+
+/** Seek a file ID synchronously to the given offset under mode given by `whence`.
+ *
+ * const file = Deno.openSync("/foo/bar.txt");
+ * Deno.seekSync(file.rid, 0, 0);
+ */
+export function seekSync(rid: number, offset: number, whence: SeekMode): void {
+ sendSyncJson(dispatch.OP_SEEK, { rid, offset, whence });
+}
+
+/** Seek a file ID to the given offset under mode given by `whence`.
+ *
+ * (async () => {
+ * const file = await Deno.open("/foo/bar.txt");
+ * await Deno.seek(file.rid, 0, 0);
+ * })();
+ */
+export async function seek(
+ rid: number,
+ offset: number,
+ whence: SeekMode
+): Promise<void> {
+ await sendAsyncJson(dispatch.OP_SEEK, { rid, offset, whence });
+}
+
+/** Close the file ID. */
+export function close(rid: number): void {
+ sendSyncJson(dispatch.OP_CLOSE, { rid });
+}
+
+/** The Deno abstraction for reading and writing files. */
+export class File
+ implements
+ Reader,
+ SyncReader,
+ Writer,
+ SyncWriter,
+ Seeker,
+ SyncSeeker,
+ Closer {
+ constructor(readonly rid: number) {}
+
+ write(p: Uint8Array): Promise<number> {
+ return write(this.rid, p);
+ }
+
+ writeSync(p: Uint8Array): number {
+ return writeSync(this.rid, p);
+ }
+
+ read(p: Uint8Array): Promise<number | EOF> {
+ return read(this.rid, p);
+ }
+
+ readSync(p: Uint8Array): number | EOF {
+ return readSync(this.rid, p);
+ }
+
+ seek(offset: number, whence: SeekMode): Promise<void> {
+ return seek(this.rid, offset, whence);
+ }
+
+ seekSync(offset: number, whence: SeekMode): void {
+ return seekSync(this.rid, offset, whence);
+ }
+
+ close(): void {
+ close(this.rid);
+ }
+}
+
+/** An instance of `File` for stdin. */
+export const stdin = new File(0);
+/** An instance of `File` for stdout. */
+export const stdout = new File(1);
+/** An instance of `File` for stderr. */
+export const stderr = new File(2);
+
+export type OpenMode =
+ /** Read-only. Default. Starts at beginning of file. */
+ | "r"
+ /** Read-write. Start at beginning of file. */
+ | "r+"
+ /** Write-only. Opens and truncates existing file or creates new one for
+ * writing only.
+ */
+ | "w"
+ /** Read-write. Opens and truncates existing file or creates new one for
+ * writing and reading.
+ */
+ | "w+"
+ /** Write-only. Opens existing file or creates new one. Each write appends
+ * content to the end of file.
+ */
+ | "a"
+ /** Read-write. Behaves like "a" and allows to read from file. */
+ | "a+"
+ /** Write-only. Exclusive create - creates new file only if one doesn't exist
+ * already.
+ */
+ | "x"
+ /** Read-write. Behaves like `x` and allows to read from file. */
+ | "x+";
+
+/** A factory function for creating instances of `File` associated with the
+ * supplied file name.
+ * @internal
+ */
+export function create(filename: string): Promise<File> {
+ return open(filename, "w+");
+}
diff --git a/cli/js/files_test.ts b/cli/js/files_test.ts
new file mode 100644
index 000000000..004cb662b
--- /dev/null
+++ b/cli/js/files_test.ts
@@ -0,0 +1,329 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, testPerm, assert, assertEquals } from "./test_util.ts";
+
+test(function filesStdioFileDescriptors(): void {
+ assertEquals(Deno.stdin.rid, 0);
+ assertEquals(Deno.stdout.rid, 1);
+ assertEquals(Deno.stderr.rid, 2);
+});
+
+testPerm({ read: true }, async function filesCopyToStdout(): Promise<void> {
+ const filename = "package.json";
+ const file = await Deno.open(filename);
+ assert(file.rid > 2);
+ const bytesWritten = await Deno.copy(Deno.stdout, file);
+ const fileSize = Deno.statSync(filename).len;
+ assertEquals(bytesWritten, fileSize);
+ console.log("bytes written", bytesWritten);
+});
+
+testPerm({ read: true }, async function filesToAsyncIterator(): Promise<void> {
+ const filename = "tests/hello.txt";
+ const file = await Deno.open(filename);
+
+ let totalSize = 0;
+ for await (const buf of Deno.toAsyncIterator(file)) {
+ totalSize += buf.byteLength;
+ }
+
+ assertEquals(totalSize, 12);
+});
+
+test(async function readerToAsyncIterator(): Promise<void> {
+ // ref: https://github.com/denoland/deno/issues/2330
+ const encoder = new TextEncoder();
+
+ class TestReader implements Deno.Reader {
+ private offset = 0;
+ private buf = new Uint8Array(encoder.encode(this.s));
+
+ constructor(private readonly s: string) {}
+
+ async read(p: Uint8Array): Promise<number | Deno.EOF> {
+ const n = Math.min(p.byteLength, this.buf.byteLength - this.offset);
+ p.set(this.buf.slice(this.offset, this.offset + n));
+ this.offset += n;
+
+ if (n === 0) {
+ return Deno.EOF;
+ }
+
+ return n;
+ }
+ }
+
+ const reader = new TestReader("hello world!");
+
+ let totalSize = 0;
+ for await (const buf of Deno.toAsyncIterator(reader)) {
+ totalSize += buf.byteLength;
+ }
+
+ assertEquals(totalSize, 12);
+});
+
+testPerm({ write: false }, async function writePermFailure(): Promise<void> {
+ const filename = "tests/hello.txt";
+ const writeModes: Deno.OpenMode[] = ["w", "a", "x"];
+ for (const mode of writeModes) {
+ let err;
+ try {
+ await Deno.open(filename, mode);
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+ }
+});
+
+testPerm({ read: false }, async function readPermFailure(): Promise<void> {
+ let caughtError = false;
+ try {
+ await Deno.open("package.json", "r");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ write: true }, async function writeNullBufferFailure(): Promise<
+ void
+> {
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "hello.txt";
+ const file = await Deno.open(filename, "w");
+
+ // writing null should throw an error
+ let err;
+ try {
+ await file.write(null);
+ } catch (e) {
+ err = e;
+ }
+ // TODO: Check error kind when dispatch_minimal pipes errors properly
+ assert(!!err);
+
+ file.close();
+ await Deno.remove(tempDir, { recursive: true });
+});
+
+testPerm(
+ { write: true, read: true },
+ async function readNullBufferFailure(): Promise<void> {
+ const tempDir = Deno.makeTempDirSync();
+ const filename = tempDir + "hello.txt";
+ const file = await Deno.open(filename, "w+");
+
+ // reading file into null buffer should throw an error
+ let err;
+ try {
+ await file.read(null);
+ } catch (e) {
+ err = e;
+ }
+ // TODO: Check error kind when dispatch_minimal pipes errors properly
+ assert(!!err);
+
+ file.close();
+ await Deno.remove(tempDir, { recursive: true });
+ }
+);
+
+testPerm(
+ { write: false, read: false },
+ async function readWritePermFailure(): Promise<void> {
+ const filename = "tests/hello.txt";
+ const writeModes: Deno.OpenMode[] = ["r+", "w+", "a+", "x+"];
+ for (const mode of writeModes) {
+ let err;
+ try {
+ await Deno.open(filename, mode);
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+ }
+ }
+);
+
+testPerm({ read: true, write: true }, async function createFile(): Promise<
+ void
+> {
+ const tempDir = await Deno.makeTempDir();
+ const filename = tempDir + "/test.txt";
+ const f = await Deno.open(filename, "w");
+ let fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile());
+ assert(fileInfo.len === 0);
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ await f.write(data);
+ fileInfo = Deno.statSync(filename);
+ assert(fileInfo.len === 5);
+ f.close();
+
+ // TODO: test different modes
+ await Deno.remove(tempDir, { recursive: true });
+});
+
+testPerm({ read: true, write: true }, async function openModeWrite(): Promise<
+ void
+> {
+ const tempDir = Deno.makeTempDirSync();
+ const encoder = new TextEncoder();
+ const filename = tempDir + "hello.txt";
+ const data = encoder.encode("Hello world!\n");
+
+ let file = await Deno.open(filename, "w");
+ // assert file was created
+ let fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile());
+ assertEquals(fileInfo.len, 0);
+ // write some data
+ await file.write(data);
+ fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.len, 13);
+ // assert we can't read from file
+ let thrown = false;
+ try {
+ const buf = new Uint8Array(20);
+ await file.read(buf);
+ } catch (e) {
+ thrown = true;
+ } finally {
+ assert(thrown, "'w' mode shouldn't allow to read file");
+ }
+ file.close();
+ // assert that existing file is truncated on open
+ file = await Deno.open(filename, "w");
+ file.close();
+ const fileSize = Deno.statSync(filename).len;
+ assertEquals(fileSize, 0);
+ await Deno.remove(tempDir, { recursive: true });
+});
+
+testPerm(
+ { read: true, write: true },
+ async function openModeWriteRead(): Promise<void> {
+ const tempDir = Deno.makeTempDirSync();
+ const encoder = new TextEncoder();
+ const filename = tempDir + "hello.txt";
+ const data = encoder.encode("Hello world!\n");
+
+ const file = await Deno.open(filename, "w+");
+ // assert file was created
+ let fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile());
+ assertEquals(fileInfo.len, 0);
+ // write some data
+ await file.write(data);
+ fileInfo = Deno.statSync(filename);
+ assertEquals(fileInfo.len, 13);
+
+ const buf = new Uint8Array(20);
+ await file.seek(0, Deno.SeekMode.SEEK_START);
+ const result = await file.read(buf);
+ assertEquals(result, 13);
+ file.close();
+
+ await Deno.remove(tempDir, { recursive: true });
+ }
+);
+
+testPerm({ read: true }, async function seekStart(): Promise<void> {
+ const filename = "tests/hello.txt";
+ const file = await Deno.open(filename);
+ // Deliberately move 1 step forward
+ await file.read(new Uint8Array(1)); // "H"
+ // Skipping "Hello "
+ await file.seek(6, Deno.SeekMode.SEEK_START);
+ const buf = new Uint8Array(6);
+ await file.read(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+testPerm({ read: true }, function seekSyncStart(): void {
+ const filename = "tests/hello.txt";
+ const file = Deno.openSync(filename);
+ // Deliberately move 1 step forward
+ file.readSync(new Uint8Array(1)); // "H"
+ // Skipping "Hello "
+ file.seekSync(6, Deno.SeekMode.SEEK_START);
+ const buf = new Uint8Array(6);
+ file.readSync(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+testPerm({ read: true }, async function seekCurrent(): Promise<void> {
+ const filename = "tests/hello.txt";
+ const file = await Deno.open(filename);
+ // Deliberately move 1 step forward
+ await file.read(new Uint8Array(1)); // "H"
+ // Skipping "ello "
+ await file.seek(5, Deno.SeekMode.SEEK_CURRENT);
+ const buf = new Uint8Array(6);
+ await file.read(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+testPerm({ read: true }, function seekSyncCurrent(): void {
+ const filename = "tests/hello.txt";
+ const file = Deno.openSync(filename);
+ // Deliberately move 1 step forward
+ file.readSync(new Uint8Array(1)); // "H"
+ // Skipping "ello "
+ file.seekSync(5, Deno.SeekMode.SEEK_CURRENT);
+ const buf = new Uint8Array(6);
+ file.readSync(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+testPerm({ read: true }, async function seekEnd(): Promise<void> {
+ const filename = "tests/hello.txt";
+ const file = await Deno.open(filename);
+ await file.seek(-6, Deno.SeekMode.SEEK_END);
+ const buf = new Uint8Array(6);
+ await file.read(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+testPerm({ read: true }, function seekSyncEnd(): void {
+ const filename = "tests/hello.txt";
+ const file = Deno.openSync(filename);
+ file.seekSync(-6, Deno.SeekMode.SEEK_END);
+ const buf = new Uint8Array(6);
+ file.readSync(buf);
+ const decoded = new TextDecoder().decode(buf);
+ assertEquals(decoded, "world!");
+});
+
+testPerm({ read: true }, async function seekMode(): Promise<void> {
+ const filename = "tests/hello.txt";
+ const file = await Deno.open(filename);
+ let err;
+ try {
+ await file.seek(1, -1);
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.InvalidSeekMode);
+ assertEquals(err.name, "InvalidSeekMode");
+
+ // We should still be able to read the file
+ // since it is still open.
+ const buf = new Uint8Array(1);
+ await file.read(buf); // "H"
+ assertEquals(new TextDecoder().decode(buf), "H");
+});
diff --git a/cli/js/form_data.ts b/cli/js/form_data.ts
new file mode 100644
index 000000000..89efb3c00
--- /dev/null
+++ b/cli/js/form_data.ts
@@ -0,0 +1,149 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types.ts";
+import * as blob from "./blob.ts";
+import * as domFile from "./dom_file.ts";
+import { DomIterableMixin } from "./mixins/dom_iterable.ts";
+import { requiredArguments } from "./util.ts";
+
+const dataSymbol = Symbol("data");
+
+class FormDataBase {
+ private [dataSymbol]: Array<[string, domTypes.FormDataEntryValue]> = [];
+
+ /** Appends a new value onto an existing key inside a `FormData`
+ * object, or adds the key if it does not already exist.
+ *
+ * formData.append('name', 'first');
+ * formData.append('name', 'second');
+ */
+ append(name: string, value: string): void;
+ append(name: string, value: blob.DenoBlob, filename?: string): void;
+ append(name: string, value: string | blob.DenoBlob, filename?: string): void {
+ requiredArguments("FormData.append", arguments.length, 2);
+ name = String(name);
+ if (value instanceof blob.DenoBlob) {
+ const dfile = new domFile.DomFileImpl([value], filename || name);
+ this[dataSymbol].push([name, dfile]);
+ } else {
+ this[dataSymbol].push([name, String(value)]);
+ }
+ }
+
+ /** Deletes a key/value pair from a `FormData` object.
+ *
+ * formData.delete('name');
+ */
+ delete(name: string): void {
+ requiredArguments("FormData.delete", arguments.length, 1);
+ name = String(name);
+ let i = 0;
+ while (i < this[dataSymbol].length) {
+ if (this[dataSymbol][i][0] === name) {
+ this[dataSymbol].splice(i, 1);
+ } else {
+ i++;
+ }
+ }
+ }
+
+ /** Returns an array of all the values associated with a given key
+ * from within a `FormData`.
+ *
+ * formData.getAll('name');
+ */
+ getAll(name: string): domTypes.FormDataEntryValue[] {
+ requiredArguments("FormData.getAll", arguments.length, 1);
+ name = String(name);
+ const values = [];
+ for (const entry of this[dataSymbol]) {
+ if (entry[0] === name) {
+ values.push(entry[1]);
+ }
+ }
+
+ return values;
+ }
+
+ /** Returns the first value associated with a given key from within a
+ * `FormData` object.
+ *
+ * formData.get('name');
+ */
+ get(name: string): domTypes.FormDataEntryValue | null {
+ requiredArguments("FormData.get", arguments.length, 1);
+ name = String(name);
+ for (const entry of this[dataSymbol]) {
+ if (entry[0] === name) {
+ return entry[1];
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns a boolean stating whether a `FormData` object contains a
+ * certain key/value pair.
+ *
+ * formData.has('name');
+ */
+ has(name: string): boolean {
+ requiredArguments("FormData.has", arguments.length, 1);
+ name = String(name);
+ return this[dataSymbol].some((entry): boolean => entry[0] === name);
+ }
+
+ /** Sets a new value for an existing key inside a `FormData` object, or
+ * adds the key/value if it does not already exist.
+ * ref: https://xhr.spec.whatwg.org/#dom-formdata-set
+ *
+ * formData.set('name', 'value');
+ */
+ set(name: string, value: string): void;
+ set(name: string, value: blob.DenoBlob, filename?: string): void;
+ set(name: string, value: string | blob.DenoBlob, filename?: string): void {
+ requiredArguments("FormData.set", arguments.length, 2);
+ name = String(name);
+
+ // If there are any entries in the context object’s entry list whose name
+ // is name, replace the first such entry with entry and remove the others
+ let found = false;
+ let i = 0;
+ while (i < this[dataSymbol].length) {
+ if (this[dataSymbol][i][0] === name) {
+ if (!found) {
+ if (value instanceof blob.DenoBlob) {
+ const dfile = new domFile.DomFileImpl([value], filename || name);
+ this[dataSymbol][i][1] = dfile;
+ } else {
+ this[dataSymbol][i][1] = String(value);
+ }
+ found = true;
+ } else {
+ this[dataSymbol].splice(i, 1);
+ continue;
+ }
+ }
+ i++;
+ }
+
+ // Otherwise, append entry to the context object’s entry list.
+ if (!found) {
+ if (value instanceof blob.DenoBlob) {
+ const dfile = new domFile.DomFileImpl([value], filename || name);
+ this[dataSymbol].push([name, dfile]);
+ } else {
+ this[dataSymbol].push([name, String(value)]);
+ }
+ }
+ }
+
+ get [Symbol.toStringTag](): string {
+ return "FormData";
+ }
+}
+
+export class FormData extends DomIterableMixin<
+ string,
+ domTypes.FormDataEntryValue,
+ typeof FormDataBase
+>(FormDataBase, dataSymbol) {}
diff --git a/cli/js/form_data_test.ts b/cli/js/form_data_test.ts
new file mode 100644
index 000000000..fe8b6cf32
--- /dev/null
+++ b/cli/js/form_data_test.ts
@@ -0,0 +1,179 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEquals } from "./test_util.ts";
+
+test(function formDataHasCorrectNameProp(): void {
+ assertEquals(FormData.name, "FormData");
+});
+
+test(function formDataParamsAppendSuccess(): void {
+ const formData = new FormData();
+ formData.append("a", "true");
+ assertEquals(formData.get("a"), "true");
+});
+
+test(function formDataParamsDeleteSuccess(): void {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ assertEquals(formData.get("b"), "false");
+ formData.delete("b");
+ assertEquals(formData.get("a"), "true");
+ assertEquals(formData.get("b"), null);
+});
+
+test(function formDataParamsGetAllSuccess(): void {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ formData.append("a", "null");
+ assertEquals(formData.getAll("a"), ["true", "null"]);
+ assertEquals(formData.getAll("b"), ["false"]);
+ assertEquals(formData.getAll("c"), []);
+});
+
+test(function formDataParamsGetSuccess(): void {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ formData.append("a", "null");
+ formData.append("d", undefined);
+ formData.append("e", null);
+ assertEquals(formData.get("a"), "true");
+ assertEquals(formData.get("b"), "false");
+ assertEquals(formData.get("c"), null);
+ assertEquals(formData.get("d"), "undefined");
+ assertEquals(formData.get("e"), "null");
+});
+
+test(function formDataParamsHasSuccess(): void {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ assert(formData.has("a"));
+ assert(formData.has("b"));
+ assert(!formData.has("c"));
+});
+
+test(function formDataParamsSetSuccess(): void {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ formData.append("a", "null");
+ assertEquals(formData.getAll("a"), ["true", "null"]);
+ assertEquals(formData.getAll("b"), ["false"]);
+ formData.set("a", "false");
+ assertEquals(formData.getAll("a"), ["false"]);
+ formData.set("d", undefined);
+ assertEquals(formData.get("d"), "undefined");
+ formData.set("e", null);
+ assertEquals(formData.get("e"), "null");
+});
+
+test(function formDataSetEmptyBlobSuccess(): void {
+ const formData = new FormData();
+ formData.set("a", new Blob([]), "blank.txt");
+ formData.get("a");
+ /* TODO Fix this test.
+ assert(file instanceof File);
+ if (typeof file !== "string") {
+ assertEquals(file.name, "blank.txt");
+ }
+ */
+});
+
+test(function formDataParamsForEachSuccess(): void {
+ const init = [["a", "54"], ["b", "true"]];
+ const formData = new FormData();
+ for (const [name, value] of init) {
+ formData.append(name, value);
+ }
+ let callNum = 0;
+ formData.forEach(
+ (value, key, parent): void => {
+ assertEquals(formData, parent);
+ assertEquals(value, init[callNum][1]);
+ assertEquals(key, init[callNum][0]);
+ callNum++;
+ }
+ );
+ assertEquals(callNum, init.length);
+});
+
+test(function formDataParamsArgumentsCheck(): void {
+ const methodRequireOneParam = ["delete", "getAll", "get", "has", "forEach"];
+
+ const methodRequireTwoParams = ["append", "set"];
+
+ methodRequireOneParam.forEach(
+ (method): void => {
+ const formData = new FormData();
+ let hasThrown = 0;
+ let errMsg = "";
+ try {
+ formData[method]();
+ hasThrown = 1;
+ } catch (err) {
+ errMsg = err.message;
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ assertEquals(
+ errMsg,
+ `FormData.${method} requires at least 1 argument, but only 0 present`
+ );
+ }
+ );
+
+ methodRequireTwoParams.forEach(
+ (method: string): void => {
+ const formData = new FormData();
+ let hasThrown = 0;
+ let errMsg = "";
+
+ try {
+ formData[method]();
+ hasThrown = 1;
+ } catch (err) {
+ errMsg = err.message;
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ assertEquals(
+ errMsg,
+ `FormData.${method} requires at least 2 arguments, but only 0 present`
+ );
+
+ hasThrown = 0;
+ errMsg = "";
+ try {
+ formData[method]("foo");
+ hasThrown = 1;
+ } catch (err) {
+ errMsg = err.message;
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ assertEquals(
+ errMsg,
+ `FormData.${method} requires at least 2 arguments, but only 1 present`
+ );
+ }
+ );
+});
+
+test(function toStringShouldBeWebCompatibility(): void {
+ const formData = new FormData();
+ assertEquals(formData.toString(), "[object FormData]");
+});
diff --git a/cli/js/format_error.ts b/cli/js/format_error.ts
new file mode 100644
index 000000000..801da0d0b
--- /dev/null
+++ b/cli/js/format_error.ts
@@ -0,0 +1,9 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as dispatch from "./dispatch.ts";
+import { sendSync } from "./dispatch_json.ts";
+
+// TODO(bartlomieju): move to `repl.ts`?
+export function formatError(errString: string): string {
+ const res = sendSync(dispatch.OP_FORMAT_ERROR, { error: errString });
+ return res.error;
+}
diff --git a/cli/js/get_random_values.ts b/cli/js/get_random_values.ts
new file mode 100644
index 000000000..e54f34785
--- /dev/null
+++ b/cli/js/get_random_values.ts
@@ -0,0 +1,31 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as dispatch from "./dispatch.ts";
+import { sendSync } from "./dispatch_json.ts";
+import { assert } from "./util.ts";
+
+/** Synchronously collects cryptographically secure random values. The
+ * underlying CSPRNG in use is Rust's `rand::rngs::ThreadRng`.
+ *
+ * const arr = new Uint8Array(32);
+ * crypto.getRandomValues(arr);
+ */
+export function getRandomValues<
+ T extends
+ | Int8Array
+ | Uint8Array
+ | Uint8ClampedArray
+ | Int16Array
+ | Uint16Array
+ | Int32Array
+ | Uint32Array
+>(typedArray: T): T {
+ assert(typedArray !== null, "Input must not be null");
+ assert(typedArray.length <= 65536, "Input must not be longer than 65536");
+ const ui8 = new Uint8Array(
+ typedArray.buffer,
+ typedArray.byteOffset,
+ typedArray.byteLength
+ );
+ sendSync(dispatch.OP_GET_RANDOM_VALUES, {}, ui8);
+ return typedArray;
+}
diff --git a/cli/js/get_random_values_test.ts b/cli/js/get_random_values_test.ts
new file mode 100644
index 000000000..68c13d597
--- /dev/null
+++ b/cli/js/get_random_values_test.ts
@@ -0,0 +1,51 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assertNotEquals, assertStrictEq } from "./test_util.ts";
+
+test(function getRandomValuesInt8Array(): void {
+ const arr = new Int8Array(32);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Int8Array(32));
+});
+
+test(function getRandomValuesUint8Array(): void {
+ const arr = new Uint8Array(32);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint8Array(32));
+});
+
+test(function getRandomValuesUint8ClampedArray(): void {
+ const arr = new Uint8ClampedArray(32);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint8ClampedArray(32));
+});
+
+test(function getRandomValuesInt16Array(): void {
+ const arr = new Int16Array(4);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Int16Array(4));
+});
+
+test(function getRandomValuesUint16Array(): void {
+ const arr = new Uint16Array(4);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint16Array(4));
+});
+
+test(function getRandomValuesInt32Array(): void {
+ const arr = new Int32Array(8);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Int32Array(8));
+});
+
+test(function getRandomValuesUint32Array(): void {
+ const arr = new Uint32Array(8);
+ crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint32Array(8));
+});
+
+test(function getRandomValuesReturnValue(): void {
+ const arr = new Uint32Array(8);
+ const rtn = crypto.getRandomValues(arr);
+ assertNotEquals(arr, new Uint32Array(8));
+ assertStrictEq(rtn, arr);
+});
diff --git a/cli/js/globals.ts b/cli/js/globals.ts
new file mode 100644
index 000000000..b734b8da3
--- /dev/null
+++ b/cli/js/globals.ts
@@ -0,0 +1,207 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+// This is a "special" module, in that it define the global runtime scope of
+// Deno, and therefore it defines a lot of the runtime environment that code
+// is evaluated in. We use this file to automatically build the runtime type
+// library.
+
+// Modules which will make up part of the global public API surface should be
+// imported as namespaces, so when the runtime type library is generated they
+// can be expressed as a namespace in the type library.
+import { window } from "./window.ts";
+import * as blob from "./blob.ts";
+import * as consoleTypes from "./console.ts";
+import * as csprng from "./get_random_values.ts";
+import * as customEvent from "./custom_event.ts";
+import * as Deno from "./deno.ts";
+import * as domTypes from "./dom_types.ts";
+import * as domFile from "./dom_file.ts";
+import * as event from "./event.ts";
+import * as eventTarget from "./event_target.ts";
+import * as formData from "./form_data.ts";
+import * as fetchTypes from "./fetch.ts";
+import * as headers from "./headers.ts";
+import * as textEncoding from "./text_encoding.ts";
+import * as timers from "./timers.ts";
+import * as url from "./url.ts";
+import * as urlSearchParams from "./url_search_params.ts";
+import * as workers from "./workers.ts";
+import * as performanceUtil from "./performance.ts";
+
+import * as request from "./request.ts";
+
+// These imports are not exposed and therefore are fine to just import the
+// symbols required.
+import { core } from "./core.ts";
+
+// During the build process, augmentations to the variable `window` in this
+// file are tracked and created as part of default library that is built into
+// Deno, we only need to declare the enough to compile Deno.
+declare global {
+ interface CallSite {
+ getThis(): unknown;
+ getTypeName(): string;
+ getFunction(): Function;
+ getFunctionName(): string;
+ getMethodName(): string;
+ getFileName(): string;
+ getLineNumber(): number | null;
+ getColumnNumber(): number | null;
+ getEvalOrigin(): string | null;
+ isToplevel(): boolean;
+ isEval(): boolean;
+ isNative(): boolean;
+ isConstructor(): boolean;
+ isAsync(): boolean;
+ isPromiseAll(): boolean;
+ getPromiseIndex(): number | null;
+ }
+
+ interface ErrorConstructor {
+ prepareStackTrace(error: Error, structuredStackTrace: CallSite[]): string;
+ }
+
+ interface Object {
+ [consoleTypes.customInspect]?(): string;
+ }
+}
+
+// A self reference to the global object.
+window.window = window;
+
+// This is the Deno namespace, it is handled differently from other window
+// properties when building the runtime type library, as the whole module
+// is flattened into a single namespace.
+window.Deno = Deno;
+
+// Globally available functions and object instances.
+window.atob = textEncoding.atob;
+window.btoa = textEncoding.btoa;
+window.fetch = fetchTypes.fetch;
+window.clearTimeout = timers.clearTimeout;
+window.clearInterval = timers.clearInterval;
+window.console = new consoleTypes.Console(core.print);
+window.setTimeout = timers.setTimeout;
+window.setInterval = timers.setInterval;
+window.location = (undefined as unknown) as domTypes.Location;
+window.onload = undefined as undefined | Function;
+window.onunload = undefined as undefined | Function;
+// The following Crypto interface implementation is not up to par with the
+// standard https://www.w3.org/TR/WebCryptoAPI/#crypto-interface as it does not
+// yet incorporate the SubtleCrypto interface as its "subtle" property.
+window.crypto = (csprng as unknown) as Crypto;
+// window.queueMicrotask added by hand to self-maintained lib.deno_runtime.d.ts
+
+// When creating the runtime type library, we use modifications to `window` to
+// determine what is in the global namespace. When we put a class in the
+// namespace, we also need its global instance type as well, otherwise users
+// won't be able to refer to instances.
+// We have to export the type aliases, so that TypeScript _knows_ they are
+// being used, which it cannot statically determine within this module.
+window.Blob = blob.DenoBlob;
+export type Blob = domTypes.Blob;
+
+export type Body = domTypes.Body;
+
+window.File = domFile.DomFileImpl as domTypes.DomFileConstructor;
+export type File = domTypes.DomFile;
+
+export type CustomEventInit = domTypes.CustomEventInit;
+window.CustomEvent = customEvent.CustomEvent;
+export type CustomEvent = domTypes.CustomEvent;
+export type EventInit = domTypes.EventInit;
+window.Event = event.Event;
+export type Event = domTypes.Event;
+export type EventListener = domTypes.EventListener;
+window.EventTarget = eventTarget.EventTarget;
+export type EventTarget = domTypes.EventTarget;
+window.URL = url.URL;
+export type URL = url.URL;
+window.URLSearchParams = urlSearchParams.URLSearchParams;
+export type URLSearchParams = domTypes.URLSearchParams;
+
+// Using the `as` keyword to use standard compliant interfaces as the Deno
+// implementations contain some implementation details we wouldn't want to
+// expose in the runtime type library.
+window.Headers = headers.Headers as domTypes.HeadersConstructor;
+export type Headers = domTypes.Headers;
+window.FormData = formData.FormData as domTypes.FormDataConstructor;
+export type FormData = domTypes.FormData;
+
+window.TextEncoder = textEncoding.TextEncoder;
+export type TextEncoder = textEncoding.TextEncoder;
+window.TextDecoder = textEncoding.TextDecoder;
+export type TextDecoder = textEncoding.TextDecoder;
+
+window.Request = request.Request as domTypes.RequestConstructor;
+export type Request = domTypes.Request;
+
+window.Response = fetchTypes.Response;
+export type Response = domTypes.Response;
+
+window.performance = new performanceUtil.Performance();
+
+// This variable functioning correctly depends on `declareAsLet`
+// in //tools/ts_library_builder/main.ts
+window.onmessage = workers.onmessage;
+
+window.workerMain = workers.workerMain;
+window.workerClose = workers.workerClose;
+window.postMessage = workers.postMessage;
+
+window.Worker = workers.WorkerImpl;
+export type Worker = workers.Worker;
+
+window[domTypes.eventTargetHost] = null;
+window[domTypes.eventTargetListeners] = {};
+window[domTypes.eventTargetMode] = "";
+window[domTypes.eventTargetNodeType] = 0;
+window[eventTarget.eventTargetAssignedSlot] = false;
+window[eventTarget.eventTargetHasActivationBehavior] = false;
+window.addEventListener = eventTarget.EventTarget.prototype.addEventListener;
+window.dispatchEvent = eventTarget.EventTarget.prototype.dispatchEvent;
+window.removeEventListener =
+ eventTarget.EventTarget.prototype.removeEventListener;
+
+// Registers the handler for window.onload function.
+window.addEventListener(
+ "load",
+ (e: domTypes.Event): void => {
+ const onload = window.onload;
+ if (typeof onload === "function") {
+ onload(e);
+ }
+ }
+);
+// Registers the handler for window.onunload function.
+window.addEventListener(
+ "unload",
+ (e: domTypes.Event): void => {
+ const onunload = window.onunload;
+ if (typeof onunload === "function") {
+ onunload(e);
+ }
+ }
+);
+
+// below are interfaces that are available in TypeScript but
+// have different signatures
+export interface ImportMeta {
+ url: string;
+ main: boolean;
+}
+
+export interface Crypto {
+ readonly subtle: null;
+ getRandomValues: <
+ T extends
+ | Int8Array
+ | Uint8Array
+ | Uint8ClampedArray
+ | Int16Array
+ | Uint16Array
+ | Int32Array
+ | Uint32Array
+ >(
+ typedArray: T
+ ) => T;
+}
diff --git a/cli/js/globals_test.ts b/cli/js/globals_test.ts
new file mode 100644
index 000000000..d7c50c5b1
--- /dev/null
+++ b/cli/js/globals_test.ts
@@ -0,0 +1,104 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert } from "./test_util.ts";
+
+test(function globalThisExists(): void {
+ assert(globalThis != null);
+});
+
+test(function windowExists(): void {
+ assert(window != null);
+});
+
+test(function windowWindowExists(): void {
+ assert(window.window === window);
+});
+
+test(function globalThisEqualsWindow(): void {
+ assert(globalThis === window);
+});
+
+test(function DenoNamespaceExists(): void {
+ assert(Deno != null);
+});
+
+test(function DenoNamespaceEqualsWindowDeno(): void {
+ assert(Deno === window.Deno);
+});
+
+test(function DenoNamespaceIsFrozen(): void {
+ assert(Object.isFrozen(Deno));
+});
+
+test(function webAssemblyExists(): void {
+ assert(typeof WebAssembly.compile === "function");
+});
+
+test(function DenoNamespaceImmutable(): void {
+ const denoCopy = window.Deno;
+ try {
+ // @ts-ignore
+ Deno = 1;
+ } catch {}
+ assert(denoCopy === Deno);
+ try {
+ // @ts-ignore
+ window.Deno = 1;
+ } catch {}
+ assert(denoCopy === Deno);
+ try {
+ delete window.Deno;
+ } catch {}
+ assert(denoCopy === Deno);
+
+ const { readFile } = Deno;
+ try {
+ // @ts-ignore
+ Deno.readFile = 1;
+ } catch {}
+ assert(readFile === Deno.readFile);
+ try {
+ delete window.Deno.readFile;
+ } catch {}
+ assert(readFile === Deno.readFile);
+
+ // @ts-ignore
+ const { print } = Deno.core;
+ try {
+ // @ts-ignore
+ Deno.core.print = 1;
+ } catch {}
+ // @ts-ignore
+ assert(print === Deno.core.print);
+ try {
+ // @ts-ignore
+ delete Deno.core.print;
+ } catch {}
+ // @ts-ignore
+ assert(print === Deno.core.print);
+});
+
+test(async function windowQueueMicrotask(): Promise<void> {
+ let resolve1: () => void | undefined;
+ let resolve2: () => void | undefined;
+ let microtaskDone = false;
+ const p1 = new Promise(
+ (res): void => {
+ resolve1 = (): void => {
+ microtaskDone = true;
+ res();
+ };
+ }
+ );
+ const p2 = new Promise(
+ (res): void => {
+ resolve2 = (): void => {
+ assert(microtaskDone);
+ res();
+ };
+ }
+ );
+ window.queueMicrotask(resolve1!);
+ setTimeout(resolve2!, 0);
+ await p1;
+ await p2;
+});
diff --git a/cli/js/headers.ts b/cli/js/headers.ts
new file mode 100644
index 000000000..dc0de54dd
--- /dev/null
+++ b/cli/js/headers.ts
@@ -0,0 +1,139 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types.ts";
+import { DomIterableMixin } from "./mixins/dom_iterable.ts";
+import { requiredArguments } from "./util.ts";
+
+// From node-fetch
+// Copyright (c) 2016 David Frank. MIT License.
+const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/;
+const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function isHeaders(value: any): value is domTypes.Headers {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ return value instanceof Headers;
+}
+
+const headerMap = Symbol("header map");
+
+// ref: https://fetch.spec.whatwg.org/#dom-headers
+class HeadersBase {
+ private [headerMap]: Map<string, string>;
+ // TODO: headerGuard? Investigate if it is needed
+ // node-fetch did not implement this but it is in the spec
+
+ private _normalizeParams(name: string, value?: string): string[] {
+ name = String(name).toLowerCase();
+ value = String(value).trim();
+ return [name, value];
+ }
+
+ // The following name/value validations are copied from
+ // https://github.com/bitinn/node-fetch/blob/master/src/headers.js
+ // Copyright (c) 2016 David Frank. MIT License.
+ private _validateName(name: string): void {
+ if (invalidTokenRegex.test(name) || name === "") {
+ throw new TypeError(`${name} is not a legal HTTP header name`);
+ }
+ }
+
+ private _validateValue(value: string): void {
+ if (invalidHeaderCharRegex.test(value)) {
+ throw new TypeError(`${value} is not a legal HTTP header value`);
+ }
+ }
+
+ constructor(init?: domTypes.HeadersInit) {
+ if (init === null) {
+ throw new TypeError(
+ "Failed to construct 'Headers'; The provided value was not valid"
+ );
+ } else if (isHeaders(init)) {
+ this[headerMap] = new Map(init);
+ } else {
+ this[headerMap] = new Map();
+ if (Array.isArray(init)) {
+ for (const tuple of init) {
+ // If header does not contain exactly two items,
+ // then throw a TypeError.
+ // ref: https://fetch.spec.whatwg.org/#concept-headers-fill
+ requiredArguments(
+ "Headers.constructor tuple array argument",
+ tuple.length,
+ 2
+ );
+
+ const [name, value] = this._normalizeParams(tuple[0], tuple[1]);
+ this._validateName(name);
+ this._validateValue(value);
+ const existingValue = this[headerMap].get(name);
+ this[headerMap].set(
+ name,
+ existingValue ? `${existingValue}, ${value}` : value
+ );
+ }
+ } else if (init) {
+ const names = Object.keys(init);
+ for (const rawName of names) {
+ const rawValue = init[rawName];
+ const [name, value] = this._normalizeParams(rawName, rawValue);
+ this._validateName(name);
+ this._validateValue(value);
+ this[headerMap].set(name, value);
+ }
+ }
+ }
+ }
+
+ // ref: https://fetch.spec.whatwg.org/#concept-headers-append
+ append(name: string, value: string): void {
+ requiredArguments("Headers.append", arguments.length, 2);
+ const [newname, newvalue] = this._normalizeParams(name, value);
+ this._validateName(newname);
+ this._validateValue(newvalue);
+ const v = this[headerMap].get(newname);
+ const str = v ? `${v}, ${newvalue}` : newvalue;
+ this[headerMap].set(newname, str);
+ }
+
+ delete(name: string): void {
+ requiredArguments("Headers.delete", arguments.length, 1);
+ const [newname] = this._normalizeParams(name);
+ this._validateName(newname);
+ this[headerMap].delete(newname);
+ }
+
+ get(name: string): string | null {
+ requiredArguments("Headers.get", arguments.length, 1);
+ const [newname] = this._normalizeParams(name);
+ this._validateName(newname);
+ const value = this[headerMap].get(newname);
+ return value || null;
+ }
+
+ has(name: string): boolean {
+ requiredArguments("Headers.has", arguments.length, 1);
+ const [newname] = this._normalizeParams(name);
+ this._validateName(newname);
+ return this[headerMap].has(newname);
+ }
+
+ set(name: string, value: string): void {
+ requiredArguments("Headers.set", arguments.length, 2);
+ const [newname, newvalue] = this._normalizeParams(name, value);
+ this._validateName(newname);
+ this._validateValue(newvalue);
+ this[headerMap].set(newname, newvalue);
+ }
+
+ get [Symbol.toStringTag](): string {
+ return "Headers";
+ }
+}
+
+// @internal
+export class Headers extends DomIterableMixin<
+ string,
+ string,
+ typeof HeadersBase
+>(HeadersBase, headerMap) {}
diff --git a/cli/js/headers_test.ts b/cli/js/headers_test.ts
new file mode 100644
index 000000000..f08283c51
--- /dev/null
+++ b/cli/js/headers_test.ts
@@ -0,0 +1,331 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEquals } from "./test_util.ts";
+
+// Logic heavily copied from web-platform-tests, make
+// sure pass mostly header basic test
+// ref: https://github.com/web-platform-tests/wpt/blob/7c50c216081d6ea3c9afe553ee7b64534020a1b2/fetch/api/headers/headers-basic.html
+test(function newHeaderTest(): void {
+ new Headers();
+ new Headers(undefined);
+ new Headers({});
+ try {
+ new Headers(null);
+ } catch (e) {
+ assertEquals(
+ e.message,
+ "Failed to construct 'Headers'; The provided value was not valid"
+ );
+ }
+});
+
+const headerDict = {
+ name1: "value1",
+ name2: "value2",
+ name3: "value3",
+ name4: undefined,
+ "Content-Type": "value4"
+};
+const headerSeq = [];
+for (const name in headerDict) {
+ headerSeq.push([name, headerDict[name]]);
+}
+
+test(function newHeaderWithSequence(): void {
+ const headers = new Headers(headerSeq);
+ for (const name in headerDict) {
+ assertEquals(headers.get(name), String(headerDict[name]));
+ }
+ assertEquals(headers.get("length"), null);
+});
+
+test(function newHeaderWithRecord(): void {
+ const headers = new Headers(headerDict);
+ for (const name in headerDict) {
+ assertEquals(headers.get(name), String(headerDict[name]));
+ }
+});
+
+test(function newHeaderWithHeadersInstance(): void {
+ const headers = new Headers(headerDict);
+ const headers2 = new Headers(headers);
+ for (const name in headerDict) {
+ assertEquals(headers2.get(name), String(headerDict[name]));
+ }
+});
+
+test(function headerAppendSuccess(): void {
+ const headers = new Headers();
+ for (const name in headerDict) {
+ headers.append(name, headerDict[name]);
+ assertEquals(headers.get(name), String(headerDict[name]));
+ }
+});
+
+test(function headerSetSuccess(): void {
+ const headers = new Headers();
+ for (const name in headerDict) {
+ headers.set(name, headerDict[name]);
+ assertEquals(headers.get(name), String(headerDict[name]));
+ }
+});
+
+test(function headerHasSuccess(): void {
+ const headers = new Headers(headerDict);
+ for (const name in headerDict) {
+ assert(headers.has(name), "headers has name " + name);
+ assert(
+ !headers.has("nameNotInHeaders"),
+ "headers do not have header: nameNotInHeaders"
+ );
+ }
+});
+
+test(function headerDeleteSuccess(): void {
+ const headers = new Headers(headerDict);
+ for (const name in headerDict) {
+ assert(headers.has(name), "headers have a header: " + name);
+ headers.delete(name);
+ assert(!headers.has(name), "headers do not have anymore a header: " + name);
+ }
+});
+
+test(function headerGetSuccess(): void {
+ const headers = new Headers(headerDict);
+ for (const name in headerDict) {
+ assertEquals(headers.get(name), String(headerDict[name]));
+ assertEquals(headers.get("nameNotInHeaders"), null);
+ }
+});
+
+test(function headerEntriesSuccess(): void {
+ const headers = new Headers(headerDict);
+ const iterators = headers.entries();
+ for (const it of iterators) {
+ const key = it[0];
+ const value = it[1];
+ assert(headers.has(key));
+ assertEquals(value, headers.get(key));
+ }
+});
+
+test(function headerKeysSuccess(): void {
+ const headers = new Headers(headerDict);
+ const iterators = headers.keys();
+ for (const it of iterators) {
+ assert(headers.has(it));
+ }
+});
+
+test(function headerValuesSuccess(): void {
+ const headers = new Headers(headerDict);
+ const iterators = headers.values();
+ const entries = headers.entries();
+ const values = [];
+ for (const pair of entries) {
+ values.push(pair[1]);
+ }
+ for (const it of iterators) {
+ assert(values.includes(it));
+ }
+});
+
+const headerEntriesDict = {
+ name1: "value1",
+ Name2: "value2",
+ name: "value3",
+ "content-Type": "value4",
+ "Content-Typ": "value5",
+ "Content-Types": "value6"
+};
+
+test(function headerForEachSuccess(): void {
+ const headers = new Headers(headerEntriesDict);
+ const keys = Object.keys(headerEntriesDict);
+ keys.forEach(
+ (key): void => {
+ const value = headerEntriesDict[key];
+ const newkey = key.toLowerCase();
+ headerEntriesDict[newkey] = value;
+ }
+ );
+ let callNum = 0;
+ headers.forEach(
+ (value, key, container): void => {
+ assertEquals(headers, container);
+ assertEquals(value, headerEntriesDict[key]);
+ callNum++;
+ }
+ );
+ assertEquals(callNum, keys.length);
+});
+
+test(function headerSymbolIteratorSuccess(): void {
+ assert(Symbol.iterator in Headers.prototype);
+ const headers = new Headers(headerEntriesDict);
+ for (const header of headers) {
+ const key = header[0];
+ const value = header[1];
+ assert(headers.has(key));
+ assertEquals(value, headers.get(key));
+ }
+});
+
+test(function headerTypesAvailable(): void {
+ function newHeaders(): Headers {
+ return new Headers();
+ }
+ const headers = newHeaders();
+ assert(headers instanceof Headers);
+});
+
+// Modified from https://github.com/bitinn/node-fetch/blob/7d3293200a91ad52b5ca7962f9d6fd1c04983edb/test/test.js#L2001-L2014
+// Copyright (c) 2016 David Frank. MIT License.
+test(function headerIllegalReject(): void {
+ let errorCount = 0;
+ try {
+ new Headers({ "He y": "ok" });
+ } catch (e) {
+ errorCount++;
+ }
+ try {
+ new Headers({ "Hé-y": "ok" });
+ } catch (e) {
+ errorCount++;
+ }
+ try {
+ new Headers({ "He-y": "ăk" });
+ } catch (e) {
+ errorCount++;
+ }
+ const headers = new Headers();
+ try {
+ headers.append("Hé-y", "ok");
+ } catch (e) {
+ errorCount++;
+ }
+ try {
+ headers.delete("Hé-y");
+ } catch (e) {
+ errorCount++;
+ }
+ try {
+ headers.get("Hé-y");
+ } catch (e) {
+ errorCount++;
+ }
+ try {
+ headers.has("Hé-y");
+ } catch (e) {
+ errorCount++;
+ }
+ try {
+ headers.set("Hé-y", "ok");
+ } catch (e) {
+ errorCount++;
+ }
+ try {
+ headers.set("", "ok");
+ } catch (e) {
+ errorCount++;
+ }
+ assertEquals(errorCount, 9);
+ // 'o k' is valid value but invalid name
+ new Headers({ "He-y": "o k" });
+});
+
+// If pair does not contain exactly two items,then throw a TypeError.
+test(function headerParamsShouldThrowTypeError(): void {
+ let hasThrown = 0;
+
+ try {
+ new Headers(([["1"]] as unknown) as Array<[string, string]>);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+
+ assertEquals(hasThrown, 2);
+});
+
+test(function headerParamsArgumentsCheck(): void {
+ const methodRequireOneParam = ["delete", "get", "has", "forEach"];
+
+ const methodRequireTwoParams = ["append", "set"];
+
+ methodRequireOneParam.forEach(
+ (method): void => {
+ const headers = new Headers();
+ let hasThrown = 0;
+ let errMsg = "";
+ try {
+ headers[method]();
+ hasThrown = 1;
+ } catch (err) {
+ errMsg = err.message;
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ assertEquals(
+ errMsg,
+ `Headers.${method} requires at least 1 argument, but only 0 present`
+ );
+ }
+ );
+
+ methodRequireTwoParams.forEach(
+ (method): void => {
+ const headers = new Headers();
+ let hasThrown = 0;
+ let errMsg = "";
+
+ try {
+ headers[method]();
+ hasThrown = 1;
+ } catch (err) {
+ errMsg = err.message;
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ assertEquals(
+ errMsg,
+ `Headers.${method} requires at least 2 arguments, but only 0 present`
+ );
+
+ hasThrown = 0;
+ errMsg = "";
+ try {
+ headers[method]("foo");
+ hasThrown = 1;
+ } catch (err) {
+ errMsg = err.message;
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ assertEquals(
+ errMsg,
+ `Headers.${method} requires at least 2 arguments, but only 1 present`
+ );
+ }
+ );
+});
+
+test(function toStringShouldBeWebCompatibility(): void {
+ const headers = new Headers();
+ assertEquals(headers.toString(), "[object Headers]");
+});
diff --git a/cli/js/io.ts b/cli/js/io.ts
new file mode 100644
index 000000000..1a7bf8c4c
--- /dev/null
+++ b/cli/js/io.ts
@@ -0,0 +1,170 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+// Interfaces 100% copied from Go.
+// Documentation liberally lifted from them too.
+// Thank you! We love Go!
+
+// TODO(kt3k): EOF should be `unique symbol` type.
+// That might require some changes of ts_library_builder.
+// See #2591 for more details.
+export const EOF = null;
+export type EOF = null;
+
+// Seek whence values.
+// https://golang.org/pkg/io/#pkg-constants
+export enum SeekMode {
+ SEEK_START = 0,
+ SEEK_CURRENT = 1,
+ SEEK_END = 2
+}
+
+// Reader is the interface that wraps the basic read() method.
+// https://golang.org/pkg/io/#Reader
+export interface Reader {
+ /** Reads up to p.byteLength bytes into `p`. It resolves to the number
+ * of bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error encountered.
+ * Even if `read()` returns `n` < `p.byteLength`, it may use all of `p` as
+ * scratch space during the call. If some data is available but not
+ * `p.byteLength` bytes, `read()` conventionally returns what is available
+ * instead of waiting for more.
+ *
+ * When `read()` encounters end-of-file condition, it returns EOF symbol.
+ *
+ * When `read()` encounters an error, it rejects with an error.
+ *
+ * Callers should always process the `n` > `0` bytes returned before
+ * considering the EOF. Doing so correctly handles I/O errors that happen
+ * after reading some bytes and also both of the allowed EOF behaviors.
+ *
+ * Implementations must not retain `p`.
+ */
+ read(p: Uint8Array): Promise<number | EOF>;
+}
+
+export interface SyncReader {
+ readSync(p: Uint8Array): number | EOF;
+}
+
+// Writer is the interface that wraps the basic write() method.
+// https://golang.org/pkg/io/#Writer
+export interface Writer {
+ /** Writes `p.byteLength` bytes from `p` to the underlying data
+ * stream. It resolves to the number of bytes written from `p` (`0` <= `n` <=
+ * `p.byteLength`) and any error encountered that caused the write to stop
+ * early. `write()` must return a non-null error if it returns `n` <
+ * `p.byteLength`. write() must not modify the slice data, even temporarily.
+ *
+ * Implementations must not retain `p`.
+ */
+ write(p: Uint8Array): Promise<number>;
+}
+
+export interface SyncWriter {
+ writeSync(p: Uint8Array): number;
+}
+// https://golang.org/pkg/io/#Closer
+export interface Closer {
+ // The behavior of Close after the first call is undefined. Specific
+ // implementations may document their own behavior.
+ close(): void;
+}
+
+// https://golang.org/pkg/io/#Seeker
+export interface Seeker {
+ /** Seek sets the offset for the next `read()` or `write()` to offset,
+ * interpreted according to `whence`: `SeekStart` means relative to the start
+ * of the file, `SeekCurrent` means relative to the current offset, and
+ * `SeekEnd` means relative to the end. Seek returns the new offset relative
+ * to the start of the file and an error, if any.
+ *
+ * Seeking to an offset before the start of the file is an error. Seeking to
+ * any positive offset is legal, but the behavior of subsequent I/O operations
+ * on the underlying object is implementation-dependent.
+ */
+ seek(offset: number, whence: SeekMode): Promise<void>;
+}
+
+export interface SyncSeeker {
+ seekSync(offset: number, whence: SeekMode): void;
+}
+
+// https://golang.org/pkg/io/#ReadCloser
+export interface ReadCloser extends Reader, Closer {}
+
+// https://golang.org/pkg/io/#WriteCloser
+export interface WriteCloser extends Writer, Closer {}
+
+// https://golang.org/pkg/io/#ReadSeeker
+export interface ReadSeeker extends Reader, Seeker {}
+
+// https://golang.org/pkg/io/#WriteSeeker
+export interface WriteSeeker extends Writer, Seeker {}
+
+// https://golang.org/pkg/io/#ReadWriteCloser
+export interface ReadWriteCloser extends Reader, Writer, Closer {}
+
+// https://golang.org/pkg/io/#ReadWriteSeeker
+export interface ReadWriteSeeker extends Reader, Writer, Seeker {}
+
+/** Copies from `src` to `dst` until either `EOF` is reached on `src`
+ * or an error occurs. It returns the number of bytes copied and the first
+ * error encountered while copying, if any.
+ *
+ * Because `copy()` is defined to read from `src` until `EOF`, it does not
+ * treat an `EOF` from `read()` as an error to be reported.
+ */
+// https://golang.org/pkg/io/#Copy
+export async function copy(dst: Writer, src: Reader): Promise<number> {
+ let n = 0;
+ const b = new Uint8Array(32 * 1024);
+ let gotEOF = false;
+ while (gotEOF === false) {
+ const result = await src.read(b);
+ if (result === EOF) {
+ gotEOF = true;
+ } else {
+ n += await dst.write(b.subarray(0, result));
+ }
+ }
+ return n;
+}
+
+/** Turns `r` into async iterator.
+ *
+ * for await (const chunk of toAsyncIterator(reader)) {
+ * console.log(chunk)
+ * }
+ */
+export function toAsyncIterator(r: Reader): AsyncIterableIterator<Uint8Array> {
+ const b = new Uint8Array(1024);
+ // Keep track if end-of-file has been reached, then
+ // signal that iterator is done during subsequent next()
+ // call. This is required because `r` can return a `number | EOF`
+ // with data read and EOF reached. But if iterator returns
+ // `done` then `value` is discarded.
+ //
+ // See https://github.com/denoland/deno/issues/2330 for reference.
+ let sawEof = false;
+
+ return {
+ [Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array> {
+ return this;
+ },
+
+ async next(): Promise<IteratorResult<Uint8Array>> {
+ if (sawEof) {
+ return { value: new Uint8Array(), done: true };
+ }
+
+ const result = await r.read(b);
+ if (result === EOF) {
+ sawEof = true;
+ return { value: new Uint8Array(), done: true };
+ }
+
+ return {
+ value: b.subarray(0, result),
+ done: false
+ };
+ }
+ };
+}
diff --git a/cli/js/lib.deno_runtime.d.ts b/cli/js/lib.deno_runtime.d.ts
new file mode 100644
index 000000000..94b6b61cd
--- /dev/null
+++ b/cli/js/lib.deno_runtime.d.ts
@@ -0,0 +1,2800 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/no-empty-interface */
+
+/// <reference no-default-lib="true" />
+/// <reference lib="esnext" />
+
+declare namespace Deno {
+ // @url js/os.d.ts
+
+ /** The current process id of the runtime. */
+ export let pid: number;
+ /** Reflects the NO_COLOR environment variable: https://no-color.org/ */
+ export let noColor: boolean;
+ /** Check if running in terminal.
+ *
+ * console.log(Deno.isTTY().stdout);
+ */
+ export function isTTY(): {
+ stdin: boolean;
+ stdout: boolean;
+ stderr: boolean;
+ };
+ /** Get the hostname.
+ * Requires the `--allow-env` flag.
+ *
+ * console.log(Deno.hostname());
+ */
+ export function hostname(): string;
+ /** Exit the Deno process with optional exit code. */
+ export function exit(code?: number): never;
+ /** Returns a snapshot of the environment variables at invocation. Mutating a
+ * property in the object will set that variable in the environment for
+ * the process. The environment object will only accept `string`s
+ * as values.
+ *
+ * const myEnv = Deno.env();
+ * console.log(myEnv.SHELL);
+ * myEnv.TEST_VAR = "HELLO";
+ * const newEnv = Deno.env();
+ * console.log(myEnv.TEST_VAR == newEnv.TEST_VAR);
+ */
+ export function env(): {
+ [index: string]: string;
+ };
+ /** Returns the value of an environment variable at invocation.
+ * If the variable is not present, `undefined` will be returned.
+ *
+ * const myEnv = Deno.env();
+ * console.log(myEnv.SHELL);
+ * myEnv.TEST_VAR = "HELLO";
+ * const newEnv = Deno.env();
+ * console.log(myEnv.TEST_VAR == newEnv.TEST_VAR);
+ */
+ export function env(key: string): string | undefined;
+ /**
+ * Returns the current user's home directory.
+ * Requires the `--allow-env` flag.
+ */
+ export function homeDir(): string;
+ /**
+ * Returns the path to the current deno executable.
+ * Requires the `--allow-env` flag.
+ */
+ export function execPath(): string;
+
+ // @url js/dir.d.ts
+
+ /**
+ * `cwd()` Return a string representing the current working directory.
+ * If the current directory can be reached via multiple paths
+ * (due to symbolic links), `cwd()` may return
+ * any one of them.
+ * throws `NotFound` exception if directory not available
+ */
+ export function cwd(): string;
+ /**
+ * `chdir()` Change the current working directory to path.
+ * throws `NotFound` exception if directory not available
+ */
+ export function chdir(directory: string): void;
+
+ // @url js/io.d.ts
+
+ export const EOF: null;
+ export type EOF = null;
+ export enum SeekMode {
+ SEEK_START = 0,
+ SEEK_CURRENT = 1,
+ SEEK_END = 2
+ }
+ export interface Reader {
+ /** Reads up to p.byteLength bytes into `p`. It resolves to the number
+ * of bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error encountered.
+ * Even if `read()` returns `n` < `p.byteLength`, it may use all of `p` as
+ * scratch space during the call. If some data is available but not
+ * `p.byteLength` bytes, `read()` conventionally returns what is available
+ * instead of waiting for more.
+ *
+ * When `read()` encounters end-of-file condition, it returns EOF symbol.
+ *
+ * When `read()` encounters an error, it rejects with an error.
+ *
+ * Callers should always process the `n` > `0` bytes returned before
+ * considering the EOF. Doing so correctly handles I/O errors that happen
+ * after reading some bytes and also both of the allowed EOF behaviors.
+ *
+ * Implementations must not retain `p`.
+ */
+ read(p: Uint8Array): Promise<number | EOF>;
+ }
+ export interface SyncReader {
+ readSync(p: Uint8Array): number | EOF;
+ }
+ export interface Writer {
+ /** Writes `p.byteLength` bytes from `p` to the underlying data
+ * stream. It resolves to the number of bytes written from `p` (`0` <= `n` <=
+ * `p.byteLength`) and any error encountered that caused the write to stop
+ * early. `write()` must return a non-null error if it returns `n` <
+ * `p.byteLength`. write() must not modify the slice data, even temporarily.
+ *
+ * Implementations must not retain `p`.
+ */
+ write(p: Uint8Array): Promise<number>;
+ }
+ export interface SyncWriter {
+ writeSync(p: Uint8Array): number;
+ }
+ export interface Closer {
+ close(): void;
+ }
+ export interface Seeker {
+ /** Seek sets the offset for the next `read()` or `write()` to offset,
+ * interpreted according to `whence`: `SeekStart` means relative to the start
+ * of the file, `SeekCurrent` means relative to the current offset, and
+ * `SeekEnd` means relative to the end. Seek returns the new offset relative
+ * to the start of the file and an error, if any.
+ *
+ * Seeking to an offset before the start of the file is an error. Seeking to
+ * any positive offset is legal, but the behavior of subsequent I/O operations
+ * on the underlying object is implementation-dependent.
+ */
+ seek(offset: number, whence: SeekMode): Promise<void>;
+ }
+ export interface SyncSeeker {
+ seekSync(offset: number, whence: SeekMode): void;
+ }
+ export interface ReadCloser extends Reader, Closer {}
+ export interface WriteCloser extends Writer, Closer {}
+ export interface ReadSeeker extends Reader, Seeker {}
+ export interface WriteSeeker extends Writer, Seeker {}
+ export interface ReadWriteCloser extends Reader, Writer, Closer {}
+ export interface ReadWriteSeeker extends Reader, Writer, Seeker {}
+ /** Copies from `src` to `dst` until either `EOF` is reached on `src`
+ * or an error occurs. It returns the number of bytes copied and the first
+ * error encountered while copying, if any.
+ *
+ * Because `copy()` is defined to read from `src` until `EOF`, it does not
+ * treat an `EOF` from `read()` as an error to be reported.
+ */
+ export function copy(dst: Writer, src: Reader): Promise<number>;
+ /** Turns `r` into async iterator.
+ *
+ * for await (const chunk of toAsyncIterator(reader)) {
+ * console.log(chunk)
+ * }
+ */
+ export function toAsyncIterator(r: Reader): AsyncIterableIterator<Uint8Array>;
+
+ // @url js/files.d.ts
+
+ /** Open a file and return an instance of the `File` object
+ * synchronously.
+ *
+ * const file = Deno.openSync("/foo/bar.txt");
+ */
+ export function openSync(filename: string, mode?: OpenMode): File;
+ /** Open a file and return an instance of the `File` object.
+ *
+ * (async () => {
+ * const file = await Deno.open("/foo/bar.txt");
+ * })();
+ */
+ export function open(filename: string, mode?: OpenMode): Promise<File>;
+ /** Read synchronously from a file ID into an array buffer.
+ *
+ * Return `number | EOF` for the operation.
+ *
+ * const file = Deno.openSync("/foo/bar.txt");
+ * const buf = new Uint8Array(100);
+ * const nread = Deno.readSync(file.rid, buf);
+ * const text = new TextDecoder().decode(buf);
+ *
+ */
+ export function readSync(rid: number, p: Uint8Array): number | EOF;
+ /** Read from a file ID into an array buffer.
+ *
+ * Resolves with the `number | EOF` for the operation.
+ *
+ * (async () => {
+ * const file = await Deno.open("/foo/bar.txt");
+ * const buf = new Uint8Array(100);
+ * const nread = await Deno.read(file.rid, buf);
+ * const text = new TextDecoder().decode(buf);
+ * })();
+ */
+ export function read(rid: number, p: Uint8Array): Promise<number | EOF>;
+ /** Write synchronously to the file ID the contents of the array buffer.
+ *
+ * Resolves with the number of bytes written.
+ *
+ * const encoder = new TextEncoder();
+ * const data = encoder.encode("Hello world\n");
+ * const file = Deno.openSync("/foo/bar.txt");
+ * Deno.writeSync(file.rid, data);
+ */
+ export function writeSync(rid: number, p: Uint8Array): number;
+ /** Write to the file ID the contents of the array buffer.
+ *
+ * Resolves with the number of bytes written.
+ *
+ * (async () => {
+ * const encoder = new TextEncoder();
+ * const data = encoder.encode("Hello world\n");
+ * const file = await Deno.open("/foo/bar.txt");
+ * await Deno.write(file.rid, data);
+ * })();
+ *
+ */
+ export function write(rid: number, p: Uint8Array): Promise<number>;
+ /** Seek a file ID synchronously to the given offset under mode given by `whence`.
+ *
+ * const file = Deno.openSync("/foo/bar.txt");
+ * Deno.seekSync(file.rid, 0, 0);
+ */
+ export function seekSync(rid: number, offset: number, whence: SeekMode): void;
+ /** Seek a file ID to the given offset under mode given by `whence`.
+ *
+ * (async () => {
+ * const file = await Deno.open("/foo/bar.txt");
+ * await Deno.seek(file.rid, 0, 0);
+ * })();
+ */
+ export function seek(
+ rid: number,
+ offset: number,
+ whence: SeekMode
+ ): Promise<void>;
+ /** Close the file ID. */
+ export function close(rid: number): void;
+ /** The Deno abstraction for reading and writing files. */
+ export class File
+ implements
+ Reader,
+ SyncReader,
+ Writer,
+ SyncWriter,
+ Seeker,
+ SyncSeeker,
+ Closer {
+ readonly rid: number;
+ constructor(rid: number);
+ write(p: Uint8Array): Promise<number>;
+ writeSync(p: Uint8Array): number;
+ read(p: Uint8Array): Promise<number | EOF>;
+ readSync(p: Uint8Array): number | EOF;
+ seek(offset: number, whence: SeekMode): Promise<void>;
+ seekSync(offset: number, whence: SeekMode): void;
+ close(): void;
+ }
+ /** An instance of `File` for stdin. */
+ export const stdin: File;
+ /** An instance of `File` for stdout. */
+ export const stdout: File;
+ /** An instance of `File` for stderr. */
+ export const stderr: File;
+ export type OpenMode =
+ | "r"
+ /** Read-write. Start at beginning of file. */
+ | "r+"
+ /** Write-only. Opens and truncates existing file or creates new one for
+ * writing only.
+ */
+ | "w"
+ /** Read-write. Opens and truncates existing file or creates new one for
+ * writing and reading.
+ */
+ | "w+"
+ /** Write-only. Opens existing file or creates new one. Each write appends
+ * content to the end of file.
+ */
+ | "a"
+ /** Read-write. Behaves like "a" and allows to read from file. */
+ | "a+"
+ /** Write-only. Exclusive create - creates new file only if one doesn't exist
+ * already.
+ */
+ | "x"
+ /** Read-write. Behaves like `x` and allows to read from file. */
+ | "x+";
+
+ // @url js/buffer.d.ts
+
+ /** A Buffer is a variable-sized buffer of bytes with read() and write()
+ * methods. Based on https://golang.org/pkg/bytes/#Buffer
+ */
+ export class Buffer implements Reader, SyncReader, Writer, SyncWriter {
+ private buf;
+ private off;
+ constructor(ab?: ArrayBuffer);
+ /** bytes() returns a slice holding the unread portion of the buffer.
+ * The slice is valid for use only until the next buffer modification (that
+ * is, only until the next call to a method like read(), write(), reset(), or
+ * truncate()). The slice aliases the buffer content at least until the next
+ * buffer modification, so immediate changes to the slice will affect the
+ * result of future reads.
+ */
+ bytes(): Uint8Array;
+ /** toString() returns the contents of the unread portion of the buffer
+ * as a string. Warning - if multibyte characters are present when data is
+ * flowing through the buffer, this method may result in incorrect strings
+ * due to a character being split.
+ */
+ toString(): string;
+ /** empty() returns whether the unread portion of the buffer is empty. */
+ empty(): boolean;
+ /** length is a getter that returns the number of bytes of the unread
+ * portion of the buffer
+ */
+ readonly length: number;
+ /** Returns the capacity of the buffer's underlying byte slice, that is,
+ * the total space allocated for the buffer's data.
+ */
+ readonly capacity: number;
+ /** truncate() discards all but the first n unread bytes from the buffer but
+ * continues to use the same allocated storage. It throws if n is negative or
+ * greater than the length of the buffer.
+ */
+ truncate(n: number): void;
+ /** reset() resets the buffer to be empty, but it retains the underlying
+ * storage for use by future writes. reset() is the same as truncate(0)
+ */
+ reset(): void;
+ /** _tryGrowByReslice() is a version of grow for the fast-case
+ * where the internal buffer only needs to be resliced. It returns the index
+ * where bytes should be written and whether it succeeded.
+ * It returns -1 if a reslice was not needed.
+ */
+ private _tryGrowByReslice;
+ private _reslice;
+ /** readSync() reads the next len(p) bytes from the buffer or until the buffer
+ * is drained. The return value n is the number of bytes read. If the
+ * buffer has no data to return, eof in the response will be true.
+ */
+ readSync(p: Uint8Array): number | EOF;
+ read(p: Uint8Array): Promise<number | EOF>;
+ writeSync(p: Uint8Array): number;
+ write(p: Uint8Array): Promise<number>;
+ /** _grow() grows the buffer to guarantee space for n more bytes.
+ * It returns the index where bytes should be written.
+ * If the buffer can't grow it will throw with ErrTooLarge.
+ */
+ private _grow;
+ /** grow() grows the buffer's capacity, if necessary, to guarantee space for
+ * another n bytes. After grow(n), at least n bytes can be written to the
+ * buffer without another allocation. If n is negative, grow() will panic. If
+ * the buffer can't grow it will throw ErrTooLarge.
+ * Based on https://golang.org/pkg/bytes/#Buffer.Grow
+ */
+ grow(n: number): void;
+ /** readFrom() reads data from r until EOF and appends it to the buffer,
+ * growing the buffer as needed. It returns the number of bytes read. If the
+ * buffer becomes too large, readFrom will panic with ErrTooLarge.
+ * Based on https://golang.org/pkg/bytes/#Buffer.ReadFrom
+ */
+ readFrom(r: Reader): Promise<number>;
+ /** Sync version of `readFrom`
+ */
+ readFromSync(r: SyncReader): number;
+ }
+ /** Read `r` until EOF and return the content as `Uint8Array`.
+ */
+ export function readAll(r: Reader): Promise<Uint8Array>;
+ /** Read synchronously `r` until EOF and return the content as `Uint8Array`.
+ */
+ export function readAllSync(r: SyncReader): Uint8Array;
+ /** Write all the content of `arr` to `w`.
+ */
+ export function writeAll(w: Writer, arr: Uint8Array): Promise<void>;
+ /** Write synchronously all the content of `arr` to `w`.
+ */
+ export function writeAllSync(w: SyncWriter, arr: Uint8Array): void;
+
+ // @url js/mkdir.d.ts
+
+ /** Creates a new directory with the specified path synchronously.
+ * If `recursive` is set to true, nested directories will be created (also known
+ * as "mkdir -p").
+ * `mode` sets permission bits (before umask) on UNIX and does nothing on
+ * Windows.
+ *
+ * Deno.mkdirSync("new_dir");
+ * Deno.mkdirSync("nested/directories", true);
+ */
+ export function mkdirSync(
+ path: string,
+ recursive?: boolean,
+ mode?: number
+ ): void;
+ /** Creates a new directory with the specified path.
+ * If `recursive` is set to true, nested directories will be created (also known
+ * as "mkdir -p").
+ * `mode` sets permission bits (before umask) on UNIX and does nothing on
+ * Windows.
+ *
+ * await Deno.mkdir("new_dir");
+ * await Deno.mkdir("nested/directories", true);
+ */
+ export function mkdir(
+ path: string,
+ recursive?: boolean,
+ mode?: number
+ ): Promise<void>;
+
+ // @url js/make_temp_dir.d.ts
+
+ export interface MakeTempDirOptions {
+ dir?: string;
+ prefix?: string;
+ suffix?: string;
+ }
+ /** makeTempDirSync is the synchronous version of `makeTempDir`.
+ *
+ * const tempDirName0 = Deno.makeTempDirSync();
+ * const tempDirName1 = Deno.makeTempDirSync({ prefix: 'my_temp' });
+ */
+ export function makeTempDirSync(options?: MakeTempDirOptions): string;
+ /** makeTempDir creates a new temporary directory in the directory `dir`, its
+ * name beginning with `prefix` and ending with `suffix`.
+ * It returns the full path to the newly created directory.
+ * If `dir` is unspecified, tempDir uses the default directory for temporary
+ * files. Multiple programs calling tempDir simultaneously will not choose the
+ * same directory. It is the caller's responsibility to remove the directory
+ * when no longer needed.
+ *
+ * const tempDirName0 = await Deno.makeTempDir();
+ * const tempDirName1 = await Deno.makeTempDir({ prefix: 'my_temp' });
+ */
+ export function makeTempDir(options?: MakeTempDirOptions): Promise<string>;
+
+ // @url js/chmod.d.ts
+
+ /** Changes the permission of a specific file/directory of specified path
+ * synchronously.
+ *
+ * Deno.chmodSync("/path/to/file", 0o666);
+ */
+ export function chmodSync(path: string, mode: number): void;
+ /** Changes the permission of a specific file/directory of specified path.
+ *
+ * await Deno.chmod("/path/to/file", 0o666);
+ */
+ export function chmod(path: string, mode: number): Promise<void>;
+
+ // @url js/chown.d.ts
+
+ /**
+ * Change owner of a regular file or directory synchronously. Unix only at the moment.
+ * @param path path to the file
+ * @param uid user id of the new owner
+ * @param gid group id of the new owner
+ */
+ export function chownSync(path: string, uid: number, gid: number): void;
+ /**
+ * Change owner of a regular file or directory asynchronously. Unix only at the moment.
+ * @param path path to the file
+ * @param uid user id of the new owner
+ * @param gid group id of the new owner
+ */
+ export function chown(path: string, uid: number, gid: number): Promise<void>;
+
+ // @url js/utime.d.ts
+
+ /** Synchronously changes the access and modification times of a file system
+ * object referenced by `filename`. Given times are either in seconds
+ * (Unix epoch time) or as `Date` objects.
+ *
+ * Deno.utimeSync("myfile.txt", 1556495550, new Date());
+ */
+ export function utimeSync(
+ filename: string,
+ atime: number | Date,
+ mtime: number | Date
+ ): void;
+ /** Changes the access and modification times of a file system object
+ * referenced by `filename`. Given times are either in seconds
+ * (Unix epoch time) or as `Date` objects.
+ *
+ * await Deno.utime("myfile.txt", 1556495550, new Date());
+ */
+ export function utime(
+ filename: string,
+ atime: number | Date,
+ mtime: number | Date
+ ): Promise<void>;
+
+ // @url js/remove.d.ts
+
+ export interface RemoveOption {
+ recursive?: boolean;
+ }
+ /** Removes the named file or directory synchronously. Would throw
+ * error if permission denied, not found, or directory not empty if `recursive`
+ * set to false.
+ * `recursive` is set to false by default.
+ *
+ * Deno.removeSync("/path/to/dir/or/file", {recursive: false});
+ */
+ export function removeSync(path: string, options?: RemoveOption): void;
+ /** Removes the named file or directory. Would throw error if
+ * permission denied, not found, or directory not empty if `recursive` set
+ * to false.
+ * `recursive` is set to false by default.
+ *
+ * await Deno.remove("/path/to/dir/or/file", {recursive: false});
+ */
+ export function remove(path: string, options?: RemoveOption): Promise<void>;
+
+ // @url js/rename.d.ts
+
+ /** Synchronously renames (moves) `oldpath` to `newpath`. If `newpath` already
+ * exists and is not a directory, `renameSync()` replaces it. OS-specific
+ * restrictions may apply when `oldpath` and `newpath` are in different
+ * directories.
+ *
+ * Deno.renameSync("old/path", "new/path");
+ */
+ export function renameSync(oldpath: string, newpath: string): void;
+ /** Renames (moves) `oldpath` to `newpath`. If `newpath` already exists and is
+ * not a directory, `rename()` replaces it. OS-specific restrictions may apply
+ * when `oldpath` and `newpath` are in different directories.
+ *
+ * await Deno.rename("old/path", "new/path");
+ */
+ export function rename(oldpath: string, newpath: string): Promise<void>;
+
+ // @url js/read_file.d.ts
+
+ /** Read the entire contents of a file synchronously.
+ *
+ * const decoder = new TextDecoder("utf-8");
+ * const data = Deno.readFileSync("hello.txt");
+ * console.log(decoder.decode(data));
+ */
+ export function readFileSync(filename: string): Uint8Array;
+ /** Read the entire contents of a file.
+ *
+ * const decoder = new TextDecoder("utf-8");
+ * const data = await Deno.readFile("hello.txt");
+ * console.log(decoder.decode(data));
+ */
+ export function readFile(filename: string): Promise<Uint8Array>;
+
+ // @url js/file_info.d.ts
+
+ /** A FileInfo describes a file and is returned by `stat`, `lstat`,
+ * `statSync`, `lstatSync`.
+ */
+ export interface FileInfo {
+ /** The size of the file, in bytes. */
+ len: number;
+ /** The last modification time of the file. This corresponds to the `mtime`
+ * field from `stat` on Unix and `ftLastWriteTime` on Windows. This may not
+ * be available on all platforms.
+ */
+ modified: number | null;
+ /** The last access time of the file. This corresponds to the `atime`
+ * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not
+ * be available on all platforms.
+ */
+ accessed: number | null;
+ /** The last access time of the file. This corresponds to the `birthtime`
+ * field from `stat` on Unix and `ftCreationTime` on Windows. This may not
+ * be available on all platforms.
+ */
+ created: number | null;
+ /** The underlying raw st_mode bits that contain the standard Unix permissions
+ * for this file/directory. TODO Match behavior with Go on windows for mode.
+ */
+ mode: number | null;
+ /** The file or directory name. */
+ name: string | null;
+ /** Returns whether this is info for a regular file. This result is mutually
+ * exclusive to `FileInfo.isDirectory` and `FileInfo.isSymlink`.
+ */
+ isFile(): boolean;
+ /** Returns whether this is info for a regular directory. This result is
+ * mutually exclusive to `FileInfo.isFile` and `FileInfo.isSymlink`.
+ */
+ isDirectory(): boolean;
+ /** Returns whether this is info for a symlink. This result is
+ * mutually exclusive to `FileInfo.isFile` and `FileInfo.isDirectory`.
+ */
+ isSymlink(): boolean;
+ }
+
+ // @url js/read_dir.d.ts
+
+ /** Reads the directory given by path and returns a list of file info
+ * synchronously.
+ *
+ * const files = Deno.readDirSync("/");
+ */
+ export function readDirSync(path: string): FileInfo[];
+ /** Reads the directory given by path and returns a list of file info.
+ *
+ * const files = await Deno.readDir("/");
+ */
+ export function readDir(path: string): Promise<FileInfo[]>;
+
+ // @url js/copy_file.d.ts
+
+ /** Copies the contents of a file to another by name synchronously.
+ * Creates a new file if target does not exists, and if target exists,
+ * overwrites original content of the target file.
+ *
+ * It would also copy the permission of the original file
+ * to the destination.
+ *
+ * Deno.copyFileSync("from.txt", "to.txt");
+ */
+ export function copyFileSync(from: string, to: string): void;
+ /** Copies the contents of a file to another by name.
+ *
+ * Creates a new file if target does not exists, and if target exists,
+ * overwrites original content of the target file.
+ *
+ * It would also copy the permission of the original file
+ * to the destination.
+ *
+ * await Deno.copyFile("from.txt", "to.txt");
+ */
+ export function copyFile(from: string, to: string): Promise<void>;
+
+ // @url js/read_link.d.ts
+
+ /** Returns the destination of the named symbolic link synchronously.
+ *
+ * const targetPath = Deno.readlinkSync("symlink/path");
+ */
+ export function readlinkSync(name: string): string;
+ /** Returns the destination of the named symbolic link.
+ *
+ * const targetPath = await Deno.readlink("symlink/path");
+ */
+ export function readlink(name: string): Promise<string>;
+
+ // @url js/stat.d.ts
+
+ interface StatResponse {
+ isFile: boolean;
+ isSymlink: boolean;
+ len: number;
+ modified: number;
+ accessed: number;
+ created: number;
+ mode: number;
+ hasMode: boolean;
+ name: string | null;
+ }
+ /** Queries the file system for information on the path provided. If the given
+ * path is a symlink information about the symlink will be returned.
+ *
+ * const fileInfo = await Deno.lstat("hello.txt");
+ * assert(fileInfo.isFile());
+ */
+ export function lstat(filename: string): Promise<FileInfo>;
+ /** Queries the file system for information on the path provided synchronously.
+ * If the given path is a symlink information about the symlink will be
+ * returned.
+ *
+ * const fileInfo = Deno.lstatSync("hello.txt");
+ * assert(fileInfo.isFile());
+ */
+ export function lstatSync(filename: string): FileInfo;
+ /** Queries the file system for information on the path provided. `stat` Will
+ * always follow symlinks.
+ *
+ * const fileInfo = await Deno.stat("hello.txt");
+ * assert(fileInfo.isFile());
+ */
+ export function stat(filename: string): Promise<FileInfo>;
+ /** Queries the file system for information on the path provided synchronously.
+ * `statSync` Will always follow symlinks.
+ *
+ * const fileInfo = Deno.statSync("hello.txt");
+ * assert(fileInfo.isFile());
+ */
+ export function statSync(filename: string): FileInfo;
+
+ // @url js/link.d.ts
+
+ /** Synchronously creates `newname` as a hard link to `oldname`.
+ *
+ * Deno.linkSync("old/name", "new/name");
+ */
+ export function linkSync(oldname: string, newname: string): void;
+ /** Creates `newname` as a hard link to `oldname`.
+ *
+ * await Deno.link("old/name", "new/name");
+ */
+ export function link(oldname: string, newname: string): Promise<void>;
+
+ // @url js/symlink.d.ts
+
+ /** Synchronously creates `newname` as a symbolic link to `oldname`. The type
+ * argument can be set to `dir` or `file` and is only available on Windows
+ * (ignored on other platforms).
+ *
+ * Deno.symlinkSync("old/name", "new/name");
+ */
+ export function symlinkSync(
+ oldname: string,
+ newname: string,
+ type?: string
+ ): void;
+ /** Creates `newname` as a symbolic link to `oldname`. The type argument can be
+ * set to `dir` or `file` and is only available on Windows (ignored on other
+ * platforms).
+ *
+ * await Deno.symlink("old/name", "new/name");
+ */
+ export function symlink(
+ oldname: string,
+ newname: string,
+ type?: string
+ ): Promise<void>;
+
+ // @url js/write_file.d.ts
+
+ /** Options for writing to a file.
+ * `perm` would change the file's permission if set.
+ * `create` decides if the file should be created if not exists (default: true)
+ * `append` decides if the file should be appended (default: false)
+ */
+ export interface WriteFileOptions {
+ perm?: number;
+ create?: boolean;
+ append?: boolean;
+ }
+ /** Write a new file, with given filename and data synchronously.
+ *
+ * const encoder = new TextEncoder();
+ * const data = encoder.encode("Hello world\n");
+ * Deno.writeFileSync("hello.txt", data);
+ */
+ export function writeFileSync(
+ filename: string,
+ data: Uint8Array,
+ options?: WriteFileOptions
+ ): void;
+ /** Write a new file, with given filename and data.
+ *
+ * const encoder = new TextEncoder();
+ * const data = encoder.encode("Hello world\n");
+ * await Deno.writeFile("hello.txt", data);
+ */
+ export function writeFile(
+ filename: string,
+ data: Uint8Array,
+ options?: WriteFileOptions
+ ): Promise<void>;
+
+ // @url js/error_stack.d.ts
+
+ interface Location {
+ /** The full url for the module, e.g. `file://some/file.ts` or
+ * `https://some/file.ts`. */
+ filename: string;
+ /** The line number in the file. It is assumed to be 1-indexed. */
+ line: number;
+ /** The column number in the file. It is assumed to be 1-indexed. */
+ column: number;
+ }
+ /** Given a current location in a module, lookup the source location and
+ * return it.
+ *
+ * When Deno transpiles code, it keep source maps of the transpiled code. This
+ * function can be used to lookup the original location. This is automatically
+ * done when accessing the `.stack` of an error, or when an uncaught error is
+ * logged. This function can be used to perform the lookup for creating better
+ * error handling.
+ *
+ * **Note:** `line` and `column` are 1 indexed, which matches display
+ * expectations, but is not typical of most index numbers in Deno.
+ *
+ * An example:
+ *
+ * const orig = Deno.applySourceMap({
+ * location: "file://my/module.ts",
+ * line: 5,
+ * column: 15
+ * });
+ * console.log(`${orig.filename}:${orig.line}:${orig.column}`);
+ *
+ */
+ export function applySourceMap(location: Location): Location;
+
+ // @url js/errors.d.ts
+
+ /** A Deno specific error. The `kind` property is set to a specific error code
+ * which can be used to in application logic.
+ *
+ * try {
+ * somethingThatMightThrow();
+ * } catch (e) {
+ * if (
+ * e instanceof Deno.DenoError &&
+ * e.kind === Deno.ErrorKind.Overflow
+ * ) {
+ * console.error("Overflow error!");
+ * }
+ * }
+ *
+ */
+ export class DenoError<T extends ErrorKind> extends Error {
+ readonly kind: T;
+ constructor(kind: T, msg: string);
+ }
+ export enum ErrorKind {
+ NoError = 0,
+ NotFound = 1,
+ PermissionDenied = 2,
+ ConnectionRefused = 3,
+ ConnectionReset = 4,
+ ConnectionAborted = 5,
+ NotConnected = 6,
+ AddrInUse = 7,
+ AddrNotAvailable = 8,
+ BrokenPipe = 9,
+ AlreadyExists = 10,
+ WouldBlock = 11,
+ InvalidInput = 12,
+ InvalidData = 13,
+ TimedOut = 14,
+ Interrupted = 15,
+ WriteZero = 16,
+ Other = 17,
+ UnexpectedEof = 18,
+ BadResource = 19,
+ CommandFailed = 20,
+ EmptyHost = 21,
+ IdnaError = 22,
+ InvalidPort = 23,
+ InvalidIpv4Address = 24,
+ InvalidIpv6Address = 25,
+ InvalidDomainCharacter = 26,
+ RelativeUrlWithoutBase = 27,
+ RelativeUrlWithCannotBeABaseBase = 28,
+ SetHostOnCannotBeABaseUrl = 29,
+ Overflow = 30,
+ HttpUser = 31,
+ HttpClosed = 32,
+ HttpCanceled = 33,
+ HttpParse = 34,
+ HttpOther = 35,
+ TooLarge = 36,
+ InvalidUri = 37,
+ InvalidSeekMode = 38,
+ OpNotAvailable = 39,
+ WorkerInitFailed = 40,
+ UnixError = 41,
+ NoAsyncSupport = 42,
+ NoSyncSupport = 43,
+ ImportMapError = 44,
+ InvalidPath = 45,
+ ImportPrefixMissing = 46,
+ UnsupportedFetchScheme = 47,
+ TooManyRedirects = 48,
+ Diagnostic = 49,
+ JSError = 50
+ }
+
+ // @url js/permissions.d.ts
+
+ /** Permissions as granted by the caller */
+ export interface Permissions {
+ read: boolean;
+ write: boolean;
+ net: boolean;
+ env: boolean;
+ run: boolean;
+ hrtime: boolean;
+ }
+ export type Permission = keyof Permissions;
+ /** Inspect granted permissions for the current program.
+ *
+ * if (Deno.permissions().read) {
+ * const file = await Deno.readFile("example.test");
+ * // ...
+ * }
+ */
+ export function permissions(): Permissions;
+ /** Revoke a permission. When the permission was already revoked nothing changes
+ *
+ * if (Deno.permissions().read) {
+ * const file = await Deno.readFile("example.test");
+ * Deno.revokePermission('read');
+ * }
+ * Deno.readFile("example.test"); // -> error or permission prompt
+ */
+ export function revokePermission(permission: Permission): void;
+
+ // @url js/truncate.d.ts
+
+ /** Truncates or extends the specified file synchronously, updating the size of
+ * this file to become size.
+ *
+ * Deno.truncateSync("hello.txt", 10);
+ */
+ export function truncateSync(name: string, len?: number): void;
+ /**
+ * Truncates or extends the specified file, updating the size of this file to
+ * become size.
+ *
+ * await Deno.truncate("hello.txt", 10);
+ */
+ export function truncate(name: string, len?: number): Promise<void>;
+
+ // @url js/net.d.ts
+
+ type Transport = "tcp";
+ interface Addr {
+ transport: Transport;
+ address: string;
+ }
+
+ /** A Listener is a generic network listener for stream-oriented protocols. */
+ export interface Listener extends AsyncIterator<Conn> {
+ /** Waits for and resolves to the next connection to the `Listener`. */
+ accept(): Promise<Conn>;
+ /** Close closes the listener. Any pending accept promises will be rejected
+ * with errors.
+ */
+ close(): void;
+ /** Return the address of the `Listener`. */
+ addr(): Addr;
+ [Symbol.asyncIterator](): AsyncIterator<Conn>;
+ }
+ export interface Conn extends Reader, Writer, Closer {
+ /** The local address of the connection. */
+ localAddr: string;
+ /** The remote address of the connection. */
+ remoteAddr: string;
+ /** The resource ID of the connection. */
+ rid: number;
+ /** Shuts down (`shutdown(2)`) the reading side of the TCP connection. Most
+ * callers should just use `close()`.
+ */
+ closeRead(): void;
+ /** Shuts down (`shutdown(2)`) the writing side of the TCP connection. Most
+ * callers should just use `close()`.
+ */
+ closeWrite(): void;
+ }
+
+ export interface ListenOptions {
+ port: number;
+ hostname?: string;
+ transport?: Transport;
+ }
+
+ /** Listen announces on the local transport address.
+ *
+ * @param options
+ * @param options.port The port to connect to. (Required.)
+ * @param options.hostname A literal IP address or host name that can be
+ * resolved to an IP address. If not specified, defaults to 0.0.0.0
+ * @param options.transport Defaults to "tcp". Later we plan to add "tcp4",
+ * "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6", "unix", "unixgram" and
+ * "unixpacket".
+ *
+ * Examples:
+ *
+ * listen({ port: 80 })
+ * listen({ hostname: "192.0.2.1", port: 80 })
+ * listen({ hostname: "[2001:db8::1]", port: 80 });
+ * listen({ hostname: "golang.org", port: 80, transport: "tcp" })
+ */
+ export function listen(options: ListenOptions): Listener;
+
+ export interface DialOptions {
+ port: number;
+ hostname?: string;
+ transport?: Transport;
+ }
+
+ /** Dial connects to the address on the named transport.
+ *
+ * @param options
+ * @param options.port The port to connect to. (Required.)
+ * @param options.hostname A literal IP address or host name that can be
+ * resolved to an IP address. If not specified, defaults to 127.0.0.1
+ * @param options.transport Defaults to "tcp". Later we plan to add "tcp4",
+ * "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6", "unix", "unixgram" and
+ * "unixpacket".
+ *
+ * Examples:
+ *
+ * dial({ port: 80 })
+ * dial({ hostname: "192.0.2.1", port: 80 })
+ * dial({ hostname: "[2001:db8::1]", port: 80 });
+ * dial({ hostname: "golang.org", port: 80, transport: "tcp" })
+ */
+ export function dial(options: DialOptions): Promise<Conn>;
+
+ export interface DialTLSOptions {
+ port: number;
+ hostname?: string;
+ }
+
+ /**
+ * dialTLS establishes a secure connection over TLS (transport layer security).
+ */
+ export function dialTLS(options: DialTLSOptions): Promise<Conn>;
+
+ // @url js/metrics.d.ts
+ export interface Metrics {
+ opsDispatched: number;
+ opsCompleted: number;
+ bytesSentControl: number;
+ bytesSentData: number;
+ bytesReceived: number;
+ }
+ /** Receive metrics from the privileged side of Deno.
+ *
+ * > console.table(Deno.metrics())
+ * ┌──────────────────┬────────┐
+ * │ (index) │ Values │
+ * ├──────────────────┼────────┤
+ * │ opsDispatched │ 9 │
+ * │ opsCompleted │ 9 │
+ * │ bytesSentControl │ 504 │
+ * │ bytesSentData │ 0 │
+ * │ bytesReceived │ 856 │
+ * └──────────────────┴────────┘
+ */
+ export function metrics(): Metrics;
+
+ // @url js/resources.d.ts
+
+ interface ResourceMap {
+ [rid: number]: string;
+ }
+ /** Returns a map of open _file like_ resource ids along with their string
+ * representation.
+ */
+ export function resources(): ResourceMap;
+
+ // @url js/process.d.ts
+
+ /** How to handle subprocess stdio.
+ *
+ * "inherit" The default if unspecified. The child inherits from the
+ * corresponding parent descriptor.
+ *
+ * "piped" A new pipe should be arranged to connect the parent and child
+ * subprocesses.
+ *
+ * "null" This stream will be ignored. This is the equivalent of attaching the
+ * stream to /dev/null.
+ */
+ type ProcessStdio = "inherit" | "piped" | "null";
+ export interface RunOptions {
+ args: string[];
+ cwd?: string;
+ env?: {
+ [key: string]: string;
+ };
+ stdout?: ProcessStdio | number;
+ stderr?: ProcessStdio | number;
+ stdin?: ProcessStdio | number;
+ }
+ /** Send a signal to process under given PID. Unix only at this moment.
+ * If pid is negative, the signal will be sent to the process group identified
+ * by -pid.
+ * Requires the `--allow-run` flag.
+ */
+ export function kill(pid: number, signo: number): void;
+ export class Process {
+ readonly rid: number;
+ readonly pid: number;
+ readonly stdin?: WriteCloser;
+ readonly stdout?: ReadCloser;
+ readonly stderr?: ReadCloser;
+ status(): Promise<ProcessStatus>;
+ /** Buffer the stdout and return it as Uint8Array after EOF.
+ * You must set stdout to "piped" when creating the process.
+ * This calls close() on stdout after its done.
+ */
+ output(): Promise<Uint8Array>;
+ /** Buffer the stderr and return it as Uint8Array after EOF.
+ * You must set stderr to "piped" when creating the process.
+ * This calls close() on stderr after its done.
+ */
+ stderrOutput(): Promise<Uint8Array>;
+ close(): void;
+ kill(signo: number): void;
+ }
+ export interface ProcessStatus {
+ success: boolean;
+ code?: number;
+ signal?: number;
+ }
+ /**
+ * Spawns new subprocess.
+ *
+ * Subprocess uses same working directory as parent process unless `opt.cwd`
+ * is specified.
+ *
+ * Environmental variables for subprocess can be specified using `opt.env`
+ * mapping.
+ *
+ * By default subprocess inherits stdio of parent process. To change that
+ * `opt.stdout`, `opt.stderr` and `opt.stdin` can be specified independently -
+ * they can be set to either `ProcessStdio` or `rid` of open file.
+ */
+ export function run(opt: RunOptions): Process;
+ enum LinuxSignal {
+ SIGHUP = 1,
+ SIGINT = 2,
+ SIGQUIT = 3,
+ SIGILL = 4,
+ SIGTRAP = 5,
+ SIGABRT = 6,
+ SIGBUS = 7,
+ SIGFPE = 8,
+ SIGKILL = 9,
+ SIGUSR1 = 10,
+ SIGSEGV = 11,
+ SIGUSR2 = 12,
+ SIGPIPE = 13,
+ SIGALRM = 14,
+ SIGTERM = 15,
+ SIGSTKFLT = 16,
+ SIGCHLD = 17,
+ SIGCONT = 18,
+ SIGSTOP = 19,
+ SIGTSTP = 20,
+ SIGTTIN = 21,
+ SIGTTOU = 22,
+ SIGURG = 23,
+ SIGXCPU = 24,
+ SIGXFSZ = 25,
+ SIGVTALRM = 26,
+ SIGPROF = 27,
+ SIGWINCH = 28,
+ SIGIO = 29,
+ SIGPWR = 30,
+ SIGSYS = 31
+ }
+ enum MacOSSignal {
+ SIGHUP = 1,
+ SIGINT = 2,
+ SIGQUIT = 3,
+ SIGILL = 4,
+ SIGTRAP = 5,
+ SIGABRT = 6,
+ SIGEMT = 7,
+ SIGFPE = 8,
+ SIGKILL = 9,
+ SIGBUS = 10,
+ SIGSEGV = 11,
+ SIGSYS = 12,
+ SIGPIPE = 13,
+ SIGALRM = 14,
+ SIGTERM = 15,
+ SIGURG = 16,
+ SIGSTOP = 17,
+ SIGTSTP = 18,
+ SIGCONT = 19,
+ SIGCHLD = 20,
+ SIGTTIN = 21,
+ SIGTTOU = 22,
+ SIGIO = 23,
+ SIGXCPU = 24,
+ SIGXFSZ = 25,
+ SIGVTALRM = 26,
+ SIGPROF = 27,
+ SIGWINCH = 28,
+ SIGINFO = 29,
+ SIGUSR1 = 30,
+ SIGUSR2 = 31
+ }
+ /** Signals numbers. This is platform dependent.
+ */
+ export const Signal: typeof MacOSSignal | typeof LinuxSignal;
+ export {};
+
+ // @url js/console.d.ts
+
+ type ConsoleOptions = Partial<{
+ showHidden: boolean;
+ depth: number;
+ colors: boolean;
+ indentLevel: number;
+ }>;
+ /** A symbol which can be used as a key for a custom method which will be called
+ * when `Deno.inspect()` is called, or when the object is logged to the console.
+ */
+ export const customInspect: unique symbol;
+ /**
+ * `inspect()` converts input into string that has the same format
+ * as printed by `console.log(...)`;
+ */
+ export function inspect(value: unknown, options?: ConsoleOptions): string;
+
+ // @url js/build.d.ts
+
+ export type OperatingSystem = "mac" | "win" | "linux";
+ export type Arch = "x64" | "arm64";
+ /** Build related information */
+ interface BuildInfo {
+ /** The CPU architecture. */
+ arch: Arch;
+ /** The operating system. */
+ os: OperatingSystem;
+ }
+ export const build: BuildInfo;
+
+ // @url js/version.d.ts
+
+ interface Version {
+ deno: string;
+ v8: string;
+ typescript: string;
+ }
+ export const version: Version;
+ export {};
+
+ // @url js/deno.d.ts
+
+ export const args: string[];
+}
+
+// @url js/globals.ts
+
+declare interface Window {
+ window: Window & typeof globalThis;
+ atob: typeof textEncoding.atob;
+ btoa: typeof textEncoding.btoa;
+ fetch: typeof fetchTypes.fetch;
+ clearTimeout: typeof timers.clearTimeout;
+ clearInterval: typeof timers.clearInterval;
+ console: consoleTypes.Console;
+ setTimeout: typeof timers.setTimeout;
+ setInterval: typeof timers.setInterval;
+ location: domTypes.Location;
+ onload: Function | undefined;
+ onunload: Function | undefined;
+ crypto: Crypto;
+ Blob: typeof blob.DenoBlob;
+ File: domTypes.DomFileConstructor;
+ CustomEvent: typeof customEvent.CustomEvent;
+ Event: typeof event.Event;
+ EventTarget: typeof eventTarget.EventTarget;
+ URL: typeof url.URL;
+ URLSearchParams: typeof urlSearchParams.URLSearchParams;
+ Headers: domTypes.HeadersConstructor;
+ FormData: domTypes.FormDataConstructor;
+ TextEncoder: typeof textEncoding.TextEncoder;
+ TextDecoder: typeof textEncoding.TextDecoder;
+ Request: domTypes.RequestConstructor;
+ Response: typeof fetchTypes.Response;
+ performance: performanceUtil.Performance;
+ onmessage: (e: { data: any }) => void;
+ workerMain: typeof workers.workerMain;
+ workerClose: typeof workers.workerClose;
+ postMessage: typeof workers.postMessage;
+ Worker: typeof workers.WorkerImpl;
+ addEventListener: (
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: boolean | domTypes.AddEventListenerOptions | undefined
+ ) => void;
+ dispatchEvent: (event: domTypes.Event) => boolean;
+ removeEventListener: (
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: boolean | domTypes.EventListenerOptions | undefined
+ ) => void;
+ queueMicrotask: (task: () => void) => void;
+ Deno: typeof Deno;
+}
+
+declare const window: Window & typeof globalThis;
+declare const atob: typeof textEncoding.atob;
+declare const btoa: typeof textEncoding.btoa;
+declare const fetch: typeof fetchTypes.fetch;
+declare const clearTimeout: typeof timers.clearTimeout;
+declare const clearInterval: typeof timers.clearInterval;
+declare const console: consoleTypes.Console;
+declare const setTimeout: typeof timers.setTimeout;
+declare const setInterval: typeof timers.setInterval;
+declare const location: domTypes.Location;
+declare const onload: Function | undefined;
+declare const onunload: Function | undefined;
+declare const crypto: Crypto;
+declare const Blob: typeof blob.DenoBlob;
+declare const File: domTypes.DomFileConstructor;
+declare const CustomEventInit: typeof customEvent.CustomEventInit;
+declare const CustomEvent: typeof customEvent.CustomEvent;
+declare const EventInit: typeof event.EventInit;
+declare const Event: typeof event.Event;
+declare const EventListener: typeof eventTarget.EventListener;
+declare const EventTarget: typeof eventTarget.EventTarget;
+declare const URL: typeof url.URL;
+declare const URLSearchParams: typeof urlSearchParams.URLSearchParams;
+declare const Headers: domTypes.HeadersConstructor;
+declare const FormData: domTypes.FormDataConstructor;
+declare const TextEncoder: typeof textEncoding.TextEncoder;
+declare const TextDecoder: typeof textEncoding.TextDecoder;
+declare const Request: domTypes.RequestConstructor;
+declare const Response: typeof fetchTypes.Response;
+declare const performance: performanceUtil.Performance;
+declare let onmessage: (e: { data: any }) => void;
+declare const workerMain: typeof workers.workerMain;
+declare const workerClose: typeof workers.workerClose;
+declare const postMessage: typeof workers.postMessage;
+declare const Worker: typeof workers.WorkerImpl;
+declare const addEventListener: (
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: boolean | domTypes.AddEventListenerOptions | undefined
+) => void;
+declare const dispatchEvent: (event: domTypes.Event) => boolean;
+declare const removeEventListener: (
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: boolean | domTypes.EventListenerOptions | undefined
+) => void;
+
+declare type Blob = domTypes.Blob;
+declare type Body = domTypes.Body;
+declare type File = domTypes.DomFile;
+declare type CustomEventInit = domTypes.CustomEventInit;
+declare type CustomEvent = domTypes.CustomEvent;
+declare type EventInit = domTypes.EventInit;
+declare type Event = domTypes.Event;
+declare type EventListener = domTypes.EventListener;
+declare type EventTarget = domTypes.EventTarget;
+declare type URL = url.URL;
+declare type URLSearchParams = domTypes.URLSearchParams;
+declare type Headers = domTypes.Headers;
+declare type FormData = domTypes.FormData;
+declare type TextEncoder = textEncoding.TextEncoder;
+declare type TextDecoder = textEncoding.TextDecoder;
+declare type Request = domTypes.Request;
+declare type Response = domTypes.Response;
+declare type Worker = workers.Worker;
+
+declare interface ImportMeta {
+ url: string;
+ main: boolean;
+}
+
+declare interface Crypto {
+ readonly subtle: null;
+ getRandomValues: <
+ T extends
+ | Int8Array
+ | Uint8Array
+ | Uint8ClampedArray
+ | Int16Array
+ | Uint16Array
+ | Int32Array
+ | Uint32Array
+ >(
+ typedArray: T
+ ) => T;
+}
+
+declare namespace domTypes {
+ // @url js/dom_types.d.ts
+
+ export type BufferSource = ArrayBufferView | ArrayBuffer;
+ export type HeadersInit =
+ | Headers
+ | Array<[string, string]>
+ | Record<string, string>;
+ export type URLSearchParamsInit =
+ | string
+ | string[][]
+ | Record<string, string>;
+ type BodyInit =
+ | Blob
+ | BufferSource
+ | FormData
+ | URLSearchParams
+ | ReadableStream
+ | string;
+ export type RequestInfo = Request | string;
+ type ReferrerPolicy =
+ | ""
+ | "no-referrer"
+ | "no-referrer-when-downgrade"
+ | "origin-only"
+ | "origin-when-cross-origin"
+ | "unsafe-url";
+ export type BlobPart = BufferSource | Blob | string;
+ export type FormDataEntryValue = DomFile | string;
+ export interface DomIterable<K, V> {
+ keys(): IterableIterator<K>;
+ values(): IterableIterator<V>;
+ entries(): IterableIterator<[K, V]>;
+ [Symbol.iterator](): IterableIterator<[K, V]>;
+ forEach(
+ callback: (value: V, key: K, parent: this) => void,
+ thisArg?: any
+ ): void;
+ }
+ type EndingType = "transparent" | "native";
+ export interface BlobPropertyBag {
+ type?: string;
+ ending?: EndingType;
+ }
+ interface AbortSignalEventMap {
+ abort: ProgressEvent;
+ }
+ export enum NodeType {
+ ELEMENT_NODE = 1,
+ TEXT_NODE = 3,
+ DOCUMENT_FRAGMENT_NODE = 11
+ }
+ export const eventTargetHost: unique symbol;
+ export const eventTargetListeners: unique symbol;
+ export const eventTargetMode: unique symbol;
+ export const eventTargetNodeType: unique symbol;
+ export interface EventTarget {
+ [eventTargetHost]: EventTarget | null;
+ [eventTargetListeners]: { [type in string]: EventListener[] };
+ [eventTargetMode]: string;
+ [eventTargetNodeType]: NodeType;
+ addEventListener(
+ type: string,
+ callback: (event: Event) => void | null,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+ dispatchEvent(event: Event): boolean;
+ removeEventListener(
+ type: string,
+ callback?: (event: Event) => void | null,
+ options?: EventListenerOptions | boolean
+ ): void;
+ }
+ export interface ProgressEventInit extends EventInit {
+ lengthComputable?: boolean;
+ loaded?: number;
+ total?: number;
+ }
+ export interface URLSearchParams extends DomIterable<string, string> {
+ /**
+ * Appends a specified key/value pair as a new search parameter.
+ */
+ append(name: string, value: string): void;
+ /**
+ * Deletes the given search parameter, and its associated value,
+ * from the list of all search parameters.
+ */
+ delete(name: string): void;
+ /**
+ * Returns the first value associated to the given search parameter.
+ */
+ get(name: string): string | null;
+ /**
+ * Returns all the values association with a given search parameter.
+ */
+ getAll(name: string): string[];
+ /**
+ * Returns a Boolean indicating if such a search parameter exists.
+ */
+ has(name: string): boolean;
+ /**
+ * Sets the value associated to a given search parameter to the given value.
+ * If there were several values, delete the others.
+ */
+ set(name: string, value: string): void;
+ /**
+ * Sort all key/value pairs contained in this object in place
+ * and return undefined. The sort order is according to Unicode
+ * code points of the keys.
+ */
+ sort(): void;
+ /**
+ * Returns a query string suitable for use in a URL.
+ */
+ toString(): string;
+ /**
+ * Iterates over each name-value pair in the query
+ * and invokes the given function.
+ */
+ forEach(
+ callbackfn: (value: string, key: string, parent: this) => void,
+ thisArg?: any
+ ): void;
+ }
+ export interface EventListener {
+ handleEvent(event: Event): void;
+ readonly callback: (event: Event) => void | null;
+ readonly options: boolean | AddEventListenerOptions;
+ }
+ export interface EventInit {
+ bubbles?: boolean;
+ cancelable?: boolean;
+ composed?: boolean;
+ }
+ export interface CustomEventInit extends EventInit {
+ detail?: any;
+ }
+ export enum EventPhase {
+ NONE = 0,
+ CAPTURING_PHASE = 1,
+ AT_TARGET = 2,
+ BUBBLING_PHASE = 3
+ }
+ export interface EventPath {
+ item: EventTarget;
+ itemInShadowTree: boolean;
+ relatedTarget: EventTarget | null;
+ rootOfClosedTree: boolean;
+ slotInClosedTree: boolean;
+ target: EventTarget | null;
+ touchTargetList: EventTarget[];
+ }
+ export interface Event {
+ readonly type: string;
+ target: EventTarget | null;
+ currentTarget: EventTarget | null;
+ composedPath(): EventPath[];
+ eventPhase: number;
+ stopPropagation(): void;
+ stopImmediatePropagation(): void;
+ readonly bubbles: boolean;
+ readonly cancelable: boolean;
+ preventDefault(): void;
+ readonly defaultPrevented: boolean;
+ readonly composed: boolean;
+ isTrusted: boolean;
+ readonly timeStamp: Date;
+ dispatched: boolean;
+ readonly initialized: boolean;
+ inPassiveListener: boolean;
+ cancelBubble: boolean;
+ cancelBubbleImmediately: boolean;
+ path: EventPath[];
+ relatedTarget: EventTarget | null;
+ }
+ export interface CustomEvent extends Event {
+ readonly detail: any;
+ initCustomEvent(
+ type: string,
+ bubbles?: boolean,
+ cancelable?: boolean,
+ detail?: any | null
+ ): void;
+ }
+ export interface DomFile extends Blob {
+ readonly lastModified: number;
+ readonly name: string;
+ }
+ export interface DomFileConstructor {
+ new (
+ bits: BlobPart[],
+ filename: string,
+ options?: FilePropertyBag
+ ): DomFile;
+ prototype: DomFile;
+ }
+ export interface FilePropertyBag extends BlobPropertyBag {
+ lastModified?: number;
+ }
+ interface ProgressEvent extends Event {
+ readonly lengthComputable: boolean;
+ readonly loaded: number;
+ readonly total: number;
+ }
+ export interface EventListenerOptions {
+ capture: boolean;
+ }
+ export interface AddEventListenerOptions extends EventListenerOptions {
+ once: boolean;
+ passive: boolean;
+ }
+ interface AbortSignal extends EventTarget {
+ readonly aborted: boolean;
+ onabort: ((this: AbortSignal, ev: ProgressEvent) => any) | null;
+ addEventListener<K extends keyof AbortSignalEventMap>(
+ type: K,
+ listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+ addEventListener(
+ type: string,
+ listener: EventListener,
+ options?: boolean | AddEventListenerOptions
+ ): void;
+ removeEventListener<K extends keyof AbortSignalEventMap>(
+ type: K,
+ listener: (this: AbortSignal, ev: AbortSignalEventMap[K]) => any,
+ options?: boolean | EventListenerOptions
+ ): void;
+ removeEventListener(
+ type: string,
+ listener: EventListener,
+ options?: boolean | EventListenerOptions
+ ): void;
+ }
+ export interface ReadableStream {
+ readonly locked: boolean;
+ cancel(): Promise<void>;
+ getReader(): ReadableStreamReader;
+ tee(): [ReadableStream, ReadableStream];
+ }
+ export interface ReadableStreamReader {
+ cancel(): Promise<void>;
+ read(): Promise<any>;
+ releaseLock(): void;
+ }
+ export interface FormData extends DomIterable<string, FormDataEntryValue> {
+ append(name: string, value: string | Blob, fileName?: string): void;
+ delete(name: string): void;
+ get(name: string): FormDataEntryValue | null;
+ getAll(name: string): FormDataEntryValue[];
+ has(name: string): boolean;
+ set(name: string, value: string | Blob, fileName?: string): void;
+ }
+ export interface FormDataConstructor {
+ new (): FormData;
+ prototype: FormData;
+ }
+ /** A blob object represents a file-like object of immutable, raw data. */
+ export interface Blob {
+ /** The size, in bytes, of the data contained in the `Blob` object. */
+ readonly size: number;
+ /** A string indicating the media type of the data contained in the `Blob`.
+ * If the type is unknown, this string is empty.
+ */
+ readonly type: string;
+ /** Returns a new `Blob` object containing the data in the specified range of
+ * bytes of the source `Blob`.
+ */
+ slice(start?: number, end?: number, contentType?: string): Blob;
+ }
+ export interface Body {
+ /** A simple getter used to expose a `ReadableStream` of the body contents. */
+ readonly body: ReadableStream | null;
+ /** Stores a `Boolean` that declares whether the body has been used in a
+ * response yet.
+ */
+ readonly bodyUsed: boolean;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with an `ArrayBuffer`.
+ */
+ arrayBuffer(): Promise<ArrayBuffer>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `Blob`.
+ */
+ blob(): Promise<Blob>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `FormData` object.
+ */
+ formData(): Promise<FormData>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with the result of parsing the body text as JSON.
+ */
+ json(): Promise<any>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `USVString` (text).
+ */
+ text(): Promise<string>;
+ }
+ export interface Headers extends DomIterable<string, string> {
+ /** Appends a new value onto an existing header inside a `Headers` object, or
+ * adds the header if it does not already exist.
+ */
+ append(name: string, value: string): void;
+ /** Deletes a header from a `Headers` object. */
+ delete(name: string): void;
+ /** Returns an iterator allowing to go through all key/value pairs
+ * contained in this Headers object. The both the key and value of each pairs
+ * are ByteString objects.
+ */
+ entries(): IterableIterator<[string, string]>;
+ /** Returns a `ByteString` sequence of all the values of a header within a
+ * `Headers` object with a given name.
+ */
+ get(name: string): string | null;
+ /** Returns a boolean stating whether a `Headers` object contains a certain
+ * header.
+ */
+ has(name: string): boolean;
+ /** Returns an iterator allowing to go through all keys contained in
+ * this Headers object. The keys are ByteString objects.
+ */
+ keys(): IterableIterator<string>;
+ /** Sets a new value for an existing header inside a Headers object, or adds
+ * the header if it does not already exist.
+ */
+ set(name: string, value: string): void;
+ /** Returns an iterator allowing to go through all values contained in
+ * this Headers object. The values are ByteString objects.
+ */
+ values(): IterableIterator<string>;
+ forEach(
+ callbackfn: (value: string, key: string, parent: this) => void,
+ thisArg?: any
+ ): void;
+ /** The Symbol.iterator well-known symbol specifies the default
+ * iterator for this Headers object
+ */
+ [Symbol.iterator](): IterableIterator<[string, string]>;
+ }
+ export interface HeadersConstructor {
+ new (init?: HeadersInit): Headers;
+ prototype: Headers;
+ }
+ type RequestCache =
+ | "default"
+ | "no-store"
+ | "reload"
+ | "no-cache"
+ | "force-cache"
+ | "only-if-cached";
+ type RequestCredentials = "omit" | "same-origin" | "include";
+ type RequestDestination =
+ | ""
+ | "audio"
+ | "audioworklet"
+ | "document"
+ | "embed"
+ | "font"
+ | "image"
+ | "manifest"
+ | "object"
+ | "paintworklet"
+ | "report"
+ | "script"
+ | "sharedworker"
+ | "style"
+ | "track"
+ | "video"
+ | "worker"
+ | "xslt";
+ type RequestMode = "navigate" | "same-origin" | "no-cors" | "cors";
+ type RequestRedirect = "follow" | "error" | "manual";
+ type ResponseType =
+ | "basic"
+ | "cors"
+ | "default"
+ | "error"
+ | "opaque"
+ | "opaqueredirect";
+ export interface RequestInit {
+ body?: BodyInit | null;
+ cache?: RequestCache;
+ credentials?: RequestCredentials;
+ headers?: HeadersInit;
+ integrity?: string;
+ keepalive?: boolean;
+ method?: string;
+ mode?: RequestMode;
+ redirect?: RequestRedirect;
+ referrer?: string;
+ referrerPolicy?: ReferrerPolicy;
+ signal?: AbortSignal | null;
+ window?: any;
+ }
+ export interface ResponseInit {
+ headers?: HeadersInit;
+ status?: number;
+ statusText?: string;
+ }
+ export interface RequestConstructor {
+ new (input: RequestInfo, init?: RequestInit): Request;
+ prototype: Request;
+ }
+ export interface Request extends Body {
+ /** Returns the cache mode associated with request, which is a string
+ * indicating how the the request will interact with the browser's cache when
+ * fetching.
+ */
+ readonly cache?: RequestCache;
+ /** Returns the credentials mode associated with request, which is a string
+ * indicating whether credentials will be sent with the request always, never,
+ * or only when sent to a same-origin URL.
+ */
+ readonly credentials?: RequestCredentials;
+ /** Returns the kind of resource requested by request, (e.g., `document` or
+ * `script`).
+ */
+ readonly destination?: RequestDestination;
+ /** Returns a Headers object consisting of the headers associated with
+ * request.
+ *
+ * Note that headers added in the network layer by the user agent
+ * will not be accounted for in this object, (e.g., the `Host` header).
+ */
+ readonly headers: Headers;
+ /** Returns request's subresource integrity metadata, which is a cryptographic
+ * hash of the resource being fetched. Its value consists of multiple hashes
+ * separated by whitespace. [SRI]
+ */
+ readonly integrity?: string;
+ /** Returns a boolean indicating whether or not request is for a history
+ * navigation (a.k.a. back-forward navigation).
+ */
+ readonly isHistoryNavigation?: boolean;
+ /** Returns a boolean indicating whether or not request is for a reload
+ * navigation.
+ */
+ readonly isReloadNavigation?: boolean;
+ /** Returns a boolean indicating whether or not request can outlive the global
+ * in which it was created.
+ */
+ readonly keepalive?: boolean;
+ /** Returns request's HTTP method, which is `GET` by default. */
+ readonly method: string;
+ /** Returns the mode associated with request, which is a string indicating
+ * whether the request will use CORS, or will be restricted to same-origin
+ * URLs.
+ */
+ readonly mode?: RequestMode;
+ /** Returns the redirect mode associated with request, which is a string
+ * indicating how redirects for the request will be handled during fetching.
+ *
+ * A request will follow redirects by default.
+ */
+ readonly redirect?: RequestRedirect;
+ /** Returns the referrer of request. Its value can be a same-origin URL if
+ * explicitly set in init, the empty string to indicate no referrer, and
+ * `about:client` when defaulting to the global's default.
+ *
+ * This is used during fetching to determine the value of the `Referer`
+ * header of the request being made.
+ */
+ readonly referrer?: string;
+ /** Returns the referrer policy associated with request. This is used during
+ * fetching to compute the value of the request's referrer.
+ */
+ readonly referrerPolicy?: ReferrerPolicy;
+ /** Returns the signal associated with request, which is an AbortSignal object
+ * indicating whether or not request has been aborted, and its abort event
+ * handler.
+ */
+ readonly signal?: AbortSignal;
+ /** Returns the URL of request as a string. */
+ readonly url: string;
+ clone(): Request;
+ }
+ export interface Response extends Body {
+ /** Contains the `Headers` object associated with the response. */
+ readonly headers: Headers;
+ /** Contains a boolean stating whether the response was successful (status in
+ * the range 200-299) or not.
+ */
+ readonly ok: boolean;
+ /** Indicates whether or not the response is the result of a redirect; that
+ * is, its URL list has more than one entry.
+ */
+ readonly redirected: boolean;
+ /** Contains the status code of the response (e.g., `200` for a success). */
+ readonly status: number;
+ /** Contains the status message corresponding to the status code (e.g., `OK`
+ * for `200`).
+ */
+ readonly statusText: string;
+ readonly trailer: Promise<Headers>;
+ /** Contains the type of the response (e.g., `basic`, `cors`). */
+ readonly type: ResponseType;
+ /** Contains the URL of the response. */
+ readonly url: string;
+ /** Creates a clone of a `Response` object. */
+ clone(): Response;
+ }
+ export interface Location {
+ /**
+ * Returns a DOMStringList object listing the origins of the ancestor browsing
+ * contexts, from the parent browsing context to the top-level browsing
+ * context.
+ */
+ readonly ancestorOrigins: string[];
+ /**
+ * Returns the Location object's URL's fragment (includes leading "#" if
+ * non-empty).
+ * Can be set, to navigate to the same URL with a changed fragment (ignores
+ * leading "#").
+ */
+ hash: string;
+ /**
+ * Returns the Location object's URL's host and port (if different from the
+ * default port for the scheme). Can be set, to navigate to the same URL with
+ * a changed host and port.
+ */
+ host: string;
+ /**
+ * Returns the Location object's URL's host. Can be set, to navigate to the
+ * same URL with a changed host.
+ */
+ hostname: string;
+ /**
+ * Returns the Location object's URL. Can be set, to navigate to the given
+ * URL.
+ */
+ href: string;
+ /** Returns the Location object's URL's origin. */
+ readonly origin: string;
+ /**
+ * Returns the Location object's URL's path.
+ * Can be set, to navigate to the same URL with a changed path.
+ */
+ pathname: string;
+ /**
+ * Returns the Location object's URL's port.
+ * Can be set, to navigate to the same URL with a changed port.
+ */
+ port: string;
+ /**
+ * Returns the Location object's URL's scheme.
+ * Can be set, to navigate to the same URL with a changed scheme.
+ */
+ protocol: string;
+ /**
+ * Returns the Location object's URL's query (includes leading "?" if
+ * non-empty). Can be set, to navigate to the same URL with a changed query
+ * (ignores leading "?").
+ */
+ search: string;
+ /**
+ * Navigates to the given URL.
+ */
+ assign(url: string): void;
+ /**
+ * Reloads the current page.
+ */
+ reload(): void;
+ /** @deprecated */
+ reload(forcedReload: boolean): void;
+ /**
+ * Removes the current page from the session history and navigates to the
+ * given URL.
+ */
+ replace(url: string): void;
+ }
+}
+
+declare namespace blob {
+ // @url js/blob.d.ts
+
+ export const bytesSymbol: unique symbol;
+ export const blobBytesWeakMap: WeakMap<domTypes.Blob, Uint8Array>;
+ export class DenoBlob implements domTypes.Blob {
+ private readonly [bytesSymbol];
+ readonly size: number;
+ readonly type: string;
+ /** A blob object represents a file-like object of immutable, raw data. */
+ constructor(
+ blobParts?: domTypes.BlobPart[],
+ options?: domTypes.BlobPropertyBag
+ );
+ slice(start?: number, end?: number, contentType?: string): DenoBlob;
+ }
+}
+
+declare namespace consoleTypes {
+ // @url js/console.d.ts
+
+ type ConsoleOptions = Partial<{
+ showHidden: boolean;
+ depth: number;
+ colors: boolean;
+ indentLevel: number;
+ }>;
+ export class CSI {
+ static kClear: string;
+ static kClearScreenDown: string;
+ }
+ const isConsoleInstance: unique symbol;
+ export class Console {
+ private printFunc;
+ indentLevel: number;
+ [isConsoleInstance]: boolean;
+ /** Writes the arguments to stdout */
+ log: (...args: unknown[]) => void;
+ /** Writes the arguments to stdout */
+ debug: (...args: unknown[]) => void;
+ /** Writes the arguments to stdout */
+ info: (...args: unknown[]) => void;
+ /** Writes the properties of the supplied `obj` to stdout */
+ dir: (
+ obj: unknown,
+ options?: Partial<{
+ showHidden: boolean;
+ depth: number;
+ colors: boolean;
+ indentLevel: number;
+ }>
+ ) => void;
+
+ /** From MDN:
+ * Displays an interactive tree of the descendant elements of
+ * the specified XML/HTML element. If it is not possible to display
+ * as an element the JavaScript Object view is shown instead.
+ * The output is presented as a hierarchical listing of expandable
+ * nodes that let you see the contents of child nodes.
+ *
+ * Since we write to stdout, we can't display anything interactive
+ * we just fall back to `console.dir`.
+ */
+ dirxml: (
+ obj: unknown,
+ options?: Partial<{
+ showHidden: boolean;
+ depth: number;
+ colors: boolean;
+ indentLevel: number;
+ }>
+ ) => void;
+
+ /** Writes the arguments to stdout */
+ warn: (...args: unknown[]) => void;
+ /** Writes the arguments to stdout */
+ error: (...args: unknown[]) => void;
+ /** Writes an error message to stdout if the assertion is `false`. If the
+ * assertion is `true`, nothing happens.
+ *
+ * ref: https://console.spec.whatwg.org/#assert
+ */
+ assert: (condition?: boolean, ...args: unknown[]) => void;
+ count: (label?: string) => void;
+ countReset: (label?: string) => void;
+ table: (data: unknown, properties?: string[] | undefined) => void;
+ time: (label?: string) => void;
+ timeLog: (label?: string, ...args: unknown[]) => void;
+ timeEnd: (label?: string) => void;
+ group: (...label: unknown[]) => void;
+ groupCollapsed: (...label: unknown[]) => void;
+ groupEnd: () => void;
+ clear: () => void;
+ trace: (...args: unknown[]) => void;
+ static [Symbol.hasInstance](instance: Console): boolean;
+ }
+ /** A symbol which can be used as a key for a custom method which will be called
+ * when `Deno.inspect()` is called, or when the object is logged to the console.
+ */
+ export const customInspect: unique symbol;
+ /**
+ * `inspect()` converts input into string that has the same format
+ * as printed by `console.log(...)`;
+ */
+ export function inspect(value: unknown, options?: ConsoleOptions): string;
+}
+
+declare namespace event {
+ // @url js/event.d.ts
+
+ export const eventAttributes: WeakMap<object, any>;
+ export class EventInit implements domTypes.EventInit {
+ bubbles: boolean;
+ cancelable: boolean;
+ composed: boolean;
+ constructor({
+ bubbles,
+ cancelable,
+ composed
+ }?: {
+ bubbles?: boolean | undefined;
+ cancelable?: boolean | undefined;
+ composed?: boolean | undefined;
+ });
+ }
+ export class Event implements domTypes.Event {
+ isTrusted: boolean;
+ private _canceledFlag;
+ private _dispatchedFlag;
+ private _initializedFlag;
+ private _inPassiveListenerFlag;
+ private _stopImmediatePropagationFlag;
+ private _stopPropagationFlag;
+ private _path;
+ constructor(type: string, eventInitDict?: domTypes.EventInit);
+ readonly bubbles: boolean;
+ cancelBubble: boolean;
+ cancelBubbleImmediately: boolean;
+ readonly cancelable: boolean;
+ readonly composed: boolean;
+ currentTarget: domTypes.EventTarget;
+ readonly defaultPrevented: boolean;
+ dispatched: boolean;
+ eventPhase: number;
+ readonly initialized: boolean;
+ inPassiveListener: boolean;
+ path: domTypes.EventPath[];
+ relatedTarget: domTypes.EventTarget;
+ target: domTypes.EventTarget;
+ readonly timeStamp: Date;
+ readonly type: string;
+ /** Returns the event’s path (objects on which listeners will be
+ * invoked). This does not include nodes in shadow trees if the
+ * shadow root was created with its ShadowRoot.mode closed.
+ *
+ * event.composedPath();
+ */
+ composedPath(): domTypes.EventPath[];
+ /** Cancels the event (if it is cancelable).
+ * See https://dom.spec.whatwg.org/#set-the-canceled-flag
+ *
+ * event.preventDefault();
+ */
+ preventDefault(): void;
+ /** Stops the propagation of events further along in the DOM.
+ *
+ * event.stopPropagation();
+ */
+ stopPropagation(): void;
+ /** For this particular event, no other listener will be called.
+ * Neither those attached on the same element, nor those attached
+ * on elements which will be traversed later (in capture phase,
+ * for instance).
+ *
+ * event.stopImmediatePropagation();
+ */
+ stopImmediatePropagation(): void;
+ }
+}
+
+declare namespace customEvent {
+ // @url js/custom_event.d.ts
+
+ export const customEventAttributes: WeakMap<object, any>;
+ export class CustomEventInit extends event.EventInit
+ implements domTypes.CustomEventInit {
+ detail: any;
+ constructor({
+ bubbles,
+ cancelable,
+ composed,
+ detail
+ }: domTypes.CustomEventInit);
+ }
+ export class CustomEvent extends event.Event implements domTypes.CustomEvent {
+ constructor(type: string, customEventInitDict?: domTypes.CustomEventInit);
+ readonly detail: any;
+ initCustomEvent(
+ type: string,
+ bubbles?: boolean,
+ cancelable?: boolean,
+ detail?: any
+ ): void;
+ readonly [Symbol.toStringTag]: string;
+ }
+}
+
+declare namespace eventTarget {
+ // @url js/event_target.d.ts
+
+ export class EventListenerOptions implements domTypes.EventListenerOptions {
+ _capture: boolean;
+ constructor({ capture }?: { capture?: boolean | undefined });
+ readonly capture: boolean;
+ }
+ export class AddEventListenerOptions extends EventListenerOptions
+ implements domTypes.AddEventListenerOptions {
+ _passive: boolean;
+ _once: boolean;
+ constructor({
+ capture,
+ passive,
+ once
+ }?: {
+ capture?: boolean | undefined;
+ passive?: boolean | undefined;
+ once?: boolean | undefined;
+ });
+ readonly passive: boolean;
+ readonly once: boolean;
+ }
+ export class EventListener implements domTypes.EventListener {
+ allEvents: domTypes.Event[];
+ atEvents: domTypes.Event[];
+ bubbledEvents: domTypes.Event[];
+ capturedEvents: domTypes.Event[];
+ private _callback;
+ private _options;
+ constructor(
+ callback: (event: domTypes.Event) => void | null,
+ options: boolean | domTypes.AddEventListenerOptions
+ );
+ handleEvent(event: domTypes.Event): void;
+ readonly callback: (event: domTypes.Event) => void | null;
+ readonly options: domTypes.AddEventListenerOptions | boolean;
+ }
+ export const eventTargetAssignedSlot: unique symbol;
+ export const eventTargetHasActivationBehavior: unique symbol;
+ export class EventTarget implements domTypes.EventTarget {
+ [domTypes.eventTargetHost]: domTypes.EventTarget | null;
+ [domTypes.eventTargetListeners]: {
+ [type in string]: domTypes.EventListener[]
+ };
+ [domTypes.eventTargetMode]: string;
+ [domTypes.eventTargetNodeType]: domTypes.NodeType;
+ private [eventTargetAssignedSlot];
+ private [eventTargetHasActivationBehavior];
+ addEventListener(
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: domTypes.AddEventListenerOptions | boolean
+ ): void;
+ removeEventListener(
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: domTypes.EventListenerOptions | boolean
+ ): void;
+ dispatchEvent(event: domTypes.Event): boolean;
+ readonly [Symbol.toStringTag]: string;
+ }
+}
+
+declare namespace io {
+ // @url js/io.d.ts
+
+ export const EOF: null;
+ export type EOF = null;
+ export enum SeekMode {
+ SEEK_START = 0,
+ SEEK_CURRENT = 1,
+ SEEK_END = 2
+ }
+ export interface Reader {
+ /** Reads up to p.byteLength bytes into `p`. It resolves to the number
+ * of bytes read (`0` < `n` <= `p.byteLength`) and rejects if any error encountered.
+ * Even if `read()` returns `n` < `p.byteLength`, it may use all of `p` as
+ * scratch space during the call. If some data is available but not
+ * `p.byteLength` bytes, `read()` conventionally returns what is available
+ * instead of waiting for more.
+ *
+ * When `read()` encounters end-of-file condition, it returns EOF symbol.
+ *
+ * When `read()` encounters an error, it rejects with an error.
+ *
+ * Callers should always process the `n` > `0` bytes returned before
+ * considering the EOF. Doing so correctly handles I/O errors that happen
+ * after reading some bytes and also both of the allowed EOF behaviors.
+ *
+ * Implementations must not retain `p`.
+ */
+ read(p: Uint8Array): Promise<number | EOF>;
+ }
+ export interface SyncReader {
+ readSync(p: Uint8Array): number | EOF;
+ }
+ export interface Writer {
+ /** Writes `p.byteLength` bytes from `p` to the underlying data
+ * stream. It resolves to the number of bytes written from `p` (`0` <= `n` <=
+ * `p.byteLength`) and any error encountered that caused the write to stop
+ * early. `write()` must return a non-null error if it returns `n` <
+ * `p.byteLength`. write() must not modify the slice data, even temporarily.
+ *
+ * Implementations must not retain `p`.
+ */
+ write(p: Uint8Array): Promise<number>;
+ }
+ export interface SyncWriter {
+ writeSync(p: Uint8Array): number;
+ }
+ export interface Closer {
+ close(): void;
+ }
+ export interface Seeker {
+ /** Seek sets the offset for the next `read()` or `write()` to offset,
+ * interpreted according to `whence`: `SeekStart` means relative to the start
+ * of the file, `SeekCurrent` means relative to the current offset, and
+ * `SeekEnd` means relative to the end. Seek returns the new offset relative
+ * to the start of the file and an error, if any.
+ *
+ * Seeking to an offset before the start of the file is an error. Seeking to
+ * any positive offset is legal, but the behavior of subsequent I/O operations
+ * on the underlying object is implementation-dependent.
+ */
+ seek(offset: number, whence: SeekMode): Promise<void>;
+ }
+ export interface SyncSeeker {
+ seekSync(offset: number, whence: SeekMode): void;
+ }
+ export interface ReadCloser extends Reader, Closer {}
+ export interface WriteCloser extends Writer, Closer {}
+ export interface ReadSeeker extends Reader, Seeker {}
+ export interface WriteSeeker extends Writer, Seeker {}
+ export interface ReadWriteCloser extends Reader, Writer, Closer {}
+ export interface ReadWriteSeeker extends Reader, Writer, Seeker {}
+ /** Copies from `src` to `dst` until either `EOF` is reached on `src`
+ * or an error occurs. It returns the number of bytes copied and the first
+ * error encountered while copying, if any.
+ *
+ * Because `copy()` is defined to read from `src` until `EOF`, it does not
+ * treat an `EOF` from `read()` as an error to be reported.
+ */
+ export function copy(dst: Writer, src: Reader): Promise<number>;
+ /** Turns `r` into async iterator.
+ *
+ * for await (const chunk of toAsyncIterator(reader)) {
+ * console.log(chunk)
+ * }
+ */
+ export function toAsyncIterator(r: Reader): AsyncIterableIterator<Uint8Array>;
+}
+
+declare namespace fetchTypes {
+ // @url js/fetch.d.ts
+
+ class Body implements domTypes.Body, domTypes.ReadableStream, io.ReadCloser {
+ private rid;
+ readonly contentType: string;
+ bodyUsed: boolean;
+ private _bodyPromise;
+ private _data;
+ readonly locked: boolean;
+ readonly body: null | Body;
+ constructor(rid: number, contentType: string);
+ private _bodyBuffer;
+ arrayBuffer(): Promise<ArrayBuffer>;
+ blob(): Promise<domTypes.Blob>;
+ formData(): Promise<domTypes.FormData>;
+ json(): Promise<any>;
+ text(): Promise<string>;
+ read(p: Uint8Array): Promise<number | io.EOF>;
+ close(): void;
+ cancel(): Promise<void>;
+ getReader(): domTypes.ReadableStreamReader;
+ tee(): [domTypes.ReadableStream, domTypes.ReadableStream];
+ [Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array>;
+ }
+ export class Response implements domTypes.Response {
+ readonly url: string;
+ readonly status: number;
+ statusText: string;
+ readonly type = "basic";
+ readonly redirected: boolean;
+ headers: domTypes.Headers;
+ readonly trailer: Promise<domTypes.Headers>;
+ bodyUsed: boolean;
+ readonly body: Body;
+ constructor(
+ url: string,
+ status: number,
+ headersList: Array<[string, string]>,
+ rid: number,
+ redirected_: boolean,
+ body_?: null | Body
+ );
+ arrayBuffer(): Promise<ArrayBuffer>;
+ blob(): Promise<domTypes.Blob>;
+ formData(): Promise<domTypes.FormData>;
+ json(): Promise<any>;
+ text(): Promise<string>;
+ readonly ok: boolean;
+ clone(): domTypes.Response;
+ }
+ /** Fetch a resource from the network. */
+ export function fetch(
+ input: domTypes.Request | string,
+ init?: domTypes.RequestInit
+ ): Promise<Response>;
+}
+
+declare namespace textEncoding {
+ // @url js/text_encoding.d.ts
+
+ export function atob(s: string): string;
+ /** Creates a base-64 ASCII string from the input string. */
+ export function btoa(s: string): string;
+ export interface TextDecodeOptions {
+ stream?: false;
+ }
+ export interface TextDecoderOptions {
+ fatal?: boolean;
+ ignoreBOM?: boolean;
+ }
+ export class TextDecoder {
+ private _encoding;
+ /** Returns encoding's name, lowercased. */
+ readonly encoding: string;
+ /** Returns `true` if error mode is "fatal", and `false` otherwise. */
+ readonly fatal: boolean;
+ /** Returns `true` if ignore BOM flag is set, and `false` otherwise. */
+ readonly ignoreBOM = false;
+ constructor(label?: string, options?: TextDecoderOptions);
+ /** Returns the result of running encoding's decoder. */
+ decode(input?: domTypes.BufferSource, options?: TextDecodeOptions): string;
+ readonly [Symbol.toStringTag]: string;
+ }
+ interface TextEncoderEncodeIntoResult {
+ read: number;
+ written: number;
+ }
+ export class TextEncoder {
+ /** Returns "utf-8". */
+ readonly encoding = "utf-8";
+ /** Returns the result of running UTF-8's encoder. */
+ encode(input?: string): Uint8Array;
+ encodeInto(input: string, dest: Uint8Array): TextEncoderEncodeIntoResult;
+ readonly [Symbol.toStringTag]: string;
+ }
+}
+
+declare namespace timers {
+ // @url js/timers.d.ts
+
+ export type Args = unknown[];
+ /** Sets a timer which executes a function once after the timer expires. */
+ export function setTimeout(
+ cb: (...args: Args) => void,
+ delay?: number,
+ ...args: Args
+ ): number;
+ /** Repeatedly calls a function , with a fixed time delay between each call. */
+ export function setInterval(
+ cb: (...args: Args) => void,
+ delay?: number,
+ ...args: Args
+ ): number;
+ export function clearTimeout(id?: number): void;
+ export function clearInterval(id?: number): void;
+}
+
+declare namespace urlSearchParams {
+ // @url js/url_search_params.d.ts
+
+ export class URLSearchParams {
+ private params;
+ private url;
+ constructor(init?: string | string[][] | Record<string, string>);
+ private updateSteps;
+ /** Appends a specified key/value pair as a new search parameter.
+ *
+ * searchParams.append('name', 'first');
+ * searchParams.append('name', 'second');
+ */
+ append(name: string, value: string): void;
+ /** Deletes the given search parameter and its associated value,
+ * from the list of all search parameters.
+ *
+ * searchParams.delete('name');
+ */
+ delete(name: string): void;
+ /** Returns all the values associated with a given search parameter
+ * as an array.
+ *
+ * searchParams.getAll('name');
+ */
+ getAll(name: string): string[];
+ /** Returns the first value associated to the given search parameter.
+ *
+ * searchParams.get('name');
+ */
+ get(name: string): string | null;
+ /** Returns a Boolean that indicates whether a parameter with the
+ * specified name exists.
+ *
+ * searchParams.has('name');
+ */
+ has(name: string): boolean;
+ /** Sets the value associated with a given search parameter to the
+ * given value. If there were several matching values, this method
+ * deletes the others. If the search parameter doesn't exist, this
+ * method creates it.
+ *
+ * searchParams.set('name', 'value');
+ */
+ set(name: string, value: string): void;
+ /** Sort all key/value pairs contained in this object in place and
+ * return undefined. The sort order is according to Unicode code
+ * points of the keys.
+ *
+ * searchParams.sort();
+ */
+ sort(): void;
+ /** Calls a function for each element contained in this object in
+ * place and return undefined. Optionally accepts an object to use
+ * as this when executing callback as second argument.
+ *
+ * searchParams.forEach((value, key, parent) => {
+ * console.log(value, key, parent);
+ * });
+ *
+ */
+ forEach(
+ callbackfn: (value: string, key: string, parent: this) => void,
+ thisArg?: any
+ ): void;
+ /** Returns an iterator allowing to go through all keys contained
+ * in this object.
+ *
+ * for (const key of searchParams.keys()) {
+ * console.log(key);
+ * }
+ */
+ keys(): IterableIterator<string>;
+ /** Returns an iterator allowing to go through all values contained
+ * in this object.
+ *
+ * for (const value of searchParams.values()) {
+ * console.log(value);
+ * }
+ */
+ values(): IterableIterator<string>;
+ /** Returns an iterator allowing to go through all key/value
+ * pairs contained in this object.
+ *
+ * for (const [key, value] of searchParams.entries()) {
+ * console.log(key, value);
+ * }
+ */
+ entries(): IterableIterator<[string, string]>;
+ /** Returns an iterator allowing to go through all key/value
+ * pairs contained in this object.
+ *
+ * for (const [key, value] of searchParams[Symbol.iterator]()) {
+ * console.log(key, value);
+ * }
+ */
+ [Symbol.iterator](): IterableIterator<[string, string]>;
+ /** Returns a query string suitable for use in a URL.
+ *
+ * searchParams.toString();
+ */
+ toString(): string;
+ private _handleStringInitialization;
+ private _handleArrayInitialization;
+ }
+}
+
+declare namespace url {
+ // @url js/url.d.ts
+
+ export const blobURLMap: Map<string, domTypes.Blob>;
+ export class URL {
+ private _parts;
+ private _searchParams;
+ private _updateSearchParams;
+ hash: string;
+ host: string;
+ hostname: string;
+ href: string;
+ readonly origin: string;
+ password: string;
+ pathname: string;
+ port: string;
+ protocol: string;
+ search: string;
+ username: string;
+ readonly searchParams: urlSearchParams.URLSearchParams;
+ constructor(url: string, base?: string | URL);
+ toString(): string;
+ toJSON(): string;
+ static createObjectURL(b: domTypes.Blob): string;
+ static revokeObjectURL(url: string): void;
+ }
+}
+
+declare namespace workers {
+ // @url js/workers.d.ts
+
+ export function encodeMessage(data: any): Uint8Array;
+ export function decodeMessage(dataIntArray: Uint8Array): any;
+ export let onmessage: (e: { data: any }) => void;
+ export function postMessage(data: any): void;
+ export function getMessage(): Promise<any>;
+ export let isClosing: boolean;
+ export function workerClose(): void;
+ export function workerMain(): Promise<void>;
+ export interface Worker {
+ onerror?: () => void;
+ onmessage?: (e: { data: any }) => void;
+ onmessageerror?: () => void;
+ postMessage(data: any): void;
+ closed: Promise<void>;
+ }
+ export interface WorkerOptions {}
+ /** Extended Deno Worker initialization options.
+ * `noDenoNamespace` hides global `window.Deno` namespace for
+ * spawned worker and nested workers spawned by it (default: false).
+ */
+ export interface DenoWorkerOptions extends WorkerOptions {
+ noDenoNamespace?: boolean;
+ }
+ export class WorkerImpl implements Worker {
+ private readonly rid;
+ private isClosing;
+ private readonly isClosedPromise;
+ onerror?: () => void;
+ onmessage?: (data: any) => void;
+ onmessageerror?: () => void;
+ constructor(specifier: string, options?: DenoWorkerOptions);
+ readonly closed: Promise<void>;
+ postMessage(data: any): void;
+ private run;
+ }
+}
+
+declare namespace performanceUtil {
+ // @url js/performance.d.ts
+
+ export class Performance {
+ /** Returns a current time from Deno's start in milliseconds.
+ *
+ * Use the flag --allow-hrtime return a precise value.
+ *
+ * const t = performance.now();
+ * console.log(`${t} ms since start!`);
+ */
+ now(): number;
+ }
+}
+
+// @url js/lib.web_assembly.d.ts
+
+// This follows the WebIDL at: https://webassembly.github.io/spec/js-api/
+// And follow on WebIDL at: https://webassembly.github.io/spec/web-api/
+
+/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */
+
+declare namespace WebAssembly {
+ interface WebAssemblyInstantiatedSource {
+ module: Module;
+ instance: Instance;
+ }
+
+ /** Compiles a `WebAssembly.Module` from WebAssembly binary code. This
+ * function is useful if it is necessary to a compile a module before it can
+ * be instantiated (otherwise, the `WebAssembly.instantiate()` function
+ * should be used). */
+ function compile(bufferSource: domTypes.BufferSource): Promise<Module>;
+
+ /** Compiles a `WebAssembly.Module` directly from a streamed underlying
+ * source. This function is useful if it is necessary to a compile a module
+ * before it can be instantiated (otherwise, the
+ * `WebAssembly.instantiateStreaming()` function should be used). */
+ function compileStreaming(
+ source: Promise<domTypes.Response>
+ ): Promise<Module>;
+
+ /** Takes the WebAssembly binary code, in the form of a typed array or
+ * `ArrayBuffer`, and performs both compilation and instantiation in one step.
+ * The returned `Promise` resolves to both a compiled `WebAssembly.Module` and
+ * its first `WebAssembly.Instance`. */
+ function instantiate(
+ bufferSource: domTypes.BufferSource,
+ importObject?: object
+ ): Promise<WebAssemblyInstantiatedSource>;
+
+ /** Takes an already-compiled `WebAssembly.Module` and returns a `Promise`
+ * that resolves to an `Instance` of that `Module`. This overload is useful if
+ * the `Module` has already been compiled. */
+ function instantiate(
+ module: Module,
+ importObject?: object
+ ): Promise<Instance>;
+
+ /** Compiles and instantiates a WebAssembly module directly from a streamed
+ * underlying source. This is the most efficient, optimized way to load wasm
+ * code. */
+ function instantiateStreaming(
+ source: Promise<domTypes.Response>,
+ importObject?: object
+ ): Promise<WebAssemblyInstantiatedSource>;
+
+ /** Validates a given typed array of WebAssembly binary code, returning
+ * whether the bytes form a valid wasm module (`true`) or not (`false`). */
+ function validate(bufferSource: domTypes.BufferSource): boolean;
+
+ type ImportExportKind = "function" | "table" | "memory" | "global";
+
+ interface ModuleExportDescriptor {
+ name: string;
+ kind: ImportExportKind;
+ }
+ interface ModuleImportDescriptor {
+ module: string;
+ name: string;
+ kind: ImportExportKind;
+ }
+
+ class Module {
+ constructor(bufferSource: domTypes.BufferSource);
+
+ /** Given a `Module` and string, returns a copy of the contents of all
+ * custom sections in the module with the given string name. */
+ static customSections(
+ moduleObject: Module,
+ sectionName: string
+ ): ArrayBuffer;
+
+ /** Given a `Module`, returns an array containing descriptions of all the
+ * declared exports. */
+ static exports(moduleObject: Module): ModuleExportDescriptor[];
+
+ /** Given a `Module`, returns an array containing descriptions of all the
+ * declared imports. */
+ static imports(moduleObject: Module): ModuleImportDescriptor[];
+ }
+
+ class Instance<T extends object = { [key: string]: any }> {
+ constructor(module: Module, importObject?: object);
+
+ /** An object containing as its members all the functions exported from the
+ * WebAssembly module instance, to allow them to be accessed and used by
+ * JavaScript. */
+ readonly exports: T;
+ }
+
+ interface MemoryDescriptor {
+ initial: number;
+ maximum?: number;
+ }
+
+ class Memory {
+ constructor(descriptor: MemoryDescriptor);
+
+ /** An accessor property that returns the buffer contained in the memory. */
+ readonly buffer: ArrayBuffer;
+
+ /** Increases the size of the memory instance by a specified number of
+ * WebAssembly pages (each one is 64KB in size). */
+ grow(delta: number): number;
+ }
+
+ type TableKind = "anyfunc";
+
+ interface TableDescriptor {
+ element: TableKind;
+ initial: number;
+ maximum?: number;
+ }
+
+ class Table {
+ constructor(descriptor: TableDescriptor);
+
+ /** Returns the length of the table, i.e. the number of elements. */
+ readonly length: number;
+
+ /** Accessor function — gets the element stored at a given index. */
+ get(index: number): (...args: any[]) => any;
+
+ /** Increases the size of the Table instance by a specified number of
+ * elements. */
+ grow(delta: number): number;
+
+ /** Sets an element stored at a given index to a given value. */
+ set(index: number, value: (...args: any[]) => any): void;
+ }
+
+ interface GlobalDescriptor {
+ value: string;
+ mutable?: boolean;
+ }
+
+ /** Represents a global variable instance, accessible from both JavaScript and
+ * importable/exportable across one or more `WebAssembly.Module` instances.
+ * This allows dynamic linking of multiple modules. */
+ class Global {
+ constructor(descriptor: GlobalDescriptor, value?: any);
+
+ /** Old-style method that returns the value contained inside the global
+ * variable. */
+ valueOf(): any;
+
+ /** The value contained inside the global variable — this can be used to
+ * directly set and get the global's value. */
+ value: any;
+ }
+
+ /** Indicates an error during WebAssembly decoding or validation */
+ class CompileError extends Error {
+ constructor(message: string, fileName?: string, lineNumber?: string);
+ }
+
+ /** Indicates an error during module instantiation (besides traps from the
+ * start function). */
+ class LinkError extends Error {
+ constructor(message: string, fileName?: string, lineNumber?: string);
+ }
+
+ /** Is thrown whenever WebAssembly specifies a trap. */
+ class RuntimeError extends Error {
+ constructor(message: string, fileName?: string, lineNumber?: string);
+ }
+}
+
+/* eslint-enable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */
diff --git a/cli/js/lib.web_assembly.d.ts b/cli/js/lib.web_assembly.d.ts
new file mode 100644
index 000000000..8c357840a
--- /dev/null
+++ b/cli/js/lib.web_assembly.d.ts
@@ -0,0 +1,173 @@
+// This follows the WebIDL at: https://webassembly.github.io/spec/js-api/
+// And follow on WebIDL at: https://webassembly.github.io/spec/web-api/
+
+/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */
+
+declare namespace WebAssembly {
+ interface WebAssemblyInstantiatedSource {
+ module: Module;
+ instance: Instance;
+ }
+
+ /** Compiles a `WebAssembly.Module` from WebAssembly binary code. This
+ * function is useful if it is necessary to a compile a module before it can
+ * be instantiated (otherwise, the `WebAssembly.instantiate()` function
+ * should be used). */
+ function compile(bufferSource: domTypes.BufferSource): Promise<Module>;
+
+ /** Compiles a `WebAssembly.Module` directly from a streamed underlying
+ * source. This function is useful if it is necessary to a compile a module
+ * before it can be instantiated (otherwise, the
+ * `WebAssembly.instantiateStreaming()` function should be used). */
+ function compileStreaming(
+ source: Promise<domTypes.Response>
+ ): Promise<Module>;
+
+ /** Takes the WebAssembly binary code, in the form of a typed array or
+ * `ArrayBuffer`, and performs both compilation and instantiation in one step.
+ * The returned `Promise` resolves to both a compiled `WebAssembly.Module` and
+ * its first `WebAssembly.Instance`. */
+ function instantiate(
+ bufferSource: domTypes.BufferSource,
+ importObject?: object
+ ): Promise<WebAssemblyInstantiatedSource>;
+
+ /** Takes an already-compiled `WebAssembly.Module` and returns a `Promise`
+ * that resolves to an `Instance` of that `Module`. This overload is useful if
+ * the `Module` has already been compiled. */
+ function instantiate(
+ module: Module,
+ importObject?: object
+ ): Promise<Instance>;
+
+ /** Compiles and instantiates a WebAssembly module directly from a streamed
+ * underlying source. This is the most efficient, optimized way to load wasm
+ * code. */
+ function instantiateStreaming(
+ source: Promise<domTypes.Response>,
+ importObject?: object
+ ): Promise<WebAssemblyInstantiatedSource>;
+
+ /** Validates a given typed array of WebAssembly binary code, returning
+ * whether the bytes form a valid wasm module (`true`) or not (`false`). */
+ function validate(bufferSource: domTypes.BufferSource): boolean;
+
+ type ImportExportKind = "function" | "table" | "memory" | "global";
+
+ interface ModuleExportDescriptor {
+ name: string;
+ kind: ImportExportKind;
+ }
+ interface ModuleImportDescriptor {
+ module: string;
+ name: string;
+ kind: ImportExportKind;
+ }
+
+ class Module {
+ constructor(bufferSource: domTypes.BufferSource);
+
+ /** Given a `Module` and string, returns a copy of the contents of all
+ * custom sections in the module with the given string name. */
+ static customSections(
+ moduleObject: Module,
+ sectionName: string
+ ): ArrayBuffer;
+
+ /** Given a `Module`, returns an array containing descriptions of all the
+ * declared exports. */
+ static exports(moduleObject: Module): ModuleExportDescriptor[];
+
+ /** Given a `Module`, returns an array containing descriptions of all the
+ * declared imports. */
+ static imports(moduleObject: Module): ModuleImportDescriptor[];
+ }
+
+ class Instance<T extends object = { [key: string]: any }> {
+ constructor(module: Module, importObject?: object);
+
+ /** An object containing as its members all the functions exported from the
+ * WebAssembly module instance, to allow them to be accessed and used by
+ * JavaScript. */
+ readonly exports: T;
+ }
+
+ interface MemoryDescriptor {
+ initial: number;
+ maximum?: number;
+ }
+
+ class Memory {
+ constructor(descriptor: MemoryDescriptor);
+
+ /** An accessor property that returns the buffer contained in the memory. */
+ readonly buffer: ArrayBuffer;
+
+ /** Increases the size of the memory instance by a specified number of
+ * WebAssembly pages (each one is 64KB in size). */
+ grow(delta: number): number;
+ }
+
+ type TableKind = "anyfunc";
+
+ interface TableDescriptor {
+ element: TableKind;
+ initial: number;
+ maximum?: number;
+ }
+
+ class Table {
+ constructor(descriptor: TableDescriptor);
+
+ /** Returns the length of the table, i.e. the number of elements. */
+ readonly length: number;
+
+ /** Accessor function — gets the element stored at a given index. */
+ get(index: number): (...args: any[]) => any;
+
+ /** Increases the size of the Table instance by a specified number of
+ * elements. */
+ grow(delta: number): number;
+
+ /** Sets an element stored at a given index to a given value. */
+ set(index: number, value: (...args: any[]) => any): void;
+ }
+
+ interface GlobalDescriptor {
+ value: string;
+ mutable?: boolean;
+ }
+
+ /** Represents a global variable instance, accessible from both JavaScript and
+ * importable/exportable across one or more `WebAssembly.Module` instances.
+ * This allows dynamic linking of multiple modules. */
+ class Global {
+ constructor(descriptor: GlobalDescriptor, value?: any);
+
+ /** Old-style method that returns the value contained inside the global
+ * variable. */
+ valueOf(): any;
+
+ /** The value contained inside the global variable — this can be used to
+ * directly set and get the global's value. */
+ value: any;
+ }
+
+ /** Indicates an error during WebAssembly decoding or validation */
+ class CompileError extends Error {
+ constructor(message: string, fileName?: string, lineNumber?: string);
+ }
+
+ /** Indicates an error during module instantiation (besides traps from the
+ * start function). */
+ class LinkError extends Error {
+ constructor(message: string, fileName?: string, lineNumber?: string);
+ }
+
+ /** Is thrown whenever WebAssembly specifies a trap. */
+ class RuntimeError extends Error {
+ constructor(message: string, fileName?: string, lineNumber?: string);
+ }
+}
+
+/* eslint-enable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any */
diff --git a/cli/js/link.ts b/cli/js/link.ts
new file mode 100644
index 000000000..a6f732926
--- /dev/null
+++ b/cli/js/link.ts
@@ -0,0 +1,19 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+/** Synchronously creates `newname` as a hard link to `oldname`.
+ *
+ * Deno.linkSync("old/name", "new/name");
+ */
+export function linkSync(oldname: string, newname: string): void {
+ sendSync(dispatch.OP_LINK, { oldname, newname });
+}
+
+/** Creates `newname` as a hard link to `oldname`.
+ *
+ * await Deno.link("old/name", "new/name");
+ */
+export async function link(oldname: string, newname: string): Promise<void> {
+ await sendAsync(dispatch.OP_LINK, { oldname, newname });
+}
diff --git a/cli/js/link_test.ts b/cli/js/link_test.ts
new file mode 100644
index 000000000..9425e6eab
--- /dev/null
+++ b/cli/js/link_test.ts
@@ -0,0 +1,115 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+testPerm({ read: true, write: true }, function linkSyncSuccess(): void {
+ const testDir = Deno.makeTempDirSync();
+ const oldData = "Hardlink";
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+ Deno.writeFileSync(oldName, new TextEncoder().encode(oldData));
+ // Create the hard link.
+ Deno.linkSync(oldName, newName);
+ // We should expect reading the same content.
+ const newData = new TextDecoder().decode(Deno.readFileSync(newName));
+ assertEquals(oldData, newData);
+ // Writing to newname also affects oldname.
+ const newData2 = "Modified";
+ Deno.writeFileSync(newName, new TextEncoder().encode(newData2));
+ assertEquals(newData2, new TextDecoder().decode(Deno.readFileSync(oldName)));
+ // Writing to oldname also affects newname.
+ const newData3 = "ModifiedAgain";
+ Deno.writeFileSync(oldName, new TextEncoder().encode(newData3));
+ assertEquals(newData3, new TextDecoder().decode(Deno.readFileSync(newName)));
+ // Remove oldname. File still accessible through newname.
+ Deno.removeSync(oldName);
+ const newNameStat = Deno.statSync(newName);
+ assert(newNameStat.isFile());
+ assert(!newNameStat.isSymlink()); // Not a symlink.
+ assertEquals(newData3, new TextDecoder().decode(Deno.readFileSync(newName)));
+});
+
+testPerm({ read: true, write: true }, function linkSyncExists(): void {
+ const testDir = Deno.makeTempDirSync();
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+ Deno.writeFileSync(oldName, new TextEncoder().encode("oldName"));
+ // newname is already created.
+ Deno.writeFileSync(newName, new TextEncoder().encode("newName"));
+
+ let err;
+ try {
+ Deno.linkSync(oldName, newName);
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.AlreadyExists);
+ assertEquals(err.name, "AlreadyExists");
+});
+
+testPerm({ read: true, write: true }, function linkSyncNotFound(): void {
+ const testDir = Deno.makeTempDirSync();
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+
+ let err;
+ try {
+ Deno.linkSync(oldName, newName);
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ read: false, write: true }, function linkSyncReadPerm(): void {
+ let err;
+ try {
+ Deno.linkSync("oldbaddir", "newbaddir");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ read: true, write: false }, function linkSyncWritePerm(): void {
+ let err;
+ try {
+ Deno.linkSync("oldbaddir", "newbaddir");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ read: true, write: true }, async function linkSuccess(): Promise<
+ void
+> {
+ const testDir = Deno.makeTempDirSync();
+ const oldData = "Hardlink";
+ const oldName = testDir + "/oldname";
+ const newName = testDir + "/newname";
+ Deno.writeFileSync(oldName, new TextEncoder().encode(oldData));
+ // Create the hard link.
+ await Deno.link(oldName, newName);
+ // We should expect reading the same content.
+ const newData = new TextDecoder().decode(Deno.readFileSync(newName));
+ assertEquals(oldData, newData);
+ // Writing to newname also affects oldname.
+ const newData2 = "Modified";
+ Deno.writeFileSync(newName, new TextEncoder().encode(newData2));
+ assertEquals(newData2, new TextDecoder().decode(Deno.readFileSync(oldName)));
+ // Writing to oldname also affects newname.
+ const newData3 = "ModifiedAgain";
+ Deno.writeFileSync(oldName, new TextEncoder().encode(newData3));
+ assertEquals(newData3, new TextDecoder().decode(Deno.readFileSync(newName)));
+ // Remove oldname. File still accessible through newname.
+ Deno.removeSync(oldName);
+ const newNameStat = Deno.statSync(newName);
+ assert(newNameStat.isFile());
+ assert(!newNameStat.isSymlink()); // Not a symlink.
+ assertEquals(newData3, new TextDecoder().decode(Deno.readFileSync(newName)));
+});
diff --git a/cli/js/location.ts b/cli/js/location.ts
new file mode 100644
index 000000000..d495f99ca
--- /dev/null
+++ b/cli/js/location.ts
@@ -0,0 +1,52 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { URL } from "./url.ts";
+import { notImplemented } from "./util.ts";
+import { Location } from "./dom_types.ts";
+import { window } from "./window.ts";
+
+export class LocationImpl implements Location {
+ constructor(url: string) {
+ const u = new URL(url);
+ this.url = u;
+ this.hash = u.hash;
+ this.host = u.host;
+ this.href = u.href;
+ this.hostname = u.hostname;
+ this.origin = u.protocol + "//" + u.host;
+ this.pathname = u.pathname;
+ this.protocol = u.protocol;
+ this.port = u.port;
+ this.search = u.search;
+ }
+
+ private url: URL;
+
+ toString(): string {
+ return this.url.toString();
+ }
+
+ readonly ancestorOrigins: string[] = [];
+ hash: string;
+ host: string;
+ hostname: string;
+ href: string;
+ readonly origin: string;
+ pathname: string;
+ port: string;
+ protocol: string;
+ search: string;
+ assign(_url: string): void {
+ throw notImplemented();
+ }
+ reload(): void {
+ throw notImplemented();
+ }
+ replace(_url: string): void {
+ throw notImplemented();
+ }
+}
+
+export function setLocation(url: string): void {
+ window.location = new LocationImpl(url);
+ Object.freeze(window.location);
+}
diff --git a/cli/js/location_test.ts b/cli/js/location_test.ts
new file mode 100644
index 000000000..c8daab16d
--- /dev/null
+++ b/cli/js/location_test.ts
@@ -0,0 +1,8 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert } from "./test_util.ts";
+
+test(function locationBasic(): void {
+ // location example: file:///Users/rld/src/deno/js/unit_tests.ts
+ console.log("location", window.location.toString());
+ assert(window.location.toString().endsWith("unit_tests.ts"));
+});
diff --git a/cli/js/main.ts b/cli/js/main.ts
new file mode 100644
index 000000000..09e7ce453
--- /dev/null
+++ b/cli/js/main.ts
@@ -0,0 +1,41 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import "./globals.ts";
+
+import { assert, log } from "./util.ts";
+import * as os from "./os.ts";
+import { args } from "./deno.ts";
+import { setPrepareStackTrace } from "./error_stack.ts";
+import { replLoop } from "./repl.ts";
+import { setVersions } from "./version.ts";
+import { window } from "./window.ts";
+import { setLocation } from "./location.ts";
+import { setBuildInfo } from "./build.ts";
+import { setSignals } from "./process.ts";
+
+function denoMain(preserveDenoNamespace = true, name?: string): void {
+ const s = os.start(preserveDenoNamespace, name);
+
+ setBuildInfo(s.os, s.arch);
+ setSignals();
+ setVersions(s.denoVersion, s.v8Version, s.tsVersion);
+
+ setPrepareStackTrace(Error);
+
+ if (s.mainModule) {
+ assert(s.mainModule.length > 0);
+ setLocation(s.mainModule);
+ }
+
+ log("cwd", s.cwd);
+
+ for (let i = 1; i < s.argv.length; i++) {
+ args.push(s.argv[i]);
+ }
+ log("args", args);
+ Object.freeze(args);
+
+ if (!s.mainModule) {
+ replLoop();
+ }
+}
+window["denoMain"] = denoMain;
diff --git a/cli/js/make_temp_dir.ts b/cli/js/make_temp_dir.ts
new file mode 100644
index 000000000..14494b5da
--- /dev/null
+++ b/cli/js/make_temp_dir.ts
@@ -0,0 +1,35 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+export interface MakeTempDirOptions {
+ dir?: string;
+ prefix?: string;
+ suffix?: string;
+}
+
+/** makeTempDirSync is the synchronous version of `makeTempDir`.
+ *
+ * const tempDirName0 = Deno.makeTempDirSync();
+ * const tempDirName1 = Deno.makeTempDirSync({ prefix: 'my_temp' });
+ */
+export function makeTempDirSync(options: MakeTempDirOptions = {}): string {
+ return sendSync(dispatch.OP_MAKE_TEMP_DIR, options);
+}
+
+/** makeTempDir creates a new temporary directory in the directory `dir`, its
+ * name beginning with `prefix` and ending with `suffix`.
+ * It returns the full path to the newly created directory.
+ * If `dir` is unspecified, tempDir uses the default directory for temporary
+ * files. Multiple programs calling tempDir simultaneously will not choose the
+ * same directory. It is the caller's responsibility to remove the directory
+ * when no longer needed.
+ *
+ * const tempDirName0 = await Deno.makeTempDir();
+ * const tempDirName1 = await Deno.makeTempDir({ prefix: 'my_temp' });
+ */
+export async function makeTempDir(
+ options: MakeTempDirOptions = {}
+): Promise<string> {
+ return await sendAsync(dispatch.OP_MAKE_TEMP_DIR, options);
+}
diff --git a/cli/js/make_temp_dir_test.ts b/cli/js/make_temp_dir_test.ts
new file mode 100644
index 000000000..aa44b65c5
--- /dev/null
+++ b/cli/js/make_temp_dir_test.ts
@@ -0,0 +1,66 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, testPerm, assert, assertEquals } from "./test_util.ts";
+
+testPerm({ write: true }, function makeTempDirSyncSuccess(): void {
+ const dir1 = Deno.makeTempDirSync({ prefix: "hello", suffix: "world" });
+ const dir2 = Deno.makeTempDirSync({ prefix: "hello", suffix: "world" });
+ // Check that both dirs are different.
+ assert(dir1 !== dir2);
+ for (const dir of [dir1, dir2]) {
+ // Check that the prefix and suffix are applied.
+ const lastPart = dir.replace(/^.*[\\\/]/, "");
+ assert(lastPart.startsWith("hello"));
+ assert(lastPart.endsWith("world"));
+ }
+ // Check that the `dir` option works.
+ const dir3 = Deno.makeTempDirSync({ dir: dir1 });
+ assert(dir3.startsWith(dir1));
+ assert(/^[\\\/]/.test(dir3.slice(dir1.length)));
+ // Check that creating a temp dir inside a nonexisting directory fails.
+ let err;
+ try {
+ Deno.makeTempDirSync({ dir: "/baddir" });
+ } catch (err_) {
+ err = err_;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+test(function makeTempDirSyncPerm(): void {
+ // makeTempDirSync should require write permissions (for now).
+ let err;
+ try {
+ Deno.makeTempDirSync({ dir: "/baddir" });
+ } catch (err_) {
+ err = err_;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ write: true }, async function makeTempDirSuccess(): Promise<void> {
+ const dir1 = await Deno.makeTempDir({ prefix: "hello", suffix: "world" });
+ const dir2 = await Deno.makeTempDir({ prefix: "hello", suffix: "world" });
+ // Check that both dirs are different.
+ assert(dir1 !== dir2);
+ for (const dir of [dir1, dir2]) {
+ // Check that the prefix and suffix are applied.
+ const lastPart = dir.replace(/^.*[\\\/]/, "");
+ assert(lastPart.startsWith("hello"));
+ assert(lastPart.endsWith("world"));
+ }
+ // Check that the `dir` option works.
+ const dir3 = await Deno.makeTempDir({ dir: dir1 });
+ assert(dir3.startsWith(dir1));
+ assert(/^[\\\/]/.test(dir3.slice(dir1.length)));
+ // Check that creating a temp dir inside a nonexisting directory fails.
+ let err;
+ try {
+ await Deno.makeTempDir({ dir: "/baddir" });
+ } catch (err_) {
+ err = err_;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
diff --git a/cli/js/metrics.ts b/cli/js/metrics.ts
new file mode 100644
index 000000000..b32c29789
--- /dev/null
+++ b/cli/js/metrics.ts
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as dispatch from "./dispatch.ts";
+import { sendSync } from "./dispatch_json.ts";
+
+export interface Metrics {
+ opsDispatched: number;
+ opsCompleted: number;
+ bytesSentControl: number;
+ bytesSentData: number;
+ bytesReceived: number;
+}
+
+/** Receive metrics from the privileged side of Deno.
+ *
+ * > console.table(Deno.metrics())
+ * ┌──────────────────┬────────┐
+ * │ (index) │ Values │
+ * ├──────────────────┼────────┤
+ * │ opsDispatched │ 9 │
+ * │ opsCompleted │ 9 │
+ * │ bytesSentControl │ 504 │
+ * │ bytesSentData │ 0 │
+ * │ bytesReceived │ 856 │
+ * └──────────────────┴────────┘
+ */
+export function metrics(): Metrics {
+ return sendSync(dispatch.OP_METRICS);
+}
diff --git a/cli/js/metrics_test.ts b/cli/js/metrics_test.ts
new file mode 100644
index 000000000..de41a0cb1
--- /dev/null
+++ b/cli/js/metrics_test.ts
@@ -0,0 +1,46 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, testPerm, assert } from "./test_util.ts";
+
+test(async function metrics(): Promise<void> {
+ const m1 = Deno.metrics();
+ assert(m1.opsDispatched > 0);
+ assert(m1.opsCompleted > 0);
+ assert(m1.bytesSentControl > 0);
+ assert(m1.bytesSentData >= 0);
+ assert(m1.bytesReceived > 0);
+
+ // Write to stdout to ensure a "data" message gets sent instead of just
+ // control messages.
+ const dataMsg = new Uint8Array([41, 42, 43]);
+ await Deno.stdout.write(dataMsg);
+
+ const m2 = Deno.metrics();
+ assert(m2.opsDispatched > m1.opsDispatched);
+ assert(m2.opsCompleted > m1.opsCompleted);
+ assert(m2.bytesSentControl > m1.bytesSentControl);
+ assert(m2.bytesSentData >= m1.bytesSentData + dataMsg.byteLength);
+ assert(m2.bytesReceived > m1.bytesReceived);
+});
+
+testPerm({ write: true }, function metricsUpdatedIfNoResponseSync(): void {
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+
+ const data = new Uint8Array([41, 42, 43]);
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+
+ const metrics = Deno.metrics();
+ assert(metrics.opsDispatched === metrics.opsCompleted);
+});
+
+testPerm(
+ { write: true },
+ async function metricsUpdatedIfNoResponseAsync(): Promise<void> {
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+
+ const data = new Uint8Array([41, 42, 43]);
+ await Deno.writeFile(filename, data, { perm: 0o666 });
+
+ const metrics = Deno.metrics();
+ assert(metrics.opsDispatched === metrics.opsCompleted);
+ }
+);
diff --git a/cli/js/mixins/dom_iterable.ts b/cli/js/mixins/dom_iterable.ts
new file mode 100644
index 000000000..bbd1905ce
--- /dev/null
+++ b/cli/js/mixins/dom_iterable.ts
@@ -0,0 +1,82 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import { DomIterable } from "../dom_types.ts";
+import { window } from "../window.ts";
+import { requiredArguments } from "../util.ts";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type Constructor<T = {}> = new (...args: any[]) => T;
+
+/** Mixes in a DOM iterable methods into a base class, assumes that there is
+ * a private data iterable that is part of the base class, located at
+ * `[dataSymbol]`.
+ * TODO Don't expose DomIterableMixin from "deno" namespace.
+ */
+export function DomIterableMixin<K, V, TBase extends Constructor>(
+ Base: TBase,
+ dataSymbol: symbol
+): TBase & Constructor<DomIterable<K, V>> {
+ // we have to cast `this` as `any` because there is no way to describe the
+ // Base class in a way where the Symbol `dataSymbol` is defined. So the
+ // runtime code works, but we do lose a little bit of type safety.
+
+ // Additionally, we have to not use .keys() nor .values() since the internal
+ // slot differs in type - some have a Map, which yields [K, V] in
+ // Symbol.iterator, and some have an Array, which yields V, in this case
+ // [K, V] too as they are arrays of tuples.
+
+ const DomIterable = class extends Base {
+ *entries(): IterableIterator<[K, V]> {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ for (const entry of (this as any)[dataSymbol]) {
+ yield entry;
+ }
+ }
+
+ *keys(): IterableIterator<K> {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ for (const [key] of (this as any)[dataSymbol]) {
+ yield key;
+ }
+ }
+
+ *values(): IterableIterator<V> {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ for (const [, value] of (this as any)[dataSymbol]) {
+ yield value;
+ }
+ }
+
+ forEach(
+ callbackfn: (value: V, key: K, parent: this) => void,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ thisArg?: any
+ ): void {
+ requiredArguments(
+ `${this.constructor.name}.forEach`,
+ arguments.length,
+ 1
+ );
+ callbackfn = callbackfn.bind(thisArg == null ? window : Object(thisArg));
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ for (const [key, value] of (this as any)[dataSymbol]) {
+ callbackfn(value, key, this);
+ }
+ }
+
+ *[Symbol.iterator](): IterableIterator<[K, V]> {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ for (const entry of (this as any)[dataSymbol]) {
+ yield entry;
+ }
+ }
+ };
+
+ // we want the Base class name to be the name of the class.
+ Object.defineProperty(DomIterable, "name", {
+ value: Base.name,
+ configurable: true
+ });
+
+ return DomIterable;
+}
diff --git a/cli/js/mixins/dom_iterable_test.ts b/cli/js/mixins/dom_iterable_test.ts
new file mode 100644
index 000000000..4c84fa68e
--- /dev/null
+++ b/cli/js/mixins/dom_iterable_test.ts
@@ -0,0 +1,79 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEquals } from "../test_util.ts";
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+function setup() {
+ const dataSymbol = Symbol("data symbol");
+ class Base {
+ private [dataSymbol] = new Map<string, number>();
+
+ constructor(
+ data: Array<[string, number]> | IterableIterator<[string, number]>
+ ) {
+ for (const [key, value] of data) {
+ this[dataSymbol].set(key, value);
+ }
+ }
+ }
+
+ return {
+ Base,
+ // This is using an internal API we don't want published as types, so having
+ // to cast to any to "trick" TypeScript
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ DomIterable: (Deno as any).DomIterableMixin(Base, dataSymbol)
+ };
+}
+
+test(function testDomIterable(): void {
+ const { DomIterable, Base } = setup();
+
+ const fixture: Array<[string, number]> = [["foo", 1], ["bar", 2]];
+
+ const domIterable = new DomIterable(fixture);
+
+ assertEquals(Array.from(domIterable.entries()), fixture);
+ assertEquals(Array.from(domIterable.values()), [1, 2]);
+ assertEquals(Array.from(domIterable.keys()), ["foo", "bar"]);
+
+ let result: Array<[string, number]> = [];
+ for (const [key, value] of domIterable) {
+ assert(key != null);
+ assert(value != null);
+ result.push([key, value]);
+ }
+ assertEquals(fixture, result);
+
+ result = [];
+ const scope = {};
+ function callback(value, key, parent): void {
+ assertEquals(parent, domIterable);
+ assert(key != null);
+ assert(value != null);
+ assert(this === scope);
+ result.push([key, value]);
+ }
+ domIterable.forEach(callback, scope);
+ assertEquals(fixture, result);
+
+ assertEquals(DomIterable.name, Base.name);
+});
+
+test(function testDomIterableScope(): void {
+ const { DomIterable } = setup();
+
+ const domIterable = new DomIterable([["foo", 1]]);
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ function checkScope(thisArg: any, expected: any): void {
+ function callback(): void {
+ assertEquals(this, expected);
+ }
+ domIterable.forEach(callback, thisArg);
+ }
+
+ checkScope(0, Object(0));
+ checkScope("", Object(""));
+ checkScope(null, window);
+ checkScope(undefined, window);
+});
diff --git a/cli/js/mkdir.ts b/cli/js/mkdir.ts
new file mode 100644
index 000000000..bc09ba358
--- /dev/null
+++ b/cli/js/mkdir.ts
@@ -0,0 +1,33 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+/** Creates a new directory with the specified path synchronously.
+ * If `recursive` is set to true, nested directories will be created (also known
+ * as "mkdir -p").
+ * `mode` sets permission bits (before umask) on UNIX and does nothing on
+ * Windows.
+ *
+ * Deno.mkdirSync("new_dir");
+ * Deno.mkdirSync("nested/directories", true);
+ */
+export function mkdirSync(path: string, recursive = false, mode = 0o777): void {
+ sendSync(dispatch.OP_MKDIR, { path, recursive, mode });
+}
+
+/** Creates a new directory with the specified path.
+ * If `recursive` is set to true, nested directories will be created (also known
+ * as "mkdir -p").
+ * `mode` sets permission bits (before umask) on UNIX and does nothing on
+ * Windows.
+ *
+ * await Deno.mkdir("new_dir");
+ * await Deno.mkdir("nested/directories", true);
+ */
+export async function mkdir(
+ path: string,
+ recursive = false,
+ mode = 0o777
+): Promise<void> {
+ await sendAsync(dispatch.OP_MKDIR, { path, recursive, mode });
+}
diff --git a/cli/js/mkdir_test.ts b/cli/js/mkdir_test.ts
new file mode 100644
index 000000000..9e97265f0
--- /dev/null
+++ b/cli/js/mkdir_test.ts
@@ -0,0 +1,66 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+testPerm({ read: true, write: true }, function mkdirSyncSuccess(): void {
+ const path = Deno.makeTempDirSync() + "/dir";
+ Deno.mkdirSync(path);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory());
+});
+
+testPerm({ read: true, write: true }, function mkdirSyncMode(): void {
+ const path = Deno.makeTempDirSync() + "/dir";
+ Deno.mkdirSync(path, false, 0o755); // no perm for x
+ const pathInfo = Deno.statSync(path);
+ if (pathInfo.mode !== null) {
+ // Skip windows
+ assertEquals(pathInfo.mode & 0o777, 0o755);
+ }
+});
+
+testPerm({ write: false }, function mkdirSyncPerm(): void {
+ let err;
+ try {
+ Deno.mkdirSync("/baddir");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ read: true, write: true }, async function mkdirSuccess(): Promise<
+ void
+> {
+ const path = Deno.makeTempDirSync() + "/dir";
+ await Deno.mkdir(path);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory());
+});
+
+testPerm({ write: true }, function mkdirErrIfExists(): void {
+ let err;
+ try {
+ Deno.mkdirSync(".");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.AlreadyExists);
+ assertEquals(err.name, "AlreadyExists");
+});
+
+testPerm({ read: true, write: true }, function mkdirSyncRecursive(): void {
+ const path = Deno.makeTempDirSync() + "/nested/directory";
+ Deno.mkdirSync(path, true);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory());
+});
+
+testPerm({ read: true, write: true }, async function mkdirRecursive(): Promise<
+ void
+> {
+ const path = Deno.makeTempDirSync() + "/nested/directory";
+ await Deno.mkdir(path, true);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory());
+});
diff --git a/cli/js/mock_builtin.js b/cli/js/mock_builtin.js
new file mode 100644
index 000000000..9c6730d69
--- /dev/null
+++ b/cli/js/mock_builtin.js
@@ -0,0 +1,2 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+export default undefined;
diff --git a/cli/js/net.ts b/cli/js/net.ts
new file mode 100644
index 000000000..a7ad2b73c
--- /dev/null
+++ b/cli/js/net.ts
@@ -0,0 +1,205 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { EOF, Reader, Writer, Closer } from "./io.ts";
+import { notImplemented } from "./util.ts";
+import { read, write, close } from "./files.ts";
+import * as dispatch from "./dispatch.ts";
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+
+export type Transport = "tcp";
+// TODO support other types:
+// export type Transport = "tcp" | "tcp4" | "tcp6" | "unix" | "unixpacket";
+
+// TODO(ry) Replace 'address' with 'hostname' and 'port', similar to DialOptions
+// and ListenOptions.
+export interface Addr {
+ transport: Transport;
+ address: string;
+}
+
+/** A Listener is a generic transport listener for stream-oriented protocols. */
+export interface Listener extends AsyncIterator<Conn> {
+ /** Waits for and resolves to the next connection to the `Listener`. */
+ accept(): Promise<Conn>;
+
+ /** Close closes the listener. Any pending accept promises will be rejected
+ * with errors.
+ */
+ close(): void;
+
+ /** Return the address of the `Listener`. */
+ addr(): Addr;
+
+ [Symbol.asyncIterator](): AsyncIterator<Conn>;
+}
+
+enum ShutdownMode {
+ // See http://man7.org/linux/man-pages/man2/shutdown.2.html
+ // Corresponding to SHUT_RD, SHUT_WR, SHUT_RDWR
+ Read = 0,
+ Write,
+ ReadWrite // unused
+}
+
+function shutdown(rid: number, how: ShutdownMode): void {
+ sendSync(dispatch.OP_SHUTDOWN, { rid, how });
+}
+
+export class ConnImpl implements Conn {
+ constructor(
+ readonly rid: number,
+ readonly remoteAddr: string,
+ readonly localAddr: string
+ ) {}
+
+ write(p: Uint8Array): Promise<number> {
+ return write(this.rid, p);
+ }
+
+ read(p: Uint8Array): Promise<number | EOF> {
+ return read(this.rid, p);
+ }
+
+ close(): void {
+ close(this.rid);
+ }
+
+ /** closeRead shuts down (shutdown(2)) the reading side of the TCP connection.
+ * Most callers should just use close().
+ */
+ closeRead(): void {
+ shutdown(this.rid, ShutdownMode.Read);
+ }
+
+ /** closeWrite shuts down (shutdown(2)) the writing side of the TCP
+ * connection. Most callers should just use close().
+ */
+ closeWrite(): void {
+ shutdown(this.rid, ShutdownMode.Write);
+ }
+}
+
+class ListenerImpl implements Listener {
+ constructor(
+ readonly rid: number,
+ private transport: Transport,
+ private localAddr: string
+ ) {}
+
+ async accept(): Promise<Conn> {
+ const res = await sendAsync(dispatch.OP_ACCEPT, { rid: this.rid });
+ return new ConnImpl(res.rid, res.remoteAddr, res.localAddr);
+ }
+
+ close(): void {
+ close(this.rid);
+ }
+
+ addr(): Addr {
+ return {
+ transport: this.transport,
+ address: this.localAddr
+ };
+ }
+
+ async next(): Promise<IteratorResult<Conn>> {
+ return {
+ done: false,
+ value: await this.accept()
+ };
+ }
+
+ [Symbol.asyncIterator](): AsyncIterator<Conn> {
+ return this;
+ }
+}
+
+export interface Conn extends Reader, Writer, Closer {
+ /** The local address of the connection. */
+ localAddr: string;
+ /** The remote address of the connection. */
+ remoteAddr: string;
+ /** The resource ID of the connection. */
+ rid: number;
+ /** Shuts down (`shutdown(2)`) the reading side of the TCP connection. Most
+ * callers should just use `close()`.
+ */
+ closeRead(): void;
+ /** Shuts down (`shutdown(2)`) the writing side of the TCP connection. Most
+ * callers should just use `close()`.
+ */
+ closeWrite(): void;
+}
+
+export interface ListenOptions {
+ port: number;
+ hostname?: string;
+ transport?: Transport;
+}
+
+/** Listen announces on the local transport address.
+ *
+ * @param options
+ * @param options.port The port to connect to. (Required.)
+ * @param options.hostname A literal IP address or host name that can be
+ * resolved to an IP address. If not specified, defaults to 0.0.0.0
+ * @param options.transport Defaults to "tcp". Later we plan to add "tcp4",
+ * "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6", "unix", "unixgram" and
+ * "unixpacket".
+ *
+ * Examples:
+ *
+ * listen({ port: 80 })
+ * listen({ hostname: "192.0.2.1", port: 80 })
+ * listen({ hostname: "[2001:db8::1]", port: 80 });
+ * listen({ hostname: "golang.org", port: 80, transport: "tcp" })
+ */
+export function listen(options: ListenOptions): Listener {
+ const hostname = options.hostname || "0.0.0.0";
+ const transport = options.transport || "tcp";
+ const res = sendSync(dispatch.OP_LISTEN, {
+ hostname,
+ port: options.port,
+ transport
+ });
+ return new ListenerImpl(res.rid, transport, res.localAddr);
+}
+
+export interface DialOptions {
+ port: number;
+ hostname?: string;
+ transport?: Transport;
+}
+
+/** Dial connects to the address on the named transport.
+ *
+ * @param options
+ * @param options.port The port to connect to. (Required.)
+ * @param options.hostname A literal IP address or host name that can be
+ * resolved to an IP address. If not specified, defaults to 127.0.0.1
+ * @param options.transport Defaults to "tcp". Later we plan to add "tcp4",
+ * "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6", "unix", "unixgram" and
+ * "unixpacket".
+ *
+ * Examples:
+ *
+ * dial({ port: 80 })
+ * dial({ hostname: "192.0.2.1", port: 80 })
+ * dial({ hostname: "[2001:db8::1]", port: 80 });
+ * dial({ hostname: "golang.org", port: 80, transport: "tcp" })
+ */
+export async function dial(options: DialOptions): Promise<Conn> {
+ const res = await sendAsync(dispatch.OP_DIAL, {
+ hostname: options.hostname || "127.0.0.1",
+ port: options.port,
+ transport: options.transport || "tcp"
+ });
+ return new ConnImpl(res.rid, res.remoteAddr!, res.localAddr!);
+}
+
+/** **RESERVED** */
+export async function connect(
+ _transport: Transport,
+ _address: string
+): Promise<Conn> {
+ return notImplemented();
+}
diff --git a/cli/js/net_test.ts b/cli/js/net_test.ts
new file mode 100644
index 000000000..33f4f7d07
--- /dev/null
+++ b/cli/js/net_test.ts
@@ -0,0 +1,229 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+testPerm({ net: true }, function netListenClose(): void {
+ const listener = Deno.listen({ hostname: "127.0.0.1", port: 4500 });
+ const addr = listener.addr();
+ assertEquals(addr.transport, "tcp");
+ // TODO(ry) Replace 'address' with 'hostname' and 'port', similar to
+ // DialOptions and ListenOptions.
+ assertEquals(addr.address, "127.0.0.1:4500");
+ listener.close();
+});
+
+testPerm({ net: true }, async function netCloseWhileAccept(): Promise<void> {
+ const listener = Deno.listen({ port: 4501 });
+ const p = listener.accept();
+ listener.close();
+ let err;
+ try {
+ await p;
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.Other);
+ assertEquals(err.message, "Listener has been closed");
+});
+
+testPerm({ net: true }, async function netConcurrentAccept(): Promise<void> {
+ const listener = Deno.listen({ port: 4502 });
+ let acceptErrCount = 0;
+ const checkErr = (e): void => {
+ assertEquals(e.kind, Deno.ErrorKind.Other);
+ if (e.message === "Listener has been closed") {
+ assertEquals(acceptErrCount, 1);
+ } else if (e.message === "Another accept task is ongoing") {
+ acceptErrCount++;
+ } else {
+ throw new Error("Unexpected error message");
+ }
+ };
+ const p = listener.accept().catch(checkErr);
+ const p1 = listener.accept().catch(checkErr);
+ await Promise.race([p, p1]);
+ listener.close();
+ await [p, p1];
+ assertEquals(acceptErrCount, 1);
+});
+
+testPerm({ net: true }, async function netDialListen(): Promise<void> {
+ const listener = Deno.listen({ port: 4500 });
+ listener.accept().then(
+ async (conn): Promise<void> => {
+ assert(conn.remoteAddr != null);
+ assertEquals(conn.localAddr, "127.0.0.1:4500");
+ await conn.write(new Uint8Array([1, 2, 3]));
+ conn.close();
+ }
+ );
+ const conn = await Deno.dial({ hostname: "127.0.0.1", port: 4500 });
+ assertEquals(conn.remoteAddr, "127.0.0.1:4500");
+ assert(conn.localAddr != null);
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(1, buf[0]);
+ assertEquals(2, buf[1]);
+ assertEquals(3, buf[2]);
+ assert(conn.rid > 0);
+
+ assert(readResult !== Deno.EOF);
+
+ const readResult2 = await conn.read(buf);
+ assertEquals(Deno.EOF, readResult2);
+
+ listener.close();
+ conn.close();
+});
+
+/* TODO(ry) Re-enable this test.
+testPerm({ net: true }, async function netListenAsyncIterator(): Promise<void> {
+ const listener = Deno.listen(":4500");
+ const runAsyncIterator = async (): Promise<void> => {
+ for await (let conn of listener) {
+ await conn.write(new Uint8Array([1, 2, 3]));
+ conn.close();
+ }
+ };
+ runAsyncIterator();
+ const conn = await Deno.dial("127.0.0.1:4500");
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(1, buf[0]);
+ assertEquals(2, buf[1]);
+ assertEquals(3, buf[2]);
+ assert(conn.rid > 0);
+
+ assert(readResult !== Deno.EOF);
+
+ const readResult2 = await conn.read(buf);
+ assertEquals(Deno.EOF, readResult2);
+
+ listener.close();
+ conn.close();
+});
+ */
+
+/* TODO Fix broken test.
+testPerm({ net: true }, async function netCloseReadSuccess() {
+ const addr = "127.0.0.1:4500";
+ const listener = Deno.listen(addr);
+ const closeDeferred = deferred();
+ const closeReadDeferred = deferred();
+ listener.accept().then(async conn => {
+ await closeReadDeferred.promise;
+ await conn.write(new Uint8Array([1, 2, 3]));
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(4, buf[0]);
+ assertEquals(5, buf[1]);
+ assertEquals(6, buf[2]);
+ conn.close();
+ closeDeferred.resolve();
+ });
+ const conn = await Deno.dial(addr);
+ conn.closeRead(); // closing read
+ closeReadDeferred.resolve();
+ const buf = new Uint8Array(1024);
+ const readResult = await conn.read(buf);
+ assertEquals(Deno.EOF, readResult); // with immediate EOF
+ // Ensure closeRead does not impact write
+ await conn.write(new Uint8Array([4, 5, 6]));
+ await closeDeferred.promise;
+ listener.close();
+ conn.close();
+});
+*/
+
+/* TODO Fix broken test.
+testPerm({ net: true }, async function netDoubleCloseRead() {
+ const addr = "127.0.0.1:4500";
+ const listener = Deno.listen(addr);
+ const closeDeferred = deferred();
+ listener.accept().then(async conn => {
+ await conn.write(new Uint8Array([1, 2, 3]));
+ await closeDeferred.promise;
+ conn.close();
+ });
+ const conn = await Deno.dial(addr);
+ conn.closeRead(); // closing read
+ let err;
+ try {
+ // Duplicated close should throw error
+ conn.closeRead();
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.NotConnected);
+ assertEquals(err.name, "NotConnected");
+ closeDeferred.resolve();
+ listener.close();
+ conn.close();
+});
+*/
+
+/* TODO Fix broken test.
+testPerm({ net: true }, async function netCloseWriteSuccess() {
+ const addr = "127.0.0.1:4500";
+ const listener = Deno.listen(addr);
+ const closeDeferred = deferred();
+ listener.accept().then(async conn => {
+ await conn.write(new Uint8Array([1, 2, 3]));
+ await closeDeferred.promise;
+ conn.close();
+ });
+ const conn = await Deno.dial(addr);
+ conn.closeWrite(); // closing write
+ const buf = new Uint8Array(1024);
+ // Check read not impacted
+ const readResult = await conn.read(buf);
+ assertEquals(3, readResult);
+ assertEquals(1, buf[0]);
+ assertEquals(2, buf[1]);
+ assertEquals(3, buf[2]);
+ // Check write should be closed
+ let err;
+ try {
+ await conn.write(new Uint8Array([1, 2, 3]));
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.BrokenPipe);
+ assertEquals(err.name, "BrokenPipe");
+ closeDeferred.resolve();
+ listener.close();
+ conn.close();
+});
+*/
+
+/* TODO Fix broken test.
+testPerm({ net: true }, async function netDoubleCloseWrite() {
+ const addr = "127.0.0.1:4500";
+ const listener = Deno.listen(addr);
+ const closeDeferred = deferred();
+ listener.accept().then(async conn => {
+ await closeDeferred.promise;
+ conn.close();
+ });
+ const conn = await Deno.dial(addr);
+ conn.closeWrite(); // closing write
+ let err;
+ try {
+ // Duplicated close should throw error
+ conn.closeWrite();
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.NotConnected);
+ assertEquals(err.name, "NotConnected");
+ closeDeferred.resolve();
+ listener.close();
+ conn.close();
+});
+*/
diff --git a/cli/js/os.ts b/cli/js/os.ts
new file mode 100644
index 000000000..2fc06434a
--- /dev/null
+++ b/cli/js/os.ts
@@ -0,0 +1,151 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { core } from "./core.ts";
+import * as dispatch from "./dispatch.ts";
+import { sendSync } from "./dispatch_json.ts";
+import { assert } from "./util.ts";
+import * as util from "./util.ts";
+import { window } from "./window.ts";
+import { OperatingSystem, Arch } from "./build.ts";
+
+// builtin modules
+import { _setGlobals } from "./deno.ts";
+
+/** Check if running in terminal.
+ *
+ * console.log(Deno.isTTY().stdout);
+ */
+export function isTTY(): { stdin: boolean; stdout: boolean; stderr: boolean } {
+ return sendSync(dispatch.OP_IS_TTY);
+}
+
+/** Get the hostname.
+ * Requires the `--allow-env` flag.
+ *
+ * console.log(Deno.hostname());
+ */
+export function hostname(): string {
+ return sendSync(dispatch.OP_HOSTNAME);
+}
+
+/** Exit the Deno process with optional exit code. */
+export function exit(code = 0): never {
+ sendSync(dispatch.OP_EXIT, { code });
+ return util.unreachable();
+}
+
+function setEnv(key: string, value: string): void {
+ sendSync(dispatch.OP_SET_ENV, { key, value });
+}
+
+function getEnv(key: string): string | undefined {
+ return sendSync(dispatch.OP_GET_ENV, { key })[0];
+}
+
+/** Returns a snapshot of the environment variables at invocation. Mutating a
+ * property in the object will set that variable in the environment for
+ * the process. The environment object will only accept `string`s
+ * as values.
+ *
+ * console.log(Deno.env("SHELL"));
+ * const myEnv = Deno.env();
+ * console.log(myEnv.SHELL);
+ * myEnv.TEST_VAR = "HELLO";
+ * const newEnv = Deno.env();
+ * console.log(myEnv.TEST_VAR == newEnv.TEST_VAR);
+ */
+export function env(): { [index: string]: string };
+export function env(key: string): string | undefined;
+export function env(
+ key?: string
+): { [index: string]: string } | string | undefined {
+ if (key) {
+ return getEnv(key);
+ }
+ const env = sendSync(dispatch.OP_ENV);
+ return new Proxy(env, {
+ set(obj, prop: string, value: string): boolean {
+ setEnv(prop, value);
+ return Reflect.set(obj, prop, value);
+ }
+ });
+}
+
+interface Start {
+ cwd: string;
+ pid: number;
+ argv: string[];
+ mainModule: string; // Absolute URL.
+ debugFlag: boolean;
+ depsFlag: boolean;
+ typesFlag: boolean;
+ versionFlag: boolean;
+ denoVersion: string;
+ v8Version: string;
+ tsVersion: string;
+ noColor: boolean;
+ xevalDelim: string;
+ os: OperatingSystem;
+ arch: Arch;
+}
+
+// This function bootstraps an environment within Deno, it is shared both by
+// the runtime and the compiler environments.
+// @internal
+export function start(preserveDenoNamespace = true, source?: string): Start {
+ core.setAsyncHandler(dispatch.asyncMsgFromRust);
+ const ops = core.ops();
+ // TODO(bartlomieju): this is a prototype, we should come up with
+ // something a bit more sophisticated
+ for (const [name, opId] of Object.entries(ops)) {
+ const opName = `OP_${name.toUpperCase()}`;
+ // Assign op ids to actual variables
+ dispatch[opName] = opId;
+ }
+ // 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
+ // args and other info.
+ const s = sendSync(dispatch.OP_START);
+
+ util.setLogDebug(s.debugFlag, source);
+
+ // pid and noColor need to be set in the Deno module before it's set to be
+ // frozen.
+ _setGlobals(s.pid, s.noColor);
+ delete window.Deno._setGlobals;
+ Object.freeze(window.Deno);
+
+ if (preserveDenoNamespace) {
+ util.immutableDefine(window, "Deno", window.Deno);
+ // Deno.core could ONLY be safely frozen here (not in globals.ts)
+ // since shared_queue.js will modify core properties.
+ Object.freeze(window.Deno.core);
+ // core.sharedQueue is an object so we should also freeze it.
+ Object.freeze(window.Deno.core.sharedQueue);
+ } else {
+ // Remove window.Deno
+ delete window.Deno;
+ assert(window.Deno === undefined);
+ }
+
+ return s;
+}
+
+/**
+ * Returns the current user's home directory.
+ * Requires the `--allow-env` flag.
+ */
+export function homeDir(): string {
+ const path = sendSync(dispatch.OP_HOME_DIR);
+ if (!path) {
+ throw new Error("Could not get home directory.");
+ }
+ return path;
+}
+
+/**
+ * Returns the path to the current deno executable.
+ * Requires the `--allow-env` flag.
+ */
+export function execPath(): string {
+ return sendSync(dispatch.OP_EXEC_PATH);
+}
diff --git a/cli/js/os_test.ts b/cli/js/os_test.ts
new file mode 100644
index 000000000..0d07df1b4
--- /dev/null
+++ b/cli/js/os_test.ts
@@ -0,0 +1,165 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import {
+ test,
+ testPerm,
+ assert,
+ assertEquals,
+ assertNotEquals
+} from "./test_util.ts";
+
+testPerm({ env: true }, function envSuccess(): void {
+ const env = Deno.env();
+ assert(env !== null);
+ // eslint-disable-next-line @typescript-eslint/camelcase
+ env.test_var = "Hello World";
+ const newEnv = Deno.env();
+ assertEquals(env.test_var, newEnv.test_var);
+ assertEquals(Deno.env("test_var"), env.test_var);
+});
+
+testPerm({ env: true }, function envNotFound(): void {
+ const r = Deno.env("env_var_does_not_exist!");
+ assertEquals(r, undefined);
+});
+
+test(function envPermissionDenied1(): void {
+ let err;
+ try {
+ Deno.env();
+ } catch (e) {
+ err = e;
+ }
+ assertNotEquals(err, undefined);
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+test(function envPermissionDenied2(): void {
+ let err;
+ try {
+ Deno.env("PATH");
+ } catch (e) {
+ err = e;
+ }
+ assertNotEquals(err, undefined);
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+if (Deno.build.os === "win") {
+ // This test verifies that on Windows, environment variables are
+ // case-insensitive. Case normalization needs be done using the collation
+ // that Windows uses, rather than naively using String.toLowerCase().
+ testPerm({ env: true, run: true }, async function envCaseInsensitive() {
+ // Utility function that runs a Deno subprocess with the environment
+ // specified in `inputEnv`. The subprocess reads the environment variables
+ // which are in the keys of `expectedEnv` and writes them to stdout as JSON.
+ // It is then verified that these match with the values of `expectedEnv`.
+ const checkChildEnv = async (inputEnv, expectedEnv): Promise<void> => {
+ const src = `
+ console.log(
+ ${JSON.stringify(Object.keys(expectedEnv))}.map(k => Deno.env(k))
+ )`;
+ const proc = Deno.run({
+ args: [Deno.execPath(), "eval", src],
+ env: inputEnv,
+ stdout: "piped"
+ });
+ const status = await proc.status();
+ assertEquals(status.success, true);
+ const expectedValues = Object.values(expectedEnv);
+ const actualValues = JSON.parse(
+ new TextDecoder().decode(await proc.output())
+ );
+ assertEquals(actualValues, expectedValues);
+ };
+
+ assertEquals(Deno.env("path"), Deno.env("PATH"));
+ assertEquals(Deno.env("Path"), Deno.env("PATH"));
+
+ // Check 'foo', 'Foo' and 'Foo' are case folded.
+ await checkChildEnv({ foo: "X" }, { foo: "X", Foo: "X", FOO: "X" });
+
+ // Check that 'µ' and 'Μ' are not case folded.
+ const lc1 = "µ";
+ const uc1 = lc1.toUpperCase();
+ assertNotEquals(lc1, uc1);
+ await checkChildEnv(
+ { [lc1]: "mu", [uc1]: "MU" },
+ { [lc1]: "mu", [uc1]: "MU" }
+ );
+
+ // Check that 'dž' and 'DŽ' are folded, but 'Dž' is preserved.
+ const c2 = "Dž";
+ const lc2 = c2.toLowerCase();
+ const uc2 = c2.toUpperCase();
+ assertNotEquals(c2, lc2);
+ assertNotEquals(c2, uc2);
+ await checkChildEnv(
+ { [c2]: "Dz", [lc2]: "dz" },
+ { [c2]: "Dz", [lc2]: "dz", [uc2]: "dz" }
+ );
+ await checkChildEnv(
+ { [c2]: "Dz", [uc2]: "DZ" },
+ { [c2]: "Dz", [uc2]: "DZ", [lc2]: "DZ" }
+ );
+ });
+}
+
+test(function osPid(): void {
+ console.log("pid", Deno.pid);
+ assert(Deno.pid > 0);
+});
+
+// See complete tests in tools/is_tty_test.py
+test(function osIsTTYSmoke(): void {
+ console.log(Deno.isTTY());
+});
+
+testPerm({ env: true }, function homeDir(): void {
+ assertNotEquals(Deno.homeDir(), "");
+});
+
+testPerm({ env: false }, function homeDirPerm(): void {
+ let caughtError = false;
+ try {
+ Deno.homeDir();
+ } catch (err) {
+ caughtError = true;
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ env: true }, function execPath(): void {
+ assertNotEquals(Deno.execPath(), "");
+});
+
+testPerm({ env: false }, function execPathPerm(): void {
+ let caughtError = false;
+ try {
+ Deno.execPath();
+ } catch (err) {
+ caughtError = true;
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ env: true }, function hostnameDir(): void {
+ assertNotEquals(Deno.hostname(), "");
+});
+
+testPerm({ env: false }, function hostnamePerm(): void {
+ let caughtError = false;
+ try {
+ Deno.hostname();
+ } catch (err) {
+ caughtError = true;
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
diff --git a/cli/js/performance.ts b/cli/js/performance.ts
new file mode 100644
index 000000000..6ea8e56e1
--- /dev/null
+++ b/cli/js/performance.ts
@@ -0,0 +1,22 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as dispatch from "./dispatch.ts";
+import { sendSync } from "./dispatch_json.ts";
+
+interface NowResponse {
+ seconds: number;
+ subsecNanos: number;
+}
+
+export class Performance {
+ /** Returns a current time from Deno's start in milliseconds.
+ *
+ * Use the flag --allow-hrtime return a precise value.
+ *
+ * const t = performance.now();
+ * console.log(`${t} ms since start!`);
+ */
+ now(): number {
+ const res = sendSync(dispatch.OP_NOW) as NowResponse;
+ return res.seconds * 1e3 + res.subsecNanos / 1e6;
+ }
+}
diff --git a/cli/js/performance_test.ts b/cli/js/performance_test.ts
new file mode 100644
index 000000000..ac682364e
--- /dev/null
+++ b/cli/js/performance_test.ts
@@ -0,0 +1,10 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert } from "./test_util.ts";
+
+testPerm({ hrtime: false }, function now(): void {
+ const start = performance.now();
+ setTimeout((): void => {
+ const end = performance.now();
+ assert(end - start >= 10);
+ }, 10);
+});
diff --git a/cli/js/permissions.ts b/cli/js/permissions.ts
new file mode 100644
index 000000000..4f393501c
--- /dev/null
+++ b/cli/js/permissions.ts
@@ -0,0 +1,39 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as dispatch from "./dispatch.ts";
+import { sendSync } from "./dispatch_json.ts";
+
+/** Permissions as granted by the caller */
+export interface Permissions {
+ read: boolean;
+ write: boolean;
+ net: boolean;
+ env: boolean;
+ run: boolean;
+ hrtime: boolean;
+ // NOTE: Keep in sync with src/permissions.rs
+}
+
+export type Permission = keyof Permissions;
+
+/** Inspect granted permissions for the current program.
+ *
+ * if (Deno.permissions().read) {
+ * const file = await Deno.readFile("example.test");
+ * // ...
+ * }
+ */
+export function permissions(): Permissions {
+ return sendSync(dispatch.OP_PERMISSIONS) as Permissions;
+}
+
+/** Revoke a permission. When the permission was already revoked nothing changes
+ *
+ * if (Deno.permissions().read) {
+ * const file = await Deno.readFile("example.test");
+ * Deno.revokePermission('read');
+ * }
+ * Deno.readFile("example.test"); // -> error or permission prompt
+ */
+export function revokePermission(permission: Permission): void {
+ sendSync(dispatch.OP_REVOKE_PERMISSION, { permission });
+}
diff --git a/cli/js/permissions_test.ts b/cli/js/permissions_test.ts
new file mode 100644
index 000000000..6511c2dcb
--- /dev/null
+++ b/cli/js/permissions_test.ts
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+const knownPermissions: Deno.Permission[] = [
+ "run",
+ "read",
+ "write",
+ "net",
+ "env",
+ "hrtime"
+];
+
+for (const grant of knownPermissions) {
+ testPerm({ [grant]: true }, function envGranted(): void {
+ const perms = Deno.permissions();
+ assert(perms !== null);
+ for (const perm in perms) {
+ assertEquals(perms[perm], perm === grant);
+ }
+
+ Deno.revokePermission(grant);
+
+ const revoked = Deno.permissions();
+ for (const perm in revoked) {
+ assertEquals(revoked[perm], false);
+ }
+ });
+}
diff --git a/cli/js/process.ts b/cli/js/process.ts
new file mode 100644
index 000000000..0c77929f9
--- /dev/null
+++ b/cli/js/process.ts
@@ -0,0 +1,307 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+import { File, close } from "./files.ts";
+import { ReadCloser, WriteCloser } from "./io.ts";
+import { readAll } from "./buffer.ts";
+import { assert, unreachable } from "./util.ts";
+import { build } from "./build.ts";
+
+/** How to handle subprocess stdio.
+ *
+ * "inherit" The default if unspecified. The child inherits from the
+ * corresponding parent descriptor.
+ *
+ * "piped" A new pipe should be arranged to connect the parent and child
+ * subprocesses.
+ *
+ * "null" This stream will be ignored. This is the equivalent of attaching the
+ * stream to /dev/null.
+ */
+export type ProcessStdio = "inherit" | "piped" | "null";
+
+// TODO Maybe extend VSCode's 'CommandOptions'?
+// See https://code.visualstudio.com/docs/editor/tasks-appendix#_schema-for-tasksjson
+export interface RunOptions {
+ args: string[];
+ cwd?: string;
+ env?: { [key: string]: string };
+ stdout?: ProcessStdio | number;
+ stderr?: ProcessStdio | number;
+ stdin?: ProcessStdio | number;
+}
+
+interface RunStatusResponse {
+ gotSignal: boolean;
+ exitCode: number;
+ exitSignal: number;
+}
+
+async function runStatus(rid: number): Promise<ProcessStatus> {
+ const res = (await sendAsync(dispatch.OP_RUN_STATUS, {
+ rid
+ })) as RunStatusResponse;
+
+ if (res.gotSignal) {
+ const signal = res.exitSignal;
+ return { signal, success: false };
+ } else {
+ const code = res.exitCode;
+ return { code, success: code === 0 };
+ }
+}
+
+/** Send a signal to process under given PID. Unix only at this moment.
+ * If pid is negative, the signal will be sent to the process group identified
+ * by -pid.
+ * Requires the `--allow-run` flag.
+ */
+export function kill(pid: number, signo: number): void {
+ sendSync(dispatch.OP_KILL, { pid, signo });
+}
+
+export class Process {
+ readonly rid: number;
+ readonly pid: number;
+ readonly stdin?: WriteCloser;
+ readonly stdout?: ReadCloser;
+ readonly stderr?: ReadCloser;
+
+ // @internal
+ constructor(res: RunResponse) {
+ this.rid = res.rid;
+ this.pid = res.pid;
+
+ if (res.stdinRid && res.stdinRid > 0) {
+ this.stdin = new File(res.stdinRid);
+ }
+
+ if (res.stdoutRid && res.stdoutRid > 0) {
+ this.stdout = new File(res.stdoutRid);
+ }
+
+ if (res.stderrRid && res.stderrRid > 0) {
+ this.stderr = new File(res.stderrRid);
+ }
+ }
+
+ async status(): Promise<ProcessStatus> {
+ return await runStatus(this.rid);
+ }
+
+ /** Buffer the stdout and return it as Uint8Array after EOF.
+ * You must set stdout to "piped" when creating the process.
+ * This calls close() on stdout after its done.
+ */
+ async output(): Promise<Uint8Array> {
+ if (!this.stdout) {
+ throw new Error("Process.output: stdout is undefined");
+ }
+ try {
+ return await readAll(this.stdout);
+ } finally {
+ this.stdout.close();
+ }
+ }
+
+ /** Buffer the stderr and return it as Uint8Array after EOF.
+ * You must set stderr to "piped" when creating the process.
+ * This calls close() on stderr after its done.
+ */
+ async stderrOutput(): Promise<Uint8Array> {
+ if (!this.stderr) {
+ throw new Error("Process.stderrOutput: stderr is undefined");
+ }
+ try {
+ return await readAll(this.stderr);
+ } finally {
+ this.stderr.close();
+ }
+ }
+
+ close(): void {
+ close(this.rid);
+ }
+
+ kill(signo: number): void {
+ kill(this.pid, signo);
+ }
+}
+
+export interface ProcessStatus {
+ success: boolean;
+ code?: number;
+ signal?: number; // TODO: Make this a string, e.g. 'SIGTERM'.
+}
+
+// TODO: this method is only used to validate proper option, probably can be renamed
+function stdioMap(s: string): string {
+ switch (s) {
+ case "inherit":
+ case "piped":
+ case "null":
+ return s;
+ default:
+ return unreachable();
+ }
+}
+
+function isRid(arg: unknown): arg is number {
+ return !isNaN(arg as number);
+}
+
+interface RunResponse {
+ rid: number;
+ pid: number;
+ stdinRid: number | null;
+ stdoutRid: number | null;
+ stderrRid: number | null;
+}
+/**
+ * Spawns new subprocess.
+ *
+ * Subprocess uses same working directory as parent process unless `opt.cwd`
+ * is specified.
+ *
+ * Environmental variables for subprocess can be specified using `opt.env`
+ * mapping.
+ *
+ * By default subprocess inherits stdio of parent process. To change that
+ * `opt.stdout`, `opt.stderr` and `opt.stdin` can be specified independently -
+ * they can be set to either `ProcessStdio` or `rid` of open file.
+ */
+export function run(opt: RunOptions): Process {
+ assert(opt.args.length > 0);
+ let env: Array<[string, string]> = [];
+ if (opt.env) {
+ env = Array.from(Object.entries(opt.env));
+ }
+
+ let stdin = stdioMap("inherit");
+ let stdout = stdioMap("inherit");
+ let stderr = stdioMap("inherit");
+ let stdinRid = 0;
+ let stdoutRid = 0;
+ let stderrRid = 0;
+
+ if (opt.stdin) {
+ if (isRid(opt.stdin)) {
+ stdinRid = opt.stdin;
+ } else {
+ stdin = stdioMap(opt.stdin);
+ }
+ }
+
+ if (opt.stdout) {
+ if (isRid(opt.stdout)) {
+ stdoutRid = opt.stdout;
+ } else {
+ stdout = stdioMap(opt.stdout);
+ }
+ }
+
+ if (opt.stderr) {
+ if (isRid(opt.stderr)) {
+ stderrRid = opt.stderr;
+ } else {
+ stderr = stdioMap(opt.stderr);
+ }
+ }
+
+ const req = {
+ args: opt.args.map(String),
+ cwd: opt.cwd,
+ env,
+ stdin,
+ stdout,
+ stderr,
+ stdinRid,
+ stdoutRid,
+ stderrRid
+ };
+
+ const res = sendSync(dispatch.OP_RUN, req) as RunResponse;
+ return new Process(res);
+}
+
+// From `kill -l`
+enum LinuxSignal {
+ SIGHUP = 1,
+ SIGINT = 2,
+ SIGQUIT = 3,
+ SIGILL = 4,
+ SIGTRAP = 5,
+ SIGABRT = 6,
+ SIGBUS = 7,
+ SIGFPE = 8,
+ SIGKILL = 9,
+ SIGUSR1 = 10,
+ SIGSEGV = 11,
+ SIGUSR2 = 12,
+ SIGPIPE = 13,
+ SIGALRM = 14,
+ SIGTERM = 15,
+ SIGSTKFLT = 16,
+ SIGCHLD = 17,
+ SIGCONT = 18,
+ SIGSTOP = 19,
+ SIGTSTP = 20,
+ SIGTTIN = 21,
+ SIGTTOU = 22,
+ SIGURG = 23,
+ SIGXCPU = 24,
+ SIGXFSZ = 25,
+ SIGVTALRM = 26,
+ SIGPROF = 27,
+ SIGWINCH = 28,
+ SIGIO = 29,
+ SIGPWR = 30,
+ SIGSYS = 31
+}
+
+// From `kill -l`
+enum MacOSSignal {
+ SIGHUP = 1,
+ SIGINT = 2,
+ SIGQUIT = 3,
+ SIGILL = 4,
+ SIGTRAP = 5,
+ SIGABRT = 6,
+ SIGEMT = 7,
+ SIGFPE = 8,
+ SIGKILL = 9,
+ SIGBUS = 10,
+ SIGSEGV = 11,
+ SIGSYS = 12,
+ SIGPIPE = 13,
+ SIGALRM = 14,
+ SIGTERM = 15,
+ SIGURG = 16,
+ SIGSTOP = 17,
+ SIGTSTP = 18,
+ SIGCONT = 19,
+ SIGCHLD = 20,
+ SIGTTIN = 21,
+ SIGTTOU = 22,
+ SIGIO = 23,
+ SIGXCPU = 24,
+ SIGXFSZ = 25,
+ SIGVTALRM = 26,
+ SIGPROF = 27,
+ SIGWINCH = 28,
+ SIGINFO = 29,
+ SIGUSR1 = 30,
+ SIGUSR2 = 31
+}
+
+/** Signals numbers. This is platform dependent.
+ */
+export const Signal = {};
+
+export function setSignals(): void {
+ if (build.os === "mac") {
+ Object.assign(Signal, MacOSSignal);
+ } else {
+ Object.assign(Signal, LinuxSignal);
+ }
+}
diff --git a/cli/js/process_test.ts b/cli/js/process_test.ts
new file mode 100644
index 000000000..42db06dee
--- /dev/null
+++ b/cli/js/process_test.ts
@@ -0,0 +1,377 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import {
+ test,
+ testPerm,
+ assert,
+ assertEquals,
+ assertStrContains
+} from "./test_util.ts";
+const {
+ kill,
+ run,
+ DenoError,
+ ErrorKind,
+ readFile,
+ open,
+ makeTempDir,
+ writeFile
+} = Deno;
+
+test(function runPermissions(): void {
+ let caughtError = false;
+ try {
+ Deno.run({ args: ["python", "-c", "print('hello world')"] });
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ run: true }, async function runSuccess(): Promise<void> {
+ const p = run({
+ args: ["python", "-c", "print('hello world')"]
+ });
+ const status = await p.status();
+ console.log("status", status);
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runCommandFailedWithCode(): Promise<
+ void
+> {
+ const p = run({
+ args: ["python", "-c", "import sys;sys.exit(41 + 1)"]
+ });
+ const status = await p.status();
+ assertEquals(status.success, false);
+ assertEquals(status.code, 42);
+ assertEquals(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runCommandFailedWithSignal(): Promise<
+ void
+> {
+ if (Deno.build.os === "win") {
+ return; // No signals on windows.
+ }
+ const p = run({
+ args: ["python", "-c", "import os;os.kill(os.getpid(), 9)"]
+ });
+ const status = await p.status();
+ assertEquals(status.success, false);
+ assertEquals(status.code, undefined);
+ assertEquals(status.signal, 9);
+ p.close();
+});
+
+testPerm({ run: true }, function runNotFound(): void {
+ let error;
+ try {
+ run({ args: ["this file hopefully doesn't exist"] });
+ } catch (e) {
+ error = e;
+ }
+ assert(error !== undefined);
+ assert(error instanceof DenoError);
+ assertEquals(error.kind, ErrorKind.NotFound);
+});
+
+testPerm(
+ { write: true, run: true },
+ async function runWithCwdIsAsync(): Promise<void> {
+ const enc = new TextEncoder();
+ const cwd = await makeTempDir({ prefix: "deno_command_test" });
+
+ const exitCodeFile = "deno_was_here";
+ const pyProgramFile = "poll_exit.py";
+ const pyProgram = `
+from sys import exit
+from time import sleep
+
+while True:
+ try:
+ with open("${exitCodeFile}", "r") as f:
+ line = f.readline()
+ code = int(line)
+ exit(code)
+ except IOError:
+ # Retry if we got here before deno wrote the file.
+ sleep(0.01)
+ pass
+`;
+
+ Deno.writeFileSync(`${cwd}/${pyProgramFile}.py`, enc.encode(pyProgram));
+ const p = run({
+ cwd,
+ args: ["python", `${pyProgramFile}.py`]
+ });
+
+ // Write the expected exit code *after* starting python.
+ // This is how we verify that `run()` is actually asynchronous.
+ const code = 84;
+ Deno.writeFileSync(`${cwd}/${exitCodeFile}`, enc.encode(`${code}`));
+
+ const status = await p.status();
+ assertEquals(status.success, false);
+ assertEquals(status.code, code);
+ assertEquals(status.signal, undefined);
+ p.close();
+ }
+);
+
+testPerm({ run: true }, async function runStdinPiped(): Promise<void> {
+ const p = run({
+ args: ["python", "-c", "import sys; assert 'hello' == sys.stdin.read();"],
+ stdin: "piped"
+ });
+ assert(!p.stdout);
+ assert(!p.stderr);
+
+ const msg = new TextEncoder().encode("hello");
+ const n = await p.stdin.write(msg);
+ assertEquals(n, msg.byteLength);
+
+ p.stdin.close();
+
+ const status = await p.status();
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runStdoutPiped(): Promise<void> {
+ const p = run({
+ args: ["python", "-c", "import sys; sys.stdout.write('hello')"],
+ stdout: "piped"
+ });
+ assert(!p.stdin);
+ assert(!p.stderr);
+
+ const data = new Uint8Array(10);
+ let r = await p.stdout.read(data);
+ if (r === Deno.EOF) {
+ throw new Error("p.stdout.read(...) should not be EOF");
+ }
+ assertEquals(r, 5);
+ const s = new TextDecoder().decode(data.subarray(0, r));
+ assertEquals(s, "hello");
+ r = await p.stdout.read(data);
+ assertEquals(r, Deno.EOF);
+ p.stdout.close();
+
+ const status = await p.status();
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runStderrPiped(): Promise<void> {
+ const p = run({
+ args: ["python", "-c", "import sys; sys.stderr.write('hello')"],
+ stderr: "piped"
+ });
+ assert(!p.stdin);
+ assert(!p.stdout);
+
+ const data = new Uint8Array(10);
+ let r = await p.stderr.read(data);
+ if (r === Deno.EOF) {
+ throw new Error("p.stderr.read should not return EOF here");
+ }
+ assertEquals(r, 5);
+ const s = new TextDecoder().decode(data.subarray(0, r));
+ assertEquals(s, "hello");
+ r = await p.stderr.read(data);
+ assertEquals(r, Deno.EOF);
+ p.stderr.close();
+
+ const status = await p.status();
+ assertEquals(status.success, true);
+ assertEquals(status.code, 0);
+ assertEquals(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runOutput(): Promise<void> {
+ const p = run({
+ args: ["python", "-c", "import sys; sys.stdout.write('hello')"],
+ stdout: "piped"
+ });
+ const output = await p.output();
+ const s = new TextDecoder().decode(output);
+ assertEquals(s, "hello");
+ p.close();
+});
+
+testPerm({ run: true }, async function runStderrOutput(): Promise<void> {
+ const p = run({
+ args: ["python", "-c", "import sys; sys.stderr.write('error')"],
+ stderr: "piped"
+ });
+ const error = await p.stderrOutput();
+ const s = new TextDecoder().decode(error);
+ assertEquals(s, "error");
+ p.close();
+});
+
+testPerm(
+ { run: true, write: true, read: true },
+ async function runRedirectStdoutStderr(): Promise<void> {
+ const tempDir = await makeTempDir();
+ const fileName = tempDir + "/redirected_stdio.txt";
+ const file = await open(fileName, "w");
+
+ const p = run({
+ args: [
+ "python",
+ "-c",
+ "import sys; sys.stderr.write('error\\n'); sys.stdout.write('output\\n');"
+ ],
+ stdout: file.rid,
+ stderr: file.rid
+ });
+
+ await p.status();
+ p.close();
+ file.close();
+
+ const fileContents = await readFile(fileName);
+ const decoder = new TextDecoder();
+ const text = decoder.decode(fileContents);
+
+ assertStrContains(text, "error");
+ assertStrContains(text, "output");
+ }
+);
+
+testPerm(
+ { run: true, write: true, read: true },
+ async function runRedirectStdin(): Promise<void> {
+ const tempDir = await makeTempDir();
+ const fileName = tempDir + "/redirected_stdio.txt";
+ const encoder = new TextEncoder();
+ await writeFile(fileName, encoder.encode("hello"));
+ const file = await open(fileName, "r");
+
+ const p = run({
+ args: ["python", "-c", "import sys; assert 'hello' == sys.stdin.read();"],
+ stdin: file.rid
+ });
+
+ const status = await p.status();
+ assertEquals(status.code, 0);
+ p.close();
+ file.close();
+ }
+);
+
+testPerm({ run: true }, async function runEnv(): Promise<void> {
+ const p = run({
+ args: [
+ "python",
+ "-c",
+ "import os, sys; sys.stdout.write(os.environ.get('FOO', '') + os.environ.get('BAR', ''))"
+ ],
+ env: {
+ FOO: "0123",
+ BAR: "4567"
+ },
+ stdout: "piped"
+ });
+ const output = await p.output();
+ const s = new TextDecoder().decode(output);
+ assertEquals(s, "01234567");
+ p.close();
+});
+
+testPerm({ run: true }, async function runClose(): Promise<void> {
+ const p = run({
+ args: [
+ "python",
+ "-c",
+ "from time import sleep; import sys; sleep(10000); sys.stderr.write('error')"
+ ],
+ stderr: "piped"
+ });
+ assert(!p.stdin);
+ assert(!p.stdout);
+
+ p.close();
+
+ const data = new Uint8Array(10);
+ const r = await p.stderr.read(data);
+ assertEquals(r, Deno.EOF);
+});
+
+test(function signalNumbers(): void {
+ if (Deno.build.os === "mac") {
+ assertEquals(Deno.Signal.SIGSTOP, 17);
+ } else if (Deno.build.os === "linux") {
+ assertEquals(Deno.Signal.SIGSTOP, 19);
+ }
+});
+
+// Ignore signal tests on windows for now...
+if (Deno.build.os !== "win") {
+ test(function killPermissions(): void {
+ let caughtError = false;
+ try {
+ // Unlike the other test cases, we don't have permission to spawn a
+ // subprocess we can safely kill. Instead we send SIGCONT to the current
+ // process - assuming that Deno does not have a special handler set for it
+ // and will just continue even if a signal is erroneously sent.
+ Deno.kill(Deno.pid, Deno.Signal.SIGCONT);
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+ });
+
+ testPerm({ run: true }, async function killSuccess(): Promise<void> {
+ const p = run({
+ args: ["python", "-c", "from time import sleep; sleep(10000)"]
+ });
+
+ assertEquals(Deno.Signal.SIGINT, 2);
+ kill(p.pid, Deno.Signal.SIGINT);
+ const status = await p.status();
+
+ assertEquals(status.success, false);
+ // TODO(ry) On Linux, status.code is sometimes undefined and sometimes 1.
+ // The following assert is causing this test to be flaky. Investigate and
+ // re-enable when it can be made deterministic.
+ // assertEquals(status.code, 1);
+ // assertEquals(status.signal, Deno.Signal.SIGINT);
+ });
+
+ testPerm({ run: true }, async function killFailed(): Promise<void> {
+ const p = run({
+ args: ["python", "-c", "from time import sleep; sleep(10000)"]
+ });
+ assert(!p.stdin);
+ assert(!p.stdout);
+
+ let err;
+ try {
+ kill(p.pid, 12345);
+ } catch (e) {
+ err = e;
+ }
+
+ assert(!!err);
+ assertEquals(err.kind, Deno.ErrorKind.InvalidInput);
+ assertEquals(err.name, "InvalidInput");
+
+ p.close();
+ });
+}
diff --git a/cli/js/read_dir.ts b/cli/js/read_dir.ts
new file mode 100644
index 000000000..2fa6a566b
--- /dev/null
+++ b/cli/js/read_dir.ts
@@ -0,0 +1,34 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+import { FileInfo, FileInfoImpl } from "./file_info.ts";
+import { StatResponse } from "./stat.ts";
+
+interface ReadDirResponse {
+ entries: StatResponse[];
+}
+
+function res(response: ReadDirResponse): FileInfo[] {
+ return response.entries.map(
+ (statRes: StatResponse): FileInfo => {
+ return new FileInfoImpl(statRes);
+ }
+ );
+}
+
+/** Reads the directory given by path and returns a list of file info
+ * synchronously.
+ *
+ * const files = Deno.readDirSync("/");
+ */
+export function readDirSync(path: string): FileInfo[] {
+ return res(sendSync(dispatch.OP_READ_DIR, { path }));
+}
+
+/** Reads the directory given by path and returns a list of file info.
+ *
+ * const files = await Deno.readDir("/");
+ */
+export async function readDir(path: string): Promise<FileInfo[]> {
+ return res(await sendAsync(dispatch.OP_READ_DIR, { path }));
+}
diff --git a/cli/js/read_dir_test.ts b/cli/js/read_dir_test.ts
new file mode 100644
index 000000000..3e11df9fe
--- /dev/null
+++ b/cli/js/read_dir_test.ts
@@ -0,0 +1,84 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+type FileInfo = Deno.FileInfo;
+
+function assertSameContent(files: FileInfo[]): void {
+ let counter = 0;
+
+ for (const file of files) {
+ if (file.name === "subdir") {
+ assert(file.isDirectory());
+ counter++;
+ }
+
+ if (file.name === "002_hello.ts") {
+ assertEquals(file.mode!, Deno.statSync(`tests/${file.name}`).mode!);
+ counter++;
+ }
+ }
+
+ assertEquals(counter, 2);
+}
+
+testPerm({ read: true }, function readDirSyncSuccess(): void {
+ const files = Deno.readDirSync("tests/");
+ assertSameContent(files);
+});
+
+testPerm({ read: false }, function readDirSyncPerm(): void {
+ let caughtError = false;
+ try {
+ Deno.readDirSync("tests/");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true }, function readDirSyncNotDir(): void {
+ let caughtError = false;
+ let src;
+
+ try {
+ src = Deno.readDirSync("package.json");
+ } catch (err) {
+ caughtError = true;
+ assertEquals(err.kind, Deno.ErrorKind.Other);
+ }
+ assert(caughtError);
+ assertEquals(src, undefined);
+});
+
+testPerm({ read: true }, function readDirSyncNotFound(): void {
+ let caughtError = false;
+ let src;
+
+ try {
+ src = Deno.readDirSync("bad_dir_name");
+ } catch (err) {
+ caughtError = true;
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ }
+ assert(caughtError);
+ assertEquals(src, undefined);
+});
+
+testPerm({ read: true }, async function readDirSuccess(): Promise<void> {
+ const files = await Deno.readDir("tests/");
+ assertSameContent(files);
+});
+
+testPerm({ read: false }, async function readDirPerm(): Promise<void> {
+ let caughtError = false;
+ try {
+ await Deno.readDir("tests/");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
diff --git a/cli/js/read_file.ts b/cli/js/read_file.ts
new file mode 100644
index 000000000..de6630cc0
--- /dev/null
+++ b/cli/js/read_file.ts
@@ -0,0 +1,29 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { open, openSync } from "./files.ts";
+import { readAll, readAllSync } from "./buffer.ts";
+
+/** Read the entire contents of a file synchronously.
+ *
+ * const decoder = new TextDecoder("utf-8");
+ * const data = Deno.readFileSync("hello.txt");
+ * console.log(decoder.decode(data));
+ */
+export function readFileSync(filename: string): Uint8Array {
+ const file = openSync(filename);
+ const contents = readAllSync(file);
+ file.close();
+ return contents;
+}
+
+/** Read the entire contents of a file.
+ *
+ * const decoder = new TextDecoder("utf-8");
+ * const data = await Deno.readFile("hello.txt");
+ * console.log(decoder.decode(data));
+ */
+export async function readFile(filename: string): Promise<Uint8Array> {
+ const file = await open(filename);
+ const contents = await readAll(file);
+ file.close();
+ return contents;
+}
diff --git a/cli/js/read_file_test.ts b/cli/js/read_file_test.ts
new file mode 100644
index 000000000..7d4f4789c
--- /dev/null
+++ b/cli/js/read_file_test.ts
@@ -0,0 +1,57 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+testPerm({ read: true }, function readFileSyncSuccess(): void {
+ const data = Deno.readFileSync("package.json");
+ assert(data.byteLength > 0);
+ const decoder = new TextDecoder("utf-8");
+ const json = decoder.decode(data);
+ const pkg = JSON.parse(json);
+ assertEquals(pkg.name, "deno");
+});
+
+testPerm({ read: false }, function readFileSyncPerm(): void {
+ let caughtError = false;
+ try {
+ Deno.readFileSync("package.json");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true }, function readFileSyncNotFound(): void {
+ let caughtError = false;
+ let data;
+ try {
+ data = Deno.readFileSync("bad_filename");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ }
+ assert(caughtError);
+ assert(data === undefined);
+});
+
+testPerm({ read: true }, async function readFileSuccess(): Promise<void> {
+ const data = await Deno.readFile("package.json");
+ assert(data.byteLength > 0);
+ const decoder = new TextDecoder("utf-8");
+ const json = decoder.decode(data);
+ const pkg = JSON.parse(json);
+ assertEquals(pkg.name, "deno");
+});
+
+testPerm({ read: false }, async function readFilePerm(): Promise<void> {
+ let caughtError = false;
+ try {
+ await Deno.readFile("package.json");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
diff --git a/cli/js/read_link.ts b/cli/js/read_link.ts
new file mode 100644
index 000000000..861fbff0b
--- /dev/null
+++ b/cli/js/read_link.ts
@@ -0,0 +1,19 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+/** Returns the destination of the named symbolic link synchronously.
+ *
+ * const targetPath = Deno.readlinkSync("symlink/path");
+ */
+export function readlinkSync(name: string): string {
+ return sendSync(dispatch.OP_READ_LINK, { name });
+}
+
+/** Returns the destination of the named symbolic link.
+ *
+ * const targetPath = await Deno.readlink("symlink/path");
+ */
+export async function readlink(name: string): Promise<string> {
+ return await sendAsync(dispatch.OP_READ_LINK, { name });
+}
diff --git a/cli/js/read_link_test.ts b/cli/js/read_link_test.ts
new file mode 100644
index 000000000..83a693e3b
--- /dev/null
+++ b/cli/js/read_link_test.ts
@@ -0,0 +1,69 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+testPerm({ write: true, read: true }, function readlinkSyncSuccess(): void {
+ const testDir = Deno.makeTempDirSync();
+ const target = testDir + "/target";
+ const symlink = testDir + "/symln";
+ Deno.mkdirSync(target);
+ // TODO Add test for Windows once symlink is implemented for Windows.
+ // See https://github.com/denoland/deno/issues/815.
+ if (Deno.build.os !== "win") {
+ Deno.symlinkSync(target, symlink);
+ const targetPath = Deno.readlinkSync(symlink);
+ assertEquals(targetPath, target);
+ }
+});
+
+testPerm({ read: false }, async function readlinkSyncPerm(): Promise<void> {
+ let caughtError = false;
+ try {
+ Deno.readlinkSync("/symlink");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true }, function readlinkSyncNotFound(): void {
+ let caughtError = false;
+ let data;
+ try {
+ data = Deno.readlinkSync("bad_filename");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ }
+ assert(caughtError);
+ assertEquals(data, undefined);
+});
+
+testPerm({ write: true, read: true }, async function readlinkSuccess(): Promise<
+ void
+> {
+ const testDir = Deno.makeTempDirSync();
+ const target = testDir + "/target";
+ const symlink = testDir + "/symln";
+ Deno.mkdirSync(target);
+ // TODO Add test for Windows once symlink is implemented for Windows.
+ // See https://github.com/denoland/deno/issues/815.
+ if (Deno.build.os !== "win") {
+ Deno.symlinkSync(target, symlink);
+ const targetPath = await Deno.readlink(symlink);
+ assertEquals(targetPath, target);
+ }
+});
+
+testPerm({ read: false }, async function readlinkPerm(): Promise<void> {
+ let caughtError = false;
+ try {
+ await Deno.readlink("/symlink");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
diff --git a/cli/js/remove.ts b/cli/js/remove.ts
new file mode 100644
index 000000000..36413a7c4
--- /dev/null
+++ b/cli/js/remove.ts
@@ -0,0 +1,32 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+export interface RemoveOption {
+ recursive?: boolean;
+}
+
+/** Removes the named file or directory synchronously. Would throw
+ * error if permission denied, not found, or directory not empty if `recursive`
+ * set to false.
+ * `recursive` is set to false by default.
+ *
+ * Deno.removeSync("/path/to/dir/or/file", {recursive: false});
+ */
+export function removeSync(path: string, options: RemoveOption = {}): void {
+ sendSync(dispatch.OP_REMOVE, { path, recursive: !!options.recursive });
+}
+
+/** Removes the named file or directory. Would throw error if
+ * permission denied, not found, or directory not empty if `recursive` set
+ * to false.
+ * `recursive` is set to false by default.
+ *
+ * await Deno.remove("/path/to/dir/or/file", {recursive: false});
+ */
+export async function remove(
+ path: string,
+ options: RemoveOption = {}
+): Promise<void> {
+ await sendAsync(dispatch.OP_REMOVE, { path, recursive: !!options.recursive });
+}
diff --git a/cli/js/remove_test.ts b/cli/js/remove_test.ts
new file mode 100644
index 000000000..f14386f7f
--- /dev/null
+++ b/cli/js/remove_test.ts
@@ -0,0 +1,335 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+// SYNC
+
+testPerm({ write: true }, function removeSyncDirSuccess(): void {
+ // REMOVE EMPTY DIRECTORY
+ const path = Deno.makeTempDirSync() + "/dir/subdir";
+ Deno.mkdirSync(path);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory()); // check exist first
+ Deno.removeSync(path); // remove
+ // We then check again after remove
+ let err;
+ try {
+ Deno.statSync(path);
+ } catch (e) {
+ err = e;
+ }
+ // Directory is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: true }, function removeSyncFileSuccess(): void {
+ // REMOVE FILE
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile()); // check exist first
+ Deno.removeSync(filename); // remove
+ // We then check again after remove
+ let err;
+ try {
+ Deno.statSync(filename);
+ } catch (e) {
+ err = e;
+ }
+ // File is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: true }, function removeSyncFail(): void {
+ // NON-EMPTY DIRECTORY
+ const path = Deno.makeTempDirSync() + "/dir/subdir";
+ const subPath = path + "/subsubdir";
+ Deno.mkdirSync(path);
+ Deno.mkdirSync(subPath);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory()); // check exist first
+ const subPathInfo = Deno.statSync(subPath);
+ assert(subPathInfo.isDirectory()); // check exist first
+ let err;
+ try {
+ // Should not be able to recursively remove
+ Deno.removeSync(path);
+ } catch (e) {
+ err = e;
+ }
+ // TODO(ry) Is Other really the error we should get here? What would Go do?
+ assertEquals(err.kind, Deno.ErrorKind.Other);
+ assertEquals(err.name, "Other");
+ // NON-EXISTENT DIRECTORY/FILE
+ try {
+ // Non-existent
+ Deno.removeSync("/baddir");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: false }, function removeSyncPerm(): void {
+ let err;
+ try {
+ Deno.removeSync("/baddir");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ write: true }, function removeAllSyncDirSuccess(): void {
+ // REMOVE EMPTY DIRECTORY
+ let path = Deno.makeTempDirSync() + "/dir/subdir";
+ Deno.mkdirSync(path);
+ let pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory()); // check exist first
+ Deno.removeSync(path, { recursive: true }); // remove
+ // We then check again after remove
+ let err;
+ try {
+ Deno.statSync(path);
+ } catch (e) {
+ err = e;
+ }
+ // Directory is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+ // REMOVE NON-EMPTY DIRECTORY
+ path = Deno.makeTempDirSync() + "/dir/subdir";
+ const subPath = path + "/subsubdir";
+ Deno.mkdirSync(path);
+ Deno.mkdirSync(subPath);
+ pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory()); // check exist first
+ const subPathInfo = Deno.statSync(subPath);
+ assert(subPathInfo.isDirectory()); // check exist first
+ Deno.removeSync(path, { recursive: true }); // remove
+ // We then check parent directory again after remove
+ try {
+ Deno.statSync(path);
+ } catch (e) {
+ err = e;
+ }
+ // Directory is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: true }, function removeAllSyncFileSuccess(): void {
+ // REMOVE FILE
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile()); // check exist first
+ Deno.removeSync(filename, { recursive: true }); // remove
+ // We then check again after remove
+ let err;
+ try {
+ Deno.statSync(filename);
+ } catch (e) {
+ err = e;
+ }
+ // File is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: true }, function removeAllSyncFail(): void {
+ // NON-EXISTENT DIRECTORY/FILE
+ let err;
+ try {
+ // Non-existent
+ Deno.removeSync("/baddir", { recursive: true });
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: false }, function removeAllSyncPerm(): void {
+ let err;
+ try {
+ Deno.removeSync("/baddir", { recursive: true });
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+// ASYNC
+
+testPerm({ write: true }, async function removeDirSuccess(): Promise<void> {
+ // REMOVE EMPTY DIRECTORY
+ const path = Deno.makeTempDirSync() + "/dir/subdir";
+ Deno.mkdirSync(path);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory()); // check exist first
+ await Deno.remove(path); // remove
+ // We then check again after remove
+ let err;
+ try {
+ Deno.statSync(path);
+ } catch (e) {
+ err = e;
+ }
+ // Directory is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: true }, async function removeFileSuccess(): Promise<void> {
+ // REMOVE FILE
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile()); // check exist first
+ await Deno.remove(filename); // remove
+ // We then check again after remove
+ let err;
+ try {
+ Deno.statSync(filename);
+ } catch (e) {
+ err = e;
+ }
+ // File is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: true }, async function removeFail(): Promise<void> {
+ // NON-EMPTY DIRECTORY
+ const path = Deno.makeTempDirSync() + "/dir/subdir";
+ const subPath = path + "/subsubdir";
+ Deno.mkdirSync(path);
+ Deno.mkdirSync(subPath);
+ const pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory()); // check exist first
+ const subPathInfo = Deno.statSync(subPath);
+ assert(subPathInfo.isDirectory()); // check exist first
+ let err;
+ try {
+ // Should not be able to recursively remove
+ await Deno.remove(path);
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.Other);
+ assertEquals(err.name, "Other");
+ // NON-EXISTENT DIRECTORY/FILE
+ try {
+ // Non-existent
+ await Deno.remove("/baddir");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: false }, async function removePerm(): Promise<void> {
+ let err;
+ try {
+ await Deno.remove("/baddir");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ write: true }, async function removeAllDirSuccess(): Promise<void> {
+ // REMOVE EMPTY DIRECTORY
+ let path = Deno.makeTempDirSync() + "/dir/subdir";
+ Deno.mkdirSync(path);
+ let pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory()); // check exist first
+ await Deno.remove(path, { recursive: true }); // remove
+ // We then check again after remove
+ let err;
+ try {
+ Deno.statSync(path);
+ } catch (e) {
+ err = e;
+ }
+ // Directory is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+ // REMOVE NON-EMPTY DIRECTORY
+ path = Deno.makeTempDirSync() + "/dir/subdir";
+ const subPath = path + "/subsubdir";
+ Deno.mkdirSync(path);
+ Deno.mkdirSync(subPath);
+ pathInfo = Deno.statSync(path);
+ assert(pathInfo.isDirectory()); // check exist first
+ const subPathInfo = Deno.statSync(subPath);
+ assert(subPathInfo.isDirectory()); // check exist first
+ await Deno.remove(path, { recursive: true }); // remove
+ // We then check parent directory again after remove
+ try {
+ Deno.statSync(path);
+ } catch (e) {
+ err = e;
+ }
+ // Directory is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: true }, async function removeAllFileSuccess(): Promise<void> {
+ // REMOVE FILE
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+ const fileInfo = Deno.statSync(filename);
+ assert(fileInfo.isFile()); // check exist first
+ await Deno.remove(filename, { recursive: true }); // remove
+ // We then check again after remove
+ let err;
+ try {
+ Deno.statSync(filename);
+ } catch (e) {
+ err = e;
+ }
+ // File is gone
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: true }, async function removeAllFail(): Promise<void> {
+ // NON-EXISTENT DIRECTORY/FILE
+ let err;
+ try {
+ // Non-existent
+ await Deno.remove("/baddir", { recursive: true });
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+});
+
+testPerm({ write: false }, async function removeAllPerm(): Promise<void> {
+ let err;
+ try {
+ await Deno.remove("/baddir", { recursive: true });
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
diff --git a/cli/js/rename.ts b/cli/js/rename.ts
new file mode 100644
index 000000000..c906ce37b
--- /dev/null
+++ b/cli/js/rename.ts
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+/** Synchronously renames (moves) `oldpath` to `newpath`. If `newpath` already
+ * exists and is not a directory, `renameSync()` replaces it. OS-specific
+ * restrictions may apply when `oldpath` and `newpath` are in different
+ * directories.
+ *
+ * Deno.renameSync("old/path", "new/path");
+ */
+export function renameSync(oldpath: string, newpath: string): void {
+ sendSync(dispatch.OP_RENAME, { oldpath, newpath });
+}
+
+/** Renames (moves) `oldpath` to `newpath`. If `newpath` already exists and is
+ * not a directory, `rename()` replaces it. OS-specific restrictions may apply
+ * when `oldpath` and `newpath` are in different directories.
+ *
+ * await Deno.rename("old/path", "new/path");
+ */
+export async function rename(oldpath: string, newpath: string): Promise<void> {
+ await sendAsync(dispatch.OP_RENAME, { oldpath, newpath });
+}
diff --git a/cli/js/rename_test.ts b/cli/js/rename_test.ts
new file mode 100644
index 000000000..43d02d419
--- /dev/null
+++ b/cli/js/rename_test.ts
@@ -0,0 +1,74 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+testPerm({ read: true, write: true }, function renameSyncSuccess(): void {
+ const testDir = Deno.makeTempDirSync();
+ const oldpath = testDir + "/oldpath";
+ const newpath = testDir + "/newpath";
+ Deno.mkdirSync(oldpath);
+ Deno.renameSync(oldpath, newpath);
+ const newPathInfo = Deno.statSync(newpath);
+ assert(newPathInfo.isDirectory());
+
+ let caughtErr = false;
+ let oldPathInfo;
+
+ try {
+ oldPathInfo = Deno.statSync(oldpath);
+ } catch (e) {
+ caughtErr = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ }
+ assert(caughtErr);
+ assertEquals(oldPathInfo, undefined);
+});
+
+testPerm({ read: false, write: true }, function renameSyncReadPerm(): void {
+ let err;
+ try {
+ const oldpath = "/oldbaddir";
+ const newpath = "/newbaddir";
+ Deno.renameSync(oldpath, newpath);
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ read: true, write: false }, function renameSyncWritePerm(): void {
+ let err;
+ try {
+ const oldpath = "/oldbaddir";
+ const newpath = "/newbaddir";
+ Deno.renameSync(oldpath, newpath);
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ read: true, write: true }, async function renameSuccess(): Promise<
+ void
+> {
+ const testDir = Deno.makeTempDirSync();
+ const oldpath = testDir + "/oldpath";
+ const newpath = testDir + "/newpath";
+ Deno.mkdirSync(oldpath);
+ await Deno.rename(oldpath, newpath);
+ const newPathInfo = Deno.statSync(newpath);
+ assert(newPathInfo.isDirectory());
+
+ let caughtErr = false;
+ let oldPathInfo;
+
+ try {
+ oldPathInfo = Deno.statSync(oldpath);
+ } catch (e) {
+ caughtErr = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ }
+ assert(caughtErr);
+ assertEquals(oldPathInfo, undefined);
+});
diff --git a/cli/js/repl.ts b/cli/js/repl.ts
new file mode 100644
index 000000000..966e809e8
--- /dev/null
+++ b/cli/js/repl.ts
@@ -0,0 +1,197 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { close } from "./files.ts";
+import { exit } from "./os.ts";
+import { window } from "./window.ts";
+import { core } from "./core.ts";
+import { formatError } from "./format_error.ts";
+import { stringifyArgs } from "./console.ts";
+import * as dispatch from "./dispatch.ts";
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+
+const { console } = window;
+
+/**
+ * REPL logging.
+ * In favor of console.log to avoid unwanted indentation
+ */
+function replLog(...args: unknown[]): void {
+ core.print(stringifyArgs(args) + "\n");
+}
+
+/**
+ * REPL logging for errors.
+ * In favor of console.error to avoid unwanted indentation
+ */
+function replError(...args: unknown[]): void {
+ core.print(stringifyArgs(args) + "\n", true);
+}
+
+const helpMsg = [
+ "_ Get last evaluation result",
+ "_error Get last thrown error",
+ "exit Exit the REPL",
+ "help Print this help message"
+].join("\n");
+
+const replCommands = {
+ exit: {
+ get(): void {
+ exit(0);
+ }
+ },
+ help: {
+ get(): string {
+ return helpMsg;
+ }
+ }
+};
+
+function startRepl(historyFile: string): number {
+ return sendSync(dispatch.OP_REPL_START, { historyFile });
+}
+
+// @internal
+export async function readline(rid: number, prompt: string): Promise<string> {
+ return sendAsync(dispatch.OP_REPL_READLINE, { rid, prompt });
+}
+
+// Error messages that allow users to continue input
+// instead of throwing an error to REPL
+// ref: https://github.com/v8/v8/blob/master/src/message-template.h
+// TODO(kevinkassimo): this list might not be comprehensive
+const recoverableErrorMessages = [
+ "Unexpected end of input", // { or [ or (
+ "Missing initializer in const declaration", // const a
+ "Missing catch or finally after try", // try {}
+ "missing ) after argument list", // console.log(1
+ "Unterminated template literal" // `template
+ // TODO(kevinkassimo): need a parser to handling errors such as:
+ // "Missing } in template expression" // `${ or `${ a 123 }`
+];
+
+function isRecoverableError(e: Error): boolean {
+ return recoverableErrorMessages.includes(e.message);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type Value = any;
+
+let lastEvalResult: Value = undefined;
+let lastThrownError: Value = undefined;
+
+// Evaluate code.
+// Returns true if code is consumed (no error/irrecoverable error).
+// Returns false if error is recoverable
+function evaluate(code: string): boolean {
+ const [result, errInfo] = core.evalContext(code);
+ if (!errInfo) {
+ lastEvalResult = result;
+ replLog(result);
+ } else if (errInfo.isCompileError && isRecoverableError(errInfo.thrown)) {
+ // Recoverable compiler error
+ return false; // don't consume code.
+ } else {
+ lastThrownError = errInfo.thrown;
+ if (errInfo.isNativeError) {
+ const formattedError = formatError(
+ core.errorToJSON(errInfo.thrown as Error)
+ );
+ replError(formattedError);
+ } else {
+ replError("Thrown:", errInfo.thrown);
+ }
+ }
+ return true;
+}
+
+// @internal
+export async function replLoop(): Promise<void> {
+ Object.defineProperties(window, replCommands);
+
+ const historyFile = "deno_history.txt";
+ const rid = startRepl(historyFile);
+
+ const quitRepl = (exitCode: number): void => {
+ // Special handling in case user calls deno.close(3).
+ try {
+ close(rid); // close signals Drop on REPL and saves history.
+ } catch {}
+ exit(exitCode);
+ };
+
+ // Configure window._ to give the last evaluation result.
+ Object.defineProperty(window, "_", {
+ configurable: true,
+ get: (): Value => lastEvalResult,
+ set: (value: Value): Value => {
+ Object.defineProperty(window, "_", {
+ value: value,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+ console.log("Last evaluation result is no longer saved to _.");
+ }
+ });
+
+ // Configure window._error to give the last thrown error.
+ Object.defineProperty(window, "_error", {
+ configurable: true,
+ get: (): Value => lastThrownError,
+ set: (value: Value): Value => {
+ Object.defineProperty(window, "_error", {
+ value: value,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+ console.log("Last thrown error is no longer saved to _error.");
+ }
+ });
+
+ while (true) {
+ let code = "";
+ // Top level read
+ try {
+ code = await readline(rid, "> ");
+ if (code.trim() === "") {
+ continue;
+ }
+ } catch (err) {
+ if (err.message === "EOF") {
+ quitRepl(0);
+ } else {
+ // If interrupted, don't print error.
+ if (err.message !== "Interrupted") {
+ // e.g. this happens when we have deno.close(3).
+ // We want to display the problem.
+ const formattedError = formatError(core.errorToJSON(err));
+ replError(formattedError);
+ }
+ // Quit REPL anyways.
+ quitRepl(1);
+ }
+ }
+ // Start continued read
+ while (!evaluate(code)) {
+ code += "\n";
+ try {
+ code += await readline(rid, " ");
+ } catch (err) {
+ // If interrupted on continued read,
+ // abort this read instead of quitting.
+ if (err.message === "Interrupted") {
+ break;
+ } else if (err.message === "EOF") {
+ quitRepl(0);
+ } else {
+ // e.g. this happens when we have deno.close(3).
+ // We want to display the problem.
+ const formattedError = formatError(core.errorToJSON(err));
+ replError(formattedError);
+ quitRepl(1);
+ }
+ }
+ }
+ }
+}
diff --git a/cli/js/request.ts b/cli/js/request.ts
new file mode 100644
index 000000000..0c77b8854
--- /dev/null
+++ b/cli/js/request.ts
@@ -0,0 +1,151 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as headers from "./headers.ts";
+import * as body from "./body.ts";
+import * as domTypes from "./dom_types.ts";
+
+const { Headers } = headers;
+
+function byteUpperCase(s: string): string {
+ return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c): string {
+ return c.toUpperCase();
+ });
+}
+
+function normalizeMethod(m: string): string {
+ const u = byteUpperCase(m);
+ if (
+ u === "DELETE" ||
+ u === "GET" ||
+ u === "HEAD" ||
+ u === "OPTIONS" ||
+ u === "POST" ||
+ u === "PUT"
+ ) {
+ return u;
+ }
+ return m;
+}
+
+/**
+ * An HTTP request
+ * @param {Blob|String} [body]
+ * @param {Object} [init]
+ */
+export class Request extends body.Body implements domTypes.Request {
+ public method: string;
+ public url: string;
+ public credentials?: "omit" | "same-origin" | "include";
+ public headers: domTypes.Headers;
+
+ constructor(input: domTypes.RequestInfo, init?: domTypes.RequestInit) {
+ if (arguments.length < 1) {
+ throw TypeError("Not enough arguments");
+ }
+
+ if (!init) {
+ init = {};
+ }
+
+ let b: body.BodySource;
+
+ // prefer body from init
+ if (init.body) {
+ b = init.body;
+ } else if (input instanceof Request && input._bodySource) {
+ if (input.bodyUsed) {
+ throw TypeError(body.BodyUsedError);
+ }
+ b = input._bodySource;
+ } else if (typeof input === "object" && "body" in input && input.body) {
+ if (input.bodyUsed) {
+ throw TypeError(body.BodyUsedError);
+ }
+ b = input.body;
+ } else {
+ b = "";
+ }
+
+ let headers: domTypes.Headers;
+
+ // prefer headers from init
+ if (init.headers) {
+ headers = new Headers(init.headers);
+ } else if (input instanceof Request) {
+ headers = input.headers;
+ } else {
+ headers = new Headers();
+ }
+
+ const contentType = headers.get("content-type") || "";
+ super(b, contentType);
+ this.headers = headers;
+
+ // readonly attribute ByteString method;
+ /**
+ * The HTTP request method
+ * @readonly
+ * @default GET
+ * @type {string}
+ */
+ this.method = "GET";
+
+ // readonly attribute USVString url;
+ /**
+ * The request URL
+ * @readonly
+ * @type {string}
+ */
+ this.url = "";
+
+ // readonly attribute RequestCredentials credentials;
+ this.credentials = "omit";
+
+ if (input instanceof Request) {
+ if (input.bodyUsed) {
+ throw TypeError(body.BodyUsedError);
+ }
+ this.method = input.method;
+ this.url = input.url;
+ this.headers = new Headers(input.headers);
+ this.credentials = input.credentials;
+ this._stream = input._stream;
+ } else if (typeof input === "string") {
+ this.url = input;
+ }
+
+ if (init && "method" in init) {
+ this.method = normalizeMethod(init.method as string);
+ }
+
+ if (
+ init &&
+ "credentials" in init &&
+ init.credentials &&
+ ["omit", "same-origin", "include"].indexOf(init.credentials) !== -1
+ ) {
+ this.credentials = init.credentials;
+ }
+ }
+
+ public clone(): domTypes.Request {
+ if (this.bodyUsed) {
+ throw TypeError(body.BodyUsedError);
+ }
+
+ const iterators = this.headers.entries();
+ const headersList: Array<[string, string]> = [];
+ for (const header of iterators) {
+ headersList.push(header);
+ }
+
+ const body2 = this._bodySource;
+
+ const cloned = new Request(this.url, {
+ body: body2,
+ method: this.method,
+ headers: new Headers(headersList),
+ credentials: this.credentials
+ });
+ return cloned;
+ }
+}
diff --git a/cli/js/request_test.ts b/cli/js/request_test.ts
new file mode 100644
index 000000000..e9e1f5164
--- /dev/null
+++ b/cli/js/request_test.ts
@@ -0,0 +1,17 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assertEquals } from "./test_util.ts";
+
+test(function fromInit(): void {
+ const req = new Request("https://example.com", {
+ body: "ahoyhoy",
+ method: "POST",
+ headers: {
+ "test-header": "value"
+ }
+ });
+
+ // @ts-ignore
+ assertEquals("ahoyhoy", req._bodySource);
+ assertEquals(req.url, "https://example.com");
+ assertEquals(req.headers.get("test-header"), "value");
+});
diff --git a/cli/js/resources.ts b/cli/js/resources.ts
new file mode 100644
index 000000000..27598ce09
--- /dev/null
+++ b/cli/js/resources.ts
@@ -0,0 +1,19 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as dispatch from "./dispatch.ts";
+import { sendSync } from "./dispatch_json.ts";
+
+export interface ResourceMap {
+ [rid: number]: string;
+}
+
+/** Returns a map of open _file like_ resource ids along with their string
+ * representation.
+ */
+export function resources(): ResourceMap {
+ const res = sendSync(dispatch.OP_RESOURCES) as Array<[number, string]>;
+ const resources: ResourceMap = {};
+ for (const resourceTuple of res) {
+ resources[resourceTuple[0]] = resourceTuple[1];
+ }
+ return resources;
+}
diff --git a/cli/js/resources_test.ts b/cli/js/resources_test.ts
new file mode 100644
index 000000000..753ef3e17
--- /dev/null
+++ b/cli/js/resources_test.ts
@@ -0,0 +1,48 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, testPerm, assertEquals } from "./test_util.ts";
+
+test(function resourcesStdio(): void {
+ const res = Deno.resources();
+
+ assertEquals(res[0], "stdin");
+ assertEquals(res[1], "stdout");
+ assertEquals(res[2], "stderr");
+});
+
+testPerm({ net: true }, async function resourcesNet(): Promise<void> {
+ const listener = Deno.listen({ port: 4501 });
+ const dialerConn = await Deno.dial({ port: 4501 });
+ const listenerConn = await listener.accept();
+
+ const res = Deno.resources();
+ assertEquals(
+ Object.values(res).filter((r): boolean => r === "tcpListener").length,
+ 1
+ );
+ assertEquals(
+ Object.values(res).filter((r): boolean => r === "tcpStream").length,
+ 2
+ );
+
+ listenerConn.close();
+ dialerConn.close();
+ listener.close();
+});
+
+testPerm({ read: true }, async function resourcesFile(): Promise<void> {
+ const resourcesBefore = Deno.resources();
+ await Deno.open("tests/hello.txt");
+ const resourcesAfter = Deno.resources();
+
+ // check that exactly one new resource (file) was added
+ assertEquals(
+ Object.keys(resourcesAfter).length,
+ Object.keys(resourcesBefore).length + 1
+ );
+ const newRid = Object.keys(resourcesAfter).find(
+ (rid): boolean => {
+ return !resourcesBefore.hasOwnProperty(rid);
+ }
+ );
+ assertEquals(resourcesAfter[newRid], "fsFile");
+});
diff --git a/cli/js/stat.ts b/cli/js/stat.ts
new file mode 100644
index 000000000..1f53e6f7b
--- /dev/null
+++ b/cli/js/stat.ts
@@ -0,0 +1,73 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+import { FileInfo, FileInfoImpl } from "./file_info.ts";
+
+export interface StatResponse {
+ isFile: boolean;
+ isSymlink: boolean;
+ len: number;
+ modified: number;
+ accessed: number;
+ created: number;
+ mode: number;
+ hasMode: boolean; // false on windows
+ name: string | null;
+}
+
+/** Queries the file system for information on the path provided. If the given
+ * path is a symlink information about the symlink will be returned.
+ *
+ * const fileInfo = await Deno.lstat("hello.txt");
+ * assert(fileInfo.isFile());
+ */
+export async function lstat(filename: string): Promise<FileInfo> {
+ const res = (await sendAsync(dispatch.OP_STAT, {
+ filename,
+ lstat: true
+ })) as StatResponse;
+ return new FileInfoImpl(res);
+}
+
+/** Queries the file system for information on the path provided synchronously.
+ * If the given path is a symlink information about the symlink will be
+ * returned.
+ *
+ * const fileInfo = Deno.lstatSync("hello.txt");
+ * assert(fileInfo.isFile());
+ */
+export function lstatSync(filename: string): FileInfo {
+ const res = sendSync(dispatch.OP_STAT, {
+ filename,
+ lstat: true
+ }) as StatResponse;
+ return new FileInfoImpl(res);
+}
+
+/** Queries the file system for information on the path provided. `stat` Will
+ * always follow symlinks.
+ *
+ * const fileInfo = await Deno.stat("hello.txt");
+ * assert(fileInfo.isFile());
+ */
+export async function stat(filename: string): Promise<FileInfo> {
+ const res = (await sendAsync(dispatch.OP_STAT, {
+ filename,
+ lstat: false
+ })) as StatResponse;
+ return new FileInfoImpl(res);
+}
+
+/** Queries the file system for information on the path provided synchronously.
+ * `statSync` Will always follow symlinks.
+ *
+ * const fileInfo = Deno.statSync("hello.txt");
+ * assert(fileInfo.isFile());
+ */
+export function statSync(filename: string): FileInfo {
+ const res = sendSync(dispatch.OP_STAT, {
+ filename,
+ lstat: false
+ }) as StatResponse;
+ return new FileInfoImpl(res);
+}
diff --git a/cli/js/stat_test.ts b/cli/js/stat_test.ts
new file mode 100644
index 000000000..1542f1080
--- /dev/null
+++ b/cli/js/stat_test.ts
@@ -0,0 +1,172 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+// TODO Add tests for modified, accessed, and created fields once there is a way
+// to create temp files.
+testPerm({ read: true }, async function statSyncSuccess(): Promise<void> {
+ const packageInfo = Deno.statSync("package.json");
+ assert(packageInfo.isFile());
+ assert(!packageInfo.isSymlink());
+
+ const modulesInfo = Deno.statSync("node_modules");
+ assert(modulesInfo.isDirectory());
+ assert(!modulesInfo.isSymlink());
+
+ const testsInfo = Deno.statSync("tests");
+ assert(testsInfo.isDirectory());
+ assert(!testsInfo.isSymlink());
+});
+
+testPerm({ read: false }, async function statSyncPerm(): Promise<void> {
+ let caughtError = false;
+ try {
+ Deno.statSync("package.json");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true }, async function statSyncNotFound(): Promise<void> {
+ let caughtError = false;
+ let badInfo;
+
+ try {
+ badInfo = Deno.statSync("bad_file_name");
+ } catch (err) {
+ caughtError = true;
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+ }
+
+ assert(caughtError);
+ assertEquals(badInfo, undefined);
+});
+
+testPerm({ read: true }, async function lstatSyncSuccess(): Promise<void> {
+ const packageInfo = Deno.lstatSync("package.json");
+ assert(packageInfo.isFile());
+ assert(!packageInfo.isSymlink());
+
+ const modulesInfo = Deno.lstatSync("node_modules");
+ assert(!modulesInfo.isDirectory());
+ assert(modulesInfo.isSymlink());
+
+ const i = Deno.lstatSync("website");
+ assert(i.isDirectory());
+ assert(!i.isSymlink());
+});
+
+testPerm({ read: false }, async function lstatSyncPerm(): Promise<void> {
+ let caughtError = false;
+ try {
+ Deno.lstatSync("package.json");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true }, async function lstatSyncNotFound(): Promise<void> {
+ let caughtError = false;
+ let badInfo;
+
+ try {
+ badInfo = Deno.lstatSync("bad_file_name");
+ } catch (err) {
+ caughtError = true;
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+ }
+
+ assert(caughtError);
+ assertEquals(badInfo, undefined);
+});
+
+testPerm({ read: true }, async function statSuccess(): Promise<void> {
+ const packageInfo = await Deno.stat("package.json");
+ assert(packageInfo.isFile());
+ assert(!packageInfo.isSymlink());
+
+ const modulesInfo = await Deno.stat("node_modules");
+ assert(modulesInfo.isDirectory());
+ assert(!modulesInfo.isSymlink());
+
+ const i = await Deno.stat("tests");
+ assert(i.isDirectory());
+ assert(!i.isSymlink());
+});
+
+testPerm({ read: false }, async function statPerm(): Promise<void> {
+ let caughtError = false;
+ try {
+ await Deno.stat("package.json");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true }, async function statNotFound(): Promise<void> {
+ let caughtError = false;
+ let badInfo;
+
+ try {
+ badInfo = await Deno.stat("bad_file_name");
+ } catch (err) {
+ caughtError = true;
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+ }
+
+ assert(caughtError);
+ assertEquals(badInfo, undefined);
+});
+
+testPerm({ read: true }, async function lstatSuccess(): Promise<void> {
+ const packageInfo = await Deno.lstat("package.json");
+ assert(packageInfo.isFile());
+ assert(!packageInfo.isSymlink());
+
+ const modulesInfo = await Deno.lstat("node_modules");
+ assert(!modulesInfo.isDirectory());
+ assert(modulesInfo.isSymlink());
+
+ const i = await Deno.lstat("website");
+ assert(i.isDirectory());
+ assert(!i.isSymlink());
+});
+
+testPerm({ read: false }, async function lstatPerm(): Promise<void> {
+ let caughtError = false;
+ try {
+ await Deno.lstat("package.json");
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true }, async function lstatNotFound(): Promise<void> {
+ let caughtError = false;
+ let badInfo;
+
+ try {
+ badInfo = await Deno.lstat("bad_file_name");
+ } catch (err) {
+ caughtError = true;
+ assertEquals(err.kind, Deno.ErrorKind.NotFound);
+ assertEquals(err.name, "NotFound");
+ }
+
+ assert(caughtError);
+ assertEquals(badInfo, undefined);
+});
diff --git a/cli/js/symlink.ts b/cli/js/symlink.ts
new file mode 100644
index 000000000..21ebb2f59
--- /dev/null
+++ b/cli/js/symlink.ts
@@ -0,0 +1,39 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+import * as util from "./util.ts";
+import { build } from "./build.ts";
+
+/** Synchronously creates `newname` as a symbolic link to `oldname`. The type
+ * argument can be set to `dir` or `file` and is only available on Windows
+ * (ignored on other platforms).
+ *
+ * Deno.symlinkSync("old/name", "new/name");
+ */
+export function symlinkSync(
+ oldname: string,
+ newname: string,
+ type?: string
+): void {
+ if (build.os === "win" && type) {
+ return util.notImplemented();
+ }
+ sendSync(dispatch.OP_SYMLINK, { oldname, newname });
+}
+
+/** Creates `newname` as a symbolic link to `oldname`. The type argument can be
+ * set to `dir` or `file` and is only available on Windows (ignored on other
+ * platforms).
+ *
+ * await Deno.symlink("old/name", "new/name");
+ */
+export async function symlink(
+ oldname: string,
+ newname: string,
+ type?: string
+): Promise<void> {
+ if (build.os === "win" && type) {
+ return util.notImplemented();
+ }
+ await sendAsync(dispatch.OP_SYMLINK, { oldname, newname });
+}
diff --git a/cli/js/symlink_test.ts b/cli/js/symlink_test.ts
new file mode 100644
index 000000000..bce1f6ae5
--- /dev/null
+++ b/cli/js/symlink_test.ts
@@ -0,0 +1,80 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, testPerm, assert, assertEquals } from "./test_util.ts";
+
+testPerm({ read: true, write: true }, function symlinkSyncSuccess(): void {
+ const testDir = Deno.makeTempDirSync();
+ const oldname = testDir + "/oldname";
+ const newname = testDir + "/newname";
+ Deno.mkdirSync(oldname);
+ let errOnWindows;
+ // Just for now, until we implement symlink for Windows.
+ try {
+ Deno.symlinkSync(oldname, newname);
+ } catch (e) {
+ errOnWindows = e;
+ }
+ if (errOnWindows) {
+ assertEquals(Deno.build.os, "win");
+ assertEquals(errOnWindows.kind, Deno.ErrorKind.Other);
+ assertEquals(errOnWindows.message, "Not implemented");
+ } else {
+ const newNameInfoLStat = Deno.lstatSync(newname);
+ const newNameInfoStat = Deno.statSync(newname);
+ assert(newNameInfoLStat.isSymlink());
+ assert(newNameInfoStat.isDirectory());
+ }
+});
+
+test(function symlinkSyncPerm(): void {
+ let err;
+ try {
+ Deno.symlinkSync("oldbaddir", "newbaddir");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+// Just for now, until we implement symlink for Windows.
+// Symlink with type should succeed on other platforms with type ignored
+testPerm({ write: true }, function symlinkSyncNotImplemented(): void {
+ const testDir = Deno.makeTempDirSync();
+ const oldname = testDir + "/oldname";
+ const newname = testDir + "/newname";
+ let err;
+ try {
+ Deno.symlinkSync(oldname, newname, "dir");
+ } catch (e) {
+ err = e;
+ }
+ if (err) {
+ assertEquals(Deno.build.os, "win");
+ assertEquals(err.message, "Not implemented");
+ }
+});
+
+testPerm({ read: true, write: true }, async function symlinkSuccess(): Promise<
+ void
+> {
+ const testDir = Deno.makeTempDirSync();
+ const oldname = testDir + "/oldname";
+ const newname = testDir + "/newname";
+ Deno.mkdirSync(oldname);
+ let errOnWindows;
+ // Just for now, until we implement symlink for Windows.
+ try {
+ await Deno.symlink(oldname, newname);
+ } catch (e) {
+ errOnWindows = e;
+ }
+ if (errOnWindows) {
+ assertEquals(errOnWindows.kind, Deno.ErrorKind.Other);
+ assertEquals(errOnWindows.message, "Not implemented");
+ } else {
+ const newNameInfoLStat = Deno.lstatSync(newname);
+ const newNameInfoStat = Deno.statSync(newname);
+ assert(newNameInfoLStat.isSymlink());
+ assert(newNameInfoStat.isDirectory());
+ }
+});
diff --git a/cli/js/test_util.ts b/cli/js/test_util.ts
new file mode 100644
index 000000000..2f2916e11
--- /dev/null
+++ b/cli/js/test_util.ts
@@ -0,0 +1,262 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+//
+// We want to test many ops in deno which have different behavior depending on
+// the permissions set. These tests can specify which permissions they expect,
+// which appends a special string like "permW1N0" to the end of the test name.
+// Here we run several copies of deno with different permissions, filtering the
+// tests by the special string. permW1N0 means allow-write but not allow-net.
+// See tools/unit_tests.py for more details.
+
+import * as testing from "../../std/testing/mod.ts";
+import { assert, assertEquals } from "../../std/testing/asserts.ts";
+export {
+ assert,
+ assertThrows,
+ assertEquals,
+ assertMatch,
+ assertNotEquals,
+ assertStrictEq,
+ assertStrContains,
+ unreachable
+} from "../../std/testing/asserts.ts";
+
+interface TestPermissions {
+ read?: boolean;
+ write?: boolean;
+ net?: boolean;
+ env?: boolean;
+ run?: boolean;
+ hrtime?: boolean;
+}
+
+const processPerms = Deno.permissions();
+
+function permissionsMatch(
+ processPerms: Deno.Permissions,
+ requiredPerms: Deno.Permissions
+): boolean {
+ for (const permName in processPerms) {
+ if (processPerms[permName] !== requiredPerms[permName]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+export const permissionCombinations: Map<string, Deno.Permissions> = new Map();
+
+function permToString(perms: Deno.Permissions): string {
+ const r = perms.read ? 1 : 0;
+ const w = perms.write ? 1 : 0;
+ const n = perms.net ? 1 : 0;
+ const e = perms.env ? 1 : 0;
+ const u = perms.run ? 1 : 0;
+ const h = perms.hrtime ? 1 : 0;
+ return `permR${r}W${w}N${n}E${e}U${u}H${h}`;
+}
+
+function registerPermCombination(perms: Deno.Permissions): void {
+ const key = permToString(perms);
+ if (!permissionCombinations.has(key)) {
+ permissionCombinations.set(key, perms);
+ }
+}
+
+function normalizeTestPermissions(perms: TestPermissions): Deno.Permissions {
+ return {
+ read: !!perms.read,
+ write: !!perms.write,
+ net: !!perms.net,
+ run: !!perms.run,
+ env: !!perms.env,
+ hrtime: !!perms.hrtime
+ };
+}
+
+export function testPerm(
+ perms: TestPermissions,
+ fn: testing.TestFunction
+): void {
+ const normalizedPerms = normalizeTestPermissions(perms);
+
+ registerPermCombination(normalizedPerms);
+
+ if (!permissionsMatch(processPerms, normalizedPerms)) {
+ return;
+ }
+
+ testing.test(fn);
+}
+
+export function test(fn: testing.TestFunction): void {
+ testPerm(
+ {
+ read: false,
+ write: false,
+ net: false,
+ env: false,
+ run: false,
+ hrtime: false
+ },
+ fn
+ );
+}
+
+function extractNumber(re: RegExp, str: string): number | undefined {
+ const match = str.match(re);
+
+ if (match) {
+ return Number.parseInt(match[1]);
+ }
+}
+
+export function parseUnitTestOutput(
+ rawOutput: Uint8Array,
+ print: boolean
+): { actual?: number; expected?: number; resultOutput?: string } {
+ const decoder = new TextDecoder();
+ const output = decoder.decode(rawOutput);
+
+ let expected, actual, result;
+
+ for (const line of output.split("\n")) {
+ if (!expected) {
+ // expect "running 30 tests"
+ expected = extractNumber(/running (\d+) tests/, line);
+ } else if (line.indexOf("test result:") !== -1) {
+ result = line;
+ }
+
+ if (print) {
+ console.log(line);
+ }
+ }
+
+ // Check that the number of expected tests equals what was reported at the
+ // bottom.
+ if (result) {
+ // result should be a string like this:
+ // "test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; ..."
+ actual = extractNumber(/(\d+) passed/, result);
+ }
+
+ return { actual, expected, resultOutput: result };
+}
+
+test(function permissionsMatches(): void {
+ assert(
+ permissionsMatch(
+ {
+ read: true,
+ write: false,
+ net: false,
+ env: false,
+ run: false,
+ hrtime: false
+ },
+ normalizeTestPermissions({ read: true })
+ )
+ );
+
+ assert(
+ permissionsMatch(
+ {
+ read: false,
+ write: false,
+ net: false,
+ env: false,
+ run: false,
+ hrtime: false
+ },
+ normalizeTestPermissions({})
+ )
+ );
+
+ assertEquals(
+ permissionsMatch(
+ {
+ read: false,
+ write: true,
+ net: true,
+ env: true,
+ run: true,
+ hrtime: true
+ },
+ normalizeTestPermissions({ read: true })
+ ),
+ false
+ );
+
+ assertEquals(
+ permissionsMatch(
+ {
+ read: true,
+ write: false,
+ net: true,
+ env: false,
+ run: false,
+ hrtime: false
+ },
+ normalizeTestPermissions({ read: true })
+ ),
+ false
+ );
+
+ assert(
+ permissionsMatch(
+ {
+ read: true,
+ write: true,
+ net: true,
+ env: true,
+ run: true,
+ hrtime: true
+ },
+ {
+ read: true,
+ write: true,
+ net: true,
+ env: true,
+ run: true,
+ hrtime: true
+ }
+ )
+ );
+});
+
+testPerm({ read: true }, async function parsingUnitTestOutput(): Promise<void> {
+ const cwd = Deno.cwd();
+ const testDataPath = `${cwd}/tools/testdata/`;
+
+ let result;
+
+ // This is an example of a successful unit test output.
+ result = parseUnitTestOutput(
+ await Deno.readFile(`${testDataPath}/unit_test_output1.txt`),
+ false
+ );
+ assertEquals(result.actual, 96);
+ assertEquals(result.expected, 96);
+
+ // This is an example of a silently dying unit test.
+ result = parseUnitTestOutput(
+ await Deno.readFile(`${testDataPath}/unit_test_output2.txt`),
+ false
+ );
+ assertEquals(result.actual, undefined);
+ assertEquals(result.expected, 96);
+
+ // This is an example of compiling before successful unit tests.
+ result = parseUnitTestOutput(
+ await Deno.readFile(`${testDataPath}/unit_test_output3.txt`),
+ false
+ );
+ assertEquals(result.actual, 96);
+ assertEquals(result.expected, 96);
+
+ // Check what happens on empty output.
+ result = parseUnitTestOutput(new TextEncoder().encode("\n\n\n"), false);
+ assertEquals(result.actual, undefined);
+ assertEquals(result.expected, undefined);
+});
diff --git a/cli/js/text_encoding.ts b/cli/js/text_encoding.ts
new file mode 100644
index 000000000..8386ff8b0
--- /dev/null
+++ b/cli/js/text_encoding.ts
@@ -0,0 +1,554 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+// The following code is based off of text-encoding at:
+// https://github.com/inexorabletash/text-encoding
+//
+// Anyone is free to copy, modify, publish, use, compile, sell, or
+// distribute this software, either in source code form or as a compiled
+// binary, for any purpose, commercial or non-commercial, and by any
+// means.
+//
+// In jurisdictions that recognize copyright laws, the author or authors
+// of this software dedicate any and all copyright interest in the
+// software to the public domain. We make this dedication for the benefit
+// of the public at large and to the detriment of our heirs and
+// successors. We intend this dedication to be an overt act of
+// relinquishment in perpetuity of all present and future rights to this
+// software under copyright law.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+
+import * as base64 from "./base64.ts";
+import * as domTypes from "./dom_types.ts";
+import { DenoError, ErrorKind } from "./errors.ts";
+
+const CONTINUE = null;
+const END_OF_STREAM = -1;
+const FINISHED = -1;
+
+function decoderError(fatal: boolean): number | never {
+ if (fatal) {
+ throw new TypeError("Decoder error.");
+ }
+ return 0xfffd; // default code point
+}
+
+function inRange(a: number, min: number, max: number): boolean {
+ return min <= a && a <= max;
+}
+
+function isASCIIByte(a: number): boolean {
+ return inRange(a, 0x00, 0x7f);
+}
+
+function stringToCodePoints(input: string): number[] {
+ const u: number[] = [];
+ for (const c of input) {
+ u.push(c.codePointAt(0)!);
+ }
+ return u;
+}
+
+class UTF8Decoder implements Decoder {
+ private _codePoint = 0;
+ private _bytesSeen = 0;
+ private _bytesNeeded = 0;
+ private _fatal: boolean;
+ private _ignoreBOM: boolean;
+ private _lowerBoundary = 0x80;
+ private _upperBoundary = 0xbf;
+
+ constructor(options: DecoderOptions) {
+ this._fatal = options.fatal || false;
+ this._ignoreBOM = options.ignoreBOM || false;
+ }
+
+ handler(stream: Stream, byte: number): number | null {
+ if (byte === END_OF_STREAM && this._bytesNeeded !== 0) {
+ this._bytesNeeded = 0;
+ return decoderError(this._fatal);
+ }
+
+ if (byte === END_OF_STREAM) {
+ return FINISHED;
+ }
+
+ if (this._ignoreBOM) {
+ if (
+ (this._bytesSeen === 0 && byte !== 0xef) ||
+ (this._bytesSeen === 1 && byte !== 0xbb)
+ ) {
+ this._ignoreBOM = false;
+ }
+
+ if (this._bytesSeen === 2) {
+ this._ignoreBOM = false;
+ if (byte === 0xbf) {
+ //Ignore BOM
+ this._codePoint = 0;
+ this._bytesNeeded = 0;
+ this._bytesSeen = 0;
+ return CONTINUE;
+ }
+ }
+ }
+
+ if (this._bytesNeeded === 0) {
+ if (isASCIIByte(byte)) {
+ // Single byte code point
+ return byte;
+ } else if (inRange(byte, 0xc2, 0xdf)) {
+ // Two byte code point
+ this._bytesNeeded = 1;
+ this._codePoint = byte & 0x1f;
+ } else if (inRange(byte, 0xe0, 0xef)) {
+ // Three byte code point
+ if (byte === 0xe0) {
+ this._lowerBoundary = 0xa0;
+ } else if (byte === 0xed) {
+ this._upperBoundary = 0x9f;
+ }
+ this._bytesNeeded = 2;
+ this._codePoint = byte & 0xf;
+ } else if (inRange(byte, 0xf0, 0xf4)) {
+ if (byte === 0xf0) {
+ this._lowerBoundary = 0x90;
+ } else if (byte === 0xf4) {
+ this._upperBoundary = 0x8f;
+ }
+ this._bytesNeeded = 3;
+ this._codePoint = byte & 0x7;
+ } else {
+ return decoderError(this._fatal);
+ }
+ return CONTINUE;
+ }
+
+ if (!inRange(byte, this._lowerBoundary, this._upperBoundary)) {
+ // Byte out of range, so encoding error
+ this._codePoint = 0;
+ this._bytesNeeded = 0;
+ this._bytesSeen = 0;
+ stream.prepend(byte);
+ return decoderError(this._fatal);
+ }
+
+ this._lowerBoundary = 0x80;
+ this._upperBoundary = 0xbf;
+
+ this._codePoint = (this._codePoint << 6) | (byte & 0x3f);
+
+ this._bytesSeen++;
+
+ if (this._bytesSeen !== this._bytesNeeded) {
+ return CONTINUE;
+ }
+
+ const codePoint = this._codePoint;
+
+ this._codePoint = 0;
+ this._bytesNeeded = 0;
+ this._bytesSeen = 0;
+
+ return codePoint;
+ }
+}
+
+class UTF8Encoder implements Encoder {
+ handler(codePoint: number): number | number[] {
+ if (codePoint === END_OF_STREAM) {
+ return FINISHED;
+ }
+
+ if (inRange(codePoint, 0x00, 0x7f)) {
+ return codePoint;
+ }
+
+ let count: number;
+ let offset: number;
+ if (inRange(codePoint, 0x0080, 0x07ff)) {
+ count = 1;
+ offset = 0xc0;
+ } else if (inRange(codePoint, 0x0800, 0xffff)) {
+ count = 2;
+ offset = 0xe0;
+ } else if (inRange(codePoint, 0x10000, 0x10ffff)) {
+ count = 3;
+ offset = 0xf0;
+ } else {
+ throw TypeError(`Code point out of range: \\x${codePoint.toString(16)}`);
+ }
+
+ const bytes = [(codePoint >> (6 * count)) + offset];
+
+ while (count > 0) {
+ const temp = codePoint >> (6 * (count - 1));
+ bytes.push(0x80 | (temp & 0x3f));
+ count--;
+ }
+
+ return bytes;
+ }
+}
+
+/** Decodes a string of data which has been encoded using base-64. */
+export function atob(s: string): string {
+ s = String(s);
+ s = s.replace(/[\t\n\f\r ]/g, "");
+
+ if (s.length % 4 === 0) {
+ s = s.replace(/==?$/, "");
+ }
+
+ const rem = s.length % 4;
+ if (rem === 1 || /[^+/0-9A-Za-z]/.test(s)) {
+ // TODO: throw `DOMException`
+ throw new DenoError(
+ ErrorKind.InvalidInput,
+ "The string to be decoded is not correctly encoded"
+ );
+ }
+
+ // base64-js requires length exactly times of 4
+ if (rem > 0) {
+ s = s.padEnd(s.length + (4 - rem), "=");
+ }
+
+ const byteArray: Uint8Array = base64.toByteArray(s);
+ let result = "";
+ for (let i = 0; i < byteArray.length; i++) {
+ result += String.fromCharCode(byteArray[i]);
+ }
+ return result;
+}
+
+/** Creates a base-64 ASCII string from the input string. */
+export function btoa(s: string): string {
+ const byteArray = [];
+ for (let i = 0; i < s.length; i++) {
+ const charCode = s[i].charCodeAt(0);
+ if (charCode > 0xff) {
+ throw new DenoError(
+ ErrorKind.InvalidInput,
+ "The string to be encoded contains characters " +
+ "outside of the Latin1 range."
+ );
+ }
+ byteArray.push(charCode);
+ }
+ const result = base64.fromByteArray(Uint8Array.from(byteArray));
+ return result;
+}
+
+interface DecoderOptions {
+ fatal?: boolean;
+ ignoreBOM?: boolean;
+}
+
+interface Decoder {
+ handler(stream: Stream, byte: number): number | null;
+}
+
+interface Encoder {
+ handler(codePoint: number): number | number[];
+}
+
+class SingleByteDecoder implements Decoder {
+ private _index: number[];
+ private _fatal: boolean;
+
+ constructor(index: number[], options: DecoderOptions) {
+ if (options.ignoreBOM) {
+ throw new TypeError("Ignoring the BOM is available only with utf-8.");
+ }
+ this._fatal = options.fatal || false;
+ this._index = index;
+ }
+ handler(stream: Stream, byte: number): number {
+ if (byte === END_OF_STREAM) {
+ return FINISHED;
+ }
+ if (isASCIIByte(byte)) {
+ return byte;
+ }
+ const codePoint = this._index[byte - 0x80];
+
+ if (codePoint == null) {
+ return decoderError(this._fatal);
+ }
+
+ return codePoint;
+ }
+}
+
+// The encodingMap is a hash of labels that are indexed by the conical
+// encoding.
+const encodingMap: { [key: string]: string[] } = {
+ "windows-1252": [
+ "ansi_x3.4-1968",
+ "ascii",
+ "cp1252",
+ "cp819",
+ "csisolatin1",
+ "ibm819",
+ "iso-8859-1",
+ "iso-ir-100",
+ "iso8859-1",
+ "iso88591",
+ "iso_8859-1",
+ "iso_8859-1:1987",
+ "l1",
+ "latin1",
+ "us-ascii",
+ "windows-1252",
+ "x-cp1252"
+ ],
+ "utf-8": ["unicode-1-1-utf-8", "utf-8", "utf8"]
+};
+// We convert these into a Map where every label resolves to its canonical
+// encoding type.
+const encodings = new Map<string, string>();
+for (const key of Object.keys(encodingMap)) {
+ const labels = encodingMap[key];
+ for (const label of labels) {
+ encodings.set(label, key);
+ }
+}
+
+// A map of functions that return new instances of a decoder indexed by the
+// encoding type.
+const decoders = new Map<string, (options: DecoderOptions) => Decoder>();
+decoders.set(
+ "utf-8",
+ (options: DecoderOptions): UTF8Decoder => {
+ return new UTF8Decoder(options);
+ }
+);
+
+// Single byte decoders are an array of code point lookups
+const encodingIndexes = new Map<string, number[]>();
+// prettier-ignore
+encodingIndexes.set("windows-1252", [8364,129,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,141,381,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,157,382,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255]);
+for (const [key, index] of encodingIndexes) {
+ decoders.set(
+ key,
+ (options: DecoderOptions): SingleByteDecoder => {
+ return new SingleByteDecoder(index, options);
+ }
+ );
+}
+
+function codePointsToString(codePoints: number[]): string {
+ let s = "";
+ for (const cp of codePoints) {
+ s += String.fromCodePoint(cp);
+ }
+ return s;
+}
+
+class Stream {
+ private _tokens: number[];
+ constructor(tokens: number[] | Uint8Array) {
+ this._tokens = [].slice.call(tokens);
+ this._tokens.reverse();
+ }
+
+ endOfStream(): boolean {
+ return !this._tokens.length;
+ }
+
+ read(): number {
+ return !this._tokens.length ? END_OF_STREAM : this._tokens.pop()!;
+ }
+
+ prepend(token: number | number[]): void {
+ if (Array.isArray(token)) {
+ while (token.length) {
+ this._tokens.push(token.pop()!);
+ }
+ } else {
+ this._tokens.push(token);
+ }
+ }
+
+ push(token: number | number[]): void {
+ if (Array.isArray(token)) {
+ while (token.length) {
+ this._tokens.unshift(token.shift()!);
+ }
+ } else {
+ this._tokens.unshift(token);
+ }
+ }
+}
+
+export interface TextDecodeOptions {
+ stream?: false;
+}
+
+export interface TextDecoderOptions {
+ fatal?: boolean;
+ ignoreBOM?: boolean;
+}
+
+type EitherArrayBuffer = SharedArrayBuffer | ArrayBuffer;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function isEitherArrayBuffer(x: any): x is EitherArrayBuffer {
+ return x instanceof SharedArrayBuffer || x instanceof ArrayBuffer;
+}
+
+export class TextDecoder {
+ private _encoding: string;
+
+ /** Returns encoding's name, lowercased. */
+ get encoding(): string {
+ return this._encoding;
+ }
+ /** Returns `true` if error mode is "fatal", and `false` otherwise. */
+ readonly fatal: boolean = false;
+ /** Returns `true` if ignore BOM flag is set, and `false` otherwise. */
+ readonly ignoreBOM: boolean = false;
+
+ constructor(label = "utf-8", options: TextDecoderOptions = { fatal: false }) {
+ if (options.ignoreBOM) {
+ this.ignoreBOM = true;
+ }
+ if (options.fatal) {
+ this.fatal = true;
+ }
+ label = String(label)
+ .trim()
+ .toLowerCase();
+ const encoding = encodings.get(label);
+ if (!encoding) {
+ throw new RangeError(
+ `The encoding label provided ('${label}') is invalid.`
+ );
+ }
+ if (!decoders.has(encoding)) {
+ throw new TypeError(`Internal decoder ('${encoding}') not found.`);
+ }
+ this._encoding = encoding;
+ }
+
+ /** Returns the result of running encoding's decoder. */
+ decode(
+ input?: domTypes.BufferSource,
+ options: TextDecodeOptions = { stream: false }
+ ): string {
+ if (options.stream) {
+ throw new TypeError("Stream not supported.");
+ }
+
+ let bytes: Uint8Array;
+ if (input instanceof Uint8Array) {
+ bytes = input;
+ } else if (isEitherArrayBuffer(input)) {
+ bytes = new Uint8Array(input);
+ } else if (
+ typeof input === "object" &&
+ "buffer" in input &&
+ isEitherArrayBuffer(input.buffer)
+ ) {
+ bytes = new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
+ } else {
+ bytes = new Uint8Array(0);
+ }
+
+ const decoder = decoders.get(this._encoding)!({
+ fatal: this.fatal,
+ ignoreBOM: this.ignoreBOM
+ });
+ const inputStream = new Stream(bytes);
+ const output: number[] = [];
+
+ while (true) {
+ const result = decoder.handler(inputStream, inputStream.read());
+ if (result === FINISHED) {
+ break;
+ }
+
+ if (result !== CONTINUE) {
+ output.push(result);
+ }
+ }
+
+ if (output.length > 0 && output[0] === 0xfeff) {
+ output.shift();
+ }
+
+ return codePointsToString(output);
+ }
+ get [Symbol.toStringTag](): string {
+ return "TextDecoder";
+ }
+}
+
+interface TextEncoderEncodeIntoResult {
+ read: number;
+ written: number;
+}
+
+export class TextEncoder {
+ /** Returns "utf-8". */
+ readonly encoding = "utf-8";
+ /** Returns the result of running UTF-8's encoder. */
+ encode(input = ""): Uint8Array {
+ const encoder = new UTF8Encoder();
+ const inputStream = new Stream(stringToCodePoints(input));
+ const output: number[] = [];
+
+ while (true) {
+ const result = encoder.handler(inputStream.read());
+ if (result === FINISHED) {
+ break;
+ }
+ if (Array.isArray(result)) {
+ output.push(...result);
+ } else {
+ output.push(result);
+ }
+ }
+
+ return new Uint8Array(output);
+ }
+ encodeInto(input: string, dest: Uint8Array): TextEncoderEncodeIntoResult {
+ const encoder = new UTF8Encoder();
+ const inputStream = new Stream(stringToCodePoints(input));
+
+ let written = 0;
+ let read = 0;
+ while (true) {
+ const result = encoder.handler(inputStream.read());
+ if (result === FINISHED) {
+ break;
+ }
+ read++;
+ if (Array.isArray(result)) {
+ dest.set(result, written);
+ written += result.length;
+ if (result.length > 3) {
+ // increment read a second time if greater than U+FFFF
+ read++;
+ }
+ } else {
+ dest[written] = result;
+ written++;
+ }
+ }
+
+ return {
+ read,
+ written
+ };
+ }
+ get [Symbol.toStringTag](): string {
+ return "TextEncoder";
+ }
+}
diff --git a/cli/js/text_encoding_test.ts b/cli/js/text_encoding_test.ts
new file mode 100644
index 000000000..aaa9e6b9d
--- /dev/null
+++ b/cli/js/text_encoding_test.ts
@@ -0,0 +1,193 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEquals } from "./test_util.ts";
+
+test(function btoaSuccess(): void {
+ const text = "hello world";
+ const encoded = btoa(text);
+ assertEquals(encoded, "aGVsbG8gd29ybGQ=");
+});
+
+test(function atobSuccess(): void {
+ const encoded = "aGVsbG8gd29ybGQ=";
+ const decoded = atob(encoded);
+ assertEquals(decoded, "hello world");
+});
+
+test(function atobWithAsciiWhitespace(): void {
+ const encodedList = [
+ " aGVsbG8gd29ybGQ=",
+ " aGVsbG8gd29ybGQ=",
+ "aGVsbG8gd29ybGQ= ",
+ "aGVsbG8gd29ybGQ=\n",
+ "aGVsbG\t8gd29ybGQ=",
+ `aGVsbG\t8g
+ d29ybGQ=`
+ ];
+
+ for (const encoded of encodedList) {
+ const decoded = atob(encoded);
+ assertEquals(decoded, "hello world");
+ }
+});
+
+test(function atobThrows(): void {
+ let threw = false;
+ try {
+ atob("aGVsbG8gd29ybGQ==");
+ } catch (e) {
+ threw = true;
+ }
+ assert(threw);
+});
+
+test(function atobThrows2(): void {
+ let threw = false;
+ try {
+ atob("aGVsbG8gd29ybGQ===");
+ } catch (e) {
+ threw = true;
+ }
+ assert(threw);
+});
+
+test(function btoaFailed(): void {
+ const text = "你好";
+ let err;
+ try {
+ btoa(text);
+ } catch (e) {
+ err = e;
+ }
+ assert(!!err);
+ assertEquals(err.name, "InvalidInput");
+});
+
+test(function textDecoder2(): void {
+ // prettier-ignore
+ const fixture = new Uint8Array([
+ 0xf0, 0x9d, 0x93, 0xbd,
+ 0xf0, 0x9d, 0x93, 0xae,
+ 0xf0, 0x9d, 0x94, 0x81,
+ 0xf0, 0x9d, 0x93, 0xbd
+ ]);
+ const decoder = new TextDecoder();
+ assertEquals(decoder.decode(fixture), "𝓽𝓮𝔁𝓽");
+});
+
+test(function textDecoderIgnoreBOM(): void {
+ // prettier-ignore
+ const fixture = new Uint8Array([
+ 0xef, 0xbb, 0xbf,
+ 0xf0, 0x9d, 0x93, 0xbd,
+ 0xf0, 0x9d, 0x93, 0xae,
+ 0xf0, 0x9d, 0x94, 0x81,
+ 0xf0, 0x9d, 0x93, 0xbd
+ ]);
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
+ assertEquals(decoder.decode(fixture), "𝓽𝓮𝔁𝓽");
+});
+
+test(function textDecoderNotBOM(): void {
+ // prettier-ignore
+ const fixture = new Uint8Array([
+ 0xef, 0xbb, 0x89,
+ 0xf0, 0x9d, 0x93, 0xbd,
+ 0xf0, 0x9d, 0x93, 0xae,
+ 0xf0, 0x9d, 0x94, 0x81,
+ 0xf0, 0x9d, 0x93, 0xbd
+ ]);
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
+ assertEquals(decoder.decode(fixture), "ﻉ𝓽𝓮𝔁𝓽");
+});
+
+test(function textDecoderASCII(): void {
+ const fixture = new Uint8Array([0x89, 0x95, 0x9f, 0xbf]);
+ const decoder = new TextDecoder("ascii");
+ assertEquals(decoder.decode(fixture), "‰•Ÿ¿");
+});
+
+test(function textDecoderErrorEncoding(): void {
+ let didThrow = false;
+ try {
+ new TextDecoder("foo");
+ } catch (e) {
+ didThrow = true;
+ assertEquals(e.message, "The encoding label provided ('foo') is invalid.");
+ }
+ assert(didThrow);
+});
+
+test(function textEncoder(): void {
+ const fixture = "𝓽𝓮𝔁𝓽";
+ const encoder = new TextEncoder();
+ // prettier-ignore
+ assertEquals(Array.from(encoder.encode(fixture)), [
+ 0xf0, 0x9d, 0x93, 0xbd,
+ 0xf0, 0x9d, 0x93, 0xae,
+ 0xf0, 0x9d, 0x94, 0x81,
+ 0xf0, 0x9d, 0x93, 0xbd
+ ]);
+});
+
+test(function textEncodeInto(): void {
+ const fixture = "text";
+ const encoder = new TextEncoder();
+ const bytes = new Uint8Array(5);
+ const result = encoder.encodeInto(fixture, bytes);
+ assertEquals(result.read, 4);
+ assertEquals(result.written, 4);
+ // prettier-ignore
+ assertEquals(Array.from(bytes), [
+ 0x74, 0x65, 0x78, 0x74, 0x00,
+ ]);
+});
+
+test(function textEncodeInto2(): void {
+ const fixture = "𝓽𝓮𝔁𝓽";
+ const encoder = new TextEncoder();
+ const bytes = new Uint8Array(17);
+ const result = encoder.encodeInto(fixture, bytes);
+ assertEquals(result.read, 8);
+ assertEquals(result.written, 16);
+ // prettier-ignore
+ assertEquals(Array.from(bytes), [
+ 0xf0, 0x9d, 0x93, 0xbd,
+ 0xf0, 0x9d, 0x93, 0xae,
+ 0xf0, 0x9d, 0x94, 0x81,
+ 0xf0, 0x9d, 0x93, 0xbd, 0x00,
+ ]);
+});
+
+test(function textDecoderSharedUint8Array(): void {
+ const ab = new SharedArrayBuffer(6);
+ const dataView = new DataView(ab);
+ const charCodeA = "A".charCodeAt(0);
+ for (let i = 0; i < ab.byteLength; i++) {
+ dataView.setUint8(i, charCodeA + i);
+ }
+ const ui8 = new Uint8Array(ab);
+ const decoder = new TextDecoder();
+ const actual = decoder.decode(ui8);
+ assertEquals(actual, "ABCDEF");
+});
+
+test(function textDecoderSharedInt32Array(): void {
+ const ab = new SharedArrayBuffer(8);
+ const dataView = new DataView(ab);
+ const charCodeA = "A".charCodeAt(0);
+ for (let i = 0; i < ab.byteLength; i++) {
+ dataView.setUint8(i, charCodeA + i);
+ }
+ const i32 = new Int32Array(ab);
+ const decoder = new TextDecoder();
+ const actual = decoder.decode(i32);
+ assertEquals(actual, "ABCDEFGH");
+});
+
+test(function toStringShouldBeWebCompatibility(): void {
+ const encoder = new TextEncoder();
+ assertEquals(encoder.toString(), "[object TextEncoder]");
+
+ const decoder = new TextDecoder();
+ assertEquals(decoder.toString(), "[object TextDecoder]");
+});
diff --git a/cli/js/timers.ts b/cli/js/timers.ts
new file mode 100644
index 000000000..5bc4922e3
--- /dev/null
+++ b/cli/js/timers.ts
@@ -0,0 +1,280 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { assert } from "./util.ts";
+import { window } from "./window.ts";
+import * as dispatch from "./dispatch.ts";
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+
+const { console } = window;
+
+interface Timer {
+ id: number;
+ callback: () => void;
+ delay: number;
+ due: number;
+ repeat: boolean;
+ scheduled: boolean;
+}
+
+// We'll subtract EPOCH every time we retrieve the time with Date.now(). This
+// ensures that absolute time values stay below UINT32_MAX - 2, which is the
+// maximum object key that EcmaScript considers "numerical". After running for
+// about a month, this is no longer true, and Deno explodes.
+// TODO(piscisaureus): fix that ^.
+const EPOCH = Date.now();
+const APOCALYPSE = 2 ** 32 - 2;
+
+// Timeout values > TIMEOUT_MAX are set to 1.
+const TIMEOUT_MAX = 2 ** 31 - 1;
+
+let globalTimeoutDue: number | null = null;
+
+let nextTimerId = 1;
+const idMap = new Map<number, Timer>();
+const dueMap: { [due: number]: Timer[] } = Object.create(null);
+
+function getTime(): number {
+ // TODO: use a monotonic clock.
+ const now = Date.now() - EPOCH;
+ assert(now >= 0 && now < APOCALYPSE);
+ return now;
+}
+
+function clearGlobalTimeout(): void {
+ globalTimeoutDue = null;
+ sendSync(dispatch.OP_GLOBAL_TIMER_STOP);
+}
+
+async function setGlobalTimeout(due: number, now: number): Promise<void> {
+ // Since JS and Rust don't use the same clock, pass the time to rust as a
+ // relative time value. On the Rust side we'll turn that into an absolute
+ // value again.
+ const timeout = due - now;
+ assert(timeout >= 0);
+
+ // Send message to the backend.
+ globalTimeoutDue = due;
+ await sendAsync(dispatch.OP_GLOBAL_TIMER, { timeout });
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ fireTimers();
+}
+
+function setOrClearGlobalTimeout(due: number | null, now: number): void {
+ if (due == null) {
+ clearGlobalTimeout();
+ } else {
+ setGlobalTimeout(due, now);
+ }
+}
+
+function schedule(timer: Timer, now: number): void {
+ assert(!timer.scheduled);
+ assert(now <= timer.due);
+ // Find or create the list of timers that will fire at point-in-time `due`.
+ let list = dueMap[timer.due];
+ if (list === undefined) {
+ list = dueMap[timer.due] = [];
+ }
+ // Append the newly scheduled timer to the list and mark it as scheduled.
+ list.push(timer);
+ timer.scheduled = true;
+ // If the new timer is scheduled to fire before any timer that existed before,
+ // update the global timeout to reflect this.
+ if (globalTimeoutDue === null || globalTimeoutDue > timer.due) {
+ setOrClearGlobalTimeout(timer.due, now);
+ }
+}
+
+function unschedule(timer: Timer): void {
+ if (!timer.scheduled) {
+ return;
+ }
+ // Find the list of timers that will fire at point-in-time `due`.
+ const list = dueMap[timer.due];
+ if (list.length === 1) {
+ // Time timer is the only one in the list. Remove the entire list.
+ assert(list[0] === timer);
+ delete dueMap[timer.due];
+ // If the unscheduled timer was 'next up', find when the next timer that
+ // still exists is due, and update the global alarm accordingly.
+ if (timer.due === globalTimeoutDue) {
+ let nextTimerDue: number | null = null;
+ for (const key in dueMap) {
+ nextTimerDue = Number(key);
+ break;
+ }
+ setOrClearGlobalTimeout(nextTimerDue, getTime());
+ }
+ } else {
+ // Multiple timers that are due at the same point in time.
+ // Remove this timer from the list.
+ const index = list.indexOf(timer);
+ assert(index > -1);
+ list.splice(index, 1);
+ }
+}
+
+function fire(timer: Timer): void {
+ // If the timer isn't found in the ID map, that means it has been cancelled
+ // between the timer firing and the promise callback (this function).
+ if (!idMap.has(timer.id)) {
+ return;
+ }
+ // Reschedule the timer if it is a repeating one, otherwise drop it.
+ if (!timer.repeat) {
+ // One-shot timer: remove the timer from this id-to-timer map.
+ idMap.delete(timer.id);
+ } else {
+ // Interval timer: compute when timer was supposed to fire next.
+ // However make sure to never schedule the next interval in the past.
+ const now = getTime();
+ timer.due = Math.max(now, timer.due + timer.delay);
+ schedule(timer, now);
+ }
+ // Call the user callback. Intermediate assignment is to avoid leaking `this`
+ // to it, while also keeping the stack trace neat when it shows up in there.
+ const callback = timer.callback;
+ callback();
+}
+
+function fireTimers(): void {
+ const now = getTime();
+ // Bail out if we're not expecting the global timer to fire.
+ if (globalTimeoutDue === null) {
+ return;
+ }
+ // After firing the timers that are due now, this will hold the due time of
+ // the first timer that hasn't fired yet.
+ let nextTimerDue: number | null = null;
+ // Walk over the keys of the 'due' map. Since dueMap is actually a regular
+ // object and its keys are numerical and smaller than UINT32_MAX - 2,
+ // keys are iterated in ascending order.
+ for (const key in dueMap) {
+ // Convert the object key (a string) to a number.
+ const due = Number(key);
+ // Break out of the loop if the next timer isn't due to fire yet.
+ if (Number(due) > now) {
+ nextTimerDue = due;
+ break;
+ }
+ // Get the list of timers that have this due time, then drop it.
+ const list = dueMap[key];
+ delete dueMap[key];
+ // Fire all the timers in the list.
+ for (const timer of list) {
+ // With the list dropped, the timer is no longer scheduled.
+ timer.scheduled = false;
+ // Place the callback on the microtask queue.
+ Promise.resolve(timer).then(fire);
+ }
+ }
+
+ // Update the global alarm to go off when the first-up timer that hasn't fired
+ // yet is due.
+ setOrClearGlobalTimeout(nextTimerDue, now);
+}
+
+export type Args = unknown[];
+
+function checkThis(thisArg: unknown): void {
+ if (thisArg !== null && thisArg !== undefined && thisArg !== window) {
+ throw new TypeError("Illegal invocation");
+ }
+}
+
+function checkBigInt(n: unknown): void {
+ if (typeof n === "bigint") {
+ throw new TypeError("Cannot convert a BigInt value to a number");
+ }
+}
+
+function setTimer(
+ cb: (...args: Args) => void,
+ delay: number,
+ args: Args,
+ repeat: boolean
+): number {
+ // Bind `args` to the callback and bind `this` to window(global).
+ const callback: () => void = cb.bind(window, ...args);
+ // In the browser, the delay value must be coercible to an integer between 0
+ // and INT32_MAX. Any other value will cause the timer to fire immediately.
+ // We emulate this behavior.
+ const now = getTime();
+ if (delay > TIMEOUT_MAX) {
+ console.warn(
+ `${delay} does not fit into` +
+ " a 32-bit signed integer." +
+ "\nTimeout duration was set to 1."
+ );
+ delay = 1;
+ }
+ delay = Math.max(0, delay | 0);
+
+ // Create a new, unscheduled timer object.
+ const timer = {
+ id: nextTimerId++,
+ callback,
+ args,
+ delay,
+ due: now + delay,
+ repeat,
+ scheduled: false
+ };
+ // Register the timer's existence in the id-to-timer map.
+ idMap.set(timer.id, timer);
+ // Schedule the timer in the due table.
+ schedule(timer, now);
+ return timer.id;
+}
+
+/** Sets a timer which executes a function once after the timer expires. */
+export function setTimeout(
+ cb: (...args: Args) => void,
+ delay = 0,
+ ...args: Args
+): number {
+ checkBigInt(delay);
+ // @ts-ignore
+ checkThis(this);
+ return setTimer(cb, delay, args, false);
+}
+
+/** Repeatedly calls a function , with a fixed time delay between each call. */
+export function setInterval(
+ cb: (...args: Args) => void,
+ delay = 0,
+ ...args: Args
+): number {
+ checkBigInt(delay);
+ // @ts-ignore
+ checkThis(this);
+ return setTimer(cb, delay, args, true);
+}
+
+/** Clears a previously set timer by id. AKA clearTimeout and clearInterval. */
+function clearTimer(id: number): void {
+ id = Number(id);
+ const timer = idMap.get(id);
+ if (timer === undefined) {
+ // Timer doesn't exist any more or never existed. This is not an error.
+ return;
+ }
+ // Unschedule the timer if it is currently scheduled, and forget about it.
+ unschedule(timer);
+ idMap.delete(timer.id);
+}
+
+export function clearTimeout(id = 0): void {
+ checkBigInt(id);
+ if (id === 0) {
+ return;
+ }
+ clearTimer(id);
+}
+
+export function clearInterval(id = 0): void {
+ checkBigInt(id);
+ if (id === 0) {
+ return;
+ }
+ clearTimer(id);
+}
diff --git a/cli/js/timers_test.ts b/cli/js/timers_test.ts
new file mode 100644
index 000000000..bc4fcffcf
--- /dev/null
+++ b/cli/js/timers_test.ts
@@ -0,0 +1,291 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEquals, assertNotEquals } from "./test_util.ts";
+
+function deferred(): {
+ promise: Promise<{}>;
+ resolve: (value?: {} | PromiseLike<{}>) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ reject: (reason?: any) => void;
+} {
+ let resolve;
+ let reject;
+ const promise = new Promise(
+ (res, rej): void => {
+ resolve = res;
+ reject = rej;
+ }
+ );
+ return {
+ promise,
+ resolve,
+ reject
+ };
+}
+
+async function waitForMs(ms): Promise<number> {
+ return new Promise((resolve): number => setTimeout(resolve, ms));
+}
+
+test(async function timeoutSuccess(): Promise<void> {
+ const { promise, resolve } = deferred();
+ let count = 0;
+ setTimeout((): void => {
+ count++;
+ resolve();
+ }, 500);
+ await promise;
+ // count should increment
+ assertEquals(count, 1);
+});
+
+test(async function timeoutArgs(): Promise<void> {
+ const { promise, resolve } = deferred();
+ const arg = 1;
+ setTimeout(
+ (a, b, c): void => {
+ assertEquals(a, arg);
+ assertEquals(b, arg.toString());
+ assertEquals(c, [arg]);
+ resolve();
+ },
+ 10,
+ arg,
+ arg.toString(),
+ [arg]
+ );
+ await promise;
+});
+
+test(async function timeoutCancelSuccess(): Promise<void> {
+ let count = 0;
+ const id = setTimeout((): void => {
+ count++;
+ }, 1);
+ // Cancelled, count should not increment
+ clearTimeout(id);
+ await waitForMs(600);
+ assertEquals(count, 0);
+});
+
+test(async function timeoutCancelMultiple(): Promise<void> {
+ function uncalled(): never {
+ throw new Error("This function should not be called.");
+ }
+
+ // Set timers and cancel them in the same order.
+ const t1 = setTimeout(uncalled, 10);
+ const t2 = setTimeout(uncalled, 10);
+ const t3 = setTimeout(uncalled, 10);
+ clearTimeout(t1);
+ clearTimeout(t2);
+ clearTimeout(t3);
+
+ // Set timers and cancel them in reverse order.
+ const t4 = setTimeout(uncalled, 20);
+ const t5 = setTimeout(uncalled, 20);
+ const t6 = setTimeout(uncalled, 20);
+ clearTimeout(t6);
+ clearTimeout(t5);
+ clearTimeout(t4);
+
+ // Sleep until we're certain that the cancelled timers aren't gonna fire.
+ await waitForMs(50);
+});
+
+test(async function timeoutCancelInvalidSilentFail(): Promise<void> {
+ // Expect no panic
+ const { promise, resolve } = deferred();
+ let count = 0;
+ const id = setTimeout((): void => {
+ count++;
+ // Should have no effect
+ clearTimeout(id);
+ resolve();
+ }, 500);
+ await promise;
+ assertEquals(count, 1);
+
+ // Should silently fail (no panic)
+ clearTimeout(2147483647);
+});
+
+test(async function intervalSuccess(): Promise<void> {
+ const { promise, resolve } = deferred();
+ let count = 0;
+ const id = setInterval((): void => {
+ count++;
+ clearInterval(id);
+ resolve();
+ }, 100);
+ await promise;
+ // Clear interval
+ clearInterval(id);
+ // count should increment twice
+ assertEquals(count, 1);
+});
+
+test(async function intervalCancelSuccess(): Promise<void> {
+ let count = 0;
+ const id = setInterval((): void => {
+ count++;
+ }, 1);
+ clearInterval(id);
+ await waitForMs(500);
+ assertEquals(count, 0);
+});
+
+test(async function intervalOrdering(): Promise<void> {
+ const timers = [];
+ let timeouts = 0;
+ function onTimeout(): void {
+ ++timeouts;
+ for (let i = 1; i < timers.length; i++) {
+ clearTimeout(timers[i]);
+ }
+ }
+ for (let i = 0; i < 10; i++) {
+ timers[i] = setTimeout(onTimeout, 1);
+ }
+ await waitForMs(500);
+ assertEquals(timeouts, 1);
+});
+
+test(async function intervalCancelInvalidSilentFail(): Promise<void> {
+ // Should silently fail (no panic)
+ clearInterval(2147483647);
+});
+
+test(async function fireCallbackImmediatelyWhenDelayOverMaxValue(): Promise<
+ void
+> {
+ let count = 0;
+ setTimeout((): void => {
+ count++;
+ }, 2 ** 31);
+ await waitForMs(1);
+ assertEquals(count, 1);
+});
+
+test(async function timeoutCallbackThis(): Promise<void> {
+ const { promise, resolve } = deferred();
+ const obj = {
+ foo(): void {
+ assertEquals(this, window);
+ resolve();
+ }
+ };
+ setTimeout(obj.foo, 1);
+ await promise;
+});
+
+test(async function timeoutBindThis(): Promise<void> {
+ function noop(): void {}
+
+ const thisCheckPassed = [null, undefined, window, globalThis];
+
+ const thisCheckFailed = [
+ 0,
+ "",
+ true,
+ false,
+ {},
+ [],
+ "foo",
+ (): void => {},
+ Object.prototype
+ ];
+
+ thisCheckPassed.forEach(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (thisArg: any): void => {
+ let hasThrown = 0;
+ try {
+ setTimeout.call(thisArg, noop, 1);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 1);
+ }
+ );
+
+ thisCheckFailed.forEach(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (thisArg: any): void => {
+ let hasThrown = 0;
+ try {
+ setTimeout.call(thisArg, noop, 1);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ }
+ );
+});
+
+test(async function clearTimeoutShouldConvertToNumber(): Promise<void> {
+ let called = false;
+ const obj = {
+ valueOf(): number {
+ called = true;
+ return 1;
+ }
+ };
+ clearTimeout((obj as unknown) as number);
+ assert(called);
+});
+
+test(function setTimeoutShouldThrowWithBigint(): void {
+ let hasThrown = 0;
+ try {
+ setTimeout((): void => {}, (1n as unknown) as number);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+});
+
+test(function clearTimeoutShouldThrowWithBigint(): void {
+ let hasThrown = 0;
+ try {
+ clearTimeout((1n as unknown) as number);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+});
+
+test(function testFunctionName(): void {
+ assertEquals(clearTimeout.name, "clearTimeout");
+ assertEquals(clearInterval.name, "clearInterval");
+});
+
+test(function testFunctionParamsLength(): void {
+ assertEquals(setTimeout.length, 1);
+ assertEquals(setInterval.length, 1);
+ assertEquals(clearTimeout.length, 0);
+ assertEquals(clearInterval.length, 0);
+});
+
+test(function clearTimeoutAndClearIntervalNotBeEquals(): void {
+ assertNotEquals(clearTimeout, clearInterval);
+});
diff --git a/cli/js/tls.ts b/cli/js/tls.ts
new file mode 100644
index 000000000..ec24b458b
--- /dev/null
+++ b/cli/js/tls.ts
@@ -0,0 +1,21 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+import { Conn, ConnImpl } from "./net.ts";
+
+// TODO(ry) There are many configuration options to add...
+// https://docs.rs/rustls/0.16.0/rustls/struct.ClientConfig.html
+interface DialTLSOptions {
+ port: number;
+ hostname?: string;
+}
+const dialTLSDefaults = { hostname: "127.0.0.1", transport: "tcp" };
+
+/**
+ * dialTLS establishes a secure connection over TLS (transport layer security).
+ */
+export async function dialTLS(options: DialTLSOptions): Promise<Conn> {
+ options = Object.assign(dialTLSDefaults, options);
+ const res = await sendAsync(dispatch.OP_DIAL_TLS, options);
+ return new ConnImpl(res.rid, res.remoteAddr!, res.localAddr!);
+}
diff --git a/cli/js/tls_test.ts b/cli/js/tls_test.ts
new file mode 100644
index 000000000..25900f876
--- /dev/null
+++ b/cli/js/tls_test.ts
@@ -0,0 +1,25 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, testPerm, assert, assertEquals } from "./test_util.ts";
+
+// TODO(ry) The tests in this file use github.com:443, but it would be better to
+// not rely on an internet connection and rather use a localhost TLS server.
+
+test(async function dialTLSNoPerm(): Promise<void> {
+ let err;
+ try {
+ await Deno.dialTLS({ hostname: "github.com", port: 443 });
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ net: true }, async function dialTLSBasic(): Promise<void> {
+ const conn = await Deno.dialTLS({ hostname: "github.com", port: 443 });
+ assert(conn.rid > 0);
+ const body = new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n");
+ const writeResult = await conn.write(body);
+ assertEquals(body.length, writeResult);
+ conn.close();
+});
diff --git a/cli/js/truncate.ts b/cli/js/truncate.ts
new file mode 100644
index 000000000..5ce7b5158
--- /dev/null
+++ b/cli/js/truncate.ts
@@ -0,0 +1,34 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import * as dispatch from "./dispatch.ts";
+
+function coerceLen(len?: number): number {
+ if (!len) {
+ return 0;
+ }
+
+ if (len < 0) {
+ return 0;
+ }
+
+ return len;
+}
+
+/** Truncates or extends the specified file synchronously, updating the size of
+ * this file to become size.
+ *
+ * Deno.truncateSync("hello.txt", 10);
+ */
+export function truncateSync(name: string, len?: number): void {
+ sendSync(dispatch.OP_TRUNCATE, { name, len: coerceLen(len) });
+}
+
+/**
+ * Truncates or extends the specified file, updating the size of this file to
+ * become size.
+ *
+ * await Deno.truncate("hello.txt", 10);
+ */
+export async function truncate(name: string, len?: number): Promise<void> {
+ await sendAsync(dispatch.OP_TRUNCATE, { name, len: coerceLen(len) });
+}
diff --git a/cli/js/truncate_test.ts b/cli/js/truncate_test.ts
new file mode 100644
index 000000000..055db8652
--- /dev/null
+++ b/cli/js/truncate_test.ts
@@ -0,0 +1,74 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assertEquals } from "./test_util.ts";
+
+function readDataSync(name: string): string {
+ const data = Deno.readFileSync(name);
+ const decoder = new TextDecoder("utf-8");
+ const text = decoder.decode(data);
+ return text;
+}
+
+async function readData(name: string): Promise<string> {
+ const data = await Deno.readFile(name);
+ const decoder = new TextDecoder("utf-8");
+ const text = decoder.decode(data);
+ return text;
+}
+
+testPerm({ read: true, write: true }, function truncateSyncSuccess(): void {
+ const enc = new TextEncoder();
+ const d = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test_truncateSync.txt";
+ Deno.writeFileSync(filename, d);
+ Deno.truncateSync(filename, 20);
+ let data = readDataSync(filename);
+ assertEquals(data.length, 20);
+ Deno.truncateSync(filename, 5);
+ data = readDataSync(filename);
+ assertEquals(data.length, 5);
+ Deno.truncateSync(filename, -5);
+ data = readDataSync(filename);
+ assertEquals(data.length, 0);
+ Deno.removeSync(filename);
+});
+
+testPerm({ read: true, write: true }, async function truncateSuccess(): Promise<
+ void
+> {
+ const enc = new TextEncoder();
+ const d = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test_truncate.txt";
+ await Deno.writeFile(filename, d);
+ await Deno.truncate(filename, 20);
+ let data = await readData(filename);
+ assertEquals(data.length, 20);
+ await Deno.truncate(filename, 5);
+ data = await readData(filename);
+ assertEquals(data.length, 5);
+ await Deno.truncate(filename, -5);
+ data = await readData(filename);
+ assertEquals(data.length, 0);
+ await Deno.remove(filename);
+});
+
+testPerm({ write: false }, function truncateSyncPerm(): void {
+ let err;
+ try {
+ Deno.mkdirSync("/test_truncateSyncPermission.txt");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
+
+testPerm({ write: false }, async function truncatePerm(): Promise<void> {
+ let err;
+ try {
+ await Deno.mkdir("/test_truncatePermission.txt");
+ } catch (e) {
+ err = e;
+ }
+ assertEquals(err.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(err.name, "PermissionDenied");
+});
diff --git a/cli/js/ts_global.d.ts b/cli/js/ts_global.d.ts
new file mode 100644
index 000000000..71a01e30e
--- /dev/null
+++ b/cli/js/ts_global.d.ts
@@ -0,0 +1,19 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+// This scopes the `ts` namespace globally, which is where it exists at runtime
+// when building Deno, but the `typescript/lib/typescript.d.ts` is defined as a
+// module.
+
+// Warning! This is a magical import. We don't want to have multiple copies of
+// typescript.d.ts around the repo, there's already one in
+// deno_typescript/typescript/lib/typescript.d.ts. Ideally we could simply point
+// to that in this import specifier, but "cargo package" is very strict and
+// requires all files to be present in a crate's subtree.
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import * as ts_ from "$asset$/typescript.d.ts";
+
+declare global {
+ namespace ts {
+ export = ts_;
+ }
+}
diff --git a/cli/js/type_directives.ts b/cli/js/type_directives.ts
new file mode 100644
index 000000000..9b27887b5
--- /dev/null
+++ b/cli/js/type_directives.ts
@@ -0,0 +1,91 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+interface FileReference {
+ fileName: string;
+ pos: number;
+ end: number;
+}
+
+/** Remap the module name based on any supplied type directives passed. */
+export function getMappedModuleName(
+ source: FileReference,
+ typeDirectives: Map<FileReference, string>
+): string {
+ const { fileName: sourceFileName, pos: sourcePos } = source;
+ for (const [{ fileName, pos }, value] of typeDirectives.entries()) {
+ if (sourceFileName === fileName && sourcePos === pos) {
+ return value;
+ }
+ }
+ return source.fileName;
+}
+
+/** Matches directives that look something like this and parses out the value
+ * of the directive:
+ *
+ * // @deno-types="./foo.d.ts"
+ *
+ * [See Diagram](http://bit.ly/31nZPCF)
+ */
+const typeDirectiveRegEx = /@deno-types\s*=\s*(["'])((?:(?=(\\?))\3.)*?)\1/gi;
+
+/** Matches `import`, `import from` or `export from` statements and parses out the value of the
+ * module specifier in the second capture group:
+ *
+ * import "./foo.js"
+ * import * as foo from "./foo.js"
+ * export { a, b, c } from "./bar.js"
+ *
+ * [See Diagram](http://bit.ly/2lOsp0K)
+ */
+const importExportRegEx = /(?:import|export)(?:\s+|\s+[\s\S]*?from\s+)?(["'])((?:(?=(\\?))\3.)*?)\1/;
+
+/** Parses out any Deno type directives that are part of the source code, or
+ * returns `undefined` if there are not any.
+ */
+export function parseTypeDirectives(
+ sourceCode: string | undefined
+): Map<FileReference, string> | undefined {
+ if (!sourceCode) {
+ return;
+ }
+
+ // collect all the directives in the file and their start and end positions
+ const directives: FileReference[] = [];
+ let maybeMatch: RegExpExecArray | null = null;
+ while ((maybeMatch = typeDirectiveRegEx.exec(sourceCode))) {
+ const [matchString, , fileName] = maybeMatch;
+ const { index: pos } = maybeMatch;
+ directives.push({
+ fileName,
+ pos,
+ end: pos + matchString.length
+ });
+ }
+ if (!directives.length) {
+ return;
+ }
+
+ // work from the last directive backwards for the next `import`/`export`
+ // statement
+ directives.reverse();
+ const results = new Map<FileReference, string>();
+ for (const { end, fileName, pos } of directives) {
+ const searchString = sourceCode.substring(end);
+ const maybeMatch = importExportRegEx.exec(searchString);
+ if (maybeMatch) {
+ const [matchString, , targetFileName] = maybeMatch;
+ const targetPos =
+ end + maybeMatch.index + matchString.indexOf(targetFileName) - 1;
+ const target: FileReference = {
+ fileName: targetFileName,
+ pos: targetPos,
+ end: targetPos + targetFileName.length
+ };
+ results.set(target, fileName);
+ }
+ sourceCode = sourceCode.substring(0, pos);
+ }
+
+ return results;
+}
diff --git a/cli/js/types.ts b/cli/js/types.ts
new file mode 100644
index 000000000..88462d758
--- /dev/null
+++ b/cli/js/types.ts
@@ -0,0 +1,2 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+export type TypedArray = Uint8Array | Float32Array | Int32Array;
diff --git a/cli/js/unit_test_runner.ts b/cli/js/unit_test_runner.ts
new file mode 100755
index 000000000..d310f0a4e
--- /dev/null
+++ b/cli/js/unit_test_runner.ts
@@ -0,0 +1,107 @@
+#!/usr/bin/env -S deno run --reload --allow-run
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import "./unit_tests.ts";
+import { permissionCombinations, parseUnitTestOutput } from "./test_util.ts";
+
+interface TestResult {
+ perms: string;
+ output: string;
+ result: number;
+}
+
+function permsToCliFlags(perms: Deno.Permissions): string[] {
+ return Object.keys(perms)
+ .map(
+ (key): string => {
+ if (!perms[key]) return "";
+
+ const cliFlag = key.replace(
+ /\.?([A-Z])/g,
+ (x, y): string => `-${y.toLowerCase()}`
+ );
+ return `--allow-${cliFlag}`;
+ }
+ )
+ .filter((e): boolean => e.length > 0);
+}
+
+function fmtPerms(perms: Deno.Permissions): string {
+ let fmt = permsToCliFlags(perms).join(" ");
+
+ if (!fmt) {
+ fmt = "<no permissions>";
+ }
+
+ return fmt;
+}
+
+async function main(): Promise<void> {
+ console.log(
+ "Discovered permission combinations for tests:",
+ permissionCombinations.size
+ );
+
+ for (const perms of permissionCombinations.values()) {
+ console.log("\t" + fmtPerms(perms));
+ }
+
+ const testResults = new Set<TestResult>();
+
+ for (const perms of permissionCombinations.values()) {
+ const permsFmt = fmtPerms(perms);
+ console.log(`Running tests for: ${permsFmt}`);
+ const cliPerms = permsToCliFlags(perms);
+ // run subsequent tests using same deno executable
+ const args = [
+ Deno.execPath(),
+ "run",
+ "--no-prompt",
+ ...cliPerms,
+ "cli/js/unit_tests.ts"
+ ];
+
+ const p = Deno.run({
+ args,
+ stdout: "piped"
+ });
+
+ const { actual, expected, resultOutput } = parseUnitTestOutput(
+ await p.output(),
+ true
+ );
+
+ let result = 0;
+
+ if (!actual && !expected) {
+ console.error("Bad cli/js/unit_test.ts output");
+ result = 1;
+ } else if (expected !== actual) {
+ result = 1;
+ }
+
+ testResults.add({
+ perms: permsFmt,
+ output: resultOutput,
+ result
+ });
+ }
+
+ // if any run tests returned non-zero status then whole test
+ // run should fail
+ let testsFailed = false;
+
+ for (const testResult of testResults) {
+ console.log(`Summary for ${testResult.perms}`);
+ console.log(testResult.output + "\n");
+ testsFailed = testsFailed || Boolean(testResult.result);
+ }
+
+ if (testsFailed) {
+ console.error("Unit tests failed");
+ Deno.exit(1);
+ }
+
+ console.log("Unit tests passed");
+}
+
+main();
diff --git a/cli/js/unit_tests.ts b/cli/js/unit_tests.ts
new file mode 100644
index 000000000..a3f150f4c
--- /dev/null
+++ b/cli/js/unit_tests.ts
@@ -0,0 +1,65 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+// This test is executed as part of tools/test.py
+// But it can also be run manually: ./target/debug/deno cli/js/unit_tests.ts
+
+import "./blob_test.ts";
+import "./body_test.ts";
+import "./buffer_test.ts";
+import "./build_test.ts";
+import "./chmod_test.ts";
+import "./chown_test.ts";
+import "./console_test.ts";
+import "./copy_file_test.ts";
+import "./custom_event_test.ts";
+import "./dir_test.ts";
+import "./dispatch_json_test.ts";
+import "./error_stack_test.ts";
+import "./event_test.ts";
+import "./event_target_test.ts";
+import "./fetch_test.ts";
+import "./file_test.ts";
+import "./files_test.ts";
+import "./form_data_test.ts";
+import "./get_random_values_test.ts";
+import "./globals_test.ts";
+import "./headers_test.ts";
+import "./link_test.ts";
+import "./location_test.ts";
+import "./make_temp_dir_test.ts";
+import "./metrics_test.ts";
+import "./mixins/dom_iterable_test.ts";
+import "./mkdir_test.ts";
+import "./net_test.ts";
+import "./os_test.ts";
+import "./process_test.ts";
+import "./read_dir_test.ts";
+import "./read_file_test.ts";
+import "./read_link_test.ts";
+import "./rename_test.ts";
+import "./request_test.ts";
+import "./resources_test.ts";
+import "./stat_test.ts";
+import "./symlink_test.ts";
+import "./text_encoding_test.ts";
+import "./timers_test.ts";
+import "./tls_test.ts";
+import "./truncate_test.ts";
+import "./url_test.ts";
+import "./url_search_params_test.ts";
+import "./utime_test.ts";
+import "./write_file_test.ts";
+import "./performance_test.ts";
+import "./permissions_test.ts";
+import "./version_test.ts";
+
+import "../../website/app_test.ts";
+
+import { runIfMain } from "../../std/testing/mod.ts";
+
+async function main(): Promise<void> {
+ // Testing entire test suite serially
+ runIfMain(import.meta);
+}
+
+main();
diff --git a/cli/js/url.ts b/cli/js/url.ts
new file mode 100644
index 000000000..f22198da4
--- /dev/null
+++ b/cli/js/url.ts
@@ -0,0 +1,376 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as urlSearchParams from "./url_search_params.ts";
+import * as domTypes from "./dom_types.ts";
+import { getRandomValues } from "./get_random_values.ts";
+import { window } from "./window.ts";
+
+interface URLParts {
+ protocol: string;
+ username: string;
+ password: string;
+ hostname: string;
+ port: string;
+ path: string;
+ query: string | null;
+ hash: string;
+}
+
+const patterns = {
+ protocol: "(?:([^:/?#]+):)",
+ authority: "(?://([^/?#]*))",
+ path: "([^?#]*)",
+ query: "(\\?[^#]*)",
+ hash: "(#.*)",
+
+ authentication: "(?:([^:]*)(?::([^@]*))?@)",
+ hostname: "([^:]+)",
+ port: "(?::(\\d+))"
+};
+
+const urlRegExp = new RegExp(
+ `^${patterns.protocol}?${patterns.authority}?${patterns.path}${
+ patterns.query
+ }?${patterns.hash}?`
+);
+
+const authorityRegExp = new RegExp(
+ `^${patterns.authentication}?${patterns.hostname}${patterns.port}?$`
+);
+
+const searchParamsMethods: Array<keyof urlSearchParams.URLSearchParams> = [
+ "append",
+ "delete",
+ "set"
+];
+
+function parse(url: string): URLParts | undefined {
+ const urlMatch = urlRegExp.exec(url);
+ if (urlMatch) {
+ const [, , authority] = urlMatch;
+ const authorityMatch = authority
+ ? authorityRegExp.exec(authority)
+ : [null, null, null, null, null];
+ if (authorityMatch) {
+ return {
+ protocol: urlMatch[1] || "",
+ username: authorityMatch[1] || "",
+ password: authorityMatch[2] || "",
+ hostname: authorityMatch[3] || "",
+ port: authorityMatch[4] || "",
+ path: urlMatch[3] || "",
+ query: urlMatch[4] || "",
+ hash: urlMatch[5] || ""
+ };
+ }
+ }
+ return undefined;
+}
+
+// Based on https://github.com/kelektiv/node-uuid
+// TODO(kevinkassimo): Use deno_std version once possible.
+function generateUUID(): string {
+ return "00000000-0000-4000-8000-000000000000".replace(
+ /[0]/g,
+ (): string =>
+ // random integer from 0 to 15 as a hex digit.
+ (getRandomValues(new Uint8Array(1))[0] % 16).toString(16)
+ );
+}
+
+// Keep it outside of URL to avoid any attempts of access.
+export const blobURLMap = new Map<string, domTypes.Blob>();
+
+function isAbsolutePath(path: string): boolean {
+ return path.startsWith("/");
+}
+
+// Resolves `.`s and `..`s where possible.
+// Preserves repeating and trailing `/`s by design.
+function normalizePath(path: string): string {
+ const isAbsolute = isAbsolutePath(path);
+ path = path.replace(/^\//, "");
+ const pathSegments = path.split("/");
+
+ const newPathSegments: string[] = [];
+ for (let i = 0; i < pathSegments.length; i++) {
+ const previous = newPathSegments[newPathSegments.length - 1];
+ if (
+ pathSegments[i] == ".." &&
+ previous != ".." &&
+ (previous != undefined || isAbsolute)
+ ) {
+ newPathSegments.pop();
+ } else if (pathSegments[i] != ".") {
+ newPathSegments.push(pathSegments[i]);
+ }
+ }
+
+ let newPath = newPathSegments.join("/");
+ if (!isAbsolute) {
+ if (newPathSegments.length == 0) {
+ newPath = ".";
+ }
+ } else {
+ newPath = `/${newPath}`;
+ }
+ return newPath;
+}
+
+// Standard URL basing logic, applied to paths.
+function resolvePathFromBase(path: string, basePath: string): string {
+ const normalizedPath = normalizePath(path);
+ if (isAbsolutePath(normalizedPath)) {
+ return normalizedPath;
+ }
+ const normalizedBasePath = normalizePath(basePath);
+ if (!isAbsolutePath(normalizedBasePath)) {
+ throw new TypeError("Base path must be absolute.");
+ }
+
+ // Special case.
+ if (path == "") {
+ return normalizedBasePath;
+ }
+
+ // Remove everything after the last `/` in `normalizedBasePath`.
+ const prefix = normalizedBasePath.replace(/[^\/]*$/, "");
+ // If `normalizedPath` ends with `.` or `..`, add a trailing space.
+ const suffix = normalizedPath.replace(/(?<=(^|\/)(\.|\.\.))$/, "/");
+
+ return normalizePath(prefix + suffix);
+}
+
+export class URL {
+ private _parts: URLParts;
+ private _searchParams!: urlSearchParams.URLSearchParams;
+
+ private _updateSearchParams(): void {
+ const searchParams = new urlSearchParams.URLSearchParams(this.search);
+
+ for (const methodName of searchParamsMethods) {
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ const method: (...args: any[]) => any = searchParams[methodName];
+ searchParams[methodName] = (...args: unknown[]): any => {
+ method.apply(searchParams, args);
+ this.search = searchParams.toString();
+ };
+ /* eslint-enable */
+ }
+ this._searchParams = searchParams;
+
+ // convert to `any` that has avoided the private limit
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (this._searchParams as any).url = this;
+ }
+
+ get hash(): string {
+ return this._parts.hash;
+ }
+
+ set hash(value: string) {
+ value = unescape(String(value));
+ if (!value) {
+ this._parts.hash = "";
+ } else {
+ if (value.charAt(0) !== "#") {
+ value = `#${value}`;
+ }
+ // hashes can contain % and # unescaped
+ this._parts.hash = escape(value)
+ .replace(/%25/g, "%")
+ .replace(/%23/g, "#");
+ }
+ }
+
+ get host(): string {
+ return `${this.hostname}${this.port ? `:${this.port}` : ""}`;
+ }
+
+ set host(value: string) {
+ value = String(value);
+ const url = new URL(`http://${value}`);
+ this._parts.hostname = url.hostname;
+ this._parts.port = url.port;
+ }
+
+ get hostname(): string {
+ return this._parts.hostname;
+ }
+
+ set hostname(value: string) {
+ value = String(value);
+ this._parts.hostname = encodeURIComponent(value);
+ }
+
+ get href(): string {
+ const authentication =
+ this.username || this.password
+ ? `${this.username}${this.password ? ":" + this.password : ""}@`
+ : "";
+
+ return `${this.protocol}//${authentication}${this.host}${this.pathname}${
+ this.search
+ }${this.hash}`;
+ }
+
+ set href(value: string) {
+ value = String(value);
+ if (value !== this.href) {
+ const url = new URL(value);
+ this._parts = { ...url._parts };
+ this._updateSearchParams();
+ }
+ }
+
+ get origin(): string {
+ return `${this.protocol}//${this.host}`;
+ }
+
+ get password(): string {
+ return this._parts.password;
+ }
+
+ set password(value: string) {
+ value = String(value);
+ this._parts.password = encodeURIComponent(value);
+ }
+
+ get pathname(): string {
+ return this._parts.path ? this._parts.path : "/";
+ }
+
+ set pathname(value: string) {
+ value = unescape(String(value));
+ if (!value || value.charAt(0) !== "/") {
+ value = `/${value}`;
+ }
+ // paths can contain % unescaped
+ this._parts.path = escape(value).replace(/%25/g, "%");
+ }
+
+ get port(): string {
+ return this._parts.port;
+ }
+
+ set port(value: string) {
+ const port = parseInt(String(value), 10);
+ this._parts.port = isNaN(port)
+ ? ""
+ : Math.max(0, port % 2 ** 16).toString();
+ }
+
+ get protocol(): string {
+ return `${this._parts.protocol}:`;
+ }
+
+ set protocol(value: string) {
+ value = String(value);
+ if (value) {
+ if (value.charAt(value.length - 1) === ":") {
+ value = value.slice(0, -1);
+ }
+ this._parts.protocol = encodeURIComponent(value);
+ }
+ }
+
+ get search(): string {
+ if (this._parts.query === null || this._parts.query === "") {
+ return "";
+ }
+
+ return this._parts.query;
+ }
+
+ set search(value: string) {
+ value = String(value);
+ let query: string | null;
+
+ if (value === "") {
+ query = null;
+ } else if (value.charAt(0) !== "?") {
+ query = `?${value}`;
+ } else {
+ query = value;
+ }
+
+ this._parts.query = query;
+ this._updateSearchParams();
+ }
+
+ get username(): string {
+ return this._parts.username;
+ }
+
+ set username(value: string) {
+ value = String(value);
+ this._parts.username = encodeURIComponent(value);
+ }
+
+ get searchParams(): urlSearchParams.URLSearchParams {
+ return this._searchParams;
+ }
+
+ constructor(url: string, base?: string | URL) {
+ let baseParts: URLParts | undefined;
+ if (base) {
+ baseParts = typeof base === "string" ? parse(base) : base._parts;
+ if (!baseParts || baseParts.protocol == "") {
+ throw new TypeError("Invalid base URL.");
+ }
+ }
+
+ const urlParts = parse(url);
+ if (!urlParts) {
+ throw new TypeError("Invalid URL.");
+ }
+
+ if (urlParts.protocol) {
+ this._parts = urlParts;
+ } else if (baseParts) {
+ this._parts = {
+ protocol: baseParts.protocol,
+ username: baseParts.username,
+ password: baseParts.password,
+ hostname: baseParts.hostname,
+ port: baseParts.port,
+ path: resolvePathFromBase(urlParts.path, baseParts.path || "/"),
+ query: urlParts.query,
+ hash: urlParts.hash
+ };
+ } else {
+ throw new TypeError("URL requires a base URL.");
+ }
+ this._updateSearchParams();
+ }
+
+ toString(): string {
+ return this.href;
+ }
+
+ toJSON(): string {
+ return this.href;
+ }
+
+ // TODO(kevinkassimo): implement MediaSource version in the future.
+ static createObjectURL(b: domTypes.Blob): string {
+ const origin = window.location.origin || "http://deno-opaque-origin";
+ const key = `blob:${origin}/${generateUUID()}`;
+ blobURLMap.set(key, b);
+ return key;
+ }
+
+ static revokeObjectURL(url: string): void {
+ let urlObject;
+ try {
+ urlObject = new URL(url);
+ } catch {
+ throw new TypeError("Provided URL string is not valid");
+ }
+ if (urlObject.protocol !== "blob:") {
+ return;
+ }
+ // Origin match check seems irrelevant for now, unless we implement
+ // persisten storage for per window.location.origin at some point.
+ blobURLMap.delete(url);
+ }
+}
diff --git a/cli/js/url_search_params.ts b/cli/js/url_search_params.ts
new file mode 100644
index 000000000..0835133d5
--- /dev/null
+++ b/cli/js/url_search_params.ts
@@ -0,0 +1,297 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { URL } from "./url.ts";
+import { requiredArguments, isIterable } from "./util.ts";
+
+export class URLSearchParams {
+ private params: Array<[string, string]> = [];
+ private url: URL | null = null;
+
+ constructor(init: string | string[][] | Record<string, string> = "") {
+ if (typeof init === "string") {
+ this._handleStringInitialization(init);
+ return;
+ }
+
+ if (Array.isArray(init) || isIterable(init)) {
+ this._handleArrayInitialization(init);
+ return;
+ }
+
+ if (Object(init) !== init) {
+ return;
+ }
+
+ if (init instanceof URLSearchParams) {
+ this.params = init.params;
+ return;
+ }
+
+ // Overload: record<USVString, USVString>
+ for (const key of Object.keys(init)) {
+ this.append(key, init[key]);
+ }
+ }
+
+ private updateSteps(): void {
+ if (this.url === null) {
+ return;
+ }
+
+ let query: string | null = this.toString();
+ if (query === "") {
+ query = null;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (this.url as any)._parts.query = query;
+ }
+
+ /** Appends a specified key/value pair as a new search parameter.
+ *
+ * searchParams.append('name', 'first');
+ * searchParams.append('name', 'second');
+ */
+ append(name: string, value: string): void {
+ requiredArguments("URLSearchParams.append", arguments.length, 2);
+ this.params.push([String(name), String(value)]);
+ this.updateSteps();
+ }
+
+ /** Deletes the given search parameter and its associated value,
+ * from the list of all search parameters.
+ *
+ * searchParams.delete('name');
+ */
+ delete(name: string): void {
+ requiredArguments("URLSearchParams.delete", arguments.length, 1);
+ name = String(name);
+ let i = 0;
+ while (i < this.params.length) {
+ if (this.params[i][0] === name) {
+ this.params.splice(i, 1);
+ } else {
+ i++;
+ }
+ }
+ this.updateSteps();
+ }
+
+ /** Returns all the values associated with a given search parameter
+ * as an array.
+ *
+ * searchParams.getAll('name');
+ */
+ getAll(name: string): string[] {
+ requiredArguments("URLSearchParams.getAll", arguments.length, 1);
+ name = String(name);
+ const values = [];
+ for (const entry of this.params) {
+ if (entry[0] === name) {
+ values.push(entry[1]);
+ }
+ }
+
+ return values;
+ }
+
+ /** Returns the first value associated to the given search parameter.
+ *
+ * searchParams.get('name');
+ */
+ get(name: string): string | null {
+ requiredArguments("URLSearchParams.get", arguments.length, 1);
+ name = String(name);
+ for (const entry of this.params) {
+ if (entry[0] === name) {
+ return entry[1];
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns a Boolean that indicates whether a parameter with the
+ * specified name exists.
+ *
+ * searchParams.has('name');
+ */
+ has(name: string): boolean {
+ requiredArguments("URLSearchParams.has", arguments.length, 1);
+ name = String(name);
+ return this.params.some((entry): boolean => entry[0] === name);
+ }
+
+ /** Sets the value associated with a given search parameter to the
+ * given value. If there were several matching values, this method
+ * deletes the others. If the search parameter doesn't exist, this
+ * method creates it.
+ *
+ * searchParams.set('name', 'value');
+ */
+ set(name: string, value: string): void {
+ requiredArguments("URLSearchParams.set", arguments.length, 2);
+
+ // If there are any name-value pairs whose name is name, in list,
+ // set the value of the first such name-value pair to value
+ // and remove the others.
+ name = String(name);
+ value = String(value);
+ let found = false;
+ let i = 0;
+ while (i < this.params.length) {
+ if (this.params[i][0] === name) {
+ if (!found) {
+ this.params[i][1] = value;
+ found = true;
+ i++;
+ } else {
+ this.params.splice(i, 1);
+ }
+ } else {
+ i++;
+ }
+ }
+
+ // Otherwise, append a new name-value pair whose name is name
+ // and value is value, to list.
+ if (!found) {
+ this.append(name, value);
+ }
+
+ this.updateSteps();
+ }
+
+ /** Sort all key/value pairs contained in this object in place and
+ * return undefined. The sort order is according to Unicode code
+ * points of the keys.
+ *
+ * searchParams.sort();
+ */
+ sort(): void {
+ this.params = this.params.sort(
+ (a, b): number => (a[0] === b[0] ? 0 : a[0] > b[0] ? 1 : -1)
+ );
+ this.updateSteps();
+ }
+
+ /** Calls a function for each element contained in this object in
+ * place and return undefined. Optionally accepts an object to use
+ * as this when executing callback as second argument.
+ *
+ * searchParams.forEach((value, key, parent) => {
+ * console.log(value, key, parent);
+ * });
+ *
+ */
+ forEach(
+ callbackfn: (value: string, key: string, parent: this) => void,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ thisArg?: any
+ ): void {
+ requiredArguments("URLSearchParams.forEach", arguments.length, 1);
+
+ if (typeof thisArg !== "undefined") {
+ callbackfn = callbackfn.bind(thisArg);
+ }
+
+ for (const [key, value] of this.entries()) {
+ callbackfn(value, key, this);
+ }
+ }
+
+ /** Returns an iterator allowing to go through all keys contained
+ * in this object.
+ *
+ * for (const key of searchParams.keys()) {
+ * console.log(key);
+ * }
+ */
+ *keys(): IterableIterator<string> {
+ for (const entry of this.params) {
+ yield entry[0];
+ }
+ }
+
+ /** Returns an iterator allowing to go through all values contained
+ * in this object.
+ *
+ * for (const value of searchParams.values()) {
+ * console.log(value);
+ * }
+ */
+ *values(): IterableIterator<string> {
+ for (const entry of this.params) {
+ yield entry[1];
+ }
+ }
+
+ /** Returns an iterator allowing to go through all key/value
+ * pairs contained in this object.
+ *
+ * for (const [key, value] of searchParams.entries()) {
+ * console.log(key, value);
+ * }
+ */
+ *entries(): IterableIterator<[string, string]> {
+ yield* this.params;
+ }
+
+ /** Returns an iterator allowing to go through all key/value
+ * pairs contained in this object.
+ *
+ * for (const [key, value] of searchParams[Symbol.iterator]()) {
+ * console.log(key, value);
+ * }
+ */
+ *[Symbol.iterator](): IterableIterator<[string, string]> {
+ yield* this.params;
+ }
+
+ /** Returns a query string suitable for use in a URL.
+ *
+ * searchParams.toString();
+ */
+ toString(): string {
+ return this.params
+ .map(
+ (tuple): string =>
+ `${encodeURIComponent(tuple[0])}=${encodeURIComponent(tuple[1])}`
+ )
+ .join("&");
+ }
+
+ private _handleStringInitialization(init: string): void {
+ // Overload: USVString
+ // If init is a string and starts with U+003F (?),
+ // remove the first code point from init.
+ if (init.charCodeAt(0) === 0x003f) {
+ init = init.slice(1);
+ }
+
+ for (const pair of init.split("&")) {
+ // Empty params are ignored
+ if (pair.length === 0) {
+ continue;
+ }
+ const position = pair.indexOf("=");
+ const name = pair.slice(0, position === -1 ? pair.length : position);
+ const value = pair.slice(name.length + 1);
+ this.append(decodeURIComponent(name), decodeURIComponent(value));
+ }
+ }
+
+ private _handleArrayInitialization(
+ init: string[][] | Iterable<[string, string]>
+ ): void {
+ // Overload: sequence<sequence<USVString>>
+ for (const tuple of init) {
+ // If pair does not contain exactly two items, then throw a TypeError.
+ if (tuple.length !== 2) {
+ throw new TypeError(
+ "URLSearchParams.constructor tuple array argument must only contain pair elements"
+ );
+ }
+ this.append(tuple[0], tuple[1]);
+ }
+ }
+}
diff --git a/cli/js/url_search_params_test.ts b/cli/js/url_search_params_test.ts
new file mode 100644
index 000000000..08b0c5a1f
--- /dev/null
+++ b/cli/js/url_search_params_test.ts
@@ -0,0 +1,238 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEquals } from "./test_util.ts";
+
+test(function urlSearchParamsInitString(): void {
+ const init = "c=4&a=2&b=3&%C3%A1=1";
+ const searchParams = new URLSearchParams(init);
+ assert(
+ init === searchParams.toString(),
+ "The init query string does not match"
+ );
+});
+
+test(function urlSearchParamsInitIterable(): void {
+ const init = [["a", "54"], ["b", "true"]];
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.toString(), "a=54&b=true");
+});
+
+test(function urlSearchParamsInitRecord(): void {
+ const init = { a: "54", b: "true" };
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.toString(), "a=54&b=true");
+});
+
+test(function urlSearchParamsInit(): void {
+ const params1 = new URLSearchParams("a=b");
+ assertEquals(params1.toString(), "a=b");
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const params2 = new URLSearchParams(params1 as any);
+ assertEquals(params2.toString(), "a=b");
+});
+
+test(function urlSearchParamsAppendSuccess(): void {
+ const searchParams = new URLSearchParams();
+ searchParams.append("a", "true");
+ assertEquals(searchParams.toString(), "a=true");
+});
+
+test(function urlSearchParamsDeleteSuccess(): void {
+ const init = "a=54&b=true";
+ const searchParams = new URLSearchParams(init);
+ searchParams.delete("b");
+ assertEquals(searchParams.toString(), "a=54");
+});
+
+test(function urlSearchParamsGetAllSuccess(): void {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.getAll("a"), ["54", "true"]);
+ assertEquals(searchParams.getAll("b"), ["true"]);
+ assertEquals(searchParams.getAll("c"), []);
+});
+
+test(function urlSearchParamsGetSuccess(): void {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.get("a"), "54");
+ assertEquals(searchParams.get("b"), "true");
+ assertEquals(searchParams.get("c"), null);
+});
+
+test(function urlSearchParamsHasSuccess(): void {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ assert(searchParams.has("a"));
+ assert(searchParams.has("b"));
+ assert(!searchParams.has("c"));
+});
+
+test(function urlSearchParamsSetReplaceFirstAndRemoveOthers(): void {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ searchParams.set("a", "false");
+ assertEquals(searchParams.toString(), "a=false&b=true");
+});
+
+test(function urlSearchParamsSetAppendNew(): void {
+ const init = "a=54&b=true&a=true";
+ const searchParams = new URLSearchParams(init);
+ searchParams.set("c", "foo");
+ assertEquals(searchParams.toString(), "a=54&b=true&a=true&c=foo");
+});
+
+test(function urlSearchParamsSortSuccess(): void {
+ const init = "c=4&a=2&b=3&a=1";
+ const searchParams = new URLSearchParams(init);
+ searchParams.sort();
+ assertEquals(searchParams.toString(), "a=2&a=1&b=3&c=4");
+});
+
+test(function urlSearchParamsForEachSuccess(): void {
+ const init = [["a", "54"], ["b", "true"]];
+ const searchParams = new URLSearchParams(init);
+ let callNum = 0;
+ searchParams.forEach(
+ (value, key, parent): void => {
+ assertEquals(searchParams, parent);
+ assertEquals(value, init[callNum][1]);
+ assertEquals(key, init[callNum][0]);
+ callNum++;
+ }
+ );
+ assertEquals(callNum, init.length);
+});
+
+test(function urlSearchParamsMissingName(): void {
+ const init = "=4";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.get(""), "4");
+ assertEquals(searchParams.toString(), "=4");
+});
+
+test(function urlSearchParamsMissingValue(): void {
+ const init = "4=";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.get("4"), "");
+ assertEquals(searchParams.toString(), "4=");
+});
+
+test(function urlSearchParamsMissingEqualSign(): void {
+ const init = "4";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.get("4"), "");
+ assertEquals(searchParams.toString(), "4=");
+});
+
+test(function urlSearchParamsMissingPair(): void {
+ const init = "c=4&&a=54&";
+ const searchParams = new URLSearchParams(init);
+ assertEquals(searchParams.toString(), "c=4&a=54");
+});
+
+// If pair does not contain exactly two items, then throw a TypeError.
+// ref https://url.spec.whatwg.org/#interface-urlsearchparams
+test(function urlSearchParamsShouldThrowTypeError(): void {
+ let hasThrown = 0;
+
+ try {
+ new URLSearchParams([["1"]]);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+
+ assertEquals(hasThrown, 2);
+
+ try {
+ new URLSearchParams([["1", "2", "3"]]);
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+
+ assertEquals(hasThrown, 2);
+});
+
+test(function urlSearchParamsAppendArgumentsCheck(): void {
+ const methodRequireOneParam = ["delete", "getAll", "get", "has", "forEach"];
+
+ const methodRequireTwoParams = ["append", "set"];
+
+ methodRequireOneParam.concat(methodRequireTwoParams).forEach(
+ (method: string): void => {
+ const searchParams = new URLSearchParams();
+ let hasThrown = 0;
+ try {
+ searchParams[method]();
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ }
+ );
+
+ methodRequireTwoParams.forEach(
+ (method: string): void => {
+ const searchParams = new URLSearchParams();
+ let hasThrown = 0;
+ try {
+ searchParams[method]("foo");
+ hasThrown = 1;
+ } catch (err) {
+ if (err instanceof TypeError) {
+ hasThrown = 2;
+ } else {
+ hasThrown = 3;
+ }
+ }
+ assertEquals(hasThrown, 2);
+ }
+ );
+});
+
+// ref: https://github.com/web-platform-tests/wpt/blob/master/url/urlsearchparams-delete.any.js
+test(function urlSearchParamsDeletingAppendedMultiple(): void {
+ const params = new URLSearchParams();
+ params.append("first", (1 as unknown) as string);
+ assert(params.has("first"));
+ assertEquals(params.get("first"), "1");
+ params.delete("first");
+ assertEquals(params.has("first"), false);
+ params.append("first", (1 as unknown) as string);
+ params.append("first", (10 as unknown) as string);
+ params.delete("first");
+ assertEquals(params.has("first"), false);
+});
+
+// ref: https://github.com/web-platform-tests/wpt/blob/master/url/urlsearchparams-constructor.any.js#L176-L182
+test(function urlSearchParamsCustomSymbolIterator(): void {
+ const params = new URLSearchParams();
+ params[Symbol.iterator] = function*(): IterableIterator<[string, string]> {
+ yield ["a", "b"];
+ };
+ const params1 = new URLSearchParams((params as unknown) as string[][]);
+ assertEquals(params1.get("a"), "b");
+});
+
+test(function urlSearchParamsCustomSymbolIteratorWithNonStringParams(): void {
+ const params = {};
+ params[Symbol.iterator] = function*(): IterableIterator<[number, number]> {
+ yield [1, 2];
+ };
+ const params1 = new URLSearchParams((params as unknown) as string[][]);
+ assertEquals(params1.get("1"), "2");
+});
diff --git a/cli/js/url_test.ts b/cli/js/url_test.ts
new file mode 100644
index 000000000..07a8028ce
--- /dev/null
+++ b/cli/js/url_test.ts
@@ -0,0 +1,181 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEquals } from "./test_util.ts";
+
+test(function urlParsing(): void {
+ const url = new URL(
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
+ );
+ assertEquals(url.hash, "#qat");
+ assertEquals(url.host, "baz.qat:8000");
+ assertEquals(url.hostname, "baz.qat");
+ assertEquals(
+ url.href,
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
+ );
+ assertEquals(url.origin, "https://baz.qat:8000");
+ assertEquals(url.password, "bar");
+ assertEquals(url.pathname, "/qux/quux");
+ assertEquals(url.port, "8000");
+ assertEquals(url.protocol, "https:");
+ assertEquals(url.search, "?foo=bar&baz=12");
+ assertEquals(url.searchParams.getAll("foo"), ["bar"]);
+ assertEquals(url.searchParams.getAll("baz"), ["12"]);
+ assertEquals(url.username, "foo");
+ assertEquals(
+ String(url),
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
+ );
+ assertEquals(
+ JSON.stringify({ key: url }),
+ `{"key":"https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"}`
+ );
+});
+
+test(function urlModifications(): void {
+ const url = new URL(
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
+ );
+ url.hash = "";
+ assertEquals(
+ url.href,
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12"
+ );
+ url.host = "qat.baz:8080";
+ assertEquals(
+ url.href,
+ "https://foo:bar@qat.baz:8080/qux/quux?foo=bar&baz=12"
+ );
+ url.hostname = "foo.bar";
+ assertEquals(
+ url.href,
+ "https://foo:bar@foo.bar:8080/qux/quux?foo=bar&baz=12"
+ );
+ url.password = "qux";
+ assertEquals(
+ url.href,
+ "https://foo:qux@foo.bar:8080/qux/quux?foo=bar&baz=12"
+ );
+ url.pathname = "/foo/bar%qat";
+ assertEquals(
+ url.href,
+ "https://foo:qux@foo.bar:8080/foo/bar%qat?foo=bar&baz=12"
+ );
+ url.port = "";
+ assertEquals(url.href, "https://foo:qux@foo.bar/foo/bar%qat?foo=bar&baz=12");
+ url.protocol = "http:";
+ assertEquals(url.href, "http://foo:qux@foo.bar/foo/bar%qat?foo=bar&baz=12");
+ url.search = "?foo=bar&foo=baz";
+ assertEquals(url.href, "http://foo:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz");
+ assertEquals(url.searchParams.getAll("foo"), ["bar", "baz"]);
+ url.username = "foo@bar";
+ assertEquals(
+ url.href,
+ "http://foo%40bar:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz"
+ );
+ url.searchParams.set("bar", "qat");
+ assertEquals(
+ url.href,
+ "http://foo%40bar:qux@foo.bar/foo/bar%qat?foo=bar&foo=baz&bar=qat"
+ );
+ url.searchParams.delete("foo");
+ assertEquals(url.href, "http://foo%40bar:qux@foo.bar/foo/bar%qat?bar=qat");
+ url.searchParams.append("foo", "bar");
+ assertEquals(
+ url.href,
+ "http://foo%40bar:qux@foo.bar/foo/bar%qat?bar=qat&foo=bar"
+ );
+});
+
+test(function urlModifyHref(): void {
+ const url = new URL("http://example.com/");
+ url.href = "https://foo:bar@example.com:8080/baz/qat#qux";
+ assertEquals(url.protocol, "https:");
+ assertEquals(url.username, "foo");
+ assertEquals(url.password, "bar");
+ assertEquals(url.host, "example.com:8080");
+ assertEquals(url.hostname, "example.com");
+ assertEquals(url.pathname, "/baz/qat");
+ assertEquals(url.hash, "#qux");
+});
+
+test(function urlModifyPathname(): void {
+ const url = new URL("http://foo.bar/baz%qat/qux%quux");
+ assertEquals(url.pathname, "/baz%qat/qux%quux");
+ url.pathname = url.pathname;
+ assertEquals(url.pathname, "/baz%qat/qux%quux");
+ url.pathname = "baz#qat qux";
+ assertEquals(url.pathname, "/baz%23qat%20qux");
+ url.pathname = url.pathname;
+ assertEquals(url.pathname, "/baz%23qat%20qux");
+});
+
+test(function urlModifyHash(): void {
+ const url = new URL("http://foo.bar");
+ url.hash = "%foo bar/qat%qux#bar";
+ assertEquals(url.hash, "#%foo%20bar/qat%qux#bar");
+ url.hash = url.hash;
+ assertEquals(url.hash, "#%foo%20bar/qat%qux#bar");
+});
+
+test(function urlSearchParamsReuse(): void {
+ const url = new URL(
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
+ );
+ const sp = url.searchParams;
+ url.host = "baz.qat";
+ assert(sp === url.searchParams, "Search params should be reused.");
+});
+
+test(function urlBaseURL(): void {
+ const base = new URL(
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
+ );
+ const url = new URL("/foo/bar?baz=foo#qux", base);
+ assertEquals(url.href, "https://foo:bar@baz.qat:8000/foo/bar?baz=foo#qux");
+});
+
+test(function urlBaseString(): void {
+ const url = new URL(
+ "/foo/bar?baz=foo#qux",
+ "https://foo:bar@baz.qat:8000/qux/quux?foo=bar&baz=12#qat"
+ );
+ assertEquals(url.href, "https://foo:bar@baz.qat:8000/foo/bar?baz=foo#qux");
+});
+
+test(function urlRelativeWithBase(): void {
+ assertEquals(new URL("", "file:///a/a/a").href, "file:///a/a/a");
+ assertEquals(new URL(".", "file:///a/a/a").href, "file:///a/a/");
+ assertEquals(new URL("..", "file:///a/a/a").href, "file:///a/");
+ assertEquals(new URL("b", "file:///a/a/a").href, "file:///a/a/b");
+ assertEquals(new URL("b", "file:///a/a/a/").href, "file:///a/a/a/b");
+ assertEquals(new URL("b/", "file:///a/a/a").href, "file:///a/a/b/");
+ assertEquals(new URL("../b", "file:///a/a/a").href, "file:///a/b");
+});
+
+test(function emptyBasePath(): void {
+ assertEquals(new URL("", "http://example.com").href, "http://example.com/");
+});
+
+test(function deletingAllParamsRemovesQuestionMarkFromURL(): void {
+ const url = new URL("http://example.com/?param1&param2");
+ url.searchParams.delete("param1");
+ url.searchParams.delete("param2");
+ assertEquals(url.href, "http://example.com/");
+ assertEquals(url.search, "");
+});
+
+test(function removingNonExistentParamRemovesQuestionMarkFromURL(): void {
+ const url = new URL("http://example.com/?");
+ assertEquals(url.href, "http://example.com/?");
+ url.searchParams.delete("param1");
+ assertEquals(url.href, "http://example.com/");
+ assertEquals(url.search, "");
+});
+
+test(function sortingNonExistentParamRemovesQuestionMarkFromURL(): void {
+ const url = new URL("http://example.com/?");
+ assertEquals(url.href, "http://example.com/?");
+ url.searchParams.sort();
+ assertEquals(url.href, "http://example.com/");
+ assertEquals(url.search, "");
+});
diff --git a/cli/js/util.ts b/cli/js/util.ts
new file mode 100644
index 000000000..013dc7ee1
--- /dev/null
+++ b/cli/js/util.ts
@@ -0,0 +1,225 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { TypedArray } from "./types.ts";
+import { window } from "./window.ts";
+
+let logDebug = false;
+let logSource = "JS";
+
+// @internal
+export function setLogDebug(debug: boolean, source?: string): void {
+ logDebug = debug;
+ if (source) {
+ logSource = source;
+ }
+}
+
+/** Debug logging for deno.
+ * Enable with the `--log-debug` or `-D` command line flag.
+ * @internal
+ */
+export function log(...args: unknown[]): void {
+ if (logDebug) {
+ // if we destructure `console` off `window` too early, we don't bind to
+ // the right console, therefore we don't log anything out.
+ window.console.log(`DEBUG ${logSource} -`, ...args);
+ }
+}
+
+// @internal
+export function assert(cond: boolean, msg = "assert"): void {
+ if (!cond) {
+ throw Error(msg);
+ }
+}
+
+// @internal
+export function typedArrayToArrayBuffer(ta: TypedArray): ArrayBuffer {
+ const ab = ta.buffer.slice(ta.byteOffset, ta.byteOffset + ta.byteLength);
+ return ab as ArrayBuffer;
+}
+
+// @internal
+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.
+ *
+ * At the time of writing, the GitHub issue is closed in favour of a proposed
+ * solution that is awaiting feedback.
+ *
+ * @see https://github.com/Microsoft/TypeScript/issues/15202
+ * @see https://github.com/Microsoft/TypeScript/issues/15397
+ * @internal
+ */
+
+export interface ResolvableMethods<T> {
+ resolve: (value?: T | PromiseLike<T>) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ reject: (reason?: any) => void;
+}
+
+// @internal
+export type Resolvable<T> = Promise<T> & ResolvableMethods<T>;
+
+// @internal
+export function createResolvable<T>(): Resolvable<T> {
+ let methods: ResolvableMethods<T>;
+ const promise = new Promise<T>(
+ (resolve, reject): void => {
+ methods = { resolve, reject };
+ }
+ );
+ // TypeScript doesn't know that the Promise callback occurs synchronously
+ // therefore use of not null assertion (`!`)
+ return Object.assign(promise, methods!) as Resolvable<T>;
+}
+
+// @internal
+export function notImplemented(): never {
+ throw new Error("Not implemented");
+}
+
+// @internal
+export function unreachable(): never {
+ throw new Error("Code not reachable");
+}
+
+// @internal
+export function hexdump(u8: Uint8Array): string {
+ return Array.prototype.map
+ .call(
+ u8,
+ (x: number): string => {
+ return ("00" + x.toString(16)).slice(-2);
+ }
+ )
+ .join(" ");
+}
+
+// @internal
+export function containsOnlyASCII(str: string): boolean {
+ if (typeof str !== "string") {
+ return false;
+ }
+ return /^[\x00-\x7F]*$/.test(str);
+}
+
+const TypedArrayConstructor = Object.getPrototypeOf(Uint8Array);
+export function isTypedArray(x: unknown): x is TypedArray {
+ return x instanceof TypedArrayConstructor;
+}
+
+// Returns whether o is an object, not null, and not a function.
+// @internal
+export function isObject(o: unknown): o is object {
+ return o != null && typeof o === "object";
+}
+
+// Returns whether o is iterable.
+export function isIterable<T, P extends keyof T, K extends T[P]>(
+ o: T
+): o is T & Iterable<[P, K]> {
+ // checks for null and undefined
+ if (o == null) {
+ return false;
+ }
+ return (
+ typeof ((o as unknown) as Iterable<[P, K]>)[Symbol.iterator] === "function"
+ );
+}
+
+// @internal
+export function requiredArguments(
+ name: string,
+ length: number,
+ required: number
+): void {
+ if (length < required) {
+ const errMsg = `${name} requires at least ${required} argument${
+ required === 1 ? "" : "s"
+ }, but only ${length} present`;
+ throw new TypeError(errMsg);
+ }
+}
+
+// @internal
+export function immutableDefine(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ o: any,
+ p: string | number | symbol,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ value: any
+): void {
+ Object.defineProperty(o, p, {
+ value,
+ configurable: false,
+ writable: false
+ });
+}
+
+// Returns values from a WeakMap to emulate private properties in JavaScript
+export function getPrivateValue<
+ K extends object,
+ V extends object,
+ W extends keyof V
+>(instance: K, weakMap: WeakMap<K, V>, key: W): V[W] {
+ if (weakMap.has(instance)) {
+ return weakMap.get(instance)![key];
+ }
+ throw new TypeError("Illegal invocation");
+}
+
+/**
+ * Determines whether an object has a property with the specified name.
+ * Avoid calling prototype builtin `hasOwnProperty` for two reasons:
+ *
+ * 1. `hasOwnProperty` is defined on the object as something else:
+ *
+ * const options = {
+ * ending: 'utf8',
+ * hasOwnProperty: 'foo'
+ * };
+ * options.hasOwnProperty('ending') // throws a TypeError
+ *
+ * 2. The object doesn't inherit from `Object.prototype`:
+ *
+ * const options = Object.create(null);
+ * options.ending = 'utf8';
+ * options.hasOwnProperty('ending'); // throws a TypeError
+ *
+ * @param obj A Object.
+ * @param v A property name.
+ * @see https://eslint.org/docs/rules/no-prototype-builtins
+ * @internal
+ */
+export function hasOwnProperty<T>(obj: T, v: PropertyKey): boolean {
+ if (obj == null) {
+ return false;
+ }
+ return Object.prototype.hasOwnProperty.call(obj, v);
+}
+
+/**
+ * Split a number into two parts: lower 32 bit and higher 32 bit
+ * (as if the number is represented as uint64.)
+ *
+ * @param n Number to split.
+ * @internal
+ */
+export function splitNumberToParts(n: number): number[] {
+ // JS bitwise operators (OR, SHIFT) operate as if number is uint32.
+ const lower = n | 0;
+ // This is also faster than Math.floor(n / 0x100000000) in V8.
+ const higher = (n - lower) / 0x100000000;
+ return [lower, higher];
+}
diff --git a/cli/js/utime.ts b/cli/js/utime.ts
new file mode 100644
index 000000000..7495378b1
--- /dev/null
+++ b/cli/js/utime.ts
@@ -0,0 +1,45 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { sendSync, sendAsync } from "./dispatch_json.ts";
+import { OP_UTIME } from "./dispatch.ts";
+
+function toSecondsFromEpoch(v: number | Date): number {
+ return v instanceof Date ? v.valueOf() / 1000 : v;
+}
+
+/** Synchronously changes the access and modification times of a file system
+ * object referenced by `filename`. Given times are either in seconds
+ * (Unix epoch time) or as `Date` objects.
+ *
+ * Deno.utimeSync("myfile.txt", 1556495550, new Date());
+ */
+export function utimeSync(
+ filename: string,
+ atime: number | Date,
+ mtime: number | Date
+): void {
+ sendSync(OP_UTIME, {
+ filename,
+ // TODO(ry) split atime, mtime into [seconds, nanoseconds] tuple
+ atime: toSecondsFromEpoch(atime),
+ mtime: toSecondsFromEpoch(mtime)
+ });
+}
+
+/** Changes the access and modification times of a file system object
+ * referenced by `filename`. Given times are either in seconds
+ * (Unix epoch time) or as `Date` objects.
+ *
+ * await Deno.utime("myfile.txt", 1556495550, new Date());
+ */
+export async function utime(
+ filename: string,
+ atime: number | Date,
+ mtime: number | Date
+): Promise<void> {
+ await sendAsync(OP_UTIME, {
+ filename,
+ // TODO(ry) split atime, mtime into [seconds, nanoseconds] tuple
+ atime: toSecondsFromEpoch(atime),
+ mtime: toSecondsFromEpoch(mtime)
+ });
+}
diff --git a/cli/js/utime_test.ts b/cli/js/utime_test.ts
new file mode 100644
index 000000000..535ee1f40
--- /dev/null
+++ b/cli/js/utime_test.ts
@@ -0,0 +1,181 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+// Allow 10 second difference.
+// Note this might not be enough for FAT (but we are not testing on such fs).
+function assertFuzzyTimestampEquals(t1: number, t2: number): void {
+ assert(Math.abs(t1 - t2) < 10);
+}
+
+testPerm({ read: true, write: true }, function utimeSyncFileSuccess(): void {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ Deno.writeFileSync(filename, new TextEncoder().encode("hello"), {
+ perm: 0o666
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ Deno.utimeSync(filename, atime, mtime);
+
+ const fileInfo = Deno.statSync(filename);
+ assertFuzzyTimestampEquals(fileInfo.accessed, atime);
+ assertFuzzyTimestampEquals(fileInfo.modified, mtime);
+});
+
+testPerm(
+ { read: true, write: true },
+ function utimeSyncDirectorySuccess(): void {
+ const testDir = Deno.makeTempDirSync();
+
+ const atime = 1000;
+ const mtime = 50000;
+ Deno.utimeSync(testDir, atime, mtime);
+
+ const dirInfo = Deno.statSync(testDir);
+ assertFuzzyTimestampEquals(dirInfo.accessed, atime);
+ assertFuzzyTimestampEquals(dirInfo.modified, mtime);
+ }
+);
+
+testPerm({ read: true, write: true }, function utimeSyncDateSuccess(): void {
+ const testDir = Deno.makeTempDirSync();
+
+ const atime = 1000;
+ const mtime = 50000;
+ Deno.utimeSync(testDir, new Date(atime * 1000), new Date(mtime * 1000));
+
+ const dirInfo = Deno.statSync(testDir);
+ assertFuzzyTimestampEquals(dirInfo.accessed, atime);
+ assertFuzzyTimestampEquals(dirInfo.modified, mtime);
+});
+
+testPerm(
+ { read: true, write: true },
+ function utimeSyncLargeNumberSuccess(): void {
+ const testDir = Deno.makeTempDirSync();
+
+ // There are Rust side caps (might be fs relate),
+ // so JUST make them slightly larger than UINT32_MAX.
+ const atime = 0x100000001;
+ const mtime = 0x100000002;
+ Deno.utimeSync(testDir, atime, mtime);
+
+ const dirInfo = Deno.statSync(testDir);
+ assertFuzzyTimestampEquals(dirInfo.accessed, atime);
+ assertFuzzyTimestampEquals(dirInfo.modified, mtime);
+ }
+);
+
+testPerm({ read: true, write: true }, function utimeSyncNotFound(): void {
+ const atime = 1000;
+ const mtime = 50000;
+
+ let caughtError = false;
+ try {
+ Deno.utimeSync("/baddir", atime, mtime);
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ assertEquals(e.name, "NotFound");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true, write: false }, function utimeSyncPerm(): void {
+ const atime = 1000;
+ const mtime = 50000;
+
+ let caughtError = false;
+ try {
+ Deno.utimeSync("/some_dir", atime, mtime);
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm(
+ { read: true, write: true },
+ async function utimeFileSuccess(): Promise<void> {
+ const testDir = Deno.makeTempDirSync();
+ const filename = testDir + "/file.txt";
+ Deno.writeFileSync(filename, new TextEncoder().encode("hello"), {
+ perm: 0o666
+ });
+
+ const atime = 1000;
+ const mtime = 50000;
+ await Deno.utime(filename, atime, mtime);
+
+ const fileInfo = Deno.statSync(filename);
+ assertFuzzyTimestampEquals(fileInfo.accessed, atime);
+ assertFuzzyTimestampEquals(fileInfo.modified, mtime);
+ }
+);
+
+testPerm(
+ { read: true, write: true },
+ async function utimeDirectorySuccess(): Promise<void> {
+ const testDir = Deno.makeTempDirSync();
+
+ const atime = 1000;
+ const mtime = 50000;
+ await Deno.utime(testDir, atime, mtime);
+
+ const dirInfo = Deno.statSync(testDir);
+ assertFuzzyTimestampEquals(dirInfo.accessed, atime);
+ assertFuzzyTimestampEquals(dirInfo.modified, mtime);
+ }
+);
+
+testPerm(
+ { read: true, write: true },
+ async function utimeDateSuccess(): Promise<void> {
+ const testDir = Deno.makeTempDirSync();
+
+ const atime = 1000;
+ const mtime = 50000;
+ await Deno.utime(testDir, new Date(atime * 1000), new Date(mtime * 1000));
+
+ const dirInfo = Deno.statSync(testDir);
+ assertFuzzyTimestampEquals(dirInfo.accessed, atime);
+ assertFuzzyTimestampEquals(dirInfo.modified, mtime);
+ }
+);
+
+testPerm({ read: true, write: true }, async function utimeNotFound(): Promise<
+ void
+> {
+ const atime = 1000;
+ const mtime = 50000;
+
+ let caughtError = false;
+ try {
+ await Deno.utime("/baddir", atime, mtime);
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ assertEquals(e.name, "NotFound");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true, write: false }, async function utimeSyncPerm(): Promise<
+ void
+> {
+ const atime = 1000;
+ const mtime = 50000;
+
+ let caughtError = false;
+ try {
+ await Deno.utime("/some_dir", atime, mtime);
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
diff --git a/cli/js/version.ts b/cli/js/version.ts
new file mode 100644
index 000000000..08ac58122
--- /dev/null
+++ b/cli/js/version.ts
@@ -0,0 +1,28 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+interface Version {
+ deno: string;
+ v8: string;
+ typescript: string;
+}
+
+export const version: Version = {
+ deno: "",
+ v8: "",
+ typescript: ""
+};
+
+/**
+ * Sets the deno, v8, and typescript versions and freezes the version object.
+ * @internal
+ */
+export function setVersions(
+ denoVersion: string,
+ v8Version: string,
+ tsVersion: string
+): void {
+ version.deno = denoVersion;
+ version.v8 = v8Version;
+ version.typescript = tsVersion;
+
+ Object.freeze(version);
+}
diff --git a/cli/js/version_test.ts b/cli/js/version_test.ts
new file mode 100644
index 000000000..b32230812
--- /dev/null
+++ b/cli/js/version_test.ts
@@ -0,0 +1,8 @@
+import { test, assert } from "./test_util.ts";
+
+test(function version(): void {
+ const pattern = /^\d+\.\d+\.\d+/;
+ assert(pattern.test(Deno.version.deno));
+ assert(pattern.test(Deno.version.v8));
+ assert(pattern.test(Deno.version.typescript));
+});
diff --git a/cli/js/window.ts b/cli/js/window.ts
new file mode 100644
index 000000000..3d3d6601f
--- /dev/null
+++ b/cli/js/window.ts
@@ -0,0 +1,9 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+// (0, eval) is indirect eval.
+// See the links below for details:
+// - https://stackoverflow.com/a/14120023
+// - https://tc39.github.io/ecma262/#sec-performeval (spec)
+export const window = (0, eval)("this");
+// TODO: The above should be replaced with globalThis
+// when the globalThis proposal goes to stage 4
+// See https://github.com/tc39/proposal-global
diff --git a/cli/js/workers.ts b/cli/js/workers.ts
new file mode 100644
index 000000000..281fe619f
--- /dev/null
+++ b/cli/js/workers.ts
@@ -0,0 +1,193 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import * as dispatch from "./dispatch.ts";
+import { sendAsync, sendSync } from "./dispatch_json.ts";
+import { log } from "./util.ts";
+import { TextDecoder, TextEncoder } from "./text_encoding.ts";
+import { window } from "./window.ts";
+import { blobURLMap } from "./url.ts";
+import { blobBytesWeakMap } from "./blob.ts";
+
+const encoder = new TextEncoder();
+const decoder = new TextDecoder();
+
+export function encodeMessage(data: any): Uint8Array {
+ const dataJson = JSON.stringify(data);
+ return encoder.encode(dataJson);
+}
+
+export function decodeMessage(dataIntArray: Uint8Array): any {
+ const dataJson = decoder.decode(dataIntArray);
+ return JSON.parse(dataJson);
+}
+
+function createWorker(
+ specifier: string,
+ includeDenoNamespace: boolean,
+ hasSourceCode: boolean,
+ sourceCode: Uint8Array
+): number {
+ return sendSync(dispatch.OP_CREATE_WORKER, {
+ specifier,
+ includeDenoNamespace,
+ hasSourceCode,
+ sourceCode: new TextDecoder().decode(sourceCode)
+ });
+}
+
+async function hostGetWorkerClosed(rid: number): Promise<void> {
+ await sendAsync(dispatch.OP_HOST_GET_WORKER_CLOSED, { rid });
+}
+
+function hostPostMessage(rid: number, data: any): void {
+ const dataIntArray = encodeMessage(data);
+ sendSync(dispatch.OP_HOST_POST_MESSAGE, { rid }, dataIntArray);
+}
+
+async function hostGetMessage(rid: number): Promise<any> {
+ const res = await sendAsync(dispatch.OP_HOST_GET_MESSAGE, { rid });
+
+ if (res.data != null) {
+ return decodeMessage(new Uint8Array(res.data));
+ } else {
+ return null;
+ }
+}
+
+// Stuff for workers
+export const onmessage: (e: { data: any }) => void = (): void => {};
+
+export function postMessage(data: any): void {
+ const dataIntArray = encodeMessage(data);
+ sendSync(dispatch.OP_WORKER_POST_MESSAGE, {}, dataIntArray);
+}
+
+export async function getMessage(): Promise<any> {
+ log("getMessage");
+ const res = await sendAsync(dispatch.OP_WORKER_GET_MESSAGE);
+
+ if (res.data != null) {
+ return decodeMessage(new Uint8Array(res.data));
+ } else {
+ return null;
+ }
+}
+
+export let isClosing = false;
+
+export function workerClose(): void {
+ isClosing = true;
+}
+
+export async function workerMain(): Promise<void> {
+ log("workerMain");
+
+ while (!isClosing) {
+ const data = await getMessage();
+ if (data == null) {
+ log("workerMain got null message. quitting.");
+ break;
+ }
+
+ if (window["onmessage"]) {
+ const event = { data };
+ const result: void | Promise<void> = window.onmessage(event);
+ if (result && "then" in result) {
+ await result;
+ }
+ }
+
+ if (!window["onmessage"]) {
+ break;
+ }
+ }
+}
+
+export interface Worker {
+ onerror?: () => void;
+ onmessage?: (e: { data: any }) => void;
+ onmessageerror?: () => void;
+ postMessage(data: any): void;
+ closed: Promise<void>;
+}
+
+// TODO(kevinkassimo): Maybe implement reasonable web worker options?
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface WorkerOptions {}
+
+/** Extended Deno Worker initialization options.
+ * `noDenoNamespace` hides global `window.Deno` namespace for
+ * spawned worker and nested workers spawned by it (default: false).
+ */
+export interface DenoWorkerOptions extends WorkerOptions {
+ noDenoNamespace?: boolean;
+}
+
+export class WorkerImpl implements Worker {
+ private readonly rid: number;
+ private isClosing = false;
+ private readonly isClosedPromise: Promise<void>;
+ public onerror?: () => void;
+ public onmessage?: (data: any) => void;
+ public onmessageerror?: () => void;
+
+ constructor(specifier: string, options?: DenoWorkerOptions) {
+ let hasSourceCode = false;
+ let sourceCode = new Uint8Array();
+
+ let includeDenoNamespace = true;
+ if (options && options.noDenoNamespace) {
+ includeDenoNamespace = false;
+ }
+ // Handle blob URL.
+ if (specifier.startsWith("blob:")) {
+ hasSourceCode = true;
+ const b = blobURLMap.get(specifier);
+ if (!b) {
+ throw new Error("No Blob associated with the given URL is found");
+ }
+ const blobBytes = blobBytesWeakMap.get(b!);
+ if (!blobBytes) {
+ throw new Error("Invalid Blob");
+ }
+ sourceCode = blobBytes!;
+ }
+
+ this.rid = createWorker(
+ specifier,
+ includeDenoNamespace,
+ hasSourceCode,
+ sourceCode
+ );
+ this.run();
+ this.isClosedPromise = hostGetWorkerClosed(this.rid);
+ this.isClosedPromise.then(
+ (): void => {
+ this.isClosing = true;
+ }
+ );
+ }
+
+ get closed(): Promise<void> {
+ return this.isClosedPromise;
+ }
+
+ postMessage(data: any): void {
+ hostPostMessage(this.rid, data);
+ }
+
+ private async run(): Promise<void> {
+ while (!this.isClosing) {
+ const data = await hostGetMessage(this.rid);
+ if (data == null) {
+ log("worker got null message. quitting.");
+ break;
+ }
+ // TODO(afinch7) stop this from eating messages before onmessage has been assigned
+ if (this.onmessage) {
+ const event = { data };
+ this.onmessage(event);
+ }
+ }
+ }
+}
diff --git a/cli/js/write_file.ts b/cli/js/write_file.ts
new file mode 100644
index 000000000..d6307e002
--- /dev/null
+++ b/cli/js/write_file.ts
@@ -0,0 +1,76 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { stat, statSync } from "./stat.ts";
+import { open, openSync } from "./files.ts";
+import { chmod, chmodSync } from "./chmod.ts";
+import { writeAll, writeAllSync } from "./buffer.ts";
+
+/** Options for writing to a file.
+ * `perm` would change the file's permission if set.
+ * `create` decides if the file should be created if not exists (default: true)
+ * `append` decides if the file should be appended (default: false)
+ */
+export interface WriteFileOptions {
+ perm?: number;
+ create?: boolean;
+ append?: boolean;
+}
+
+/** Write a new file, with given filename and data synchronously.
+ *
+ * const encoder = new TextEncoder();
+ * const data = encoder.encode("Hello world\n");
+ * Deno.writeFileSync("hello.txt", data);
+ */
+export function writeFileSync(
+ filename: string,
+ data: Uint8Array,
+ options: WriteFileOptions = {}
+): void {
+ if (options.create !== undefined) {
+ const create = !!options.create;
+ if (!create) {
+ // verify that file exists
+ statSync(filename);
+ }
+ }
+
+ const openMode = !!options.append ? "a" : "w";
+ const file = openSync(filename, openMode);
+
+ if (options.perm !== undefined && options.perm !== null) {
+ chmodSync(filename, options.perm);
+ }
+
+ writeAllSync(file, data);
+ file.close();
+}
+
+/** Write a new file, with given filename and data.
+ *
+ * const encoder = new TextEncoder();
+ * const data = encoder.encode("Hello world\n");
+ * await Deno.writeFile("hello.txt", data);
+ */
+export async function writeFile(
+ filename: string,
+ data: Uint8Array,
+ options: WriteFileOptions = {}
+): Promise<void> {
+ if (options.create !== undefined) {
+ const create = !!options.create;
+ if (!create) {
+ // verify that file exists
+ await stat(filename);
+ }
+ }
+
+ const openMode = !!options.append ? "a" : "w";
+ const file = await open(filename, openMode);
+
+ if (options.perm !== undefined && options.perm !== null) {
+ await chmod(filename, options.perm);
+ }
+
+ await writeAll(file, data);
+ file.close();
+}
diff --git a/cli/js/write_file_test.ts b/cli/js/write_file_test.ts
new file mode 100644
index 000000000..e1bbb67b3
--- /dev/null
+++ b/cli/js/write_file_test.ts
@@ -0,0 +1,219 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { testPerm, assert, assertEquals } from "./test_util.ts";
+
+testPerm({ read: true, write: true }, function writeFileSyncSuccess(): void {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data);
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals("Hello", actual);
+});
+
+testPerm({ write: true }, function writeFileSyncFail(): void {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = "/baddir/test.txt";
+ // The following should fail because /baddir doesn't exist (hopefully).
+ let caughtError = false;
+ try {
+ Deno.writeFileSync(filename, data);
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ assertEquals(e.name, "NotFound");
+ }
+ assert(caughtError);
+});
+
+testPerm({ write: false }, function writeFileSyncPerm(): void {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = "/baddir/test.txt";
+ // The following should fail due to no write permission
+ let caughtError = false;
+ try {
+ Deno.writeFileSync(filename, data);
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ read: true, write: true }, function writeFileSyncUpdatePerm(): void {
+ if (Deno.build.os !== "win") {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data, { perm: 0o755 });
+ assertEquals(Deno.statSync(filename).mode & 0o777, 0o755);
+ Deno.writeFileSync(filename, data, { perm: 0o666 });
+ assertEquals(Deno.statSync(filename).mode & 0o777, 0o666);
+ }
+});
+
+testPerm({ read: true, write: true }, function writeFileSyncCreate(): void {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ let caughtError = false;
+ // if create turned off, the file won't be created
+ try {
+ Deno.writeFileSync(filename, data, { create: false });
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ assertEquals(e.name, "NotFound");
+ }
+ assert(caughtError);
+
+ // Turn on create, should have no error
+ Deno.writeFileSync(filename, data, { create: true });
+ Deno.writeFileSync(filename, data, { create: false });
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals("Hello", actual);
+});
+
+testPerm({ read: true, write: true }, function writeFileSyncAppend(): void {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ Deno.writeFileSync(filename, data);
+ Deno.writeFileSync(filename, data, { append: true });
+ let dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ let actual = dec.decode(dataRead);
+ assertEquals("HelloHello", actual);
+ // Now attempt overwrite
+ Deno.writeFileSync(filename, data, { append: false });
+ dataRead = Deno.readFileSync(filename);
+ actual = dec.decode(dataRead);
+ assertEquals("Hello", actual);
+ // append not set should also overwrite
+ Deno.writeFileSync(filename, data);
+ dataRead = Deno.readFileSync(filename);
+ actual = dec.decode(dataRead);
+ assertEquals("Hello", actual);
+});
+
+testPerm(
+ { read: true, write: true },
+ async function writeFileSuccess(): Promise<void> {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeFile(filename, data);
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals("Hello", actual);
+ }
+);
+
+testPerm(
+ { read: true, write: true },
+ async function writeFileNotFound(): Promise<void> {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = "/baddir/test.txt";
+ // The following should fail because /baddir doesn't exist (hopefully).
+ let caughtError = false;
+ try {
+ await Deno.writeFile(filename, data);
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ assertEquals(e.name, "NotFound");
+ }
+ assert(caughtError);
+ }
+);
+
+testPerm({ read: true, write: false }, async function writeFilePerm(): Promise<
+ void
+> {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = "/baddir/test.txt";
+ // The following should fail due to no write permission
+ let caughtError = false;
+ try {
+ await Deno.writeFile(filename, data);
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.PermissionDenied);
+ assertEquals(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm(
+ { read: true, write: true },
+ async function writeFileUpdatePerm(): Promise<void> {
+ if (Deno.build.os !== "win") {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeFile(filename, data, { perm: 0o755 });
+ assertEquals(Deno.statSync(filename).mode & 0o777, 0o755);
+ await Deno.writeFile(filename, data, { perm: 0o666 });
+ assertEquals(Deno.statSync(filename).mode & 0o777, 0o666);
+ }
+ }
+);
+
+testPerm({ read: true, write: true }, async function writeFileCreate(): Promise<
+ void
+> {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ let caughtError = false;
+ // if create turned off, the file won't be created
+ try {
+ await Deno.writeFile(filename, data, { create: false });
+ } catch (e) {
+ caughtError = true;
+ assertEquals(e.kind, Deno.ErrorKind.NotFound);
+ assertEquals(e.name, "NotFound");
+ }
+ assert(caughtError);
+
+ // Turn on create, should have no error
+ await Deno.writeFile(filename, data, { create: true });
+ await Deno.writeFile(filename, data, { create: false });
+ const dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ const actual = dec.decode(dataRead);
+ assertEquals("Hello", actual);
+});
+
+testPerm({ read: true, write: true }, async function writeFileAppend(): Promise<
+ void
+> {
+ const enc = new TextEncoder();
+ const data = enc.encode("Hello");
+ const filename = Deno.makeTempDirSync() + "/test.txt";
+ await Deno.writeFile(filename, data);
+ await Deno.writeFile(filename, data, { append: true });
+ let dataRead = Deno.readFileSync(filename);
+ const dec = new TextDecoder("utf-8");
+ let actual = dec.decode(dataRead);
+ assertEquals("HelloHello", actual);
+ // Now attempt overwrite
+ await Deno.writeFile(filename, data, { append: false });
+ dataRead = Deno.readFileSync(filename);
+ actual = dec.decode(dataRead);
+ assertEquals("Hello", actual);
+ // append not set should also overwrite
+ await Deno.writeFile(filename, data);
+ dataRead = Deno.readFileSync(filename);
+ actual = dec.decode(dataRead);
+ assertEquals("Hello", actual);
+});