diff options
Diffstat (limited to 'cli/rt')
59 files changed, 14638 insertions, 0 deletions
diff --git a/cli/rt/00_bootstrap_namespace.js b/cli/rt/00_bootstrap_namespace.js new file mode 100644 index 000000000..bccbc09c1 --- /dev/null +++ b/cli/rt/00_bootstrap_namespace.js @@ -0,0 +1,9 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// The only purpose of this file is to set up "globalThis.__bootstrap" namespace, +// that is used by scripts in this directory to reference exports between +// the files. + +// This namespace is removed during runtime bootstrapping process. + +globalThis.__bootstrap = {}; diff --git a/cli/rt/00_dom_exception.js b/cli/rt/00_dom_exception.js new file mode 100644 index 000000000..6d72779b0 --- /dev/null +++ b/cli/rt/00_dom_exception.js @@ -0,0 +1,15 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +class DOMException extends Error { + #name = ""; + + constructor(message = "", name = "Error") { + super(message); + this.#name = name; + } + + get name() { + return this.#name; + } +} diff --git a/cli/rt/01_build.js b/cli/rt/01_build.js new file mode 100644 index 000000000..7c1dc817e --- /dev/null +++ b/cli/rt/01_build.js @@ -0,0 +1,26 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const build = { + target: "unknown", + arch: "unknown", + os: "unknown", + vendor: "unknown", + env: undefined, + }; + + function setBuildInfo(target) { + const [arch, vendor, os, env] = target.split("-", 4); + build.target = target; + build.arch = arch; + build.vendor = vendor; + build.os = os; + build.env = env; + Object.freeze(build); + } + + window.__bootstrap.build = { + build, + setBuildInfo, + }; +})(this); diff --git a/cli/rt/01_colors.js b/cli/rt/01_colors.js new file mode 100644 index 000000000..2dc559186 --- /dev/null +++ b/cli/rt/01_colors.js @@ -0,0 +1,89 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + function code(open, close) { + return { + open: `\x1b[${open}m`, + close: `\x1b[${close}m`, + regexp: new RegExp(`\\x1b\\[${close}m`, "g"), + }; + } + + function run(str, code) { + return !globalThis || !globalThis.Deno || globalThis.Deno.noColor + ? str + : `${code.open}${str.replace(code.regexp, code.open)}${code.close}`; + } + + function bold(str) { + return run(str, code(1, 22)); + } + + function italic(str) { + return run(str, code(3, 23)); + } + + function yellow(str) { + return run(str, code(33, 39)); + } + + function cyan(str) { + return run(str, code(36, 39)); + } + + function red(str) { + return run(str, code(31, 39)); + } + + function green(str) { + return run(str, code(32, 39)); + } + + function bgRed(str) { + return run(str, code(41, 49)); + } + + function white(str) { + return run(str, code(37, 39)); + } + + function gray(str) { + return run(str, code(90, 39)); + } + + function magenta(str) { + return run(str, code(35, 39)); + } + + function dim(str) { + return run(str, code(2, 22)); + } + + // https://github.com/chalk/ansi-regex/blob/2b56fb0c7a07108e5b54241e8faec160d393aedb/index.js + const ANSI_PATTERN = new RegExp( + [ + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", + ].join("|"), + "g", + ); + + function stripColor(string) { + return string.replace(ANSI_PATTERN, ""); + } + + window.__bootstrap.colors = { + bold, + italic, + yellow, + cyan, + red, + green, + bgRed, + white, + gray, + magenta, + dim, + stripColor, + }; +})(this); diff --git a/cli/rt/01_errors.js b/cli/rt/01_errors.js new file mode 100644 index 000000000..8390dd803 --- /dev/null +++ b/cli/rt/01_errors.js @@ -0,0 +1,250 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + // Warning! The values in this enum are duplicated in cli/op_error.rs + // Update carefully! + const ErrorKind = { + 1: "NotFound", + 2: "PermissionDenied", + 3: "ConnectionRefused", + 4: "ConnectionReset", + 5: "ConnectionAborted", + 6: "NotConnected", + 7: "AddrInUse", + 8: "AddrNotAvailable", + 9: "BrokenPipe", + 10: "AlreadyExists", + 13: "InvalidData", + 14: "TimedOut", + 15: "Interrupted", + 16: "WriteZero", + 17: "UnexpectedEof", + 18: "BadResource", + 19: "Http", + 20: "URIError", + 21: "TypeError", + 22: "Other", + 23: "Busy", + + NotFound: 1, + PermissionDenied: 2, + ConnectionRefused: 3, + ConnectionReset: 4, + ConnectionAborted: 5, + NotConnected: 6, + AddrInUse: 7, + AddrNotAvailable: 8, + BrokenPipe: 9, + AlreadyExists: 10, + InvalidData: 13, + TimedOut: 14, + Interrupted: 15, + WriteZero: 16, + UnexpectedEof: 17, + BadResource: 18, + Http: 19, + URIError: 20, + TypeError: 21, + Other: 22, + Busy: 23, + }; + + function getErrorClass(kind) { + switch (kind) { + case ErrorKind.TypeError: + return TypeError; + case ErrorKind.Other: + return Error; + case ErrorKind.URIError: + return URIError; + case ErrorKind.NotFound: + return NotFound; + case ErrorKind.PermissionDenied: + return PermissionDenied; + case ErrorKind.ConnectionRefused: + return ConnectionRefused; + case ErrorKind.ConnectionReset: + return ConnectionReset; + case ErrorKind.ConnectionAborted: + return ConnectionAborted; + case ErrorKind.NotConnected: + return NotConnected; + case ErrorKind.AddrInUse: + return AddrInUse; + case ErrorKind.AddrNotAvailable: + return AddrNotAvailable; + case ErrorKind.BrokenPipe: + return BrokenPipe; + case ErrorKind.AlreadyExists: + return AlreadyExists; + case ErrorKind.InvalidData: + return InvalidData; + case ErrorKind.TimedOut: + return TimedOut; + case ErrorKind.Interrupted: + return Interrupted; + case ErrorKind.WriteZero: + return WriteZero; + case ErrorKind.UnexpectedEof: + return UnexpectedEof; + case ErrorKind.BadResource: + return BadResource; + case ErrorKind.Http: + return Http; + case ErrorKind.Busy: + return Busy; + } + } + + class NotFound extends Error { + constructor(msg) { + super(msg); + this.name = "NotFound"; + } + } + + class PermissionDenied extends Error { + constructor(msg) { + super(msg); + this.name = "PermissionDenied"; + } + } + + class ConnectionRefused extends Error { + constructor(msg) { + super(msg); + this.name = "ConnectionRefused"; + } + } + + class ConnectionReset extends Error { + constructor(msg) { + super(msg); + this.name = "ConnectionReset"; + } + } + + class ConnectionAborted extends Error { + constructor(msg) { + super(msg); + this.name = "ConnectionAborted"; + } + } + + class NotConnected extends Error { + constructor(msg) { + super(msg); + this.name = "NotConnected"; + } + } + + class AddrInUse extends Error { + constructor(msg) { + super(msg); + this.name = "AddrInUse"; + } + } + + class AddrNotAvailable extends Error { + constructor(msg) { + super(msg); + this.name = "AddrNotAvailable"; + } + } + + class BrokenPipe extends Error { + constructor(msg) { + super(msg); + this.name = "BrokenPipe"; + } + } + + class AlreadyExists extends Error { + constructor(msg) { + super(msg); + this.name = "AlreadyExists"; + } + } + + class InvalidData extends Error { + constructor(msg) { + super(msg); + this.name = "InvalidData"; + } + } + + class TimedOut extends Error { + constructor(msg) { + super(msg); + this.name = "TimedOut"; + } + } + + class Interrupted extends Error { + constructor(msg) { + super(msg); + this.name = "Interrupted"; + } + } + + class WriteZero extends Error { + constructor(msg) { + super(msg); + this.name = "WriteZero"; + } + } + + class UnexpectedEof extends Error { + constructor(msg) { + super(msg); + this.name = "UnexpectedEof"; + } + } + + class BadResource extends Error { + constructor(msg) { + super(msg); + this.name = "BadResource"; + } + } + + class Http extends Error { + constructor(msg) { + super(msg); + this.name = "Http"; + } + } + + class Busy extends Error { + constructor(msg) { + super(msg); + this.name = "Busy"; + } + } + + const errors = { + NotFound, + PermissionDenied, + ConnectionRefused, + ConnectionReset, + ConnectionAborted, + NotConnected, + AddrInUse, + AddrNotAvailable, + BrokenPipe, + AlreadyExists, + InvalidData, + TimedOut, + Interrupted, + WriteZero, + UnexpectedEof, + BadResource, + Http, + Busy, + }; + + window.__bootstrap.errors = { + errors, + getErrorClass, + }; +})(this); diff --git a/cli/rt/01_event.js b/cli/rt/01_event.js new file mode 100644 index 000000000..35967e0a1 --- /dev/null +++ b/cli/rt/01_event.js @@ -0,0 +1,1044 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This module follows most of the WHATWG Living Standard for the DOM logic. +// Many parts of the DOM are not implemented in Deno, but the logic for those +// parts still exists. This means you will observe a lot of strange structures +// and impossible logic branches based on what Deno currently supports. + +((window) => { + const eventData = new WeakMap(); + + function requiredArguments( + name, + length, + required, + ) { + if (length < required) { + const errMsg = `${name} requires at least ${required} argument${ + required === 1 ? "" : "s" + }, but only ${length} present`; + throw new TypeError(errMsg); + } + } + + // accessors for non runtime visible data + + function getDispatched(event) { + return Boolean(eventData.get(event)?.dispatched); + } + + function getPath(event) { + return eventData.get(event)?.path ?? []; + } + + function getStopImmediatePropagation(event) { + return Boolean(eventData.get(event)?.stopImmediatePropagation); + } + + function setCurrentTarget( + event, + value, + ) { + event.currentTarget = value; + } + + function setDispatched(event, value) { + const data = eventData.get(event); + if (data) { + data.dispatched = value; + } + } + + function setEventPhase(event, value) { + event.eventPhase = value; + } + + function setInPassiveListener(event, value) { + const data = eventData.get(event); + if (data) { + data.inPassiveListener = value; + } + } + + function setPath(event, value) { + const data = eventData.get(event); + if (data) { + data.path = value; + } + } + + function setRelatedTarget( + event, + value, + ) { + if ("relatedTarget" in event) { + event.relatedTarget = value; + } + } + + function setTarget(event, value) { + event.target = value; + } + + function setStopImmediatePropagation( + event, + value, + ) { + const data = eventData.get(event); + if (data) { + data.stopImmediatePropagation = value; + } + } + + // Type guards that widen the event type + + function hasRelatedTarget( + event, + ) { + return "relatedTarget" in event; + } + + function isTrusted() { + return eventData.get(this).isTrusted; + } + + class Event { + #canceledFlag = false; + #stopPropagationFlag = false; + #attributes = {}; + + constructor(type, eventInitDict = {}) { + requiredArguments("Event", arguments.length, 1); + type = String(type); + this.#attributes = { + type, + bubbles: eventInitDict.bubbles ?? false, + cancelable: eventInitDict.cancelable ?? false, + composed: eventInitDict.composed ?? false, + currentTarget: null, + eventPhase: Event.NONE, + target: null, + timeStamp: Date.now(), + }; + eventData.set(this, { + dispatched: false, + inPassiveListener: false, + isTrusted: false, + path: [], + stopImmediatePropagation: false, + }); + Reflect.defineProperty(this, "isTrusted", { + enumerable: true, + get: isTrusted, + }); + } + + get bubbles() { + return this.#attributes.bubbles; + } + + get cancelBubble() { + return this.#stopPropagationFlag; + } + + set cancelBubble(value) { + this.#stopPropagationFlag = value; + } + + get cancelable() { + return this.#attributes.cancelable; + } + + get composed() { + return this.#attributes.composed; + } + + get currentTarget() { + return this.#attributes.currentTarget; + } + + set currentTarget(value) { + this.#attributes = { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: value, + eventPhase: this.eventPhase, + target: this.target, + timeStamp: this.timeStamp, + }; + } + + get defaultPrevented() { + return this.#canceledFlag; + } + + get eventPhase() { + return this.#attributes.eventPhase; + } + + set eventPhase(value) { + this.#attributes = { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: this.currentTarget, + eventPhase: value, + target: this.target, + timeStamp: this.timeStamp, + }; + } + + get initialized() { + return true; + } + + get target() { + return this.#attributes.target; + } + + set target(value) { + this.#attributes = { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: this.currentTarget, + eventPhase: this.eventPhase, + target: value, + timeStamp: this.timeStamp, + }; + } + + get timeStamp() { + return this.#attributes.timeStamp; + } + + get type() { + return this.#attributes.type; + } + + composedPath() { + const path = eventData.get(this).path; + if (path.length === 0) { + return []; + } + + if (!this.currentTarget) { + throw new Error("assertion error"); + } + const composedPath = [ + { + item: this.currentTarget, + itemInShadowTree: false, + relatedTarget: null, + rootOfClosedTree: false, + slotInClosedTree: false, + target: null, + touchTargetList: [], + }, + ]; + + let currentTargetIndex = 0; + let currentTargetHiddenSubtreeLevel = 0; + + for (let index = path.length - 1; index >= 0; index--) { + const { item, rootOfClosedTree, slotInClosedTree } = 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 } = 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 < path.length; index++) { + const { item, rootOfClosedTree, slotInClosedTree } = 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.map((p) => p.item); + } + + preventDefault() { + if (this.cancelable && !eventData.get(this).inPassiveListener) { + this.#canceledFlag = true; + } + } + + stopPropagation() { + this.#stopPropagationFlag = true; + } + + stopImmediatePropagation() { + this.#stopPropagationFlag = true; + eventData.get(this).stopImmediatePropagation = true; + } + + get NONE() { + return Event.NONE; + } + + get CAPTURING_PHASE() { + return Event.CAPTURING_PHASE; + } + + get AT_TARGET() { + return Event.AT_TARGET; + } + + get BUBBLING_PHASE() { + return Event.BUBBLING_PHASE; + } + + static get NONE() { + return 0; + } + + static get CAPTURING_PHASE() { + return 1; + } + + static get AT_TARGET() { + return 2; + } + + static get BUBBLING_PHASE() { + return 3; + } + } + + function defineEnumerableProps( + Ctor, + props, + ) { + for (const prop of props) { + Reflect.defineProperty(Ctor.prototype, prop, { enumerable: true }); + } + } + + defineEnumerableProps(Event, [ + "bubbles", + "cancelable", + "composed", + "currentTarget", + "defaultPrevented", + "eventPhase", + "target", + "timeStamp", + "type", + ]); + + // This is currently the only node type we are using, so instead of implementing + // the whole of the Node interface at the moment, this just gives us the one + // value to power the standards based logic + const DOCUMENT_FRAGMENT_NODE = 11; + + // DOM Logic Helper functions and type guards + + /** Get the parent node, for event targets that have a parent. + * + * Ref: https://dom.spec.whatwg.org/#get-the-parent */ + function getParent(eventTarget) { + return isNode(eventTarget) ? eventTarget.parentNode : null; + } + + function getRoot(eventTarget) { + return isNode(eventTarget) + ? eventTarget.getRootNode({ composed: true }) + : null; + } + + function isNode( + eventTarget, + ) { + return Boolean(eventTarget && "nodeType" in eventTarget); + } + + // https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor + function isShadowInclusiveAncestor( + ancestor, + node, + ) { + while (isNode(node)) { + if (node === ancestor) { + return true; + } + + if (isShadowRoot(node)) { + node = node && getHost(node); + } else { + node = getParent(node); + } + } + + return false; + } + + function isShadowRoot(nodeImpl) { + return Boolean( + nodeImpl && + isNode(nodeImpl) && + nodeImpl.nodeType === DOCUMENT_FRAGMENT_NODE && + getHost(nodeImpl) != null, + ); + } + + function isSlotable( + nodeImpl, + ) { + return Boolean(isNode(nodeImpl) && "assignedSlot" in nodeImpl); + } + + // DOM Logic functions + + /** Append a path item to an event's path. + * + * Ref: https://dom.spec.whatwg.org/#concept-event-path-append + */ + function appendToEventPath( + eventImpl, + target, + targetOverride, + relatedTarget, + touchTargets, + slotInClosedTree, + ) { + const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target)); + const rootOfClosedTree = isShadowRoot(target) && + getMode(target) === "closed"; + + getPath(eventImpl).push({ + item: target, + itemInShadowTree, + target: targetOverride, + relatedTarget, + touchTargetList: touchTargets, + rootOfClosedTree, + slotInClosedTree, + }); + } + + function dispatch( + targetImpl, + eventImpl, + targetOverride, + ) { + let clearTargets = false; + let activationTarget = null; + + setDispatched(eventImpl, true); + + targetOverride = targetOverride ?? targetImpl; + const eventRelatedTarget = hasRelatedTarget(eventImpl) + ? eventImpl.relatedTarget + : null; + let relatedTarget = retarget(eventRelatedTarget, targetImpl); + + if (targetImpl !== relatedTarget || targetImpl === eventRelatedTarget) { + const touchTargets = []; + + appendToEventPath( + eventImpl, + targetImpl, + targetOverride, + relatedTarget, + touchTargets, + false, + ); + + const isActivationEvent = eventImpl.type === "click"; + + if (isActivationEvent && getHasActivationBehavior(targetImpl)) { + activationTarget = targetImpl; + } + + let slotInClosedTree = false; + let slotable = isSlotable(targetImpl) && getAssignedSlot(targetImpl) + ? targetImpl + : null; + let parent = getParent(targetImpl); + + // 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 && + getMode(parentRoot) === "closed" + ) { + slotInClosedTree = true; + } + } + + relatedTarget = retarget(eventRelatedTarget, parent); + + if ( + isNode(parent) && + isShadowInclusiveAncestor(getRoot(targetImpl), parent) + ) { + appendToEventPath( + eventImpl, + parent, + null, + relatedTarget, + touchTargets, + slotInClosedTree, + ); + } else if (parent === relatedTarget) { + parent = null; + } else { + targetImpl = parent; + + if ( + isActivationEvent && + activationTarget === null && + getHasActivationBehavior(targetImpl) + ) { + activationTarget = targetImpl; + } + + appendToEventPath( + eventImpl, + parent, + targetImpl, + relatedTarget, + touchTargets, + slotInClosedTree, + ); + } + + if (parent !== null) { + parent = getParent(parent); + } + + slotInClosedTree = false; + } + + let clearTargetsTupleIndex = -1; + const path = getPath(eventImpl); + for ( + let i = path.length - 1; + i >= 0 && clearTargetsTupleIndex === -1; + i-- + ) { + if (path[i].target !== null) { + clearTargetsTupleIndex = i; + } + } + const clearTargetsTuple = path[clearTargetsTupleIndex]; + + clearTargets = (isNode(clearTargetsTuple.target) && + isShadowRoot(getRoot(clearTargetsTuple.target))) || + (isNode(clearTargetsTuple.relatedTarget) && + isShadowRoot(getRoot(clearTargetsTuple.relatedTarget))); + + setEventPhase(eventImpl, Event.CAPTURING_PHASE); + + for (let i = path.length - 1; i >= 0; --i) { + const tuple = path[i]; + + if (tuple.target === null) { + invokeEventListeners(tuple, eventImpl); + } + } + + for (let i = 0; i < path.length; i++) { + const tuple = path[i]; + + if (tuple.target !== null) { + setEventPhase(eventImpl, Event.AT_TARGET); + } else { + setEventPhase(eventImpl, Event.BUBBLING_PHASE); + } + + if ( + (eventImpl.eventPhase === Event.BUBBLING_PHASE && + eventImpl.bubbles) || + eventImpl.eventPhase === Event.AT_TARGET + ) { + invokeEventListeners(tuple, eventImpl); + } + } + } + + setEventPhase(eventImpl, Event.NONE); + setCurrentTarget(eventImpl, null); + setPath(eventImpl, []); + setDispatched(eventImpl, false); + eventImpl.cancelBubble = false; + setStopImmediatePropagation(eventImpl, false); + + if (clearTargets) { + setTarget(eventImpl, null); + setRelatedTarget(eventImpl, null); + } + + // TODO: invoke activation targets if HTML nodes will be implemented + // if (activationTarget !== null) { + // if (!eventImpl.defaultPrevented) { + // activationTarget._activationBehavior(); + // } + // } + + return !eventImpl.defaultPrevented; + } + + /** Inner invoking of the event listeners where the resolved listeners are + * called. + * + * Ref: https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke */ + function innerInvokeEventListeners( + eventImpl, + targetListeners, + ) { + 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 === Event.CAPTURING_PHASE && !capture) || + (eventImpl.eventPhase === Event.BUBBLING_PHASE && capture) + ) { + continue; + } + + if (once) { + targetListeners[type].splice( + targetListeners[type].indexOf(listener), + 1, + ); + } + + if (passive) { + setInPassiveListener(eventImpl, true); + } + + if (typeof listener.callback === "object") { + if (typeof listener.callback.handleEvent === "function") { + listener.callback.handleEvent(eventImpl); + } + } else { + listener.callback.call(eventImpl.currentTarget, eventImpl); + } + + setInPassiveListener(eventImpl, false); + + if (getStopImmediatePropagation(eventImpl)) { + return found; + } + } + + return found; + } + + /** Invokes the listeners on a given event path with the supplied event. + * + * Ref: https://dom.spec.whatwg.org/#concept-event-listener-invoke */ + function invokeEventListeners(tuple, eventImpl) { + const path = getPath(eventImpl); + const tupleIndex = path.indexOf(tuple); + for (let i = tupleIndex; i >= 0; i--) { + const t = path[i]; + if (t.target) { + setTarget(eventImpl, t.target); + break; + } + } + + setRelatedTarget(eventImpl, tuple.relatedTarget); + + if (eventImpl.cancelBubble) { + return; + } + + setCurrentTarget(eventImpl, tuple.item); + + innerInvokeEventListeners(eventImpl, getListeners(tuple.item)); + } + + function normalizeAddEventHandlerOptions( + options, + ) { + if (typeof options === "boolean" || typeof options === "undefined") { + return { + capture: Boolean(options), + once: false, + passive: false, + }; + } else { + return options; + } + } + + function normalizeEventHandlerOptions( + options, + ) { + if (typeof options === "boolean" || typeof options === "undefined") { + return { + capture: Boolean(options), + }; + } else { + return options; + } + } + + /** Retarget the target following the spec logic. + * + * Ref: https://dom.spec.whatwg.org/#retarget */ + function retarget(a, b) { + while (true) { + if (!isNode(a)) { + return a; + } + + const aRoot = a.getRootNode(); + + if (aRoot) { + if ( + !isShadowRoot(aRoot) || + (isNode(b) && isShadowInclusiveAncestor(aRoot, b)) + ) { + return a; + } + + a = getHost(aRoot); + } + } + } + + // Accessors for non-public data + + const eventTargetData = new WeakMap(); + + function setEventTargetData(value) { + eventTargetData.set(value, getDefaultTargetData()); + } + + function getAssignedSlot(target) { + return Boolean(eventTargetData.get(target)?.assignedSlot); + } + + function getHasActivationBehavior(target) { + return Boolean( + eventTargetData.get(target)?.hasActivationBehavior, + ); + } + + function getHost(target) { + return eventTargetData.get(target)?.host ?? null; + } + + function getListeners(target) { + return eventTargetData.get(target)?.listeners ?? {}; + } + + function getMode(target) { + return eventTargetData.get(target)?.mode ?? null; + } + + function getDefaultTargetData() { + return { + assignedSlot: false, + hasActivationBehavior: false, + host: null, + listeners: Object.create(null), + mode: "", + }; + } + + class EventTarget { + constructor() { + eventTargetData.set(this, getDefaultTargetData()); + } + + addEventListener( + type, + callback, + options, + ) { + requiredArguments("EventTarget.addEventListener", arguments.length, 2); + if (callback === null) { + return; + } + + options = normalizeAddEventHandlerOptions(options); + const { listeners } = eventTargetData.get(this ?? globalThis); + + if (!(type in listeners)) { + listeners[type] = []; + } + + for (const listener of listeners[type]) { + if ( + ((typeof listener.options === "boolean" && + listener.options === options.capture) || + (typeof listener.options === "object" && + listener.options.capture === options.capture)) && + listener.callback === callback + ) { + return; + } + } + + listeners[type].push({ callback, options }); + } + + removeEventListener( + type, + callback, + options, + ) { + requiredArguments("EventTarget.removeEventListener", arguments.length, 2); + + const listeners = eventTargetData.get(this ?? globalThis).listeners; + if (callback !== null && type in listeners) { + listeners[type] = listeners[type].filter( + (listener) => listener.callback !== callback, + ); + } else if (callback === null || !listeners[type]) { + return; + } + + options = normalizeEventHandlerOptions(options); + + for (let i = 0; i < listeners[type].length; ++i) { + const listener = listeners[type][i]; + if ( + ((typeof listener.options === "boolean" && + listener.options === options.capture) || + (typeof listener.options === "object" && + listener.options.capture === options.capture)) && + listener.callback === callback + ) { + listeners[type].splice(i, 1); + break; + } + } + } + + dispatchEvent(event) { + requiredArguments("EventTarget.dispatchEvent", arguments.length, 1); + const self = this ?? globalThis; + + const listeners = eventTargetData.get(self).listeners; + if (!(event.type in listeners)) { + return true; + } + + if (getDispatched(event)) { + throw new DOMException("Invalid event state.", "InvalidStateError"); + } + + if (event.eventPhase !== Event.NONE) { + throw new DOMException("Invalid event state.", "InvalidStateError"); + } + + return dispatch(self, event); + } + + get [Symbol.toStringTag]() { + return "EventTarget"; + } + + getParent(_event) { + return null; + } + } + + defineEnumerableProps(EventTarget, [ + "addEventListener", + "removeEventListener", + "dispatchEvent", + ]); + + class ErrorEvent extends Event { + #message = ""; + #filename = ""; + #lineno = ""; + #colno = ""; + #error = ""; + + get message() { + return this.#message; + } + get filename() { + return this.#filename; + } + get lineno() { + return this.#lineno; + } + get colno() { + return this.#colno; + } + get error() { + return this.#error; + } + + constructor( + type, + { + bubbles, + cancelable, + composed, + message = "", + filename = "", + lineno = 0, + colno = 0, + error = null, + } = {}, + ) { + super(type, { + bubbles: bubbles, + cancelable: cancelable, + composed: composed, + }); + + this.#message = message; + this.#filename = filename; + this.#lineno = lineno; + this.#colno = colno; + this.#error = error; + } + + get [Symbol.toStringTag]() { + return "ErrorEvent"; + } + } + + defineEnumerableProps(ErrorEvent, [ + "message", + "filename", + "lineno", + "colno", + "error", + ]); + + class CustomEvent extends Event { + #detail = null; + + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + requiredArguments("CustomEvent", arguments.length, 1); + const { detail } = eventInitDict; + this.#detail = detail; + } + + get detail() { + return this.#detail; + } + + get [Symbol.toStringTag]() { + return "CustomEvent"; + } + } + + Reflect.defineProperty(CustomEvent.prototype, "detail", { + enumerable: true, + }); + + window.Event = Event; + window.EventTarget = EventTarget; + window.ErrorEvent = ErrorEvent; + window.CustomEvent = CustomEvent; + window.__bootstrap.eventTarget = { + setEventTargetData, + }; +})(this); diff --git a/cli/rt/01_internals.js b/cli/rt/01_internals.js new file mode 100644 index 000000000..eee9eeaf7 --- /dev/null +++ b/cli/rt/01_internals.js @@ -0,0 +1,23 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const internalSymbol = Symbol("Deno.internal"); + + // The object where all the internal fields for testing will be living. + const internalObject = {}; + + // Register a field to internalObject for test access, + // through Deno[Deno.internal][name]. + function exposeForTest(name, value) { + Object.defineProperty(internalObject, name, { + value, + enumerable: false, + }); + } + + window.__bootstrap.internals = { + internalSymbol, + internalObject, + exposeForTest, + }; +})(this); diff --git a/cli/rt/01_version.js b/cli/rt/01_version.js new file mode 100644 index 000000000..325e1156f --- /dev/null +++ b/cli/rt/01_version.js @@ -0,0 +1,26 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const version = { + deno: "", + v8: "", + typescript: "", + }; + + function setVersions( + denoVersion, + v8Version, + tsVersion, + ) { + version.deno = denoVersion; + version.v8 = v8Version; + version.typescript = tsVersion; + + Object.freeze(version); + } + + window.__bootstrap.version = { + version, + setVersions, + }; +})(this); diff --git a/cli/rt/01_web_util.js b/cli/rt/01_web_util.js new file mode 100644 index 000000000..596dcbfcd --- /dev/null +++ b/cli/rt/01_web_util.js @@ -0,0 +1,202 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + function isTypedArray(x) { + return ArrayBuffer.isView(x) && !(x instanceof DataView); + } + + function isInvalidDate(x) { + return isNaN(x.getTime()); + } + + function requiredArguments( + name, + length, + required, + ) { + if (length < required) { + const errMsg = `${name} requires at least ${required} argument${ + required === 1 ? "" : "s" + }, but only ${length} present`; + throw new TypeError(errMsg); + } + } + + function immutableDefine( + o, + p, + value, + ) { + Object.defineProperty(o, p, { + value, + configurable: false, + writable: false, + }); + } + + function hasOwnProperty(obj, v) { + if (obj == null) { + return false; + } + return Object.prototype.hasOwnProperty.call(obj, v); + } + + /** Returns whether o is iterable. */ + function isIterable( + o, + ) { + // checks for null and undefined + if (o == null) { + return false; + } + return ( + typeof (o)[Symbol.iterator] === "function" + ); + } + + const objectCloneMemo = new WeakMap(); + + function cloneArrayBuffer( + srcBuffer, + srcByteOffset, + srcLength, + _cloneConstructor, + ) { + // this function fudges the return type but SharedArrayBuffer is disabled for a while anyway + return srcBuffer.slice( + srcByteOffset, + srcByteOffset + srcLength, + ); + } + + /** Clone a value in a similar way to structured cloning. It is similar to a + * StructureDeserialize(StructuredSerialize(...)). */ + function cloneValue(value) { + switch (typeof value) { + case "number": + case "string": + case "boolean": + case "undefined": + case "bigint": + return value; + case "object": { + if (objectCloneMemo.has(value)) { + return objectCloneMemo.get(value); + } + if (value === null) { + return value; + } + if (value instanceof Date) { + return new Date(value.valueOf()); + } + if (value instanceof RegExp) { + return new RegExp(value); + } + if (value instanceof SharedArrayBuffer) { + return value; + } + if (value instanceof ArrayBuffer) { + const cloned = cloneArrayBuffer( + value, + 0, + value.byteLength, + ArrayBuffer, + ); + objectCloneMemo.set(value, cloned); + return cloned; + } + if (ArrayBuffer.isView(value)) { + const clonedBuffer = cloneValue(value.buffer); + // Use DataViewConstructor type purely for type-checking, can be a + // DataView or TypedArray. They use the same constructor signature, + // only DataView has a length in bytes and TypedArrays use a length in + // terms of elements, so we adjust for that. + let length; + if (value instanceof DataView) { + length = value.byteLength; + } else { + length = value.length; + } + return new (value.constructor)( + clonedBuffer, + value.byteOffset, + length, + ); + } + if (value instanceof Map) { + const clonedMap = new Map(); + objectCloneMemo.set(value, clonedMap); + value.forEach((v, k) => clonedMap.set(k, cloneValue(v))); + return clonedMap; + } + if (value instanceof Set) { + const clonedSet = new Map(); + objectCloneMemo.set(value, clonedSet); + value.forEach((v, k) => clonedSet.set(k, cloneValue(v))); + return clonedSet; + } + + const clonedObj = {}; + objectCloneMemo.set(value, clonedObj); + const sourceKeys = Object.getOwnPropertyNames(value); + for (const key of sourceKeys) { + clonedObj[key] = cloneValue(value[key]); + } + return clonedObj; + } + case "symbol": + case "function": + default: + throw new DOMException("Uncloneable value in stream", "DataCloneError"); + } + } + + /** A helper function which ensures accessors are enumerable, as they normally + * are not. */ + function defineEnumerableProps( + Ctor, + props, + ) { + for (const prop of props) { + Reflect.defineProperty(Ctor.prototype, prop, { enumerable: true }); + } + } + + function getHeaderValueParams(value) { + const params = new Map(); + // Forced to do so for some Map constructor param mismatch + value + .split(";") + .slice(1) + .map((s) => s.trim().split("=")) + .filter((arr) => arr.length > 1) + .map(([k, v]) => [k, v.replace(/^"([^"]*)"$/, "$1")]) + .forEach(([k, v]) => params.set(k, v)); + return params; + } + + function hasHeaderValueOf(s, value) { + return new RegExp(`^${value}[\t\s]*;?`).test(s); + } + + /** An internal function which provides a function name for some generated + * functions, so stack traces are a bit more readable. + */ + function setFunctionName(fn, value) { + Object.defineProperty(fn, "name", { value, configurable: true }); + } + + window.__bootstrap.webUtil = { + isTypedArray, + isInvalidDate, + requiredArguments, + immutableDefine, + hasOwnProperty, + isIterable, + cloneValue, + defineEnumerableProps, + getHeaderValueParams, + hasHeaderValueOf, + setFunctionName, + }; +})(this); diff --git a/cli/rt/02_abort_signal.js b/cli/rt/02_abort_signal.js new file mode 100644 index 000000000..cd38fff64 --- /dev/null +++ b/cli/rt/02_abort_signal.js @@ -0,0 +1,75 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const add = Symbol("add"); + const signalAbort = Symbol("signalAbort"); + const remove = Symbol("remove"); + + class AbortSignal extends EventTarget { + #aborted = false; + #abortAlgorithms = new Set(); + + [add](algorithm) { + this.#abortAlgorithms.add(algorithm); + } + + [signalAbort]() { + if (this.#aborted) { + return; + } + this.#aborted = true; + for (const algorithm of this.#abortAlgorithms) { + algorithm(); + } + this.#abortAlgorithms.clear(); + this.dispatchEvent(new Event("abort")); + } + + [remove](algorithm) { + this.#abortAlgorithms.delete(algorithm); + } + + constructor() { + super(); + this.onabort = null; + this.addEventListener("abort", (evt) => { + const { onabort } = this; + if (typeof onabort === "function") { + onabort.call(this, evt); + } + }); + } + + get aborted() { + return Boolean(this.#aborted); + } + + get [Symbol.toStringTag]() { + return "AbortSignal"; + } + } + + class AbortController { + #signal = new AbortSignal(); + + get signal() { + return this.#signal; + } + + abort() { + this.#signal[signalAbort](); + } + + get [Symbol.toStringTag]() { + return "AbortController"; + } + } + + window.__bootstrap.abortSignal = { + AbortSignal, + add, + signalAbort, + remove, + AbortController, + }; +})(this); diff --git a/cli/rt/02_console.js b/cli/rt/02_console.js new file mode 100644 index 000000000..5a9dd4186 --- /dev/null +++ b/cli/rt/02_console.js @@ -0,0 +1,1183 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const exposeForTest = window.__bootstrap.internals.exposeForTest; + const { + stripColor, + yellow, + dim, + cyan, + red, + green, + magenta, + bold, + } = window.__bootstrap.colors; + + const { + isTypedArray, + isInvalidDate, + hasOwnProperty, + } = window.__bootstrap.webUtil; + + // Copyright Joyent, Inc. and other Node contributors. MIT license. + // Forked from Node's lib/internal/cli_table.js + + const tableChars = { + middleMiddle: "─", + rowMiddle: "┼", + topRight: "┐", + topLeft: "┌", + leftMiddle: "├", + topMiddle: "┬", + bottomRight: "┘", + bottomLeft: "└", + bottomMiddle: "┴", + rightMiddle: "┤", + left: "│ ", + right: " │", + middle: " │ ", + }; + + function isFullWidthCodePoint(code) { + // Code points are partially derived from: + // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt + return ( + code >= 0x1100 && + (code <= 0x115f || // Hangul Jamo + code === 0x2329 || // LEFT-POINTING ANGLE BRACKET + code === 0x232a || // RIGHT-POINTING ANGLE BRACKET + // CJK Radicals Supplement .. Enclosed CJK Letters and Months + (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || + // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A + (code >= 0x3250 && code <= 0x4dbf) || + // CJK Unified Ideographs .. Yi Radicals + (code >= 0x4e00 && code <= 0xa4c6) || + // Hangul Jamo Extended-A + (code >= 0xa960 && code <= 0xa97c) || + // Hangul Syllables + (code >= 0xac00 && code <= 0xd7a3) || + // CJK Compatibility Ideographs + (code >= 0xf900 && code <= 0xfaff) || + // Vertical Forms + (code >= 0xfe10 && code <= 0xfe19) || + // CJK Compatibility Forms .. Small Form Variants + (code >= 0xfe30 && code <= 0xfe6b) || + // Halfwidth and Fullwidth Forms + (code >= 0xff01 && code <= 0xff60) || + (code >= 0xffe0 && code <= 0xffe6) || + // Kana Supplement + (code >= 0x1b000 && code <= 0x1b001) || + // Enclosed Ideographic Supplement + (code >= 0x1f200 && code <= 0x1f251) || + // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff + // Emoticons 0x1f600 - 0x1f64f + (code >= 0x1f300 && code <= 0x1f64f) || + // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane + (code >= 0x20000 && code <= 0x3fffd)) + ); + } + + function getStringWidth(str) { + str = stripColor(str).normalize("NFC"); + let width = 0; + + for (const ch of str) { + width += isFullWidthCodePoint(ch.codePointAt(0)) ? 2 : 1; + } + + return width; + } + + function renderRow(row, columnWidths) { + let out = tableChars.left; + for (let i = 0; i < row.length; i++) { + const cell = row[i]; + const len = getStringWidth(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; + } + + function cliTable(head, columns) { + const rows = []; + const columnWidths = head.map((h) => getStringWidth(h)); + const longestColumn = columns.reduce( + (n, a) => 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 = getStringWidth(value); + columnWidths[i] = Math.max(width, counted); + } + } + + const divider = columnWidths.map((i) => + 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; + } + /* End of forked part */ + + const DEFAULT_INSPECT_OPTIONS = { + depth: 4, + indentLevel: 0, + sorted: false, + trailingComma: false, + compact: true, + iterableLimit: 100, + }; + + const DEFAULT_INDENT = " "; // Default indent string + + const LINE_BREAKING_LENGTH = 80; + const MIN_GROUP_LENGTH = 6; + 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 */ + + const PROMISE_STRING_BASE_LENGTH = 12; + + class CSI { + static kClear = "\x1b[1;1H"; + static kClearScreenDown = "\x1b[0J"; + } + + /* eslint-disable @typescript-eslint/no-use-before-define */ + + function getClassInstanceName(instance) { + 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 inspectFunction(value, _ctx) { + // 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}]`; + } + + function inspectIterable( + value, + ctx, + level, + options, + inspectOptions, + ) { + if (level >= inspectOptions.depth) { + return cyan(`[${options.typeName}]`); + } + ctx.add(value); + + const entries = []; + + const iter = value.entries(); + let entriesLength = 0; + const next = () => { + return iter.next(); + }; + for (const el of iter) { + if (entriesLength < inspectOptions.iterableLimit) { + entries.push( + options.entryHandler( + el, + ctx, + level + 1, + inspectOptions, + next.bind(iter), + ), + ); + } + entriesLength++; + } + ctx.delete(value); + + if (options.sort) { + entries.sort(); + } + + if (entriesLength > inspectOptions.iterableLimit) { + const nmore = entriesLength - inspectOptions.iterableLimit; + entries.push(`... ${nmore} more items`); + } + + const iPrefix = `${options.displayName ? options.displayName + " " : ""}`; + + const initIndentation = `\n${DEFAULT_INDENT.repeat(level + 1)}`; + const entryIndentation = `,\n${DEFAULT_INDENT.repeat(level + 1)}`; + const closingIndentation = `${inspectOptions.trailingComma ? "," : ""}\n${ + DEFAULT_INDENT.repeat(level) + }`; + + let iContent; + if (options.group && entries.length > MIN_GROUP_LENGTH) { + const groups = groupEntries(entries, level, value); + iContent = `${initIndentation}${ + groups.join(entryIndentation) + }${closingIndentation}`; + } else { + iContent = entries.length === 0 ? "" : ` ${entries.join(", ")} `; + if ( + stripColor(iContent).length > LINE_BREAKING_LENGTH || + !inspectOptions.compact + ) { + iContent = `${initIndentation}${ + entries.join(entryIndentation) + }${closingIndentation}`; + } + } + + return `${iPrefix}${options.delims[0]}${iContent}${options.delims[1]}`; + } + + // Ported from Node.js + // Copyright Node.js contributors. All rights reserved. + function groupEntries( + entries, + level, + value, + iterableLimit = 100, + ) { + let totalLength = 0; + let maxLength = 0; + let entriesLength = entries.length; + if (iterableLimit < entriesLength) { + // This makes sure the "... n more items" part is not taken into account. + entriesLength--; + } + const separatorSpace = 2; // Add 1 for the space and 1 for the separator. + const dataLen = new Array(entriesLength); + // Calculate the total length of all output entries and the individual max + // entries length of all output entries. + // IN PROGRESS: Colors are being taken into account. + for (let i = 0; i < entriesLength; i++) { + // Taking colors into account: removing the ANSI color + // codes from the string before measuring its length + const len = stripColor(entries[i]).length; + dataLen[i] = len; + totalLength += len + separatorSpace; + if (maxLength < len) maxLength = len; + } + // Add two to `maxLength` as we add a single whitespace character plus a comma + // in-between two entries. + const actualMax = maxLength + separatorSpace; + // Check if at least three entries fit next to each other and prevent grouping + // of arrays that contains entries of very different length (i.e., if a single + // entry is longer than 1/5 of all other entries combined). Otherwise the + // space in-between small entries would be enormous. + if ( + actualMax * 3 + (level + 1) < LINE_BREAKING_LENGTH && + (totalLength / actualMax > 5 || maxLength <= 6) + ) { + const approxCharHeights = 2.5; + const averageBias = Math.sqrt(actualMax - totalLength / entries.length); + const biasedMax = Math.max(actualMax - 3 - averageBias, 1); + // Dynamically check how many columns seem possible. + const columns = Math.min( + // Ideally a square should be drawn. We expect a character to be about 2.5 + // times as high as wide. This is the area formula to calculate a square + // which contains n rectangles of size `actualMax * approxCharHeights`. + // Divide that by `actualMax` to receive the correct number of columns. + // The added bias increases the columns for short entries. + Math.round( + Math.sqrt(approxCharHeights * biasedMax * entriesLength) / biasedMax, + ), + // Do not exceed the breakLength. + Math.floor((LINE_BREAKING_LENGTH - (level + 1)) / actualMax), + // Limit the columns to a maximum of fifteen. + 15, + ); + // Return with the original output if no grouping should happen. + if (columns <= 1) { + return entries; + } + const tmp = []; + const maxLineLength = []; + for (let i = 0; i < columns; i++) { + let lineMaxLength = 0; + for (let j = i; j < entries.length; j += columns) { + if (dataLen[j] > lineMaxLength) lineMaxLength = dataLen[j]; + } + lineMaxLength += separatorSpace; + maxLineLength[i] = lineMaxLength; + } + let order = "padStart"; + if (value !== undefined) { + for (let i = 0; i < entries.length; i++) { + /* eslint-disable @typescript-eslint/no-explicit-any */ + if ( + typeof value[i] !== "number" && + typeof value[i] !== "bigint" + ) { + order = "padEnd"; + break; + } + /* eslint-enable */ + } + } + // Each iteration creates a single line of grouped entries. + for (let i = 0; i < entriesLength; i += columns) { + // The last lines may contain less entries than columns. + const max = Math.min(i + columns, entriesLength); + let str = ""; + let j = i; + for (; j < max - 1; j++) { + // In future, colors should be taken here into the account + const padding = maxLineLength[j - i]; + str += `${entries[j]}, `[order](padding, " "); + } + if (order === "padStart") { + const padding = maxLineLength[j - i] + + entries[j].length - + dataLen[j] - + separatorSpace; + str += entries[j].padStart(padding, " "); + } else { + str += entries[j]; + } + tmp.push(str); + } + if (iterableLimit < entries.length) { + tmp.push(entries[entriesLength]); + } + entries = tmp; + } + return entries; + } + + function inspectValue( + value, + ctx, + level, + inspectOptions, + ) { + switch (typeof value) { + case "string": + return value; + case "number": // Numbers are yellow + // Special handling of -0 + return yellow(Object.is(value, -0) ? "-0" : `${value}`); + case "boolean": // booleans are yellow + return yellow(String(value)); + case "undefined": // undefined is dim + return dim(String(value)); + case "symbol": // Symbols are green + return green(String(value)); + case "bigint": // Bigints are yellow + return yellow(`${value}n`); + case "function": // Function string is cyan + return cyan(inspectFunction(value, ctx)); + case "object": // null is bold + if (value === null) { + return bold("null"); + } + + if (ctx.has(value)) { + // Circular string is cyan + return cyan("[Circular]"); + } + + return inspectObject(value, ctx, level, inspectOptions); + default: + // Not implemented is red + return red("[Not Implemented]"); + } + } + + // We can match Node's quoting behavior exactly by swapping the double quote and + // single quote in this array. That would give preference to single quotes. + // However, we prefer double quotes as the default. + const QUOTES = ['"', "'", "`"]; + + /** Surround the string in quotes. + * + * The quote symbol is chosen by taking the first of the `QUOTES` array which + * does not occur in the string. If they all occur, settle with `QUOTES[0]`. + * + * Insert a backslash before any occurrence of the chosen quote symbol and + * before any backslash. */ + function quoteString(string) { + const quote = QUOTES.find((c) => !string.includes(c)) ?? QUOTES[0]; + const escapePattern = new RegExp(`(?=[${quote}\\\\])`, "g"); + return `${quote}${string.replace(escapePattern, "\\")}${quote}`; + } + + // Print strings when they are inside of arrays or objects with quotes + function inspectValueWithQuotes( + value, + ctx, + level, + inspectOptions, + ) { + switch (typeof value) { + case "string": + const trunc = value.length > STR_ABBREVIATE_SIZE + ? value.slice(0, STR_ABBREVIATE_SIZE) + "..." + : value; + return green(quoteString(trunc)); // Quoted strings are green + default: + return inspectValue(value, ctx, level, inspectOptions); + } + } + + function inspectArray( + value, + ctx, + level, + inspectOptions, + ) { + const options = { + typeName: "Array", + displayName: "", + delims: ["[", "]"], + entryHandler: (entry, ctx, level, inspectOptions, next) => { + const [index, val] = entry; + let i = index; + if (!value.hasOwnProperty(i)) { + i++; + while (!value.hasOwnProperty(i) && i < value.length) { + next(); + i++; + } + const emptyItems = i - index; + const ending = emptyItems > 1 ? "s" : ""; + return dim(`<${emptyItems} empty item${ending}>`); + } else { + return inspectValueWithQuotes(val, ctx, level, inspectOptions); + } + }, + group: inspectOptions.compact, + sort: false, + }; + return inspectIterable(value, ctx, level, options, inspectOptions); + } + + function inspectTypedArray( + typedArrayName, + value, + ctx, + level, + inspectOptions, + ) { + const valueLength = value.length; + const options = { + typeName: typedArrayName, + displayName: `${typedArrayName}(${valueLength})`, + delims: ["[", "]"], + entryHandler: (entry, ctx, level, inspectOptions) => { + const val = entry[1]; + return inspectValueWithQuotes(val, ctx, level + 1, inspectOptions); + }, + group: inspectOptions.compact, + sort: false, + }; + return inspectIterable(value, ctx, level, options, inspectOptions); + } + + function inspectSet( + value, + ctx, + level, + inspectOptions, + ) { + const options = { + typeName: "Set", + displayName: "Set", + delims: ["{", "}"], + entryHandler: (entry, ctx, level, inspectOptions) => { + const val = entry[1]; + return inspectValueWithQuotes(val, ctx, level + 1, inspectOptions); + }, + group: false, + sort: inspectOptions.sorted, + }; + return inspectIterable(value, ctx, level, options, inspectOptions); + } + + function inspectMap( + value, + ctx, + level, + inspectOptions, + ) { + const options = { + typeName: "Map", + displayName: "Map", + delims: ["{", "}"], + entryHandler: (entry, ctx, level, inspectOptions) => { + const [key, val] = entry; + return `${ + inspectValueWithQuotes( + key, + ctx, + level + 1, + inspectOptions, + ) + } => ${inspectValueWithQuotes(val, ctx, level + 1, inspectOptions)}`; + }, + group: false, + sort: inspectOptions.sorted, + }; + return inspectIterable( + value, + ctx, + level, + options, + inspectOptions, + ); + } + + function inspectWeakSet() { + return `WeakSet { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color + } + + function inspectWeakMap() { + return `WeakMap { ${cyan("[items unknown]")} }`; // as seen in Node, with cyan color + } + + function inspectDate(value) { + // without quotes, ISO format, in magenta like before + return magenta(isInvalidDate(value) ? "Invalid Date" : value.toISOString()); + } + + function inspectRegExp(value) { + return red(value.toString()); // RegExps are red + } + + function inspectStringObject(value) { + return cyan(`[String: "${value.toString()}"]`); // wrappers are in cyan + } + + function inspectBooleanObject(value) { + return cyan(`[Boolean: ${value.toString()}]`); // wrappers are in cyan + } + + function inspectNumberObject(value) { + return cyan(`[Number: ${value.toString()}]`); // wrappers are in cyan + } + + const PromiseState = { + Pending: 0, + Fulfilled: 1, + Rejected: 2, + }; + + function inspectPromise( + value, + ctx, + level, + inspectOptions, + ) { + const [state, result] = Deno.core.getPromiseDetails(value); + + if (state === PromiseState.Pending) { + return `Promise { ${cyan("<pending>")} }`; + } + + const prefix = state === PromiseState.Fulfilled + ? "" + : `${red("<rejected>")} `; + + const str = `${prefix}${ + inspectValueWithQuotes( + result, + ctx, + level + 1, + inspectOptions, + ) + }`; + + if (str.length + PROMISE_STRING_BASE_LENGTH > LINE_BREAKING_LENGTH) { + return `Promise {\n${DEFAULT_INDENT.repeat(level + 1)}${str}\n}`; + } + + return `Promise { ${str} }`; + } + + // TODO: Proxy + + function inspectRawObject( + value, + ctx, + level, + inspectOptions, + ) { + if (level >= inspectOptions.depth) { + return cyan("[Object]"); // wrappers are in cyan + } + ctx.add(value); + + let baseString; + + let shouldShowDisplayName = false; + let displayName = value[ + Symbol.toStringTag + ]; + if (!displayName) { + displayName = getClassInstanceName(value); + } + if ( + displayName && displayName !== "Object" && displayName !== "anonymous" + ) { + shouldShowDisplayName = true; + } + + const entries = []; + const stringKeys = Object.keys(value); + const symbolKeys = Object.getOwnPropertySymbols(value); + if (inspectOptions.sorted) { + stringKeys.sort(); + symbolKeys.sort((s1, s2) => + (s1.description ?? "").localeCompare(s2.description ?? "") + ); + } + + for (const key of stringKeys) { + entries.push( + `${key}: ${ + inspectValueWithQuotes( + value[key], + ctx, + level + 1, + inspectOptions, + ) + }`, + ); + } + for (const key of symbolKeys) { + entries.push( + `${key.toString()}: ${ + inspectValueWithQuotes( + value[key], + ctx, + level + 1, + inspectOptions, + ) + }`, + ); + } + // Making sure color codes are ignored when calculating the total length + const totalLength = entries.length + level + + stripColor(entries.join("")).length; + + ctx.delete(value); + + if (entries.length === 0) { + baseString = "{}"; + } else if (totalLength > LINE_BREAKING_LENGTH || !inspectOptions.compact) { + const entryIndent = DEFAULT_INDENT.repeat(level + 1); + const closingIndent = DEFAULT_INDENT.repeat(level); + baseString = `{\n${entryIndent}${entries.join(`,\n${entryIndent}`)}${ + inspectOptions.trailingComma ? "," : "" + }\n${closingIndent}}`; + } else { + baseString = `{ ${entries.join(", ")} }`; + } + + if (shouldShowDisplayName) { + baseString = `${displayName} ${baseString}`; + } + + return baseString; + } + + function inspectObject( + value, + consoleContext, + level, + inspectOptions, + ) { + if (customInspect in value && typeof value[customInspect] === "function") { + try { + return String(value[customInspect]()); + } catch {} + } + if (value instanceof Error) { + return String(value.stack); + } else if (Array.isArray(value)) { + return inspectArray(value, consoleContext, level, inspectOptions); + } else if (value instanceof Number) { + return inspectNumberObject(value); + } else if (value instanceof Boolean) { + return inspectBooleanObject(value); + } else if (value instanceof String) { + return inspectStringObject(value); + } else if (value instanceof Promise) { + return inspectPromise(value, consoleContext, level, inspectOptions); + } else if (value instanceof RegExp) { + return inspectRegExp(value); + } else if (value instanceof Date) { + return inspectDate(value); + } else if (value instanceof Set) { + return inspectSet(value, consoleContext, level, inspectOptions); + } else if (value instanceof Map) { + return inspectMap(value, consoleContext, level, inspectOptions); + } else if (value instanceof WeakSet) { + return inspectWeakSet(); + } else if (value instanceof WeakMap) { + return inspectWeakMap(); + } else if (isTypedArray(value)) { + return inspectTypedArray( + Object.getPrototypeOf(value).constructor.name, + value, + consoleContext, + level, + inspectOptions, + ); + } else { + // Otherwise, default object formatting + return inspectRawObject(value, consoleContext, level, inspectOptions); + } + } + + function inspectArgs( + args, + inspectOptions = {}, + ) { + const rInspectOptions = { ...DEFAULT_INSPECT_OPTIONS, ...inspectOptions }; + const first = args[0]; + let a = 0; + let str = ""; + let join = ""; + + if (typeof first === "string") { + let tempStr; + 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 = inspectValue( + args[++a], + new Set(), + 0, + rInspectOptions, + ); + 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 += inspectValue(value, new Set(), 0, rInspectOptions); + } + join = " "; + a++; + } + + if (rInspectOptions.indentLevel > 0) { + const groupIndent = DEFAULT_INDENT.repeat(rInspectOptions.indentLevel); + if (str.indexOf("\n") !== -1) { + str = str.replace(/\n/g, `\n${groupIndent}`); + } + str = groupIndent + str; + } + + return str; + } + + const countMap = new Map(); + const timerMap = new Map(); + const isConsoleInstance = Symbol("isConsoleInstance"); + + class Console { + #printFunc = null; + [isConsoleInstance] = false; + + constructor(printFunc) { + this.#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({}); + Object.assign(console, this); + return console; + } + + log = (...args) => { + this.#printFunc( + inspectArgs(args, { + indentLevel: this.indentLevel, + }) + "\n", + false, + ); + }; + + debug = this.log; + info = this.log; + + dir = (obj, options = {}) => { + this.#printFunc(inspectArgs([obj], options) + "\n", false); + }; + + dirxml = this.dir; + + warn = (...args) => { + this.#printFunc( + inspectArgs(args, { + indentLevel: this.indentLevel, + }) + "\n", + true, + ); + }; + + error = this.warn; + + assert = (condition = false, ...args) => { + 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") => { + 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") => { + label = String(label); + + if (countMap.has(label)) { + countMap.set(label, 0); + } else { + this.warn(`Count for '${label}' does not exist`); + } + }; + + table = (data, properties) => { + 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 = {}; + const indexKeys = []; + const values = []; + + const stringifyValue = (value) => + inspectValueWithQuotes(value, new Set(), 0, { + ...DEFAULT_INSPECT_OPTIONS, + depth: 1, + }); + const toTable = (header, body) => this.log(cliTable(header, body)); + const createColumn = (value, shift) => [ + ...(shift ? [...new Array(shift)].map(() => "") : []), + stringifyValue(value), + ]; + + let resultData; + const isSet = data instanceof Set; + const isMap = data instanceof Map; + const valuesKey = "Values"; + const indexKey = isSet || isMap ? "(iter idx)" : "(idx)"; + + if (data instanceof Set) { + resultData = [...data]; + } else if (data instanceof Map) { + let idx = 0; + resultData = {}; + + data.forEach((v, k) => { + resultData[idx] = { Key: k, Values: v }; + idx++; + }); + } else { + resultData = data; + } + + let hasPrimitives = false; + Object.keys(resultData).forEach((k, idx) => { + const value = resultData[k]; + const primitive = value === null || + (typeof value !== "function" && typeof value !== "object"); + if (properties === undefined && primitive) { + hasPrimitives = true; + values.push(stringifyValue(value)); + } else { + const valueObj = value || {}; + const keys = properties || Object.keys(valueObj); + for (const k of keys) { + if (primitive || !valueObj.hasOwnProperty(k)) { + if (objectValues[k]) { + // fill with blanks for idx to avoid misplacing from later values + objectValues[k].push(""); + } + } else { + if (objectValues[k]) { + objectValues[k].push(stringifyValue(valueObj[k])); + } else { + objectValues[k] = createColumn(valueObj[k], idx); + } + } + } + values.push(""); + } + + indexKeys.push(k); + }); + + const headerKeys = Object.keys(objectValues); + const bodyValues = Object.values(objectValues); + const header = [ + indexKey, + ...(properties || + [...headerKeys, !isMap && hasPrimitives && valuesKey]), + ].filter(Boolean); + const body = [indexKeys, ...bodyValues, values]; + + toTable(header, body); + }; + + time = (label = "default") => { + label = String(label); + + if (timerMap.has(label)) { + this.warn(`Timer '${label}' already exists`); + return; + } + + timerMap.set(label, Date.now()); + }; + + timeLog = (label = "default", ...args) => { + label = String(label); + + if (!timerMap.has(label)) { + this.warn(`Timer '${label}' does not exists`); + return; + } + + const startTime = timerMap.get(label); + const duration = Date.now() - startTime; + + this.info(`${label}: ${duration}ms`, ...args); + }; + + timeEnd = (label = "default") => { + label = String(label); + + if (!timerMap.has(label)) { + this.warn(`Timer '${label}' does not exists`); + return; + } + + const startTime = timerMap.get(label); + timerMap.delete(label); + const duration = Date.now() - startTime; + + this.info(`${label}: ${duration}ms`); + }; + + group = (...label) => { + if (label.length > 0) { + this.log(...label); + } + this.indentLevel += 2; + }; + + groupCollapsed = this.group; + + groupEnd = () => { + if (this.indentLevel > 0) { + this.indentLevel -= 2; + } + }; + + clear = () => { + this.indentLevel = 0; + this.#printFunc(CSI.kClear, false); + this.#printFunc(CSI.kClearScreenDown, false); + }; + + trace = (...args) => { + const message = inspectArgs(args, { indentLevel: 0 }); + const err = { + name: "Trace", + message, + }; + Error.captureStackTrace(err, this.trace); + this.error(err.stack); + }; + + static [Symbol.hasInstance](instance) { + return instance[isConsoleInstance]; + } + } + + const customInspect = Symbol("Deno.customInspect"); + + function inspect( + value, + inspectOptions = {}, + ) { + if (typeof value === "string") { + return value; + } else { + return inspectValue(value, new Set(), 0, { + ...DEFAULT_INSPECT_OPTIONS, + ...inspectOptions, + // TODO(nayeemrmn): Indent level is not supported. + indentLevel: 0, + }); + } + } + + // Expose these fields to internalObject for tests. + exposeForTest("Console", Console); + exposeForTest("inspectArgs", inspectArgs); + + window.__bootstrap.console = { + CSI, + inspectArgs, + Console, + customInspect, + inspect, + }; +})(this); diff --git a/cli/rt/03_dom_iterable.js b/cli/rt/03_dom_iterable.js new file mode 100644 index 000000000..cd190b9cd --- /dev/null +++ b/cli/rt/03_dom_iterable.js @@ -0,0 +1,77 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { requiredArguments } = window.__bootstrap.webUtil; + const { exposeForTest } = window.__bootstrap.internals; + + function DomIterableMixin( + Base, + dataSymbol, + ) { + // 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() { + for (const entry of this[dataSymbol]) { + yield entry; + } + } + + *keys() { + for (const [key] of this[dataSymbol]) { + yield key; + } + } + + *values() { + for (const [, value] of this[dataSymbol]) { + yield value; + } + } + + forEach( + callbackfn, + thisArg, + ) { + requiredArguments( + `${this.constructor.name}.forEach`, + arguments.length, + 1, + ); + callbackfn = callbackfn.bind( + thisArg == null ? globalThis : Object(thisArg), + ); + for (const [key, value] of this[dataSymbol]) { + callbackfn(value, key, this); + } + } + + *[Symbol.iterator]() { + for (const entry of this[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; + } + + exposeForTest("DomIterableMixin", DomIterableMixin); + + window.__bootstrap.domIterable = { + DomIterableMixin, + }; +})(this); diff --git a/cli/rt/06_util.js b/cli/rt/06_util.js new file mode 100644 index 000000000..086275bd8 --- /dev/null +++ b/cli/rt/06_util.js @@ -0,0 +1,154 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { build } = window.__bootstrap.build; + const internals = window.__bootstrap.internals; + let logDebug = false; + let logSource = "JS"; + + function setLogDebug(debug, source) { + logDebug = debug; + if (source) { + logSource = source; + } + } + + function log(...args) { + if (logDebug) { + // if we destructure `console` off `globalThis` too early, we don't bind to + // the right console, therefore we don't log anything out. + globalThis.console.log(`DEBUG ${logSource} -`, ...args); + } + } + + class AssertionError extends Error { + constructor(msg) { + super(msg); + this.name = "AssertionError"; + } + } + + function assert(cond, msg = "Assertion failed.") { + if (!cond) { + throw new AssertionError(msg); + } + } + + function createResolvable() { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + promise.resolve = resolve; + promise.reject = reject; + return promise; + } + + function notImplemented() { + throw new Error("not implemented"); + } + + function immutableDefine( + o, + p, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value, + ) { + Object.defineProperty(o, p, { + value, + configurable: false, + writable: false, + }); + } + + function pathFromURLWin32(url) { + const hostname = url.hostname; + const pathname = decodeURIComponent(url.pathname.replace(/\//g, "\\")); + + if (hostname !== "") { + //TODO(actual-size) Node adds a punycode decoding step, we should consider adding this + return `\\\\${hostname}${pathname}`; + } + + const validPath = /^\\(?<driveLetter>[A-Za-z]):\\/; + const matches = validPath.exec(pathname); + + if (!matches?.groups?.driveLetter) { + throw new TypeError("A URL with the file schema must be absolute."); + } + + // we don't want a leading slash on an absolute path in Windows + return pathname.slice(1); + } + + function pathFromURLPosix(url) { + if (url.hostname !== "") { + throw new TypeError(`Host must be empty.`); + } + + return decodeURIComponent(url.pathname); + } + + function pathFromURL(pathOrUrl) { + if (pathOrUrl instanceof URL) { + if (pathOrUrl.protocol != "file:") { + throw new TypeError("Must be a file URL."); + } + + return build.os == "windows" + ? pathFromURLWin32(pathOrUrl) + : pathFromURLPosix(pathOrUrl); + } + return pathOrUrl; + } + + internals.exposeForTest("pathFromURL", pathFromURL); + + function writable(value) { + return { + value, + writable: true, + enumerable: true, + configurable: true, + }; + } + + function nonEnumerable(value) { + return { + value, + writable: true, + configurable: true, + }; + } + + function readOnly(value) { + return { + value, + enumerable: true, + }; + } + + function getterOnly(getter) { + return { + get: getter, + enumerable: true, + }; + } + + window.__bootstrap.util = { + log, + setLogDebug, + notImplemented, + createResolvable, + assert, + AssertionError, + immutableDefine, + pathFromURL, + writable, + nonEnumerable, + readOnly, + getterOnly, + }; +})(this); diff --git a/cli/rt/07_base64.js b/cli/rt/07_base64.js new file mode 100644 index 000000000..7e7f5ca78 --- /dev/null +++ b/cli/rt/07_base64.js @@ -0,0 +1,157 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// Forked from https://github.com/beatgammit/base64-js +// Copyright (c) 2014 Jameson Little. MIT License. + +((window) => { + const lookup = []; + const revLookup = []; + + 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) { + 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 + function byteLength(b64) { + const lens = getLens(b64); + const validLen = lens[0]; + const placeHoldersLen = lens[1]; + return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; + } + + function _byteLength( + b64, + validLen, + placeHoldersLen, + ) { + return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; + } + + function toByteArray(b64) { + 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) { + return ( + lookup[(num >> 18) & 0x3f] + + lookup[(num >> 12) & 0x3f] + + lookup[(num >> 6) & 0x3f] + + lookup[num & 0x3f] + ); + } + + function encodeChunk(uint8, start, end) { + 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(""); + } + + function fromByteArray(uint8) { + 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(""); + } + + window.__base64 = { + byteLength, + toByteArray, + fromByteArray, + }; +})(this); diff --git a/cli/rt/08_text_encoding.js b/cli/rt/08_text_encoding.js new file mode 100644 index 000000000..f12429641 --- /dev/null +++ b/cli/rt/08_text_encoding.js @@ -0,0 +1,686 @@ +// Copyright 2018-2020 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. + +((window) => { + const core = Deno.core; + const base64 = window.__base64; + + const CONTINUE = null; + const END_OF_STREAM = -1; + const FINISHED = -1; + + function decoderError(fatal) { + if (fatal) { + throw new TypeError("Decoder error."); + } + return 0xfffd; // default code point + } + + function inRange(a, min, max) { + return min <= a && a <= max; + } + + function isASCIIByte(a) { + return inRange(a, 0x00, 0x7f); + } + + function stringToCodePoints(input) { + const u = []; + for (const c of input) { + u.push(c.codePointAt(0)); + } + return u; + } + + class UTF8Encoder { + handler(codePoint) { + if (codePoint === END_OF_STREAM) { + return "finished"; + } + + if (inRange(codePoint, 0x00, 0x7f)) { + return [codePoint]; + } + + let count; + let offset; + 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; + } + } + + function atob(s) { + 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)) { + throw new DOMException( + "The string to be decoded is not correctly encoded", + "DataDecodeError", + ); + } + + // base64-js requires length exactly times of 4 + if (rem > 0) { + s = s.padEnd(s.length + (4 - rem), "="); + } + + const byteArray = base64.toByteArray(s); + let result = ""; + for (let i = 0; i < byteArray.length; i++) { + result += String.fromCharCode(byteArray[i]); + } + return result; + } + + function btoa(s) { + const byteArray = []; + for (let i = 0; i < s.length; i++) { + const charCode = s[i].charCodeAt(0); + if (charCode > 0xff) { + throw new TypeError( + "The string to be encoded contains characters " + + "outside of the Latin1 range.", + ); + } + byteArray.push(charCode); + } + const result = base64.fromByteArray(Uint8Array.from(byteArray)); + return result; + } + + class SingleByteDecoder { + #index = []; + #fatal = false; + + constructor( + index, + { ignoreBOM = false, fatal = false } = {}, + ) { + if (ignoreBOM) { + throw new TypeError("Ignoring the BOM is available only with utf-8."); + } + this.#fatal = fatal; + this.#index = index; + } + handler(_stream, byte) { + 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 = { + "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(); + 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(); + + // Single byte decoders are an array of code point lookups + const encodingIndexes = new Map(); + // deno-fmt-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) => { + return new SingleByteDecoder(index, options); + }, + ); + } + + function codePointsToString(codePoints) { + let s = ""; + for (const cp of codePoints) { + s += String.fromCodePoint(cp); + } + return s; + } + + class Stream { + #tokens = []; + constructor(tokens) { + this.#tokens = [...tokens]; + this.#tokens.reverse(); + } + + endOfStream() { + return !this.#tokens.length; + } + + read() { + return !this.#tokens.length ? END_OF_STREAM : this.#tokens.pop(); + } + + prepend(token) { + if (Array.isArray(token)) { + while (token.length) { + this.#tokens.push(token.pop()); + } + } else { + this.#tokens.push(token); + } + } + + push(token) { + if (Array.isArray(token)) { + while (token.length) { + this.#tokens.unshift(token.shift()); + } + } else { + this.#tokens.unshift(token); + } + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function isEitherArrayBuffer(x) { + return x instanceof SharedArrayBuffer || x instanceof ArrayBuffer; + } + + class TextDecoder { + #encoding = ""; + + get encoding() { + return this.#encoding; + } + fatal = false; + ignoreBOM = false; + + constructor(label = "utf-8", options = { 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) && encoding !== "utf-8") { + throw new TypeError(`Internal decoder ('${encoding}') not found.`); + } + this.#encoding = encoding; + } + + decode( + input, + options = { stream: false }, + ) { + if (options.stream) { + throw new TypeError("Stream not supported."); + } + + let bytes; + 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); + } + + // For simple utf-8 decoding "Deno.core.decode" can be used for performance + if ( + this.#encoding === "utf-8" && + this.fatal === false && + this.ignoreBOM === false + ) { + return core.decode(bytes); + } + + // For performance reasons we utilise a highly optimised decoder instead of + // the general decoder. + if (this.#encoding === "utf-8") { + return decodeUtf8(bytes, this.fatal, this.ignoreBOM); + } + + const decoder = decoders.get(this.#encoding)({ + fatal: this.fatal, + ignoreBOM: this.ignoreBOM, + }); + const inputStream = new Stream(bytes); + const output = []; + + 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]() { + return "TextDecoder"; + } + } + + class TextEncoder { + encoding = "utf-8"; + encode(input = "") { + // Deno.core.encode() provides very efficient utf-8 encoding + if (this.encoding === "utf-8") { + return core.encode(input); + } + + const encoder = new UTF8Encoder(); + const inputStream = new Stream(stringToCodePoints(input)); + const output = []; + + while (true) { + const result = encoder.handler(inputStream.read()); + if (result === "finished") { + break; + } + output.push(...result); + } + + return new Uint8Array(output); + } + encodeInto(input, dest) { + 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; + } + if (dest.length - written >= result.length) { + read++; + dest.set(result, written); + written += result.length; + if (result.length > 3) { + // increment read a second time if greater than U+FFFF + read++; + } + } else { + break; + } + } + + return { + read, + written, + }; + } + get [Symbol.toStringTag]() { + return "TextEncoder"; + } + } + + // This function is based on Bjoern Hoehrmann's DFA UTF-8 decoder. + // See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details. + // + // Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de> + // + // Permission is hereby granted, free of charge, to any person obtaining a copy + // of this software and associated documentation files (the "Software"), to deal + // in the Software without restriction, including without limitation the rights + // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + // copies of the Software, and to permit persons to whom the Software is + // furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in + // all copies or substantial portions of the Software. + // + // 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 OR COPYRIGHT HOLDERS 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. + function decodeUtf8( + input, + fatal, + ignoreBOM, + ) { + let outString = ""; + + // Prepare a buffer so that we don't have to do a lot of string concats, which + // are very slow. + const outBufferLength = Math.min(1024, input.length); + const outBuffer = new Uint16Array(outBufferLength); + let outIndex = 0; + + let state = 0; + let codepoint = 0; + let type; + + let i = + ignoreBOM && input[0] === 0xef && input[1] === 0xbb && input[2] === 0xbf + ? 3 + : 0; + + for (; i < input.length; ++i) { + // Encoding error handling + if (state === 12 || (state !== 0 && (input[i] & 0xc0) !== 0x80)) { + if (fatal) { + throw new TypeError( + `Decoder error. Invalid byte in sequence at position ${i} in data.`, + ); + } + outBuffer[outIndex++] = 0xfffd; // Replacement character + if (outIndex === outBufferLength) { + outString += String.fromCharCode.apply(null, outBuffer); + outIndex = 0; + } + state = 0; + } + + // deno-fmt-ignore + type = [ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8 + ][input[i]]; + codepoint = state !== 0 + ? (input[i] & 0x3f) | (codepoint << 6) + : (0xff >> type) & input[i]; + // deno-fmt-ignore + state = [ + 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12, + 12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12, + 12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12, + 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, + 12,36,12,12,12,12,12,12,12,12,12,12 + ][state + type]; + + if (state !== 0) continue; + + // Add codepoint to buffer (as charcodes for utf-16), and flush buffer to + // string if needed. + if (codepoint > 0xffff) { + outBuffer[outIndex++] = 0xd7c0 + (codepoint >> 10); + if (outIndex === outBufferLength) { + outString += String.fromCharCode.apply(null, outBuffer); + outIndex = 0; + } + outBuffer[outIndex++] = 0xdc00 | (codepoint & 0x3ff); + if (outIndex === outBufferLength) { + outString += String.fromCharCode.apply(null, outBuffer); + outIndex = 0; + } + } else { + outBuffer[outIndex++] = codepoint; + if (outIndex === outBufferLength) { + outString += String.fromCharCode.apply(null, outBuffer); + outIndex = 0; + } + } + } + + // Add a replacement character if we ended in the middle of a sequence or + // encountered an invalid code at the end. + if (state !== 0) { + if (fatal) throw new TypeError(`Decoder error. Unexpected end of data.`); + outBuffer[outIndex++] = 0xfffd; // Replacement character + } + + // Final flush of buffer + outString += String.fromCharCode.apply( + null, + outBuffer.subarray(0, outIndex), + ); + + return outString; + } + + window.TextEncoder = TextEncoder; + window.TextDecoder = TextDecoder; + window.atob = atob; + window.btoa = btoa; +})(this); diff --git a/cli/rt/10_dispatch_json.js b/cli/rt/10_dispatch_json.js new file mode 100644 index 000000000..3d19ea62a --- /dev/null +++ b/cli/rt/10_dispatch_json.js @@ -0,0 +1,84 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const util = window.__bootstrap.util; + const getErrorClass = window.__bootstrap.errors.getErrorClass; + // Using an object without a prototype because `Map` was causing GC problems. + const promiseTable = Object.create(null); + let _nextPromiseId = 1; + + function nextPromiseId() { + return _nextPromiseId++; + } + + function decode(ui8) { + return JSON.parse(core.decode(ui8)); + } + + function encode(args) { + return core.encode(JSON.stringify(args)); + } + + function unwrapResponse(res) { + if (res.err != null) { + throw new (getErrorClass(res.err.kind))(res.err.message); + } + util.assert(res.ok != null); + return res.ok; + } + + function asyncMsgFromRust(resUi8) { + const res = decode(resUi8); + util.assert(res.promiseId != null); + + const promise = promiseTable[res.promiseId]; + util.assert(promise != null); + delete promiseTable[res.promiseId]; + promise.resolve(res); + } + + function sendSync( + opName, + args = {}, + ...zeroCopy + ) { + util.log("sendSync", opName); + const argsUi8 = encode(args); + const resUi8 = core.dispatchByName(opName, argsUi8, ...zeroCopy); + util.assert(resUi8 != null); + const res = decode(resUi8); + util.assert(res.promiseId == null); + return unwrapResponse(res); + } + + async function sendAsync( + opName, + args = {}, + ...zeroCopy + ) { + util.log("sendAsync", opName); + const promiseId = nextPromiseId(); + args = Object.assign(args, { promiseId }); + const promise = util.createResolvable(); + const argsUi8 = encode(args); + const buf = core.dispatchByName(opName, argsUi8, ...zeroCopy); + if (buf != null) { + // Sync result. + const res = decode(buf); + promise.resolve(res); + } else { + // Async result. + promiseTable[promiseId] = promise; + } + + const res = await promise; + return unwrapResponse(res); + } + + window.__bootstrap.dispatchJson = { + asyncMsgFromRust, + sendSync, + sendAsync, + }; +})(this); diff --git a/cli/rt/10_dispatch_minimal.js b/cli/rt/10_dispatch_minimal.js new file mode 100644 index 000000000..6137449f4 --- /dev/null +++ b/cli/rt/10_dispatch_minimal.js @@ -0,0 +1,115 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const util = window.__bootstrap.util; + const errorNs = window.__bootstrap.errors; + + // Using an object without a prototype because `Map` was causing GC problems. + const promiseTableMin = Object.create(null); + + // 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; + + const decoder = new TextDecoder(); + + function nextPromiseId() { + return _nextPromiseId++; + } + + function recordFromBufMinimal(ui8) { + const header = ui8.subarray(0, 12); + const buf32 = new Int32Array( + header.buffer, + header.byteOffset, + header.byteLength / 4, + ); + const promiseId = buf32[0]; + const arg = buf32[1]; + const result = buf32[2]; + let err; + + if (arg < 0) { + const kind = result; + const message = decoder.decode(ui8.subarray(12)); + err = { kind, message }; + } else if (ui8.length != 12) { + throw new errorNs.errors.InvalidData("BadMessage"); + } + + return { + promiseId, + arg, + result, + err, + }; + } + + function unwrapResponse(res) { + if (res.err != null) { + throw new (errorNs.getErrorClass(res.err.kind))(res.err.message); + } + return res.result; + } + + const scratch32 = new Int32Array(3); + const scratchBytes = new Uint8Array( + scratch32.buffer, + scratch32.byteOffset, + scratch32.byteLength, + ); + util.assert(scratchBytes.byteLength === scratch32.length * 4); + + function asyncMsgFromRust(ui8) { + const record = recordFromBufMinimal(ui8); + const { promiseId } = record; + const promise = promiseTableMin[promiseId]; + delete promiseTableMin[promiseId]; + util.assert(promise); + promise.resolve(record); + } + + async function sendAsync( + opName, + arg, + zeroCopy, + ) { + const promiseId = nextPromiseId(); // AKA cmdId + scratch32[0] = promiseId; + scratch32[1] = arg; + scratch32[2] = 0; // result + const promise = util.createResolvable(); + const buf = core.dispatchByName(opName, scratchBytes, zeroCopy); + if (buf != null) { + const record = recordFromBufMinimal(buf); + // Sync result. + promise.resolve(record); + } else { + // Async result. + promiseTableMin[promiseId] = promise; + } + + const res = await promise; + return unwrapResponse(res); + } + + function sendSync( + opName, + arg, + zeroCopy, + ) { + scratch32[0] = 0; // promiseId 0 indicates sync + scratch32[1] = arg; + const res = core.dispatchByName(opName, scratchBytes, zeroCopy); + const resRecord = recordFromBufMinimal(res); + return unwrapResponse(resRecord); + } + + window.__bootstrap.dispatchMinimal = { + asyncMsgFromRust, + sendSync, + sendAsync, + }; +})(this); diff --git a/cli/rt/11_crypto.js b/cli/rt/11_crypto.js new file mode 100644 index 000000000..ab4a49200 --- /dev/null +++ b/cli/rt/11_crypto.js @@ -0,0 +1,22 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { sendSync } = window.__bootstrap.dispatchJson; + const { assert } = window.__bootstrap.util; + + function getRandomValues(typedArray) { + 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("op_get_random_values", {}, ui8); + return typedArray; + } + + window.__bootstrap.crypto = { + getRandomValues, + }; +})(this); diff --git a/cli/rt/11_resources.js b/cli/rt/11_resources.js new file mode 100644 index 000000000..247e033cc --- /dev/null +++ b/cli/rt/11_resources.js @@ -0,0 +1,23 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const sendSync = window.__bootstrap.dispatchJson.sendSync; + + function resources() { + const res = sendSync("op_resources"); + const resources = {}; + for (const resourceTuple of res) { + resources[resourceTuple[0]] = resourceTuple[1]; + } + return resources; + } + + function close(rid) { + sendSync("op_close", { rid }); + } + + window.__bootstrap.resources = { + close, + resources, + }; +})(this); diff --git a/cli/rt/11_streams.js b/cli/rt/11_streams.js new file mode 100644 index 000000000..4bdbfbc5c --- /dev/null +++ b/cli/rt/11_streams.js @@ -0,0 +1,3290 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This code closely follows the WHATWG Stream Specification +// See: https://streams.spec.whatwg.org/ +// +// There are some parts that are not fully implemented, and there are some +// comments which point to steps of the specification that are not implemented. + +((window) => { + /* eslint-disable @typescript-eslint/no-explicit-any,require-await */ + + const { AbortSignal } = window.__bootstrap.abortSignal; + const { cloneValue, setFunctionName } = window.__bootstrap.webUtil; + const { assert, AssertionError } = window.__bootstrap.util; + const { customInspect, inspect } = window.__bootstrap.console; + + const sym = { + abortAlgorithm: Symbol("abortAlgorithm"), + abortSteps: Symbol("abortSteps"), + asyncIteratorReader: Symbol("asyncIteratorReader"), + autoAllocateChunkSize: Symbol("autoAllocateChunkSize"), + backpressure: Symbol("backpressure"), + backpressureChangePromise: Symbol("backpressureChangePromise"), + byobRequest: Symbol("byobRequest"), + cancelAlgorithm: Symbol("cancelAlgorithm"), + cancelSteps: Symbol("cancelSteps"), + closeAlgorithm: Symbol("closeAlgorithm"), + closedPromise: Symbol("closedPromise"), + closeRequest: Symbol("closeRequest"), + closeRequested: Symbol("closeRequested"), + controlledReadableByteStream: Symbol( + "controlledReadableByteStream", + ), + controlledReadableStream: Symbol("controlledReadableStream"), + controlledTransformStream: Symbol("controlledTransformStream"), + controlledWritableStream: Symbol("controlledWritableStream"), + disturbed: Symbol("disturbed"), + errorSteps: Symbol("errorSteps"), + flushAlgorithm: Symbol("flushAlgorithm"), + forAuthorCode: Symbol("forAuthorCode"), + inFlightWriteRequest: Symbol("inFlightWriteRequest"), + inFlightCloseRequest: Symbol("inFlightCloseRequest"), + isFakeDetached: Symbol("isFakeDetached"), + ownerReadableStream: Symbol("ownerReadableStream"), + ownerWritableStream: Symbol("ownerWritableStream"), + pendingAbortRequest: Symbol("pendingAbortRequest"), + preventCancel: Symbol("preventCancel"), + pullAgain: Symbol("pullAgain"), + pullAlgorithm: Symbol("pullAlgorithm"), + pulling: Symbol("pulling"), + pullSteps: Symbol("pullSteps"), + queue: Symbol("queue"), + queueTotalSize: Symbol("queueTotalSize"), + readable: Symbol("readable"), + readableStreamController: Symbol("readableStreamController"), + reader: Symbol("reader"), + readRequests: Symbol("readRequests"), + readyPromise: Symbol("readyPromise"), + started: Symbol("started"), + state: Symbol("state"), + storedError: Symbol("storedError"), + strategyHWM: Symbol("strategyHWM"), + strategySizeAlgorithm: Symbol("strategySizeAlgorithm"), + transformAlgorithm: Symbol("transformAlgorithm"), + transformStreamController: Symbol("transformStreamController"), + writableStreamController: Symbol("writableStreamController"), + writeAlgorithm: Symbol("writeAlgorithm"), + writable: Symbol("writable"), + writer: Symbol("writer"), + writeRequests: Symbol("writeRequests"), + }; + class ReadableByteStreamController { + constructor() { + throw new TypeError( + "ReadableByteStreamController's constructor cannot be called.", + ); + } + + get byobRequest() { + return undefined; + } + + get desiredSize() { + if (!isReadableByteStreamController(this)) { + throw new TypeError("Invalid ReadableByteStreamController."); + } + return readableByteStreamControllerGetDesiredSize(this); + } + + close() { + if (!isReadableByteStreamController(this)) { + throw new TypeError("Invalid ReadableByteStreamController."); + } + if (this[sym.closeRequested]) { + throw new TypeError("Closed already requested."); + } + if (this[sym.controlledReadableByteStream][sym.state] !== "readable") { + throw new TypeError( + "ReadableByteStreamController's stream is not in a readable state.", + ); + } + readableByteStreamControllerClose(this); + } + + enqueue(chunk) { + if (!isReadableByteStreamController(this)) { + throw new TypeError("Invalid ReadableByteStreamController."); + } + if (this[sym.closeRequested]) { + throw new TypeError("Closed already requested."); + } + if (this[sym.controlledReadableByteStream][sym.state] !== "readable") { + throw new TypeError( + "ReadableByteStreamController's stream is not in a readable state.", + ); + } + if (!ArrayBuffer.isView(chunk)) { + throw new TypeError( + "You can only enqueue array buffer views when using a ReadableByteStreamController", + ); + } + if (isDetachedBuffer(chunk.buffer)) { + throw new TypeError( + "Cannot enqueue a view onto a detached ArrayBuffer", + ); + } + readableByteStreamControllerEnqueue(this, chunk); + } + + error(error) { + if (!isReadableByteStreamController(this)) { + throw new TypeError("Invalid ReadableByteStreamController."); + } + readableByteStreamControllerError(this, error); + } + + [sym.cancelSteps](reason) { + // 3.11.5.1.1 If this.[[pendingPullIntos]] is not empty, + resetQueue(this); + const result = this[sym.cancelAlgorithm](reason); + readableByteStreamControllerClearAlgorithms(this); + return result; + } + + [sym.pullSteps]() { + const stream = this[sym.controlledReadableByteStream]; + assert(readableStreamHasDefaultReader(stream)); + if (this[sym.queueTotalSize] > 0) { + assert(readableStreamGetNumReadRequests(stream) === 0); + const entry = this[sym.queue].shift(); + assert(entry); + this[sym.queueTotalSize] -= entry.size; + readableByteStreamControllerHandleQueueDrain(this); + const view = new Uint8Array(entry.value, entry.offset, entry.size); + return Promise.resolve( + readableStreamCreateReadResult( + view, + false, + stream[sym.reader][sym.forAuthorCode], + ), + ); + } + // 3.11.5.2.5 If autoAllocateChunkSize is not undefined, + const promise = readableStreamAddReadRequest(stream); + readableByteStreamControllerCallPullIfNeeded(this); + return promise; + } + + [customInspect]() { + return `${this.constructor.name} { byobRequest: ${ + String(this.byobRequest) + }, desiredSize: ${String(this.desiredSize)} }`; + } + } + + class ReadableStreamDefaultController { + constructor() { + throw new TypeError( + "ReadableStreamDefaultController's constructor cannot be called.", + ); + } + + get desiredSize() { + if (!isReadableStreamDefaultController(this)) { + throw new TypeError("Invalid ReadableStreamDefaultController."); + } + return readableStreamDefaultControllerGetDesiredSize(this); + } + + close() { + if (!isReadableStreamDefaultController(this)) { + throw new TypeError("Invalid ReadableStreamDefaultController."); + } + if (!readableStreamDefaultControllerCanCloseOrEnqueue(this)) { + throw new TypeError( + "ReadableStreamDefaultController cannot close or enqueue.", + ); + } + readableStreamDefaultControllerClose(this); + } + + enqueue(chunk) { + if (!isReadableStreamDefaultController(this)) { + throw new TypeError("Invalid ReadableStreamDefaultController."); + } + if (!readableStreamDefaultControllerCanCloseOrEnqueue(this)) { + throw new TypeError("ReadableSteamController cannot enqueue."); + } + return readableStreamDefaultControllerEnqueue(this, chunk); + } + + error(error) { + if (!isReadableStreamDefaultController(this)) { + throw new TypeError("Invalid ReadableStreamDefaultController."); + } + readableStreamDefaultControllerError(this, error); + } + + [sym.cancelSteps](reason) { + resetQueue(this); + const result = this[sym.cancelAlgorithm](reason); + readableStreamDefaultControllerClearAlgorithms(this); + return result; + } + + [sym.pullSteps]() { + const stream = this[sym.controlledReadableStream]; + if (this[sym.queue].length) { + const chunk = dequeueValue(this); + if (this[sym.closeRequested] && this[sym.queue].length === 0) { + readableStreamDefaultControllerClearAlgorithms(this); + readableStreamClose(stream); + } else { + readableStreamDefaultControllerCallPullIfNeeded(this); + } + return Promise.resolve( + readableStreamCreateReadResult( + chunk, + false, + stream[sym.reader][sym.forAuthorCode], + ), + ); + } + const pendingPromise = readableStreamAddReadRequest(stream); + readableStreamDefaultControllerCallPullIfNeeded(this); + return pendingPromise; + } + + [customInspect]() { + return `${this.constructor.name} { desiredSize: ${ + String(this.desiredSize) + } }`; + } + } + + class ReadableStreamDefaultReader { + constructor(stream) { + if (!isReadableStream(stream)) { + throw new TypeError("stream is not a ReadableStream."); + } + if (isReadableStreamLocked(stream)) { + throw new TypeError("stream is locked."); + } + readableStreamReaderGenericInitialize(this, stream); + this[sym.readRequests] = []; + } + + get closed() { + if (!isReadableStreamDefaultReader(this)) { + return Promise.reject( + new TypeError("Invalid ReadableStreamDefaultReader."), + ); + } + return ( + this[sym.closedPromise].promise ?? + Promise.reject(new TypeError("Invalid reader.")) + ); + } + + cancel(reason) { + if (!isReadableStreamDefaultReader(this)) { + return Promise.reject( + new TypeError("Invalid ReadableStreamDefaultReader."), + ); + } + if (!this[sym.ownerReadableStream]) { + return Promise.reject(new TypeError("Invalid reader.")); + } + return readableStreamReaderGenericCancel(this, reason); + } + + read() { + if (!isReadableStreamDefaultReader(this)) { + return Promise.reject( + new TypeError("Invalid ReadableStreamDefaultReader."), + ); + } + if (!this[sym.ownerReadableStream]) { + return Promise.reject(new TypeError("Invalid reader.")); + } + return readableStreamDefaultReaderRead(this); + } + + releaseLock() { + if (!isReadableStreamDefaultReader(this)) { + throw new TypeError("Invalid ReadableStreamDefaultReader."); + } + if (this[sym.ownerReadableStream] === undefined) { + return; + } + if (this[sym.readRequests].length) { + throw new TypeError("Cannot release lock with pending read requests."); + } + readableStreamReaderGenericRelease(this); + } + + [customInspect]() { + return `${this.constructor.name} { closed: Promise }`; + } + } + + const AsyncIteratorPrototype = Object + .getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype); + + const ReadableStreamAsyncIteratorPrototype = Object.setPrototypeOf({ + next() { + if (!isReadableStreamAsyncIterator(this)) { + return Promise.reject( + new TypeError("invalid ReadableStreamAsyncIterator."), + ); + } + const reader = this[sym.asyncIteratorReader]; + if (!reader[sym.ownerReadableStream]) { + return Promise.reject( + new TypeError("reader owner ReadableStream is undefined."), + ); + } + return readableStreamDefaultReaderRead(reader).then((result) => { + assert(typeof result === "object"); + const { done } = result; + assert(typeof done === "boolean"); + if (done) { + readableStreamReaderGenericRelease(reader); + } + const { value } = result; + return readableStreamCreateReadResult(value, done, true); + }); + }, + return( + value, + ) { + if (!isReadableStreamAsyncIterator(this)) { + return Promise.reject( + new TypeError("invalid ReadableStreamAsyncIterator."), + ); + } + const reader = this[sym.asyncIteratorReader]; + if (!reader[sym.ownerReadableStream]) { + return Promise.reject( + new TypeError("reader owner ReadableStream is undefined."), + ); + } + if (reader[sym.readRequests].length) { + return Promise.reject( + new TypeError("reader has outstanding read requests."), + ); + } + if (!this[sym.preventCancel]) { + const result = readableStreamReaderGenericCancel(reader, value); + readableStreamReaderGenericRelease(reader); + return result.then(() => + readableStreamCreateReadResult(value, true, true) + ); + } + readableStreamReaderGenericRelease(reader); + return Promise.resolve( + readableStreamCreateReadResult(value, true, true), + ); + }, + }, AsyncIteratorPrototype); + + class ReadableStream { + constructor( + underlyingSource = {}, + strategy = {}, + ) { + initializeReadableStream(this); + const { size } = strategy; + let { highWaterMark } = strategy; + const { type } = underlyingSource; + + if (isUnderlyingByteSource(underlyingSource)) { + if (size !== undefined) { + throw new RangeError( + `When underlying source is "bytes", strategy.size must be undefined.`, + ); + } + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark ?? 0); + setUpReadableByteStreamControllerFromUnderlyingSource( + this, + underlyingSource, + highWaterMark, + ); + } else if (type === undefined) { + const sizeAlgorithm = makeSizeAlgorithmFromSizeFunction(size); + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark ?? 1); + setUpReadableStreamDefaultControllerFromUnderlyingSource( + this, + underlyingSource, + highWaterMark, + sizeAlgorithm, + ); + } else { + throw new RangeError( + `Valid values for underlyingSource are "bytes" or undefined. Received: "${type}".`, + ); + } + } + + get locked() { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + return isReadableStreamLocked(this); + } + + cancel(reason) { + if (!isReadableStream(this)) { + return Promise.reject(new TypeError("Invalid ReadableStream.")); + } + if (isReadableStreamLocked(this)) { + return Promise.reject( + new TypeError("Cannot cancel a locked ReadableStream."), + ); + } + return readableStreamCancel(this, reason); + } + + getIterator({ + preventCancel, + } = {}) { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + const reader = acquireReadableStreamDefaultReader(this); + const iterator = Object.create(ReadableStreamAsyncIteratorPrototype); + iterator[sym.asyncIteratorReader] = reader; + iterator[sym.preventCancel] = Boolean(preventCancel); + return iterator; + } + + getReader({ mode } = {}) { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + if (mode === undefined) { + return acquireReadableStreamDefaultReader(this, true); + } + mode = String(mode); + // 3.2.5.4.4 If mode is "byob", return ? AcquireReadableStreamBYOBReader(this, true). + throw new RangeError(`Unsupported mode "${mode}"`); + } + + pipeThrough( + { + writable, + readable, + }, + { preventClose, preventAbort, preventCancel, signal } = {}, + ) { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + if (!isWritableStream(writable)) { + throw new TypeError("writable is not a valid WritableStream."); + } + if (!isReadableStream(readable)) { + throw new TypeError("readable is not a valid ReadableStream."); + } + preventClose = Boolean(preventClose); + preventAbort = Boolean(preventAbort); + preventCancel = Boolean(preventCancel); + if (signal && !(signal instanceof AbortSignal)) { + throw new TypeError("Invalid signal."); + } + if (isReadableStreamLocked(this)) { + throw new TypeError("ReadableStream is locked."); + } + if (isWritableStreamLocked(writable)) { + throw new TypeError("writable is locked."); + } + const promise = readableStreamPipeTo( + this, + writable, + preventClose, + preventAbort, + preventCancel, + signal, + ); + setPromiseIsHandledToTrue(promise); + return readable; + } + + pipeTo( + dest, + { preventClose, preventAbort, preventCancel, signal } = {}, + ) { + if (!isReadableStream(this)) { + return Promise.reject(new TypeError("Invalid ReadableStream.")); + } + if (!isWritableStream(dest)) { + return Promise.reject( + new TypeError("dest is not a valid WritableStream."), + ); + } + preventClose = Boolean(preventClose); + preventAbort = Boolean(preventAbort); + preventCancel = Boolean(preventCancel); + if (signal && !(signal instanceof AbortSignal)) { + return Promise.reject(new TypeError("Invalid signal.")); + } + if (isReadableStreamLocked(this)) { + return Promise.reject(new TypeError("ReadableStream is locked.")); + } + if (isWritableStreamLocked(dest)) { + return Promise.reject(new TypeError("dest is locked.")); + } + return readableStreamPipeTo( + this, + dest, + preventClose, + preventAbort, + preventCancel, + signal, + ); + } + + tee() { + if (!isReadableStream(this)) { + throw new TypeError("Invalid ReadableStream."); + } + return readableStreamTee(this, false); + } + + [customInspect]() { + return `${this.constructor.name} { locked: ${String(this.locked)} }`; + } + + [Symbol.asyncIterator]( + options = {}, + ) { + return this.getIterator(options); + } + } + + class TransformStream { + constructor( + transformer = {}, + writableStrategy = {}, + readableStrategy = {}, + ) { + const writableSizeFunction = writableStrategy.size; + let writableHighWaterMark = writableStrategy.highWaterMark; + const readableSizeFunction = readableStrategy.size; + let readableHighWaterMark = readableStrategy.highWaterMark; + const writableType = transformer.writableType; + if (writableType !== undefined) { + throw new RangeError( + `Expected transformer writableType to be undefined, received "${ + String(writableType) + }"`, + ); + } + const writableSizeAlgorithm = makeSizeAlgorithmFromSizeFunction( + writableSizeFunction, + ); + if (writableHighWaterMark === undefined) { + writableHighWaterMark = 1; + } + writableHighWaterMark = validateAndNormalizeHighWaterMark( + writableHighWaterMark, + ); + const readableType = transformer.readableType; + if (readableType !== undefined) { + throw new RangeError( + `Expected transformer readableType to be undefined, received "${ + String(readableType) + }"`, + ); + } + const readableSizeAlgorithm = makeSizeAlgorithmFromSizeFunction( + readableSizeFunction, + ); + if (readableHighWaterMark === undefined) { + readableHighWaterMark = 1; + } + readableHighWaterMark = validateAndNormalizeHighWaterMark( + readableHighWaterMark, + ); + const startPromise = getDeferred(); + initializeTransformStream( + this, + startPromise.promise, + writableHighWaterMark, + writableSizeAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, + ); + // the brand check expects this, and the brand check occurs in the following + // but the property hasn't been defined. + Object.defineProperty(this, sym.transformStreamController, { + value: undefined, + writable: true, + configurable: true, + }); + setUpTransformStreamDefaultControllerFromTransformer(this, transformer); + const startResult = invokeOrNoop( + transformer, + "start", + this[sym.transformStreamController], + ); + startPromise.resolve(startResult); + } + + get readable() { + if (!isTransformStream(this)) { + throw new TypeError("Invalid TransformStream."); + } + return this[sym.readable]; + } + + get writable() { + if (!isTransformStream(this)) { + throw new TypeError("Invalid TransformStream."); + } + return this[sym.writable]; + } + + [customInspect]() { + return `${this.constructor.name} {\n readable: ${ + inspect(this.readable) + }\n writable: ${inspect(this.writable)}\n}`; + } + } + + class TransformStreamDefaultController { + constructor() { + throw new TypeError( + "TransformStreamDefaultController's constructor cannot be called.", + ); + } + + get desiredSize() { + if (!isTransformStreamDefaultController(this)) { + throw new TypeError("Invalid TransformStreamDefaultController."); + } + const readableController = this[sym.controlledTransformStream][ + sym.readable + ][sym.readableStreamController]; + return readableStreamDefaultControllerGetDesiredSize( + readableController, + ); + } + + enqueue(chunk) { + if (!isTransformStreamDefaultController(this)) { + throw new TypeError("Invalid TransformStreamDefaultController."); + } + transformStreamDefaultControllerEnqueue(this, chunk); + } + + error(reason) { + if (!isTransformStreamDefaultController(this)) { + throw new TypeError("Invalid TransformStreamDefaultController."); + } + transformStreamDefaultControllerError(this, reason); + } + + terminate() { + if (!isTransformStreamDefaultController(this)) { + throw new TypeError("Invalid TransformStreamDefaultController."); + } + transformStreamDefaultControllerTerminate(this); + } + + [customInspect]() { + return `${this.constructor.name} { desiredSize: ${ + String(this.desiredSize) + } }`; + } + } + + class WritableStreamDefaultController { + constructor() { + throw new TypeError( + "WritableStreamDefaultController's constructor cannot be called.", + ); + } + + error(e) { + if (!isWritableStreamDefaultController(this)) { + throw new TypeError("Invalid WritableStreamDefaultController."); + } + const state = this[sym.controlledWritableStream][sym.state]; + if (state !== "writable") { + return; + } + writableStreamDefaultControllerError(this, e); + } + + [sym.abortSteps](reason) { + const result = this[sym.abortAlgorithm](reason); + writableStreamDefaultControllerClearAlgorithms(this); + return result; + } + + [sym.errorSteps]() { + resetQueue(this); + } + + [customInspect]() { + return `${this.constructor.name} { }`; + } + } + + class WritableStreamDefaultWriter { + constructor(stream) { + if (!isWritableStream(stream)) { + throw new TypeError("Invalid stream."); + } + if (isWritableStreamLocked(stream)) { + throw new TypeError("Cannot create a writer for a locked stream."); + } + this[sym.ownerWritableStream] = stream; + stream[sym.writer] = this; + const state = stream[sym.state]; + if (state === "writable") { + if ( + !writableStreamCloseQueuedOrInFlight(stream) && + stream[sym.backpressure] + ) { + this[sym.readyPromise] = getDeferred(); + } else { + this[sym.readyPromise] = { promise: Promise.resolve() }; + } + this[sym.closedPromise] = getDeferred(); + } else if (state === "erroring") { + this[sym.readyPromise] = { + promise: Promise.reject(stream[sym.storedError]), + }; + setPromiseIsHandledToTrue(this[sym.readyPromise].promise); + this[sym.closedPromise] = getDeferred(); + } else if (state === "closed") { + this[sym.readyPromise] = { promise: Promise.resolve() }; + this[sym.closedPromise] = { promise: Promise.resolve() }; + } else { + assert(state === "errored"); + const storedError = stream[sym.storedError]; + this[sym.readyPromise] = { promise: Promise.reject(storedError) }; + setPromiseIsHandledToTrue(this[sym.readyPromise].promise); + this[sym.closedPromise] = { promise: Promise.reject(storedError) }; + setPromiseIsHandledToTrue(this[sym.closedPromise].promise); + } + } + + get closed() { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + return this[sym.closedPromise].promise; + } + + get desiredSize() { + if (!isWritableStreamDefaultWriter(this)) { + throw new TypeError("Invalid WritableStreamDefaultWriter."); + } + if (!this[sym.ownerWritableStream]) { + throw new TypeError("WritableStreamDefaultWriter has no owner."); + } + return writableStreamDefaultWriterGetDesiredSize(this); + } + + get ready() { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + return this[sym.readyPromise].promise; + } + + abort(reason) { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + if (!this[sym.ownerWritableStream]) { + Promise.reject( + new TypeError("WritableStreamDefaultWriter has no owner."), + ); + } + return writableStreamDefaultWriterAbort(this, reason); + } + + close() { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + const stream = this[sym.ownerWritableStream]; + if (!stream) { + Promise.reject( + new TypeError("WritableStreamDefaultWriter has no owner."), + ); + } + if (writableStreamCloseQueuedOrInFlight(stream)) { + Promise.reject( + new TypeError("Stream is in an invalid state to be closed."), + ); + } + return writableStreamDefaultWriterClose(this); + } + + releaseLock() { + if (!isWritableStreamDefaultWriter(this)) { + throw new TypeError("Invalid WritableStreamDefaultWriter."); + } + const stream = this[sym.ownerWritableStream]; + if (!stream) { + return; + } + assert(stream[sym.writer]); + writableStreamDefaultWriterRelease(this); + } + + write(chunk) { + if (!isWritableStreamDefaultWriter(this)) { + return Promise.reject( + new TypeError("Invalid WritableStreamDefaultWriter."), + ); + } + if (!this[sym.ownerWritableStream]) { + Promise.reject( + new TypeError("WritableStreamDefaultWriter has no owner."), + ); + } + return writableStreamDefaultWriterWrite(this, chunk); + } + + [customInspect]() { + return `${this.constructor.name} { closed: Promise, desiredSize: ${ + String(this.desiredSize) + }, ready: Promise }`; + } + } + + class WritableStream { + constructor( + underlyingSink = {}, + strategy = {}, + ) { + initializeWritableStream(this); + const size = strategy.size; + let highWaterMark = strategy.highWaterMark ?? 1; + const { type } = underlyingSink; + if (type !== undefined) { + throw new RangeError(`Sink type of "${String(type)}" not supported.`); + } + const sizeAlgorithm = makeSizeAlgorithmFromSizeFunction(size); + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark); + setUpWritableStreamDefaultControllerFromUnderlyingSink( + this, + underlyingSink, + highWaterMark, + sizeAlgorithm, + ); + } + + get locked() { + if (!isWritableStream(this)) { + throw new TypeError("Invalid WritableStream."); + } + return isWritableStreamLocked(this); + } + + abort(reason) { + if (!isWritableStream(this)) { + return Promise.reject(new TypeError("Invalid WritableStream.")); + } + if (isWritableStreamLocked(this)) { + return Promise.reject( + new TypeError("Cannot abort a locked WritableStream."), + ); + } + return writableStreamAbort(this, reason); + } + + close() { + if (!isWritableStream(this)) { + return Promise.reject(new TypeError("Invalid WritableStream.")); + } + if (isWritableStreamLocked(this)) { + return Promise.reject( + new TypeError("Cannot abort a locked WritableStream."), + ); + } + if (writableStreamCloseQueuedOrInFlight(this)) { + return Promise.reject( + new TypeError("Cannot close an already closing WritableStream."), + ); + } + return writableStreamClose(this); + } + + getWriter() { + if (!isWritableStream(this)) { + throw new TypeError("Invalid WritableStream."); + } + return acquireWritableStreamDefaultWriter(this); + } + + [customInspect]() { + return `${this.constructor.name} { locked: ${String(this.locked)} }`; + } + } + + function acquireReadableStreamDefaultReader( + stream, + forAuthorCode = false, + ) { + const reader = new ReadableStreamDefaultReader(stream); + reader[sym.forAuthorCode] = forAuthorCode; + return reader; + } + + function acquireWritableStreamDefaultWriter( + stream, + ) { + return new WritableStreamDefaultWriter(stream); + } + + function call( + fn, + v, + args, + ) { + return Function.prototype.apply.call(fn, v, args); + } + + function createAlgorithmFromUnderlyingMethod( + underlyingObject, + methodName, + algoArgCount, + ...extraArgs + ) { + const method = underlyingObject[methodName]; + if (method) { + if (!isCallable(method)) { + throw new TypeError("method is not callable"); + } + if (algoArgCount === 0) { + return async () => call(method, underlyingObject, extraArgs); + } else { + return async (arg) => { + const fullArgs = [arg, ...extraArgs]; + return call(method, underlyingObject, fullArgs); + }; + } + } + return async () => undefined; + } + + function createReadableStream( + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark = 1, + sizeAlgorithm = () => 1, + ) { + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark); + const stream = Object.create( + ReadableStream.prototype, + ); + initializeReadableStream(stream); + const controller = Object.create( + ReadableStreamDefaultController.prototype, + ); + setUpReadableStreamDefaultController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + sizeAlgorithm, + ); + return stream; + } + + function createWritableStream( + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark = 1, + sizeAlgorithm = () => 1, + ) { + highWaterMark = validateAndNormalizeHighWaterMark(highWaterMark); + const stream = Object.create(WritableStream.prototype); + initializeWritableStream(stream); + const controller = Object.create( + WritableStreamDefaultController.prototype, + ); + setUpWritableStreamDefaultController( + stream, + controller, + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, + ); + return stream; + } + + function dequeueValue(container) { + assert(sym.queue in container && sym.queueTotalSize in container); + assert(container[sym.queue].length); + const pair = container[sym.queue].shift(); + container[sym.queueTotalSize] -= pair.size; + if (container[sym.queueTotalSize] <= 0) { + container[sym.queueTotalSize] = 0; + } + return pair.value; + } + + function enqueueValueWithSize( + container, + value, + size, + ) { + assert(sym.queue in container && sym.queueTotalSize in container); + size = Number(size); + if (!isFiniteNonNegativeNumber(size)) { + throw new RangeError("size must be a finite non-negative number."); + } + container[sym.queue].push({ value, size }); + container[sym.queueTotalSize] += size; + } + + /** Non-spec mechanism to "unwrap" a promise and store it to be resolved + * later. */ + function getDeferred() { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve: resolve, reject: reject }; + } + + function initializeReadableStream( + stream, + ) { + stream[sym.state] = "readable"; + stream[sym.reader] = stream[sym.storedError] = undefined; + stream[sym.disturbed] = false; + } + + function initializeTransformStream( + stream, + startPromise, + writableHighWaterMark, + writableSizeAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, + ) { + const startAlgorithm = () => startPromise; + const writeAlgorithm = (chunk) => + transformStreamDefaultSinkWriteAlgorithm(stream, chunk); + const abortAlgorithm = (reason) => + transformStreamDefaultSinkAbortAlgorithm(stream, reason); + const closeAlgorithm = () => + transformStreamDefaultSinkCloseAlgorithm(stream); + stream[sym.writable] = createWritableStream( + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + writableHighWaterMark, + writableSizeAlgorithm, + ); + const pullAlgorithm = () => + transformStreamDefaultSourcePullAlgorithm(stream); + const cancelAlgorithm = (reason) => { + transformStreamErrorWritableAndUnblockWrite(stream, reason); + return Promise.resolve(undefined); + }; + stream[sym.readable] = createReadableStream( + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + readableHighWaterMark, + readableSizeAlgorithm, + ); + stream[sym.backpressure] = stream[sym.backpressureChangePromise] = + undefined; + transformStreamSetBackpressure(stream, true); + Object.defineProperty(stream, sym.transformStreamController, { + value: undefined, + configurable: true, + }); + } + + function initializeWritableStream( + stream, + ) { + stream[sym.state] = "writable"; + stream[sym.storedError] = stream[sym.writer] = stream[ + sym.writableStreamController + ] = stream[sym.inFlightWriteRequest] = stream[sym.closeRequest] = stream[ + sym.inFlightCloseRequest + ] = stream[sym.pendingAbortRequest] = undefined; + stream[sym.writeRequests] = []; + stream[sym.backpressure] = false; + } + + function invokeOrNoop( + o, + p, + ...args + ) { + assert(o); + const method = o[p]; + if (!method) { + return undefined; + } + return call(method, o, args); + } + + function isCallable(value) { + return typeof value === "function"; + } + + function isDetachedBuffer(value) { + return sym.isFakeDetached in value; + } + + function isFiniteNonNegativeNumber(v) { + return Number.isFinite(v) && (v) >= 0; + } + + function isReadableByteStreamController( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.controlledReadableByteStream in x) + ); + } + + function isReadableStream(x) { + return !( + typeof x !== "object" || + x === null || + !(sym.readableStreamController in x) + ); + } + + function isReadableStreamAsyncIterator( + x, + ) { + if (typeof x !== "object" || x === null) { + return false; + } + return sym.asyncIteratorReader in x; + } + + function isReadableStreamDefaultController( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.controlledReadableStream in x) + ); + } + + function isReadableStreamDefaultReader( + x, + ) { + return !(typeof x !== "object" || x === null || !(sym.readRequests in x)); + } + + function isReadableStreamLocked(stream) { + assert(isReadableStream(stream)); + return !!stream[sym.reader]; + } + + function isReadableStreamDisturbed(stream) { + assert(isReadableStream(stream)); + return !!stream[sym.disturbed]; + } + + function isTransformStream(x) { + return !( + typeof x !== "object" || + x === null || + !(sym.transformStreamController in x) + ); + } + + function isTransformStreamDefaultController( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.controlledTransformStream in x) + ); + } + + function isUnderlyingByteSource( + underlyingSource, + ) { + const { type } = underlyingSource; + const typeString = String(type); + return typeString === "bytes"; + } + + function isWritableStream(x) { + return !( + typeof x !== "object" || + x === null || + !(sym.writableStreamController in x) + ); + } + + function isWritableStreamDefaultController( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.controlledWritableStream in x) + ); + } + + function isWritableStreamDefaultWriter( + x, + ) { + return !( + typeof x !== "object" || + x === null || + !(sym.ownerWritableStream in x) + ); + } + + function isWritableStreamLocked(stream) { + assert(isWritableStream(stream)); + return stream[sym.writer] !== undefined; + } + + function makeSizeAlgorithmFromSizeFunction( + size, + ) { + if (size === undefined) { + return () => 1; + } + if (typeof size !== "function") { + throw new TypeError("size must be callable."); + } + return (chunk) => { + return size.call(undefined, chunk); + }; + } + + function peekQueueValue(container) { + assert(sym.queue in container && sym.queueTotalSize in container); + assert(container[sym.queue].length); + const [pair] = container[sym.queue]; + return pair.value; + } + + function readableByteStreamControllerShouldCallPull( + controller, + ) { + const stream = controller[sym.controlledReadableByteStream]; + if ( + stream[sym.state] !== "readable" || + controller[sym.closeRequested] || + !controller[sym.started] + ) { + return false; + } + if ( + readableStreamHasDefaultReader(stream) && + readableStreamGetNumReadRequests(stream) > 0 + ) { + return true; + } + // 3.13.25.6 If ! ReadableStreamHasBYOBReader(stream) is true and ! + // ReadableStreamGetNumReadIntoRequests(stream) > 0, return true. + const desiredSize = readableByteStreamControllerGetDesiredSize(controller); + assert(desiredSize !== null); + return desiredSize > 0; + } + + function readableByteStreamControllerCallPullIfNeeded( + controller, + ) { + const shouldPull = readableByteStreamControllerShouldCallPull(controller); + if (!shouldPull) { + return; + } + if (controller[sym.pulling]) { + controller[sym.pullAgain] = true; + return; + } + assert(controller[sym.pullAgain] === false); + controller[sym.pulling] = true; + const pullPromise = controller[sym.pullAlgorithm](); + setPromiseIsHandledToTrue( + pullPromise.then( + () => { + controller[sym.pulling] = false; + if (controller[sym.pullAgain]) { + controller[sym.pullAgain] = false; + readableByteStreamControllerCallPullIfNeeded(controller); + } + }, + (e) => { + readableByteStreamControllerError(controller, e); + }, + ), + ); + } + + function readableByteStreamControllerClearAlgorithms( + controller, + ) { + controller[sym.pullAlgorithm] = undefined; + controller[sym.cancelAlgorithm] = undefined; + } + + function readableByteStreamControllerClose( + controller, + ) { + const stream = controller[sym.controlledReadableByteStream]; + if (controller[sym.closeRequested] || stream[sym.state] !== "readable") { + return; + } + if (controller[sym.queueTotalSize] > 0) { + controller[sym.closeRequested] = true; + return; + } + // 3.13.6.4 If controller.[[pendingPullIntos]] is not empty, (BYOB Support) + readableByteStreamControllerClearAlgorithms(controller); + readableStreamClose(stream); + } + + function readableByteStreamControllerEnqueue( + controller, + chunk, + ) { + const stream = controller[sym.controlledReadableByteStream]; + if (controller[sym.closeRequested] || stream[sym.state] !== "readable") { + return; + } + const { buffer, byteOffset, byteLength } = chunk; + const transferredBuffer = transferArrayBuffer(buffer); + if (readableStreamHasDefaultReader(stream)) { + if (readableStreamGetNumReadRequests(stream) === 0) { + readableByteStreamControllerEnqueueChunkToQueue( + controller, + transferredBuffer, + byteOffset, + byteLength, + ); + } else { + assert(controller[sym.queue].length === 0); + const transferredView = new Uint8Array( + transferredBuffer, + byteOffset, + byteLength, + ); + readableStreamFulfillReadRequest(stream, transferredView, false); + } + // 3.13.9.8 Otherwise, if ! ReadableStreamHasBYOBReader(stream) is true + } else { + assert(!isReadableStreamLocked(stream)); + readableByteStreamControllerEnqueueChunkToQueue( + controller, + transferredBuffer, + byteOffset, + byteLength, + ); + } + readableByteStreamControllerCallPullIfNeeded(controller); + } + + function readableByteStreamControllerEnqueueChunkToQueue( + controller, + buffer, + byteOffset, + byteLength, + ) { + controller[sym.queue].push({ + value: buffer, + offset: byteOffset, + size: byteLength, + }); + controller[sym.queueTotalSize] += byteLength; + } + + function readableByteStreamControllerError( + controller, + e, + ) { + const stream = controller[sym.controlledReadableByteStream]; + if (stream[sym.state] !== "readable") { + return; + } + // 3.13.11.3 Perform ! ReadableByteStreamControllerClearPendingPullIntos(controller). + resetQueue(controller); + readableByteStreamControllerClearAlgorithms(controller); + readableStreamError(stream, e); + } + + function readableByteStreamControllerGetDesiredSize( + controller, + ) { + const stream = controller[sym.controlledReadableByteStream]; + const state = stream[sym.state]; + if (state === "errored") { + return null; + } + if (state === "closed") { + return 0; + } + return controller[sym.strategyHWM] - controller[sym.queueTotalSize]; + } + + function readableByteStreamControllerHandleQueueDrain( + controller, + ) { + assert( + controller[sym.controlledReadableByteStream][sym.state] === "readable", + ); + if ( + controller[sym.queueTotalSize] === 0 && controller[sym.closeRequested] + ) { + readableByteStreamControllerClearAlgorithms(controller); + readableStreamClose(controller[sym.controlledReadableByteStream]); + } else { + readableByteStreamControllerCallPullIfNeeded(controller); + } + } + + function readableStreamAddReadRequest( + stream, + ) { + assert(isReadableStreamDefaultReader(stream[sym.reader])); + assert(stream[sym.state] === "readable"); + const promise = getDeferred(); + stream[sym.reader][sym.readRequests].push(promise); + return promise.promise; + } + + function readableStreamCancel( + stream, + reason, + ) { + stream[sym.disturbed] = true; + if (stream[sym.state] === "closed") { + return Promise.resolve(); + } + if (stream[sym.state] === "errored") { + return Promise.reject(stream[sym.storedError]); + } + readableStreamClose(stream); + return stream[sym.readableStreamController][sym.cancelSteps](reason).then( + () => undefined, + ); + } + + function readableStreamClose(stream) { + assert(stream[sym.state] === "readable"); + stream[sym.state] = "closed"; + const reader = stream[sym.reader]; + if (!reader) { + return; + } + if (isReadableStreamDefaultReader(reader)) { + for (const readRequest of reader[sym.readRequests]) { + assert(readRequest.resolve); + readRequest.resolve( + readableStreamCreateReadResult( + undefined, + true, + reader[sym.forAuthorCode], + ), + ); + } + reader[sym.readRequests] = []; + } + const resolve = reader[sym.closedPromise].resolve; + assert(resolve); + resolve(); + } + + function readableStreamCreateReadResult( + value, + done, + forAuthorCode, + ) { + const prototype = forAuthorCode ? Object.prototype : null; + assert(typeof done === "boolean"); + const obj = Object.create(prototype); + Object.defineProperties(obj, { + value: { value, writable: true, enumerable: true, configurable: true }, + done: { + value: done, + writable: true, + enumerable: true, + configurable: true, + }, + }); + return obj; + } + + function readableStreamDefaultControllerCallPullIfNeeded( + controller, + ) { + const shouldPull = readableStreamDefaultControllerShouldCallPull( + controller, + ); + if (!shouldPull) { + return; + } + if (controller[sym.pulling]) { + controller[sym.pullAgain] = true; + return; + } + assert(controller[sym.pullAgain] === false); + controller[sym.pulling] = true; + const pullPromise = controller[sym.pullAlgorithm](); + pullPromise.then( + () => { + controller[sym.pulling] = false; + if (controller[sym.pullAgain]) { + controller[sym.pullAgain] = false; + readableStreamDefaultControllerCallPullIfNeeded(controller); + } + }, + (e) => { + readableStreamDefaultControllerError(controller, e); + }, + ); + } + + function readableStreamDefaultControllerCanCloseOrEnqueue( + controller, + ) { + const state = controller[sym.controlledReadableStream][sym.state]; + return !controller[sym.closeRequested] && state === "readable"; + } + + function readableStreamDefaultControllerClearAlgorithms( + controller, + ) { + controller[sym.pullAlgorithm] = undefined; + controller[sym.cancelAlgorithm] = undefined; + controller[sym.strategySizeAlgorithm] = undefined; + } + + function readableStreamDefaultControllerClose( + controller, + ) { + if (!readableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return; + } + const stream = controller[sym.controlledReadableStream]; + controller[sym.closeRequested] = true; + if (controller[sym.queue].length === 0) { + readableStreamDefaultControllerClearAlgorithms(controller); + readableStreamClose(stream); + } + } + + function readableStreamDefaultControllerEnqueue( + controller, + chunk, + ) { + if (!readableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + return; + } + const stream = controller[sym.controlledReadableStream]; + if ( + isReadableStreamLocked(stream) && + readableStreamGetNumReadRequests(stream) > 0 + ) { + readableStreamFulfillReadRequest(stream, chunk, false); + } else { + try { + const chunkSize = controller[sym.strategySizeAlgorithm](chunk); + enqueueValueWithSize(controller, chunk, chunkSize); + } catch (err) { + readableStreamDefaultControllerError(controller, err); + throw err; + } + } + readableStreamDefaultControllerCallPullIfNeeded(controller); + } + + function readableStreamDefaultControllerGetDesiredSize( + controller, + ) { + const stream = controller[sym.controlledReadableStream]; + const state = stream[sym.state]; + if (state === "errored") { + return null; + } + if (state === "closed") { + return 0; + } + return controller[sym.strategyHWM] - controller[sym.queueTotalSize]; + } + + function readableStreamDefaultControllerError( + controller, + e, + ) { + const stream = controller[sym.controlledReadableStream]; + if (stream[sym.state] !== "readable") { + return; + } + resetQueue(controller); + readableStreamDefaultControllerClearAlgorithms(controller); + readableStreamError(stream, e); + } + + function readableStreamDefaultControllerHasBackpressure( + controller, + ) { + return readableStreamDefaultControllerShouldCallPull(controller); + } + + function readableStreamDefaultControllerShouldCallPull( + controller, + ) { + const stream = controller[sym.controlledReadableStream]; + if ( + !readableStreamDefaultControllerCanCloseOrEnqueue(controller) || + controller[sym.started] === false + ) { + return false; + } + if ( + isReadableStreamLocked(stream) && + readableStreamGetNumReadRequests(stream) > 0 + ) { + return true; + } + const desiredSize = readableStreamDefaultControllerGetDesiredSize( + controller, + ); + assert(desiredSize !== null); + return desiredSize > 0; + } + + function readableStreamDefaultReaderRead( + reader, + ) { + const stream = reader[sym.ownerReadableStream]; + assert(stream); + stream[sym.disturbed] = true; + if (stream[sym.state] === "closed") { + return Promise.resolve( + readableStreamCreateReadResult( + undefined, + true, + reader[sym.forAuthorCode], + ), + ); + } + if (stream[sym.state] === "errored") { + return Promise.reject(stream[sym.storedError]); + } + assert(stream[sym.state] === "readable"); + return (stream[ + sym.readableStreamController + ])[sym.pullSteps](); + } + + function readableStreamError(stream, e) { + assert(isReadableStream(stream)); + assert(stream[sym.state] === "readable"); + stream[sym.state] = "errored"; + stream[sym.storedError] = e; + const reader = stream[sym.reader]; + if (reader === undefined) { + return; + } + if (isReadableStreamDefaultReader(reader)) { + for (const readRequest of reader[sym.readRequests]) { + assert(readRequest.reject); + readRequest.reject(e); + readRequest.reject = undefined; + readRequest.resolve = undefined; + } + reader[sym.readRequests] = []; + } + // 3.5.6.8 Otherwise, support BYOB Reader + reader[sym.closedPromise].reject(e); + reader[sym.closedPromise].reject = undefined; + reader[sym.closedPromise].resolve = undefined; + setPromiseIsHandledToTrue(reader[sym.closedPromise].promise); + } + + function readableStreamFulfillReadRequest( + stream, + chunk, + done, + ) { + const reader = stream[sym.reader]; + const readRequest = reader[sym.readRequests].shift(); + assert(readRequest.resolve); + readRequest.resolve( + readableStreamCreateReadResult(chunk, done, reader[sym.forAuthorCode]), + ); + } + + function readableStreamGetNumReadRequests( + stream, + ) { + return stream[sym.reader]?.[sym.readRequests].length ?? 0; + } + + function readableStreamHasDefaultReader( + stream, + ) { + const reader = stream[sym.reader]; + return !(reader === undefined || !isReadableStreamDefaultReader(reader)); + } + + function readableStreamPipeTo( + source, + dest, + preventClose, + preventAbort, + preventCancel, + signal, + ) { + assert(isReadableStream(source)); + assert(isWritableStream(dest)); + assert( + typeof preventClose === "boolean" && + typeof preventAbort === "boolean" && + typeof preventCancel === "boolean", + ); + assert(signal === undefined || signal instanceof AbortSignal); + assert(!isReadableStreamLocked(source)); + assert(!isWritableStreamLocked(dest)); + const reader = acquireReadableStreamDefaultReader(source); + const writer = acquireWritableStreamDefaultWriter(dest); + source[sym.disturbed] = true; + let shuttingDown = false; + const promise = getDeferred(); + let abortAlgorithm; + if (signal) { + abortAlgorithm = () => { + const error = new DOMException("Abort signal received.", "AbortSignal"); + const actions = []; + if (!preventAbort) { + actions.push(() => { + if (dest[sym.state] === "writable") { + return writableStreamAbort(dest, error); + } else { + return Promise.resolve(undefined); + } + }); + } + if (!preventCancel) { + actions.push(() => { + if (source[sym.state] === "readable") { + return readableStreamCancel(source, error); + } else { + return Promise.resolve(undefined); + } + }); + } + shutdownWithAction( + () => Promise.all(actions.map((action) => action())), + true, + error, + ); + }; + if (signal.aborted) { + abortAlgorithm(); + return promise.promise; + } + signal.addEventListener("abort", abortAlgorithm); + } + + let currentWrite = Promise.resolve(); + + // At this point, the spec becomes non-specific and vague. Most of the rest + // of this code is based on the reference implementation that is part of the + // specification. This is why the functions are only scoped to this function + // to ensure they don't leak into the spec compliant parts. + + function isOrBecomesClosed( + stream, + promise, + action, + ) { + if (stream[sym.state] === "closed") { + action(); + } else { + setPromiseIsHandledToTrue(promise.then(action)); + } + } + + function isOrBecomesErrored( + stream, + promise, + action, + ) { + if (stream[sym.state] === "errored") { + action(stream[sym.storedError]); + } else { + setPromiseIsHandledToTrue(promise.catch((error) => action(error))); + } + } + + function finalize(isError, error) { + writableStreamDefaultWriterRelease(writer); + readableStreamReaderGenericRelease(reader); + + if (signal) { + signal.removeEventListener("abort", abortAlgorithm); + } + if (isError) { + promise.reject(error); + } else { + promise.resolve(); + } + } + + function waitForWritesToFinish() { + const oldCurrentWrite = currentWrite; + return currentWrite.then(() => + oldCurrentWrite !== currentWrite ? waitForWritesToFinish() : undefined + ); + } + + function shutdownWithAction( + action, + originalIsError, + originalError, + ) { + function doTheRest() { + setPromiseIsHandledToTrue( + action().then( + () => finalize(originalIsError, originalError), + (newError) => finalize(true, newError), + ), + ); + } + + if (shuttingDown) { + return; + } + shuttingDown = true; + + if ( + dest[sym.state] === "writable" && + writableStreamCloseQueuedOrInFlight(dest) === false + ) { + setPromiseIsHandledToTrue(waitForWritesToFinish().then(doTheRest)); + } else { + doTheRest(); + } + } + + function shutdown(isError, error) { + if (shuttingDown) { + return; + } + shuttingDown = true; + + if ( + dest[sym.state] === "writable" && + !writableStreamCloseQueuedOrInFlight(dest) + ) { + setPromiseIsHandledToTrue( + waitForWritesToFinish().then(() => finalize(isError, error)), + ); + } + finalize(isError, error); + } + + function pipeStep() { + if (shuttingDown) { + return Promise.resolve(true); + } + return writer[sym.readyPromise].promise.then(() => { + return readableStreamDefaultReaderRead(reader).then( + ({ value, done }) => { + if (done === true) { + return true; + } + currentWrite = writableStreamDefaultWriterWrite( + writer, + value, + ).then(undefined, () => {}); + return false; + }, + ); + }); + } + + function pipeLoop() { + return new Promise((resolveLoop, rejectLoop) => { + function next(done) { + if (done) { + resolveLoop(undefined); + } else { + setPromiseIsHandledToTrue(pipeStep().then(next, rejectLoop)); + } + } + next(false); + }); + } + + isOrBecomesErrored( + source, + reader[sym.closedPromise].promise, + (storedError) => { + if (!preventAbort) { + shutdownWithAction( + () => writableStreamAbort(dest, storedError), + true, + storedError, + ); + } else { + shutdown(true, storedError); + } + }, + ); + + isOrBecomesErrored( + dest, + writer[sym.closedPromise].promise, + (storedError) => { + if (!preventCancel) { + shutdownWithAction( + () => readableStreamCancel(source, storedError), + true, + storedError, + ); + } else { + shutdown(true, storedError); + } + }, + ); + + isOrBecomesClosed(source, reader[sym.closedPromise].promise, () => { + if (!preventClose) { + shutdownWithAction(() => + writableStreamDefaultWriterCloseWithErrorPropagation(writer) + ); + } + }); + + if ( + writableStreamCloseQueuedOrInFlight(dest) || + dest[sym.state] === "closed" + ) { + const destClosed = new TypeError( + "The destination writable stream closed before all data could be piped to it.", + ); + if (!preventCancel) { + shutdownWithAction( + () => readableStreamCancel(source, destClosed), + true, + destClosed, + ); + } else { + shutdown(true, destClosed); + } + } + + setPromiseIsHandledToTrue(pipeLoop()); + return promise.promise; + } + + function readableStreamReaderGenericCancel( + reader, + reason, + ) { + const stream = reader[sym.ownerReadableStream]; + assert(stream); + return readableStreamCancel(stream, reason); + } + + function readableStreamReaderGenericInitialize( + reader, + stream, + ) { + reader[sym.forAuthorCode] = true; + reader[sym.ownerReadableStream] = stream; + stream[sym.reader] = reader; + if (stream[sym.state] === "readable") { + reader[sym.closedPromise] = getDeferred(); + } else if (stream[sym.state] === "closed") { + reader[sym.closedPromise] = { promise: Promise.resolve() }; + } else { + assert(stream[sym.state] === "errored"); + reader[sym.closedPromise] = { + promise: Promise.reject(stream[sym.storedError]), + }; + setPromiseIsHandledToTrue(reader[sym.closedPromise].promise); + } + } + + function readableStreamReaderGenericRelease( + reader, + ) { + assert(reader[sym.ownerReadableStream]); + assert(reader[sym.ownerReadableStream][sym.reader] === reader); + const closedPromise = reader[sym.closedPromise]; + if (reader[sym.ownerReadableStream][sym.state] === "readable") { + assert(closedPromise.reject); + closedPromise.reject(new TypeError("ReadableStream state is readable.")); + } else { + closedPromise.promise = Promise.reject( + new TypeError("Reading is closed."), + ); + delete closedPromise.reject; + delete closedPromise.resolve; + } + setPromiseIsHandledToTrue(closedPromise.promise); + reader[sym.ownerReadableStream][sym.reader] = undefined; + reader[sym.ownerReadableStream] = undefined; + } + + function readableStreamTee( + stream, + cloneForBranch2, + ) { + assert(isReadableStream(stream)); + assert(typeof cloneForBranch2 === "boolean"); + const reader = acquireReadableStreamDefaultReader(stream); + let reading = false; + let canceled1 = false; + let canceled2 = false; + let reason1 = undefined; + let reason2 = undefined; + /* eslint-disable prefer-const */ + let branch1; + let branch2; + /* eslint-enable prefer-const */ + const cancelPromise = getDeferred(); + const pullAlgorithm = () => { + if (reading) { + return Promise.resolve(); + } + reading = true; + const readPromise = readableStreamDefaultReaderRead(reader).then( + (result) => { + reading = false; + assert(typeof result === "object"); + const { done } = result; + assert(typeof done === "boolean"); + if (done) { + if (!canceled1) { + readableStreamDefaultControllerClose( + branch1[ + sym.readableStreamController + ], + ); + } + if (!canceled2) { + readableStreamDefaultControllerClose( + branch2[ + sym.readableStreamController + ], + ); + } + return; + } + const { value } = result; + const value1 = value; + let value2 = value; + if (!canceled2 && cloneForBranch2) { + value2 = cloneValue(value2); + } + if (!canceled1) { + readableStreamDefaultControllerEnqueue( + branch1[ + sym.readableStreamController + ], + value1, + ); + } + if (!canceled2) { + readableStreamDefaultControllerEnqueue( + branch2[ + sym.readableStreamController + ], + value2, + ); + } + }, + ); + setPromiseIsHandledToTrue(readPromise); + return Promise.resolve(); + }; + const cancel1Algorithm = (reason) => { + canceled1 = true; + reason1 = reason; + if (canceled2) { + const compositeReason = [reason1, reason2]; + const cancelResult = readableStreamCancel(stream, compositeReason); + cancelPromise.resolve(cancelResult); + } + return cancelPromise.promise; + }; + const cancel2Algorithm = (reason) => { + canceled2 = true; + reason2 = reason; + if (canceled1) { + const compositeReason = [reason1, reason2]; + const cancelResult = readableStreamCancel(stream, compositeReason); + cancelPromise.resolve(cancelResult); + } + return cancelPromise.promise; + }; + const startAlgorithm = () => undefined; + branch1 = createReadableStream( + startAlgorithm, + pullAlgorithm, + cancel1Algorithm, + ); + branch2 = createReadableStream( + startAlgorithm, + pullAlgorithm, + cancel2Algorithm, + ); + setPromiseIsHandledToTrue( + reader[sym.closedPromise].promise.catch((r) => { + readableStreamDefaultControllerError( + branch1[ + sym.readableStreamController + ], + r, + ); + readableStreamDefaultControllerError( + branch2[ + sym.readableStreamController + ], + r, + ); + }), + ); + return [branch1, branch2]; + } + + function resetQueue(container) { + assert(sym.queue in container && sym.queueTotalSize in container); + container[sym.queue] = []; + container[sym.queueTotalSize] = 0; + } + + /** An internal function which mimics the behavior of setting the promise to + * handled in JavaScript. In this situation, an assertion failure, which + * shouldn't happen will get thrown, instead of swallowed. */ + function setPromiseIsHandledToTrue(promise) { + promise.then(undefined, (e) => { + if (e && e instanceof AssertionError) { + queueMicrotask(() => { + throw e; + }); + } + }); + } + + function setUpReadableByteStreamController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + autoAllocateChunkSize, + ) { + assert(stream[sym.readableStreamController] === undefined); + if (autoAllocateChunkSize !== undefined) { + assert(Number.isInteger(autoAllocateChunkSize)); + assert(autoAllocateChunkSize >= 0); + } + controller[sym.controlledReadableByteStream] = stream; + controller[sym.pulling] = controller[sym.pullAgain] = false; + controller[sym.byobRequest] = undefined; + controller[sym.queue] = []; + controller[sym.queueTotalSize] = 0; + controller[sym.closeRequested] = controller[sym.started] = false; + controller[sym.strategyHWM] = validateAndNormalizeHighWaterMark( + highWaterMark, + ); + controller[sym.pullAlgorithm] = pullAlgorithm; + controller[sym.cancelAlgorithm] = cancelAlgorithm; + controller[sym.autoAllocateChunkSize] = autoAllocateChunkSize; + // 3.13.26.12 Set controller.[[pendingPullIntos]] to a new empty List. + stream[sym.readableStreamController] = controller; + const startResult = startAlgorithm(); + const startPromise = Promise.resolve(startResult); + setPromiseIsHandledToTrue( + startPromise.then( + () => { + controller[sym.started] = true; + assert(!controller[sym.pulling]); + assert(!controller[sym.pullAgain]); + readableByteStreamControllerCallPullIfNeeded(controller); + }, + (r) => { + readableByteStreamControllerError(controller, r); + }, + ), + ); + } + + function setUpReadableByteStreamControllerFromUnderlyingSource( + stream, + underlyingByteSource, + highWaterMark, + ) { + assert(underlyingByteSource); + const controller = Object.create( + ReadableByteStreamController.prototype, + ); + const startAlgorithm = () => { + return invokeOrNoop(underlyingByteSource, "start", controller); + }; + const pullAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingByteSource, + "pull", + 0, + controller, + ); + setFunctionName(pullAlgorithm, "[[pullAlgorithm]]"); + const cancelAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingByteSource, + "cancel", + 1, + ); + setFunctionName(cancelAlgorithm, "[[cancelAlgorithm]]"); + // 3.13.27.6 Let autoAllocateChunkSize be ? GetV(underlyingByteSource, "autoAllocateChunkSize"). + const autoAllocateChunkSize = undefined; + setUpReadableByteStreamController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + autoAllocateChunkSize, + ); + } + + function setUpReadableStreamDefaultController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + sizeAlgorithm, + ) { + assert(stream[sym.readableStreamController] === undefined); + controller[sym.controlledReadableStream] = stream; + controller[sym.queue] = []; + controller[sym.queueTotalSize] = 0; + controller[sym.started] = controller[sym.closeRequested] = controller[ + sym.pullAgain + ] = controller[sym.pulling] = false; + controller[sym.strategySizeAlgorithm] = sizeAlgorithm; + controller[sym.strategyHWM] = highWaterMark; + controller[sym.pullAlgorithm] = pullAlgorithm; + controller[sym.cancelAlgorithm] = cancelAlgorithm; + stream[sym.readableStreamController] = controller; + const startResult = startAlgorithm(); + const startPromise = Promise.resolve(startResult); + setPromiseIsHandledToTrue( + startPromise.then( + () => { + controller[sym.started] = true; + assert(controller[sym.pulling] === false); + assert(controller[sym.pullAgain] === false); + readableStreamDefaultControllerCallPullIfNeeded(controller); + }, + (r) => { + readableStreamDefaultControllerError(controller, r); + }, + ), + ); + } + + function setUpReadableStreamDefaultControllerFromUnderlyingSource( + stream, + underlyingSource, + highWaterMark, + sizeAlgorithm, + ) { + assert(underlyingSource); + const controller = Object.create( + ReadableStreamDefaultController.prototype, + ); + const startAlgorithm = () => + invokeOrNoop(underlyingSource, "start", controller); + const pullAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSource, + "pull", + 0, + controller, + ); + setFunctionName(pullAlgorithm, "[[pullAlgorithm]]"); + const cancelAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSource, + "cancel", + 1, + ); + setFunctionName(cancelAlgorithm, "[[cancelAlgorithm]]"); + setUpReadableStreamDefaultController( + stream, + controller, + startAlgorithm, + pullAlgorithm, + cancelAlgorithm, + highWaterMark, + sizeAlgorithm, + ); + } + + function setUpTransformStreamDefaultController( + stream, + controller, + transformAlgorithm, + flushAlgorithm, + ) { + assert(isTransformStream(stream)); + assert(stream[sym.transformStreamController] === undefined); + controller[sym.controlledTransformStream] = stream; + stream[sym.transformStreamController] = controller; + controller[sym.transformAlgorithm] = transformAlgorithm; + controller[sym.flushAlgorithm] = flushAlgorithm; + } + + function setUpTransformStreamDefaultControllerFromTransformer( + stream, + transformer, + ) { + assert(transformer); + const controller = Object.create( + TransformStreamDefaultController.prototype, + ); + let transformAlgorithm = (chunk) => { + try { + transformStreamDefaultControllerEnqueue( + controller, + // it defaults to no tranformation, so I is assumed to be O + chunk, + ); + } catch (e) { + return Promise.reject(e); + } + return Promise.resolve(); + }; + const transformMethod = transformer.transform; + if (transformMethod) { + if (typeof transformMethod !== "function") { + throw new TypeError("tranformer.transform must be callable."); + } + transformAlgorithm = async (chunk) => + call(transformMethod, transformer, [chunk, controller]); + } + const flushAlgorithm = createAlgorithmFromUnderlyingMethod( + transformer, + "flush", + 0, + controller, + ); + setUpTransformStreamDefaultController( + stream, + controller, + transformAlgorithm, + flushAlgorithm, + ); + } + + function setUpWritableStreamDefaultController( + stream, + controller, + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, + ) { + assert(isWritableStream(stream)); + assert(stream[sym.writableStreamController] === undefined); + controller[sym.controlledWritableStream] = stream; + stream[sym.writableStreamController] = controller; + controller[sym.queue] = []; + controller[sym.queueTotalSize] = 0; + controller[sym.started] = false; + controller[sym.strategySizeAlgorithm] = sizeAlgorithm; + controller[sym.strategyHWM] = highWaterMark; + controller[sym.writeAlgorithm] = writeAlgorithm; + controller[sym.closeAlgorithm] = closeAlgorithm; + controller[sym.abortAlgorithm] = abortAlgorithm; + const backpressure = writableStreamDefaultControllerGetBackpressure( + controller, + ); + writableStreamUpdateBackpressure(stream, backpressure); + const startResult = startAlgorithm(); + const startPromise = Promise.resolve(startResult); + setPromiseIsHandledToTrue( + startPromise.then( + () => { + assert( + stream[sym.state] === "writable" || + stream[sym.state] === "erroring", + ); + controller[sym.started] = true; + writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + }, + (r) => { + assert( + stream[sym.state] === "writable" || + stream[sym.state] === "erroring", + ); + controller[sym.started] = true; + writableStreamDealWithRejection(stream, r); + }, + ), + ); + } + + function setUpWritableStreamDefaultControllerFromUnderlyingSink( + stream, + underlyingSink, + highWaterMark, + sizeAlgorithm, + ) { + assert(underlyingSink); + const controller = Object.create( + WritableStreamDefaultController.prototype, + ); + const startAlgorithm = () => { + return invokeOrNoop(underlyingSink, "start", controller); + }; + const writeAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSink, + "write", + 1, + controller, + ); + setFunctionName(writeAlgorithm, "[[writeAlgorithm]]"); + const closeAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSink, + "close", + 0, + ); + setFunctionName(closeAlgorithm, "[[closeAlgorithm]]"); + const abortAlgorithm = createAlgorithmFromUnderlyingMethod( + underlyingSink, + "abort", + 1, + ); + setFunctionName(abortAlgorithm, "[[abortAlgorithm]]"); + setUpWritableStreamDefaultController( + stream, + controller, + startAlgorithm, + writeAlgorithm, + closeAlgorithm, + abortAlgorithm, + highWaterMark, + sizeAlgorithm, + ); + } + + function transformStreamDefaultControllerClearAlgorithms( + controller, + ) { + controller[sym.transformAlgorithm] = undefined; + controller[sym.flushAlgorithm] = undefined; + } + + function transformStreamDefaultControllerEnqueue( + controller, + chunk, + ) { + const stream = controller[sym.controlledTransformStream]; + const readableController = stream[sym.readable][ + sym.readableStreamController + ]; + if (!readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) { + throw new TypeError( + "TransformStream's readable controller cannot be closed or enqueued.", + ); + } + try { + readableStreamDefaultControllerEnqueue(readableController, chunk); + } catch (e) { + transformStreamErrorWritableAndUnblockWrite(stream, e); + throw stream[sym.readable][sym.storedError]; + } + const backpressure = readableStreamDefaultControllerHasBackpressure( + readableController, + ); + if (backpressure) { + transformStreamSetBackpressure(stream, true); + } + } + + function transformStreamDefaultControllerError( + controller, + e, + ) { + transformStreamError(controller[sym.controlledTransformStream], e); + } + + function transformStreamDefaultControllerPerformTransform( + controller, + chunk, + ) { + const transformPromise = controller[sym.transformAlgorithm](chunk); + return transformPromise.then(undefined, (r) => { + transformStreamError(controller[sym.controlledTransformStream], r); + throw r; + }); + } + + function transformStreamDefaultSinkAbortAlgorithm( + stream, + reason, + ) { + transformStreamError(stream, reason); + return Promise.resolve(undefined); + } + + function transformStreamDefaultSinkCloseAlgorithm( + stream, + ) { + const readable = stream[sym.readable]; + const controller = stream[sym.transformStreamController]; + const flushPromise = controller[sym.flushAlgorithm](); + transformStreamDefaultControllerClearAlgorithms(controller); + return flushPromise.then( + () => { + if (readable[sym.state] === "errored") { + throw readable[sym.storedError]; + } + const readableController = readable[ + sym.readableStreamController + ]; + if ( + readableStreamDefaultControllerCanCloseOrEnqueue(readableController) + ) { + readableStreamDefaultControllerClose(readableController); + } + }, + (r) => { + transformStreamError(stream, r); + throw readable[sym.storedError]; + }, + ); + } + + function transformStreamDefaultSinkWriteAlgorithm( + stream, + chunk, + ) { + assert(stream[sym.writable][sym.state] === "writable"); + const controller = stream[sym.transformStreamController]; + if (stream[sym.backpressure]) { + const backpressureChangePromise = stream[sym.backpressureChangePromise]; + assert(backpressureChangePromise); + return backpressureChangePromise.promise.then(() => { + const writable = stream[sym.writable]; + const state = writable[sym.state]; + if (state === "erroring") { + throw writable[sym.storedError]; + } + assert(state === "writable"); + return transformStreamDefaultControllerPerformTransform( + controller, + chunk, + ); + }); + } + return transformStreamDefaultControllerPerformTransform(controller, chunk); + } + + function transformStreamDefaultSourcePullAlgorithm( + stream, + ) { + assert(stream[sym.backpressure] === true); + assert(stream[sym.backpressureChangePromise] !== undefined); + transformStreamSetBackpressure(stream, false); + return stream[sym.backpressureChangePromise].promise; + } + + function transformStreamError( + stream, + e, + ) { + readableStreamDefaultControllerError( + stream[sym.readable][ + sym.readableStreamController + ], + e, + ); + transformStreamErrorWritableAndUnblockWrite(stream, e); + } + + function transformStreamDefaultControllerTerminate( + controller, + ) { + const stream = controller[sym.controlledTransformStream]; + const readableController = stream[sym.readable][ + sym.readableStreamController + ]; + readableStreamDefaultControllerClose(readableController); + const error = new TypeError("TransformStream is closed."); + transformStreamErrorWritableAndUnblockWrite(stream, error); + } + + function transformStreamErrorWritableAndUnblockWrite( + stream, + e, + ) { + transformStreamDefaultControllerClearAlgorithms( + stream[sym.transformStreamController], + ); + writableStreamDefaultControllerErrorIfNeeded( + stream[sym.writable][sym.writableStreamController], + e, + ); + if (stream[sym.backpressure]) { + transformStreamSetBackpressure(stream, false); + } + } + + function transformStreamSetBackpressure( + stream, + backpressure, + ) { + assert(stream[sym.backpressure] !== backpressure); + if (stream[sym.backpressureChangePromise] !== undefined) { + stream[sym.backpressureChangePromise].resolve(undefined); + } + stream[sym.backpressureChangePromise] = getDeferred(); + stream[sym.backpressure] = backpressure; + } + + function transferArrayBuffer(buffer) { + assert(!isDetachedBuffer(buffer)); + const transferredIshVersion = buffer.slice(0); + + Object.defineProperty(buffer, "byteLength", { + get() { + return 0; + }, + }); + buffer[sym.isFakeDetached] = true; + + return transferredIshVersion; + } + + function validateAndNormalizeHighWaterMark( + highWaterMark, + ) { + highWaterMark = Number(highWaterMark); + if (Number.isNaN(highWaterMark) || highWaterMark < 0) { + throw new RangeError( + `highWaterMark must be a positive number or Infinity. Received: ${highWaterMark}.`, + ); + } + return highWaterMark; + } + + function writableStreamAbort( + stream, + reason, + ) { + const state = stream[sym.state]; + if (state === "closed" || state === "errored") { + return Promise.resolve(undefined); + } + if (stream[sym.pendingAbortRequest]) { + return stream[sym.pendingAbortRequest].promise.promise; + } + assert(state === "writable" || state === "erroring"); + let wasAlreadyErroring = false; + if (state === "erroring") { + wasAlreadyErroring = true; + reason = undefined; + } + const promise = getDeferred(); + stream[sym.pendingAbortRequest] = { promise, reason, wasAlreadyErroring }; + + if (wasAlreadyErroring === false) { + writableStreamStartErroring(stream, reason); + } + return promise.promise; + } + + function writableStreamAddWriteRequest( + stream, + ) { + assert(isWritableStream(stream)); + assert(stream[sym.state] === "writable"); + const promise = getDeferred(); + stream[sym.writeRequests].push(promise); + return promise.promise; + } + + function writableStreamClose( + stream, + ) { + const state = stream[sym.state]; + if (state === "closed" || state === "errored") { + return Promise.reject( + new TypeError( + "Cannot close an already closed or errored WritableStream.", + ), + ); + } + assert(!writableStreamCloseQueuedOrInFlight(stream)); + const promise = getDeferred(); + stream[sym.closeRequest] = promise; + const writer = stream[sym.writer]; + if (writer && stream[sym.backpressure] && state === "writable") { + writer[sym.readyPromise].resolve(); + writer[sym.readyPromise].resolve = undefined; + writer[sym.readyPromise].reject = undefined; + } + writableStreamDefaultControllerClose(stream[sym.writableStreamController]); + return promise.promise; + } + + function writableStreamCloseQueuedOrInFlight( + stream, + ) { + return !( + stream[sym.closeRequest] === undefined && + stream[sym.inFlightCloseRequest] === undefined + ); + } + + function writableStreamDealWithRejection( + stream, + error, + ) { + const state = stream[sym.state]; + if (state === "writable") { + writableStreamStartErroring(stream, error); + return; + } + assert(state === "erroring"); + writableStreamFinishErroring(stream); + } + + function writableStreamDefaultControllerAdvanceQueueIfNeeded( + controller, + ) { + const stream = controller[sym.controlledWritableStream]; + if (!controller[sym.started]) { + return; + } + if (stream[sym.inFlightWriteRequest]) { + return; + } + const state = stream[sym.state]; + assert(state !== "closed" && state !== "errored"); + if (state === "erroring") { + writableStreamFinishErroring(stream); + return; + } + if (!controller[sym.queue].length) { + return; + } + const writeRecord = peekQueueValue(controller); + if (writeRecord === "close") { + writableStreamDefaultControllerProcessClose(controller); + } else { + writableStreamDefaultControllerProcessWrite( + controller, + writeRecord.chunk, + ); + } + } + + function writableStreamDefaultControllerClearAlgorithms( + controller, + ) { + controller[sym.writeAlgorithm] = undefined; + controller[sym.closeAlgorithm] = undefined; + controller[sym.abortAlgorithm] = undefined; + controller[sym.strategySizeAlgorithm] = undefined; + } + + function writableStreamDefaultControllerClose( + controller, + ) { + enqueueValueWithSize(controller, "close", 0); + writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + } + + function writableStreamDefaultControllerError( + controller, + error, + ) { + const stream = controller[sym.controlledWritableStream]; + assert(stream[sym.state] === "writable"); + writableStreamDefaultControllerClearAlgorithms(controller); + writableStreamStartErroring(stream, error); + } + + function writableStreamDefaultControllerErrorIfNeeded( + controller, + error, + ) { + if (controller[sym.controlledWritableStream][sym.state] === "writable") { + writableStreamDefaultControllerError(controller, error); + } + } + + function writableStreamDefaultControllerGetBackpressure( + controller, + ) { + const desiredSize = writableStreamDefaultControllerGetDesiredSize( + controller, + ); + return desiredSize <= 0; + } + + function writableStreamDefaultControllerGetChunkSize( + controller, + chunk, + ) { + let returnValue; + try { + returnValue = controller[sym.strategySizeAlgorithm](chunk); + } catch (e) { + writableStreamDefaultControllerErrorIfNeeded(controller, e); + return 1; + } + return returnValue; + } + + function writableStreamDefaultControllerGetDesiredSize( + controller, + ) { + return controller[sym.strategyHWM] - controller[sym.queueTotalSize]; + } + + function writableStreamDefaultControllerProcessClose( + controller, + ) { + const stream = controller[sym.controlledWritableStream]; + writableStreamMarkCloseRequestInFlight(stream); + dequeueValue(controller); + assert(controller[sym.queue].length === 0); + const sinkClosePromise = controller[sym.closeAlgorithm](); + writableStreamDefaultControllerClearAlgorithms(controller); + setPromiseIsHandledToTrue( + sinkClosePromise.then( + () => { + writableStreamFinishInFlightClose(stream); + }, + (reason) => { + writableStreamFinishInFlightCloseWithError(stream, reason); + }, + ), + ); + } + + function writableStreamDefaultControllerProcessWrite( + controller, + chunk, + ) { + const stream = controller[sym.controlledWritableStream]; + writableStreamMarkFirstWriteRequestInFlight(stream); + const sinkWritePromise = controller[sym.writeAlgorithm](chunk); + setPromiseIsHandledToTrue( + sinkWritePromise.then( + () => { + writableStreamFinishInFlightWrite(stream); + const state = stream[sym.state]; + assert(state === "writable" || state === "erroring"); + dequeueValue(controller); + if ( + !writableStreamCloseQueuedOrInFlight(stream) && + state === "writable" + ) { + const backpressure = writableStreamDefaultControllerGetBackpressure( + controller, + ); + writableStreamUpdateBackpressure(stream, backpressure); + } + writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + }, + (reason) => { + if (stream[sym.state] === "writable") { + writableStreamDefaultControllerClearAlgorithms(controller); + } + writableStreamFinishInFlightWriteWithError(stream, reason); + }, + ), + ); + } + + function writableStreamDefaultControllerWrite( + controller, + chunk, + chunkSize, + ) { + const writeRecord = { chunk }; + try { + enqueueValueWithSize(controller, writeRecord, chunkSize); + } catch (e) { + writableStreamDefaultControllerErrorIfNeeded(controller, e); + return; + } + const stream = controller[sym.controlledWritableStream]; + if ( + !writableStreamCloseQueuedOrInFlight(stream) && + stream[sym.state] === "writable" + ) { + const backpressure = writableStreamDefaultControllerGetBackpressure( + controller, + ); + writableStreamUpdateBackpressure(stream, backpressure); + } + writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); + } + + function writableStreamDefaultWriterAbort( + writer, + reason, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + return writableStreamAbort(stream, reason); + } + + function writableStreamDefaultWriterClose( + writer, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + return writableStreamClose(stream); + } + + function writableStreamDefaultWriterCloseWithErrorPropagation( + writer, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + const state = stream[sym.state]; + if (writableStreamCloseQueuedOrInFlight(stream) || state === "closed") { + return Promise.resolve(); + } + if (state === "errored") { + return Promise.reject(stream[sym.storedError]); + } + assert(state === "writable" || state === "erroring"); + return writableStreamDefaultWriterClose(writer); + } + + function writableStreamDefaultWriterEnsureClosePromiseRejected( + writer, + error, + ) { + if (writer[sym.closedPromise].reject) { + writer[sym.closedPromise].reject(error); + } else { + writer[sym.closedPromise] = { + promise: Promise.reject(error), + }; + } + setPromiseIsHandledToTrue(writer[sym.closedPromise].promise); + } + + function writableStreamDefaultWriterEnsureReadyPromiseRejected( + writer, + error, + ) { + if (writer[sym.readyPromise].reject) { + writer[sym.readyPromise].reject(error); + writer[sym.readyPromise].reject = undefined; + writer[sym.readyPromise].resolve = undefined; + } else { + writer[sym.readyPromise] = { + promise: Promise.reject(error), + }; + } + setPromiseIsHandledToTrue(writer[sym.readyPromise].promise); + } + + function writableStreamDefaultWriterWrite( + writer, + chunk, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + const controller = stream[sym.writableStreamController]; + assert(controller); + const chunkSize = writableStreamDefaultControllerGetChunkSize( + controller, + chunk, + ); + if (stream !== writer[sym.ownerWritableStream]) { + return Promise.reject("Writer has incorrect WritableStream."); + } + const state = stream[sym.state]; + if (state === "errored") { + return Promise.reject(stream[sym.storedError]); + } + if (writableStreamCloseQueuedOrInFlight(stream) || state === "closed") { + return Promise.reject(new TypeError("The stream is closed or closing.")); + } + if (state === "erroring") { + return Promise.reject(stream[sym.storedError]); + } + assert(state === "writable"); + const promise = writableStreamAddWriteRequest(stream); + writableStreamDefaultControllerWrite(controller, chunk, chunkSize); + return promise; + } + + function writableStreamDefaultWriterGetDesiredSize( + writer, + ) { + const stream = writer[sym.ownerWritableStream]; + const state = stream[sym.state]; + if (state === "errored" || state === "erroring") { + return null; + } + if (state === "closed") { + return 0; + } + return writableStreamDefaultControllerGetDesiredSize( + stream[sym.writableStreamController], + ); + } + + function writableStreamDefaultWriterRelease( + writer, + ) { + const stream = writer[sym.ownerWritableStream]; + assert(stream); + assert(stream[sym.writer] === writer); + const releasedError = new TypeError( + "Writer was released and can no longer be used to monitor the stream's closedness.", + ); + writableStreamDefaultWriterEnsureReadyPromiseRejected( + writer, + releasedError, + ); + writableStreamDefaultWriterEnsureClosePromiseRejected( + writer, + releasedError, + ); + stream[sym.writer] = undefined; + writer[sym.ownerWritableStream] = undefined; + } + + function writableStreamFinishErroring(stream) { + assert(stream[sym.state] === "erroring"); + assert(!writableStreamHasOperationMarkedInFlight(stream)); + stream[sym.state] = "errored"; + stream[sym.writableStreamController][sym.errorSteps](); + const storedError = stream[sym.storedError]; + for (const writeRequest of stream[sym.writeRequests]) { + assert(writeRequest.reject); + writeRequest.reject(storedError); + } + stream[sym.writeRequests] = []; + if (!stream[sym.pendingAbortRequest]) { + writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + const abortRequest = stream[sym.pendingAbortRequest]; + assert(abortRequest); + stream[sym.pendingAbortRequest] = undefined; + if (abortRequest.wasAlreadyErroring) { + assert(abortRequest.promise.reject); + abortRequest.promise.reject(storedError); + writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + return; + } + const promise = stream[sym.writableStreamController][sym.abortSteps]( + abortRequest.reason, + ); + setPromiseIsHandledToTrue( + promise.then( + () => { + assert(abortRequest.promise.resolve); + abortRequest.promise.resolve(); + writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + }, + (reason) => { + assert(abortRequest.promise.reject); + abortRequest.promise.reject(reason); + writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); + }, + ), + ); + } + + function writableStreamFinishInFlightClose( + stream, + ) { + assert(stream[sym.inFlightCloseRequest]); + stream[sym.inFlightCloseRequest]?.resolve(); + stream[sym.inFlightCloseRequest] = undefined; + const state = stream[sym.state]; + assert(state === "writable" || state === "erroring"); + if (state === "erroring") { + stream[sym.storedError] = undefined; + if (stream[sym.pendingAbortRequest]) { + stream[sym.pendingAbortRequest].promise.resolve(); + stream[sym.pendingAbortRequest] = undefined; + } + } + stream[sym.state] = "closed"; + const writer = stream[sym.writer]; + if (writer) { + writer[sym.closedPromise].resolve(); + } + assert(stream[sym.pendingAbortRequest] === undefined); + assert(stream[sym.storedError] === undefined); + } + + function writableStreamFinishInFlightCloseWithError( + stream, + error, + ) { + assert(stream[sym.inFlightCloseRequest]); + stream[sym.inFlightCloseRequest]?.reject(error); + stream[sym.inFlightCloseRequest] = undefined; + assert( + stream[sym.state] === "writable" || stream[sym.state] === "erroring", + ); + if (stream[sym.pendingAbortRequest]) { + stream[sym.pendingAbortRequest]?.promise.reject(error); + stream[sym.pendingAbortRequest] = undefined; + } + writableStreamDealWithRejection(stream, error); + } + + function writableStreamFinishInFlightWrite( + stream, + ) { + assert(stream[sym.inFlightWriteRequest]); + stream[sym.inFlightWriteRequest].resolve(); + stream[sym.inFlightWriteRequest] = undefined; + } + + function writableStreamFinishInFlightWriteWithError( + stream, + error, + ) { + assert(stream[sym.inFlightWriteRequest]); + stream[sym.inFlightWriteRequest].reject(error); + stream[sym.inFlightWriteRequest] = undefined; + assert( + stream[sym.state] === "writable" || stream[sym.state] === "erroring", + ); + writableStreamDealWithRejection(stream, error); + } + + function writableStreamHasOperationMarkedInFlight( + stream, + ) { + return !( + stream[sym.inFlightWriteRequest] === undefined && + stream[sym.inFlightCloseRequest] === undefined + ); + } + + function writableStreamMarkCloseRequestInFlight( + stream, + ) { + assert(stream[sym.inFlightCloseRequest] === undefined); + assert(stream[sym.closeRequest] !== undefined); + stream[sym.inFlightCloseRequest] = stream[sym.closeRequest]; + stream[sym.closeRequest] = undefined; + } + + function writableStreamMarkFirstWriteRequestInFlight( + stream, + ) { + assert(stream[sym.inFlightWriteRequest] === undefined); + assert(stream[sym.writeRequests].length); + const writeRequest = stream[sym.writeRequests].shift(); + stream[sym.inFlightWriteRequest] = writeRequest; + } + + function writableStreamRejectCloseAndClosedPromiseIfNeeded( + stream, + ) { + assert(stream[sym.state] === "errored"); + if (stream[sym.closeRequest]) { + assert(stream[sym.inFlightCloseRequest] === undefined); + stream[sym.closeRequest].reject(stream[sym.storedError]); + stream[sym.closeRequest] = undefined; + } + const writer = stream[sym.writer]; + if (writer) { + writer[sym.closedPromise].reject(stream[sym.storedError]); + setPromiseIsHandledToTrue(writer[sym.closedPromise].promise); + } + } + + function writableStreamStartErroring( + stream, + reason, + ) { + assert(stream[sym.storedError] === undefined); + assert(stream[sym.state] === "writable"); + const controller = stream[sym.writableStreamController]; + assert(controller); + stream[sym.state] = "erroring"; + stream[sym.storedError] = reason; + const writer = stream[sym.writer]; + if (writer) { + writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); + } + if ( + !writableStreamHasOperationMarkedInFlight(stream) && + controller[sym.started] + ) { + writableStreamFinishErroring(stream); + } + } + + function writableStreamUpdateBackpressure( + stream, + backpressure, + ) { + assert(stream[sym.state] === "writable"); + assert(!writableStreamCloseQueuedOrInFlight(stream)); + const writer = stream[sym.writer]; + if (writer && backpressure !== stream[sym.backpressure]) { + if (backpressure) { + writer[sym.readyPromise] = getDeferred(); + } else { + assert(backpressure === false); + writer[sym.readyPromise].resolve(); + writer[sym.readyPromise].resolve = undefined; + writer[sym.readyPromise].reject = undefined; + } + } + stream[sym.backpressure] = backpressure; + } + /* eslint-enable */ + + window.__bootstrap.streams = { + ReadableStream, + TransformStream, + WritableStream, + isReadableStreamDisturbed, + }; +})(this); diff --git a/cli/rt/11_timers.js b/cli/rt/11_timers.js new file mode 100644 index 000000000..519a2f461 --- /dev/null +++ b/cli/rt/11_timers.js @@ -0,0 +1,544 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const assert = window.__bootstrap.util.assert; + const dispatchJson = window.__bootstrap.dispatchJson; + + function opStopGlobalTimer() { + dispatchJson.sendSync("op_global_timer_stop"); + } + + async function opStartGlobalTimer(timeout) { + await dispatchJson.sendAsync("op_global_timer", { timeout }); + } + + function opNow() { + return dispatchJson.sendSync("op_now"); + } + + // Derived from https://github.com/vadimg/js_bintrees. MIT Licensed. + + class RBNode { + constructor(data) { + this.data = data; + this.left = null; + this.right = null; + this.red = true; + } + + getChild(dir) { + return dir ? this.right : this.left; + } + + setChild(dir, val) { + if (dir) { + this.right = val; + } else { + this.left = val; + } + } + } + + class RBTree { + #comparator = null; + #root = null; + + constructor(comparator) { + this.#comparator = comparator; + this.#root = null; + } + + /** Returns `null` if tree is empty. */ + min() { + let res = this.#root; + if (res === null) { + return null; + } + while (res.left !== null) { + res = res.left; + } + return res.data; + } + + /** Returns node `data` if found, `null` otherwise. */ + find(data) { + let res = this.#root; + while (res !== null) { + const c = this.#comparator(data, res.data); + if (c === 0) { + return res.data; + } else { + res = res.getChild(c > 0); + } + } + return null; + } + + /** returns `true` if inserted, `false` if duplicate. */ + insert(data) { + let ret = false; + + if (this.#root === null) { + // empty tree + this.#root = new RBNode(data); + ret = true; + } else { + const head = new RBNode(null); // fake tree root + + let dir = 0; + let last = 0; + + // setup + let gp = null; // grandparent + let ggp = head; // grand-grand-parent + let p = null; // parent + let node = this.#root; + ggp.right = this.#root; + + // search down + while (true) { + if (node === null) { + // insert new node at the bottom + node = new RBNode(data); + p.setChild(dir, node); + ret = true; + } else if (isRed(node.left) && isRed(node.right)) { + // color flip + node.red = true; + node.left.red = false; + node.right.red = false; + } + + // fix red violation + if (isRed(node) && isRed(p)) { + const dir2 = ggp.right === gp; + + assert(gp); + if (node === p.getChild(last)) { + ggp.setChild(dir2, singleRotate(gp, !last)); + } else { + ggp.setChild(dir2, doubleRotate(gp, !last)); + } + } + + const cmp = this.#comparator(node.data, data); + + // stop if found + if (cmp === 0) { + break; + } + + last = dir; + dir = Number(cmp < 0); // Fix type + + // update helpers + if (gp !== null) { + ggp = gp; + } + gp = p; + p = node; + node = node.getChild(dir); + } + + // update root + this.#root = head.right; + } + + // make root black + this.#root.red = false; + + return ret; + } + + /** Returns `true` if removed, `false` if not found. */ + remove(data) { + if (this.#root === null) { + return false; + } + + const head = new RBNode(null); // fake tree root + let node = head; + node.right = this.#root; + let p = null; // parent + let gp = null; // grand parent + let found = null; // found item + let dir = 1; + + while (node.getChild(dir) !== null) { + const last = dir; + + // update helpers + gp = p; + p = node; + node = node.getChild(dir); + + const cmp = this.#comparator(data, node.data); + + dir = cmp > 0; + + // save found node + if (cmp === 0) { + found = node; + } + + // push the red node down + if (!isRed(node) && !isRed(node.getChild(dir))) { + if (isRed(node.getChild(!dir))) { + const sr = singleRotate(node, dir); + p.setChild(last, sr); + p = sr; + } else if (!isRed(node.getChild(!dir))) { + const sibling = p.getChild(!last); + if (sibling !== null) { + if ( + !isRed(sibling.getChild(!last)) && + !isRed(sibling.getChild(last)) + ) { + // color flip + p.red = false; + sibling.red = true; + node.red = true; + } else { + assert(gp); + const dir2 = gp.right === p; + + if (isRed(sibling.getChild(last))) { + gp.setChild(dir2, doubleRotate(p, last)); + } else if (isRed(sibling.getChild(!last))) { + gp.setChild(dir2, singleRotate(p, last)); + } + + // ensure correct coloring + const gpc = gp.getChild(dir2); + assert(gpc); + gpc.red = true; + node.red = true; + assert(gpc.left); + gpc.left.red = false; + assert(gpc.right); + gpc.right.red = false; + } + } + } + } + } + + // replace and remove if found + if (found !== null) { + found.data = node.data; + assert(p); + p.setChild(p.right === node, node.getChild(node.left === null)); + } + + // update root and make it black + this.#root = head.right; + if (this.#root !== null) { + this.#root.red = false; + } + + return found !== null; + } + } + + function isRed(node) { + return node !== null && node.red; + } + + function singleRotate(root, dir) { + const save = root.getChild(!dir); + assert(save); + + root.setChild(!dir, save.getChild(dir)); + save.setChild(dir, root); + + root.red = true; + save.red = false; + + return save; + } + + function doubleRotate(root, dir) { + root.setChild(!dir, singleRotate(root.getChild(!dir), !dir)); + return singleRotate(root, dir); + } + + const { console } = globalThis; + const OriginalDate = Date; + + // Timeout values > TIMEOUT_MAX are set to 1. + const TIMEOUT_MAX = 2 ** 31 - 1; + + let globalTimeoutDue = null; + + let nextTimerId = 1; + const idMap = new Map(); + const dueTree = new RBTree((a, b) => a.due - b.due); + + function clearGlobalTimeout() { + globalTimeoutDue = null; + opStopGlobalTimer(); + } + + let pendingEvents = 0; + const pendingFireTimers = []; + + /** Process and run a single ready timer macrotask. + * This function should be registered through Deno.core.setMacrotaskCallback. + * Returns true when all ready macrotasks have been processed, false if more + * ready ones are available. The Isolate future would rely on the return value + * to repeatedly invoke this function until depletion. Multiple invocations + * of this function one at a time ensures newly ready microtasks are processed + * before next macrotask timer callback is invoked. */ + function handleTimerMacrotask() { + if (pendingFireTimers.length > 0) { + fire(pendingFireTimers.shift()); + return pendingFireTimers.length === 0; + } + return true; + } + + async function setGlobalTimeout(due, now) { + // 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; + pendingEvents++; + // FIXME(bartlomieju): this is problematic, because `clearGlobalTimeout` + // is synchronous. That means that timer is cancelled, but this promise is still pending + // until next turn of event loop. This leads to "leaking of async ops" in tests; + // because `clearTimeout/clearInterval` might be the last statement in test function + // `opSanitizer` will immediately complain that there is pending op going on, unless + // some timeout/defer is put in place to allow promise resolution. + // Ideally `clearGlobalTimeout` doesn't return until this op is resolved, but + // I'm not if that's possible. + await opStartGlobalTimer(timeout); + pendingEvents--; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + prepareReadyTimers(); + } + + function prepareReadyTimers() { + const now = OriginalDate.now(); + // Bail out if we're not expecting the global timer to fire. + if (globalTimeoutDue === null || pendingEvents > 0) { + return; + } + // After firing the timers that are due now, this will hold the first timer + // list that hasn't fired yet. + let nextDueNode; + while ((nextDueNode = dueTree.min()) !== null && nextDueNode.due <= now) { + dueTree.remove(nextDueNode); + // Fire all the timers in the list. + for (const timer of nextDueNode.timers) { + // With the list dropped, the timer is no longer scheduled. + timer.scheduled = false; + // Place the callback to pending timers to fire. + pendingFireTimers.push(timer); + } + } + setOrClearGlobalTimeout(nextDueNode && nextDueNode.due, now); + } + + function setOrClearGlobalTimeout(due, now) { + if (due == null) { + clearGlobalTimeout(); + } else { + setGlobalTimeout(due, now); + } + } + + function schedule(timer, now) { + assert(!timer.scheduled); + assert(now <= timer.due); + // Find or create the list of timers that will fire at point-in-time `due`. + const maybeNewDueNode = { due: timer.due, timers: [] }; + let dueNode = dueTree.find(maybeNewDueNode); + if (dueNode === null) { + dueTree.insert(maybeNewDueNode); + dueNode = maybeNewDueNode; + } + // Append the newly scheduled timer to the list and mark it as scheduled. + dueNode.timers.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) { + // Check if our timer is pending scheduling or pending firing. + // If either is true, they are not in tree, and their idMap entry + // will be deleted soon. Remove it from queue. + let index = -1; + if ((index = pendingFireTimers.indexOf(timer)) >= 0) { + pendingFireTimers.splice(index); + return; + } + // If timer is not in the 2 pending queues and is unscheduled, + // it is not in the tree. + if (!timer.scheduled) { + return; + } + const searchKey = { due: timer.due, timers: [] }; + // Find the list of timers that will fire at point-in-time `due`. + const list = dueTree.find(searchKey).timers; + if (list.length === 1) { + // Time timer is the only one in the list. Remove the entire list. + assert(list[0] === timer); + dueTree.remove(searchKey); + // 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) { + const nextDueNode = dueTree.min(); + setOrClearGlobalTimeout( + nextDueNode && nextDueNode.due, + OriginalDate.now(), + ); + } + } 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) { + // 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 = OriginalDate.now(); + 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 checkThis(thisArg) { + if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis) { + throw new TypeError("Illegal invocation"); + } + } + + function checkBigInt(n) { + if (typeof n === "bigint") { + throw new TypeError("Cannot convert a BigInt value to a number"); + } + } + + function setTimer( + cb, + delay, + args, + repeat, + ) { + // Bind `args` to the callback and bind `this` to globalThis(global). + const callback = cb.bind(globalThis, ...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 = OriginalDate.now(); + 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; + } + + function setTimeout( + cb, + delay = 0, + ...args + ) { + checkBigInt(delay); + checkThis(this); + return setTimer(cb, delay, args, false); + } + + function setInterval( + cb, + delay = 0, + ...args + ) { + checkBigInt(delay); + checkThis(this); + return setTimer(cb, delay, args, true); + } + + function clearTimer(id) { + 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); + } + + function clearTimeout(id = 0) { + checkBigInt(id); + if (id === 0) { + return; + } + clearTimer(id); + } + + function clearInterval(id = 0) { + checkBigInt(id); + if (id === 0) { + return; + } + clearTimer(id); + } + + window.__bootstrap.timers = { + clearInterval, + setInterval, + clearTimeout, + setTimeout, + handleTimerMacrotask, + opStopGlobalTimer, + opStartGlobalTimer, + opNow, + }; +})(this); diff --git a/cli/rt/11_url.js b/cli/rt/11_url.js new file mode 100644 index 000000000..435d3454e --- /dev/null +++ b/cli/rt/11_url.js @@ -0,0 +1,858 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { build } = window.__bootstrap.build; + const { getRandomValues } = window.__bootstrap.crypto; + const { customInspect } = window.__bootstrap.console; + const { sendSync } = window.__bootstrap.dispatchJson; + const { isIterable, requiredArguments } = window.__bootstrap.webUtil; + + /** https://url.spec.whatwg.org/#idna */ + function domainToAscii( + domain, + { beStrict = false } = {}, + ) { + return sendSync("op_domain_to_ascii", { domain, beStrict }); + } + + const urls = new WeakMap(); + + class URLSearchParams { + #params = []; + + constructor(init = "") { + 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]); + } + + urls.set(this, null); + } + + #handleStringInitialization = (init) => { + // 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)); + } + }; + + #handleArrayInitialization = ( + init, + ) => { + // 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]); + } + }; + + #updateSteps = () => { + const url = urls.get(this); + if (url == null) { + return; + } + parts.get(url).query = this.toString(); + }; + + #append = (name, value) => { + this.#params.push([String(name), String(value)]); + }; + + append(name, value) { + requiredArguments("URLSearchParams.append", arguments.length, 2); + this.#append(name, value); + this.#updateSteps(); + } + + delete(name) { + 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(); + } + + getAll(name) { + 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; + } + + get(name) { + requiredArguments("URLSearchParams.get", arguments.length, 1); + name = String(name); + for (const entry of this.#params) { + if (entry[0] === name) { + return entry[1]; + } + } + + return null; + } + + has(name) { + requiredArguments("URLSearchParams.has", arguments.length, 1); + name = String(name); + return this.#params.some((entry) => entry[0] === name); + } + + set(name, value) { + 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() { + this.#params.sort((a, b) => (a[0] === b[0] ? 0 : a[0] > b[0] ? 1 : -1)); + this.#updateSteps(); + } + + forEach( + callbackfn, + thisArg, + ) { + requiredArguments("URLSearchParams.forEach", arguments.length, 1); + + if (typeof thisArg !== "undefined") { + callbackfn = callbackfn.bind(thisArg); + } + + for (const [key, value] of this.#params) { + callbackfn(value, key, this); + } + } + + *keys() { + for (const [key] of this.#params) { + yield key; + } + } + + *values() { + for (const [, value] of this.#params) { + yield value; + } + } + + *entries() { + yield* this.#params; + } + + *[Symbol.iterator]() { + yield* this.#params; + } + + toString() { + return this.#params + .map( + (tuple) => + `${encodeURIComponent(tuple[0])}=${encodeURIComponent(tuple[1])}`, + ) + .join("&"); + } + } + + const searchParamsMethods = [ + "append", + "delete", + "set", + ]; + + const specialSchemes = ["ftp", "file", "http", "https", "ws", "wss"]; + + // https://url.spec.whatwg.org/#special-scheme + const schemePorts = { + ftp: "21", + file: "", + http: "80", + https: "443", + ws: "80", + wss: "443", + }; + const MAX_PORT = 2 ** 16 - 1; + + // Remove the part of the string that matches the pattern and return the + // remainder (RHS) as well as the first captured group of the matched substring + // (LHS). e.g. + // takePattern("https://deno.land:80", /^([a-z]+):[/]{2}/) + // = ["http", "deno.land:80"] + // takePattern("deno.land:80", /^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/) + // = ["deno.land", "80"] + function takePattern(string, pattern) { + let capture = ""; + const rest = string.replace(pattern, (_, capture_) => { + capture = capture_; + return ""; + }); + return [capture, rest]; + } + + function parse(url, isBase = true) { + const parts = {}; + let restUrl; + [parts.protocol, restUrl] = takePattern(url.trim(), /^([a-z]+):/); + if (isBase && parts.protocol == "") { + return undefined; + } + const isSpecial = specialSchemes.includes(parts.protocol); + if (parts.protocol == "file") { + parts.slashes = "//"; + parts.username = ""; + parts.password = ""; + [parts.hostname, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/); + parts.port = ""; + if (build.os == "windows" && parts.hostname == "") { + // UNC paths. e.g. "\\\\localhost\\foo\\bar" on Windows should be + // representable as `new URL("file:////localhost/foo/bar")` which is + // equivalent to: `new URL("file://localhost/foo/bar")`. + [parts.hostname, restUrl] = takePattern( + restUrl, + /^[/\\]{2,}([^/\\?#]*)/, + ); + } + } else { + let restAuthority; + if (isSpecial) { + parts.slashes = "//"; + [restAuthority, restUrl] = takePattern( + restUrl, + /^[/\\]{2,}([^/\\?#]*)/, + ); + } else { + parts.slashes = restUrl.match(/^[/\\]{2}/) ? "//" : ""; + [restAuthority, restUrl] = takePattern(restUrl, /^[/\\]{2}([^/\\?#]*)/); + } + let restAuthentication; + [restAuthentication, restAuthority] = takePattern( + restAuthority, + /^(.*)@/, + ); + [parts.username, restAuthentication] = takePattern( + restAuthentication, + /^([^:]*)/, + ); + parts.username = encodeUserinfo(parts.username); + [parts.password] = takePattern(restAuthentication, /^:(.*)/); + parts.password = encodeUserinfo(parts.password); + [parts.hostname, restAuthority] = takePattern( + restAuthority, + /^(\[[0-9a-fA-F.:]{2,}\]|[^:]+)/, + ); + [parts.port] = takePattern(restAuthority, /^:(.*)/); + if (!isValidPort(parts.port)) { + return undefined; + } + if (parts.hostname == "" && isSpecial && isBase) { + return undefined; + } + } + try { + parts.hostname = encodeHostname(parts.hostname, isSpecial); + } catch { + return undefined; + } + [parts.path, restUrl] = takePattern(restUrl, /^([^?#]*)/); + parts.path = encodePathname(parts.path.replace(/\\/g, "/")); + [parts.query, restUrl] = takePattern(restUrl, /^(\?[^#]*)/); + parts.query = encodeSearch(parts.query); + [parts.hash] = takePattern(restUrl, /^(#.*)/); + parts.hash = encodeHash(parts.hash); + return parts; + } + + // Based on https://github.com/kelektiv/node-uuid + // TODO(kevinkassimo): Use deno_std version once possible. + function generateUUID() { + return "00000000-0000-4000-8000-000000000000".replace(/[0]/g, () => + // 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. + const blobURLMap = new Map(); + + function isAbsolutePath(path) { + return path.startsWith("/"); + } + + // Resolves `.`s and `..`s where possible. + // Preserves repeating and trailing `/`s by design. + // On Windows, drive letter paths will be given a leading slash, and also a + // trailing slash if there are no other components e.g. "C:" -> "/C:/". + function normalizePath(path, isFilePath = false) { + if (build.os == "windows" && isFilePath) { + path = path.replace(/^\/*([A-Za-z]:)(\/|$)/, "/$1/"); + } + const isAbsolute = isAbsolutePath(path); + path = path.replace(/^\//, ""); + const pathSegments = path.split("/"); + + const newPathSegments = []; + 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, + basePath, + isFilePath = false, + ) { + let normalizedPath = normalizePath(path, isFilePath); + let normalizedBasePath = normalizePath(basePath, isFilePath); + + let driveLetterPrefix = ""; + if (build.os == "windows" && isFilePath) { + let driveLetter; + let baseDriveLetter; + [driveLetter, normalizedPath] = takePattern( + normalizedPath, + /^(\/[A-Za-z]:)(?=\/)/, + ); + [baseDriveLetter, normalizedBasePath] = takePattern( + normalizedBasePath, + /^(\/[A-Za-z]:)(?=\/)/, + ); + driveLetterPrefix = driveLetter || baseDriveLetter; + } + + if (isAbsolutePath(normalizedPath)) { + return `${driveLetterPrefix}${normalizedPath}`; + } + if (!isAbsolutePath(normalizedBasePath)) { + throw new TypeError("Base path must be absolute."); + } + + // Special case. + if (path == "") { + return `${driveLetterPrefix}${normalizedBasePath}`; + } + + // Remove everything after the last `/` in `normalizedBasePath`. + const prefix = normalizedBasePath.replace(/[^\/]*$/, ""); + // If `normalizedPath` ends with `.` or `..`, add a trailing slash. + const suffix = normalizedPath.replace(/(?<=(^|\/)(\.|\.\.))$/, "/"); + + return `${driveLetterPrefix}${normalizePath(prefix + suffix)}`; + } + + function isValidPort(value) { + // https://url.spec.whatwg.org/#port-state + if (value === "") return true; + + const port = Number(value); + return Number.isInteger(port) && port >= 0 && port <= MAX_PORT; + } + + const parts = new WeakMap(); + + class URL { + #searchParams = null; + + [customInspect]() { + const keys = [ + "href", + "origin", + "protocol", + "username", + "password", + "host", + "hostname", + "port", + "pathname", + "hash", + "search", + ]; + const objectString = keys + .map((key) => `${key}: "${this[key] || ""}"`) + .join(", "); + return `URL { ${objectString} }`; + } + + #updateSearchParams = () => { + const searchParams = new URLSearchParams(this.search); + + for (const methodName of searchParamsMethods) { + const method = searchParams[methodName]; + searchParams[methodName] = (...args) => { + method.apply(searchParams, args); + this.search = searchParams.toString(); + }; + } + this.#searchParams = searchParams; + + urls.set(searchParams, this); + }; + + get hash() { + return parts.get(this).hash; + } + + set hash(value) { + value = unescape(String(value)); + if (!value) { + parts.get(this).hash = ""; + } else { + if (value.charAt(0) !== "#") { + value = `#${value}`; + } + // hashes can contain % and # unescaped + parts.get(this).hash = encodeHash(value); + } + } + + get host() { + return `${this.hostname}${this.port ? `:${this.port}` : ""}`; + } + + set host(value) { + value = String(value); + const url = new URL(`http://${value}`); + parts.get(this).hostname = url.hostname; + parts.get(this).port = url.port; + } + + get hostname() { + return parts.get(this).hostname; + } + + set hostname(value) { + value = String(value); + try { + const isSpecial = specialSchemes.includes(parts.get(this).protocol); + parts.get(this).hostname = encodeHostname(value, isSpecial); + } catch {} + } + + get href() { + const authentication = this.username || this.password + ? `${this.username}${this.password ? ":" + this.password : ""}@` + : ""; + const host = this.host; + const slashes = host ? "//" : parts.get(this).slashes; + let pathname = this.pathname; + if (pathname.charAt(0) != "/" && pathname != "" && host != "") { + pathname = `/${pathname}`; + } + return `${this.protocol}${slashes}${authentication}${host}${pathname}${this.search}${this.hash}`; + } + + set href(value) { + value = String(value); + if (value !== this.href) { + const url = new URL(value); + parts.set(this, { ...parts.get(url) }); + this.#updateSearchParams(); + } + } + + get origin() { + if (this.host) { + return `${this.protocol}//${this.host}`; + } + return "null"; + } + + get password() { + return parts.get(this).password; + } + + set password(value) { + value = String(value); + parts.get(this).password = encodeUserinfo(value); + } + + get pathname() { + let path = parts.get(this).path; + if (specialSchemes.includes(parts.get(this).protocol)) { + if (path.charAt(0) != "/") { + path = `/${path}`; + } + } + return path; + } + + set pathname(value) { + parts.get(this).path = encodePathname(String(value)); + } + + get port() { + const port = parts.get(this).port; + if (schemePorts[parts.get(this).protocol] === port) { + return ""; + } + + return port; + } + + set port(value) { + if (!isValidPort(value)) { + return; + } + parts.get(this).port = value.toString(); + } + + get protocol() { + return `${parts.get(this).protocol}:`; + } + + set protocol(value) { + value = String(value); + if (value) { + if (value.charAt(value.length - 1) === ":") { + value = value.slice(0, -1); + } + parts.get(this).protocol = encodeURIComponent(value); + } + } + + get search() { + return parts.get(this).query; + } + + set search(value) { + value = String(value); + const query = value == "" || value.charAt(0) == "?" ? value : `?${value}`; + parts.get(this).query = encodeSearch(query); + this.#updateSearchParams(); + } + + get username() { + return parts.get(this).username; + } + + set username(value) { + value = String(value); + parts.get(this).username = encodeUserinfo(value); + } + + get searchParams() { + return this.#searchParams; + } + + constructor(url, base) { + let baseParts; + if (base) { + baseParts = typeof base === "string" ? parse(base) : parts.get(base); + if (baseParts === undefined) { + throw new TypeError("Invalid base URL."); + } + } + + const urlParts = typeof url === "string" + ? parse(url, !baseParts) + : parts.get(url); + if (urlParts == undefined) { + throw new TypeError("Invalid URL."); + } + + if (urlParts.protocol) { + urlParts.path = normalizePath( + urlParts.path, + urlParts.protocol == "file", + ); + parts.set(this, urlParts); + } else if (baseParts) { + parts.set(this, { + protocol: baseParts.protocol, + slashes: baseParts.slashes, + username: baseParts.username, + password: baseParts.password, + hostname: baseParts.hostname, + port: baseParts.port, + path: resolvePathFromBase( + urlParts.path, + baseParts.path || "/", + baseParts.protocol == "file", + ), + query: urlParts.query, + hash: urlParts.hash, + }); + } else { + throw new TypeError("Invalid URL."); + } + + this.#updateSearchParams(); + } + + toString() { + return this.href; + } + + toJSON() { + return this.href; + } + + // TODO(kevinkassimo): implement MediaSource version in the future. + static createObjectURL(blob) { + const origin = "http://deno-opaque-origin"; + const key = `blob:${origin}/${generateUUID()}`; + blobURLMap.set(key, blob); + return key; + } + + static revokeObjectURL(url) { + 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 globalThis.location.origin at some point. + blobURLMap.delete(url); + } + } + + function parseIpv4Number(s) { + if (s.match(/^(0[Xx])[0-9A-Za-z]+$/)) { + return Number(s); + } + if (s.match(/^[0-9]+$/)) { + return Number(s.startsWith("0") ? `0o${s}` : s); + } + return NaN; + } + + function parseIpv4(s) { + const parts = s.split("."); + if (parts[parts.length - 1] == "" && parts.length > 1) { + parts.pop(); + } + if (parts.includes("") || parts.length > 4) { + return s; + } + const numbers = parts.map(parseIpv4Number); + if (numbers.includes(NaN)) { + return s; + } + const last = numbers.pop(); + if (last >= 256 ** (4 - numbers.length) || numbers.find((n) => n >= 256)) { + throw new TypeError("Invalid hostname."); + } + const ipv4 = numbers.reduce((sum, n, i) => sum + n * 256 ** (3 - i), last); + const ipv4Hex = ipv4.toString(16).padStart(8, "0"); + const ipv4HexParts = ipv4Hex.match(/(..)(..)(..)(..)$/).slice(1); + return ipv4HexParts.map((s) => String(Number(`0x${s}`))).join("."); + } + + function charInC0ControlSet(c) { + return (c >= "\u0000" && c <= "\u001F") || c > "\u007E"; + } + + function charInSearchSet(c) { + // deno-fmt-ignore + return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u0023", "\u0027", "\u003C", "\u003E"].includes(c) || c > "\u007E"; + } + + function charInFragmentSet(c) { + // deno-fmt-ignore + return charInC0ControlSet(c) || ["\u0020", "\u0022", "\u003C", "\u003E", "\u0060"].includes(c); + } + + function charInPathSet(c) { + // deno-fmt-ignore + return charInFragmentSet(c) || ["\u0023", "\u003F", "\u007B", "\u007D"].includes(c); + } + + function charInUserinfoSet(c) { + // "\u0027" ("'") seemingly isn't in the spec, but matches Chrome and Firefox. + // deno-fmt-ignore + return charInPathSet(c) || ["\u0027", "\u002F", "\u003A", "\u003B", "\u003D", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E", "\u007C"].includes(c); + } + + function charIsForbiddenInHost(c) { + // deno-fmt-ignore + return ["\u0000", "\u0009", "\u000A", "\u000D", "\u0020", "\u0023", "\u0025", "\u002F", "\u003A", "\u003C", "\u003E", "\u003F", "\u0040", "\u005B", "\u005C", "\u005D", "\u005E"].includes(c); + } + + const encoder = new TextEncoder(); + + function encodeChar(c) { + return [...encoder.encode(c)] + .map((n) => `%${n.toString(16)}`) + .join("") + .toUpperCase(); + } + + function encodeUserinfo(s) { + return [...s].map((c) => (charInUserinfoSet(c) ? encodeChar(c) : c)).join( + "", + ); + } + + function encodeHostname(s, isSpecial = true) { + // IPv6 parsing. + if (s.startsWith("[") && s.endsWith("]")) { + if (!s.match(/^\[[0-9A-Fa-f.:]{2,}\]$/)) { + throw new TypeError("Invalid hostname."); + } + // IPv6 address compress + return s.toLowerCase().replace(/\b:?(?:0+:?){2,}/, "::"); + } + + let result = s; + + if (!isSpecial) { + // Check against forbidden host code points except for "%". + for (const c of result) { + if (charIsForbiddenInHost(c) && c != "\u0025") { + throw new TypeError("Invalid hostname."); + } + } + + // Percent-encode C0 control set. + result = [...result] + .map((c) => (charInC0ControlSet(c) ? encodeChar(c) : c)) + .join(""); + + return result; + } + + // Percent-decode. + if (result.match(/%(?![0-9A-Fa-f]{2})/) != null) { + throw new TypeError("Invalid hostname."); + } + result = result.replace( + /%(.{2})/g, + (_, hex) => String.fromCodePoint(Number(`0x${hex}`)), + ); + + // IDNA domain to ASCII. + result = domainToAscii(result); + + // Check against forbidden host code points. + for (const c of result) { + if (charIsForbiddenInHost(c)) { + throw new TypeError("Invalid hostname."); + } + } + + // IPv4 parsing. + if (isSpecial) { + result = parseIpv4(result); + } + + return result; + } + + function encodePathname(s) { + return [...s].map((c) => (charInPathSet(c) ? encodeChar(c) : c)).join(""); + } + + function encodeSearch(s) { + return [...s].map((c) => (charInSearchSet(c) ? encodeChar(c) : c)).join(""); + } + + function encodeHash(s) { + return [...s].map((c) => (charInFragmentSet(c) ? encodeChar(c) : c)).join( + "", + ); + } + + window.__bootstrap.url = { + URL, + URLSearchParams, + blobURLMap, + }; +})(this); diff --git a/cli/rt/11_workers.js b/cli/rt/11_workers.js new file mode 100644 index 000000000..8ae0d5ad5 --- /dev/null +++ b/cli/rt/11_workers.js @@ -0,0 +1,231 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +((window) => { + const { log } = window.__bootstrap.util; + const { sendSync, sendAsync } = window.__bootstrap.dispatchJson; + /* + import { blobURLMap } from "./web/url.ts"; + */ + + function createWorker( + specifier, + hasSourceCode, + sourceCode, + useDenoNamespace, + name, + ) { + return sendSync("op_create_worker", { + specifier, + hasSourceCode, + sourceCode, + name, + useDenoNamespace, + }); + } + + function hostTerminateWorker(id) { + sendSync("op_host_terminate_worker", { id }); + } + + function hostPostMessage(id, data) { + sendSync("op_host_post_message", { id }, data); + } + + function hostGetMessage(id) { + return sendAsync("op_host_get_message", { id }); + } + + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + class MessageEvent extends Event { + constructor(type, eventInitDict) { + super(type, { + bubbles: eventInitDict?.bubbles ?? false, + cancelable: eventInitDict?.cancelable ?? false, + composed: eventInitDict?.composed ?? false, + }); + + this.data = eventInitDict?.data ?? null; + this.origin = eventInitDict?.origin ?? ""; + this.lastEventId = eventInitDict?.lastEventId ?? ""; + } + } + + function encodeMessage(data) { + const dataJson = JSON.stringify(data); + return encoder.encode(dataJson); + } + + function decodeMessage(dataIntArray) { + const dataJson = decoder.decode(dataIntArray); + return JSON.parse(dataJson); + } + + class Worker extends EventTarget { + #id = 0; + #name = ""; + #terminated = false; + + constructor(specifier, options) { + super(); + const { type = "classic", name = "unknown" } = options ?? {}; + + if (type !== "module") { + throw new Error( + 'Not yet implemented: only "module" type workers are supported', + ); + } + + this.#name = name; + const hasSourceCode = false; + const sourceCode = decoder.decode(new Uint8Array()); + + /* TODO(bartlomieju): + // 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!; + } + */ + + const useDenoNamespace = options ? !!options.deno : false; + + const { id } = createWorker( + specifier, + hasSourceCode, + sourceCode, + useDenoNamespace, + options?.name, + ); + this.#id = id; + this.#poll(); + } + + #handleMessage = (msgData) => { + let data; + try { + data = decodeMessage(new Uint8Array(msgData)); + } catch (e) { + const msgErrorEvent = new MessageEvent("messageerror", { + cancelable: false, + data, + }); + if (this.onmessageerror) { + this.onmessageerror(msgErrorEvent); + } + return; + } + + const msgEvent = new MessageEvent("message", { + cancelable: false, + data, + }); + + if (this.onmessage) { + this.onmessage(msgEvent); + } + + this.dispatchEvent(msgEvent); + }; + + #handleError = (e) => { + const event = new ErrorEvent("error", { + cancelable: true, + message: e.message, + lineno: e.lineNumber ? e.lineNumber + 1 : undefined, + colno: e.columnNumber ? e.columnNumber + 1 : undefined, + filename: e.fileName, + error: null, + }); + + let handled = false; + if (this.onerror) { + this.onerror(event); + } + + this.dispatchEvent(event); + if (event.defaultPrevented) { + handled = true; + } + + return handled; + }; + + #poll = async () => { + while (!this.#terminated) { + const event = await hostGetMessage(this.#id); + + // If terminate was called then we ignore all messages + if (this.#terminated) { + return; + } + + const type = event.type; + + if (type === "terminalError") { + this.#terminated = true; + if (!this.#handleError(event.error)) { + throw Error(event.error.message); + } + continue; + } + + if (type === "msg") { + this.#handleMessage(event.data); + continue; + } + + if (type === "error") { + if (!this.#handleError(event.error)) { + throw Error(event.error.message); + } + continue; + } + + if (type === "close") { + log(`Host got "close" message from worker: ${this.#name}`); + this.#terminated = true; + return; + } + + throw new Error(`Unknown worker event: "${type}"`); + } + }; + + postMessage(message, transferOrOptions) { + if (transferOrOptions) { + throw new Error( + "Not yet implemented: `transfer` and `options` are not supported.", + ); + } + + if (this.#terminated) { + return; + } + + hostPostMessage(this.#id, encodeMessage(message)); + } + + terminate() { + if (!this.#terminated) { + this.#terminated = true; + hostTerminateWorker(this.#id); + } + } + } + + window.__bootstrap.worker = { + Worker, + MessageEvent, + }; +})(this); diff --git a/cli/rt/12_io.js b/cli/rt/12_io.js new file mode 100644 index 000000000..006d51cdd --- /dev/null +++ b/cli/rt/12_io.js @@ -0,0 +1,135 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// Interfaces 100% copied from Go. +// Documentation liberally lifted from them too. +// Thank you! We love Go! <3 + +((window) => { + const DEFAULT_BUFFER_SIZE = 32 * 1024; + const { sendSync, sendAsync } = window.__bootstrap.dispatchMinimal; + // Seek whence values. + // https://golang.org/pkg/io/#pkg-constants + const SeekMode = { + 0: "Start", + 1: "Current", + 2: "End", + + Start: 0, + Current: 1, + End: 2, + }; + + async function copy( + src, + dst, + options, + ) { + let n = 0; + const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE; + const b = new Uint8Array(bufSize); + let gotEOF = false; + while (gotEOF === false) { + const result = await src.read(b); + if (result === null) { + gotEOF = true; + } else { + let nwritten = 0; + while (nwritten < result) { + nwritten += await dst.write(b.subarray(nwritten, result)); + } + n += nwritten; + } + } + return n; + } + + async function* iter( + r, + options, + ) { + const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE; + const b = new Uint8Array(bufSize); + while (true) { + const result = await r.read(b); + if (result === null) { + break; + } + + yield b.subarray(0, result); + } + } + + function* iterSync( + r, + options, + ) { + const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE; + const b = new Uint8Array(bufSize); + while (true) { + const result = r.readSync(b); + if (result === null) { + break; + } + + yield b.subarray(0, result); + } + } + + function readSync(rid, buffer) { + if (buffer.length === 0) { + return 0; + } + + const nread = sendSync("op_read", rid, buffer); + if (nread < 0) { + throw new Error("read error"); + } + + return nread === 0 ? null : nread; + } + + async function read( + rid, + buffer, + ) { + if (buffer.length === 0) { + return 0; + } + + const nread = await sendAsync("op_read", rid, buffer); + if (nread < 0) { + throw new Error("read error"); + } + + return nread === 0 ? null : nread; + } + + function writeSync(rid, data) { + const result = sendSync("op_write", rid, data); + if (result < 0) { + throw new Error("write error"); + } + + return result; + } + + async function write(rid, data) { + const result = await sendAsync("op_write", rid, data); + if (result < 0) { + throw new Error("write error"); + } + + return result; + } + + window.__bootstrap.io = { + iterSync, + iter, + copy, + SeekMode, + read, + readSync, + write, + writeSync, + }; +})(this); diff --git a/cli/rt/13_buffer.js b/cli/rt/13_buffer.js new file mode 100644 index 000000000..e06e2138b --- /dev/null +++ b/cli/rt/13_buffer.js @@ -0,0 +1,241 @@ +// Copyright 2018-2020 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 + +((window) => { + const { assert } = window.__bootstrap.util; + + // 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 = 32 * 1024; + 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(src, dst, off = 0) { + const r = dst.byteLength - off; + if (src.byteLength > r) { + src = src.subarray(0, r); + } + dst.set(src, off); + return src.byteLength; + } + + class Buffer { + #buf = null; // contents are the bytes buf[off : len(buf)] + #off = 0; // read at buf[off], write at buf[buf.byteLength] + + constructor(ab) { + if (ab == null) { + this.#buf = new Uint8Array(0); + return; + } + + this.#buf = new Uint8Array(ab); + } + + bytes(options = { copy: true }) { + if (options.copy === false) return this.#buf.subarray(this.#off); + return this.#buf.slice(this.#off); + } + + empty() { + return this.#buf.byteLength <= this.#off; + } + + get length() { + return this.#buf.byteLength - this.#off; + } + + get capacity() { + return this.#buf.buffer.byteLength; + } + + truncate(n) { + 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() { + this.#reslice(0); + this.#off = 0; + } + + #tryGrowByReslice = (n) => { + const l = this.#buf.byteLength; + if (n <= this.capacity - l) { + this.#reslice(l + n); + return l; + } + return -1; + }; + + #reslice = (len) => { + assert(len <= this.#buf.buffer.byteLength); + this.#buf = new Uint8Array(this.#buf.buffer, 0, len); + }; + + readSync(p) { + 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 null; + } + const nread = copyBytes(this.#buf.subarray(this.#off), p); + this.#off += nread; + return nread; + } + + read(p) { + const rr = this.readSync(p); + return Promise.resolve(rr); + } + + writeSync(p) { + const m = this.#grow(p.byteLength); + return copyBytes(p, this.#buf, m); + } + + write(p) { + const n = this.writeSync(p); + return Promise.resolve(n); + } + + #grow = (n) => { + 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.subarray(this.#off), this.#buf); + } else if (c + n > MAX_SIZE) { + throw new Error("The buffer cannot be grown beyond the maximum size."); + } else { + // Not enough space anywhere, we need to allocate. + const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE)); + copyBytes(this.#buf.subarray(this.#off), buf); + this.#buf = buf; + } + // Restore this.#off and len(this.#buf). + this.#off = 0; + this.#reslice(Math.min(m + n, MAX_SIZE)); + return m; + }; + + grow(n) { + if (n < 0) { + throw Error("Buffer.grow: negative count"); + } + const m = this.#grow(n); + this.#reslice(m); + } + + async readFrom(r) { + let n = 0; + const tmp = new Uint8Array(MIN_READ); + while (true) { + const shouldGrow = this.capacity - this.length < MIN_READ; + // read into tmp buffer if there's not enough room + // otherwise read directly into the internal buffer + const buf = shouldGrow + ? tmp + : new Uint8Array(this.#buf.buffer, this.length); + + const nread = await r.read(buf); + if (nread === null) { + return n; + } + + // write will grow if needed + if (shouldGrow) this.writeSync(buf.subarray(0, nread)); + else this.#reslice(this.length + nread); + + n += nread; + } + } + + readFromSync(r) { + let n = 0; + const tmp = new Uint8Array(MIN_READ); + while (true) { + const shouldGrow = this.capacity - this.length < MIN_READ; + // read into tmp buffer if there's not enough room + // otherwise read directly into the internal buffer + const buf = shouldGrow + ? tmp + : new Uint8Array(this.#buf.buffer, this.length); + + const nread = r.readSync(buf); + if (nread === null) { + return n; + } + + // write will grow if needed + if (shouldGrow) this.writeSync(buf.subarray(0, nread)); + else this.#reslice(this.length + nread); + + n += nread; + } + } + } + + async function readAll(r) { + const buf = new Buffer(); + await buf.readFrom(r); + return buf.bytes(); + } + + function readAllSync(r) { + const buf = new Buffer(); + buf.readFromSync(r); + return buf.bytes(); + } + + async function writeAll(w, arr) { + let nwritten = 0; + while (nwritten < arr.length) { + nwritten += await w.write(arr.subarray(nwritten)); + } + } + + function writeAllSync(w, arr) { + let nwritten = 0; + while (nwritten < arr.length) { + nwritten += w.writeSync(arr.subarray(nwritten)); + } + } + + window.__bootstrap.buffer = { + writeAll, + writeAllSync, + readAll, + readAllSync, + Buffer, + }; +})(this); diff --git a/cli/rt/20_blob.js b/cli/rt/20_blob.js new file mode 100644 index 000000000..5b0ef349e --- /dev/null +++ b/cli/rt/20_blob.js @@ -0,0 +1,223 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { build } = window.__bootstrap.build; + const { ReadableStream } = window.__bootstrap.streams; + + const bytesSymbol = Symbol("bytes"); + + function containsOnlyASCII(str) { + if (typeof str !== "string") { + return false; + } + return /^[\x00-\x7F]*$/.test(str); + } + + function convertLineEndingsToNative(s) { + const nativeLineEnd = build.os == "windows" ? "\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, + position, + ) { + 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, + doNormalizeLineEndingsToNative, + ) { + const ret = []; + 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 Blob) { + 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, + options, + ) { + 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) => u8.byteLength) + .reduce((a, b) => 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; + } + + function getStream(blobBytes) { + // TODO: Align to spec https://fetch.spec.whatwg.org/#concept-construct-readablestream + return new ReadableStream({ + type: "bytes", + start: (controller) => { + controller.enqueue(blobBytes); + controller.close(); + }, + }); + } + + async function readBytes( + reader, + ) { + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (!done && value instanceof Uint8Array) { + chunks.push(value); + } else if (done) { + const size = chunks.reduce((p, i) => p + i.byteLength, 0); + const bytes = new Uint8Array(size); + let offs = 0; + for (const chunk of chunks) { + bytes.set(chunk, offs); + offs += chunk.byteLength; + } + return bytes; + } else { + throw new TypeError("Invalid reader result."); + } + } + } + + // A WeakMap holding blob to byte array mapping. + // Ensures it does not impact garbage collection. + const blobBytesWeakMap = new WeakMap(); + + class Blob { + constructor(blobParts, options) { + if (arguments.length === 0) { + this[bytesSymbol] = new Uint8Array(); + return; + } + + const { ending = "transparent", type = "" } = options ?? {}; + // Normalize options.type. + let normalizedType = type; + if (!containsOnlyASCII(type)) { + normalizedType = ""; + } else { + if (type.length) { + for (let i = 0; i < type.length; ++i) { + const char = type[i]; + if (char < "\u0020" || char > "\u007E") { + normalizedType = ""; + break; + } + } + normalizedType = type.toLowerCase(); + } + } + const bytes = processBlobParts(blobParts, { ending, type }); + // Set Blob object's properties. + this[bytesSymbol] = bytes; + this.size = bytes.byteLength; + this.type = normalizedType; + } + + slice(start, end, contentType) { + return new Blob([this[bytesSymbol].slice(start, end)], { + type: contentType || this.type, + }); + } + + stream() { + return getStream(this[bytesSymbol]); + } + + async text() { + const reader = getStream(this[bytesSymbol]).getReader(); + const decoder = new TextDecoder(); + return decoder.decode(await readBytes(reader)); + } + + arrayBuffer() { + return readBytes(getStream(this[bytesSymbol]).getReader()); + } + } + + window.__bootstrap.blob = { + Blob, + bytesSymbol, + containsOnlyASCII, + blobBytesWeakMap, + }; +})(this); diff --git a/cli/rt/20_headers.js b/cli/rt/20_headers.js new file mode 100644 index 000000000..7b9ef8c8e --- /dev/null +++ b/cli/rt/20_headers.js @@ -0,0 +1,257 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { DomIterableMixin } = window.__bootstrap.domIterable; + const { requiredArguments } = window.__bootstrap.webUtil; + const { customInspect } = window.__bootstrap.console; + + // From node-fetch + // Copyright (c) 2016 David Frank. MIT License. + const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/; + const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/; + + function isHeaders(value) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return value instanceof Headers; + } + + const headersData = Symbol("headers data"); + + // TODO: headerGuard? Investigate if it is needed + // node-fetch did not implement this but it is in the spec + function normalizeParams(name, value) { + 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. + function validateName(name) { + if (invalidTokenRegex.test(name) || name === "") { + throw new TypeError(`${name} is not a legal HTTP header name`); + } + } + + function validateValue(value) { + if (invalidHeaderCharRegex.test(value)) { + throw new TypeError(`${value} is not a legal HTTP header value`); + } + } + + /** Appends a key and value to the header list. + * + * The spec indicates that when a key already exists, the append adds the new + * value onto the end of the existing value. The behaviour of this though + * varies when the key is `set-cookie`. In this case, if the key of the cookie + * already exists, the value is replaced, but if the key of the cookie does not + * exist, and additional `set-cookie` header is added. + * + * The browser specification of `Headers` is written for clients, and not + * servers, and Deno is a server, meaning that it needs to follow the patterns + * expected for servers, of which a `set-cookie` header is expected for each + * unique cookie key, but duplicate cookie keys should not exist. */ + function dataAppend( + data, + key, + value, + ) { + for (let i = 0; i < data.length; i++) { + const [dataKey] = data[i]; + if (key === "set-cookie" && dataKey === "set-cookie") { + const [, dataValue] = data[i]; + const [dataCookieKey] = dataValue.split("="); + const [cookieKey] = value.split("="); + if (dataCookieKey === cookieKey) { + data[i][1] = value; + return; + } + } else { + if (dataKey === key) { + data[i][1] += `, ${value}`; + return; + } + } + } + data.push([key, value]); + } + + /** Gets a value of a key in the headers list. + * + * This varies slightly from spec behaviour in that when the key is `set-cookie` + * the value returned will look like a concatenated value, when in fact, if the + * headers were iterated over, each individual `set-cookie` value is a unique + * entry in the headers list. */ + function dataGet( + data, + key, + ) { + const setCookieValues = []; + for (const [dataKey, value] of data) { + if (dataKey === key) { + if (key === "set-cookie") { + setCookieValues.push(value); + } else { + return value; + } + } + } + if (setCookieValues.length) { + return setCookieValues.join(", "); + } + return undefined; + } + + /** Sets a value of a key in the headers list. + * + * The spec indicates that the value should be replaced if the key already + * exists. The behaviour here varies, where if the key is `set-cookie` the key + * of the cookie is inspected, and if the key of the cookie already exists, + * then the value is replaced. If the key of the cookie is not found, then + * the value of the `set-cookie` is added to the list of headers. + * + * The browser specification of `Headers` is written for clients, and not + * servers, and Deno is a server, meaning that it needs to follow the patterns + * expected for servers, of which a `set-cookie` header is expected for each + * unique cookie key, but duplicate cookie keys should not exist. */ + function dataSet( + data, + key, + value, + ) { + for (let i = 0; i < data.length; i++) { + const [dataKey] = data[i]; + if (dataKey === key) { + // there could be multiple set-cookie headers, but all others are unique + if (key === "set-cookie") { + const [, dataValue] = data[i]; + const [dataCookieKey] = dataValue.split("="); + const [cookieKey] = value.split("="); + if (cookieKey === dataCookieKey) { + data[i][1] = value; + return; + } + } else { + data[i][1] = value; + return; + } + } + } + data.push([key, value]); + } + + function dataDelete(data, key) { + let i = 0; + while (i < data.length) { + const [dataKey] = data[i]; + if (dataKey === key) { + data.splice(i, 1); + } else { + i++; + } + } + } + + function dataHas(data, key) { + for (const [dataKey] of data) { + if (dataKey === key) { + return true; + } + } + return false; + } + + // ref: https://fetch.spec.whatwg.org/#dom-headers + class HeadersBase { + constructor(init) { + if (init === null) { + throw new TypeError( + "Failed to construct 'Headers'; The provided value was not valid", + ); + } else if (isHeaders(init)) { + this[headersData] = [...init]; + } else { + this[headersData] = []; + 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, + ); + + this.append(tuple[0], tuple[1]); + } + } else if (init) { + for (const [rawName, rawValue] of Object.entries(init)) { + this.append(rawName, rawValue); + } + } + } + } + + [customInspect]() { + let length = this[headersData].length; + let output = ""; + for (const [key, value] of this[headersData]) { + const prefix = length === this[headersData].length ? " " : ""; + const postfix = length === 1 ? " " : ", "; + output = output + `${prefix}${key}: ${value}${postfix}`; + length--; + } + return `Headers {${output}}`; + } + + // ref: https://fetch.spec.whatwg.org/#concept-headers-append + append(name, value) { + requiredArguments("Headers.append", arguments.length, 2); + const [newname, newvalue] = normalizeParams(name, value); + validateName(newname); + validateValue(newvalue); + dataAppend(this[headersData], newname, newvalue); + } + + delete(name) { + requiredArguments("Headers.delete", arguments.length, 1); + const [newname] = normalizeParams(name); + validateName(newname); + dataDelete(this[headersData], newname); + } + + get(name) { + requiredArguments("Headers.get", arguments.length, 1); + const [newname] = normalizeParams(name); + validateName(newname); + return dataGet(this[headersData], newname) ?? null; + } + + has(name) { + requiredArguments("Headers.has", arguments.length, 1); + const [newname] = normalizeParams(name); + validateName(newname); + return dataHas(this[headersData], newname); + } + + set(name, value) { + requiredArguments("Headers.set", arguments.length, 2); + const [newname, newvalue] = normalizeParams(name, value); + validateName(newname); + validateValue(newvalue); + dataSet(this[headersData], newname, newvalue); + } + + get [Symbol.toStringTag]() { + return "Headers"; + } + } + + class Headers extends DomIterableMixin(HeadersBase, headersData) {} + + window.__bootstrap.headers = { + Headers, + }; +})(this); diff --git a/cli/rt/20_streams_queuing_strategy.js b/cli/rt/20_streams_queuing_strategy.js new file mode 100644 index 000000000..cbd30664f --- /dev/null +++ b/cli/rt/20_streams_queuing_strategy.js @@ -0,0 +1,50 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { customInspect } = window.__bootstrap.console; + + class CountQueuingStrategy { + constructor({ highWaterMark }) { + this.highWaterMark = highWaterMark; + } + + size() { + return 1; + } + + [customInspect]() { + return `${this.constructor.name} { highWaterMark: ${ + String(this.highWaterMark) + }, size: f }`; + } + } + + Object.defineProperty(CountQueuingStrategy.prototype, "size", { + enumerable: true, + }); + + class ByteLengthQueuingStrategy { + constructor({ highWaterMark }) { + this.highWaterMark = highWaterMark; + } + + size(chunk) { + return chunk.byteLength; + } + + [customInspect]() { + return `${this.constructor.name} { highWaterMark: ${ + String(this.highWaterMark) + }, size: f }`; + } + } + + Object.defineProperty(ByteLengthQueuingStrategy.prototype, "size", { + enumerable: true, + }); + + window.__bootstrap.queuingStrategy = { + CountQueuingStrategy, + ByteLengthQueuingStrategy, + }; +})(this); diff --git a/cli/rt/21_dom_file.js b/cli/rt/21_dom_file.js new file mode 100644 index 000000000..9d2f7fb6b --- /dev/null +++ b/cli/rt/21_dom_file.js @@ -0,0 +1,27 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const blob = window.__bootstrap.blob; + + class DomFile extends blob.Blob { + constructor( + fileBits, + fileName, + options, + ) { + const { lastModified = Date.now(), ...blobPropertyBag } = options ?? {}; + super(fileBits, blobPropertyBag); + + // 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 = lastModified; + } + } + + window.__bootstrap.domFile = { + DomFile, + }; +})(this); diff --git a/cli/rt/22_form_data.js b/cli/rt/22_form_data.js new file mode 100644 index 000000000..cc656d387 --- /dev/null +++ b/cli/rt/22_form_data.js @@ -0,0 +1,116 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const blob = window.__bootstrap.blob; + const domFile = window.__bootstrap.domFile; + const { DomIterableMixin } = window.__bootstrap.domIterable; + const { requiredArguments } = window.__bootstrap.webUtil; + + const dataSymbol = Symbol("data"); + + function parseFormDataValue(value, filename) { + if (value instanceof domFile.DomFile) { + return new domFile.DomFile([value], filename || value.name, { + type: value.type, + lastModified: value.lastModified, + }); + } else if (value instanceof blob.Blob) { + return new domFile.DomFile([value], filename || "blob", { + type: value.type, + }); + } else { + return String(value); + } + } + + class FormDataBase { + [dataSymbol] = []; + + append(name, value, filename) { + requiredArguments("FormData.append", arguments.length, 2); + name = String(name); + this[dataSymbol].push([name, parseFormDataValue(value, filename)]); + } + + delete(name) { + 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++; + } + } + } + + getAll(name) { + 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; + } + + get(name) { + requiredArguments("FormData.get", arguments.length, 1); + name = String(name); + for (const entry of this[dataSymbol]) { + if (entry[0] === name) { + return entry[1]; + } + } + + return null; + } + + has(name) { + requiredArguments("FormData.has", arguments.length, 1); + name = String(name); + return this[dataSymbol].some((entry) => entry[0] === name); + } + + set(name, value, filename) { + 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) { + this[dataSymbol][i][1] = parseFormDataValue(value, filename); + found = true; + } else { + this[dataSymbol].splice(i, 1); + continue; + } + } + i++; + } + + // Otherwise, append entry to the context object’s entry list. + if (!found) { + this[dataSymbol].push([name, parseFormDataValue(value, filename)]); + } + } + + get [Symbol.toStringTag]() { + return "FormData"; + } + } + + class FormData extends DomIterableMixin(FormDataBase, dataSymbol) {} + + window.__bootstrap.formData = { + FormData, + }; +})(this); diff --git a/cli/rt/23_multipart.js b/cli/rt/23_multipart.js new file mode 100644 index 000000000..78c1d28a1 --- /dev/null +++ b/cli/rt/23_multipart.js @@ -0,0 +1,199 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { Buffer } = window.__bootstrap.buffer; + const { bytesSymbol, Blob } = window.__bootstrap.blob; + const { DomFile } = window.__bootstrap.domFile; + const { getHeaderValueParams } = window.__bootstrap.webUtil; + + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + const CR = "\r".charCodeAt(0); + const LF = "\n".charCodeAt(0); + + class MultipartBuilder { + constructor(formData, boundary) { + this.formData = formData; + this.boundary = boundary ?? this.#createBoundary(); + this.writer = new Buffer(); + } + + getContentType() { + return `multipart/form-data; boundary=${this.boundary}`; + } + + getBody() { + for (const [fieldName, fieldValue] of this.formData.entries()) { + if (fieldValue instanceof DomFile) { + this.#writeFile(fieldName, fieldValue); + } else this.#writeField(fieldName, fieldValue); + } + + this.writer.writeSync(encoder.encode(`\r\n--${this.boundary}--`)); + + return this.writer.bytes(); + } + + #createBoundary = () => { + return ( + "----------" + + Array.from(Array(32)) + .map(() => Math.random().toString(36)[2] || 0) + .join("") + ); + }; + + #writeHeaders = (headers) => { + let buf = this.writer.empty() ? "" : "\r\n"; + + buf += `--${this.boundary}\r\n`; + for (const [key, value] of headers) { + buf += `${key}: ${value}\r\n`; + } + buf += `\r\n`; + + this.writer.write(encoder.encode(buf)); + }; + + #writeFileHeaders = ( + field, + filename, + type, + ) => { + const headers = [ + [ + "Content-Disposition", + `form-data; name="${field}"; filename="${filename}"`, + ], + ["Content-Type", type || "application/octet-stream"], + ]; + return this.#writeHeaders(headers); + }; + + #writeFieldHeaders = (field) => { + const headers = [["Content-Disposition", `form-data; name="${field}"`]]; + return this.#writeHeaders(headers); + }; + + #writeField = (field, value) => { + this.#writeFieldHeaders(field); + this.writer.writeSync(encoder.encode(value)); + }; + + #writeFile = (field, value) => { + this.#writeFileHeaders(field, value.name, value.type); + this.writer.writeSync(value[bytesSymbol]); + }; + } + + class MultipartParser { + constructor(body, boundary) { + if (!boundary) { + throw new TypeError("multipart/form-data must provide a boundary"); + } + + this.boundary = `--${boundary}`; + this.body = body; + this.boundaryChars = encoder.encode(this.boundary); + } + + #parseHeaders = (headersText) => { + const headers = new Headers(); + const rawHeaders = headersText.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); + } + + return { + headers, + disposition: getHeaderValueParams( + headers.get("Content-Disposition") ?? "", + ), + }; + }; + + parse() { + const formData = new FormData(); + let headerText = ""; + let boundaryIndex = 0; + let state = 0; + let fileStart = 0; + + for (let i = 0; i < this.body.length; i++) { + const byte = this.body[i]; + const prevByte = this.body[i - 1]; + const isNewLine = byte === LF && prevByte === CR; + + if (state === 1 || state === 2 || state == 3) { + headerText += String.fromCharCode(byte); + } + if (state === 0 && isNewLine) { + state = 1; + } else if (state === 1 && isNewLine) { + state = 2; + const headersDone = this.body[i + 1] === CR && + this.body[i + 2] === LF; + + if (headersDone) { + state = 3; + } + } else if (state === 2 && isNewLine) { + state = 3; + } else if (state === 3 && isNewLine) { + state = 4; + fileStart = i + 1; + } else if (state === 4) { + if (this.boundaryChars[boundaryIndex] !== byte) { + boundaryIndex = 0; + } else { + boundaryIndex++; + } + + if (boundaryIndex >= this.boundary.length) { + const { headers, disposition } = this.#parseHeaders(headerText); + const content = this.body.subarray( + fileStart, + i - boundaryIndex - 1, + ); + // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata + const filename = disposition.get("filename"); + const name = disposition.get("name"); + + state = 5; + // Reset + boundaryIndex = 0; + headerText = ""; + + if (!name) { + continue; // Skip, unknown name + } + + if (filename) { + const blob = new Blob([content], { + type: headers.get("Content-Type") || "application/octet-stream", + }); + formData.append(name, blob, filename); + } else { + formData.append(name, decoder.decode(content)); + } + } + } else if (state === 5 && isNewLine) { + state = 1; + } + } + + return formData; + } + } + + window.__bootstrap.multipart = { + MultipartBuilder, + MultipartParser, + }; +})(this); diff --git a/cli/rt/24_body.js b/cli/rt/24_body.js new file mode 100644 index 000000000..ebd0ddc6d --- /dev/null +++ b/cli/rt/24_body.js @@ -0,0 +1,207 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { Blob } = window.__bootstrap.blob; + const { ReadableStream, isReadableStreamDisturbed } = + window.__bootstrap.streams; + const { Buffer } = window.__bootstrap.buffer; + const { + getHeaderValueParams, + hasHeaderValueOf, + isTypedArray, + } = window.__bootstrap.webUtil; + const { MultipartParser } = window.__bootstrap.multipart; + + function validateBodyType(owner, bodySource) { + if (isTypedArray(bodySource)) { + return true; + } else if (bodySource instanceof ArrayBuffer) { + return true; + } else if (typeof bodySource === "string") { + return true; + } else if (bodySource instanceof ReadableStream) { + return true; + } else if (bodySource instanceof FormData) { + return true; + } else if (bodySource instanceof URLSearchParams) { + return true; + } else if (!bodySource) { + return true; // null body is fine + } + throw new Error( + `Bad ${owner.constructor.name} body type: ${bodySource.constructor.name}`, + ); + } + + async function bufferFromStream( + stream, + size, + ) { + const encoder = new TextEncoder(); + const buffer = new Buffer(); + + if (size) { + // grow to avoid unnecessary allocations & copies + buffer.grow(size); + } + + while (true) { + const { done, value } = await stream.read(); + + if (done) break; + + if (typeof value === "string") { + buffer.writeSync(encoder.encode(value)); + } else if (value instanceof ArrayBuffer) { + buffer.writeSync(new Uint8Array(value)); + } else if (value instanceof Uint8Array) { + buffer.writeSync(value); + } else if (!value) { + // noop for undefined + } else { + throw new Error("unhandled type on stream read"); + } + } + + return buffer.bytes().buffer; + } + + const BodyUsedError = + "Failed to execute 'clone' on 'Body': body is already used"; + + class Body { + #contentType = ""; + #size = undefined; + + constructor(_bodySource, meta) { + validateBodyType(this, _bodySource); + this._bodySource = _bodySource; + this.#contentType = meta.contentType; + this.#size = meta.size; + this._stream = null; + } + + get body() { + if (this._stream) { + return this._stream; + } + + if (this._bodySource instanceof ReadableStream) { + this._stream = this._bodySource; + } + if (typeof this._bodySource === "string") { + const bodySource = this._bodySource; + this._stream = new ReadableStream({ + start(controller) { + controller.enqueue(bodySource); + controller.close(); + }, + }); + } + return this._stream; + } + + get bodyUsed() { + if (this.body && isReadableStreamDisturbed(this.body)) { + return true; + } + return false; + } + + async blob() { + return new Blob([await this.arrayBuffer()], { + type: this.#contentType, + }); + } + + // ref: https://fetch.spec.whatwg.org/#body-mixin + async formData() { + const formData = new FormData(); + if (hasHeaderValueOf(this.#contentType, "multipart/form-data")) { + const params = getHeaderValueParams(this.#contentType); + + // ref: https://tools.ietf.org/html/rfc2046#section-5.1 + const boundary = params.get("boundary"); + const body = new Uint8Array(await this.arrayBuffer()); + const multipartParser = new MultipartParser(body, boundary); + + return multipartParser.parse(); + } 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) => { + 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"); + } + } + + async text() { + if (typeof this._bodySource === "string") { + return this._bodySource; + } + + const ab = await this.arrayBuffer(); + const decoder = new TextDecoder("utf-8"); + return decoder.decode(ab); + } + + async json() { + const raw = await this.text(); + return JSON.parse(raw); + } + + arrayBuffer() { + if (isTypedArray(this._bodySource)) { + return Promise.resolve(this._bodySource.buffer); + } else if (this._bodySource instanceof ArrayBuffer) { + return Promise.resolve(this._bodySource); + } else if (typeof this._bodySource === "string") { + const enc = new TextEncoder(); + return Promise.resolve( + enc.encode(this._bodySource).buffer, + ); + } else if (this._bodySource instanceof ReadableStream) { + return bufferFromStream(this._bodySource.getReader(), this.#size); + } else if ( + this._bodySource instanceof FormData || + this._bodySource instanceof URLSearchParams + ) { + const enc = new TextEncoder(); + return Promise.resolve( + enc.encode(this._bodySource.toString()).buffer, + ); + } else if (!this._bodySource) { + return Promise.resolve(new ArrayBuffer(0)); + } + throw new Error( + `Body type not yet implemented: ${this._bodySource.constructor.name}`, + ); + } + } + + window.__bootstrap.body = { + Body, + BodyUsedError, + }; +})(this); diff --git a/cli/rt/25_request.js b/cli/rt/25_request.js new file mode 100644 index 000000000..467a66fe9 --- /dev/null +++ b/cli/rt/25_request.js @@ -0,0 +1,139 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const body = window.__bootstrap.body; + const { ReadableStream } = window.__bootstrap.streams; + + function byteUpperCase(s) { + return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c) { + return c.toUpperCase(); + }); + } + + function normalizeMethod(m) { + const u = byteUpperCase(m); + if ( + u === "DELETE" || + u === "GET" || + u === "HEAD" || + u === "OPTIONS" || + u === "POST" || + u === "PUT" + ) { + return u; + } + return m; + } + + class Request extends body.Body { + constructor(input, init) { + if (arguments.length < 1) { + throw TypeError("Not enough arguments"); + } + + if (!init) { + init = {}; + } + + let b; + + // 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; + + // 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; + this.method = "GET"; + + // readonly attribute USVString url; + 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); + } + + if ( + init && + "credentials" in init && + init.credentials && + ["omit", "same-origin", "include"].indexOf(init.credentials) !== -1 + ) { + this.credentials = init.credentials; + } + } + + clone() { + if (this.bodyUsed) { + throw TypeError(body.BodyUsedError); + } + + const iterators = this.headers.entries(); + const headersList = []; + for (const header of iterators) { + headersList.push(header); + } + + let body2 = this._bodySource; + + if (this._bodySource instanceof ReadableStream) { + const tees = this._bodySource.tee(); + this._stream = this._bodySource = tees[0]; + body2 = tees[1]; + } + + return new Request(this.url, { + body: body2, + method: this.method, + headers: new Headers(headersList), + credentials: this.credentials, + }); + } + } + + window.__bootstrap.request = { + Request, + }; +})(this); diff --git a/cli/rt/26_fetch.js b/cli/rt/26_fetch.js new file mode 100644 index 000000000..2aee7c457 --- /dev/null +++ b/cli/rt/26_fetch.js @@ -0,0 +1,370 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { notImplemented } = window.__bootstrap.util; + const { getHeaderValueParams, isTypedArray } = window.__bootstrap.webUtil; + const { Blob, bytesSymbol: blobBytesSymbol } = window.__bootstrap.blob; + const { read } = window.__bootstrap.io; + const { close } = window.__bootstrap.resources; + const { sendAsync } = window.__bootstrap.dispatchJson; + const Body = window.__bootstrap.body; + const { ReadableStream } = window.__bootstrap.streams; + const { MultipartBuilder } = window.__bootstrap.multipart; + const { Headers } = window.__bootstrap.headers; + + function opFetch( + args, + body, + ) { + let zeroCopy; + if (body != null) { + zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength); + } + + return sendAsync("op_fetch", args, ...(zeroCopy ? [zeroCopy] : [])); + } + + const NULL_BODY_STATUS = [101, 204, 205, 304]; + const REDIRECT_STATUS = [301, 302, 303, 307, 308]; + + const responseData = new WeakMap(); + class Response extends Body.Body { + constructor(body = null, init) { + init = init ?? {}; + + if (typeof init !== "object") { + throw new TypeError(`'init' is not an object`); + } + + const extraInit = responseData.get(init) || {}; + let { type = "default", url = "" } = extraInit; + + let status = init.status === undefined ? 200 : Number(init.status || 0); + let statusText = init.statusText ?? ""; + let headers = init.headers instanceof Headers + ? init.headers + : new Headers(init.headers); + + if (init.status !== undefined && (status < 200 || status > 599)) { + throw new RangeError( + `The status provided (${init.status}) is outside the range [200, 599]`, + ); + } + + // null body status + if (body && NULL_BODY_STATUS.includes(status)) { + throw new TypeError("Response with null body status cannot have body"); + } + + if (!type) { + type = "default"; + } else { + if (type == "error") { + // spec: https://fetch.spec.whatwg.org/#concept-network-error + status = 0; + statusText = ""; + headers = new Headers(); + body = null; + /* spec for other Response types: + https://fetch.spec.whatwg.org/#concept-filtered-response-basic + Please note that type "basic" is not the same thing as "default".*/ + } else if (type == "basic") { + for (const h of headers) { + /* Forbidden Response-Header Names: + https://fetch.spec.whatwg.org/#forbidden-response-header-name */ + if (["set-cookie", "set-cookie2"].includes(h[0].toLowerCase())) { + headers.delete(h[0]); + } + } + } else if (type == "cors") { + /* CORS-safelisted Response-Header Names: + https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name */ + const allowedHeaders = [ + "Cache-Control", + "Content-Language", + "Content-Length", + "Content-Type", + "Expires", + "Last-Modified", + "Pragma", + ].map((c) => c.toLowerCase()); + for (const h of headers) { + /* Technically this is still not standards compliant because we are + supposed to allow headers allowed in the + 'Access-Control-Expose-Headers' header in the 'internal response' + However, this implementation of response doesn't seem to have an + easy way to access the internal response, so we ignore that + header. + TODO(serverhiccups): change how internal responses are handled + so we can do this properly. */ + if (!allowedHeaders.includes(h[0].toLowerCase())) { + headers.delete(h[0]); + } + } + /* TODO(serverhiccups): Once I fix the 'internal response' thing, + these actually need to treat the internal response differently */ + } else if (type == "opaque" || type == "opaqueredirect") { + url = ""; + status = 0; + statusText = ""; + headers = new Headers(); + body = null; + } + } + + const contentType = headers.get("content-type") || ""; + const size = Number(headers.get("content-length")) || undefined; + + super(body, { contentType, size }); + + this.url = url; + this.statusText = statusText; + this.status = extraInit.status || status; + this.headers = headers; + this.redirected = extraInit.redirected || false; + this.type = type; + } + + get ok() { + return 200 <= this.status && this.status < 300; + } + + clone() { + if (this.bodyUsed) { + throw TypeError(Body.BodyUsedError); + } + + const iterators = this.headers.entries(); + const headersList = []; + for (const header of iterators) { + headersList.push(header); + } + + let resBody = this._bodySource; + + if (this._bodySource instanceof ReadableStream) { + const tees = this._bodySource.tee(); + this._stream = this._bodySource = tees[0]; + resBody = tees[1]; + } + + return new Response(resBody, { + status: this.status, + statusText: this.statusText, + headers: new Headers(headersList), + }); + } + + static redirect(url, status) { + if (![301, 302, 303, 307, 308].includes(status)) { + throw new RangeError( + "The redirection status must be one of 301, 302, 303, 307 and 308.", + ); + } + return new Response(null, { + status, + statusText: "", + headers: [["Location", typeof url === "string" ? url : url.toString()]], + }); + } + } + + function sendFetchReq( + url, + method, + headers, + body, + ) { + let headerArray = []; + if (headers) { + headerArray = Array.from(headers.entries()); + } + + const args = { + method, + url, + headers: headerArray, + }; + + return opFetch(args, body); + } + + async function fetch( + input, + init, + ) { + let url; + let method = null; + let headers = null; + let body; + let redirected = false; + let remRedirectCount = 20; // TODO: use a better way to handle + + if (typeof input === "string" || input instanceof URL) { + url = typeof input === "string" ? input : input.href; + 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 ArrayBuffer) { + body = new Uint8Array(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 Blob) { + body = init.body[blobBytesSymbol]; + contentType = init.body.type; + } else if (init.body instanceof FormData) { + let boundary; + if (headers.has("content-type")) { + const params = getHeaderValueParams("content-type"); + boundary = params.get("boundary"); + } + const multipartBuilder = new MultipartBuilder(init.body, boundary); + body = multipartBuilder.getBody(); + contentType = multipartBuilder.getContentType(); + } else { + // TODO: ReadableStream + notImplemented(); + } + if (contentType && !headers.has("content-type")) { + headers.set("content-type", contentType); + } + } + } + } else { + url = input.url; + method = input.method; + headers = input.headers; + + if (input._bodySource) { + body = new DataView(await input.arrayBuffer()); + } + } + + let responseBody; + let responseInit = {}; + while (remRedirectCount) { + const fetchResponse = await sendFetchReq(url, method, headers, body); + + if ( + NULL_BODY_STATUS.includes(fetchResponse.status) || + REDIRECT_STATUS.includes(fetchResponse.status) + ) { + // We won't use body of received response, so close it now + // otherwise it will be kept in resource table. + close(fetchResponse.bodyRid); + responseBody = null; + } else { + responseBody = new ReadableStream({ + async pull(controller) { + try { + const b = new Uint8Array(1024 * 32); + const result = await read(fetchResponse.bodyRid, b); + if (result === null) { + controller.close(); + return close(fetchResponse.bodyRid); + } + + controller.enqueue(b.subarray(0, result)); + } catch (e) { + controller.error(e); + controller.close(); + close(fetchResponse.bodyRid); + } + }, + cancel() { + // When reader.cancel() is called + close(fetchResponse.bodyRid); + }, + }); + } + + responseInit = { + status: 200, + statusText: fetchResponse.statusText, + headers: fetchResponse.headers, + }; + + responseData.set(responseInit, { + redirected, + rid: fetchResponse.bodyRid, + status: fetchResponse.status, + url, + }); + + const response = new Response(responseBody, responseInit); + + if (REDIRECT_STATUS.includes(fetchResponse.status)) { + // We're in a redirect status + switch ((init && init.redirect) || "follow") { + case "error": + responseInit = {}; + responseData.set(responseInit, { + type: "error", + redirected: false, + url: "", + }); + return new Response(null, responseInit); + case "manual": + responseInit = {}; + responseData.set(responseInit, { + type: "opaqueredirect", + redirected: false, + url: "", + }); + return new Response(null, responseInit); + case "follow": + default: + let redirectUrl = response.headers.get("Location"); + if (redirectUrl == null) { + return response; // Unspecified + } + if ( + !redirectUrl.startsWith("http://") && + !redirectUrl.startsWith("https://") + ) { + redirectUrl = new URL(redirectUrl, url).href; + } + url = redirectUrl; + redirected = true; + remRedirectCount--; + } + } else { + return response; + } + } + + responseData.set(responseInit, { + type: "error", + redirected: false, + url: "", + }); + + return new Response(null, responseInit); + } + + window.__bootstrap.fetch = { + fetch, + Response, + }; +})(this); diff --git a/cli/rt/30_files.js b/cli/rt/30_files.js new file mode 100644 index 000000000..0b6d9c67f --- /dev/null +++ b/cli/rt/30_files.js @@ -0,0 +1,204 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { close } = window.__bootstrap.resources; + const { read, readSync, write, writeSync } = window.__bootstrap.io; + const { sendSync, sendAsync } = window.__bootstrap.dispatchJson; + const { pathFromURL } = window.__bootstrap.util; + + function seekSync( + rid, + offset, + whence, + ) { + return sendSync("op_seek", { rid, offset, whence }); + } + + function seek( + rid, + offset, + whence, + ) { + return sendAsync("op_seek", { rid, offset, whence }); + } + + function opOpenSync(path, options) { + const mode = options?.mode; + return sendSync("op_open", { path: pathFromURL(path), options, mode }); + } + + function opOpen( + path, + options, + ) { + const mode = options?.mode; + return sendAsync("op_open", { path: pathFromURL(path), options, mode }); + } + + function openSync( + path, + options = { read: true }, + ) { + checkOpenOptions(options); + const rid = opOpenSync(path, options); + return new File(rid); + } + + async function open( + path, + options = { read: true }, + ) { + checkOpenOptions(options); + const rid = await opOpen(path, options); + return new File(rid); + } + + function createSync(path) { + return openSync(path, { + read: true, + write: true, + truncate: true, + create: true, + }); + } + + function create(path) { + return open(path, { + read: true, + write: true, + truncate: true, + create: true, + }); + } + + class File { + #rid = 0; + + constructor(rid) { + this.#rid = rid; + } + + get rid() { + return this.#rid; + } + + write(p) { + return write(this.rid, p); + } + + writeSync(p) { + return writeSync(this.rid, p); + } + + read(p) { + return read(this.rid, p); + } + + readSync(p) { + return readSync(this.rid, p); + } + + seek(offset, whence) { + return seek(this.rid, offset, whence); + } + + seekSync(offset, whence) { + return seekSync(this.rid, offset, whence); + } + + close() { + close(this.rid); + } + } + + class Stdin { + constructor() { + this.rid = 0; + } + + read(p) { + return read(this.rid, p); + } + + readSync(p) { + return readSync(this.rid, p); + } + + close() { + close(this.rid); + } + } + + class Stdout { + constructor() { + this.rid = 1; + } + + write(p) { + return write(this.rid, p); + } + + writeSync(p) { + return writeSync(this.rid, p); + } + + close() { + close(this.rid); + } + } + + class Stderr { + constructor() { + this.rid = 2; + } + + write(p) { + return write(this.rid, p); + } + + writeSync(p) { + return writeSync(this.rid, p); + } + + close() { + close(this.rid); + } + } + + const stdin = new Stdin(); + const stdout = new Stdout(); + const stderr = new Stderr(); + + function checkOpenOptions(options) { + if (Object.values(options).filter((val) => val === true).length === 0) { + throw new Error("OpenOptions requires at least one option to be true"); + } + + if (options.truncate && !options.write) { + throw new Error("'truncate' option requires 'write' option"); + } + + const createOrCreateNewWithoutWriteOrAppend = + (options.create || options.createNew) && + !(options.write || options.append); + + if (createOrCreateNewWithoutWriteOrAppend) { + throw new Error( + "'create' or 'createNew' options require 'write' or 'append' option", + ); + } + } + + window.__bootstrap.files = { + stdin, + stdout, + stderr, + File, + create, + createSync, + open, + openSync, + seek, + seekSync, + }; +})(this); diff --git a/cli/rt/30_fs.js b/cli/rt/30_fs.js new file mode 100644 index 000000000..163c00604 --- /dev/null +++ b/cli/rt/30_fs.js @@ -0,0 +1,375 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { sendSync, sendAsync } = window.__bootstrap.dispatchJson; + const { pathFromURL } = window.__bootstrap.util; + const build = window.__bootstrap.build.build; + + function chmodSync(path, mode) { + sendSync("op_chmod", { path: pathFromURL(path), mode }); + } + + async function chmod(path, mode) { + await sendAsync("op_chmod", { path: pathFromURL(path), mode }); + } + + function chownSync( + path, + uid, + gid, + ) { + sendSync("op_chown", { path: pathFromURL(path), uid, gid }); + } + + async function chown( + path, + uid, + gid, + ) { + await sendAsync("op_chown", { path: pathFromURL(path), uid, gid }); + } + + function copyFileSync( + fromPath, + toPath, + ) { + sendSync("op_copy_file", { + from: pathFromURL(fromPath), + to: pathFromURL(toPath), + }); + } + + async function copyFile( + fromPath, + toPath, + ) { + await sendAsync("op_copy_file", { + from: pathFromURL(fromPath), + to: pathFromURL(toPath), + }); + } + + function cwd() { + return sendSync("op_cwd"); + } + + function chdir(directory) { + sendSync("op_chdir", { directory }); + } + + function makeTempDirSync(options = {}) { + return sendSync("op_make_temp_dir", options); + } + + function makeTempDir(options = {}) { + return sendAsync("op_make_temp_dir", options); + } + + function makeTempFileSync(options = {}) { + return sendSync("op_make_temp_file", options); + } + + function makeTempFile(options = {}) { + return sendAsync("op_make_temp_file", options); + } + + function mkdirArgs(path, options) { + const args = { path, recursive: false }; + if (options != null) { + if (typeof options.recursive == "boolean") { + args.recursive = options.recursive; + } + if (options.mode) { + args.mode = options.mode; + } + } + return args; + } + + function mkdirSync(path, options) { + sendSync("op_mkdir", mkdirArgs(path, options)); + } + + async function mkdir( + path, + options, + ) { + await sendAsync("op_mkdir", mkdirArgs(path, options)); + } + + function res(response) { + return response.entries; + } + + function readDirSync(path) { + return res(sendSync("op_read_dir", { path: pathFromURL(path) }))[ + Symbol.iterator + ](); + } + + function readDir(path) { + const array = sendAsync("op_read_dir", { path: pathFromURL(path) }).then( + res, + ); + return { + async *[Symbol.asyncIterator]() { + yield* await array; + }, + }; + } + + function readLinkSync(path) { + return sendSync("op_read_link", { path }); + } + + function readLink(path) { + return sendAsync("op_read_link", { path }); + } + + function realPathSync(path) { + return sendSync("op_realpath", { path }); + } + + function realPath(path) { + return sendAsync("op_realpath", { path }); + } + + function removeSync( + path, + options = {}, + ) { + sendSync("op_remove", { + path: pathFromURL(path), + recursive: !!options.recursive, + }); + } + + async function remove( + path, + options = {}, + ) { + await sendAsync("op_remove", { + path: pathFromURL(path), + recursive: !!options.recursive, + }); + } + + function renameSync(oldpath, newpath) { + sendSync("op_rename", { oldpath, newpath }); + } + + async function rename(oldpath, newpath) { + await sendAsync("op_rename", { oldpath, newpath }); + } + + function parseFileInfo(response) { + const unix = build.os === "darwin" || build.os === "linux"; + return { + isFile: response.isFile, + isDirectory: response.isDirectory, + isSymlink: response.isSymlink, + size: response.size, + mtime: response.mtime != null ? new Date(response.mtime) : null, + atime: response.atime != null ? new Date(response.atime) : null, + birthtime: response.birthtime != null + ? new Date(response.birthtime) + : null, + // Only non-null if on Unix + dev: unix ? response.dev : null, + ino: unix ? response.ino : null, + mode: unix ? response.mode : null, + nlink: unix ? response.nlink : null, + uid: unix ? response.uid : null, + gid: unix ? response.gid : null, + rdev: unix ? response.rdev : null, + blksize: unix ? response.blksize : null, + blocks: unix ? response.blocks : null, + }; + } + + function fstatSync(rid) { + return parseFileInfo(sendSync("op_fstat", { rid })); + } + + async function fstat(rid) { + return parseFileInfo(await sendAsync("op_fstat", { rid })); + } + + async function lstat(path) { + const res = await sendAsync("op_stat", { + path: pathFromURL(path), + lstat: true, + }); + return parseFileInfo(res); + } + + function lstatSync(path) { + const res = sendSync("op_stat", { + path: pathFromURL(path), + lstat: true, + }); + return parseFileInfo(res); + } + + async function stat(path) { + const res = await sendAsync("op_stat", { + path: pathFromURL(path), + lstat: false, + }); + return parseFileInfo(res); + } + + function statSync(path) { + const res = sendSync("op_stat", { + path: pathFromURL(path), + lstat: false, + }); + return parseFileInfo(res); + } + + function coerceLen(len) { + if (len == null || len < 0) { + return 0; + } + + return len; + } + + function ftruncateSync(rid, len) { + sendSync("op_ftruncate", { rid, len: coerceLen(len) }); + } + + async function ftruncate(rid, len) { + await sendAsync("op_ftruncate", { rid, len: coerceLen(len) }); + } + + function truncateSync(path, len) { + sendSync("op_truncate", { path, len: coerceLen(len) }); + } + + async function truncate(path, len) { + await sendAsync("op_truncate", { path, len: coerceLen(len) }); + } + + function umask(mask) { + return sendSync("op_umask", { mask }); + } + + function linkSync(oldpath, newpath) { + sendSync("op_link", { oldpath, newpath }); + } + + async function link(oldpath, newpath) { + await sendAsync("op_link", { oldpath, newpath }); + } + + function toSecondsFromEpoch(v) { + return v instanceof Date ? Math.trunc(v.valueOf() / 1000) : v; + } + + function utimeSync( + path, + atime, + mtime, + ) { + sendSync("op_utime", { + path, + // TODO(ry) split atime, mtime into [seconds, nanoseconds] tuple + atime: toSecondsFromEpoch(atime), + mtime: toSecondsFromEpoch(mtime), + }); + } + + async function utime( + path, + atime, + mtime, + ) { + await sendAsync("op_utime", { + path, + // TODO(ry) split atime, mtime into [seconds, nanoseconds] tuple + atime: toSecondsFromEpoch(atime), + mtime: toSecondsFromEpoch(mtime), + }); + } + + function symlinkSync( + oldpath, + newpath, + options, + ) { + sendSync("op_symlink", { oldpath, newpath, options }); + } + + async function symlink( + oldpath, + newpath, + options, + ) { + await sendAsync("op_symlink", { oldpath, newpath, options }); + } + + function fdatasyncSync(rid) { + sendSync("op_fdatasync", { rid }); + } + + async function fdatasync(rid) { + await sendAsync("op_fdatasync", { rid }); + } + + function fsyncSync(rid) { + sendSync("op_fsync", { rid }); + } + + async function fsync(rid) { + await sendAsync("op_fsync", { rid }); + } + + window.__bootstrap.fs = { + cwd, + chdir, + chmodSync, + chmod, + chown, + chownSync, + copyFile, + copyFileSync, + makeTempFile, + makeTempDir, + makeTempFileSync, + makeTempDirSync, + mkdir, + mkdirSync, + readDir, + readDirSync, + readLinkSync, + readLink, + realPathSync, + realPath, + remove, + removeSync, + renameSync, + rename, + fstatSync, + fstat, + lstat, + lstatSync, + stat, + statSync, + ftruncate, + ftruncateSync, + truncate, + truncateSync, + umask, + link, + linkSync, + utime, + utimeSync, + symlink, + symlinkSync, + fdatasync, + fdatasyncSync, + fsync, + fsyncSync, + }; +})(this); diff --git a/cli/rt/30_metrics.js b/cli/rt/30_metrics.js new file mode 100644 index 000000000..59a76d910 --- /dev/null +++ b/cli/rt/30_metrics.js @@ -0,0 +1,13 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { sendSync } = window.__bootstrap.dispatchJson; + + function metrics() { + return sendSync("op_metrics"); + } + + window.__bootstrap.metrics = { + metrics, + }; +})(this); diff --git a/cli/rt/30_net.js b/cli/rt/30_net.js new file mode 100644 index 000000000..78d8b3276 --- /dev/null +++ b/cli/rt/30_net.js @@ -0,0 +1,242 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { errors } = window.__bootstrap.errors; + const { read, write } = window.__bootstrap.io; + const { close } = window.__bootstrap.resources; + const { sendSync, sendAsync } = window.__bootstrap.dispatchJson; + + const ShutdownMode = { + // See http://man7.org/linux/man-pages/man2/shutdown.2.html + // Corresponding to SHUT_RD, SHUT_WR, SHUT_RDWR + 0: "Read", + 1: "Write", + 2: "ReadWrite", + Read: 0, + Write: 1, + ReadWrite: 2, // unused + }; + + function shutdown(rid, how) { + sendSync("op_shutdown", { rid, how }); + return Promise.resolve(); + } + + function opAccept( + rid, + transport, + ) { + return sendAsync("op_accept", { rid, transport }); + } + + function opListen(args) { + return sendSync("op_listen", args); + } + + function opConnect(args) { + return sendAsync("op_connect", args); + } + + function opReceive( + rid, + transport, + zeroCopy, + ) { + return sendAsync("op_datagram_receive", { rid, transport }, zeroCopy); + } + + function opSend(args, zeroCopy) { + return sendAsync("op_datagram_send", args, zeroCopy); + } + + class Conn { + #rid = 0; + #remoteAddr = null; + #localAddr = null; + constructor( + rid, + remoteAddr, + localAddr, + ) { + this.#rid = rid; + this.#remoteAddr = remoteAddr; + this.#localAddr = localAddr; + } + + get rid() { + return this.#rid; + } + + get remoteAddr() { + return this.#remoteAddr; + } + + get localAddr() { + return this.#localAddr; + } + + write(p) { + return write(this.rid, p); + } + + read(p) { + return read(this.rid, p); + } + + close() { + close(this.rid); + } + + // TODO(lucacasonato): make this unavailable in stable + closeWrite() { + shutdown(this.rid, ShutdownMode.Write); + } + } + + class Listener { + #rid = 0; + #addr = null; + + constructor(rid, addr) { + this.#rid = rid; + this.#addr = addr; + } + + get rid() { + return this.#rid; + } + + get addr() { + return this.#addr; + } + + async accept() { + const res = await opAccept(this.rid, this.addr.transport); + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + + async next() { + let conn; + try { + conn = await this.accept(); + } catch (error) { + if (error instanceof errors.BadResource) { + return { value: undefined, done: true }; + } + throw error; + } + return { value: conn, done: false }; + } + + return(value) { + this.close(); + return Promise.resolve({ value, done: true }); + } + + close() { + close(this.rid); + } + + [Symbol.asyncIterator]() { + return this; + } + } + + class Datagram { + #rid = 0; + #addr = null; + + constructor( + rid, + addr, + bufSize = 1024, + ) { + this.#rid = rid; + this.#addr = addr; + this.bufSize = bufSize; + } + + get rid() { + return this.#rid; + } + + get addr() { + return this.#addr; + } + + async receive(p) { + const buf = p || new Uint8Array(this.bufSize); + const { size, remoteAddr } = await opReceive( + this.rid, + this.addr.transport, + buf, + ); + const sub = buf.subarray(0, size); + return [sub, remoteAddr]; + } + + send(p, addr) { + const remote = { hostname: "127.0.0.1", ...addr }; + + const args = { ...remote, rid: this.rid }; + return opSend(args, p); + } + + close() { + close(this.rid); + } + + async *[Symbol.asyncIterator]() { + while (true) { + try { + yield await this.receive(); + } catch (err) { + if (err instanceof errors.BadResource) { + break; + } + throw err; + } + } + } + } + + function listen(options) { + const res = opListen({ + transport: "tcp", + hostname: "0.0.0.0", + ...options, + }); + + return new Listener(res.rid, res.localAddr); + } + + async function connect( + options, + ) { + let res; + + if (options.transport === "unix") { + res = await opConnect(options); + } else { + res = await opConnect({ + transport: "tcp", + hostname: "127.0.0.1", + ...options, + }); + } + + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + + window.__bootstrap.net = { + connect, + Conn, + opConnect, + listen, + opListen, + Listener, + shutdown, + ShutdownMode, + Datagram, + }; +})(this); diff --git a/cli/rt/30_os.js b/cli/rt/30_os.js new file mode 100644 index 000000000..743ecd585 --- /dev/null +++ b/cli/rt/30_os.js @@ -0,0 +1,56 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const sendSync = window.__bootstrap.dispatchJson.sendSync; + + function loadavg() { + return sendSync("op_loadavg"); + } + + function hostname() { + return sendSync("op_hostname"); + } + + function osRelease() { + return sendSync("op_os_release"); + } + + function exit(code = 0) { + sendSync("op_exit", { code }); + throw new Error("Code not reachable"); + } + + function setEnv(key, value) { + sendSync("op_set_env", { key, value }); + } + + function getEnv(key) { + return sendSync("op_get_env", { key })[0]; + } + + function deleteEnv(key) { + sendSync("op_delete_env", { key }); + } + + const env = { + get: getEnv, + toObject() { + return sendSync("op_env"); + }, + set: setEnv, + delete: deleteEnv, + }; + + function execPath() { + return sendSync("op_exec_path"); + } + + window.__bootstrap.os = { + env, + execPath, + exit, + osRelease, + hostname, + loadavg, + }; +})(this); diff --git a/cli/rt/40_compiler_api.js b/cli/rt/40_compiler_api.js new file mode 100644 index 000000000..8a2aa759a --- /dev/null +++ b/cli/rt/40_compiler_api.js @@ -0,0 +1,100 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This file contains the runtime APIs which will dispatch work to the internal +// compiler within Deno. +((window) => { + const util = window.__bootstrap.util; + const { sendAsync } = window.__bootstrap.dispatchJson; + + function opCompile(request) { + return sendAsync("op_compile", request); + } + + function opTranspile( + request, + ) { + return sendAsync("op_transpile", request); + } + + function checkRelative(specifier) { + return specifier.match(/^([\.\/\\]|https?:\/{2}|file:\/{2})/) + ? specifier + : `./${specifier}`; + } + + // TODO(bartlomieju): change return type to interface? + function transpileOnly( + sources, + options = {}, + ) { + util.log("Deno.transpileOnly", { sources: Object.keys(sources), options }); + const payload = { + sources, + options: JSON.stringify(options), + }; + return opTranspile(payload); + } + + // TODO(bartlomieju): change return type to interface? + async function compile( + rootName, + sources, + options = {}, + ) { + const payload = { + rootName: sources ? rootName : checkRelative(rootName), + sources, + options: JSON.stringify(options), + bundle: false, + }; + util.log("Deno.compile", { + rootName: payload.rootName, + sources: !!sources, + options, + }); + const result = await opCompile(payload); + util.assert(result.emitMap); + const maybeDiagnostics = result.diagnostics.length === 0 + ? undefined + : result.diagnostics; + + const emitMap = {}; + + for (const [key, emittedSource] of Object.entries(result.emitMap)) { + emitMap[key] = emittedSource.contents; + } + + return [maybeDiagnostics, emitMap]; + } + + // TODO(bartlomieju): change return type to interface? + async function bundle( + rootName, + sources, + options = {}, + ) { + const payload = { + rootName: sources ? rootName : checkRelative(rootName), + sources, + options: JSON.stringify(options), + bundle: true, + }; + util.log("Deno.bundle", { + rootName: payload.rootName, + sources: !!sources, + options, + }); + const result = await opCompile(payload); + util.assert(result.output); + const maybeDiagnostics = result.diagnostics.length === 0 + ? undefined + : result.diagnostics; + return [maybeDiagnostics, result.output]; + } + + window.__bootstrap.compilerApi = { + bundle, + compile, + transpileOnly, + }; +})(this); diff --git a/cli/rt/40_diagnostics.js b/cli/rt/40_diagnostics.js new file mode 100644 index 000000000..110d3d767 --- /dev/null +++ b/cli/rt/40_diagnostics.js @@ -0,0 +1,27 @@ +// Copyright 2018-2020 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. + +((window) => { + const DiagnosticCategory = { + 0: "Log", + 1: "Debug", + 2: "Info", + 3: "Error", + 4: "Warning", + 5: "Suggestion", + + Log: 0, + Debug: 1, + Info: 2, + Error: 3, + Warning: 4, + Suggestion: 5, + }; + + window.__bootstrap.diagnostics = { + DiagnosticCategory, + }; +})(this); diff --git a/cli/rt/40_error_stack.js b/cli/rt/40_error_stack.js new file mode 100644 index 000000000..80f4fc5ed --- /dev/null +++ b/cli/rt/40_error_stack.js @@ -0,0 +1,267 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + // 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 + const colors = window.__bootstrap.colors; + const assert = window.__bootstrap.util.assert; + const internals = window.__bootstrap.internals; + const dispatchJson = window.__bootstrap.dispatchJson; + + function opFormatDiagnostics(items) { + return dispatchJson.sendSync("op_format_diagnostic", { items }); + } + + function opApplySourceMap(location) { + const res = dispatchJson.sendSync("op_apply_source_map", location); + return { + fileName: res.fileName, + lineNumber: res.lineNumber, + columnNumber: res.columnNumber, + }; + } + + function patchCallSite(callSite, location) { + return { + getThis() { + return callSite.getThis(); + }, + getTypeName() { + return callSite.getTypeName(); + }, + getFunction() { + return callSite.getFunction(); + }, + getFunctionName() { + return callSite.getFunctionName(); + }, + getMethodName() { + return callSite.getMethodName(); + }, + getFileName() { + return location.fileName; + }, + getLineNumber() { + return location.lineNumber; + }, + getColumnNumber() { + return location.columnNumber; + }, + getEvalOrigin() { + return callSite.getEvalOrigin(); + }, + isToplevel() { + return callSite.isToplevel(); + }, + isEval() { + return callSite.isEval(); + }, + isNative() { + return callSite.isNative(); + }, + isConstructor() { + return callSite.isConstructor(); + }, + isAsync() { + return callSite.isAsync(); + }, + isPromiseAll() { + return callSite.isPromiseAll(); + }, + getPromiseIndex() { + return callSite.getPromiseIndex(); + }, + }; + } + + function getMethodCall(callSite) { + 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; + } + + function getFileLocation(callSite, internal = false) { + const cyan = internal ? colors.gray : colors.cyan; + const yellow = internal ? colors.gray : colors.yellow; + const black = internal ? colors.gray : (s) => s; + if (callSite.isNative()) { + return cyan("native"); + } + + let result = ""; + + const fileName = callSite.getFileName(); + if (!fileName && callSite.isEval()) { + const evalOrigin = callSite.getEvalOrigin(); + assert(evalOrigin != null); + result += cyan(`${evalOrigin}, `); + } + + if (fileName) { + result += cyan(fileName); + } else { + result += cyan("<anonymous>"); + } + + const lineNumber = callSite.getLineNumber(); + if (lineNumber != null) { + result += `${black(":")}${yellow(lineNumber.toString())}`; + + const columnNumber = callSite.getColumnNumber(); + if (columnNumber != null) { + result += `${black(":")}${yellow(columnNumber.toString())}`; + } + } + + return result; + } + + function callSiteToString(callSite, internal = false) { + const cyan = internal ? colors.gray : colors.cyan; + const black = internal ? colors.gray : (s) => s; + + 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 += colors.gray("async "); + } + if (isPromiseAll) { + result += colors.bold( + colors.italic( + black(`Promise.all (index ${callSite.getPromiseIndex()})`), + ), + ); + return result; + } + if (isMethodCall) { + result += colors.bold(colors.italic(black(getMethodCall(callSite)))); + } else if (isConstructor) { + result += colors.gray("new "); + if (functionName) { + result += colors.bold(colors.italic(black(functionName))); + } else { + result += cyan("<anonymous>"); + } + } else if (functionName) { + result += colors.bold(colors.italic(black(functionName))); + } else { + result += getFileLocation(callSite, internal); + return result; + } + + result += ` ${black("(")}${getFileLocation(callSite, internal)}${ + black(")") + }`; + return result; + } + + function evaluateCallSite(callSite) { + return { + this: callSite.getThis(), + typeName: callSite.getTypeName(), + function: callSite.getFunction(), + functionName: callSite.getFunctionName(), + methodName: callSite.getMethodName(), + fileName: callSite.getFileName(), + lineNumber: callSite.getLineNumber(), + columnNumber: callSite.getColumnNumber(), + evalOrigin: callSite.getEvalOrigin(), + isToplevel: callSite.isToplevel(), + isEval: callSite.isEval(), + isNative: callSite.isNative(), + isConstructor: callSite.isConstructor(), + isAsync: callSite.isAsync(), + isPromiseAll: callSite.isPromiseAll(), + promiseIndex: callSite.getPromiseIndex(), + }; + } + + function prepareStackTrace( + error, + callSites, + ) { + const mappedCallSites = callSites.map( + (callSite) => { + const fileName = callSite.getFileName(); + const lineNumber = callSite.getLineNumber(); + const columnNumber = callSite.getColumnNumber(); + if (fileName && lineNumber != null && columnNumber != null) { + return patchCallSite( + callSite, + opApplySourceMap({ + fileName, + lineNumber, + columnNumber, + }), + ); + } + return callSite; + }, + ); + Object.defineProperties(error, { + __callSiteEvals: { value: [], configurable: true }, + __formattedFrames: { value: [], configurable: true }, + }); + for (const callSite of mappedCallSites) { + error.__callSiteEvals.push(Object.freeze(evaluateCallSite(callSite))); + const isInternal = callSite.getFileName()?.startsWith("$deno$") ?? false; + error.__formattedFrames.push(callSiteToString(callSite, isInternal)); + } + Object.freeze(error.__callSiteEvals); + Object.freeze(error.__formattedFrames); + return ( + `${error.name}: ${error.message}\n` + + error.__formattedFrames + .map((s) => ` at ${colors.stripColor(s)}`) + .join("\n") + ); + } + + function setPrepareStackTrace(ErrorConstructor) { + ErrorConstructor.prepareStackTrace = prepareStackTrace; + } + + internals.exposeForTest("setPrepareStackTrace", setPrepareStackTrace); + + window.__bootstrap.errorStack = { + setPrepareStackTrace, + opApplySourceMap, + opFormatDiagnostics, + }; +})(this); diff --git a/cli/rt/40_fs_events.js b/cli/rt/40_fs_events.js new file mode 100644 index 000000000..ad1fd678f --- /dev/null +++ b/cli/rt/40_fs_events.js @@ -0,0 +1,45 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { sendSync, sendAsync } = window.__bootstrap.dispatchJson; + const { close } = window.__bootstrap.resources; + + class FsWatcher { + #rid = 0; + + constructor(paths, options) { + const { recursive } = options; + this.#rid = sendSync("op_fs_events_open", { recursive, paths }); + } + + get rid() { + return this.#rid; + } + + next() { + return sendAsync("op_fs_events_poll", { + rid: this.rid, + }); + } + + return(value) { + close(this.rid); + return Promise.resolve({ value, done: true }); + } + + [Symbol.asyncIterator]() { + return this; + } + } + + function watchFs( + paths, + options = { recursive: true }, + ) { + return new FsWatcher(Array.isArray(paths) ? paths : [paths], options); + } + + window.__bootstrap.fsEvents = { + watchFs, + }; +})(this); diff --git a/cli/rt/40_net_unstable.js b/cli/rt/40_net_unstable.js new file mode 100644 index 000000000..fcc899a30 --- /dev/null +++ b/cli/rt/40_net_unstable.js @@ -0,0 +1,48 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const net = window.__bootstrap.net; + + function listen(options) { + if (options.transport === "unix") { + const res = net.opListen(options); + return new net.Listener(res.rid, res.localAddr); + } else { + return net.listen(options); + } + } + + function listenDatagram( + options, + ) { + let res; + if (options.transport === "unixpacket") { + res = net.opListen(options); + } else { + res = net.opListen({ + transport: "udp", + hostname: "127.0.0.1", + ...options, + }); + } + + return new net.Datagram(res.rid, res.localAddr); + } + + async function connect( + options, + ) { + if (options.transport === "unix") { + const res = await net.opConnect(options); + return new net.Conn(res.rid, res.remoteAddr, res.localAddr); + } else { + return net.connect(options); + } + } + + window.__bootstrap.netUnstable = { + connect, + listenDatagram, + listen, + }; +})(this); diff --git a/cli/rt/40_performance.js b/cli/rt/40_performance.js new file mode 100644 index 000000000..768c43a6a --- /dev/null +++ b/cli/rt/40_performance.js @@ -0,0 +1,321 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { opNow } = window.__bootstrap.timers; + const { customInspect, inspect } = window.__bootstrap.console; + const { cloneValue } = window.__bootstrap.webUtil; + + let performanceEntries = []; + + function findMostRecent( + name, + type, + ) { + return performanceEntries + .slice() + .reverse() + .find((entry) => entry.name === name && entry.entryType === type); + } + + function convertMarkToTimestamp(mark) { + if (typeof mark === "string") { + const entry = findMostRecent(mark, "mark"); + if (!entry) { + throw new SyntaxError(`Cannot find mark: "${mark}".`); + } + return entry.startTime; + } + if (mark < 0) { + throw new TypeError("Mark cannot be negative."); + } + return mark; + } + + function filterByNameType( + name, + type, + ) { + return performanceEntries.filter( + (entry) => + (name ? entry.name === name : true) && + (type ? entry.entryType === type : true), + ); + } + + function now() { + const res = opNow(); + return res.seconds * 1e3 + res.subsecNanos / 1e6; + } + + class PerformanceEntry { + #name = ""; + #entryType = ""; + #startTime = 0; + #duration = 0; + + get name() { + return this.#name; + } + + get entryType() { + return this.#entryType; + } + + get startTime() { + return this.#startTime; + } + + get duration() { + return this.#duration; + } + + constructor( + name, + entryType, + startTime, + duration, + ) { + this.#name = name; + this.#entryType = entryType; + this.#startTime = startTime; + this.#duration = duration; + } + + toJSON() { + return { + name: this.#name, + entryType: this.#entryType, + startTime: this.#startTime, + duration: this.#duration, + }; + } + + [customInspect]() { + return `${this.constructor.name} { name: "${this.name}", entryType: "${this.entryType}", startTime: ${this.startTime}, duration: ${this.duration} }`; + } + } + + class PerformanceMark extends PerformanceEntry { + #detail = null; + + get detail() { + return this.#detail; + } + + get entryType() { + return "mark"; + } + + constructor( + name, + { detail = null, startTime = now() } = {}, + ) { + super(name, "mark", startTime, 0); + if (startTime < 0) { + throw new TypeError("startTime cannot be negative"); + } + this.#detail = cloneValue(detail); + } + + toJSON() { + return { + name: this.name, + entryType: this.entryType, + startTime: this.startTime, + duration: this.duration, + detail: this.detail, + }; + } + + [customInspect]() { + return this.detail + ? `${this.constructor.name} {\n detail: ${ + inspect(this.detail, { depth: 3 }) + },\n name: "${this.name}",\n entryType: "${this.entryType}",\n startTime: ${this.startTime},\n duration: ${this.duration}\n}` + : `${this.constructor.name} { detail: ${this.detail}, name: "${this.name}", entryType: "${this.entryType}", startTime: ${this.startTime}, duration: ${this.duration} }`; + } + } + + class PerformanceMeasure extends PerformanceEntry { + #detail = null; + + get detail() { + return this.#detail; + } + + get entryType() { + return "measure"; + } + + constructor( + name, + startTime, + duration, + detail = null, + ) { + super(name, "measure", startTime, duration); + this.#detail = cloneValue(detail); + } + + toJSON() { + return { + name: this.name, + entryType: this.entryType, + startTime: this.startTime, + duration: this.duration, + detail: this.detail, + }; + } + + [customInspect]() { + return this.detail + ? `${this.constructor.name} {\n detail: ${ + inspect(this.detail, { depth: 3 }) + },\n name: "${this.name}",\n entryType: "${this.entryType}",\n startTime: ${this.startTime},\n duration: ${this.duration}\n}` + : `${this.constructor.name} { detail: ${this.detail}, name: "${this.name}", entryType: "${this.entryType}", startTime: ${this.startTime}, duration: ${this.duration} }`; + } + } + + class Performance { + clearMarks(markName) { + if (markName == null) { + performanceEntries = performanceEntries.filter( + (entry) => entry.entryType !== "mark", + ); + } else { + performanceEntries = performanceEntries.filter( + (entry) => !(entry.name === markName && entry.entryType === "mark"), + ); + } + } + + clearMeasures(measureName) { + if (measureName == null) { + performanceEntries = performanceEntries.filter( + (entry) => entry.entryType !== "measure", + ); + } else { + performanceEntries = performanceEntries.filter( + (entry) => + !(entry.name === measureName && entry.entryType === "measure"), + ); + } + } + + getEntries() { + return filterByNameType(); + } + + getEntriesByName( + name, + type, + ) { + return filterByNameType(name, type); + } + + getEntriesByType(type) { + return filterByNameType(undefined, type); + } + + mark( + markName, + options = {}, + ) { + // 3.1.1.1 If the global object is a Window object and markName uses the + // same name as a read only attribute in the PerformanceTiming interface, + // throw a SyntaxError. - not implemented + const entry = new PerformanceMark(markName, options); + // 3.1.1.7 Queue entry - not implemented + performanceEntries.push(entry); + return entry; + } + + measure( + measureName, + startOrMeasureOptions = {}, + endMark, + ) { + if (startOrMeasureOptions && typeof startOrMeasureOptions === "object") { + if (endMark) { + throw new TypeError("Options cannot be passed with endMark."); + } + if ( + !("start" in startOrMeasureOptions) && + !("end" in startOrMeasureOptions) + ) { + throw new TypeError( + "A start or end mark must be supplied in options.", + ); + } + if ( + "start" in startOrMeasureOptions && + "duration" in startOrMeasureOptions && + "end" in startOrMeasureOptions + ) { + throw new TypeError( + "Cannot specify start, end, and duration together in options.", + ); + } + } + let endTime; + if (endMark) { + endTime = convertMarkToTimestamp(endMark); + } else if ( + typeof startOrMeasureOptions === "object" && + "end" in startOrMeasureOptions + ) { + endTime = convertMarkToTimestamp(startOrMeasureOptions.end); + } else if ( + typeof startOrMeasureOptions === "object" && + "start" in startOrMeasureOptions && + "duration" in startOrMeasureOptions + ) { + const start = convertMarkToTimestamp(startOrMeasureOptions.start); + const duration = convertMarkToTimestamp(startOrMeasureOptions.duration); + endTime = start + duration; + } else { + endTime = now(); + } + let startTime; + if ( + typeof startOrMeasureOptions === "object" && + "start" in startOrMeasureOptions + ) { + startTime = convertMarkToTimestamp(startOrMeasureOptions.start); + } else if ( + typeof startOrMeasureOptions === "object" && + "end" in startOrMeasureOptions && + "duration" in startOrMeasureOptions + ) { + const end = convertMarkToTimestamp(startOrMeasureOptions.end); + const duration = convertMarkToTimestamp(startOrMeasureOptions.duration); + startTime = end - duration; + } else if (typeof startOrMeasureOptions === "string") { + startTime = convertMarkToTimestamp(startOrMeasureOptions); + } else { + startTime = 0; + } + const entry = new PerformanceMeasure( + measureName, + startTime, + endTime - startTime, + typeof startOrMeasureOptions === "object" + ? startOrMeasureOptions.detail ?? null + : null, + ); + performanceEntries.push(entry); + return entry; + } + + now() { + return now(); + } + } + + window.__bootstrap.performance = { + PerformanceEntry, + PerformanceMark, + PerformanceMeasure, + Performance, + }; +})(this); diff --git a/cli/rt/40_permissions.js b/cli/rt/40_permissions.js new file mode 100644 index 000000000..4aebc94e7 --- /dev/null +++ b/cli/rt/40_permissions.js @@ -0,0 +1,49 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { sendSync } = window.__bootstrap.dispatchJson; + + function opQuery(desc) { + return sendSync("op_query_permission", desc).state; + } + + function opRevoke(desc) { + return sendSync("op_revoke_permission", desc).state; + } + + function opRequest(desc) { + return sendSync("op_request_permission", desc).state; + } + + class PermissionStatus { + constructor(state) { + this.state = state; + } + // TODO(kt3k): implement onchange handler + } + + class Permissions { + query(desc) { + const state = opQuery(desc); + return Promise.resolve(new PermissionStatus(state)); + } + + revoke(desc) { + const state = opRevoke(desc); + return Promise.resolve(new PermissionStatus(state)); + } + + request(desc) { + const state = opRequest(desc); + return Promise.resolve(new PermissionStatus(state)); + } + } + + const permissions = new Permissions(); + + window.__bootstrap.permissions = { + permissions, + Permissions, + PermissionStatus, + }; +})(this); diff --git a/cli/rt/40_plugins.js b/cli/rt/40_plugins.js new file mode 100644 index 000000000..dda28d6b2 --- /dev/null +++ b/cli/rt/40_plugins.js @@ -0,0 +1,13 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { sendSync } = window.__bootstrap.dispatchJson; + + function openPlugin(filename) { + return sendSync("op_open_plugin", { filename }); + } + + window.__bootstrap.plugins = { + openPlugin, + }; +})(this); diff --git a/cli/rt/40_process.js b/cli/rt/40_process.js new file mode 100644 index 000000000..97744a600 --- /dev/null +++ b/cli/rt/40_process.js @@ -0,0 +1,120 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { File } = window.__bootstrap.files; + const { close } = window.__bootstrap.resources; + const { readAll } = window.__bootstrap.buffer; + const { sendSync, sendAsync } = window.__bootstrap.dispatchJson; + const { assert } = window.__bootstrap.util; + + function opKill(pid, signo) { + sendSync("op_kill", { pid, signo }); + } + + function opRunStatus(rid) { + return sendAsync("op_run_status", { rid }); + } + + function opRun(request) { + assert(request.cmd.length > 0); + return sendSync("op_run", request); + } + + async function runStatus(rid) { + const res = await opRunStatus(rid); + + if (res.gotSignal) { + const signal = res.exitSignal; + return { success: false, code: 128 + signal, signal }; + } else if (res.exitCode != 0) { + return { success: false, code: res.exitCode }; + } else { + return { success: true, code: 0 }; + } + } + + class Process { + constructor(res) { + 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); + } + } + + status() { + return runStatus(this.rid); + } + + async output() { + if (!this.stdout) { + throw new TypeError("stdout was not piped"); + } + try { + return await readAll(this.stdout); + } finally { + this.stdout.close(); + } + } + + async stderrOutput() { + if (!this.stderr) { + throw new TypeError("stderr was not piped"); + } + try { + return await readAll(this.stderr); + } finally { + this.stderr.close(); + } + } + + close() { + close(this.rid); + } + + kill(signo) { + opKill(this.pid, signo); + } + } + + function isRid(arg) { + return !isNaN(arg); + } + + function run({ + cmd, + cwd = undefined, + env = {}, + stdout = "inherit", + stderr = "inherit", + stdin = "inherit", + }) { + const res = opRun({ + cmd: cmd.map(String), + cwd, + env: Object.entries(env), + stdin: isRid(stdin) ? "" : stdin, + stdout: isRid(stdout) ? "" : stdout, + stderr: isRid(stderr) ? "" : stderr, + stdinRid: isRid(stdin) ? stdin : 0, + stdoutRid: isRid(stdout) ? stdout : 0, + stderrRid: isRid(stderr) ? stderr : 0, + }); + return new Process(res); + } + + window.__bootstrap.process = { + run, + Process, + kill: opKill, + }; +})(this); diff --git a/cli/rt/40_read_file.js b/cli/rt/40_read_file.js new file mode 100644 index 000000000..9a36f335b --- /dev/null +++ b/cli/rt/40_read_file.js @@ -0,0 +1,43 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { open, openSync } = window.__bootstrap.files; + const { readAll, readAllSync } = window.__bootstrap.buffer; + + function readFileSync(path) { + const file = openSync(path); + const contents = readAllSync(file); + file.close(); + return contents; + } + + async function readFile(path) { + const file = await open(path); + const contents = await readAll(file); + file.close(); + return contents; + } + + function readTextFileSync(path) { + const file = openSync(path); + const contents = readAllSync(file); + file.close(); + const decoder = new TextDecoder(); + return decoder.decode(contents); + } + + async function readTextFile(path) { + const file = await open(path); + const contents = await readAll(file); + file.close(); + const decoder = new TextDecoder(); + return decoder.decode(contents); + } + + window.__bootstrap.readFile = { + readFile, + readFileSync, + readTextFileSync, + readTextFile, + }; +})(this); diff --git a/cli/rt/40_repl.js b/cli/rt/40_repl.js new file mode 100644 index 000000000..4966c45be --- /dev/null +++ b/cli/rt/40_repl.js @@ -0,0 +1,186 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const core = window.Deno.core; + const exit = window.__bootstrap.os.exit; + const version = window.__bootstrap.version.version; + const dispatchJson = window.__bootstrap.dispatchJson; + const close = window.__bootstrap.resources.close; + const inspectArgs = window.__bootstrap.console.inspectArgs; + + function opStartRepl(historyFile) { + return dispatchJson.sendSync("op_repl_start", { historyFile }); + } + + function opReadline(rid, prompt) { + return dispatchJson.sendAsync("op_repl_readline", { rid, prompt }); + } + + function replLog(...args) { + core.print(inspectArgs(args) + "\n"); + } + + function replError(...args) { + core.print(inspectArgs(args) + "\n", true); + } + + // 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) { + return recoverableErrorMessages.includes(e.message); + } + + // Returns `true` if `close()` is called in REPL. + // We should quit the REPL when this function returns `true`. + function isCloseCalled() { + return globalThis.closed; + } + + let lastEvalResult = undefined; + let lastThrownError = undefined; + + // Evaluate code. + // Returns true if code is consumed (no error/irrecoverable error). + // Returns false if error is recoverable + function evaluate(code) { + // each evalContext is a separate function body, and we want strict mode to + // work, so we should ensure that the code starts with "use strict" + const [result, errInfo] = core.evalContext(`"use strict";\n\n${code}`); + if (!errInfo) { + // when a function is eval'ed with just "use strict" sometimes the result + // is "use strict" which should be discarded + lastEvalResult = typeof result === "string" && result === "use strict" + ? undefined + : result; + if (!isCloseCalled()) { + replLog(lastEvalResult); + } + } 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 = core.formatError(errInfo.thrown); + replError(formattedError); + } else { + replError("Thrown:", errInfo.thrown); + } + } + return true; + } + + async function replLoop() { + const { console } = globalThis; + + const historyFile = "deno_history.txt"; + const rid = opStartRepl(historyFile); + + const quitRepl = (exitCode) => { + // Special handling in case user calls deno.close(3). + try { + close(rid); // close signals Drop on REPL and saves history. + } catch {} + exit(exitCode); + }; + + // Configure globalThis._ to give the last evaluation result. + Object.defineProperty(globalThis, "_", { + configurable: true, + get: () => lastEvalResult, + set: (value) => { + Object.defineProperty(globalThis, "_", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + console.log("Last evaluation result is no longer saved to _."); + }, + }); + + // Configure globalThis._error to give the last thrown error. + Object.defineProperty(globalThis, "_error", { + configurable: true, + get: () => lastThrownError, + set: (value) => { + Object.defineProperty(globalThis, "_error", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + console.log("Last thrown error is no longer saved to _error."); + }, + }); + + replLog(`Deno ${version.deno}`); + replLog("exit using ctrl+d or close()"); + + while (true) { + if (isCloseCalled()) { + quitRepl(0); + } + + let code = ""; + // Top level read + try { + code = await opReadline(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 = core.formatError(err); + replError(formattedError); + } + // Quit REPL anyways. + quitRepl(1); + } + } + // Start continued read + while (!evaluate(code)) { + code += "\n"; + try { + code += await opReadline(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 = core.formatError(err); + replError(formattedError); + quitRepl(1); + } + } + } + } + } + + window.__bootstrap.repl = { + replLoop, + }; +})(this); diff --git a/cli/rt/40_signals.js b/cli/rt/40_signals.js new file mode 100644 index 000000000..ab060598f --- /dev/null +++ b/cli/rt/40_signals.js @@ -0,0 +1,256 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { sendSync, sendAsync } = window.__bootstrap.dispatchJson; + const { build } = window.__bootstrap.build; + + function bindSignal(signo) { + return sendSync("op_signal_bind", { signo }); + } + + function pollSignal(rid) { + return sendAsync("op_signal_poll", { rid }); + } + + function unbindSignal(rid) { + sendSync("op_signal_unbind", { rid }); + } + + // From `kill -l` + const LinuxSignal = { + 1: "SIGHUP", + 2: "SIGINT", + 3: "SIGQUIT", + 4: "SIGILL", + 5: "SIGTRAP", + 6: "SIGABRT", + 7: "SIGBUS", + 8: "SIGFPE", + 9: "SIGKILL", + 10: "SIGUSR1", + 11: "SIGSEGV", + 12: "SIGUSR2", + 13: "SIGPIPE", + 14: "SIGALRM", + 15: "SIGTERM", + 16: "SIGSTKFLT", + 17: "SIGCHLD", + 18: "SIGCONT", + 19: "SIGSTOP", + 20: "SIGTSTP", + 21: "SIGTTIN", + 22: "SIGTTOU", + 23: "SIGURG", + 24: "SIGXCPU", + 25: "SIGXFSZ", + 26: "SIGVTALRM", + 27: "SIGPROF", + 28: "SIGWINCH", + 29: "SIGIO", + 30: "SIGPWR", + 31: "SIGSYS", + 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` + const MacOSSignal = { + 1: "SIGHUP", + 2: "SIGINT", + 3: "SIGQUIT", + 4: "SIGILL", + 5: "SIGTRAP", + 6: "SIGABRT", + 7: "SIGEMT", + 8: "SIGFPE", + 9: "SIGKILL", + 10: "SIGBUS", + 11: "SIGSEGV", + 12: "SIGSYS", + 13: "SIGPIPE", + 14: "SIGALRM", + 15: "SIGTERM", + 16: "SIGURG", + 17: "SIGSTOP", + 18: "SIGTSTP", + 19: "SIGCONT", + 20: "SIGCHLD", + 21: "SIGTTIN", + 22: "SIGTTOU", + 23: "SIGIO", + 24: "SIGXCPU", + 25: "SIGXFSZ", + 26: "SIGVTALRM", + 27: "SIGPROF", + 28: "SIGWINCH", + 29: "SIGINFO", + 30: "SIGUSR1", + 31: "SIGUSR2", + 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, + }; + + const Signal = {}; + + function setSignals() { + if (build.os === "darwin") { + Object.assign(Signal, MacOSSignal); + } else { + Object.assign(Signal, LinuxSignal); + } + } + + function signal(signo) { + if (build.os === "windows") { + throw new Error("not implemented!"); + } + return new SignalStream(signo); + } + + const signals = { + alarm() { + return signal(Signal.SIGALRM); + }, + child() { + return signal(Signal.SIGCHLD); + }, + hungup() { + return signal(Signal.SIGHUP); + }, + interrupt() { + return signal(Signal.SIGINT); + }, + io() { + return signal(Signal.SIGIO); + }, + pipe() { + return signal(Signal.SIGPIPE); + }, + quit() { + return signal(Signal.SIGQUIT); + }, + terminate() { + return signal(Signal.SIGTERM); + }, + userDefined1() { + return signal(Signal.SIGUSR1); + }, + userDefined2() { + return signal(Signal.SIGUSR2); + }, + windowChange() { + return signal(Signal.SIGWINCH); + }, + }; + + class SignalStream { + #disposed = false; + #pollingPromise = Promise.resolve(false); + #rid = 0; + + constructor(signo) { + this.#rid = bindSignal(signo).rid; + this.#loop(); + } + + #pollSignal = async () => { + const res = await pollSignal(this.#rid); + return res.done; + }; + + #loop = async () => { + do { + this.#pollingPromise = this.#pollSignal(); + } while (!(await this.#pollingPromise) && !this.#disposed); + }; + + then( + f, + g, + ) { + return this.#pollingPromise.then(() => {}).then(f, g); + } + + async next() { + return { done: await this.#pollingPromise, value: undefined }; + } + + [Symbol.asyncIterator]() { + return this; + } + + dispose() { + if (this.#disposed) { + throw new Error("The stream has already been disposed."); + } + this.#disposed = true; + unbindSignal(this.#rid); + } + } + + window.__bootstrap.signals = { + signal, + signals, + Signal, + SignalStream, + setSignals, + }; +})(this); diff --git a/cli/rt/40_testing.js b/cli/rt/40_testing.js new file mode 100644 index 000000000..128f8ca93 --- /dev/null +++ b/cli/rt/40_testing.js @@ -0,0 +1,345 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { gray, green, italic, red, yellow } = window.__bootstrap.colors; + const { exit } = window.__bootstrap.os; + const { Console, inspectArgs } = window.__bootstrap.console; + const { stdout } = window.__bootstrap.files; + const { exposeForTest } = window.__bootstrap.internals; + const { metrics } = window.__bootstrap.metrics; + const { resources } = window.__bootstrap.resources; + const { assert } = window.__bootstrap.util; + + const disabledConsole = new Console(() => {}); + + function delay(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } + + function formatDuration(time = 0) { + const timeStr = `(${time}ms)`; + return gray(italic(timeStr)); + } + + // Wrap test function in additional assertion that makes sure + // the test case does not leak async "ops" - ie. number of async + // completed ops after the test is the same as number of dispatched + // ops. Note that "unref" ops are ignored since in nature that are + // optional. + function assertOps(fn) { + return async function asyncOpSanitizer() { + const pre = metrics(); + await fn(); + // Defer until next event loop turn - that way timeouts and intervals + // cleared can actually be removed from resource table, otherwise + // false positives may occur (https://github.com/denoland/deno/issues/4591) + await delay(0); + const post = metrics(); + // We're checking diff because one might spawn HTTP server in the background + // that will be a pending async op before test starts. + const dispatchedDiff = post.opsDispatchedAsync - pre.opsDispatchedAsync; + const completedDiff = post.opsCompletedAsync - pre.opsCompletedAsync; + assert( + dispatchedDiff === completedDiff, + `Test case is leaking async ops. +Before: + - dispatched: ${pre.opsDispatchedAsync} + - completed: ${pre.opsCompletedAsync} +After: + - dispatched: ${post.opsDispatchedAsync} + - completed: ${post.opsCompletedAsync} + +Make sure to await all promises returned from Deno APIs before +finishing test case.`, + ); + }; + } + + // Wrap test function in additional assertion that makes sure + // the test case does not "leak" resources - ie. resource table after + // the test has exactly the same contents as before the test. + function assertResources( + fn, + ) { + return async function resourceSanitizer() { + const pre = resources(); + await fn(); + const post = resources(); + + const preStr = JSON.stringify(pre, null, 2); + const postStr = JSON.stringify(post, null, 2); + const msg = `Test case is leaking resources. +Before: ${preStr} +After: ${postStr} + +Make sure to close all open resource handles returned from Deno APIs before +finishing test case.`; + assert(preStr === postStr, msg); + }; + } + + const TEST_REGISTRY = []; + + // Main test function provided by Deno, as you can see it merely + // creates a new object with "name" and "fn" fields. + function test( + t, + fn, + ) { + let testDef; + const defaults = { + ignore: false, + only: false, + sanitizeOps: true, + sanitizeResources: true, + }; + + if (typeof t === "string") { + if (!fn || typeof fn != "function") { + throw new TypeError("Missing test function"); + } + if (!t) { + throw new TypeError("The test name can't be empty"); + } + testDef = { fn: fn, name: t, ...defaults }; + } else { + if (!t.fn) { + throw new TypeError("Missing test function"); + } + if (!t.name) { + throw new TypeError("The test name can't be empty"); + } + testDef = { ...defaults, ...t }; + } + + if (testDef.sanitizeOps) { + testDef.fn = assertOps(testDef.fn); + } + + if (testDef.sanitizeResources) { + testDef.fn = assertResources(testDef.fn); + } + + TEST_REGISTRY.push(testDef); + } + + const encoder = new TextEncoder(); + + function log(msg, noNewLine = false) { + if (!noNewLine) { + msg += "\n"; + } + + // Using `stdout` here because it doesn't force new lines + // compared to `console.log`; `core.print` on the other hand + // is line-buffered and doesn't output message without newline + stdout.writeSync(encoder.encode(msg)); + } + + function reportToConsole(message) { + const redFailed = red("FAILED"); + const greenOk = green("ok"); + const yellowIgnored = yellow("ignored"); + if (message.start != null) { + log(`running ${message.start.tests.length} tests`); + } else if (message.testStart != null) { + const { name } = message.testStart; + + log(`test ${name} ... `, true); + return; + } else if (message.testEnd != null) { + switch (message.testEnd.status) { + case "passed": + log(`${greenOk} ${formatDuration(message.testEnd.duration)}`); + break; + case "failed": + log(`${redFailed} ${formatDuration(message.testEnd.duration)}`); + break; + case "ignored": + log(`${yellowIgnored} ${formatDuration(message.testEnd.duration)}`); + break; + } + } else if (message.end != null) { + const failures = message.end.results.filter((m) => m.error != null); + if (failures.length > 0) { + log(`\nfailures:\n`); + + for (const { name, error } of failures) { + log(name); + log(inspectArgs([error])); + log(""); + } + + log(`failures:\n`); + + for (const { name } of failures) { + log(`\t${name}`); + } + } + log( + `\ntest result: ${message.end.failed ? redFailed : greenOk}. ` + + `${message.end.passed} passed; ${message.end.failed} failed; ` + + `${message.end.ignored} ignored; ${message.end.measured} measured; ` + + `${message.end.filtered} filtered out ` + + `${formatDuration(message.end.duration)}\n`, + ); + + if (message.end.usedOnly && message.end.failed == 0) { + log(`${redFailed} because the "only" option was used\n`); + } + } + } + + exposeForTest("reportToConsole", reportToConsole); + + // TODO: already implements AsyncGenerator<RunTestsMessage>, but add as "implements to class" + // TODO: implements PromiseLike<RunTestsEndResult> + class TestRunner { + #usedOnly = false; + + constructor( + tests, + filterFn, + failFast, + ) { + this.stats = { + filtered: 0, + ignored: 0, + measured: 0, + passed: 0, + failed: 0, + }; + this.filterFn = filterFn; + this.failFast = failFast; + const onlyTests = tests.filter(({ only }) => only); + this.#usedOnly = onlyTests.length > 0; + const unfilteredTests = this.#usedOnly ? onlyTests : tests; + this.testsToRun = unfilteredTests.filter(filterFn); + this.stats.filtered = unfilteredTests.length - this.testsToRun.length; + } + + async *[Symbol.asyncIterator]() { + yield { start: { tests: this.testsToRun } }; + + const results = []; + const suiteStart = +new Date(); + for (const test of this.testsToRun) { + const endMessage = { + name: test.name, + duration: 0, + }; + yield { testStart: { ...test } }; + if (test.ignore) { + endMessage.status = "ignored"; + this.stats.ignored++; + } else { + const start = +new Date(); + try { + await test.fn(); + endMessage.status = "passed"; + this.stats.passed++; + } catch (err) { + endMessage.status = "failed"; + endMessage.error = err; + this.stats.failed++; + } + endMessage.duration = +new Date() - start; + } + results.push(endMessage); + yield { testEnd: endMessage }; + if (this.failFast && endMessage.error != null) { + break; + } + } + + const duration = +new Date() - suiteStart; + + yield { + end: { ...this.stats, usedOnly: this.#usedOnly, duration, results }, + }; + } + } + + function createFilterFn( + filter, + skip, + ) { + return (def) => { + let passes = true; + + if (filter) { + if (filter instanceof RegExp) { + passes = passes && filter.test(def.name); + } else if (filter.startsWith("/") && filter.endsWith("/")) { + const filterAsRegex = new RegExp(filter.slice(1, filter.length - 1)); + passes = passes && filterAsRegex.test(def.name); + } else { + passes = passes && def.name.includes(filter); + } + } + + if (skip) { + if (skip instanceof RegExp) { + passes = passes && !skip.test(def.name); + } else { + passes = passes && !def.name.includes(skip); + } + } + + return passes; + }; + } + + exposeForTest("createFilterFn", createFilterFn); + + async function runTests({ + exitOnFail = true, + failFast = false, + filter = undefined, + skip = undefined, + disableLog = false, + reportToConsole: reportToConsole_ = true, + onMessage = undefined, + } = {}) { + const filterFn = createFilterFn(filter, skip); + const testRunner = new TestRunner(TEST_REGISTRY, filterFn, failFast); + + const originalConsole = globalThis.console; + + if (disableLog) { + globalThis.console = disabledConsole; + } + + let endMsg; + + for await (const message of testRunner) { + if (onMessage != null) { + await onMessage(message); + } + if (reportToConsole_) { + reportToConsole(message); + } + if (message.end != null) { + endMsg = message.end; + } + } + + if (disableLog) { + globalThis.console = originalConsole; + } + + if ((endMsg.failed > 0 || endMsg?.usedOnly) && exitOnFail) { + exit(1); + } + + return endMsg; + } + + exposeForTest("runTests", runTests); + + window.__bootstrap.testing = { + test, + }; +})(this); diff --git a/cli/rt/40_tls.js b/cli/rt/40_tls.js new file mode 100644 index 000000000..f4ae55112 --- /dev/null +++ b/cli/rt/40_tls.js @@ -0,0 +1,82 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { Listener, Conn } = window.__bootstrap.net; + const { sendAsync, sendSync } = window.__bootstrap.dispatchJson; + + function opConnectTls( + args, + ) { + return sendAsync("op_connect_tls", args); + } + + function opAcceptTLS(rid) { + return sendAsync("op_accept_tls", { rid }); + } + + function opListenTls(args) { + return sendSync("op_listen_tls", args); + } + + function opStartTls(args) { + return sendAsync("op_start_tls", args); + } + + async function connectTls({ + port, + hostname = "127.0.0.1", + transport = "tcp", + certFile = undefined, + }) { + const res = await opConnectTls({ + port, + hostname, + transport, + certFile, + }); + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + + class TLSListener extends Listener { + async accept() { + const res = await opAcceptTLS(this.rid); + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + } + + function listenTls({ + port, + certFile, + keyFile, + hostname = "0.0.0.0", + transport = "tcp", + }) { + const res = opListenTls({ + port, + certFile, + keyFile, + hostname, + transport, + }); + return new TLSListener(res.rid, res.localAddr); + } + + async function startTls( + conn, + { hostname = "127.0.0.1", certFile } = {}, + ) { + const res = await opStartTls({ + rid: conn.rid, + hostname, + certFile, + }); + return new Conn(res.rid, res.remoteAddr, res.localAddr); + } + + window.__bootstrap.tls = { + startTls, + listenTls, + connectTls, + TLSListener, + }; +})(this); diff --git a/cli/rt/40_tty.js b/cli/rt/40_tty.js new file mode 100644 index 000000000..3bab4f321 --- /dev/null +++ b/cli/rt/40_tty.js @@ -0,0 +1,23 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +((window) => { + const { sendSync } = window.__bootstrap.dispatchJson; + + function consoleSize(rid) { + return sendSync("op_console_size", { rid }); + } + + function isatty(rid) { + return sendSync("op_isatty", { rid }); + } + + function setRaw(rid, mode) { + sendSync("op_set_raw", { rid, mode }); + } + + window.__bootstrap.tty = { + consoleSize, + isatty, + setRaw, + }; +})(this); diff --git a/cli/rt/40_write_file.js b/cli/rt/40_write_file.js new file mode 100644 index 000000000..2f54aa1cf --- /dev/null +++ b/cli/rt/40_write_file.js @@ -0,0 +1,92 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +((window) => { + const { stat, statSync, chmod, chmodSync } = window.__bootstrap.fs; + const { open, openSync } = window.__bootstrap.files; + const { writeAll, writeAllSync } = window.__bootstrap.buffer; + const { build } = window.__bootstrap.build; + + function writeFileSync( + path, + data, + options = {}, + ) { + if (options.create !== undefined) { + const create = !!options.create; + if (!create) { + // verify that file exists + statSync(path); + } + } + + const openOptions = !!options.append + ? { write: true, create: true, append: true } + : { write: true, create: true, truncate: true }; + const file = openSync(path, openOptions); + + if ( + options.mode !== undefined && + options.mode !== null && + build.os !== "windows" + ) { + chmodSync(path, options.mode); + } + + writeAllSync(file, data); + file.close(); + } + + async function writeFile( + path, + data, + options = {}, + ) { + if (options.create !== undefined) { + const create = !!options.create; + if (!create) { + // verify that file exists + await stat(path); + } + } + + const openOptions = !!options.append + ? { write: true, create: true, append: true } + : { write: true, create: true, truncate: true }; + const file = await open(path, openOptions); + + if ( + options.mode !== undefined && + options.mode !== null && + build.os !== "windows" + ) { + await chmod(path, options.mode); + } + + await writeAll(file, data); + file.close(); + } + + function writeTextFileSync( + path, + data, + options = {}, + ) { + const encoder = new TextEncoder(); + return writeFileSync(path, encoder.encode(data), options); + } + + function writeTextFile( + path, + data, + options = {}, + ) { + const encoder = new TextEncoder(); + return writeFile(path, encoder.encode(data), options); + } + + window.__bootstrap.writeFile = { + writeTextFile, + writeTextFileSync, + writeFile, + writeFileSync, + }; +})(this); diff --git a/cli/rt/90_deno_ns.js b/cli/rt/90_deno_ns.js new file mode 100644 index 000000000..714326e93 --- /dev/null +++ b/cli/rt/90_deno_ns.js @@ -0,0 +1,89 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This module exports stable Deno APIs. + +((window) => { + window.__bootstrap.denoNs = { + test: window.__bootstrap.testing.test, + metrics: window.__bootstrap.metrics.metrics, + Process: window.__bootstrap.process.Process, + run: window.__bootstrap.process.run, + isatty: window.__bootstrap.tty.isatty, + writeFileSync: window.__bootstrap.writeFile.writeFileSync, + writeFile: window.__bootstrap.writeFile.writeFile, + writeTextFileSync: window.__bootstrap.writeFile.writeTextFileSync, + writeTextFile: window.__bootstrap.writeFile.writeTextFile, + readTextFile: window.__bootstrap.readFile.readTextFile, + readTextFileSync: window.__bootstrap.readFile.readTextFileSync, + readFile: window.__bootstrap.readFile.readFile, + readFileSync: window.__bootstrap.readFile.readFileSync, + watchFs: window.__bootstrap.fsEvents.watchFs, + chmodSync: window.__bootstrap.fs.chmodSync, + chmod: window.__bootstrap.fs.chmod, + chown: window.__bootstrap.fs.chown, + chownSync: window.__bootstrap.fs.chownSync, + copyFileSync: window.__bootstrap.fs.copyFileSync, + cwd: window.__bootstrap.fs.cwd, + makeTempDirSync: window.__bootstrap.fs.makeTempDirSync, + makeTempDir: window.__bootstrap.fs.makeTempDir, + makeTempFileSync: window.__bootstrap.fs.makeTempFileSync, + makeTempFile: window.__bootstrap.fs.makeTempFile, + mkdirSync: window.__bootstrap.fs.mkdirSync, + mkdir: window.__bootstrap.fs.mkdir, + chdir: window.__bootstrap.fs.chdir, + copyFile: window.__bootstrap.fs.copyFile, + readDirSync: window.__bootstrap.fs.readDirSync, + readDir: window.__bootstrap.fs.readDir, + readLinkSync: window.__bootstrap.fs.readLinkSync, + readLink: window.__bootstrap.fs.readLink, + realPathSync: window.__bootstrap.fs.realPathSync, + realPath: window.__bootstrap.fs.realPath, + removeSync: window.__bootstrap.fs.removeSync, + remove: window.__bootstrap.fs.remove, + renameSync: window.__bootstrap.fs.renameSync, + rename: window.__bootstrap.fs.rename, + version: window.__bootstrap.version.version, + build: window.__bootstrap.build.build, + statSync: window.__bootstrap.fs.statSync, + lstatSync: window.__bootstrap.fs.lstatSync, + stat: window.__bootstrap.fs.stat, + lstat: window.__bootstrap.fs.lstat, + truncateSync: window.__bootstrap.fs.truncateSync, + truncate: window.__bootstrap.fs.truncate, + errors: window.__bootstrap.errors.errors, + customInspect: window.__bootstrap.console.customInspect, + inspect: window.__bootstrap.console.inspect, + env: window.__bootstrap.os.env, + exit: window.__bootstrap.os.exit, + execPath: window.__bootstrap.os.execPath, + resources: window.__bootstrap.resources.resources, + close: window.__bootstrap.resources.close, + Buffer: window.__bootstrap.buffer.Buffer, + readAll: window.__bootstrap.buffer.readAll, + readAllSync: window.__bootstrap.buffer.readAllSync, + writeAll: window.__bootstrap.buffer.writeAll, + writeAllSync: window.__bootstrap.buffer.writeAllSync, + copy: window.__bootstrap.io.copy, + iter: window.__bootstrap.io.iter, + iterSync: window.__bootstrap.io.iterSync, + SeekMode: window.__bootstrap.io.SeekMode, + read: window.__bootstrap.io.read, + readSync: window.__bootstrap.io.readSync, + write: window.__bootstrap.io.write, + writeSync: window.__bootstrap.io.writeSync, + File: window.__bootstrap.files.File, + open: window.__bootstrap.files.open, + openSync: window.__bootstrap.files.openSync, + create: window.__bootstrap.files.create, + createSync: window.__bootstrap.files.createSync, + stdin: window.__bootstrap.files.stdin, + stdout: window.__bootstrap.files.stdout, + stderr: window.__bootstrap.files.stderr, + seek: window.__bootstrap.files.seek, + seekSync: window.__bootstrap.files.seekSync, + connect: window.__bootstrap.net.connect, + listen: window.__bootstrap.net.listen, + connectTls: window.__bootstrap.tls.connectTls, + listenTls: window.__bootstrap.tls.listenTls, + }; +})(this); diff --git a/cli/rt/90_deno_ns_unstable.js b/cli/rt/90_deno_ns_unstable.js new file mode 100644 index 000000000..722effeaf --- /dev/null +++ b/cli/rt/90_deno_ns_unstable.js @@ -0,0 +1,48 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This module exports unstable Deno APIs. +((window) => { + window.__bootstrap.denoNsUnstable = { + signal: window.__bootstrap.signals.signal, + signals: window.__bootstrap.signals.signals, + Signal: window.__bootstrap.signals.Signal, + SignalStream: window.__bootstrap.signals.SignalStream, + transpileOnly: window.__bootstrap.compilerApi.transpileOnly, + compile: window.__bootstrap.compilerApi.compile, + bundle: window.__bootstrap.compilerApi.bundle, + permissions: window.__bootstrap.permissions.permissions, + Permissions: window.__bootstrap.permissions.Permissions, + PermissionStatus: window.__bootstrap.permissions.PermissionStatus, + openPlugin: window.__bootstrap.plugins.openPlugin, + kill: window.__bootstrap.process.kill, + setRaw: window.__bootstrap.tty.setRaw, + consoleSize: window.__bootstrap.tty.consoleSize, + DiagnosticCategory: window.__bootstrap.diagnostics.DiagnosticCategory, + loadavg: window.__bootstrap.os.loadavg, + hostname: window.__bootstrap.os.hostname, + osRelease: window.__bootstrap.os.osRelease, + applySourceMap: window.__bootstrap.errorStack.opApplySourceMap, + formatDiagnostics: window.__bootstrap.errorStack.opFormatDiagnostics, + shutdown: window.__bootstrap.net.shutdown, + ShutdownMode: window.__bootstrap.net.ShutdownMode, + listen: window.__bootstrap.netUnstable.listen, + connect: window.__bootstrap.netUnstable.connect, + listenDatagram: window.__bootstrap.netUnstable.listenDatagram, + startTls: window.__bootstrap.tls.startTls, + fstatSync: window.__bootstrap.fs.fstatSync, + fstat: window.__bootstrap.fs.fstat, + ftruncateSync: window.__bootstrap.fs.ftruncateSync, + ftruncate: window.__bootstrap.fs.ftruncate, + umask: window.__bootstrap.fs.umask, + link: window.__bootstrap.fs.link, + linkSync: window.__bootstrap.fs.linkSync, + utime: window.__bootstrap.fs.utime, + utimeSync: window.__bootstrap.fs.utimeSync, + symlink: window.__bootstrap.fs.symlink, + symlinkSync: window.__bootstrap.fs.symlinkSync, + fdatasyncSync: window.__bootstrap.fs.fdatasyncSync, + fdatasync: window.__bootstrap.fs.fdatasync, + fsyncSync: window.__bootstrap.fs.fsyncSync, + fsync: window.__bootstrap.fs.fsync, + }; +})(this); diff --git a/cli/rt/99_main.js b/cli/rt/99_main.js new file mode 100644 index 000000000..325881b5a --- /dev/null +++ b/cli/rt/99_main.js @@ -0,0 +1,388 @@ +// Removes the `__proto__` for security reasons. This intentionally makes +// Deno non compliant with ECMA-262 Annex B.2.2.1 +// +// eslint-disable-next-line @typescript-eslint/no-explicit-any +delete Object.prototype.__proto__; + +((window) => { + const core = Deno.core; + const util = window.__bootstrap.util; + const eventTarget = window.__bootstrap.eventTarget; + const dispatchJson = window.__bootstrap.dispatchJson; + const dispatchMinimal = window.__bootstrap.dispatchMinimal; + const build = window.__bootstrap.build; + const version = window.__bootstrap.version; + const errorStack = window.__bootstrap.errorStack; + const os = window.__bootstrap.os; + const timers = window.__bootstrap.timers; + const replLoop = window.__bootstrap.repl.replLoop; + const Console = window.__bootstrap.console.Console; + const worker = window.__bootstrap.worker; + const signals = window.__bootstrap.signals; + const { internalSymbol, internalObject } = window.__bootstrap.internals; + const abortSignal = window.__bootstrap.abortSignal; + const performance = window.__bootstrap.performance; + const crypto = window.__bootstrap.crypto; + const url = window.__bootstrap.url; + const headers = window.__bootstrap.headers; + const queuingStrategy = window.__bootstrap.queuingStrategy; + const streams = window.__bootstrap.streams; + const blob = window.__bootstrap.blob; + const domFile = window.__bootstrap.domFile; + const formData = window.__bootstrap.formData; + const request = window.__bootstrap.request; + const fetch = window.__bootstrap.fetch; + const denoNs = window.__bootstrap.denoNs; + const denoNsUnstable = window.__bootstrap.denoNsUnstable; + + let windowIsClosing = false; + + function windowClose() { + if (!windowIsClosing) { + windowIsClosing = true; + // Push a macrotask to exit after a promise resolve. + // This is not perfect, but should be fine for first pass. + Promise.resolve().then(() => + timers.setTimeout.call( + null, + () => { + // This should be fine, since only Window/MainWorker has .close() + os.exit(0); + }, + 0, + ) + ); + } + } + + const encoder = new TextEncoder(); + + function workerClose() { + if (isClosing) { + return; + } + + isClosing = true; + opCloseWorker(); + } + + // TODO(bartlomieju): remove these funtions + // Stuff for workers + const onmessage = () => {}; + const onerror = () => {}; + + function postMessage(data) { + const dataJson = JSON.stringify(data); + const dataIntArray = encoder.encode(dataJson); + opPostMessage(dataIntArray); + } + + let isClosing = false; + async function workerMessageRecvCallback(data) { + const msgEvent = new worker.MessageEvent("message", { + cancelable: false, + data, + }); + + try { + if (globalThis["onmessage"]) { + const result = globalThis.onmessage(msgEvent); + if (result && "then" in result) { + await result; + } + } + globalThis.dispatchEvent(msgEvent); + } catch (e) { + let handled = false; + + const errorEvent = new ErrorEvent("error", { + cancelable: true, + message: e.message, + lineno: e.lineNumber ? e.lineNumber + 1 : undefined, + colno: e.columnNumber ? e.columnNumber + 1 : undefined, + filename: e.fileName, + error: null, + }); + + if (globalThis["onerror"]) { + const ret = globalThis.onerror( + e.message, + e.fileName, + e.lineNumber, + e.columnNumber, + e, + ); + handled = ret === true; + } + + globalThis.dispatchEvent(errorEvent); + if (errorEvent.defaultPrevented) { + handled = true; + } + + if (!handled) { + throw e; + } + } + } + + function opPostMessage(data) { + dispatchJson.sendSync("op_worker_post_message", {}, data); + } + + function opCloseWorker() { + dispatchJson.sendSync("op_worker_close"); + } + + function opStart() { + return dispatchJson.sendSync("op_start"); + } + + function opMainModule() { + return dispatchJson.sendSync("op_main_module"); + } + + function getAsyncHandler(opName) { + switch (opName) { + case "op_write": + case "op_read": + return dispatchMinimal.asyncMsgFromRust; + default: + return dispatchJson.asyncMsgFromRust; + } + } + + // TODO(bartlomieju): temporary solution, must be fixed when moving + // dispatches to separate crates + function initOps() { + const opsMap = core.ops(); + for (const [name, opId] of Object.entries(opsMap)) { + core.setAsyncHandler(opId, getAsyncHandler(name)); + } + core.setMacrotaskCallback(timers.handleTimerMacrotask); + } + + function runtimeStart(source) { + initOps(); + // 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 = opStart(); + version.setVersions(s.denoVersion, s.v8Version, s.tsVersion); + build.setBuildInfo(s.target); + util.setLogDebug(s.debugFlag, source); + errorStack.setPrepareStackTrace(Error); + return s; + } + + // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope + const windowOrWorkerGlobalScopeMethods = { + atob: util.writable(atob), + btoa: util.writable(btoa), + clearInterval: util.writable(timers.clearInterval), + clearTimeout: util.writable(timers.clearTimeout), + fetch: util.writable(fetch.fetch), + // queueMicrotask is bound in Rust + setInterval: util.writable(timers.setInterval), + setTimeout: util.writable(timers.setTimeout), + }; + + // Other properties shared between WindowScope and WorkerGlobalScope + const windowOrWorkerGlobalScopeProperties = { + console: util.writable(new Console(core.print)), + AbortController: util.nonEnumerable(abortSignal.AbortController), + AbortSignal: util.nonEnumerable(abortSignal.AbortSignal), + Blob: util.nonEnumerable(blob.Blob), + ByteLengthQueuingStrategy: util.nonEnumerable( + queuingStrategy.ByteLengthQueuingStrategy, + ), + CountQueuingStrategy: util.nonEnumerable( + queuingStrategy.CountQueuingStrategy, + ), + crypto: util.readOnly(crypto), + File: util.nonEnumerable(domFile.DomFile), + CustomEvent: util.nonEnumerable(CustomEvent), + DOMException: util.nonEnumerable(DOMException), + ErrorEvent: util.nonEnumerable(ErrorEvent), + Event: util.nonEnumerable(Event), + EventTarget: util.nonEnumerable(EventTarget), + Headers: util.nonEnumerable(headers.Headers), + FormData: util.nonEnumerable(formData.FormData), + ReadableStream: util.nonEnumerable(streams.ReadableStream), + Request: util.nonEnumerable(request.Request), + Response: util.nonEnumerable(fetch.Response), + performance: util.writable(new performance.Performance()), + Performance: util.nonEnumerable(performance.Performance), + PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry), + PerformanceMark: util.nonEnumerable(performance.PerformanceMark), + PerformanceMeasure: util.nonEnumerable(performance.PerformanceMeasure), + TextDecoder: util.nonEnumerable(TextDecoder), + TextEncoder: util.nonEnumerable(TextEncoder), + TransformStream: util.nonEnumerable(streams.TransformStream), + URL: util.nonEnumerable(url.URL), + URLSearchParams: util.nonEnumerable(url.URLSearchParams), + Worker: util.nonEnumerable(worker.Worker), + WritableStream: util.nonEnumerable(streams.WritableStream), + }; + + const eventTargetProperties = { + addEventListener: util.readOnly( + EventTarget.prototype.addEventListener, + ), + dispatchEvent: util.readOnly(EventTarget.prototype.dispatchEvent), + removeEventListener: util.readOnly( + EventTarget.prototype.removeEventListener, + ), + }; + + const mainRuntimeGlobalProperties = { + window: util.readOnly(globalThis), + self: util.readOnly(globalThis), + // TODO(bartlomieju): from MDN docs (https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope) + // it seems those two properties should be available to workers as well + onload: util.writable(null), + onunload: util.writable(null), + close: util.writable(windowClose), + closed: util.getterOnly(() => windowIsClosing), + }; + + const workerRuntimeGlobalProperties = { + self: util.readOnly(globalThis), + onmessage: util.writable(onmessage), + onerror: util.writable(onerror), + // TODO: should be readonly? + close: util.nonEnumerable(workerClose), + postMessage: util.writable(postMessage), + workerMessageRecvCallback: util.nonEnumerable(workerMessageRecvCallback), + }; + + let hasBootstrapped = false; + + function bootstrapMainRuntime() { + if (hasBootstrapped) { + throw new Error("Worker runtime already bootstrapped"); + } + // Remove bootstrapping methods from global scope + globalThis.__bootstrap = undefined; + globalThis.bootstrap = undefined; + util.log("bootstrapMainRuntime"); + hasBootstrapped = true; + Object.defineProperties(globalThis, windowOrWorkerGlobalScopeMethods); + Object.defineProperties(globalThis, windowOrWorkerGlobalScopeProperties); + Object.defineProperties(globalThis, eventTargetProperties); + Object.defineProperties(globalThis, mainRuntimeGlobalProperties); + eventTarget.setEventTargetData(globalThis); + // Registers the handler for window.onload function. + globalThis.addEventListener("load", (e) => { + const { onload } = globalThis; + if (typeof onload === "function") { + onload(e); + } + }); + // Registers the handler for window.onunload function. + globalThis.addEventListener("unload", (e) => { + const { onunload } = globalThis; + if (typeof onunload === "function") { + onunload(e); + } + }); + + const { args, cwd, noColor, pid, ppid, repl, unstableFlag } = + runtimeStart(); + + const finalDenoNs = { + core, + internal: internalSymbol, + [internalSymbol]: internalObject, + ...denoNs, + }; + Object.defineProperties(finalDenoNs, { + pid: util.readOnly(pid), + ppid: util.readOnly(ppid), + noColor: util.readOnly(noColor), + args: util.readOnly(Object.freeze(args)), + }); + + if (unstableFlag) { + Object.defineProperty( + finalDenoNs, + "mainModule", + util.getterOnly(opMainModule), + ); + Object.assign(finalDenoNs, denoNsUnstable); + } + + // Setup `Deno` global - we're actually overriding already + // existing global `Deno` with `Deno` namespace from "./deno.ts". + util.immutableDefine(globalThis, "Deno", finalDenoNs); + Object.freeze(globalThis.Deno); + Object.freeze(globalThis.Deno.core); + Object.freeze(globalThis.Deno.core.sharedQueue); + signals.setSignals(); + + util.log("cwd", cwd); + util.log("args", args); + + if (repl) { + replLoop(); + } + } + + function bootstrapWorkerRuntime(name, useDenoNamespace, internalName) { + if (hasBootstrapped) { + throw new Error("Worker runtime already bootstrapped"); + } + // Remove bootstrapping methods from global scope + globalThis.__bootstrap = undefined; + globalThis.bootstrap = undefined; + util.log("bootstrapWorkerRuntime"); + hasBootstrapped = true; + Object.defineProperties(globalThis, windowOrWorkerGlobalScopeMethods); + Object.defineProperties(globalThis, windowOrWorkerGlobalScopeProperties); + Object.defineProperties(globalThis, workerRuntimeGlobalProperties); + Object.defineProperties(globalThis, eventTargetProperties); + Object.defineProperties(globalThis, { name: util.readOnly(name) }); + eventTarget.setEventTargetData(globalThis); + const { unstableFlag, pid, noColor, args } = runtimeStart( + internalName ?? name, + ); + + const finalDenoNs = { + core, + internal: internalSymbol, + [internalSymbol]: internalObject, + ...denoNs, + }; + if (useDenoNamespace) { + if (unstableFlag) { + Object.assign(finalDenoNs, denoNsUnstable); + } + Object.defineProperties(finalDenoNs, { + pid: util.readOnly(pid), + noColor: util.readOnly(noColor), + args: util.readOnly(Object.freeze(args)), + }); + // Setup `Deno` global - we're actually overriding already + // existing global `Deno` with `Deno` namespace from "./deno.ts". + util.immutableDefine(globalThis, "Deno", finalDenoNs); + Object.freeze(globalThis.Deno); + Object.freeze(globalThis.Deno.core); + Object.freeze(globalThis.Deno.core.sharedQueue); + signals.setSignals(); + } else { + delete globalThis.Deno; + util.assert(globalThis.Deno === undefined); + } + } + + Object.defineProperties(globalThis, { + bootstrap: { + value: { + mainRuntime: bootstrapMainRuntime, + workerRuntime: bootstrapWorkerRuntime, + }, + configurable: true, + writable: true, + }, + }); +})(this); diff --git a/cli/rt/README.md b/cli/rt/README.md new file mode 100644 index 000000000..f73163df2 --- /dev/null +++ b/cli/rt/README.md @@ -0,0 +1,59 @@ +# Runtime JavaScript Code + +This directory contains Deno runtime code written in plain JavaScript. + +Each file is a plain, old **script**, not ES modules. The reason is that +snapshotting ES modules is much harder, especially if one needs to manipulate +global scope (like in case of Deno). + +Each file is prefixed with a number, telling in which order scripts should be +loaded into V8 isolate. This is temporary solution and we're striving not to +require specific order (though it's not 100% obvious if that's feasible). + +## Deno Web APIs + +This directory facilities Web APIs that are available in Deno. + +Please note, that some implementations might not be completely aligned with +specification. + +Some Web APIs are using ops under the hood, eg. `console`, `performance`. + +## Implemented Web APIs + +- [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob): for + representing opaque binary data +- [Console](https://developer.mozilla.org/en-US/docs/Web/API/Console): for + logging purposes +- [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent), + [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) + and + [EventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventListener): + to work with DOM events + - **Implementation notes:** There is no DOM hierarchy in Deno, so there is no + tree for Events to bubble/capture through. +- [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch), + [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), + [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response), + [Body](https://developer.mozilla.org/en-US/docs/Web/API/Body) and + [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers): modern + Promise-based HTTP Request API +- [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData): access + to a `multipart/form-data` serialization +- [Performance](https://developer.mozilla.org/en-US/docs/Web/API/Performance): + retrieving current time with a high precision +- [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout), + [setInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval), + [clearTimeout](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearTimeout): + scheduling callbacks in future and + [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval) +- [Stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) for + creating, composing, and consuming streams of data +- [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) and + [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams): + to construct and parse URLSs +- [Worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker): executing + additional code in a separate thread + - **Implementation notes:** Blob URLs are not supported, object ownership + cannot be transferred, posted data is serialized to JSON instead of + [structured cloning](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). |