diff --git a/README.md b/README.md index 7a66fb2..81972dd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,48 @@ -# webidl +# WebIDL helpers for JavaScript -📚 WebIDL infrastructure for making JavaScript ↔️ JavaScript bindings +📚 [WebIDL] infrastructure for making JavaScript ↔️ JavaScript bindings + +
+ +![]() + +
+ +## Installation + +```sh +npm install @webfill/webidl +``` + +## Usage + +```js +import { bindInterface, defineOperation, types } from "@webfill/webidl"; + +class Dog { + bark() { + console.log("woof"); + } + eat(food) { + console.log(`eating ${food}`); + } +} +Dog = bindInterface(Dog, "Dog"); +defineOperation(Dog.prototype, "bark", [], types.undefined) +defineOperation(Dog.prototype, "eat", [types.DOMString], types.undefined); + +const dog = new Dog(); +//=> Uncaught TypeError: Illegal constructor + +const dog = Object.create(Dog.prototype); +dog.bark(); +//=> 'woof' + +dog.eat(); +//=> Uncaught TypeError: Failed to execute 'eat' on 'Dog': 1 argument required, but only 0 present. + +dog.eat("🥩"); +//=> 'eating 🥩' +``` + +[WebIDL]: https://webidl.spec.whatwg.org/ diff --git a/package-lock.json b/package-lock.json index b734a6f..c3d4ab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@jcbhmr/webidl", - "version": "1.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@jcbhmr/webidl", - "version": "1.0.0", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@types/node": "^20.2.5", diff --git a/package.json b/package.json index 89f19c1..def692f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,12 @@ "type": "module", "exports": { ".": "./dist/index.js", + "./DOMException.js": { + "deno": "./src/DOMException.js", + "bun": "./src/DOMException.js", + "node": "./src/DOMException-node.js", + "default": "./src/DOMException.js" + }, "./*.js": "./dist/*.js", "./internal/*": null }, diff --git a/src/DOMException-node-16.js b/src/DOMException-node-16.js new file mode 100644 index 0000000..1e42968 --- /dev/null +++ b/src/DOMException-node-16.js @@ -0,0 +1,9 @@ +/** @type {typeof globalThis.DOMException} */ +let DOMException; +try { + atob(1); +} catch (error) { + DOMException = error.constructor; +} + +export default DOMException; diff --git a/src/DOMException-node.js b/src/DOMException-node.js new file mode 100644 index 0000000..422a9bc --- /dev/null +++ b/src/DOMException-node.js @@ -0,0 +1,9 @@ +/** @type {typeof globalThis.DOMException} */ +let DOMException; +if (process.version.startsWith("v16")) { + ({ default: DOMException } = await import("./DOMException-node-16.js")); +} else { + DOMException = globalThis.DOMException; +} + +export default DOMException; diff --git a/src/DOMException.js b/src/DOMException.js new file mode 100644 index 0000000..3c01dfb --- /dev/null +++ b/src/DOMException.js @@ -0,0 +1 @@ +export default DOMException; diff --git a/src/bindInterface.js b/src/bindInterface.js new file mode 100644 index 0000000..3a0c9dd --- /dev/null +++ b/src/bindInterface.js @@ -0,0 +1,31 @@ +/** + * @template {{ new (...a: any[]): any }} T + * @param {T} Class + * @param {string} name + * @param {boolean} [constructable] + * @returns {T} + */ +export default function bindInterface(Class, name, constructable = undefined) { + Object.defineProperty(Class, "name", { value: name, configurable: true }); + Object.defineProperty(Class.prototype, Symbol.toStringTag, { + value: name, + configurable: true, + }); + + if (constructable == null) { + constructable = /\Wconstructor\(/.test( + Function.prototype.toString.call(Class) + ); + } + + if (!constructable) { + Object.defineProperty(Class, "length", { value: 0, configurable: true }); + Class = new Proxy(Class, { + construct() { + throw new TypeError(`Illegal constructor`); + }, + }); + } + + return Class; +} diff --git a/src/defineAttribute.js b/src/defineAttribute.js new file mode 100644 index 0000000..5ca6aad --- /dev/null +++ b/src/defineAttribute.js @@ -0,0 +1,32 @@ +/** + * @template {object} O + * @template {keyof O | string} N + * @template {{ from(x: unknown): any }[]} T + * @param {O} object + * @param {N} name + * @param {T} [attrType] + * @returns {O} + */ +export default function defineAttribute(object, name, attrType = undefined) { + const { get, set } = Object.getOwnPropertyDescriptor(object, name); + Object.defineProperty(object, name, { + get() { + let x = get.call(this); + if (attrType) { + x = attrType.from(x); + } + return x; + }, + ...(set && { + set(x) { + if (attrType) { + x = attrType.from(x); + } + set.call(this, x); + }, + }), + enumerable: true, + configurable: true, + }); + return object; +} diff --git a/src/defineExtendedAttribute.js b/src/defineExtendedAttribute.js new file mode 100644 index 0000000..ba5f73a --- /dev/null +++ b/src/defineExtendedAttribute.js @@ -0,0 +1,17 @@ +/** + * @template {object} O + * @param {O} object + * @param {keyof O | string} name + * @param {(t: "descriptor", d: PropertyDescriptor) => PropertyDescriptor | null | undefined} ExtendedAttribute + * @returns {O} + */ +export default function defineExtendedAttribute( + object, + name, + ExtendedAttribute +) { + let d = Object.getOwnPropertyDescriptor(object, name); + d = ExtendedAttribute("descriptor", d) ?? d; + Object.defineProperty(object, name, d); + return object; +} diff --git a/src/defineOperation.js b/src/defineOperation.js new file mode 100644 index 0000000..2104123 --- /dev/null +++ b/src/defineOperation.js @@ -0,0 +1,41 @@ +/** + * @template {object} O + * @template {keyof O | string} N + * @template {{ from(x: unknown): any }[]} A + * @template {{ from(x: any): any }} R + * @param {O} object + * @param {N} name + * @param {A} [argTypes] + * @param {R} [returnType] + * @returns {O} + */ +export default function defineOperation( + object, + name, + argTypes = undefined, + returnType = undefined +) { + const original = object[name]; + const { value } = { + value() { + if (argTypes) { + for (const [i, argType] of argTypes.entries()) { + arguments[i] = argType.from(arguments[i]); + } + } + let result = original.apply(this, arguments); + if (returnType) { + result = returnType.from(result); + } + return result; + }, + }; + Object.defineProperties(value, Object.getOwnPropertyDescriptors(original)); + Object.defineProperty(object, name, { + value, + writable: true, + enumerable: true, + configurable: true, + }); + return object; +} diff --git a/src/extended-attributes/Global.js b/src/extended-attributes/Global.js new file mode 100644 index 0000000..4d01717 --- /dev/null +++ b/src/extended-attributes/Global.js @@ -0,0 +1,20 @@ +/** + * @param {string | string[]} nameOrNames + * @returns {(t: "value", n: string, v: any) => void} + */ +export default function Global(nameOrNames) { + const names = Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames]; + return (t, n, v) => { + for (const name of names) { + if (isInstanceOf(globalThis, name)) { + Object.defineProperty(globalThis, n, { + value: v, + writable: true, + configurable: true, + enumerable: true, + }); + return; + } + } + }; +} diff --git a/src/index.ts b/src/index.js similarity index 63% rename from src/index.ts rename to src/index.js index c40b671..d60147f 100644 --- a/src/index.ts +++ b/src/index.js @@ -1,3 +1,3 @@ export * as types from "./types/index.js"; -export { default as interface_ } from "./interface_.js"; +export { default as interface_ } from "./interface.js"; export { default as operation } from "./operation.js"; diff --git a/src/interface_.ts b/src/interface_.ts deleted file mode 100644 index 3ec046c..0000000 --- a/src/interface_.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Class } from "./internal/REF-type-fest/basic.js"; - -export default function interface_>( - name: string, - Class: T -): T { - Object.defineProperty(Class, "name", { value: name, configurable: true }); - Object.defineProperty(Class.prototype, Symbol.toStringTag, { - value: name, - configurable: true, - }); - - if (/\Wconstructor\(/.test(Function.prototype.toString.call(Class))) { - return Class; - } else { - Object.defineProperty(Class, "length", { value: 0, configurable: true }); - return new Proxy(Class, { - construct() { - throw new TypeError(`Illegal constructor`); - }, - }); - } -} diff --git a/src/internal/REF-type-fest/README.md b/src/internal/REF-type-fest/README.md deleted file mode 100644 index 34c2849..0000000 --- a/src/internal/REF-type-fest/README.md +++ /dev/null @@ -1 +0,0 @@ -https://github.com/sindresorhus/type-fest#readme diff --git a/src/internal/REF-type-fest/basic.d.ts b/src/internal/REF-type-fest/basic.d.ts deleted file mode 100644 index 41c972d..0000000 --- a/src/internal/REF-type-fest/basic.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Matches a - * [`class`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes). - * - * @category Class - */ -export type Class = Constructor< - T, - Arguments -> & { prototype: T }; - -/** - * Matches a [`class` - * constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes). - * - * @category Class - */ -export type Constructor = new ( - ...arguments_: Arguments -) => T; diff --git a/src/internal/isInstanceOf.js b/src/internal/isInstanceOf.js new file mode 100644 index 0000000..e37b19b --- /dev/null +++ b/src/internal/isInstanceOf.js @@ -0,0 +1,8 @@ +export function isInstanceOf(value, classOrClassName) { + for (let p = value; p; p = Object.getPrototypeOf(p)) { + if (Object.prototype.toString.call(p).slice(8, -1) === classOrClassName) { + return true; + } + } + return false; +} diff --git a/src/operation.ts b/src/operation.ts deleted file mode 100644 index 143cf6d..0000000 --- a/src/operation.ts +++ /dev/null @@ -1,34 +0,0 @@ -type Coercer = { from(x: unknown): T }; - -function operation< - R extends Coercer, - A extends Coercer[], - F extends (...a: any[]) => any ->(returnType: R, argumentTypes: A, function_: F): F { - function w(this: ThisType, ...a: Parameters): ReturnType { - if (a.length < function_.length) { - throw new TypeError( - `Failed to execute '${function_.name}' on '${this}': ${ - function_.length - } argument${function_.length === 1 ? "" : "s"} required, but only ${ - a.length - } present.` - ); - } - - const c = a.map((x, i) => - i < argumentTypes.length ? argumentTypes[i].from(x) : x - ); - const r = function_.call(this, ...c); - return returnType.from(r) as ReturnType; - } - const s = Object.getOwnPropertyDescriptors(function_); - delete s.arguments; - delete s.caller; - delete s.callee; - delete s.prototype; - Object.defineProperties(w, s); - return w as F; -} - -export default operation; diff --git a/src/types/DOMString.js b/src/types/DOMString.js new file mode 100644 index 0000000..08af636 --- /dev/null +++ b/src/types/DOMString.js @@ -0,0 +1,11 @@ +/** @typedef {string} DOMString */ +const DOMString = { + /** + * @param {unknown} x + * @returns {DOMString} + */ + from(x) { + return `${x}`; + }, +}; +export default DOMString; diff --git a/src/types/DOMString.ts b/src/types/DOMString.ts deleted file mode 100644 index 09264a3..0000000 --- a/src/types/DOMString.ts +++ /dev/null @@ -1,8 +0,0 @@ -type DOMString = string; -const DOMString = { - from(x: unknown): DOMString { - return `${x}`; - }, -}; - -export default DOMString; diff --git a/src/types/index.ts b/src/types/index.js similarity index 63% rename from src/types/index.ts rename to src/types/index.js index d938506..7273233 100644 --- a/src/types/index.ts +++ b/src/types/index.js @@ -1,3 +1,3 @@ export { default as DOMString } from "./DOMString.js"; export { default as long } from "./long.js"; -export { default as undefined_ } from "./undefined_.js"; +export { default as undefined } from "./undefined.js"; diff --git a/src/types/long.ts b/src/types/long.js similarity index 99% rename from src/types/long.ts rename to src/types/long.js index e7f80b6..819d9da 100644 --- a/src/types/long.ts +++ b/src/types/long.js @@ -5,5 +5,4 @@ const long = { return Math.trunc(x); }, }; - export default long; diff --git a/src/types/undefined_.ts b/src/types/undefined.js similarity index 99% rename from src/types/undefined_.ts rename to src/types/undefined.js index a6b0d95..d8ae7cb 100644 --- a/src/types/undefined_.ts +++ b/src/types/undefined.js @@ -4,5 +4,4 @@ const undefined_ = { return undefined; }, }; - export default undefined_; diff --git a/test/types/index.test.ts b/test/types/index.test.ts deleted file mode 100644 index b9cf6d1..0000000 --- a/test/types/index.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import test from "node:test"; -import assert from "node:assert"; -import * as types from "../../src/types/index.js"; - -test("types", async (t) => { - console.log(types); -}); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index a587366..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "outDir": "dist", - "declaration": true, - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "nodenext", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "isolatedModules": true, - "strict": true, - "skipLibCheck": true - }, - "include": ["src"], - "exclude": ["node_modules", "dist"], - "typedocOptions": { - "entryPoints": ["src/index.ts"], - "out": "docs/dist", - "skipErrorChecking": true - } -} diff --git a/wiki/Decorators.md b/wiki/Decorators.md deleted file mode 100644 index 0a5566d..0000000 --- a/wiki/Decorators.md +++ /dev/null @@ -1,105 +0,0 @@ -https://github.com/tc39/proposal-decorators - -```ts -type Decorator = ( - value: Input, - context: { - kind: string; - name: string | symbol; - access: { - get?(): unknown; - set?(value: unknown): void; - }; - private?: boolean; - static?: boolean; - addInitializer?(initializer: () => void): void; - } -) => Output | void; -``` - -```ts -type ClassMethodDecorator = ( - value: Function, - context: { - kind: "method"; - name: string | symbol; - access: { get(): unknown }; - static: boolean; - private: boolean; - addInitializer(initializer: () => void): void; - } -) => Function | void; -``` - -```ts -type ClassGetterDecorator = ( - value: Function, - context: { - kind: "getter"; - name: string | symbol; - access: { get(): unknown }; - static: boolean; - private: boolean; - addInitializer(initializer: () => void): void; - } -) => Function | void; -``` - -```ts -type ClassSetterDecorator = ( - value: Function, - context: { - kind: "setter"; - name: string | symbol; - access: { set(value: unknown): void }; - static: boolean; - private: boolean; - addInitializer(initializer: () => void): void; - } -) => Function | void; -``` - -```ts -type ClassFieldDecorator = ( - value: undefined, - context: { - kind: "field"; - name: string | symbol; - access: { get(): unknown; set(value: unknown): void }; - static: boolean; - private: boolean; - } -) => (initialValue: unknown) => unknown | void; -``` - -```ts -type ClassDecorator = ( - value: Function, - context: { - kind: "class"; - name: string | undefined; - addInitializer(initializer: () => void): void; - } -) => Function | void; -``` - -```ts -type ClassAutoAccessorDecorator = ( - value: { - get: () => unknown; - set(value: unknown) => void; - }, - context: { - kind: "accessor"; - name: string | symbol; - access: { get(): unknown, set(value: unknown): void }; - static: boolean; - private: boolean; - addInitializer(initializer: () => void): void; - } -) => { - get?: () => unknown; - set?: (value: unknown) => void; - init?: (initialValue: unknown) => unknown; -} | void; -```