diff options
author | 2022-03-07 15:36:22 -0600 | |
---|---|---|
committer | 2022-03-07 15:36:22 -0600 | |
commit | f18ee36dc0abdc5c8ec87734de7962966d16fe65 (patch) | |
tree | c01a7034186cb0bbe5e1d042f4a5dd09bad21ed5 /packages/webapi/src | |
parent | 10a9c3412b4f6e8607687a74eafdb150d3222047 (diff) | |
download | astro-f18ee36dc0abdc5c8ec87734de7962966d16fe65.tar.gz astro-f18ee36dc0abdc5c8ec87734de7962966d16fe65.tar.zst astro-f18ee36dc0abdc5c8ec87734de7962966d16fe65.zip |
Add `@astrojs/webapi` package (#2729)@astrojs/webapi@0.11.0
* chore: add @astrojs/webapi
* chore: update package.json
* fix: update file case
* fix: remove lowercase file
* chore: update tests to use mocha
* chore: update LICENSE
Diffstat (limited to 'packages/webapi/src')
37 files changed, 2197 insertions, 0 deletions
diff --git a/packages/webapi/src/exclusions.ts b/packages/webapi/src/exclusions.ts new file mode 100644 index 000000000..d5a00f84f --- /dev/null +++ b/packages/webapi/src/exclusions.ts @@ -0,0 +1,17 @@ +const exclusionsForHTMLElement = [ 'CustomElementsRegistry', 'HTMLElement', 'HTMLBodyElement', 'HTMLCanvasElement', 'HTMLDivElement', 'HTMLHeadElement', 'HTMLHtmlElement', 'HTMLImageElement', 'HTMLStyleElement', 'HTMLTemplateElement', 'HTMLUnknownElement', 'Image' ] +const exclusionsForElement = [ 'Element', ...exclusionsForHTMLElement ] as const +const exclusionsForDocument = [ 'CustomElementsRegistry', 'Document', 'HTMLDocument', 'document', 'customElements' ] as const +const exclusionsForNode = [ 'Node', 'DocumentFragment', 'ShadowRoot', ...exclusionsForDocument, ...exclusionsForElement ] as const +const exclusionsForEventTarget = [ 'AbortSignal', 'Event', 'CustomEvent', 'EventTarget', 'OffscreenCanvas', 'MediaQueryList', 'Window', ...exclusionsForNode ] as const +const exclusionsForEvent = [ 'AbortSignal', 'Event', 'CustomEvent', 'EventTarget', 'MediaQueryList', 'OffscreenCanvas', 'Window', ...exclusionsForNode ] as const + +export const exclusions = { + 'Blob+': [ 'Blob', 'File' ], + 'Document+': exclusionsForDocument, + 'Element+': exclusionsForElement, + 'Event+': exclusionsForEvent, + 'EventTarget+': exclusionsForEventTarget, + 'HTMLElement+': exclusionsForHTMLElement, + 'Node+': exclusionsForNode, + 'StyleSheet+': [ 'StyleSheet', 'CSSStyleSheet' ], +} diff --git a/packages/webapi/src/inheritence.ts b/packages/webapi/src/inheritence.ts new file mode 100644 index 000000000..f3b1476ae --- /dev/null +++ b/packages/webapi/src/inheritence.ts @@ -0,0 +1,27 @@ +export const inheritence = { + CSSStyleSheet: 'StyleSheet', + CustomEvent: 'Event', + DOMException: 'Error', + Document: 'Node', + DocumentFragment: 'Node', + Element: 'Node', + File: 'Blob', + HTMLDocument: 'Document', + HTMLElement: 'Element', + HTMLBodyElement: 'HTMLElement', + HTMLCanvasElement: 'HTMLElement', + HTMLDivElement: 'HTMLElement', + HTMLHeadElement: 'HTMLElement', + HTMLHtmlElement: 'HTMLElement', + HTMLImageElement: 'HTMLElement', + HTMLSpanElement: 'HTMLElement', + HTMLStyleElement: 'HTMLElement', + HTMLTemplateElement: 'HTMLElement', + HTMLUnknownElement: 'HTMLElement', + Image: 'HTMLElement', + MediaQueryList: 'EventTarget', + Node: 'EventTarget', + OffscreenCanvas: 'EventTarget', + ShadowRoot: 'DocumentFragment', + Window: 'EventTarget', +} as const diff --git a/packages/webapi/src/lib/Alert.ts b/packages/webapi/src/lib/Alert.ts new file mode 100644 index 000000000..0aacd645f --- /dev/null +++ b/packages/webapi/src/lib/Alert.ts @@ -0,0 +1,3 @@ +export function alert(...messages: any[]) { + console.log(...messages) +} diff --git a/packages/webapi/src/lib/AnimationFrame.ts b/packages/webapi/src/lib/AnimationFrame.ts new file mode 100644 index 000000000..744c445bf --- /dev/null +++ b/packages/webapi/src/lib/AnimationFrame.ts @@ -0,0 +1,35 @@ +import { setTimeout as nodeSetTimeout, clearTimeout as nodeClearTimeout } from 'node:timers' +import * as _ from './utils.js' + +const INTERNAL = { tick: 0, pool: new Map } + +export function requestAnimationFrame<TArgs extends any[], TFunc extends (...args: TArgs) => any>(callback: TFunc): number { + if (!INTERNAL.pool.size) { + nodeSetTimeout(() => { + const next = _.__performance_now() + + for (const func of INTERNAL.pool.values()) { + func(next) + } + + INTERNAL.pool.clear() + }, 1000 / 16) + } + + const func = _.__function_bind(callback, undefined) + const tick = ++INTERNAL.tick + + INTERNAL.pool.set(tick, func) + + return tick +} + +export function cancelAnimationFrame(requestId: number): void { + const timeout = INTERNAL.pool.get(requestId) + + if (timeout) { + nodeClearTimeout(timeout) + + INTERNAL.pool.delete(requestId) + } +} diff --git a/packages/webapi/src/lib/Base64.ts b/packages/webapi/src/lib/Base64.ts new file mode 100644 index 000000000..593c56ae0 --- /dev/null +++ b/packages/webapi/src/lib/Base64.ts @@ -0,0 +1,7 @@ +export function atob(data: string): string { + return Buffer.from(data, 'base64').toString('binary') +} + +export function btoa(data: string): string { + return Buffer.from(data, 'binary').toString('base64') +} diff --git a/packages/webapi/src/lib/CanvasRenderingContext2D.ts b/packages/webapi/src/lib/CanvasRenderingContext2D.ts new file mode 100644 index 000000000..d83a227aa --- /dev/null +++ b/packages/webapi/src/lib/CanvasRenderingContext2D.ts @@ -0,0 +1,182 @@ +import type { HTMLCanvasElement } from './HTMLCanvasElement' +import type { OffscreenCanvas } from './OffscreenCanvas' + +import * as _ from './utils' +import { ImageData } from './ImageData' + +export class CanvasRenderingContext2D { + get canvas(): HTMLCanvasElement | OffscreenCanvas | null { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'canvas').canvas + } + + get direction(): 'ltr' | 'rtl' | 'inherit' { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'direction').direction + } + + get fillStyle(): string { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'fillStyle').fillStyle + } + + get filter(): string { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'filter').filter + } + + get globalAlpha(): number { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'globalAlpha').globalAlpha + } + + get globalCompositeOperation(): string { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'globalCompositeOperation').globalCompositeOperation + } + + get font(): string { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'font').font + } + + get imageSmoothingEnabled(): boolean { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'imageSmoothingEnabled').imageSmoothingEnabled + } + + get imageSmoothingQuality(): 'low' | 'medium' | 'high' { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'imageSmoothingQuality').imageSmoothingQuality + } + + get lineCap(): 'butt' | 'round' | 'square' { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'lineCap').lineCap + } + + get lineDashOffset(): number { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'lineDashOffset').lineDashOffset + } + + get lineJoin(): 'bevel' | 'round' | 'miter' { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'lineJoin').lineJoin + } + + get lineWidth(): number { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'lineWidth').lineWidth + } + + get miterLimit(): number { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'miterLimit').miterLimit + } + + get strokeStyle(): string { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'strokeStyle').strokeStyle + } + + get shadowOffsetX(): number { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'shadowOffsetX').shadowOffsetX + } + + get shadowOffsetY(): number { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'shadowOffsetY').shadowOffsetY + } + + get shadowBlur(): number { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'shadowBlur').shadowBlur + } + + get shadowColor(): string { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'shadowColor').shadowColor + } + + get textAlign(): 'left' | 'right' | 'center' | 'start' | 'end' { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'textAlign').textAlign + } + + get textBaseline(): 'top' | 'hanging' | 'middle' | 'alphabetic' | 'ideographic' | 'bottom' { + return _.internalsOf(this, 'CanvasRenderingContext2D', 'textBaseline').textBaseline + } + + arc() {} + arcTo() {} + beginPath() {} + bezierCurveTo() {} + clearRect() {} + clip() {} + closePath() {} + + createImageData(width: number, height: number): void + createImageData(imagedata: ImageData): void + + createImageData(arg0: number | ImageData, arg1?: void | number) { + /** Whether ImageData is provided. */ + const hasData = _.__object_isPrototypeOf(ImageData.prototype, arg0) + + const w = hasData ? (arg0 as ImageData).width : arg0 as number + const h = hasData ? (arg0 as ImageData).height : arg1 as number + const d = hasData ? (arg0 as ImageData).data : new Uint8ClampedArray(w * h * 4) + + return new ImageData(d, w, h) + } + + createLinearGradient() {} + createPattern() {} + createRadialGradient() {} + drawFocusIfNeeded() {} + drawImage() {} + ellipse() {} + fill() {} + fillRect() {} + fillText() {} + getContextAttributes() {} + getImageData() {} + getLineDash() {} + getTransform() {} + isPointInPath() {} + isPointInStroke() {} + lineTo() {} + measureText() {} + moveTo() {} + putImageData() {} + quadraticCurveTo() {} + rect() {} + resetTransform() {} + restore() {} + rotate() {} + save() {} + scale() {} + setLineDash() {} + setTransform() {} + stroke() {} + strokeRect() {} + strokeText() {} + transform() {} + translate() {} +} + +_.allowStringTag(CanvasRenderingContext2D) + +export const __createCanvasRenderingContext2D = (canvas: EventTarget): CanvasRenderingContext2D => { + const renderingContext2D = Object.create(CanvasRenderingContext2D.prototype) as CanvasRenderingContext2D + + _.INTERNALS.set(renderingContext2D, { + canvas, + direction: 'inherit', + fillStyle: '#000', + filter: 'none', + font: '10px sans-serif', + globalAlpha: 0, + globalCompositeOperation: 'source-over', + imageSmoothingEnabled: false, + imageSmoothingQuality: 'high', + lineCap: 'butt', + lineDashOffset: 0.0, + lineJoin: 'miter', + lineWidth: 1.0, + miterLimit: 10.0, + shadowBlur: 0, + shadowColor: '#000', + shadowOffsetX: 0, + shadowOffsetY: 0, + strokeStyle: '#000', + textAlign: 'start', + textBaseline: 'alphabetic', + }) + + return renderingContext2D +} + +/** Returns whether the value is an instance of ImageData. */ +const isImageData = <T>(value: T) => (Object(value).data instanceof Uint8ClampedArray) as T extends ImageData ? true : false diff --git a/packages/webapi/src/lib/CharacterData.ts b/packages/webapi/src/lib/CharacterData.ts new file mode 100644 index 000000000..8bbcce12d --- /dev/null +++ b/packages/webapi/src/lib/CharacterData.ts @@ -0,0 +1,36 @@ +import * as _ from './utils' + +export class CharacterData extends Node { + constructor(data: string) { + _.INTERNALS.set(super(), { + data: String(data), + } as CharacterDataInternals) + } + get data(): string { + return _.internalsOf<CharacterDataInternals>(this, 'CharacterData', 'data').data + } + + get textContent(): string { + return _.internalsOf<CharacterDataInternals>(this, 'CharacterData', 'textContent').data + } +} + +export class Comment extends CharacterData {} + +export class Text extends CharacterData { + get assignedSlot(): HTMLSlotElement | null { + return null + } + + get wholeText(): string { + return _.internalsOf<CharacterDataInternals>(this, 'CharacterData', 'textContent').data + } +} + +_.allowStringTag(CharacterData) +_.allowStringTag(Text) +_.allowStringTag(Comment) + +interface CharacterDataInternals { + data: string +} diff --git a/packages/webapi/src/lib/ContextEvent.ts b/packages/webapi/src/lib/ContextEvent.ts new file mode 100644 index 000000000..98cd72de5 --- /dev/null +++ b/packages/webapi/src/lib/ContextEvent.ts @@ -0,0 +1,52 @@ +import { Event } from 'event-target-shim' + +/** An event fired by a context requester to signal it desires a named context. */ +export class ContextEvent<T = unknown> extends Event<'context-request'> { + constructor(init: ContextEventInit<T>) { + super('context-request', { bubbles: true, composed: true }) + + init = Object(init) as Required<ContextEventInit<T>> + + this.context = init.context + } + + context!: Context<T> + multiple!: boolean + callback!: ContextCallback<Context<T>> +} + +interface ContextEventInit<T = unknown> { + context: Context<T> + multiple?: boolean + callback: ContextCallback<Context<T>> +} + +/** A Context object defines an optional initial value for a Context, as well as a name identifier for debugging purposes. */ +export type Context<T = unknown> = { + name: string + initialValue?: T +} + +/** A helper type which can extract a Context value type from a Context type. */ +export type ContextType<T extends Context> = T extends Context<infer Y> ? Y : never + +/** A function which creates a Context value object */ +export function createContext<T>(name: string, initialValue?: T): Readonly<Context<T>> { + return { + name, + initialValue, + } +} + +/** A callback which is provided by a context requester and is called with the value satisfying the request. */ +export type ContextCallback<ValueType> = ( + value: ValueType, + dispose?: () => void +) => void + +declare global { + interface HTMLElementEventMap { + /** A 'context-request' event can be emitted by any element which desires a context value to be injected by an external provider. */ + 'context-request': ContextEvent + } +} diff --git a/packages/webapi/src/lib/CustomElementRegistry.ts b/packages/webapi/src/lib/CustomElementRegistry.ts new file mode 100644 index 000000000..650435c17 --- /dev/null +++ b/packages/webapi/src/lib/CustomElementRegistry.ts @@ -0,0 +1,58 @@ +import * as _ from './utils' + +export class CustomElementRegistry { + /** Defines a new custom element using the given tag name and HTMLElement constructor. */ + define(name: string, constructor: Function, options?: ElementDefinitionOptions) { + const internals = _.internalsOf<CustomElementRegistryInternals>(this, 'CustomElementRegistry', 'define') + + name = String(name) + + if (/[A-Z]/.test(name)) throw new SyntaxError('Custom element name cannot contain an uppercase ASCII letter') + if (!/^[a-z]/.test(name)) throw new SyntaxError('Custom element name must have a lowercase ASCII letter as its first character') + if (!/-/.test(name)) throw new SyntaxError('Custom element name must contain a hyphen') + + internals.constructorByName.set(name, constructor) + internals.nameByConstructor.set(constructor, name) + + void options + } + + /** Returns the constructor associated with the given tag name. */ + get(name: string) { + const internals = _.internalsOf<CustomElementRegistryInternals>(this, 'CustomElementRegistry', 'get') + + name = String(name).toLowerCase() + + return internals.constructorByName.get(name) + } + + getName(constructor: Function) { + const internals = _.internalsOf<CustomElementRegistryInternals>(this, 'CustomElementRegistry', 'getName') + + return internals.nameByConstructor.get(constructor) + } +} + +_.allowStringTag(CustomElementRegistry) + +interface CustomElementRegistryInternals { + constructorByName: Map<string, Function>; + nameByConstructor: Map<Function, string>; +} + +interface ElementDefinitionOptions { + extends?: string | undefined; +} + +export const initCustomElementRegistry = (target: Record<any, any>, exclude: Set<string>) => { + if (exclude.has('customElements')) return + + const CustomElementRegistry = target.CustomElementRegistry || globalThis.CustomElementRegistry + + const customElements: CustomElementRegistry = target.customElements = Object.create(CustomElementRegistry.prototype) + + _.INTERNALS.set(customElements, { + constructorByName: new Map, + nameByConstructor: new Map, + } as CustomElementRegistryInternals) +} diff --git a/packages/webapi/src/lib/CustomEvent.ts b/packages/webapi/src/lib/CustomEvent.ts new file mode 100644 index 000000000..951ca8e81 --- /dev/null +++ b/packages/webapi/src/lib/CustomEvent.ts @@ -0,0 +1,24 @@ +import * as _ from './utils' +import { Event } from 'event-target-shim' + +class CustomEvent<TEventType extends string = string> extends Event<TEventType> { + constructor(type: TEventType, params?: CustomEventInit) { + params = Object(params) as Required<CustomEventInit> + + super(type, params) + + if ('detail' in params) this.detail = params.detail + } + + detail!: any +} + +_.allowStringTag(CustomEvent) + +export { CustomEvent } + +interface CustomEventInit { + bubbles?: boolean + cancelable?: false + detail?: any +} diff --git a/packages/webapi/src/lib/DOMException.ts b/packages/webapi/src/lib/DOMException.ts new file mode 100644 index 000000000..539871c6b --- /dev/null +++ b/packages/webapi/src/lib/DOMException.ts @@ -0,0 +1,40 @@ +import * as _ from './utils' + +export class DOMException extends Error { + constructor(message = '', name = 'Error') { + super(message) + + this.code = 0 + this.name = name + } + + code!: number + + static INDEX_SIZE_ERR = 1 + static DOMSTRING_SIZE_ERR = 2 + static HIERARCHY_REQUEST_ERR = 3 + static WRONG_DOCUMENT_ERR = 4 + static INVALID_CHARACTER_ERR = 5 + static NO_DATA_ALLOWED_ERR = 6 + static NO_MODIFICATION_ALLOWED_ERR = 7 + static NOT_FOUND_ERR = 8 + static NOT_SUPPORTED_ERR = 9 + static INUSE_ATTRIBUTE_ERR = 10 + static INVALID_STATE_ERR = 11 + static SYNTAX_ERR = 12 + static INVALID_MODIFICATION_ERR = 13 + static NAMESPACE_ERR = 14 + static INVALID_ACCESS_ERR = 15 + static VALIDATION_ERR = 16 + static TYPE_MISMATCH_ERR = 17 + static SECURITY_ERR = 18 + static NETWORK_ERR = 19 + static ABORT_ERR = 20 + static URL_MISMATCH_ERR = 21 + static QUOTA_EXCEEDED_ERR = 22 + static TIMEOUT_ERR = 23 + static INVALID_NODE_TYPE_ERR = 24 + static DATA_CLONE_ERR = 25 +} + +_.allowStringTag(DOMException) diff --git a/packages/webapi/src/lib/Document.ts b/packages/webapi/src/lib/Document.ts new file mode 100644 index 000000000..867cc4448 --- /dev/null +++ b/packages/webapi/src/lib/Document.ts @@ -0,0 +1,159 @@ +import * as _ from './utils' +import { Text } from './CharacterData' +import { TreeWalker } from './TreeWalker' + +export class Document extends Node { + createElement(name: string) { + const internals = _.internalsOf<DocumentInternals>(this, 'Document', 'createElement') + + const customElementInternals: CustomElementRegistryInternals = _.INTERNALS.get(internals.target.customElements) + + name = String(name).toLowerCase() + + const TypeOfHTMLElement = internals.constructorByName.get(name) || (customElementInternals && customElementInternals.constructorByName.get(name)) || HTMLUnknownElement + + const element = Object.setPrototypeOf(new EventTarget(), TypeOfHTMLElement.prototype) as HTMLElement + + _.INTERNALS.set(element, { + attributes: {}, + localName: name, + ownerDocument: this, + shadowInit: null as unknown as ShadowRootInit, + shadowRoot: null as unknown as ShadowRoot, + } as ElementInternals) + + return element + } + + createNodeIterator(root: Node, whatToShow: number = NodeFilter.SHOW_ALL, filter?: NodeIteratorInternals['filter']) { + const target = Object.create(NodeIterator.prototype) + + _.INTERNALS.set(target, { filter, pointerBeforeReferenceNode: false, referenceNode: root, root, whatToShow } as NodeIteratorInternals) + + return target + } + + createTextNode(data: string) { + return new Text(data) + } + + createTreeWalker(root: Node, whatToShow: number = NodeFilter.SHOW_ALL, filter?: NodeFilter, expandEntityReferences?: boolean) { + const target = Object.create(TreeWalker.prototype) + + _.INTERNALS.set(target, { filter, currentNode: root, root, whatToShow } as TreeWalkerInternals) + + return target + } + + get adoptedStyleSheets(): StyleSheet[] { + return [] + } + + get styleSheets(): StyleSheet[] { + return [] + } + + body!: HTMLBodyElement + documentElement!: HTMLHtmlElement + head!: HTMLHeadElement +} + +export class HTMLDocument extends Document {} + +_.allowStringTag(Document) +_.allowStringTag(HTMLDocument) + +export const initDocument = (target: Target, exclude: Set<string>) => { + if (exclude.has('document')) return + + const EventTarget = target.EventTarget || globalThis.EventTarget + const HTMLDocument = target.HTMLDocument || globalThis.HTMLDocument + + const document: HTMLDocument = target.document = Object.setPrototypeOf(new EventTarget(), HTMLDocument.prototype) + + _.INTERNALS.set(document, { + target, + constructorByName: new Map<string, Function>([ + ['body', target.HTMLBodyElement], + ['canvas', target.HTMLCanvasElement], + ['div', target.HTMLDivElement], + ['head', target.HTMLHeadElement], + ['html', target.HTMLHtmlElement], + ['img', target.HTMLImageElement], + ['span', target.HTMLSpanElement], + ['style', target.HTMLStyleElement], + ]), + nameByConstructor: new Map, + } as DocumentInternals) + + const initElement = (name: string, Class: Function) => { + const target = Object.setPrototypeOf(new EventTarget(), Class.prototype) + + _.INTERNALS.set(target, { + attributes: {}, + localName: name, + ownerDocument: document, + shadowRoot: null as unknown as ShadowRoot, + shadowInit: null as unknown as ShadowRootInit, + } as ElementInternals) + + return target + } + + document.body = initElement('body', target.HTMLBodyElement) as HTMLBodyElement + document.head = initElement('head', target.HTMLHeadElement) as HTMLHeadElement + document.documentElement = initElement('html', target.HTMLHtmlElement) as HTMLHtmlElement +} + +interface DocumentInternals { + body: HTMLBodyElement + documentElement: HTMLHtmlElement + head: HTMLHeadElement + constructorByName: Map<string, Function> + nameByConstructor: Map<Function, string> + target: Target +} + +interface CustomElementRegistryInternals { + constructorByName: Map<string, Function> + nameByConstructor: Map<Function, string> +} + +interface ElementInternals { + attributes: { [name: string]: string }, + localName: string + ownerDocument: Document + shadowRoot: ShadowRoot + shadowInit: ShadowRootInit +} + +interface ShadowRootInit extends Record<any, any> { + mode?: string +} + +interface Target extends Record<any, any> { + HTMLBodyElement: typeof HTMLBodyElement + HTMLDivElement: typeof HTMLDivElement + HTMLElement: typeof HTMLElement + HTMLHeadElement: typeof HTMLHeadElement + HTMLHtmlElement: typeof HTMLHtmlElement + HTMLSpanElement: typeof HTMLSpanElement + HTMLStyleElement: typeof HTMLStyleElement + customElements: CustomElementRegistry + document: DocumentInternals +} + +interface NodeIteratorInternals { + filter: NodeFilter + pointerBeforeReferenceNode: boolean + referenceNode: Node + root: Node + whatToShow: number +} + +interface TreeWalkerInternals { + filter: NodeFilter + currentNode: Node + root: Node + whatToShow: number +} diff --git a/packages/webapi/src/lib/Element.ts b/packages/webapi/src/lib/Element.ts new file mode 100644 index 000000000..ec096fb6d --- /dev/null +++ b/packages/webapi/src/lib/Element.ts @@ -0,0 +1,118 @@ +import * as _ from './utils' + +export class Element extends Node { + hasAttribute(name: string): boolean { + void name + + return false + } + + getAttribute(name: string): string | null { + return null + } + + setAttribute(name: string, value: string): void { + void name + void value + } + + removeAttribute(name: string): void { + void name + } + + attachShadow(init: Partial<ShadowRootInit>) { + if (arguments.length < 1) throw new TypeError(`Failed to execute 'attachShadow' on 'Element': 1 argument required, but only 0 present.`) + + if (init !== Object(init)) throw new TypeError(`Failed to execute 'attachShadow' on 'Element': The provided value is not of type 'ShadowRootInit'.`) + + if (init.mode !== 'open' && init.mode !== 'closed') throw new TypeError(`Failed to execute 'attachShadow' on 'Element': Failed to read the 'mode' property from 'ShadowRootInit': The provided value '${init.mode}' is not a valid enum value of type ShadowRootMode.`) + + const internals = _.internalsOf<ElementInternals>(this, 'Element', 'attachShadow') + + if (internals.shadowRoot) throw new Error('The operation is not supported.') + + internals.shadowInit = internals.shadowInit || { + mode: init.mode, + delegatesFocus: Boolean(init.delegatesFocus), + } + + internals.shadowRoot = internals.shadowRoot || (/^open$/.test(internals.shadowInit.mode as string) ? Object.setPrototypeOf(new EventTarget(), ShadowRoot.prototype) as ShadowRoot : null) + + return internals.shadowRoot + } + + get assignedSlot(): HTMLSlotElement | null { + return null + } + + get innerHTML(): string { + _.internalsOf<ElementInternals>(this, 'Element', 'innerHTML') + + return '' + } + + set innerHTML(value) { + _.internalsOf<ElementInternals>(this, 'Element', 'innerHTML') + + void value + } + + get shadowRoot(): ShadowRoot | null { + const internals = _.internalsOf<ElementInternals>(this, 'Element', 'shadowRoot') + + return Object(internals.shadowInit).mode === 'open' ? internals.shadowRoot : null + } + + get localName(): string { + return _.internalsOf<ElementInternals>(this, 'Element', 'localName').localName as string + } + + get nodeName(): string { + return (_.internalsOf<ElementInternals>(this, 'Element', 'nodeName').localName as string).toUpperCase() + } + + get tagName(): string { + return (_.internalsOf<ElementInternals>(this, 'Element', 'tagName').localName as string).toUpperCase() + } +} + +export class HTMLElement extends Element {} + +export class HTMLBodyElement extends HTMLElement {} + +export class HTMLDivElement extends HTMLElement {} + +export class HTMLHeadElement extends HTMLElement {} + +export class HTMLHtmlElement extends HTMLElement {} + +export class HTMLSpanElement extends HTMLElement {} + +export class HTMLStyleElement extends HTMLElement {} + +export class HTMLTemplateElement extends HTMLElement {} + +export class HTMLUnknownElement extends HTMLElement {} + +_.allowStringTag(Element) +_.allowStringTag(HTMLElement) +_.allowStringTag(HTMLBodyElement) +_.allowStringTag(HTMLDivElement) +_.allowStringTag(HTMLHeadElement) +_.allowStringTag(HTMLHtmlElement) +_.allowStringTag(HTMLSpanElement) +_.allowStringTag(HTMLStyleElement) +_.allowStringTag(HTMLTemplateElement) +_.allowStringTag(HTMLUnknownElement) + +export interface ElementInternals { + attributes: { [name: string]: string }, + localName?: string + shadowRoot: ShadowRoot | null + shadowInit: ShadowRootInit | void +} + +export interface ShadowRootInit { + mode: 'open' | 'closed' + delegatesFocus: boolean +}
\ No newline at end of file diff --git a/packages/webapi/src/lib/HTMLCanvasElement.ts b/packages/webapi/src/lib/HTMLCanvasElement.ts new file mode 100644 index 000000000..2558490a4 --- /dev/null +++ b/packages/webapi/src/lib/HTMLCanvasElement.ts @@ -0,0 +1,57 @@ +import type { CanvasRenderingContext2D } from './CanvasRenderingContext2D' + +import * as _ from './utils' +import { __createCanvasRenderingContext2D } from './CanvasRenderingContext2D' + +export class HTMLCanvasElement extends HTMLElement { + get height(): number { + return _.internalsOf(this, 'HTMLCanvasElement', 'height').height + } + + set height(value) { + _.internalsOf(this, 'HTMLCanvasElement', 'height').height = Number(value) || 0 + } + + get width(): number { + return _.internalsOf(this, 'HTMLCanvasElement', 'width').width + } + + set width(value) { + _.internalsOf(this, 'HTMLCanvasElement', 'width').width = Number(value) || 0 + } + + captureStream(): null { + return null + } + + getContext(contextType: PredefinedContextId): CanvasRenderingContext2D | null { + const internals = _.internalsOf<HTMLCanvasElementInternals>(this, 'HTMLCanvasElement', 'getContext') + + switch (contextType) { + case '2d': + if (internals.renderingContext2D) return internals.renderingContext2D + + internals.renderingContext2D = __createCanvasRenderingContext2D(this) + + return internals.renderingContext2D + default: + return null + } + } + + toBlob() {} + + toDataURL() {} + + transferControlToOffscreen() {} +} + +_.allowStringTag(HTMLCanvasElement) + +interface HTMLCanvasElementInternals { + width: number + height: number + renderingContext2D: CanvasRenderingContext2D +} + +type PredefinedContextId = '2d' | 'bitmaprenderer' | 'webgl' | 'webgl2' | 'webgpu' diff --git a/packages/webapi/src/lib/HTMLImageElement.ts b/packages/webapi/src/lib/HTMLImageElement.ts new file mode 100644 index 000000000..9ddcb0ef5 --- /dev/null +++ b/packages/webapi/src/lib/HTMLImageElement.ts @@ -0,0 +1,16 @@ +import * as _ from './utils' +import { HTMLElement } from './Element' + +export class HTMLImageElement extends HTMLElement { + get src(): string { + return _.internalsOf(this, 'HTMLImageElement', 'src').src + } + + set src(value) { + const internals = _.internalsOf(this, 'HTMLImageElement', 'src') + + internals.src = String(value) + } +} + +_.allowStringTag(HTMLImageElement) diff --git a/packages/webapi/src/lib/IdleCallback.ts b/packages/webapi/src/lib/IdleCallback.ts new file mode 100644 index 000000000..0d0f25bdf --- /dev/null +++ b/packages/webapi/src/lib/IdleCallback.ts @@ -0,0 +1,35 @@ +import { setTimeout as nodeSetTimeout, clearTimeout as nodeClearTimeout } from 'node:timers' +import * as _ from './utils.js' + +const INTERNAL = { tick: 0, pool: new Map } + +export function requestIdleCallback<TArgs extends any[], TFunc extends (...args: TArgs) => any>(callback: TFunc): number { + if (!INTERNAL.pool.size) { + nodeSetTimeout(() => { + const next = _.__performance_now() + + for (const func of INTERNAL.pool.values()) { + func(next) + } + + INTERNAL.pool.clear() + }, 1000 / 16) + } + + const func = _.__function_bind(callback, undefined) + const tick = ++INTERNAL.tick + + INTERNAL.pool.set(tick, func) + + return tick +} + +export function cancelIdleCallback(requestId: number): void { + const timeout = INTERNAL.pool.get(requestId) + + if (timeout) { + nodeClearTimeout(timeout) + + INTERNAL.pool.delete(requestId) + } +} diff --git a/packages/webapi/src/lib/Image.ts b/packages/webapi/src/lib/Image.ts new file mode 100644 index 000000000..d72c33159 --- /dev/null +++ b/packages/webapi/src/lib/Image.ts @@ -0,0 +1,15 @@ +import * as _ from './utils' +import { HTMLImageElement } from './HTMLImageElement' + +export function Image() { + // @ts-ignore + _.INTERNALS.set(this, { + attributes: {}, + localName: 'img', + innerHTML: '', + shadowRoot: null, + shadowInit: null, + }) +} + +Image.prototype = HTMLImageElement.prototype diff --git a/packages/webapi/src/lib/ImageData.ts b/packages/webapi/src/lib/ImageData.ts new file mode 100644 index 000000000..776213880 --- /dev/null +++ b/packages/webapi/src/lib/ImageData.ts @@ -0,0 +1,75 @@ +import * as _ from './utils' + +export class ImageData { + constructor(width: number, height: number); + constructor(width: number, height: number, settings: ImageDataSettings); + constructor(data: Uint8ClampedArray, width: number); + constructor(data: Uint8ClampedArray, width: number, height: number); + constructor(data: Uint8ClampedArray, width: number, height: number, settings: ImageDataSettings); + + constructor(arg0: number | Uint8ClampedArray, arg1: number, ...args: [] | [number] | [ImageDataSettings] | [number, ImageDataSettings]) { + if (arguments.length < 2) throw new TypeError(`Failed to construct 'ImageData': 2 arguments required.`) + + /** Whether Uint8ClampedArray data is provided. */ + const hasData = _.__object_isPrototypeOf(Uint8ClampedArray.prototype, arg0) + + /** Image data, either provided or calculated. */ + const d = hasData ? arg0 as Uint8ClampedArray : new Uint8ClampedArray(asNumber(arg0, 'width') * asNumber(arg1, 'height') * 4) + + /** Image width. */ + const w = asNumber(hasData ? arg1 : arg0, 'width') + + /** Image height. */ + const h = d.length / w / 4 + + /** Image color space. */ + const c = String(Object(hasData ? args[1] : args[0]).colorSpace || 'srgb') as PredefinedColorSpace + + // throw if a provided height does not match the calculated height + if (args.length && asNumber(args[0], 'height') !== h) throw new DOMException('height is not equal to (4 * width * height)', 'IndexSizeError') + + // throw if a provided colorspace does not match a known colorspace + if (c !== 'srgb' && c !== 'rec2020' && c !== 'display-p3') throw new TypeError('colorSpace is not known value') + + Object.defineProperty(this, 'data', { configurable: true, enumerable: true, value: d }) + + _.INTERNALS.set(this, { width: w, height: h, colorSpace: c } as ImageDataInternals) + } + + get data(): Uint8ClampedArray { + _.internalsOf<ImageDataInternals>(this, 'ImageData', 'data') + + return (Object.getOwnPropertyDescriptor(this, 'data') as { value: Uint8ClampedArray }).value + } + + get width(): ImageDataInternals['width'] { + return _.internalsOf<ImageDataInternals>(this, 'ImageData', 'width').width + } + + get height(): ImageDataInternals['height'] { + return _.internalsOf<ImageDataInternals>(this, 'ImageData', 'height').height + } +} + +_.allowStringTag(ImageData) + +/** Returns a coerced number, optionally throwing if the number is zero-ish. */ +const asNumber = (value: any, axis: string): number => { + value = Number(value) || 0 + + if (value === 0) throw new TypeError(`The source ${axis} is zero or not a number.`) + + return value +} + +interface ImageDataInternals { + colorSpace: PredefinedColorSpace + height: number + width: number +} + +interface ImageDataSettings { + colorSpace?: PredefinedColorSpace +} + +type PredefinedColorSpace = 'srgb' | 'rec2020' | 'display-p3' diff --git a/packages/webapi/src/lib/MediaQueryList.ts b/packages/webapi/src/lib/MediaQueryList.ts new file mode 100644 index 000000000..44edd89be --- /dev/null +++ b/packages/webapi/src/lib/MediaQueryList.ts @@ -0,0 +1,37 @@ +import * as _ from './utils' + +export class MediaQueryList extends EventTarget { + get matches(): boolean { + return _.internalsOf(this, 'MediaQueryList', 'matches').matches + } + + get media(): string { + return _.internalsOf(this, 'MediaQueryList', 'media').media + } +} + +_.allowStringTag(MediaQueryList) + +export const initMediaQueryList = (target: Target, exclude: Set<string>) => { + if (exclude.has('MediaQueryList') || exclude.has('matchMedia')) return + + const EventTarget = target.EventTarget || globalThis.EventTarget + const MediaQueryList = target.MediaQueryList || globalThis.MediaQueryList + + target.matchMedia = function matchMedia(media: string) { + const mql = Object.setPrototypeOf(new EventTarget(), MediaQueryList.prototype) as MediaQueryList + + _.INTERNALS.set(mql, { + matches: false, + media, + }) + + return mql + } +} + +interface Target extends Record<any, any> { + matchMedia: { + (media: string): MediaQueryList + } +} diff --git a/packages/webapi/src/lib/Node.ts b/packages/webapi/src/lib/Node.ts new file mode 100644 index 000000000..c2f621950 --- /dev/null +++ b/packages/webapi/src/lib/Node.ts @@ -0,0 +1,166 @@ +import * as _ from './utils' + +export class Node extends EventTarget { + append(...nodesOrDOMStrings: NodeOrString[]): void { + void nodesOrDOMStrings + } + + appendChild(childNode: Node): Node { + return childNode + } + + after(...nodesOrDOMStrings: NodeOrString[]): void { + void nodesOrDOMStrings + } + + before(...nodesOrDOMStrings: NodeOrString[]): void { + void nodesOrDOMStrings + } + + prepend(...nodesOrDOMStrings: NodeOrString[]): void { + void nodesOrDOMStrings + } + + replaceChild(newChild: Node, oldChild: Node): Node { + void newChild + + return oldChild + } + + removeChild(childNode: Node): Node { + return childNode + } + + get attributes(): object { + return {} + } + + get childNodes(): Node[] { + return [] + } + + get children(): Element[] { + return [] + } + + get ownerDocument(): Node | null { + return null + } + + get nodeValue(): string { + return '' + } + + set nodeValue(value: string) { + void value + } + + get textContent(): string { + return '' + } + + set textContent(value: string) { + void value + } + + get previousElementSibling(): Node | null { + return null + } + + get nextElementSibling(): Node | null { + return null + } + + [Symbol.for('nodejs.util.inspect.custom')](depth: number, options: Record<string, any>) { + return `${this.constructor.name}`; + } +} + +export class DocumentFragment extends Node {} + +export class ShadowRoot extends DocumentFragment { + get innerHTML() { + return '' + } + + set innerHTML(value: string) { + void value + } +} + +export const NodeFilter = Object.assign({ + NodeFilter() { + throw new TypeError('Illegal constructor') + } +}.NodeFilter, { + FILTER_ACCEPT: 1, + FILTER_REJECT: 2, + FILTER_SKIP: 3, + SHOW_ALL: 4294967295, + SHOW_ELEMENT: 1, + SHOW_ATTRIBUTE: 2, + SHOW_TEXT: 4, + SHOW_CDATA_SECTION: 8, + SHOW_ENTITY_REFERENCE: 16, + SHOW_ENTITY: 32, + SHOW_PROCESSING_INSTRUCTION: 64, + SHOW_COMMENT: 128, + SHOW_DOCUMENT: 256, + SHOW_DOCUMENT_TYPE: 512, + SHOW_DOCUMENT_FRAGMENT: 1024, + SHOW_NOTATION: 2048, +}) + +export class NodeIterator { + nextNode(): Node | null { + return null + } + + previousNode(): Node | null { + return null + } + + get filter(): NodeFilter { + const internals = _.internalsOf<NodeIteratorInternals>(this, 'NodeIterator', 'filter') + return internals.filter + } + + get pointerBeforeReferenceNode(): boolean { + const internals = _.internalsOf<NodeIteratorInternals>(this, 'NodeIterator', 'pointerBeforeReferenceNode') + return internals.pointerBeforeReferenceNode + } + + get referenceNode(): Node { + const internals = _.internalsOf<NodeIteratorInternals>(this, 'NodeIterator', 'referenceNode') + return internals.referenceNode + } + + get root(): Node { + const internals = _.internalsOf<NodeIteratorInternals>(this, 'NodeIterator', 'root') + return internals.root + } + + get whatToShow(): number { + const internals = _.internalsOf<NodeIteratorInternals>(this, 'NodeIterator', 'whatToShow') + return internals.whatToShow + } +} + +_.allowStringTag(Node) +_.allowStringTag(NodeIterator) +_.allowStringTag(DocumentFragment) +_.allowStringTag(ShadowRoot) + +type NodeOrString = string | Node + +export interface NodeFilter { + acceptNode(node: Node): number +} + +export interface NodeIteratorInternals { + filter: NodeFilter + pointerBeforeReferenceNode: boolean + referenceNode: Node + root: Node + whatToShow: number +} diff --git a/packages/webapi/src/lib/Object.ts b/packages/webapi/src/lib/Object.ts new file mode 100644 index 000000000..5ad5b1388 --- /dev/null +++ b/packages/webapi/src/lib/Object.ts @@ -0,0 +1,20 @@ +import * as _ from './utils' + +export const hasOwn = { + hasOwn(instance: object, property: any) { + return _.__object_hasOwnProperty(instance, property) + } +}.hasOwn + +export const initObject = (target: any, exclude: Set<string>) => { + if (exclude.has('Object') || exclude.has('object') || exclude.has('hasOwn')) return + + const Class = target.Object || globalThis.Object + + Object.defineProperty(Class, 'hasOwn', { + value: hasOwn, + writable: true, + enumerable: false, + configurable: true + }) +} diff --git a/packages/webapi/src/lib/Observer.ts b/packages/webapi/src/lib/Observer.ts new file mode 100644 index 000000000..845de1312 --- /dev/null +++ b/packages/webapi/src/lib/Observer.ts @@ -0,0 +1,41 @@ +import * as _ from './utils' + +export class IntersectionObserver { + disconnect() {} + + observe() {} + + takeRecords() { + return [] + } + + unobserve() {} +} + +export class MutationObserver { + disconnect() {} + + observe() {} + + takeRecords() { + return [] + } + + unobserve() {} +} + +export class ResizeObserver { + disconnect() {} + + observe() {} + + takeRecords() { + return [] + } + + unobserve() {} +} + +_.allowStringTag(MutationObserver) +_.allowStringTag(IntersectionObserver) +_.allowStringTag(ResizeObserver) diff --git a/packages/webapi/src/lib/OffscreenCanvas.ts b/packages/webapi/src/lib/OffscreenCanvas.ts new file mode 100644 index 000000000..01ee5d34a --- /dev/null +++ b/packages/webapi/src/lib/OffscreenCanvas.ts @@ -0,0 +1,80 @@ +import type { CanvasRenderingContext2D } from './CanvasRenderingContext2D' + +import * as _ from './utils' +import { __createCanvasRenderingContext2D } from './CanvasRenderingContext2D' + +export class OffscreenCanvas extends EventTarget { + constructor(width: number, height: number) { + super() + + if (arguments.length < 2) throw new TypeError(`Failed to construct 'OffscreenCanvas': 2 arguments required.`) + + width = Number(width) || 0 + height = Number(height) || 0 + + _.INTERNALS.set(this, { width, height } as OffscreenCanvasInternals) + } + + get height(): number { + return _.internalsOf(this, 'OffscreenCanvas', 'height').height + } + + set height(value) { + _.internalsOf(this, 'OffscreenCanvas', 'height').height = Number(value) || 0 + } + + get width(): number { + return _.internalsOf(this, 'OffscreenCanvas', 'width').width + } + + set width(value) { + _.internalsOf(this, 'OffscreenCanvas', 'width').width = Number(value) || 0 + } + + getContext(contextType: PredefinedContextId): CanvasRenderingContext2D | null { + const internals = _.internalsOf<OffscreenCanvasInternals>(this, 'HTMLCanvasElement', 'getContext') + + switch (contextType) { + case '2d': + if (internals.renderingContext2D) return internals.renderingContext2D + + internals.renderingContext2D = __createCanvasRenderingContext2D(this) + + return internals.renderingContext2D + default: + return null + } + } + + convertToBlob(options: Partial<ConvertToBlobOptions>) { + options = Object(options) + + const quality = Number(options.quality) || 0 + const type = getImageType(String(options.type).trim().toLowerCase()) + + void quality + + return Promise.resolve( + new Blob([], { type }) + ) + } +} + +_.allowStringTag(OffscreenCanvas) + +const getImageType = (type: string): PredefinedImageType => type === 'image/avif' || type === 'image/jpeg' || type === 'image/png' || type === 'image/webp' ? type : 'image/png' + +interface OffscreenCanvasInternals { + height: number + renderingContext2D: CanvasRenderingContext2D + width: number +} + +interface ConvertToBlobOptions { + quality: number + type: PredefinedImageType +} + +type PredefinedContextId = '2d' | 'bitmaprenderer' | 'webgl' | 'webgl2' | 'webgpu' + +type PredefinedImageType = 'image/avif' | 'image/jpeg' | 'image/png' | 'image/webp' diff --git a/packages/webapi/src/lib/Promise.ts b/packages/webapi/src/lib/Promise.ts new file mode 100644 index 000000000..ea2c7c419 --- /dev/null +++ b/packages/webapi/src/lib/Promise.ts @@ -0,0 +1,29 @@ +export const any = { + async any <T>( + iterable: Iterable<T | PromiseLike<T>> + ): Promise<T> { + return Promise.all( + [...iterable].map(promise => { + return new Promise((resolve, reject) => + Promise.resolve(promise).then(reject, resolve) + ) + }) + ).then( + errors => Promise.reject(errors), + value => Promise.resolve<T>(value) + ) + } +}.any + +export const initPromise = (target: any, exclude: Set<string>) => { + if (exclude.has('Promise') || exclude.has('any')) return + + const Class = target.Promise || globalThis.Promise + + if (!Class.any) Object.defineProperty(Class, 'any', { + value: any, + writable: true, + enumerable: false, + configurable: true + }) +} diff --git a/packages/webapi/src/lib/RelativeIndexingMethod.ts b/packages/webapi/src/lib/RelativeIndexingMethod.ts new file mode 100644 index 000000000..56627dfc3 --- /dev/null +++ b/packages/webapi/src/lib/RelativeIndexingMethod.ts @@ -0,0 +1,32 @@ +type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array + +export const at = { + at<T extends Array<any> | string | TypedArray>(this: T, index: number) { + index = Math.trunc(index) || 0 + + if (index < 0) index += this.length; + + if (index < 0 || index >= this.length) return undefined; + + return this[index]; + } +}.at + +export const initRelativeIndexingMethod = (target: any, exclude: Set<string>) => { + if (exclude.has('at')) return + + const Classes = [] + + if (!exclude.has('TypedArray')) Classes.push(Object.getPrototypeOf(target.Int8Array || globalThis.Int8Array)) + if (!exclude.has('Array')) Classes.push(target.Array || globalThis.Array) + if (!exclude.has('String')) Classes.push(target.String || globalThis.String) + + for (const Class of Classes) { + if (!Class.prototype.at) Object.defineProperty(Class.prototype, 'at', { + value: at, + writable: true, + enumerable: false, + configurable: true + }) + } +} diff --git a/packages/webapi/src/lib/Storage.ts b/packages/webapi/src/lib/Storage.ts new file mode 100644 index 000000000..672c537c8 --- /dev/null +++ b/packages/webapi/src/lib/Storage.ts @@ -0,0 +1,51 @@ +import * as _ from './utils' + +export class Storage { + clear(): void { + _.internalsOf<StorageInternals>(this, 'Storage', 'clear').storage.clear() + } + + getItem(key: string): string | null { + return getStringOrNull( + _.internalsOf<StorageInternals>(this, 'Storage', 'getItem').storage.get(String(key)) + ) + } + + key(index: number): string | null { + return getStringOrNull([ ..._.internalsOf<StorageInternals>(this, 'Storage', 'key').storage.keys() ][Number(index) || 0]) + } + + removeItem(key: string): void { + _.internalsOf<StorageInternals>(this, 'Storage', 'getItem').storage.delete(String(key)) + } + + setItem(key: string, value: any): void { + _.internalsOf<StorageInternals>(this, 'Storage', 'getItem').storage.set(String(key), String(value)) + } + + get length() { + return _.internalsOf<StorageInternals>(this, 'Storage', 'size').storage.size + } +} + +const getStringOrNull = (value: string | void) => typeof value === 'string' ? value : null + +export const initStorage = (target: Target, exclude: Set<string>) => { + if (exclude.has('Storage') || exclude.has('localStorage')) return + + target.localStorage = Object.create(Storage.prototype) + + const storageInternals = new Map<string, string>() + + _.INTERNALS.set(target.localStorage, { + storage: storageInternals + } as StorageInternals) +} + +interface StorageInternals { + storage: Map<string, string> +} + +interface Target { + localStorage: Storage +} diff --git a/packages/webapi/src/lib/String.ts b/packages/webapi/src/lib/String.ts new file mode 100644 index 000000000..49df3261c --- /dev/null +++ b/packages/webapi/src/lib/String.ts @@ -0,0 +1,22 @@ +import * as _ from './utils' + +export const replaceAll = { + replaceAll(this: string, searchValue: RegExp | string, replaceValue: string | ((substring: string, ...args: any[]) => string)) { + return _.__object_isPrototypeOf(RegExp.prototype, searchValue) + ? this.replace(searchValue as RegExp, replaceValue as string) + : this.replace(new RegExp(_.__string_escapeRegExp(searchValue as string), 'g'), replaceValue as string) + } +}.replaceAll + +export const initString = (target: any, exclude: Set<string>) => { + if (exclude.has('String') || exclude.has('replaceAll')) return + + const Class = target.String || globalThis.String + + if (!Class.prototype.replaceAll) Object.defineProperty(Class.prototype, 'replaceAll', { + value: replaceAll, + writable: true, + enumerable: false, + configurable: true + }) +} diff --git a/packages/webapi/src/lib/StyleSheet.ts b/packages/webapi/src/lib/StyleSheet.ts new file mode 100644 index 000000000..d4b2d9e33 --- /dev/null +++ b/packages/webapi/src/lib/StyleSheet.ts @@ -0,0 +1,24 @@ +import * as _ from './utils' + +export class StyleSheet {} + +export class CSSStyleSheet extends StyleSheet { + async replace(text: string) { + void text + + return new CSSStyleSheet() + } + + replaceSync(text: string) { + void text + + return new CSSStyleSheet() + } + + get cssRules() { + return [] + } +} + +_.allowStringTag(StyleSheet) +_.allowStringTag(CSSStyleSheet) diff --git a/packages/webapi/src/lib/Timeout.ts b/packages/webapi/src/lib/Timeout.ts new file mode 100644 index 000000000..53412adfd --- /dev/null +++ b/packages/webapi/src/lib/Timeout.ts @@ -0,0 +1,24 @@ +import { setTimeout as nodeSetTimeout, clearTimeout as nodeClearTimeout } from 'node:timers' +import * as _ from './utils.js' + +const INTERNAL = { tick: 0, pool: new Map } + +export function setTimeout<TArgs extends any[], TFunc extends (...args: TArgs) => any>(callback: TFunc, delay = 0, ...args: TArgs): number { + const func = _.__function_bind(callback, globalThis) + const tick = ++INTERNAL.tick + const timeout = nodeSetTimeout(func, delay, ...args) + + INTERNAL.pool.set(tick, timeout) + + return tick +} + +export function clearTimeout(timeoutId: number): void { + const timeout = INTERNAL.pool.get(timeoutId) + + if (timeout) { + nodeClearTimeout(timeout) + + INTERNAL.pool.delete(timeoutId) + } +} diff --git a/packages/webapi/src/lib/TreeWalker.ts b/packages/webapi/src/lib/TreeWalker.ts new file mode 100644 index 000000000..86e32ff20 --- /dev/null +++ b/packages/webapi/src/lib/TreeWalker.ts @@ -0,0 +1,55 @@ +import * as _ from './utils' + +export class TreeWalker { + parentNode(): Node | null { + return null + } + + firstChild(): Node | null { + return null + } + + lastChild(): Node | null { + return null + } + + previousSibling(): Node | null { + return null + } + + nextSibling(): Node | null { + return null + } + + previousNode(): Node | null { + return null + } + + nextNode(): Node | null { + return null + } + + get currentNode(): Node { + const internals = _.internalsOf<TreeWalkerInternals>(this, 'TreeWalker', 'currentNode') + return internals.currentNode + } + + get root(): Node { + const internals = _.internalsOf<TreeWalkerInternals>(this, 'TreeWalker', 'root') + return internals.root + } + + get whatToShow(): number { + const internals = _.internalsOf<TreeWalkerInternals>(this, 'TreeWalker', 'whatToShow') + return internals.whatToShow + } +} + +_.allowStringTag(TreeWalker) + +export interface TreeWalkerInternals { + filter: NodeFilter + currentNode: Node + root: Node + whatToShow: number +} diff --git a/packages/webapi/src/lib/Window.ts b/packages/webapi/src/lib/Window.ts new file mode 100644 index 000000000..ac3312c93 --- /dev/null +++ b/packages/webapi/src/lib/Window.ts @@ -0,0 +1,49 @@ +import * as _ from './utils' + +export class Window extends EventTarget { + get self(): this { + return this + } + + get top(): this { + return this + } + + get window(): this { + return this + } + + get innerHeight(): number { + return 0 + } + + get innerWidth(): number { + return 0 + } + + get scrollX(): number { + return 0 + } + + get scrollY(): number { + return 0 + } +} + +_.allowStringTag(Window) + +export const initWindow = (target: Target, exclude: Set<string>) => { + if (exclude.has('Window') || exclude.has('window')) return + + target.window = target +} + +export interface WindowInternals { + document: null + location: URL + window: this +} + +interface Target extends Record<any, any> { + window: this +} diff --git a/packages/webapi/src/lib/fetch.ts b/packages/webapi/src/lib/fetch.ts new file mode 100644 index 000000000..8ab1358e7 --- /dev/null +++ b/packages/webapi/src/lib/fetch.ts @@ -0,0 +1,75 @@ +import { default as nodeFetch, Headers, Request, Response } from 'node-fetch/src/index.js' +import Stream from 'node:stream' +import * as _ from './utils' + +export { Headers, Request, Response } + +export const fetch = { + fetch(resource: string | URL | Request, init?: Partial<FetchInit>): Promise<Response> { + const resourceURL = new URL( + _.__object_isPrototypeOf(Request.prototype, resource) + ? (resource as Request).url + : _.pathToPosix(resource), + typeof Object(globalThis.process).cwd === 'function' ? 'file:' + _.pathToPosix(process.cwd()) + '/' : 'file:' + ) + + if (resourceURL.protocol.toLowerCase() === 'file:') { + return import('node:fs').then( + fs => { + try { + const stats = fs.statSync(resourceURL) + const body = fs.createReadStream(resourceURL) + + return new Response( + body, + { + status: 200, + statusText: '', + headers: { + 'content-length': String(stats.size), + 'date': new Date().toUTCString(), + 'last-modified': new Date(stats.mtimeMs).toUTCString(), + } + } + ) + } catch (error) { + const body = new Stream.Readable() + + body._read = () => {} + body.push(null) + + return new Response( + body, + { + status: 404, + statusText: '', + headers: { + 'date': new Date().toUTCString(), + } + } + ) + } + } + ) + } else { + return nodeFetch(resource, init) + } + } +}.fetch + +type USVString = ({} & string) + +interface FetchInit { + body: Blob | BufferSource | FormData | URLSearchParams | ReadableStream | USVString + cache: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached' + credentials: 'omit' | 'same-origin' | 'include' + headers: Headers | Record<string, string> + method: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH' | USVString + mode: 'cors' | 'no-cors' | 'same-origin' | USVString + redirect: 'follow' | 'manual' | 'error' + referrer: USVString + referrerPolicy: 'no-referrer' | 'no-referrer-when-downgrade' | 'same-origin' | 'origin' | 'strict-origin' | 'origin-when-cross-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' + integrity: USVString + keepalive: boolean + signal: AbortSignal +} diff --git a/packages/webapi/src/lib/structuredClone.ts b/packages/webapi/src/lib/structuredClone.ts new file mode 100644 index 000000000..e60252015 --- /dev/null +++ b/packages/webapi/src/lib/structuredClone.ts @@ -0,0 +1,4 @@ +import { deserialize } from '@ungap/structured-clone/esm/deserialize.js'; +import { serialize } from '@ungap/structured-clone/esm/serialize.js'; + +export default (any: any, options: any) => deserialize(serialize(any, options)) diff --git a/packages/webapi/src/lib/utils.ts b/packages/webapi/src/lib/utils.ts new file mode 100644 index 000000000..e7c2c6115 --- /dev/null +++ b/packages/webapi/src/lib/utils.ts @@ -0,0 +1,61 @@ +import { performance } from 'node:perf_hooks' + +/** Returns the milliseconds elapsed since January 1, 1970 00:00:00 UTC. */ +export const __date_now = Date.now + +/** Returns the function bound to the given object. */ +export const __function_bind = Function.bind.bind(Function.call as unknown as any) as <TArgs extends any[], TFunc extends (...args: TArgs) => any>(callback: TFunc, thisArg: unknown, ...args: TArgs) => TFunc + +/** Returns the function called with the specified values. */ +export const __function_call = Function.call.bind(Function.call as unknown as any) as <TArgs extends any, TFunc extends (...args: TArgs[]) => any>(callback: TFunc, thisArg: unknown, ...args: TArgs[]) => ReturnType<TFunc> + +/** Returns an object with the specified prototype. */ +export const __object_create = Object.create as { <T extends any = any>(value: T): any extends T ? Record<any, any> : T } + +/** Returns whether an object has a property with the specified name. */ +export const __object_hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty) as { <T1 extends object, T2>(object: T1, key: T2): T2 extends keyof T1 ? true : false } + +/** Returns a string representation of an object. */ +export const __object_toString = Function.call.bind(Object.prototype.toString) as { (value: any): string } + +/** Returns whether the object prototype exists in another object. */ +export const __object_isPrototypeOf = Function.call.bind(Object.prototype.isPrototypeOf) as { <T1 extends object, T2>(p: T1, v: T2): T2 extends T1 ? true : false } + +/** Current high resolution millisecond timestamp. */ +export const __performance_now = performance.now as () => number + +/** Returns the string escaped for use inside regular expressions. */ +export const __string_escapeRegExp = (value: string) => value.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&') + +// @ts-ignore +export const INTERNALS = new WeakMap<unknown, any>() + +export const internalsOf = <T extends object>(target: T | object, className: string, propName: string): T => { + const internals: T = INTERNALS.get(target) + + if (!internals) throw new TypeError(`${className}.${propName} can only be used on instances of ${className}`) + + return internals +} + +export const allowStringTag = (value: any) => value.prototype[Symbol.toStringTag] = value.name + +/** Returns any kind of path as a posix path. */ +export const pathToPosix = (pathname: any) => String( + pathname == null ? '' : pathname +).replace( + // convert slashes + /\\+/g, '/' +).replace( + // prefix a slash to drive letters + /^(?=[A-Za-z]:\/)/, '/' +).replace( + // encode path characters + /%/g, '%25' +).replace( + /\n/g, '%0A' +).replace( + /\r/g, '%0D' +).replace( + /\t/g, '%09' +) diff --git a/packages/webapi/src/polyfill.ts b/packages/webapi/src/polyfill.ts new file mode 100644 index 000000000..81df33185 --- /dev/null +++ b/packages/webapi/src/polyfill.ts @@ -0,0 +1,338 @@ +import { + AbortController, + AbortSignal, + Blob, + ByteLengthQueuingStrategy, + CanvasRenderingContext2D, + CharacterData, + Comment, + CountQueuingStrategy, + CSSStyleSheet, + CustomElementRegistry, + CustomEvent, + DOMException, + Document, + DocumentFragment, + Element, + Event, + EventTarget, + File, + FormData, + HTMLDocument, + HTMLElement, + HTMLBodyElement, + HTMLCanvasElement, + HTMLDivElement, + HTMLHeadElement, + HTMLHtmlElement, + HTMLImageElement, + HTMLSpanElement, + HTMLStyleElement, + HTMLTemplateElement, + HTMLUnknownElement, + Headers, + IntersectionObserver, + Image, + ImageData, + MediaQueryList, + MutationObserver, + Node, + NodeFilter, + NodeIterator, + OffscreenCanvas, + ReadableByteStreamController, + ReadableStream, + ReadableStreamBYOBReader, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + Request, + ResizeObserver, + Response, + ShadowRoot, + Storage, + StyleSheet, + Text, + TransformStream, + TreeWalker, + URLPattern, + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + Window, + + alert, + atob, + btoa, + cancelAnimationFrame, + cancelIdleCallback, + clearTimeout, + fetch, + requestAnimationFrame, + requestIdleCallback, + setTimeout, + structuredClone, + + initCustomElementRegistry, + initDocument, + initMediaQueryList, + initObject, + initPromise, + initRelativeIndexingMethod, + initStorage, + initString, + initWindow, +} from './ponyfill' + +import { exclusions } from './exclusions' +import { inheritence } from './inheritence' + +export { + AbortController, + AbortSignal, + Blob, + ByteLengthQueuingStrategy, + CanvasRenderingContext2D, + CharacterData, + Comment, + CountQueuingStrategy, + CSSStyleSheet, + CustomElementRegistry, + CustomEvent, + DOMException, + Document, + DocumentFragment, + Element, + Event, + EventTarget, + File, + FormData, + HTMLDocument, + HTMLElement, + HTMLBodyElement, + HTMLCanvasElement, + HTMLDivElement, + HTMLHeadElement, + HTMLHtmlElement, + HTMLImageElement, + HTMLSpanElement, + HTMLStyleElement, + HTMLTemplateElement, + HTMLUnknownElement, + Headers, + IntersectionObserver, + Image, + ImageData, + MediaQueryList, + MutationObserver, + Node, + NodeFilter, + NodeIterator, + OffscreenCanvas, + ReadableByteStreamController, + ReadableStream, + ReadableStreamBYOBReader, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + Request, + ResizeObserver, + Response, + ShadowRoot, + StyleSheet, + Text, + TransformStream, + TreeWalker, + URLPattern, + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + Window, + + alert, + atob, + btoa, + cancelAnimationFrame, + cancelIdleCallback, + clearTimeout, + fetch, + requestAnimationFrame, + requestIdleCallback, + setTimeout, + structuredClone, +} from './ponyfill.js' + +export { pathToPosix } from './lib/utils' + +export const polyfill = (target: any, options?: PolyfillOptions) => { + const webAPIs = { + AbortController, + AbortSignal, + Blob, + ByteLengthQueuingStrategy, + CanvasRenderingContext2D, + CharacterData, + Comment, + CountQueuingStrategy, + CSSStyleSheet, + CustomElementRegistry, + CustomEvent, + Document, + DocumentFragment, + DOMException, + Element, + Event, + EventTarget, + File, + FormData, + HTMLDocument, + HTMLElement, + HTMLBodyElement, + HTMLCanvasElement, + HTMLDivElement, + HTMLHeadElement, + HTMLHtmlElement, + HTMLImageElement, + HTMLSpanElement, + HTMLStyleElement, + HTMLTemplateElement, + HTMLUnknownElement, + Headers, + IntersectionObserver, + Image, + ImageData, + MediaQueryList, + MutationObserver, + Node, + NodeFilter, + NodeIterator, + OffscreenCanvas, + ReadableByteStreamController, + ReadableStream, + ReadableStreamBYOBReader, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + Request, + ResizeObserver, + Response, + ShadowRoot, + Storage, + StyleSheet, + Text, + TransformStream, + TreeWalker, + URLPattern, + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + Window, + + alert, + atob, + btoa, + cancelAnimationFrame, + cancelIdleCallback, + clearTimeout, + fetch, + requestAnimationFrame, + requestIdleCallback, + setTimeout, + structuredClone, + } + + // initialize exclude options + const excludeOptions = new Set( + typeof Object(options).exclude === 'string' + ? String(Object(options).exclude).trim().split(/\s+/) + : Array.isArray(Object(options).exclude) + ? Object(options).exclude.reduce( + (array: string[], entry: unknown) => array.splice(array.length, 0, ...(typeof entry === 'string' ? entry.trim().split(/\s+/) : [])) && array, + [] + ) + : [] + ) as Set<string> + + // expand exclude options using exclusion shorthands + for (const excludeOption of excludeOptions) { + if (excludeOption in exclusions) { + for (const exclusion of exclusions[excludeOption as keyof typeof exclusions]) { + excludeOptions.add(exclusion) + } + } + } + + // apply each WebAPI + for (const name of Object.keys(webAPIs)) { + // skip WebAPIs that are excluded + if (excludeOptions.has(name)) continue + + // skip WebAPIs that are built-in + if (Object.hasOwnProperty.call(target, name)) continue + + // define WebAPIs on the target + Object.defineProperty(target, name, { configurable: true, enumerable: true, writable: true, value: webAPIs[name as keyof typeof webAPIs] }) + } + + // ensure WebAPIs correctly inherit other WebAPIs + for (const name of Object.keys(webAPIs)) { + // skip WebAPIs that are excluded + if (excludeOptions.has(name)) continue + + // skip WebAPIs that do not extend other WebAPIs + if (!Object.hasOwnProperty.call(inheritence, name)) continue + + const Class = target[name] + const Super = target[inheritence[name as keyof typeof inheritence]] + + // skip WebAPIs that are not available + if (!Class || !Super) continue + + // skip WebAPIs that are already inherited correctly + if (Object.getPrototypeOf(Class.prototype) === Super.prototype) continue + + // define WebAPIs inheritence + Object.setPrototypeOf(Class.prototype, Super.prototype) + } + + if (!excludeOptions.has('HTMLDocument') && !excludeOptions.has('HTMLElement')) { + initDocument(target, excludeOptions) + + if (!excludeOptions.has('CustomElementRegistry')) { + initCustomElementRegistry(target, excludeOptions) + } + } + + initObject(target, excludeOptions) + initMediaQueryList(target, excludeOptions) + initPromise(target, excludeOptions) + initRelativeIndexingMethod(target, excludeOptions) + initStorage(target, excludeOptions) + initString(target, excludeOptions) + initWindow(target, excludeOptions) + + return target +} + +polyfill.internals = (target: any, name: string) => { + const init = { + CustomElementRegistry: initCustomElementRegistry, + Document: initDocument, + MediaQueryList: initMediaQueryList, + Object: initObject, + Promise: initPromise, + RelativeIndexingMethod: initRelativeIndexingMethod, + Storage: initStorage, + String: initString, + Window: initWindow, + } + + init[name as keyof typeof init](target, new Set<string>()) + + return target +} + +interface PolyfillOptions { + exclude?: string | string[] + override?: Record<string, { (...args: any[]): any }> +} diff --git a/packages/webapi/src/ponyfill.ts b/packages/webapi/src/ponyfill.ts new file mode 100644 index 000000000..cbbfba909 --- /dev/null +++ b/packages/webapi/src/ponyfill.ts @@ -0,0 +1,127 @@ +// @ts-check + +import { AbortController, AbortSignal } from 'abort-controller/dist/abort-controller.mjs' +import { requestAnimationFrame, cancelAnimationFrame } from './lib/AnimationFrame' +import { atob, btoa } from './lib/Base64' +import { CharacterData, Comment, Text } from './lib/CharacterData' +import { File, Blob } from 'fetch-blob/from.js' +import { CustomEvent } from './lib/CustomEvent' +import { DOMException } from './lib/DOMException' +import { TreeWalker } from './lib/TreeWalker' +import { cancelIdleCallback, requestIdleCallback } from './lib/IdleCallback' +import { Event, EventTarget } from 'event-target-shim' +import { fetch, Headers, Request, Response } from './lib/fetch' +import { FormData } from 'formdata-polyfill/esm.min.js' +import { ByteLengthQueuingStrategy, CountQueuingStrategy, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, TransformStream, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter } from 'web-streams-polyfill/dist/ponyfill.es6.mjs' +import { URLPattern } from 'urlpattern-polyfill' +import { setTimeout, clearTimeout } from './lib/Timeout' +import structuredClone from './lib/structuredClone' + +import { CanvasRenderingContext2D } from './lib/CanvasRenderingContext2D' +import { CSSStyleSheet, StyleSheet } from './lib/StyleSheet' +import { CustomElementRegistry, initCustomElementRegistry } from './lib/CustomElementRegistry' +import { Document, HTMLDocument, initDocument } from './lib/Document' +import { DocumentFragment, Node, NodeFilter, NodeIterator, ShadowRoot } from './lib/Node' +import { Element, HTMLElement, HTMLBodyElement, HTMLDivElement, HTMLHeadElement, HTMLHtmlElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement } from './lib/Element' +import { HTMLCanvasElement } from './lib/HTMLCanvasElement' +import { HTMLImageElement } from './lib/HTMLImageElement' +import { Image } from './lib/Image' +import { ImageData } from './lib/ImageData' +import { IntersectionObserver, MutationObserver, ResizeObserver } from './lib/Observer' +import { MediaQueryList, initMediaQueryList } from './lib/MediaQueryList' +import { OffscreenCanvas } from './lib/OffscreenCanvas' +import { Storage, initStorage } from './lib/Storage' +import { Window, initWindow } from './lib/Window' + +import { alert } from './lib/Alert' + +import { initObject } from './lib/Object' +import { initPromise } from './lib/Promise' +import { initRelativeIndexingMethod } from './lib/RelativeIndexingMethod' +import { initString } from './lib/String' + +export { + AbortController, + AbortSignal, + Blob, + ByteLengthQueuingStrategy, + CanvasRenderingContext2D, + CharacterData, + Comment, + CountQueuingStrategy, + CSSStyleSheet, + CustomElementRegistry, + CustomEvent, + DOMException, + Document, + DocumentFragment, + Element, + Event, + EventTarget, + File, + FormData, + Headers, + HTMLBodyElement, + HTMLCanvasElement, + HTMLDivElement, + HTMLDocument, + HTMLElement, + HTMLHeadElement, + HTMLHtmlElement, + HTMLImageElement, + HTMLSpanElement, + HTMLStyleElement, + HTMLTemplateElement, + HTMLUnknownElement, + Image, + ImageData, + IntersectionObserver, + MediaQueryList, + MutationObserver, + Node, + NodeFilter, + NodeIterator, + OffscreenCanvas, + ReadableByteStreamController, + ReadableStream, + ReadableStreamBYOBReader, + ReadableStreamBYOBRequest, + ReadableStreamDefaultController, + ReadableStreamDefaultReader, + Request, + ResizeObserver, + Response, + ShadowRoot, + Storage, + StyleSheet, + Text, + TransformStream, + TreeWalker, + URLPattern, + WritableStream, + WritableStreamDefaultController, + WritableStreamDefaultWriter, + Window, + + alert, + atob, + btoa, + cancelAnimationFrame, + cancelIdleCallback, + clearTimeout, + fetch, + requestAnimationFrame, + requestIdleCallback, + setTimeout, + structuredClone, + + initCustomElementRegistry, + initDocument, + initMediaQueryList, + initObject, + initPromise, + initRelativeIndexingMethod, + initStorage, + initString, + initWindow, +} diff --git a/packages/webapi/src/types.d.ts b/packages/webapi/src/types.d.ts new file mode 100644 index 000000000..7f8f96eec --- /dev/null +++ b/packages/webapi/src/types.d.ts @@ -0,0 +1,6 @@ +declare module '@ungap/structured-clone/esm/index.js' +declare module '@ungap/structured-clone/esm/deserialize.js' +declare module '@ungap/structured-clone/esm/serialize.js' +declare module 'abort-controller/dist/abort-controller.mjs' +declare module 'node-fetch/src/index.js' +declare module 'web-streams-polyfill/dist/ponyfill.es6.mjs' |