diff --git a/cli/tests/error_009_op_crates_error.js.out b/cli/tests/error_009_op_crates_error.js.out index fd428b28e9..b46ba3c168 100644 --- a/cli/tests/error_009_op_crates_error.js.out +++ b/cli/tests/error_009_op_crates_error.js.out @@ -1,3 +1,3 @@ -[WILDCARD]error: Uncaught TypeError: Event requires at least 1 argument, but only 0 present[WILDCARD] +[WILDCARD]error: Uncaught TypeError: Failed to construct 'Event': 1 argument, but only 0 present.[WILDCARD] at new Event (deno:op_crates/web/[WILDCARD]) at [WILDCARD] diff --git a/cli/tests/unit/event_test.ts b/cli/tests/unit/event_test.ts index 149509c9d4..9eb90fa36c 100644 --- a/cli/tests/unit/event_test.ts +++ b/cli/tests/unit/event_test.ts @@ -99,27 +99,27 @@ unitTest(function eventInspectOutput(): void { [ new Event("test"), (event: Event) => - `Event {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n target: null,\n timeStamp: ${event.timeStamp},\n type: "test"\n}`, + `Event {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "test"\n}`, ], [ new ErrorEvent("error"), (event: Event) => - `ErrorEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n target: null,\n timeStamp: ${event.timeStamp},\n type: "error",\n message: "",\n filename: "",\n lineno: 0,\n colno: 0,\n error: null\n}`, + `ErrorEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "error",\n message: "",\n filename: "",\n lineno: 0,\n colno: 0,\n error: null\n}`, ], [ new CloseEvent("close"), (event: Event) => - `CloseEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n target: null,\n timeStamp: ${event.timeStamp},\n type: "close",\n wasClean: false,\n code: 0,\n reason: ""\n}`, + `CloseEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "close",\n wasClean: false,\n code: 0,\n reason: ""\n}`, ], [ new CustomEvent("custom"), (event: Event) => - `CustomEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n target: null,\n timeStamp: ${event.timeStamp},\n type: "custom",\n detail: undefined\n}`, + `CustomEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "custom",\n detail: undefined\n}`, ], [ new ProgressEvent("progress"), (event: Event) => - `ProgressEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n target: null,\n timeStamp: ${event.timeStamp},\n type: "progress",\n lengthComputable: false,\n loaded: 0,\n total: 0\n}`, + `ProgressEvent {\n bubbles: false,\n cancelable: false,\n composed: false,\n currentTarget: null,\n defaultPrevented: false,\n eventPhase: 0,\n srcElement: null,\n target: null,\n returnValue: true,\n timeStamp: ${event.timeStamp},\n type: "progress",\n lengthComputable: false,\n loaded: 0,\n total: 0\n}`, ], ]; diff --git a/op_crates/web/00_webidl.js b/op_crates/web/00_webidl.js new file mode 100644 index 0000000000..72c58c3772 --- /dev/null +++ b/op_crates/web/00_webidl.js @@ -0,0 +1,671 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// Adapted from https://github.com/jsdom/webidl-conversions. +// Copyright Domenic Denicola. Licensed under BSD-2-Clause License. +// Original license at https://github.com/jsdom/webidl-conversions/blob/master/LICENSE.md. + +"use strict"; + +((window) => { + function makeException(ErrorType, message, opts = {}) { + if (opts.globals) { + ErrorType = opts.globals[ErrorType.name]; + } + return new ErrorType( + `${opts.prefix ? opts.prefix + ": " : ""}${ + opts.context ? opts.context : "Value" + } ${message}.`, + ); + } + + function toNumber(value, opts = {}) { + if (!opts.globals) { + return +value; + } + if (typeof value === "bigint") { + throw opts.globals.TypeError("Cannot convert a BigInt value to a number"); + } + return opts.globals.Number(value); + } + + function type(V) { + if (V === null) { + return "Null"; + } + switch (typeof V) { + case "undefined": + return "Undefined"; + case "boolean": + return "Boolean"; + case "number": + return "Number"; + case "string": + return "String"; + case "symbol": + return "Symbol"; + case "bigint": + return "BigInt"; + case "object": + // Falls through + case "function": + // Falls through + default: + // Per ES spec, typeof returns an implemention-defined value that is not any of the existing ones for + // uncallable non-standard exotic objects. Yet Type() which the Web IDL spec depends on returns Object for + // such cases. So treat the default case as an object. + return "Object"; + } + } + + // Round x to the nearest integer, choosing the even integer if it lies halfway between two. + function evenRound(x) { + // There are four cases for numbers with fractional part being .5: + // + // case | x | floor(x) | round(x) | expected | x <> 0 | x % 1 | x & 1 | example + // 1 | 2n + 0.5 | 2n | 2n + 1 | 2n | > | 0.5 | 0 | 0.5 -> 0 + // 2 | 2n + 1.5 | 2n + 1 | 2n + 2 | 2n + 2 | > | 0.5 | 1 | 1.5 -> 2 + // 3 | -2n - 0.5 | -2n - 1 | -2n | -2n | < | -0.5 | 0 | -0.5 -> 0 + // 4 | -2n - 1.5 | -2n - 2 | -2n - 1 | -2n - 2 | < | -0.5 | 1 | -1.5 -> -2 + // (where n is a non-negative integer) + // + // Branch here for cases 1 and 4 + if ( + (x > 0 && x % 1 === +0.5 && (x & 1) === 0) || + (x < 0 && x % 1 === -0.5 && (x & 1) === 1) + ) { + return censorNegativeZero(Math.floor(x)); + } + + return censorNegativeZero(Math.round(x)); + } + + function integerPart(n) { + return censorNegativeZero(Math.trunc(n)); + } + + function sign(x) { + return x < 0 ? -1 : 1; + } + + function modulo(x, y) { + // https://tc39.github.io/ecma262/#eqn-modulo + // Note that http://stackoverflow.com/a/4467559/3191 does NOT work for large modulos + const signMightNotMatch = x % y; + if (sign(y) !== sign(signMightNotMatch)) { + return signMightNotMatch + y; + } + return signMightNotMatch; + } + + function censorNegativeZero(x) { + return x === 0 ? 0 : x; + } + + function createIntegerConversion(bitLength, typeOpts) { + const isSigned = !typeOpts.unsigned; + + let lowerBound; + let upperBound; + if (bitLength === 64) { + upperBound = Number.MAX_SAFE_INTEGER; + lowerBound = !isSigned ? 0 : Number.MIN_SAFE_INTEGER; + } else if (!isSigned) { + lowerBound = 0; + upperBound = Math.pow(2, bitLength) - 1; + } else { + lowerBound = -Math.pow(2, bitLength - 1); + upperBound = Math.pow(2, bitLength - 1) - 1; + } + + const twoToTheBitLength = Math.pow(2, bitLength); + const twoToOneLessThanTheBitLength = Math.pow(2, bitLength - 1); + + return (V, opts = {}) => { + let x = toNumber(V, opts); + x = censorNegativeZero(x); + + if (opts.enforceRange) { + if (!Number.isFinite(x)) { + throw makeException(TypeError, "is not a finite number", opts); + } + + x = integerPart(x); + + if (x < lowerBound || x > upperBound) { + throw makeException( + TypeError, + `is outside the accepted range of ${lowerBound} to ${upperBound}, inclusive`, + opts, + ); + } + + return x; + } + + if (!Number.isNaN(x) && opts.clamp) { + x = Math.min(Math.max(x, lowerBound), upperBound); + x = evenRound(x); + return x; + } + + if (!Number.isFinite(x) || x === 0) { + return 0; + } + x = integerPart(x); + + // Math.pow(2, 64) is not accurately representable in JavaScript, so try to avoid these per-spec operations if + // possible. Hopefully it's an optimization for the non-64-bitLength cases too. + if (x >= lowerBound && x <= upperBound) { + return x; + } + + // These will not work great for bitLength of 64, but oh well. See the README for more details. + x = modulo(x, twoToTheBitLength); + if (isSigned && x >= twoToOneLessThanTheBitLength) { + return x - twoToTheBitLength; + } + return x; + }; + } + + function createLongLongConversion(bitLength, { unsigned }) { + const upperBound = Number.MAX_SAFE_INTEGER; + const lowerBound = unsigned ? 0 : Number.MIN_SAFE_INTEGER; + const asBigIntN = unsigned ? BigInt.asUintN : BigInt.asIntN; + + return (V, opts = {}) => { + let x = toNumber(V, opts); + x = censorNegativeZero(x); + + if (opts.enforceRange) { + if (!Number.isFinite(x)) { + throw makeException(TypeError, "is not a finite number", opts); + } + + x = integerPart(x); + + if (x < lowerBound || x > upperBound) { + throw makeException( + TypeError, + `is outside the accepted range of ${lowerBound} to ${upperBound}, inclusive`, + opts, + ); + } + + return x; + } + + if (!Number.isNaN(x) && opts.clamp) { + x = Math.min(Math.max(x, lowerBound), upperBound); + x = evenRound(x); + return x; + } + + if (!Number.isFinite(x) || x === 0) { + return 0; + } + + let xBigInt = BigInt(integerPart(x)); + xBigInt = asBigIntN(bitLength, xBigInt); + return Number(xBigInt); + }; + } + + const converters = []; + + converters.any = (V) => { + return V; + }; + + converters.boolean = function (val) { + return !!val; + }; + + converters.byte = createIntegerConversion(8, { unsigned: false }); + converters.octet = createIntegerConversion(8, { unsigned: true }); + + converters.short = createIntegerConversion(16, { unsigned: false }); + converters["unsigned short"] = createIntegerConversion(16, { + unsigned: true, + }); + + converters.long = createIntegerConversion(32, { unsigned: false }); + converters["unsigned long"] = createIntegerConversion(32, { unsigned: true }); + + converters["long long"] = createLongLongConversion(64, { unsigned: false }); + converters["unsigned long long"] = createLongLongConversion(64, { + unsigned: true, + }); + + converters.float = (V, opts) => { + const x = toNumber(V, opts); + + if (!Number.isFinite(x)) { + throw makeException( + TypeError, + "is not a finite floating-point value", + opts, + ); + } + + if (Object.is(x, -0)) { + return x; + } + + const y = Math.fround(x); + + if (!Number.isFinite(y)) { + throw makeException( + TypeError, + "is outside the range of a single-precision floating-point value", + opts, + ); + } + + return y; + }; + + converters["unrestricted float"] = (V, opts) => { + const x = toNumber(V, opts); + + if (isNaN(x)) { + return x; + } + + if (Object.is(x, -0)) { + return x; + } + + return Math.fround(x); + }; + + converters.double = (V, opts) => { + const x = toNumber(V, opts); + + if (!Number.isFinite(x)) { + throw makeException( + TypeError, + "is not a finite floating-point value", + opts, + ); + } + + return x; + }; + + converters["unrestricted double"] = (V, opts) => { + const x = toNumber(V, opts); + + return x; + }; + + converters.DOMString = function (V, opts = {}) { + if (opts.treatNullAsEmptyString && V === null) { + return ""; + } + + if (typeof V === "symbol") { + throw makeException( + TypeError, + "is a symbol, which cannot be converted to a string", + opts, + ); + } + + const StringCtor = opts.globals ? opts.globals.String : String; + return StringCtor(V); + }; + + converters.ByteString = (V, opts) => { + const x = converters.DOMString(V, opts); + let c; + for (let i = 0; (c = x.codePointAt(i)) !== undefined; ++i) { + if (c > 255) { + throw makeException(TypeError, "is not a valid ByteString", opts); + } + } + + return x; + }; + + converters.USVString = (V, opts) => { + const S = converters.DOMString(V, opts); + const n = S.length; + const U = []; + for (let i = 0; i < n; ++i) { + const c = S.charCodeAt(i); + if (c < 0xd800 || c > 0xdfff) { + U.push(String.fromCodePoint(c)); + } else if (0xdc00 <= c && c <= 0xdfff) { + U.push(String.fromCodePoint(0xfffd)); + } else if (i === n - 1) { + U.push(String.fromCodePoint(0xfffd)); + } else { + const d = S.charCodeAt(i + 1); + if (0xdc00 <= d && d <= 0xdfff) { + const a = c & 0x3ff; + const b = d & 0x3ff; + U.push(String.fromCodePoint((2 << 15) + (2 << 9) * a + b)); + ++i; + } else { + U.push(String.fromCodePoint(0xfffd)); + } + } + } + + return U.join(""); + }; + + converters.object = (V, opts) => { + if (type(V) !== "Object") { + throw makeException(TypeError, "is not an object", opts); + } + + return V; + }; + + // Not exported, but used in Function and VoidFunction. + + // Neither Function nor VoidFunction is defined with [TreatNonObjectAsNull], so + // handling for that is omitted. + function convertCallbackFunction(V, opts) { + if (typeof V !== "function") { + throw makeException(TypeError, "is not a function", opts); + } + return V; + } + + const abByteLengthGetter = Object.getOwnPropertyDescriptor( + ArrayBuffer.prototype, + "byteLength", + ).get; + + function isNonSharedArrayBuffer(V) { + try { + // This will throw on SharedArrayBuffers, but not detached ArrayBuffers. + // (The spec says it should throw, but the spec conflicts with implementations: https://github.com/tc39/ecma262/issues/678) + abByteLengthGetter.call(V); + + return true; + } catch { + return false; + } + } + + let sabByteLengthGetter; + + function isSharedArrayBuffer(V) { + // TODO(lucacasonato): vulnerable to prototype pollution. Needs to happen + // here because SharedArrayBuffer is not available during snapshotting. + if (!sabByteLengthGetter) { + sabByteLengthGetter = Object.getOwnPropertyDescriptor( + SharedArrayBuffer.prototype, + "byteLength", + ).get; + } + try { + sabByteLengthGetter.call(V); + return true; + } catch { + return false; + } + } + + function isArrayBufferDetached(V) { + try { + // eslint-disable-next-line no-new + new Uint8Array(V); + return false; + } catch { + return true; + } + } + + converters.ArrayBuffer = (V, opts = {}) => { + if (!isNonSharedArrayBuffer(V)) { + if (opts.allowShared && !isSharedArrayBuffer(V)) { + throw makeException( + TypeError, + "is not an ArrayBuffer or SharedArrayBuffer", + opts, + ); + } + throw makeException(TypeError, "is not an ArrayBuffer", opts); + } + if (isArrayBufferDetached(V)) { + throw makeException(TypeError, "is a detached ArrayBuffer", opts); + } + + return V; + }; + + const dvByteLengthGetter = Object.getOwnPropertyDescriptor( + DataView.prototype, + "byteLength", + ).get; + converters.DataView = (V, opts = {}) => { + try { + dvByteLengthGetter.call(V); + } catch (e) { + throw makeException(TypeError, "is not a DataView", opts); + } + + if (!opts.allowShared && isSharedArrayBuffer(V.buffer)) { + throw makeException( + TypeError, + "is backed by a SharedArrayBuffer, which is not allowed", + opts, + ); + } + if (isArrayBufferDetached(V.buffer)) { + throw makeException( + TypeError, + "is backed by a detached ArrayBuffer", + opts, + ); + } + + return V; + }; + + // Returns the unforgeable `TypedArray` constructor name or `undefined`, + // if the `this` value isn't a valid `TypedArray` object. + // + // https://tc39.es/ecma262/#sec-get-%typedarray%.prototype-@@tostringtag + const typedArrayNameGetter = Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(Uint8Array).prototype, + Symbol.toStringTag, + ).get; + [ + Int8Array, + Int16Array, + Int32Array, + Uint8Array, + Uint16Array, + Uint32Array, + Uint8ClampedArray, + Float32Array, + Float64Array, + ].forEach((func) => { + const name = func.name; + const article = /^[AEIOU]/.test(name) ? "an" : "a"; + converters[name] = (V, opts = {}) => { + if (!ArrayBuffer.isView(V) || typedArrayNameGetter.call(V) !== name) { + throw makeException( + TypeError, + `is not ${article} ${name} object`, + opts, + ); + } + if (!opts.allowShared && isSharedArrayBuffer(V.buffer)) { + throw makeException( + TypeError, + "is a view on a SharedArrayBuffer, which is not allowed", + opts, + ); + } + if (isArrayBufferDetached(V.buffer)) { + throw makeException( + TypeError, + "is a view on a detached ArrayBuffer", + opts, + ); + } + + return V; + }; + }); + + // Common definitions + + converters.ArrayBufferView = (V, opts = {}) => { + if (!ArrayBuffer.isView(V)) { + throw makeException( + TypeError, + "is not a view on an ArrayBuffer or SharedArrayBuffer", + opts, + ); + } + + if (!opts.allowShared && isSharedArrayBuffer(V.buffer)) { + throw makeException( + TypeError, + "is a view on a SharedArrayBuffer, which is not allowed", + opts, + ); + } + + if (isArrayBufferDetached(V.buffer)) { + throw makeException( + TypeError, + "is a view on a detached ArrayBuffer", + opts, + ); + } + return V; + }; + + converters.BufferSource = (V, opts = {}) => { + if (ArrayBuffer.isView(V)) { + if (!opts.allowShared && isSharedArrayBuffer(V.buffer)) { + throw makeException( + TypeError, + "is a view on a SharedArrayBuffer, which is not allowed", + opts, + ); + } + + if (isArrayBufferDetached(V.buffer)) { + throw makeException( + TypeError, + "is a view on a detached ArrayBuffer", + opts, + ); + } + return V; + } + + if (!opts.allowShared && !isNonSharedArrayBuffer(V)) { + throw makeException( + TypeError, + "is not an ArrayBuffer or a view on one", + opts, + ); + } + if ( + opts.allowShared && + !isSharedArrayBuffer(V) && + !isNonSharedArrayBuffer(V) + ) { + throw makeException( + TypeError, + "is not an ArrayBuffer, SharedArrayBuffer, or a view on one", + opts, + ); + } + if (isArrayBufferDetached(V)) { + throw makeException(TypeError, "is a detached ArrayBuffer", opts); + } + + return V; + }; + + converters.DOMTimeStamp = converters["unsigned long long"]; + + converters.Function = convertCallbackFunction; + + converters.VoidFunction = convertCallbackFunction; + + function requiredArguments(length, required, opts = {}) { + if (length < required) { + const errMsg = `${ + opts.prefix ? opts.prefix + ": " : "" + }${required} argument${ + required === 1 ? "" : "s" + }, but only ${length} present.`; + throw new TypeError(errMsg); + } + } + + function createDictionaryConverter(name, ...dictionaries) { + return function (V, opts = {}) { + const typeV = type(V); + switch (typeV) { + case "Undefined": + case "Null": + case "Object": + break; + default: + throw makeException( + TypeError, + "can not be converted to a dictionary", + opts, + ); + } + const esDict = V; + + const idlDict = {}; + + for (const members of dictionaries) { + for (const member of members) { + const key = member.key; + + let esMemberValue; + if (typeV === "Undefined" || typeV === "Null") { + esMemberValue = undefined; + } else { + esMemberValue = esDict[key]; + } + + if (esMemberValue !== undefined) { + const converter = member.converter; + const idlMemberValue = converter(esMemberValue, { + ...opts, + context: `${key} of '${name}'${ + opts.context ? `(${opts.context})` : "" + }`, + }); + idlDict[key] = idlMemberValue; + } else if ("defaultValue" in member) { + const defaultValue = member.defaultValue; + const idlMemberValue = defaultValue; + idlDict[key] = idlMemberValue; + } else if (member.required) { + throw new TypeError( + `can not be converted to '${name}' because ${key} is required in '${name}'.`, + ); + } + } + } + + return idlDict; + }; + } + + window.__bootstrap = window.__bootstrap || {}; + window.__bootstrap.webidl = { + converters, + requiredArguments, + createDictionaryConverter, + }; +})(this); diff --git a/op_crates/web/00_dom_exception.js b/op_crates/web/01_dom_exception.js similarity index 85% rename from op_crates/web/00_dom_exception.js rename to op_crates/web/01_dom_exception.js index 22fd842fea..14f4ca8e93 100644 --- a/op_crates/web/00_dom_exception.js +++ b/op_crates/web/01_dom_exception.js @@ -1,7 +1,15 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// @ts-check +/// +/// +/// + "use strict"; ((window) => { + const webidl = window.__bootstrap.webidl; + const { defineProperty } = Object; // Defined in WebIDL 4.3. // https://heycam.github.io/webidl/#idl-DOMException @@ -33,6 +41,7 @@ // Defined in WebIDL 2.8.1. // https://heycam.github.io/webidl/#dfn-error-names-table + /** @type {Record} */ const nameToCodeMapping = { IndexSizeError: INDEX_SIZE_ERR, HierarchyRequestError: HIERARCHY_REQUEST_ERR, @@ -67,9 +76,15 @@ constructor(message = "", name = "Error") { super(); - this.#message = String(message); - this.#name = name; - this.#code = nameToCodeMapping[name] ?? 0; + this.#message = webidl.converters.DOMString(message, { + prefix: "Failed to construct 'DOMException'", + context: "Argument 1", + }); + this.#name = webidl.converters.DOMString(name, { + prefix: "Failed to construct 'DOMException'", + context: "Argument 2", + }); + this.#code = nameToCodeMapping[this.#name] ?? 0; } get message() { diff --git a/op_crates/web/01_event.js b/op_crates/web/02_event.js similarity index 82% rename from op_crates/web/01_event.js rename to op_crates/web/02_event.js index 91ae2ef680..d253599e5e 100644 --- a/op_crates/web/01_event.js +++ b/op_crates/web/02_event.js @@ -7,95 +7,65 @@ "use strict"; ((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); - } - } + const webidl = window.__bootstrap.webidl; // accessors for non runtime visible data function getDispatched(event) { - return Boolean(eventData.get(event)?.dispatched); + return Boolean(event[_dispatched]); } function getPath(event) { - return eventData.get(event)?.path ?? []; + return event[_path] ?? []; } function getStopImmediatePropagation(event) { - return Boolean(eventData.get(event)?.stopImmediatePropagation); + return Boolean(event[_stopImmediatePropagationFlag]); } function setCurrentTarget( event, value, ) { - event.currentTarget = value; + event[_attributes].currentTarget = value; } function setIsTrusted(event, value) { - const data = eventData.get(event); - if (data) { - data.isTrusted = value; - } + event[_isTrusted] = value; } function setDispatched(event, value) { - const data = eventData.get(event); - if (data) { - data.dispatched = value; - } + event[_dispatched] = value; } function setEventPhase(event, value) { - event.eventPhase = value; + event[_attributes].eventPhase = value; } function setInPassiveListener(event, value) { - const data = eventData.get(event); - if (data) { - data.inPassiveListener = value; - } + event[_inPassiveListener] = value; } function setPath(event, value) { - const data = eventData.get(event); - if (data) { - data.path = value; - } + event[_path] = value; } function setRelatedTarget( event, value, ) { - if ("relatedTarget" in event) { - event.relatedTarget = value; - } + event[_attributes].relatedTarget = value; } function setTarget(event, value) { - event.target = value; + event[_attributes].target = value; } function setStopImmediatePropagation( event, value, ) { - const data = eventData.get(event); - if (data) { - data.stopImmediatePropagation = value; - } + event[_stopImmediatePropagationFlag] = value; } // Type guards that widen the event type @@ -108,35 +78,65 @@ const isTrusted = Object.getOwnPropertyDescriptor({ get isTrusted() { - return eventData.get(this).isTrusted; + return this[_isTrusted]; }, }, "isTrusted").get; + const eventInitConverter = webidl.createDictionaryConverter("EventInit", [{ + key: "bubbles", + defaultValue: false, + converter: webidl.converters.boolean, + }, { + key: "cancelable", + defaultValue: false, + converter: webidl.converters.boolean, + }, { + key: "composed", + defaultValue: false, + converter: webidl.converters.boolean, + }]); + + const _attributes = Symbol("[[attributes]]"); + const _canceledFlag = Symbol("[[canceledFlag]]"); + const _stopPropagationFlag = Symbol("[[stopPropagationFlag]]"); + const _stopImmediatePropagationFlag = Symbol( + "[[stopImmediatePropagationFlag]]", + ); + const _inPassiveListener = Symbol("[[inPassiveListener]]"); + const _dispatched = Symbol("[[dispatched]]"); + const _isTrusted = Symbol("[[isTrusted]]"); + const _path = Symbol("[[path]]"); + class Event { - #canceledFlag = false; - #stopPropagationFlag = false; - #attributes = {}; + [_attributes] = {}; + [_canceledFlag] = false; + [_stopPropagationFlag] = false; + [_stopImmediatePropagationFlag] = false; + [_inPassiveListener] = false; + [_dispatched] = false; + [_isTrusted] = false; + [_path] = []; constructor(type, eventInitDict = {}) { - requiredArguments("Event", arguments.length, 1); - type = String(type); - this.#attributes = { + webidl.requiredArguments(arguments.length, 1, { + prefix: "Failed to construct 'Event'", + }); + type = webidl.converters.DOMString(type, { + prefix: "Failed to construct 'Event'", + context: "Argument 1", + }); + const eventInit = eventInitConverter(eventInitDict, { + prefix: "Failed to construct 'Event'", + context: "Argument 2", + }); + this[_attributes] = { type, - bubbles: eventInitDict.bubbles ?? false, - cancelable: eventInitDict.cancelable ?? false, - composed: eventInitDict.composed ?? false, + ...eventInit, 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, @@ -147,95 +147,32 @@ return buildCustomInspectOutput(this, EVENT_PROPS, inspect); } - 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; + return this[_attributes].type; + } + set type(_) { + // this is a no-op because this member is readonly + } + get target() { + return this[_attributes].target; + } + set target(_) { + // this is a no-op because this member is readonly + } + get srcElement() { + return null; + } + set srcElement(_) { + // this is a no-op because this member is readonly + } + get currentTarget() { + return this[_attributes].currentTarget; + } + set currentTarget(_) { + // this is a no-op because this member is readonly } - composedPath() { - const path = eventData.get(this).path; + const path = this[_path]; if (path.length === 0) { return []; } @@ -339,52 +276,123 @@ 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; } - + set NONE(_) { + // this is a no-op because this member is readonly + } get CAPTURING_PHASE() { return Event.CAPTURING_PHASE; } - + set CAPTURING_PHASE(_) { + // this is a no-op because this member is readonly + } get AT_TARGET() { return Event.AT_TARGET; } - + set AT_TARGET(_) { + // this is a no-op because this member is readonly + } get BUBBLING_PHASE() { return Event.BUBBLING_PHASE; } - + set BUBBLING_PHASE(_) { + // this is a no-op because this member is readonly + } static get NONE() { return 0; } - + static set NONE(_) { + // this is a no-op because this member is readonly + } static get CAPTURING_PHASE() { return 1; } - + static set CAPTURING_PHASE(_) { + // this is a no-op because this member is readonly + } static get AT_TARGET() { return 2; } - + static set AT_TARGET(_) { + // this is a no-op because this member is readonly + } static get BUBBLING_PHASE() { return 3; } + static set BUBBLING_PHASE(_) { + // this is a no-op because this member is readonly + } + get eventPhase() { + return this[_attributes].eventPhase; + } + set eventPhase(_) { + // this is a no-op because this member is readonly + } + + stopPropagation() { + this[_stopPropagationFlag] = true; + } + get cancelBubble() { + return this[_stopPropagationFlag]; + } + set cancelBubble(value) { + this[_stopPropagationFlag] = webidl.converters.boolean(value); + } + stopImmediatePropagation() { + this[_stopPropagationFlag] = true; + this[_stopImmediatePropagationFlag] = true; + } + + get bubbles() { + return this[_attributes].bubbles; + } + set bubbles(_) { + // this is a no-op because this member is readonly + } + get cancelable() { + return this[_attributes].cancelable; + } + set cancelable(value) { + // this is a no-op because this member is readonly + } + get returnValue() { + return !this[_canceledFlag]; + } + set returnValue(value) { + if (!webidl.converters.boolean(value)) { + this[_canceledFlag] = true; + } + } + preventDefault() { + if (this[_attributes].cancelable && !this[_inPassiveListener]) { + this[_canceledFlag] = true; + } + } + get defaultPrevented() { + return this[_canceledFlag]; + } + set defaultPrevented(_) { + // this is a no-op because this member is readonly + } + get composed() { + return this[_attributes].composed; + } + set composed(_) { + // this is a no-op because this member is readonly + } + + get initialized() { + return true; + } + + get timeStamp() { + return this[_attributes].timeStamp; + } + set timeStamp(_) { + // this is a no-op because this member is readonly + } } function buildCustomInspectOutput(object, keys, inspect) { @@ -408,7 +416,9 @@ "currentTarget", "defaultPrevented", "eventPhase", + "srcElement", "target", + "returnValue", "timeStamp", "type", ]; @@ -875,7 +885,9 @@ callback, options, ) { - requiredArguments("EventTarget.addEventListener", arguments.length, 2); + webidl.requiredArguments(arguments.length, 2, { + prefix: "Failed to execute 'addEventListener' on 'EventTarget'", + }); if (callback === null) { return; } @@ -919,7 +931,9 @@ callback, options, ) { - requiredArguments("EventTarget.removeEventListener", arguments.length, 2); + webidl.requiredArguments(arguments.length, 2, { + prefix: "Failed to execute 'removeEventListener' on 'EventTarget'", + }); const listeners = eventTargetData.get(this ?? globalThis).listeners; if (callback !== null && type in listeners) { @@ -948,7 +962,9 @@ } dispatchEvent(event) { - requiredArguments("EventTarget.dispatchEvent", arguments.length, 1); + webidl.requiredArguments(arguments.length, 1, { + prefix: "Failed to execute 'dispatchEvent' on 'EventTarget'", + }); const self = this ?? globalThis; const listeners = eventTargetData.get(self).listeners; @@ -1128,7 +1144,9 @@ constructor(type, eventInitDict = {}) { super(type, eventInitDict); - requiredArguments("CustomEvent", arguments.length, 1); + webidl.requiredArguments(arguments.length, 1, { + prefix: "Failed to construct 'CustomEvent'", + }); const { detail } = eventInitDict; this.#detail = detail; } diff --git a/op_crates/web/02_abort_signal.js b/op_crates/web/03_abort_signal.js similarity index 100% rename from op_crates/web/02_abort_signal.js rename to op_crates/web/03_abort_signal.js diff --git a/op_crates/web/03_global_interfaces.js b/op_crates/web/04_global_interfaces.js similarity index 100% rename from op_crates/web/03_global_interfaces.js rename to op_crates/web/04_global_interfaces.js diff --git a/op_crates/web/internal.d.ts b/op_crates/web/internal.d.ts index 8f91601654..6f6849a7e7 100644 --- a/op_crates/web/internal.d.ts +++ b/op_crates/web/internal.d.ts @@ -1,10 +1,223 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file no-explicit-any ban-types /// /// declare namespace globalThis { declare namespace __bootstrap { + declare namespace webidl { + declare interface ConverterOpts { + /** + * The prefix for error messages created by this converter. + * Examples: + * - `Failed to construct 'Event'` + * - `Failed to execute 'removeEventListener' on 'EventTarget'` + */ + prefix: string; + } + declare interface ValueConverterOpts extends ConverterOpts { + /** + * The context of this value error messages created by this converter. + * Examples: + * - `Argument 1` + * - `Argument 3` + */ + context: string; + } + declare interface IntConverterOpts extends ValueConverterOpts { + /** + * Wether to throw if the number is outside of the acceptable values for + * this type. + */ + enforceRange?: boolean; + /** + * Wether to clamp this number to the acceptable values for this type. + */ + clamp?: boolean; + } + declare interface StringConverterOpts extends ValueConverterOpts { + /** + * Wether to treat `null` value as an empty string. + */ + treatNullAsEmptyString?: boolean; + } + declare interface BufferConverterOpts extends ValueConverterOpts { + /** + * Wether to allow `SharedArrayBuffer` (not just `ArrayBuffer`). + */ + allowShared?: boolean; + } + declare const converters: { + any(v: any): any; + /** + * Convert a value into a `boolean` (bool). + */ + boolean(v: any, opts?: IntConverterOpts): boolean; + /** + * Convert a value into a `byte` (int8). + */ + byte(v: any, opts?: IntConverterOpts): number; + /** + * Convert a value into a `octet` (uint8). + */ + octet(v: any, opts?: IntConverterOpts): number; + /** + * Convert a value into a `short` (int16). + */ + short(v: any, opts?: IntConverterOpts): number; + /** + * Convert a value into a `unsigned short` (uint16). + */ + ["unsigned short"](v: any, opts?: IntConverterOpts): number; + /** + * Convert a value into a `long` (int32). + */ + long(v: any, opts?: IntConverterOpts): number; + /** + * Convert a value into a `unsigned long` (uint32). + */ + ["unsigned long"](v: any, opts?: IntConverterOpts): number; + /** + * Convert a value into a `long long` (int64). + * **Note this is truncated to a JS number (53 bit precision).** + */ + ["long long"](v: any, opts?: IntConverterOpts): number; + /** + * Convert a value into a `unsigned long long` (uint64). + * **Note this is truncated to a JS number (53 bit precision).** + */ + ["unsigned long long"](v: any, opts?: IntConverterOpts): number; + /** + * Convert a value into a `float` (f32). + */ + float(v: any, opts?: ValueConverterOpts): number; + /** + * Convert a value into a `unrestricted float` (f32, infinity, or NaN). + */ + ["unrestricted float"](v: any, opts?: ValueConverterOpts): number; + /** + * Convert a value into a `double` (f64). + */ + double(v: any, opts?: ValueConverterOpts): number; + /** + * Convert a value into a `unrestricted double` (f64, infinity, or NaN). + */ + ["unrestricted double"](v: any, opts?: ValueConverterOpts): number; + /** + * Convert a value into a `DOMString` (string). + */ + DOMString(v: any, opts?: StringConverterOpts): string; + /** + * Convert a value into a `ByteString` (string with only u8 codepoints). + */ + ByteString(v: any, opts?: StringConverterOpts): string; + /** + * Convert a value into a `USVString` (string with only valid non + * surrogate Unicode code points). + */ + USVString(v: any, opts?: StringConverterOpts): string; + /** + * Convert a value into an `object` (object). + */ + object(v: any, opts?: ValueConverterOpts): object; + /** + * Convert a value into an `ArrayBuffer` (ArrayBuffer). + */ + ArrayBuffer(v: any, opts?: BufferConverterOpts): ArrayBuffer; + /** + * Convert a value into a `DataView` (ArrayBuffer). + */ + DataView(v: any, opts?: BufferConverterOpts): DataView; + /** + * Convert a value into a `Int8Array` (Int8Array). + */ + Int8Array(v: any, opts?: BufferConverterOpts): Int8Array; + /** + * Convert a value into a `Int16Array` (Int16Array). + */ + Int16Array(v: any, opts?: BufferConverterOpts): Int16Array; + /** + * Convert a value into a `Int32Array` (Int32Array). + */ + Int32Array(v: any, opts?: BufferConverterOpts): Int32Array; + /** + * Convert a value into a `Uint8Array` (Uint8Array). + */ + Uint8Array(v: any, opts?: BufferConverterOpts): Uint8Array; + /** + * Convert a value into a `Uint16Array` (Uint16Array). + */ + Uint16Array(v: any, opts?: BufferConverterOpts): Uint16Array; + /** + * Convert a value into a `Uint32Array` (Uint32Array). + */ + Uint32Array(v: any, opts?: BufferConverterOpts): Uint32Array; + /** + * Convert a value into a `Uint8ClampedArray` (Uint8ClampedArray). + */ + Uint8ClampedArray( + v: any, + opts?: BufferConverterOpts, + ): Uint8ClampedArray; + /** + * Convert a value into a `Float32Array` (Float32Array). + */ + Float32Array(v: any, opts?: BufferConverterOpts): Float32Array; + /** + * Convert a value into a `Float64Array` (Float64Array). + */ + Float64Array(v: any, opts?: BufferConverterOpts): Float64Array; + /** + * Convert a value into an `ArrayBufferView` (ArrayBufferView). + */ + ArrayBufferView(v: any, opts?: BufferConverterOpts): ArrayBufferView; + /** + * Convert a value into a `BufferSource` (ArrayBuffer or ArrayBufferView). + */ + BufferSource( + v: any, + opts?: BufferConverterOpts, + ): ArrayBuffer | ArrayBufferView; + /** + * Convert a value into a `DOMTimeStamp` (u64). Alias for unsigned long long + */ + DOMTimeStamp(v: any, opts?: IntConverterOpts): number; + /** + * Convert a value into a `Function` ((...args: any[]) => any). + */ + Function(v: any, opts?: ValueConverterOpts): (...args: any) => any; + /** + * Convert a value into a `VoidFunction` (() => void). + */ + VoidFunction(v: any, opts?: ValueConverterOpts): () => void; + }; + + /** + * Assert that the a function has at least a required amount of arguments. + */ + declare function requiredArguments( + length: number, + required: number, + opts: ConverterOpts, + ): void; + declare type Dictionary = DictionaryMember[]; + declare interface DictionaryMember { + key: string; + converter: (v: any, opts: ValueConverterOpts) => any; + defaultValue?: boolean; + required?: boolean; + } + + /**ie + * Assert that the a function has at least a required amount of arguments. + */ + declare function createDictionaryConverter( + name: string, + ...dictionaries: Dictionary[] + ): (v: any, opts: ValueConverterOpts) => T; + } + declare var url: { URLSearchParams: typeof URLSearchParams; }; diff --git a/op_crates/web/lib.rs b/op_crates/web/lib.rs index 209183d817..0008840920 100644 --- a/op_crates/web/lib.rs +++ b/op_crates/web/lib.rs @@ -41,20 +41,24 @@ pub fn op_domain_to_ascii( pub fn init(isolate: &mut JsRuntime) { let files = vec![ ( - "deno:op_crates/web/00_dom_exception.js", - include_str!("00_dom_exception.js"), + "deno:op_crates/web/00_webidl.js", + include_str!("00_webidl.js"), ), ( - "deno:op_crates/web/01_event.js", - include_str!("01_event.js"), + "deno:op_crates/web/01_dom_exception.js", + include_str!("01_dom_exception.js"), ), ( - "deno:op_crates/web/02_abort_signal.js", - include_str!("02_abort_signal.js"), + "deno:op_crates/web/02_event.js", + include_str!("02_event.js"), ), ( - "deno:op_crates/web/03_global_interfaces.js", - include_str!("03_global_interfaces.js"), + "deno:op_crates/web/03_abort_signal.js", + include_str!("03_abort_signal.js"), + ), + ( + "deno:op_crates/web/04_global_interfaces.js", + include_str!("04_global_interfaces.js"), ), ( "deno:op_crates/web/08_text_encoding.js", @@ -136,7 +140,7 @@ mod tests { if let Err(error) = result { let error_string = error.to_string(); // Test that the script specifier is a URL: `deno:`. - assert!(error_string.contains("deno:op_crates/web/01_event.js")); + assert!(error_string.contains("deno:op_crates/web/02_event.js")); assert!(error_string.contains("TypeError")); } else { unreachable!(); diff --git a/test_util/wpt b/test_util/wpt index 81837c9a0f..182191819b 160000 --- a/test_util/wpt +++ b/test_util/wpt @@ -1 +1 @@ -Subproject commit 81837c9a0fb4a11a7bc3b062219c758b238f1b39 +Subproject commit 182191819bbfdc409bf5f29a5ea3bedb4545b91e diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json index 510f71a52e..f719141756 100644 --- a/tools/wpt/expectation.json +++ b/tools/wpt/expectation.json @@ -22,7 +22,11 @@ "event-global-extra.window.js": true, "event-global.worker.js": true, "legacy-pre-activation-behavior.window.js": true, - "relatedTarget.window.js": true + "relatedTarget.window.js": true, + "Event-constructors.any.js": [ + "Untitled 2", + "Untitled 3" + ] }, "idlharness.any.js": false, "idlharness.window.js": false