diff options
author | Ryan Dahl <ry@tinyclouds.org> | 2019-10-04 20:28:51 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-04 20:28:51 -0400 |
commit | b81e5db17aa8b3088d6034ddf86b79c69410f012 (patch) | |
tree | 579e4c23d60d1b0d038156bc28a04f74ea87b2f0 /cli/js | |
parent | 9049213867d30f7df090a83b6baf3e0717a4d2d2 (diff) |
Merge deno_cli_snapshots into deno_cli (#3064)
Diffstat (limited to 'cli/js')
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¶m2=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¶m2"); + 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); +}); |