├── src ├── lib │ ├── Alert.ts │ ├── Base64.ts │ ├── structuredClone.ts │ ├── Image.ts │ ├── HTMLImageElement.ts │ ├── StyleSheet.ts │ ├── Object.ts │ ├── CustomEvent.ts │ ├── Observer.ts │ ├── Timeout.ts │ ├── Promise.ts │ ├── Window.ts │ ├── String.ts │ ├── MediaQueryList.ts │ ├── IdleCallback.ts │ ├── AnimationFrame.ts │ ├── CharacterData.ts │ ├── DOMException.ts │ ├── relativeIndexingMethod.ts │ ├── TreeWalker.ts │ ├── Storage.ts │ ├── HTMLCanvasElement.ts │ ├── ContextEvent.ts │ ├── fetch.ts │ ├── OffscreenCanvas.ts │ ├── ImageData.ts │ ├── utils.ts │ ├── CustomElementRegistry.ts │ ├── Node.ts │ ├── Document.ts │ ├── Element.ts │ └── CanvasRenderingContext2D.ts ├── types.d.ts ├── inheritence.ts ├── exclusions.ts ├── ponyfill.ts └── polyfill.ts ├── .gitignore ├── tsconfig.json ├── run ├── test.all.js ├── test.js ├── test.setup.js └── build.js ├── test ├── media.js ├── urlpattern.js ├── internals.js ├── structuredclone.js ├── base64.js ├── offscreencanvas.js ├── options.js ├── storage.js ├── characterdata.js ├── imagedata.js ├── elements.js ├── fetch.js └── basic.js ├── package.json └── README.md /src/lib/Alert.ts: -------------------------------------------------------------------------------- 1 | export function alert(...messages: any[]) { 2 | console.log(...messages) 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | apply.* 4 | mod.* 5 | yarn.lock 6 | *.log* 7 | .* 8 | !.gitignore 9 | -------------------------------------------------------------------------------- /src/lib/Base64.ts: -------------------------------------------------------------------------------- 1 | export function atob(data: string): string { 2 | return Buffer.from(data, 'base64').toString('binary') 3 | } 4 | 5 | export function btoa(data: string): string { 6 | return Buffer.from(data, 'binary').toString('base64') 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/structuredClone.ts: -------------------------------------------------------------------------------- 1 | import { deserialize } from '@ungap/structured-clone/esm/deserialize.js'; 2 | import { serialize } from '@ungap/structured-clone/esm/serialize.js'; 3 | 4 | export default (any: any, options: any) => deserialize(serialize(any, options)) 5 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@ungap/structured-clone/esm/index.js' 2 | declare module '@ungap/structured-clone/esm/deserialize.js' 3 | declare module '@ungap/structured-clone/esm/serialize.js' 4 | declare module 'abort-controller/dist/abort-controller.mjs' 5 | declare module 'node-fetch/src/index.js' 6 | declare module 'web-streams-polyfill/dist/ponyfill.es6.mjs' 7 | -------------------------------------------------------------------------------- /src/lib/Image.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | import { HTMLImageElement } from './HTMLImageElement' 3 | 4 | export function Image() { 5 | // @ts-ignore 6 | _.INTERNALS.set(this, { 7 | attributes: {}, 8 | localName: 'img', 9 | innerHTML: '', 10 | shadowRoot: null, 11 | shadowInit: null, 12 | }) 13 | } 14 | 15 | Image.prototype = HTMLImageElement.prototype 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/*"], 3 | "exclude": ["node_modules"], 4 | "compilerOptions": { 5 | "target": "ES2021", 6 | "module": "ES2020", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "declaration": true, 10 | "declarationDir": ".", 11 | "strict": true, 12 | "sourceMap": true, 13 | "declarationMap": true 14 | } 15 | } -------------------------------------------------------------------------------- /src/lib/HTMLImageElement.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | import { HTMLElement } from './Element' 3 | 4 | export class HTMLImageElement extends HTMLElement { 5 | get src(): string { 6 | return _.internalsOf(this, 'HTMLImageElement', 'src').src 7 | } 8 | 9 | set src(value) { 10 | const internals = _.internalsOf(this, 'HTMLImageElement', 'src') 11 | 12 | internals.src = String(value) 13 | } 14 | } 15 | 16 | _.assignStringTag(HTMLImageElement) 17 | -------------------------------------------------------------------------------- /src/lib/StyleSheet.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class StyleSheet {} 4 | 5 | export class CSSStyleSheet extends StyleSheet { 6 | async replace(text: string) { 7 | void text 8 | 9 | return new CSSStyleSheet() 10 | } 11 | 12 | replaceSync(text: string) { 13 | void text 14 | 15 | return new CSSStyleSheet() 16 | } 17 | 18 | get cssRules() { 19 | return [] 20 | } 21 | } 22 | 23 | _.assignStringTag(StyleSheet) 24 | _.assignStringTag(CSSStyleSheet) 25 | -------------------------------------------------------------------------------- /run/test.all.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import { args, fork, pathFrom } from './test.setup.js' 3 | 4 | const test = async () => { 5 | const { opts } = args() 6 | 7 | if (!opts['--only']) opts['--only'] = '.+' 8 | 9 | const only = new RegExp(opts['--only'], 'i') 10 | 11 | const testDir = pathFrom(import.meta.url, '../test/') 12 | 13 | for await (const dirent of await fs.opendir(testDir)) { 14 | if (only.test(dirent.name)) { 15 | await fork(pathFrom(testDir, dirent.name)) 16 | } 17 | } 18 | } 19 | 20 | test() 21 | -------------------------------------------------------------------------------- /run/test.js: -------------------------------------------------------------------------------- 1 | import { args, pathFrom, spawn } from './test.setup.js' 2 | 3 | const pathToRoot = pathFrom(import.meta.url, '../') 4 | const pathToTest = pathFrom(import.meta.url, './test.all.js') 5 | 6 | const test = async () => { 7 | const { opts } = args() 8 | 9 | if (!opts['--node']) opts['--node'] = [ '12', '14', '16' ] 10 | 11 | for (const version of opts['--node']) { 12 | await spawn('volta', ['run', '--node', version, 'node', pathToTest, ...process.argv.slice(2)], { cwd: pathToRoot, env: { ...process.env }, stdio: 'inherit' }) 13 | } 14 | } 15 | 16 | test() 17 | -------------------------------------------------------------------------------- /src/lib/Object.ts: -------------------------------------------------------------------------------- 1 | import { hasOwn as objectHasOwn } from './utils' 2 | 3 | export const hasOwn = { 4 | hasOwn(instance: object, property: any) { 5 | return objectHasOwn(instance, property) 6 | } 7 | }.hasOwn 8 | 9 | export const initObject = (target: any, exclude: Set) => { 10 | if (exclude.has('Object') || exclude.has('object') || exclude.has('hasOwn')) return 11 | 12 | const Class = target.Object || globalThis.Object 13 | 14 | Object.defineProperty(Class, 'hasOwn', { 15 | value: hasOwn, 16 | writable: true, 17 | enumerable: false, 18 | configurable: true 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/CustomEvent.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | import { Event } from 'event-target-shim' 3 | 4 | class CustomEvent extends Event { 5 | constructor(type: TEventType, params?: CustomEventInit) { 6 | params = Object(params) as Required 7 | 8 | super(type, params) 9 | 10 | if ('detail' in params) this.detail = params.detail 11 | } 12 | 13 | detail!: any 14 | } 15 | 16 | _.assignStringTag(CustomEvent) 17 | 18 | export { CustomEvent } 19 | 20 | interface CustomEventInit { 21 | bubbles?: boolean 22 | cancelable?: false 23 | detail?: any 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/Observer.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class IntersectionObserver { 4 | disconnect() {} 5 | 6 | observe() {} 7 | 8 | takeRecords() { 9 | return [] 10 | } 11 | 12 | unobserve() {} 13 | } 14 | 15 | export class MutationObserver { 16 | disconnect() {} 17 | 18 | observe() {} 19 | 20 | takeRecords() { 21 | return [] 22 | } 23 | 24 | unobserve() {} 25 | } 26 | 27 | export class ResizeObserver { 28 | disconnect() {} 29 | 30 | observe() {} 31 | 32 | takeRecords() { 33 | return [] 34 | } 35 | 36 | unobserve() {} 37 | } 38 | 39 | _.assignStringTag(MutationObserver) 40 | _.assignStringTag(IntersectionObserver) 41 | _.assignStringTag(ResizeObserver) 42 | -------------------------------------------------------------------------------- /test/media.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Includes MediaQueryList functionality', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal(Reflect.has(target, 'MediaQueryList'), true) 14 | assert.equal(Reflect.has(target, 'matchMedia'), true) 15 | }, 16 | }, 17 | { 18 | name: 'Supports matchMedia creation', 19 | test() { 20 | const target = {} 21 | 22 | polyfill(target) 23 | 24 | const mql = target.matchMedia('(min-width: 640px)') 25 | 26 | assert.equal(mql.matches, false) 27 | assert.equal(mql.media, '(min-width: 640px)') 28 | }, 29 | }, 30 | ] 31 | }) 32 | -------------------------------------------------------------------------------- /test/urlpattern.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Includes URLPattern', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal(Reflect.has(target, 'URLPattern'), true) 14 | assert.equal(typeof target.URLPattern, 'function') 15 | }, 16 | }, 17 | { 18 | name: 'Supports URLPattern usage', 19 | test() { 20 | const target = {} 21 | 22 | polyfill(target) 23 | 24 | const pattern = new target.URLPattern({ pathname: '/hello/:name' }) 25 | const match = pattern.exec('https://example.com/hello/Deno') 26 | 27 | assert.deepEqual(match.pathname.groups, { name: 'Deno' }) 28 | }, 29 | }, 30 | ] 31 | }) 32 | -------------------------------------------------------------------------------- /src/lib/Timeout.ts: -------------------------------------------------------------------------------- 1 | import { setTimeout as nodeSetTimeout, clearTimeout as nodeClearTimeout } from 'node:timers' 2 | import * as _ from './utils.js' 3 | 4 | const INTERNAL = { tick: 0, pool: new Map } 5 | 6 | export function setTimeout any>(callback: TFunc, delay = 0, ...args: TArgs): number { 7 | const func = _.__function_bind(callback, globalThis) 8 | const tick = ++INTERNAL.tick 9 | const timeout = nodeSetTimeout(func, delay, ...args) 10 | 11 | INTERNAL.pool.set(tick, timeout) 12 | 13 | return tick 14 | } 15 | 16 | export function clearTimeout(timeoutId: number): void { 17 | const timeout = INTERNAL.pool.get(timeoutId) 18 | 19 | if (timeout) { 20 | nodeClearTimeout(timeout) 21 | 22 | INTERNAL.pool.delete(timeoutId) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/Promise.ts: -------------------------------------------------------------------------------- 1 | export const any = { 2 | async any ( 3 | iterable: Iterable> 4 | ): Promise { 5 | return Promise.all( 6 | [...iterable].map(promise => { 7 | return new Promise((resolve, reject) => 8 | Promise.resolve(promise).then(reject, resolve) 9 | ) 10 | }) 11 | ).then( 12 | errors => Promise.reject(errors), 13 | value => Promise.resolve(value) 14 | ) 15 | } 16 | }.any 17 | 18 | export const initPromise = (target: any, exclude: Set) => { 19 | if (exclude.has('Promise') || exclude.has('any')) return 20 | 21 | const Class = target.Promise || globalThis.Promise 22 | 23 | if (!Class.any) Object.defineProperty(Class, 'any', { 24 | value: any, 25 | writable: true, 26 | enumerable: false, 27 | configurable: true 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/Window.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class Window extends EventTarget { 4 | get self(): this { 5 | return this 6 | } 7 | 8 | get top(): this { 9 | return this 10 | } 11 | 12 | get window(): this { 13 | return this 14 | } 15 | 16 | get innerHeight(): number { 17 | return 0 18 | } 19 | 20 | get innerWidth(): number { 21 | return 0 22 | } 23 | 24 | get scrollX(): number { 25 | return 0 26 | } 27 | 28 | get scrollY(): number { 29 | return 0 30 | } 31 | } 32 | 33 | _.assignStringTag(Window) 34 | 35 | export const initWindow = (target: any, exclude: Set) => { 36 | if (exclude.has('Window') || exclude.has('window')) return 37 | 38 | target.window = target 39 | } 40 | 41 | export interface WindowInternals { 42 | document: null 43 | location: URL 44 | window: this 45 | } 46 | -------------------------------------------------------------------------------- /test/internals.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Includes polyfill.internals functionality', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target, { exclude: 'window document' }) 12 | 13 | const pseudo = target 14 | 15 | assert.equal(Reflect.has(pseudo, 'document'), false) 16 | 17 | const CustomElement = class extends pseudo.HTMLElement {} 18 | 19 | pseudo.customElements.define('custom-element', CustomElement) 20 | 21 | polyfill.internals(pseudo, 'Document') 22 | 23 | assert.equal(Reflect.has(pseudo, 'document'), true) 24 | 25 | assert.equal(CustomElement.prototype.isPrototypeOf(pseudo.document.createElement('custom-element')), true) 26 | }, 27 | } 28 | ] 29 | }) 30 | -------------------------------------------------------------------------------- /src/inheritence.ts: -------------------------------------------------------------------------------- 1 | export const inheritence = { 2 | CSSStyleSheet: 'StyleSheet', 3 | CustomEvent: 'Event', 4 | DOMException: 'Error', 5 | Document: 'Node', 6 | DocumentFragment: 'Node', 7 | Element: 'Node', 8 | File: 'Blob', 9 | HTMLDocument: 'Document', 10 | HTMLElement: 'Element', 11 | HTMLBodyElement: 'HTMLElement', 12 | HTMLCanvasElement: 'HTMLElement', 13 | HTMLDivElement: 'HTMLElement', 14 | HTMLHeadElement: 'HTMLElement', 15 | HTMLHtmlElement: 'HTMLElement', 16 | HTMLImageElement: 'HTMLElement', 17 | HTMLSpanElement: 'HTMLElement', 18 | HTMLStyleElement: 'HTMLElement', 19 | HTMLTemplateElement: 'HTMLElement', 20 | HTMLUnknownElement: 'HTMLElement', 21 | Image: 'HTMLElement', 22 | MediaQueryList: 'EventTarget', 23 | Node: 'EventTarget', 24 | OffscreenCanvas: 'EventTarget', 25 | ShadowRoot: 'DocumentFragment', 26 | Window: 'EventTarget', 27 | } as const 28 | -------------------------------------------------------------------------------- /src/lib/String.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export const replaceAll = { 4 | replaceAll(this: string, searchValue: RegExp | string, replaceValue: string | ((substring: string, ...args: any[]) => string)) { 5 | return _.__object_isPrototypeOf(RegExp.prototype, searchValue) 6 | ? this.replace(searchValue as RegExp, replaceValue as string) 7 | : this.replace(new RegExp(_.__string_escapeRegExp(searchValue as string), 'g'), replaceValue as string) 8 | } 9 | }.replaceAll 10 | 11 | export const initString = (target: any, exclude: Set) => { 12 | if (exclude.has('String') || exclude.has('replaceAll')) return 13 | 14 | const Class = target.String || globalThis.String 15 | 16 | if (!Class.prototype.replaceAll) Object.defineProperty(Class.prototype, 'replaceAll', { 17 | value: replaceAll, 18 | writable: true, 19 | enumerable: false, 20 | configurable: true 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /test/structuredclone.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Includes structuredClone', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal(Reflect.has(target, 'structuredClone'), true) 14 | assert.equal(typeof target.structuredClone, 'function') 15 | }, 16 | }, 17 | { 18 | name: 'Supports structuredClone usage', 19 | test() { 20 | const target = {} 21 | 22 | polyfill(target) 23 | 24 | const obj = { 25 | foo: "bar", 26 | baz: { 27 | qux: "quux", 28 | }, 29 | } 30 | 31 | const clone = target.structuredClone(obj) 32 | 33 | assert.notEqual(obj, clone) 34 | assert.notEqual(obj.baz, clone.baz) 35 | 36 | assert.equal(obj.baz.qux, clone.baz.qux) 37 | }, 38 | }, 39 | ] 40 | }) 41 | -------------------------------------------------------------------------------- /src/lib/MediaQueryList.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class MediaQueryList extends EventTarget { 4 | get matches(): boolean { 5 | return _.internalsOf(this, 'MediaQueryList', 'matches').matches 6 | } 7 | 8 | get media(): string { 9 | return _.internalsOf(this, 'MediaQueryList', 'media').media 10 | } 11 | } 12 | 13 | _.assignStringTag(MediaQueryList) 14 | 15 | export const initMediaQueryList = (target: any, exclude: Set, pseudo: any) => { 16 | if (!_.hasOwn(pseudo, 'matchMedia')) { 17 | pseudo.matchMedia = function matchMedia(media: string) { 18 | const mql = Object.setPrototypeOf(new pseudo.EventTarget, pseudo.MediaQueryList.prototype) as MediaQueryList 19 | 20 | _.INTERNALS.set(mql, { 21 | matches: false, 22 | media, 23 | }) 24 | 25 | return mql 26 | } 27 | } 28 | 29 | if (!exclude.has('MediaQueryList') && !exclude.has('matchMedia')) target.matchMedia = pseudo.matchMedia 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/IdleCallback.ts: -------------------------------------------------------------------------------- 1 | import { setTimeout as nodeSetTimeout, clearTimeout as nodeClearTimeout } from 'node:timers' 2 | import * as _ from './utils.js' 3 | 4 | const INTERNAL = { tick: 0, pool: new Map } 5 | 6 | export function requestIdleCallback any>(callback: TFunc): number { 7 | if (!INTERNAL.pool.size) { 8 | nodeSetTimeout(() => { 9 | const next = _.__performance_now() 10 | 11 | for (const func of INTERNAL.pool.values()) { 12 | func(next) 13 | } 14 | 15 | INTERNAL.pool.clear() 16 | }, 1000 / 16) 17 | } 18 | 19 | const func = _.__function_bind(callback, undefined) 20 | const tick = ++INTERNAL.tick 21 | 22 | INTERNAL.pool.set(tick, func) 23 | 24 | return tick 25 | } 26 | 27 | export function cancelIdleCallback(requestId: number): void { 28 | const timeout = INTERNAL.pool.get(requestId) 29 | 30 | if (timeout) { 31 | nodeClearTimeout(timeout) 32 | 33 | INTERNAL.pool.delete(requestId) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/AnimationFrame.ts: -------------------------------------------------------------------------------- 1 | import { setTimeout as nodeSetTimeout, clearTimeout as nodeClearTimeout } from 'node:timers' 2 | import * as _ from './utils.js' 3 | 4 | const INTERNAL = { tick: 0, pool: new Map } 5 | 6 | export function requestAnimationFrame any>(callback: TFunc): number { 7 | if (!INTERNAL.pool.size) { 8 | nodeSetTimeout(() => { 9 | const next = _.__performance_now() 10 | 11 | for (const func of INTERNAL.pool.values()) { 12 | func(next) 13 | } 14 | 15 | INTERNAL.pool.clear() 16 | }, 1000 / 16) 17 | } 18 | 19 | const func = _.__function_bind(callback, undefined) 20 | const tick = ++INTERNAL.tick 21 | 22 | INTERNAL.pool.set(tick, func) 23 | 24 | return tick 25 | } 26 | 27 | export function cancelAnimationFrame(requestId: number): void { 28 | const timeout = INTERNAL.pool.get(requestId) 29 | 30 | if (timeout) { 31 | nodeClearTimeout(timeout) 32 | 33 | INTERNAL.pool.delete(requestId) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/CharacterData.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class CharacterData extends Node { 4 | constructor(data: string) { 5 | super() 6 | 7 | _.INTERNALS.set(this, { 8 | data: String(data), 9 | } as CharacterDataInternals) 10 | } 11 | 12 | get data(): string { 13 | return _.internalsOf(this, 'CharacterData', 'data').data 14 | } 15 | 16 | get textContent(): string { 17 | return _.internalsOf(this, 'CharacterData', 'textContent').data 18 | } 19 | } 20 | 21 | export class Comment extends CharacterData {} 22 | 23 | export class Text extends CharacterData { 24 | get assignedSlot(): HTMLSlotElement | null { 25 | return null 26 | } 27 | 28 | get wholeText(): string { 29 | return _.internalsOf(this, 'CharacterData', 'textContent').data 30 | } 31 | 32 | get nodeName() { 33 | return '#text' 34 | } 35 | } 36 | 37 | _.assignStringTag(CharacterData) 38 | _.assignStringTag(Text) 39 | _.assignStringTag(Comment) 40 | 41 | interface CharacterDataInternals { 42 | data: string 43 | } 44 | -------------------------------------------------------------------------------- /test/base64.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Supports Base64 Methods', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal('atob' in target, true) 14 | assert.equal('btoa' in target, true) 15 | assert.equal(typeof target['atob'], 'function') 16 | assert.equal(typeof target['btoa'], 'function') 17 | }, 18 | }, 19 | { 20 | name: 'Supports atob(data)', 21 | test() { 22 | const target = {} 23 | 24 | polyfill(target) 25 | 26 | const a = 'SGVsbG8sIHdvcmxk' 27 | const b = target.atob(a) 28 | 29 | assert.equal(a, 'SGVsbG8sIHdvcmxk') 30 | assert.equal(b, 'Hello, world') 31 | }, 32 | }, 33 | { 34 | name: 'Supports btoa(data)', 35 | test() { 36 | const target = {} 37 | 38 | polyfill(target) 39 | 40 | const b = 'Hello, world' 41 | const a = target.btoa(b) 42 | 43 | assert.equal(a, 'SGVsbG8sIHdvcmxk') 44 | assert.equal(b, 'Hello, world') 45 | }, 46 | }, 47 | ] 48 | }) 49 | -------------------------------------------------------------------------------- /src/lib/DOMException.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class DOMException extends Error { 4 | constructor(message = '', name = 'Error') { 5 | super(message) 6 | 7 | this.code = 0 8 | this.name = name 9 | } 10 | 11 | code!: number 12 | 13 | static INDEX_SIZE_ERR = 1 14 | static DOMSTRING_SIZE_ERR = 2 15 | static HIERARCHY_REQUEST_ERR = 3 16 | static WRONG_DOCUMENT_ERR = 4 17 | static INVALID_CHARACTER_ERR = 5 18 | static NO_DATA_ALLOWED_ERR = 6 19 | static NO_MODIFICATION_ALLOWED_ERR = 7 20 | static NOT_FOUND_ERR = 8 21 | static NOT_SUPPORTED_ERR = 9 22 | static INUSE_ATTRIBUTE_ERR = 10 23 | static INVALID_STATE_ERR = 11 24 | static SYNTAX_ERR = 12 25 | static INVALID_MODIFICATION_ERR = 13 26 | static NAMESPACE_ERR = 14 27 | static INVALID_ACCESS_ERR = 15 28 | static VALIDATION_ERR = 16 29 | static TYPE_MISMATCH_ERR = 17 30 | static SECURITY_ERR = 18 31 | static NETWORK_ERR = 19 32 | static ABORT_ERR = 20 33 | static URL_MISMATCH_ERR = 21 34 | static QUOTA_EXCEEDED_ERR = 22 35 | static TIMEOUT_ERR = 23 36 | static INVALID_NODE_TYPE_ERR = 24 37 | static DATA_CLONE_ERR = 25 38 | } 39 | 40 | _.assignStringTag(DOMException) 41 | -------------------------------------------------------------------------------- /src/lib/relativeIndexingMethod.ts: -------------------------------------------------------------------------------- 1 | type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array 2 | 3 | export const at = { 4 | at | string | TypedArray>(this: T, index: number) { 5 | index = Math.trunc(index) || 0 6 | 7 | if (index < 0) index += this.length; 8 | 9 | if (index < 0 || index >= this.length) return undefined; 10 | 11 | return this[index]; 12 | } 13 | }.at 14 | 15 | export const initRelativeIndexingMethod = (target: any, exclude: Set) => { 16 | if (exclude.has('at')) return 17 | 18 | const Classes = [] 19 | 20 | if (!exclude.has('TypedArray')) Classes.push(Object.getPrototypeOf(target.Int8Array || globalThis.Int8Array)) 21 | if (!exclude.has('Array')) Classes.push(target.Array || globalThis.Array) 22 | if (!exclude.has('String')) Classes.push(target.String || globalThis.String) 23 | 24 | for (const Class of Classes) { 25 | if (!Class.prototype.at) Object.defineProperty(Class.prototype, 'at', { 26 | value: at, 27 | writable: true, 28 | enumerable: false, 29 | configurable: true 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/TreeWalker.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class TreeWalker { 4 | parentNode(): Node | null { 5 | return null 6 | } 7 | 8 | firstChild(): Node | null { 9 | return null 10 | } 11 | 12 | lastChild(): Node | null { 13 | return null 14 | } 15 | 16 | previousSibling(): Node | null { 17 | return null 18 | } 19 | 20 | nextSibling(): Node | null { 21 | return null 22 | } 23 | 24 | previousNode(): Node | null { 25 | return null 26 | } 27 | 28 | nextNode(): Node | null { 29 | return null 30 | } 31 | 32 | get currentNode(): Node { 33 | const internals = _.internalsOf(this, 'TreeWalker', 'currentNode') 34 | return internals.currentNode 35 | } 36 | 37 | get root(): Node { 38 | const internals = _.internalsOf(this, 'TreeWalker', 'root') 39 | return internals.root 40 | } 41 | 42 | get whatToShow(): number { 43 | const internals = _.internalsOf(this, 'TreeWalker', 'whatToShow') 44 | return internals.whatToShow 45 | } 46 | } 47 | 48 | _.assignStringTag(TreeWalker) 49 | 50 | export interface TreeWalkerInternals { 51 | filter: NodeFilter 52 | currentNode: Node 53 | root: Node 54 | whatToShow: number 55 | } 56 | -------------------------------------------------------------------------------- /src/exclusions.ts: -------------------------------------------------------------------------------- 1 | const exclusionsForHTMLElement = [ 'CustomElementsRegistry', 'HTMLElement', 'HTMLBodyElement', 'HTMLCanvasElement', 'HTMLDivElement', 'HTMLHeadElement', 'HTMLHtmlElement', 'HTMLImageElement', 'HTMLStyleElement', 'HTMLTemplateElement', 'HTMLUnknownElement', 'Image' ] 2 | const exclusionsForElement = [ 'Element', ...exclusionsForHTMLElement ] as const 3 | const exclusionsForDocument = [ 'CustomElementsRegistry', 'Document', 'HTMLDocument', 'document', 'customElements' ] as const 4 | const exclusionsForNode = [ 'Node', 'DocumentFragment', 'ShadowRoot', ...exclusionsForDocument, ...exclusionsForElement ] as const 5 | const exclusionsForEventTarget = [ 'AbortSignal', 'Event', 'CustomEvent', 'EventTarget', 'OffscreenCanvas', 'MediaQueryList', 'Window', ...exclusionsForNode ] as const 6 | const exclusionsForEvent = [ 'AbortSignal', 'Event', 'CustomEvent', 'EventTarget', 'MediaQueryList', 'OffscreenCanvas', 'Window', ...exclusionsForNode ] as const 7 | 8 | export const exclusions = { 9 | 'Blob+': [ 'Blob', 'File' ], 10 | 'Document+': exclusionsForDocument, 11 | 'Element+': exclusionsForElement, 12 | 'Event+': exclusionsForEvent, 13 | 'EventTarget+': exclusionsForEventTarget, 14 | 'HTMLElement+': exclusionsForHTMLElement, 15 | 'Node+': exclusionsForNode, 16 | 'StyleSheet+': [ 'StyleSheet', 'CSSStyleSheet' ], 17 | } 18 | -------------------------------------------------------------------------------- /test/offscreencanvas.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Supports OffscreenCanvas', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal('OffscreenCanvas' in target, true) 14 | assert.equal(typeof target['OffscreenCanvas'], 'function') 15 | }, 16 | }, 17 | { 18 | name: 'Supports new (width: number, height: number): OffscreenCanvas', 19 | test() { 20 | const target = {} 21 | 22 | polyfill(target) 23 | 24 | const w = 640 25 | const h = 480 26 | 27 | const canvas = new target.OffscreenCanvas(w, h) 28 | 29 | assert.equal(canvas.width, w) 30 | assert.equal(canvas.height, h) 31 | }, 32 | }, 33 | { 34 | name: 'Supports OffscreenCanvas#getContext', 35 | test() { 36 | const target = {} 37 | 38 | polyfill(target) 39 | 40 | const w = 640 41 | const h = 480 42 | 43 | const canvas = new target.OffscreenCanvas(w, h) 44 | 45 | const context = canvas.getContext('2d') 46 | 47 | assert.equal(context.canvas, canvas) 48 | 49 | const imageData = context.createImageData(w, h) 50 | 51 | assert.equal(imageData.width, w) 52 | assert.equal(imageData.height, h) 53 | assert.equal(imageData.data.length, w * h * 4) 54 | }, 55 | }, 56 | ] 57 | }) 58 | -------------------------------------------------------------------------------- /run/test.setup.js: -------------------------------------------------------------------------------- 1 | import { fork as nodeFork, spawn as nodeSpawn } from 'child_process' 2 | import { fileURLToPath } from 'url' 3 | 4 | export { strict as assert } from 'assert' 5 | 6 | export const args = () => process.argv.slice(2).reduce( 7 | (argo, arg) => { 8 | if (arg.startsWith('-')) { 9 | argo.opts[argo.prop = arg] = [] 10 | } else if (argo.prop) { 11 | argo.opts[argo.prop].push(arg) 12 | } else { 13 | argo.paths.push(arg) 14 | } 15 | 16 | return argo 17 | }, 18 | { opts: {}, paths: [] } 19 | ) 20 | 21 | export const fork = (...args) => new Promise((resolve, reject) => { 22 | const child = nodeFork(...args) 23 | 24 | child.on('error', reject) 25 | child.on('exit', resolve) 26 | }) 27 | 28 | export const spawn = (...args) => new Promise((resolve, reject) => { 29 | const child = nodeSpawn(...args) 30 | 31 | child.on('error', reject) 32 | child.on('exit', resolve) 33 | }) 34 | 35 | export const pathFrom = (...args) => fileURLToPath(args.reduce((url, bit) => new URL(bit, url), new URL('file:'))) 36 | 37 | export const test = async (setup) => { 38 | console.log(`Testing Node ${process.version}:`) 39 | console.log('') 40 | 41 | for (const test of setup()) { 42 | try { 43 | console.log(`- ${test.name}`) 44 | 45 | await test.test() 46 | } catch (error) { 47 | console.error(error) 48 | 49 | process.exit(1) 50 | } 51 | } 52 | 53 | console.log('') 54 | console.log('Pass!') 55 | console.log('') 56 | 57 | process.exit(0) 58 | } 59 | -------------------------------------------------------------------------------- /test/options.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Can exclude HTMLElement+', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target, { 12 | exclude: 'HTMLElement+' 13 | }) 14 | 15 | assert.equal(Reflect.has(target, 'Event'), true) 16 | assert.equal(Reflect.has(target, 'EventTarget'), true) 17 | assert.equal(Reflect.has(target, 'Element'), true) 18 | assert.equal(Reflect.has(target, 'HTMLElement'), false) 19 | assert.equal(Reflect.has(target, 'HTMLDivElement'), false) 20 | }, 21 | }, 22 | { 23 | name: 'Can exclude Event+', 24 | test() { 25 | const target = {} 26 | 27 | polyfill(target, { 28 | exclude: 'Event+' 29 | }) 30 | 31 | assert.equal(Reflect.has(target, 'Event'), false) 32 | assert.equal(Reflect.has(target, 'EventTarget'), false) 33 | assert.equal(Reflect.has(target, 'Element'), false) 34 | assert.equal(Reflect.has(target, 'HTMLElement'), false) 35 | assert.equal(Reflect.has(target, 'HTMLDivElement'), false) 36 | }, 37 | }, 38 | { 39 | name: 'Can exclude document', 40 | test() { 41 | const target = {} 42 | 43 | polyfill(target, { 44 | exclude: 'document' 45 | }) 46 | 47 | assert.equal(Reflect.has(target, 'Document'), true) 48 | assert.equal(Reflect.has(target, 'HTMLDocument'), true) 49 | assert.equal(Reflect.has(target, 'document'), false) 50 | }, 51 | }, 52 | ] 53 | }) 54 | -------------------------------------------------------------------------------- /src/lib/Storage.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class Storage { 4 | clear(): void { 5 | _.internalsOf(this, 'Storage', 'clear').storage.clear() 6 | } 7 | 8 | getItem(key: string): string | null { 9 | return getStringOrNull( 10 | _.internalsOf(this, 'Storage', 'getItem').storage.get(String(key)) 11 | ) 12 | } 13 | 14 | key(index: number): string | null { 15 | return getStringOrNull([ ..._.internalsOf(this, 'Storage', 'key').storage.keys() ][Number(index) || 0]) 16 | } 17 | 18 | removeItem(key: string): void { 19 | _.internalsOf(this, 'Storage', 'getItem').storage.delete(String(key)) 20 | } 21 | 22 | setItem(key: string, value: any): void { 23 | _.internalsOf(this, 'Storage', 'getItem').storage.set(String(key), String(value)) 24 | } 25 | 26 | get length() { 27 | return _.internalsOf(this, 'Storage', 'size').storage.size 28 | } 29 | } 30 | 31 | const getStringOrNull = (value: string | void) => typeof value === 'string' ? value : null 32 | 33 | export const initStorage = (target: any, exclude: Set) => { 34 | if (exclude.has('Storage') || exclude.has('localStorage')) return 35 | 36 | target.localStorage = Object.create(Storage.prototype) 37 | 38 | const storageInternals = new Map() 39 | 40 | _.INTERNALS.set(target.localStorage, { 41 | storage: storageInternals 42 | } as StorageInternals) 43 | } 44 | 45 | interface StorageInternals { 46 | storage: Map 47 | } 48 | -------------------------------------------------------------------------------- /test/storage.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Includes Storage functionality', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal(Reflect.has(target, 'Storage'), true) 14 | assert.equal(Reflect.has(target, 'localStorage'), true) 15 | assert.equal(typeof target.Storage, 'function') 16 | assert.equal(typeof target.localStorage, 'object') 17 | }, 18 | }, 19 | { 20 | name: 'Supports Storage methods', 21 | test() { 22 | const target = {} 23 | 24 | polyfill(target) 25 | 26 | assert.equal(target.localStorage.setItem('hello', 'world'), undefined) 27 | assert.equal(target.localStorage.getItem('hello'), 'world') 28 | assert.equal(target.localStorage.key(0), 'hello') 29 | assert.equal(target.localStorage.key(1), null) 30 | assert.equal(target.localStorage.length, 1) 31 | assert.equal(target.localStorage.setItem('world', 'hello'), undefined) 32 | assert.equal(target.localStorage.key(1), 'world') 33 | assert.equal(target.localStorage.key(2), null) 34 | assert.equal(target.localStorage.length, 2) 35 | assert.equal(target.localStorage.removeItem('hello'), undefined) 36 | assert.equal(target.localStorage.key(0), 'world') 37 | assert.equal(target.localStorage.key(1), null) 38 | assert.equal(target.localStorage.length, 1) 39 | assert.equal(target.localStorage.clear(), undefined) 40 | assert.equal(target.localStorage.key(0), null) 41 | assert.equal(target.localStorage.length, 0) 42 | }, 43 | }, 44 | ] 45 | }) 46 | -------------------------------------------------------------------------------- /src/lib/HTMLCanvasElement.ts: -------------------------------------------------------------------------------- 1 | import type { CanvasRenderingContext2D } from './CanvasRenderingContext2D' 2 | 3 | import * as _ from './utils' 4 | import { __createCanvasRenderingContext2D } from './CanvasRenderingContext2D' 5 | 6 | export class HTMLCanvasElement extends HTMLElement { 7 | get height(): number { 8 | return _.internalsOf(this, 'HTMLCanvasElement', 'height').height 9 | } 10 | 11 | set height(value) { 12 | _.internalsOf(this, 'HTMLCanvasElement', 'height').height = Number(value) || 0 13 | } 14 | 15 | get width(): number { 16 | return _.internalsOf(this, 'HTMLCanvasElement', 'width').width 17 | } 18 | 19 | set width(value) { 20 | _.internalsOf(this, 'HTMLCanvasElement', 'width').width = Number(value) || 0 21 | } 22 | 23 | captureStream(): null { 24 | return null 25 | } 26 | 27 | getContext(contextType: PredefinedContextId): CanvasRenderingContext2D | null { 28 | const internals = _.internalsOf(this, 'HTMLCanvasElement', 'getContext') 29 | 30 | switch (contextType) { 31 | case '2d': 32 | if (internals.renderingContext2D) return internals.renderingContext2D 33 | 34 | internals.renderingContext2D = __createCanvasRenderingContext2D(this) 35 | 36 | return internals.renderingContext2D 37 | default: 38 | return null 39 | } 40 | } 41 | 42 | toBlob() {} 43 | 44 | toDataURL() {} 45 | 46 | transferControlToOffscreen() {} 47 | } 48 | 49 | _.assignStringTag(HTMLCanvasElement) 50 | 51 | interface HTMLCanvasElementInternals { 52 | width: number 53 | height: number 54 | renderingContext2D: CanvasRenderingContext2D 55 | } 56 | 57 | type PredefinedContextId = '2d' | 'bitmaprenderer' | 'webgl' | 'webgl2' | 'webgpu' 58 | -------------------------------------------------------------------------------- /test/characterdata.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Includes CharacterData functionality', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal(Reflect.has(target, 'CharacterData'), true) 14 | assert.equal(Reflect.has(target, 'Text'), true) 15 | assert.equal(Reflect.has(target, 'Comment'), true) 16 | }, 17 | }, 18 | { 19 | name: 'Throws new CharacterData', 20 | test() { 21 | const target = {} 22 | 23 | polyfill(target) 24 | }, 25 | }, 26 | { 27 | name: 'Supports new Comment', 28 | test() { 29 | const target = polyfill({}) 30 | 31 | assert.doesNotThrow(() => { 32 | new target.Comment() 33 | }) 34 | 35 | assert.equal(new target.Comment().constructor.name, 'Comment') 36 | assert.equal(Object.prototype.toString.call(new target.Comment()), '[object Comment]') 37 | 38 | assert.equal(new target.Comment('hello').data, 'hello') 39 | assert.equal(new target.Comment('hello').textContent, 'hello') 40 | }, 41 | }, 42 | { 43 | name: 'Supports new Text', 44 | test() { 45 | const target = polyfill({}) 46 | 47 | assert.doesNotThrow(() => { 48 | new target.Text() 49 | }) 50 | 51 | assert.equal(new target.Text().constructor.name, 'Text') 52 | assert.equal(new target.Text().nodeName, '#text') 53 | assert.equal(Object.prototype.toString.call(new target.Text()), '[object Text]') 54 | 55 | assert.equal(new target.Text('hello').data, 'hello') 56 | assert.equal(new target.Text('hello').textContent, 'hello') 57 | }, 58 | }, 59 | ] 60 | }) 61 | -------------------------------------------------------------------------------- /src/lib/ContextEvent.ts: -------------------------------------------------------------------------------- 1 | import { Event } from 'event-target-shim' 2 | 3 | /** An event fired by a context requester to signal it desires a named context. */ 4 | export class ContextEvent extends Event<'context-request'> { 5 | constructor(init: ContextEventInit) { 6 | super('context-request', { bubbles: true, composed: true }) 7 | 8 | init = Object(init) as Required> 9 | 10 | this.context = init.context 11 | } 12 | 13 | context!: Context 14 | multiple!: boolean 15 | callback!: ContextCallback> 16 | } 17 | 18 | interface ContextEventInit { 19 | context: Context 20 | multiple?: boolean 21 | callback: ContextCallback> 22 | } 23 | 24 | /** A Context object defines an optional initial value for a Context, as well as a name identifier for debugging purposes. */ 25 | export type Context = { 26 | name: string 27 | initialValue?: T 28 | } 29 | 30 | /** A helper type which can extract a Context value type from a Context type. */ 31 | export type ContextType = T extends Context ? Y : never 32 | 33 | /** A function which creates a Context value object */ 34 | export function createContext(name: string, initialValue?: T): Readonly> { 35 | return { 36 | name, 37 | initialValue, 38 | } 39 | } 40 | 41 | /** A callback which is provided by a context requester and is called with the value satisfying the request. */ 42 | export type ContextCallback = ( 43 | value: ValueType, 44 | dispose?: () => void 45 | ) => void 46 | 47 | declare global { 48 | interface HTMLElementEventMap { 49 | /** A 'context-request' event can be emitted by any element which desires a context value to be injected by an external provider. */ 50 | 'context-request': ContextEvent 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/imagedata.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Supports ImageData', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal('ImageData' in target, true) 14 | assert.equal(typeof target['ImageData'], 'function') 15 | }, 16 | }, 17 | { 18 | name: 'Supports new (data: Uint8ClampedArray, width: number, height: number): ImageData', 19 | test() { 20 | const target = {} 21 | 22 | polyfill(target) 23 | 24 | const w = 640 25 | const h = 480 26 | const d = new Uint8ClampedArray(w * h * 4) 27 | 28 | const id = new target.ImageData(d, w, h) 29 | 30 | assert.equal(id.data, d) 31 | assert.equal(id.width, w) 32 | assert.equal(id.height, h) 33 | }, 34 | }, 35 | { 36 | name: 'Supports new (data: Uint8ClampedArray, width: number): ImageData', 37 | test() { 38 | const target = {} 39 | 40 | polyfill(target) 41 | 42 | const w = 640 43 | const h = 480 44 | const d = new Uint8ClampedArray(w * h * 4) 45 | 46 | const id = new target.ImageData(d, w) 47 | 48 | assert.equal(id.data, d) 49 | assert.equal(id.width, w) 50 | assert.equal(id.height, h) 51 | }, 52 | }, 53 | { 54 | name: 'Supports new (width: number, height: number): ImageData', 55 | test() { 56 | const target = {} 57 | 58 | polyfill(target) 59 | 60 | const w = 640 61 | const h = 480 62 | 63 | const id = new target.ImageData(w, h) 64 | 65 | assert.equal(id.data.length, w * h * 4) 66 | assert.equal(id.width, w) 67 | assert.equal(id.height, h) 68 | }, 69 | }, 70 | { 71 | name: 'Supports Object.keys(new ImageData(640, 480))', 72 | test() { 73 | const target = {} 74 | 75 | polyfill(target) 76 | 77 | const keys = Object.keys(new target.ImageData(640, 480)) 78 | 79 | assert.equal(keys.length, 1) 80 | assert.equal(keys[0], 'data') 81 | }, 82 | }, 83 | ] 84 | }) 85 | -------------------------------------------------------------------------------- /src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import { default as nodeFetch, Headers, Request, Response } from 'node-fetch/src/index.js' 2 | import Stream from 'node:stream' 3 | import * as _ from './utils' 4 | 5 | export { Headers, Request, Response } 6 | 7 | export const fetch = { 8 | fetch(resource: string | URL | Request, init?: Partial): Promise { 9 | const resourceURL = new URL( 10 | _.__object_isPrototypeOf(Request.prototype, resource) 11 | ? (resource as Request).url 12 | : _.pathToPosix(resource), 13 | typeof Object(globalThis.process).cwd === 'function' ? 'file:' + _.pathToPosix(process.cwd()) + '/' : 'file:' 14 | ) 15 | 16 | if (resourceURL.protocol.toLowerCase() === 'file:') { 17 | return import('node:fs').then( 18 | fs => { 19 | try { 20 | const stats = fs.statSync(resourceURL) 21 | const body = fs.createReadStream(resourceURL) 22 | 23 | return new Response( 24 | body, 25 | { 26 | status: 200, 27 | statusText: '', 28 | headers: { 29 | 'content-length': String(stats.size), 30 | 'date': new Date().toUTCString(), 31 | 'last-modified': new Date(stats.mtimeMs).toUTCString(), 32 | } 33 | } 34 | ) 35 | } catch (error) { 36 | const body = new Stream.Readable() 37 | 38 | body._read = () => {} 39 | body.push(null) 40 | 41 | return new Response( 42 | body, 43 | { 44 | status: 404, 45 | statusText: '', 46 | headers: { 47 | 'date': new Date().toUTCString(), 48 | } 49 | } 50 | ) 51 | } 52 | } 53 | ) 54 | } else { 55 | return nodeFetch(resource, init) 56 | } 57 | } 58 | }.fetch 59 | 60 | type USVString = ({} & string) 61 | 62 | interface FetchInit { 63 | body: Blob | BufferSource | FormData | URLSearchParams | ReadableStream | USVString 64 | cache: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache' | 'only-if-cached' 65 | credentials: 'omit' | 'same-origin' | 'include' 66 | headers: Headers | Record 67 | method: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH' | USVString 68 | mode: 'cors' | 'no-cors' | 'same-origin' | USVString 69 | redirect: 'follow' | 'manual' | 'error' 70 | referrer: USVString 71 | referrerPolicy: 'no-referrer' | 'no-referrer-when-downgrade' | 'same-origin' | 'origin' | 'strict-origin' | 'origin-when-cross-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' 72 | integrity: USVString 73 | keepalive: boolean 74 | signal: AbortSignal 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/OffscreenCanvas.ts: -------------------------------------------------------------------------------- 1 | import type { CanvasRenderingContext2D } from './CanvasRenderingContext2D' 2 | 3 | import * as _ from './utils' 4 | import { __createCanvasRenderingContext2D } from './CanvasRenderingContext2D' 5 | 6 | export class OffscreenCanvas extends EventTarget { 7 | constructor(width: number, height: number) { 8 | super() 9 | 10 | if (arguments.length < 2) throw new TypeError(`Failed to construct 'OffscreenCanvas': 2 arguments required.`) 11 | 12 | width = Number(width) || 0 13 | height = Number(height) || 0 14 | 15 | _.INTERNALS.set(this, { width, height } as OffscreenCanvasInternals) 16 | } 17 | 18 | get height(): number { 19 | return _.internalsOf(this, 'OffscreenCanvas', 'height').height 20 | } 21 | 22 | set height(value) { 23 | _.internalsOf(this, 'OffscreenCanvas', 'height').height = Number(value) || 0 24 | } 25 | 26 | get width(): number { 27 | return _.internalsOf(this, 'OffscreenCanvas', 'width').width 28 | } 29 | 30 | set width(value) { 31 | _.internalsOf(this, 'OffscreenCanvas', 'width').width = Number(value) || 0 32 | } 33 | 34 | getContext(contextType: PredefinedContextId): CanvasRenderingContext2D | null { 35 | const internals = _.internalsOf(this, 'HTMLCanvasElement', 'getContext') 36 | 37 | switch (contextType) { 38 | case '2d': 39 | if (internals.renderingContext2D) return internals.renderingContext2D 40 | 41 | internals.renderingContext2D = __createCanvasRenderingContext2D(this) 42 | 43 | return internals.renderingContext2D 44 | default: 45 | return null 46 | } 47 | } 48 | 49 | convertToBlob(options: Partial) { 50 | options = Object(options) 51 | 52 | const quality = Number(options.quality) || 0 53 | const type = getImageType(String(options.type).trim().toLowerCase()) 54 | 55 | void quality 56 | 57 | return Promise.resolve( 58 | new Blob([], { type }) 59 | ) 60 | } 61 | } 62 | 63 | _.assignStringTag(OffscreenCanvas) 64 | 65 | const getImageType = (type: string): PredefinedImageType => type === 'image/avif' || type === 'image/jpeg' || type === 'image/png' || type === 'image/webp' ? type : 'image/png' 66 | 67 | interface OffscreenCanvasInternals { 68 | height: number 69 | renderingContext2D: CanvasRenderingContext2D 70 | width: number 71 | } 72 | 73 | interface ConvertToBlobOptions { 74 | quality: number 75 | type: PredefinedImageType 76 | } 77 | 78 | type PredefinedContextId = '2d' | 'bitmaprenderer' | 'webgl' | 'webgl2' | 'webgpu' 79 | 80 | type PredefinedImageType = 'image/avif' | 'image/jpeg' | 'image/png' | 'image/webp' 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astropub/webapi", 3 | "description": "Use Web APIs in Node", 4 | "version": "0.10.14", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "import": "./mod.js", 9 | "types": "./mod.d.ts" 10 | }, 11 | "./apply": { 12 | "import": "./apply.js" 13 | } 14 | }, 15 | "main": "mod.js", 16 | "types": "mod.d.ts", 17 | "files": [ 18 | "apply.js", 19 | "mod.d.ts", 20 | "mod.js", 21 | "mod.js.map" 22 | ], 23 | "keywords": [ 24 | "astro", 25 | "api", 26 | "cancelAnimationFrame", 27 | "clearImmediate", 28 | "clearInterval", 29 | "fetch", 30 | "requestAnimationFrame", 31 | "setImmediate", 32 | "setInterval", 33 | "web" 34 | ], 35 | "license": "(CC0-1.0 AND MIT)", 36 | "repository": "astro-community/webapi", 37 | "author": "Jonathan Neal ", 38 | "contributors": [ 39 | "Jonathan Neal (https://github.com/jonathantneal)", 40 | "Toru Nagashima (https://github.com/mysticatea)", 41 | "Jimmy Wärting (https://github.com/jimmywarting)", 42 | "David Frank (https://github.com/bitinn)", 43 | "Mattias Buelens (https://github.com/MattiasBuelens)", 44 | "Diwank Singh (https://github.com/creatorrr)" 45 | ], 46 | "bugs": "https://github.com/astro-community/webapi/issues", 47 | "homepage": "https://github.com/astro-community/webapi#readme", 48 | "devDependencies": { 49 | "@rollup/plugin-alias": "3.1.9", 50 | "@rollup/plugin-inject": "4.0.4", 51 | "@rollup/plugin-node-resolve": "13.2.1", 52 | "@rollup/plugin-typescript": "8.3.2", 53 | "@ungap/structured-clone": "0.3.4", 54 | "abort-controller": "3.0.0", 55 | "event-target-shim": "6.0.2", 56 | "fetch-blob": "3.1.5", 57 | "formdata-polyfill": "4.0.10", 58 | "magic-string": "0.26.1", 59 | "node-fetch": "3.2.3", 60 | "rollup": "2.70.2", 61 | "rollup-plugin-terser": "7.0.2", 62 | "urlpattern-polyfill": "4.0.3", 63 | "web-streams-polyfill": "3.2.1" 64 | }, 65 | "scripts": { 66 | "build": "node run/build.js", 67 | "release": "node run/build.js && npm publish --access public", 68 | "test": "node run/test.js" 69 | }, 70 | "prettier": { 71 | "semi": false, 72 | "singleQuote": true, 73 | "trailingComma": "es5", 74 | "useTabs": true, 75 | "overrides": [ 76 | { 77 | "files": [ 78 | ".stackblitzrc", 79 | "*.json" 80 | ], 81 | "options": { 82 | "useTabs": false 83 | } 84 | } 85 | ] 86 | }, 87 | "volta": { 88 | "node": "16.13.1", 89 | "npm": "8.3.0" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/lib/ImageData.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class ImageData { 4 | constructor(width: number, height: number); 5 | constructor(width: number, height: number, settings: ImageDataSettings); 6 | constructor(data: Uint8ClampedArray, width: number); 7 | constructor(data: Uint8ClampedArray, width: number, height: number); 8 | constructor(data: Uint8ClampedArray, width: number, height: number, settings: ImageDataSettings); 9 | 10 | constructor(arg0: number | Uint8ClampedArray, arg1: number, ...args: [] | [number] | [ImageDataSettings] | [number, ImageDataSettings]) { 11 | if (arguments.length < 2) throw new TypeError(`Failed to construct 'ImageData': 2 arguments required.`) 12 | 13 | /** Whether Uint8ClampedArray data is provided. */ 14 | const hasData = _.__object_isPrototypeOf(Uint8ClampedArray.prototype, arg0) 15 | 16 | /** Image data, either provided or calculated. */ 17 | const d = hasData ? arg0 as Uint8ClampedArray : new Uint8ClampedArray(asNumber(arg0, 'width') * asNumber(arg1, 'height') * 4) 18 | 19 | /** Image width. */ 20 | const w = asNumber(hasData ? arg1 : arg0, 'width') 21 | 22 | /** Image height. */ 23 | const h = d.length / w / 4 24 | 25 | /** Image color space. */ 26 | const c = String(Object(hasData ? args[1] : args[0]).colorSpace || 'srgb') as PredefinedColorSpace 27 | 28 | // throw if a provided height does not match the calculated height 29 | if (args.length && asNumber(args[0], 'height') !== h) throw new DOMException('height is not equal to (4 * width * height)', 'IndexSizeError') 30 | 31 | // throw if a provided colorspace does not match a known colorspace 32 | if (c !== 'srgb' && c !== 'rec2020' && c !== 'display-p3') throw new TypeError('colorSpace is not known value') 33 | 34 | Object.defineProperty(this, 'data', { configurable: true, enumerable: true, value: d }) 35 | 36 | _.INTERNALS.set(this, { width: w, height: h, colorSpace: c } as ImageDataInternals) 37 | } 38 | 39 | get data(): Uint8ClampedArray { 40 | _.internalsOf(this, 'ImageData', 'data') 41 | 42 | return (Object.getOwnPropertyDescriptor(this, 'data') as { value: Uint8ClampedArray }).value 43 | } 44 | 45 | get width(): ImageDataInternals['width'] { 46 | return _.internalsOf(this, 'ImageData', 'width').width 47 | } 48 | 49 | get height(): ImageDataInternals['height'] { 50 | return _.internalsOf(this, 'ImageData', 'height').height 51 | } 52 | } 53 | 54 | _.assignStringTag(ImageData) 55 | 56 | /** Returns a coerced number, optionally throwing if the number is zero-ish. */ 57 | const asNumber = (value: any, axis: string): number => { 58 | value = Number(value) || 0 59 | 60 | if (value === 0) throw new TypeError(`The source ${axis} is zero or not a number.`) 61 | 62 | return value 63 | } 64 | 65 | interface ImageDataInternals { 66 | colorSpace: PredefinedColorSpace 67 | height: number 68 | width: number 69 | } 70 | 71 | interface ImageDataSettings { 72 | colorSpace?: PredefinedColorSpace 73 | } 74 | 75 | type PredefinedColorSpace = 'srgb' | 'rec2020' | 'display-p3' 76 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { performance } from 'node:perf_hooks' 2 | import { polyfill } from '../polyfill' 3 | 4 | /** Returns the milliseconds elapsed since January 1, 1970 00:00:00 UTC. */ 5 | export const __date_now = Date.now 6 | 7 | /** Returns the function bound to the given object. */ 8 | export const __function_bind = Function.bind.bind(Function.call as unknown as any) as any>(callback: TFunc, thisArg: unknown, ...args: TArgs) => TFunc 9 | 10 | /** Returns the function called with the specified values. */ 11 | export const __function_call = Function.call.bind(Function.call as unknown as any) as any>(callback: TFunc, thisArg: unknown, ...args: TArgs[]) => ReturnType 12 | 13 | /** Returns an object with the specified prototype. */ 14 | export const assign = Object.assign as (base: T, extension: E) => T 15 | 16 | /** Returns an object with the specified prototype. */ 17 | export const create = Object.create as { (value: T): any extends T ? Record : T } 18 | 19 | /** Returns whether an object has a property with the specified name. */ 20 | export const hasOwn = Function.call.bind(Object.prototype.hasOwnProperty) as { (object: T1, key: T2): T2 extends keyof T1 ? true : false } 21 | 22 | /** Returns a string representation of an object. */ 23 | export const __object_toString = Function.call.bind(Object.prototype.toString) as { (value: any): string } 24 | 25 | /** Returns whether the object prototype exists in another object. */ 26 | export const __object_isPrototypeOf = Function.call.bind(Object.prototype.isPrototypeOf) as { (p: T1, v: T2): T2 extends T1 ? true : false } 27 | 28 | /** Current high resolution millisecond timestamp. */ 29 | export const __performance_now = performance.now as () => number 30 | 31 | /** Returns the string escaped for use inside regular expressions. */ 32 | export const __string_escapeRegExp = (value: string) => value.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&') 33 | 34 | export const INTERNALS = new WeakMap() 35 | 36 | export const internalsOf = (target: T | object, className: string, propName: string): T => { 37 | const internals: T = INTERNALS.get(target) 38 | 39 | if (!internals) throw new TypeError(`${className}.${propName} can only be used on instances of ${className}`) 40 | 41 | return internals 42 | } 43 | 44 | export const internalsTo = (target: object, internals: T): T => assign( 45 | INTERNALS.get(target) || INTERNALS.set(target, {}).get(target), 46 | internals 47 | ) 48 | 49 | /** Assigns a string tag to the given constructor. */ 50 | export const assignStringTag = (Class: new (...args: any) => object) => Class.prototype[Symbol.toStringTag] = Class.name 51 | 52 | /** Returns any kind of path as a posix path. */ 53 | export const pathToPosix = (pathname: any) => String( 54 | pathname == null ? '' : pathname 55 | ).replace( 56 | // convert slashes 57 | /\\+/g, '/' 58 | ).replace( 59 | // prefix a slash to drive letters 60 | /^(?=[A-Za-z]:\/)/, '/' 61 | ).replace( 62 | // encode path characters 63 | /%/g, '%25' 64 | ).replace( 65 | /\n/g, '%0A' 66 | ).replace( 67 | /\r/g, '%0D' 68 | ).replace( 69 | /\t/g, '%09' 70 | ) 71 | -------------------------------------------------------------------------------- /test/elements.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Includes Custom Element functionality', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal(Reflect.has(target, 'CustomElementRegistry'), true) 14 | assert.equal(Reflect.has(target, 'customElements'), true) 15 | assert.equal(Reflect.has(target, 'HTMLElement'), true) 16 | }, 17 | }, 18 | { 19 | name: 'Supports Custom Element creation', 20 | test() { 21 | const target = {} 22 | 23 | polyfill(target) 24 | 25 | const CustomElement = class HTMLCustomElement extends target.HTMLElement {} 26 | 27 | target.customElements.define('custom-element', CustomElement) 28 | 29 | assert.equal(target.customElements.get('custom-element'), CustomElement) 30 | assert.equal(target.customElements.getName(CustomElement), 'custom-element') 31 | }, 32 | }, 33 | { 34 | name: 'Supports Custom Elements created from Document', 35 | test() { 36 | const target = {} 37 | 38 | polyfill(target) 39 | 40 | assert.equal(target.document.body.localName, 'body') 41 | assert.equal(target.document.body.tagName, 'BODY') 42 | 43 | assert.equal(target.document.createElement('custom-element').constructor.name, 'HTMLUnknownElement') 44 | 45 | const CustomElement = class HTMLCustomElement extends target.HTMLElement {} 46 | 47 | target.customElements.define('custom-element', CustomElement) 48 | 49 | assert.equal(target.document.createElement('custom-element').constructor.name, 'HTMLCustomElement') 50 | }, 51 | }, 52 | { 53 | name: 'Supports Custom Elements with properties', 54 | test() { 55 | const target = {} 56 | 57 | polyfill(target) 58 | 59 | const testSymbol = Symbol.for('webapi.test') 60 | 61 | const CustomElement = class HTMLCustomElement extends target.HTMLElement { 62 | otherMethod = () => testSymbol 63 | 64 | method() { 65 | return this.otherMethod() 66 | } 67 | 68 | static method() { 69 | return this.otherMethod() 70 | } 71 | 72 | static otherMethod() { 73 | return testSymbol 74 | } 75 | } 76 | 77 | target.customElements.define('custom-element', CustomElement) 78 | 79 | assert.equal(CustomElement.method(), testSymbol) 80 | 81 | const customElement = new CustomElement() 82 | 83 | assert.equal(customElement.method(), testSymbol) 84 | }, 85 | }, 86 | { 87 | name: 'Supports Custom Elements with new', 88 | test() { 89 | const target = {} 90 | 91 | polyfill(target) 92 | 93 | const CustomElement = class HTMLCustomElement extends target.HTMLElement {} 94 | 95 | target.customElements.define('custom-element', CustomElement) 96 | 97 | const customElement = new CustomElement() 98 | 99 | assert.equal(customElement instanceof CustomElement, true) 100 | assert.equal(customElement instanceof target.HTMLElement, true) 101 | assert.equal(customElement instanceof target.Element, true) 102 | assert.equal(customElement instanceof target.Node, true) 103 | assert.equal(customElement instanceof target.EventTarget, true) 104 | }, 105 | }, 106 | ] 107 | }) 108 | -------------------------------------------------------------------------------- /src/lib/CustomElementRegistry.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class CustomElementRegistry { 4 | /** Defines a new custom element using the given tag name and HTMLElement constructor. */ 5 | define(localName: string, constructor: Function, options?: ElementDefinitionOptions) { 6 | const internals = _.internalsOf(this, 'CustomElementRegistry', 'define') 7 | const document = internals.document 8 | const documentInternals = _.internalsOf(document, 'CustomElementRegistry', 'define') 9 | 10 | localName = getCustomElementLocalName(localName) 11 | 12 | documentInternals.constructorByName.set(localName, constructor) 13 | documentInternals.nameByConstructor.set(constructor, localName) 14 | 15 | _.INTERNALS.set(constructor, { document }) 16 | 17 | void options 18 | } 19 | 20 | /** Returns the constructor associated with the given tag name. */ 21 | get(localName: string) { 22 | const internals = _.internalsOf(this, 'CustomElementRegistry', 'define') 23 | const documentInternals = _.internalsOf(internals.document, 'CustomElementRegistry', 'define') 24 | 25 | return documentInternals.constructorByName.get(localName) 26 | } 27 | 28 | getName(constructor: Function) { 29 | const internals = _.internalsOf(this, 'CustomElementRegistry', 'define') 30 | const documentInternals = _.internalsOf(internals.document, 'CustomElementRegistry', 'define') 31 | 32 | return documentInternals.nameByConstructor.get(constructor) 33 | } 34 | } 35 | 36 | _.assignStringTag(CustomElementRegistry) 37 | 38 | // initialization 39 | // ----------------------------------------------------------------------------- 40 | 41 | export const initCustomElementRegistry = (target: any, exclude: Set, pseudo: any) => { 42 | if (!_.hasOwn(pseudo, 'customElements')) { 43 | /** Reference to the CustomElementRegistry object. */ 44 | const customElements: CustomElementRegistry = pseudo.customElements = target.customElements || Object.create(target.CustomElementRegistry.prototype) 45 | 46 | _.internalsTo(customElements, { 47 | document: pseudo.document 48 | }) 49 | } 50 | 51 | if (!exclude.has('customElements')) target.customElements = pseudo.customElements 52 | } 53 | 54 | // internal functionality 55 | // ----------------------------------------------------------------------------- 56 | 57 | const getCustomElementLocalName = (localName: any): string => { 58 | localName = String(localName) 59 | 60 | if (/[A-Z]/.test(localName)) throw new SyntaxError('Custom element name cannot contain an uppercase ASCII letter') 61 | if (!/^[a-z]/.test(localName)) throw new SyntaxError('Custom element name must have a lowercase ASCII letter as its first character') 62 | if (!/-/.test(localName)) throw new SyntaxError('Custom element name must contain a hyphen') 63 | 64 | localName = localName.toLowerCase() 65 | 66 | return localName 67 | } 68 | 69 | // interfaces 70 | // ----------------------------------------------------------------------------- 71 | 72 | interface ElementDefinitionOptions { 73 | extends?: string | undefined; 74 | } 75 | 76 | interface CustomElementRegistryInternals { 77 | document: Document 78 | } 79 | 80 | interface DocumentInternals { 81 | activeElement: HTMLElement, 82 | body: HTMLBodyElement 83 | documentElement: HTMLHtmlElement 84 | head: HTMLHeadElement 85 | 86 | constructorByName: Map 87 | nameByConstructor: Map 88 | } 89 | 90 | interface ElementConstructorInternals { 91 | document: Document 92 | } 93 | -------------------------------------------------------------------------------- /test/fetch.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | return [ 6 | { 7 | name: 'Fetch functionality', 8 | test() { 9 | const target = {} 10 | 11 | polyfill(target) 12 | 13 | assert.equal(Reflect.has(target, 'fetch'), true) 14 | assert.equal(typeof target.fetch, 'function') 15 | }, 16 | }, 17 | { 18 | name: 'Fetch with https', 19 | async test() { 20 | const target = {} 21 | 22 | polyfill(target) 23 | 24 | const { fetch } = target 25 | 26 | const response = await fetch('https://api.openbrewerydb.org/breweries') 27 | 28 | assert.equal(response.constructor, target.Response) 29 | 30 | const json = await response.json() 31 | 32 | assert.equal(Array.isArray(json), true) 33 | }, 34 | }, 35 | { 36 | name: 'Fetch with file', 37 | async test() { 38 | const target = {} 39 | 40 | polyfill(target) 41 | 42 | const { fetch } = target 43 | 44 | const url = new URL('../package.json', import.meta.url) 45 | 46 | const response = await fetch(url) 47 | 48 | assert.equal(response.constructor, target.Response) 49 | 50 | assert.equal(response.status, 200) 51 | assert.equal(response.statusText, '') 52 | assert.equal(response.headers.has('date'), true) 53 | assert.equal(response.headers.has('content-length'), true) 54 | assert.equal(response.headers.has('last-modified'), true) 55 | 56 | const json = await response.json() 57 | 58 | assert.equal(json.name, '@astropub/webapi') 59 | }, 60 | }, 61 | { 62 | name: 'Fetch with missing file', 63 | async test() { 64 | const target = {} 65 | 66 | polyfill(target) 67 | 68 | const { fetch } = target 69 | 70 | const url = new URL('../missing.json', import.meta.url) 71 | 72 | const response = await fetch(url) 73 | 74 | assert.equal(response.constructor, target.Response) 75 | 76 | assert.equal(response.status, 404) 77 | assert.equal(response.statusText, '') 78 | assert.equal(response.headers.has('date'), true) 79 | assert.equal(response.headers.has('content-length'), false) 80 | assert.equal(response.headers.has('last-modified'), false) 81 | }, 82 | }, 83 | { 84 | name: 'Fetch with (file) Request', 85 | async test() { 86 | const target = {} 87 | 88 | polyfill(target) 89 | 90 | const { Request, fetch } = target 91 | 92 | const request = new Request(new URL('../package.json', import.meta.url)) 93 | 94 | const response = await fetch(request) 95 | 96 | assert.equal(response.constructor, target.Response) 97 | 98 | const json = await response.json() 99 | 100 | assert.equal(json.name, '@astropub/webapi') 101 | }, 102 | }, 103 | { 104 | name: 'Fetch with relative file', 105 | async test() { 106 | const target = {} 107 | 108 | polyfill(target) 109 | 110 | const { fetch } = target 111 | 112 | const response = await fetch('package.json') 113 | 114 | const json = await response.json() 115 | 116 | assert.equal(json.name, '@astropub/webapi') 117 | }, 118 | }, 119 | { 120 | name: 'Fetch with data', 121 | async test() { 122 | const target = {} 123 | 124 | polyfill(target) 125 | 126 | const { fetch } = target 127 | 128 | const jsonURI = `data:application/json,${encodeURIComponent(JSON.stringify({ 129 | name: '@astropub/webapi' 130 | }))}` 131 | 132 | const response = await fetch(jsonURI) 133 | 134 | const json = await response.json() 135 | 136 | assert.equal(json.name, '@astropub/webapi') 137 | }, 138 | }, 139 | ] 140 | }) 141 | -------------------------------------------------------------------------------- /src/lib/Node.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class Node extends EventTarget { 4 | append(...nodesOrDOMStrings: NodeOrString[]): void { 5 | void nodesOrDOMStrings 6 | } 7 | 8 | appendChild(childNode: Node): Node { 9 | return childNode 10 | } 11 | 12 | after(...nodesOrDOMStrings: NodeOrString[]): void { 13 | void nodesOrDOMStrings 14 | } 15 | 16 | before(...nodesOrDOMStrings: NodeOrString[]): void { 17 | void nodesOrDOMStrings 18 | } 19 | 20 | prepend(...nodesOrDOMStrings: NodeOrString[]): void { 21 | void nodesOrDOMStrings 22 | } 23 | 24 | replaceChild(newChild: Node, oldChild: Node): Node { 25 | void newChild 26 | 27 | return oldChild 28 | } 29 | 30 | removeChild(childNode: Node): Node { 31 | return childNode 32 | } 33 | 34 | get attributes(): object { 35 | return {} 36 | } 37 | 38 | get childNodes(): Node[] { 39 | return [] 40 | } 41 | 42 | get children(): Element[] { 43 | return [] 44 | } 45 | 46 | get ownerDocument(): Node | null { 47 | return null 48 | } 49 | 50 | get nodeValue(): string { 51 | return '' 52 | } 53 | 54 | set nodeValue(value: string) { 55 | void value 56 | } 57 | 58 | get textContent(): string { 59 | return '' 60 | } 61 | 62 | set textContent(value: string) { 63 | void value 64 | } 65 | 66 | get previousElementSibling(): Node | null { 67 | return null 68 | } 69 | 70 | get nextElementSibling(): Node | null { 71 | return null 72 | } 73 | 74 | [Symbol.for('nodejs.util.inspect.custom')](depth: number, options: Record) { 75 | return `${this.constructor.name}`; 76 | } 77 | } 78 | 79 | export class DocumentFragment extends Node {} 80 | 81 | export class ShadowRoot extends DocumentFragment { 82 | get innerHTML() { 83 | return '' 84 | } 85 | 86 | set innerHTML(value: string) { 87 | void value 88 | } 89 | } 90 | 91 | export const NodeFilter = Object.assign({ 92 | NodeFilter() { 93 | throw new TypeError('Illegal constructor') 94 | } 95 | }.NodeFilter, { 96 | FILTER_ACCEPT: 1, 97 | FILTER_REJECT: 2, 98 | FILTER_SKIP: 3, 99 | SHOW_ALL: 4294967295, 100 | SHOW_ELEMENT: 1, 101 | SHOW_ATTRIBUTE: 2, 102 | SHOW_TEXT: 4, 103 | SHOW_CDATA_SECTION: 8, 104 | SHOW_ENTITY_REFERENCE: 16, 105 | SHOW_ENTITY: 32, 106 | SHOW_PROCESSING_INSTRUCTION: 64, 107 | SHOW_COMMENT: 128, 108 | SHOW_DOCUMENT: 256, 109 | SHOW_DOCUMENT_TYPE: 512, 110 | SHOW_DOCUMENT_FRAGMENT: 1024, 111 | SHOW_NOTATION: 2048, 112 | }) 113 | 114 | export class NodeIterator { 115 | nextNode(): Node | null { 116 | return null 117 | } 118 | 119 | previousNode(): Node | null { 120 | return null 121 | } 122 | 123 | get filter(): NodeFilter { 124 | const internals = _.internalsOf(this, 'NodeIterator', 'filter') 125 | return internals.filter 126 | } 127 | 128 | get pointerBeforeReferenceNode(): boolean { 129 | const internals = _.internalsOf(this, 'NodeIterator', 'pointerBeforeReferenceNode') 130 | return internals.pointerBeforeReferenceNode 131 | } 132 | 133 | get referenceNode(): Node { 134 | const internals = _.internalsOf(this, 'NodeIterator', 'referenceNode') 135 | return internals.referenceNode 136 | } 137 | 138 | get root(): Node { 139 | const internals = _.internalsOf(this, 'NodeIterator', 'root') 140 | return internals.root 141 | } 142 | 143 | get whatToShow(): number { 144 | const internals = _.internalsOf(this, 'NodeIterator', 'whatToShow') 145 | return internals.whatToShow 146 | } 147 | } 148 | 149 | _.assignStringTag(Node) 150 | _.assignStringTag(NodeIterator) 151 | _.assignStringTag(DocumentFragment) 152 | _.assignStringTag(ShadowRoot) 153 | 154 | type NodeOrString = string | Node 155 | 156 | export interface NodeFilter { 157 | acceptNode(node: Node): number 158 | } 159 | 160 | export interface NodeIteratorInternals { 161 | filter: NodeFilter 162 | pointerBeforeReferenceNode: boolean 163 | referenceNode: Node 164 | root: Node 165 | whatToShow: number 166 | } 167 | -------------------------------------------------------------------------------- /src/ponyfill.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { AbortController, AbortSignal } from 'abort-controller/dist/abort-controller.mjs' 4 | import { requestAnimationFrame, cancelAnimationFrame } from './lib/AnimationFrame' 5 | import { atob, btoa } from './lib/Base64' 6 | import { CharacterData, Comment, Text } from './lib/CharacterData' 7 | import { File, Blob } from 'fetch-blob/from.js' 8 | import { CustomEvent } from './lib/CustomEvent' 9 | import { DOMException } from './lib/DOMException' 10 | import { TreeWalker } from './lib/TreeWalker' 11 | import { cancelIdleCallback, requestIdleCallback } from './lib/IdleCallback' 12 | import { Event, EventTarget } from 'event-target-shim' 13 | import { fetch, Headers, Request, Response } from './lib/fetch' 14 | import { FormData } from 'formdata-polyfill/esm.min.js' 15 | import { ByteLengthQueuingStrategy, CountQueuingStrategy, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, TransformStream, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter } from 'web-streams-polyfill/dist/ponyfill.es6.mjs' 16 | // @ts-expect-error 17 | import { URLPattern } from 'urlpattern-polyfill' 18 | import { setTimeout, clearTimeout } from './lib/Timeout' 19 | import structuredClone from './lib/structuredClone' 20 | 21 | import { CanvasRenderingContext2D } from './lib/CanvasRenderingContext2D' 22 | import { CSSStyleSheet, StyleSheet } from './lib/StyleSheet' 23 | import { CustomElementRegistry, initCustomElementRegistry } from './lib/CustomElementRegistry' 24 | import { Document, HTMLDocument, initDocument } from './lib/Document' 25 | import { DocumentFragment, Node, NodeFilter, NodeIterator, ShadowRoot } from './lib/Node' 26 | import { Element, HTMLElement, HTMLBodyElement, HTMLDivElement, HTMLHeadElement, HTMLHtmlElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement } from './lib/Element' 27 | import { HTMLCanvasElement } from './lib/HTMLCanvasElement' 28 | import { HTMLImageElement } from './lib/HTMLImageElement' 29 | import { Image } from './lib/Image' 30 | import { ImageData } from './lib/ImageData' 31 | import { IntersectionObserver, MutationObserver, ResizeObserver } from './lib/Observer' 32 | import { MediaQueryList, initMediaQueryList } from './lib/MediaQueryList' 33 | import { OffscreenCanvas } from './lib/OffscreenCanvas' 34 | import { Storage, initStorage } from './lib/Storage' 35 | import { Window, initWindow } from './lib/Window' 36 | 37 | import { alert } from './lib/Alert' 38 | 39 | import { initObject } from './lib/Object' 40 | import { initPromise } from './lib/Promise' 41 | import { initRelativeIndexingMethod } from './lib/RelativeIndexingMethod' 42 | import { initString } from './lib/String' 43 | 44 | export { 45 | AbortController, 46 | AbortSignal, 47 | Blob, 48 | ByteLengthQueuingStrategy, 49 | CanvasRenderingContext2D, 50 | CharacterData, 51 | Comment, 52 | CountQueuingStrategy, 53 | CSSStyleSheet, 54 | CustomElementRegistry, 55 | CustomEvent, 56 | DOMException, 57 | Document, 58 | DocumentFragment, 59 | Element, 60 | Event, 61 | EventTarget, 62 | File, 63 | FormData, 64 | Headers, 65 | HTMLBodyElement, 66 | HTMLCanvasElement, 67 | HTMLDivElement, 68 | HTMLDocument, 69 | HTMLElement, 70 | HTMLHeadElement, 71 | HTMLHtmlElement, 72 | HTMLImageElement, 73 | HTMLSpanElement, 74 | HTMLStyleElement, 75 | HTMLTemplateElement, 76 | HTMLUnknownElement, 77 | Image, 78 | ImageData, 79 | IntersectionObserver, 80 | MediaQueryList, 81 | MutationObserver, 82 | Node, 83 | NodeFilter, 84 | NodeIterator, 85 | OffscreenCanvas, 86 | ReadableByteStreamController, 87 | ReadableStream, 88 | ReadableStreamBYOBReader, 89 | ReadableStreamBYOBRequest, 90 | ReadableStreamDefaultController, 91 | ReadableStreamDefaultReader, 92 | Request, 93 | ResizeObserver, 94 | Response, 95 | ShadowRoot, 96 | Storage, 97 | StyleSheet, 98 | Text, 99 | TransformStream, 100 | TreeWalker, 101 | URLPattern, 102 | WritableStream, 103 | WritableStreamDefaultController, 104 | WritableStreamDefaultWriter, 105 | Window, 106 | 107 | alert, 108 | atob, 109 | btoa, 110 | cancelAnimationFrame, 111 | cancelIdleCallback, 112 | clearTimeout, 113 | fetch, 114 | requestAnimationFrame, 115 | requestIdleCallback, 116 | setTimeout, 117 | structuredClone, 118 | 119 | initCustomElementRegistry, 120 | initDocument, 121 | initMediaQueryList, 122 | initObject, 123 | initPromise, 124 | initRelativeIndexingMethod, 125 | initStorage, 126 | initString, 127 | initWindow, 128 | } 129 | -------------------------------------------------------------------------------- /src/lib/Document.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | import { Text } from './CharacterData' 3 | import { TreeWalker } from './TreeWalker' 4 | 5 | export class Document extends Node { 6 | createElement(name: string) { 7 | const internals = _.internalsOf(this, 'Document', 'createElement') 8 | 9 | name = String(name).toLowerCase() 10 | 11 | const TypeOfHTMLElement = internals.constructorByName.get(name) || HTMLUnknownElement 12 | 13 | const element = Object.setPrototypeOf(new EventTarget(), TypeOfHTMLElement.prototype) as HTMLElement 14 | 15 | _.INTERNALS.set(element, { 16 | attributes: {}, 17 | localName: name, 18 | ownerDocument: this, 19 | shadowInit: null as unknown as ShadowRootInit, 20 | shadowRoot: null as unknown as ShadowRoot, 21 | }) 22 | 23 | return element 24 | } 25 | 26 | createNodeIterator(root: Node, whatToShow: number = NodeFilter.SHOW_ALL, filter?: NodeIteratorInternals['filter']) { 27 | const target = Object.create(NodeIterator.prototype) 28 | 29 | _.INTERNALS.set(target, { filter, pointerBeforeReferenceNode: false, referenceNode: root, root, whatToShow } as NodeIteratorInternals) 30 | 31 | return target 32 | } 33 | 34 | createTextNode(data: string) { 35 | return new Text(data) 36 | } 37 | 38 | createTreeWalker(root: Node, whatToShow: number = NodeFilter.SHOW_ALL, filter?: NodeFilter, expandEntityReferences?: boolean) { 39 | const target = Object.create(TreeWalker.prototype) 40 | 41 | _.INTERNALS.set(target, { filter, currentNode: root, root, whatToShow } as TreeWalkerInternals) 42 | 43 | return target 44 | } 45 | 46 | get adoptedStyleSheets(): StyleSheet[] { 47 | return [] 48 | } 49 | 50 | get styleSheets(): StyleSheet[] { 51 | return [] 52 | } 53 | 54 | activeElement!: HTMLElement 55 | body!: HTMLBodyElement 56 | documentElement!: HTMLHtmlElement 57 | head!: HTMLHeadElement 58 | } 59 | 60 | export class HTMLDocument extends Document {} 61 | 62 | _.assignStringTag(Document) 63 | _.assignStringTag(HTMLDocument) 64 | 65 | // initialization 66 | // ----------------------------------------------------------------------------- 67 | 68 | export const initDocument = (target: any, exclude: Set, pseudo: any) => { 69 | if (!_.hasOwn(pseudo, 'document')) { 70 | /** Reference to the HTMLDocument object. */ 71 | const document: HTMLDocument = pseudo.document = pseudo.document || Object.setPrototypeOf(new pseudo.EventTarget, pseudo.HTMLDocument.prototype) 72 | 73 | /** Document internals. */ 74 | const internals = _.internalsTo(document, { 75 | constructorByName: new Map, 76 | nameByConstructor: new Map, 77 | }) 78 | 79 | const registerConstructor = (constructor: Function, name: string) => { 80 | internals.constructorByName.set(name, constructor) 81 | internals.nameByConstructor.set(constructor, name) 82 | 83 | _.internalsTo(constructor, { document }) 84 | } 85 | 86 | registerConstructor(pseudo.HTMLElement, 'article') 87 | registerConstructor(pseudo.HTMLElement, 'aside') 88 | registerConstructor(pseudo.HTMLBodyElement, 'body') 89 | registerConstructor(pseudo.HTMLCanvasElement, 'canvas') 90 | registerConstructor(pseudo.HTMLDivElement, 'div') 91 | registerConstructor(pseudo.HTMLElement, 'footer') 92 | registerConstructor(pseudo.HTMLHeadElement, 'head') 93 | registerConstructor(pseudo.HTMLElement, 'header') 94 | registerConstructor(pseudo.HTMLHtmlElement, 'html') 95 | registerConstructor(pseudo.HTMLImageElement, 'img') 96 | registerConstructor(pseudo.HTMLElement, 'main') 97 | registerConstructor(pseudo.HTMLElement, 'nav') 98 | registerConstructor(pseudo.HTMLElement, 'section') 99 | registerConstructor(pseudo.HTMLSpanElement, 'span') 100 | registerConstructor(pseudo.HTMLStyleElement, 'style') 101 | 102 | _.assign(document, { 103 | body: document.createElement('body'), 104 | documentElement: document.createElement('html'), 105 | head: document.createElement('head'), 106 | }) 107 | } 108 | 109 | if (!exclude.has('document')) target.document = pseudo.document 110 | } 111 | 112 | // interfaces 113 | // ----------------------------------------------------------------------------- 114 | 115 | interface DocumentInternals { 116 | constructorByName: Map 117 | nameByConstructor: Map 118 | } 119 | 120 | interface ElementInternals { 121 | attributes: { [name: string]: string }, 122 | localName: string 123 | ownerDocument: Document 124 | shadowRoot: ShadowRoot 125 | shadowInit: ShadowRootInit 126 | } 127 | 128 | interface ShadowRootInit extends Record { 129 | mode?: string 130 | } 131 | 132 | interface NodeIteratorInternals { 133 | filter: NodeFilter 134 | pointerBeforeReferenceNode: boolean 135 | referenceNode: Node 136 | root: Node 137 | whatToShow: number 138 | } 139 | 140 | interface TreeWalkerInternals { 141 | filter: NodeFilter 142 | currentNode: Node 143 | root: Node 144 | whatToShow: number 145 | } 146 | 147 | interface ElementConstructorInternals { 148 | document: Document 149 | } 150 | -------------------------------------------------------------------------------- /src/lib/Element.ts: -------------------------------------------------------------------------------- 1 | import * as _ from './utils' 2 | 3 | export class Element extends Node { 4 | constructor() { 5 | super() 6 | 7 | if (_.INTERNALS.has(new.target)) { 8 | const constructorInternals = _.internalsOf(new.target, 'Element', 'ownerDocument') 9 | const documentInternals = _.INTERNALS.get(constructorInternals.document) as DocumentInternals 10 | 11 | _.INTERNALS.set(this, { 12 | attributes: {}, 13 | localName: documentInternals.nameByConstructor.get(new.target) || '', 14 | ownerDocument: constructorInternals.document, 15 | shadowInit: null as unknown as ShadowRootInit, 16 | shadowRoot: null as unknown as ShadowRoot, 17 | }) 18 | } 19 | } 20 | 21 | hasAttribute(name: string): boolean { 22 | void name 23 | 24 | return false 25 | } 26 | 27 | getAttribute(name: string): string | null { 28 | return null 29 | } 30 | 31 | setAttribute(name: string, value: string): void { 32 | void name 33 | void value 34 | } 35 | 36 | removeAttribute(name: string): void { 37 | void name 38 | } 39 | 40 | attachShadow(init: Partial) { 41 | if (arguments.length < 1) throw new TypeError(`Failed to execute 'attachShadow' on 'Element': 1 argument required, but only 0 present.`) 42 | 43 | if (init !== Object(init)) throw new TypeError(`Failed to execute 'attachShadow' on 'Element': The provided value is not of type 'ShadowRootInit'.`) 44 | 45 | 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.`) 46 | 47 | const internals = _.internalsOf(this, 'Element', 'attachShadow') 48 | 49 | if (internals.shadowRoot) throw new Error('The operation is not supported.') 50 | 51 | internals.shadowInit = internals.shadowInit || { 52 | mode: init.mode, 53 | delegatesFocus: Boolean(init.delegatesFocus), 54 | } 55 | 56 | internals.shadowRoot = internals.shadowRoot || (/^open$/.test(internals.shadowInit.mode) ? Object.setPrototypeOf(new EventTarget(), ShadowRoot.prototype) as ShadowRoot : null) 57 | 58 | return internals.shadowRoot 59 | } 60 | 61 | get assignedSlot(): HTMLSlotElement | null { 62 | return null 63 | } 64 | 65 | get innerHTML(): string { 66 | _.internalsOf(this, 'Element', 'innerHTML') 67 | 68 | return '' 69 | } 70 | 71 | set innerHTML(value) { 72 | _.internalsOf(this, 'Element', 'innerHTML') 73 | 74 | void value 75 | } 76 | 77 | get ownerDocument(): Document | null { 78 | const internals = _.internalsOf(this, 'Element', 'ownerDocument') 79 | 80 | return internals.ownerDocument 81 | } 82 | 83 | get shadowRoot(): ShadowRoot | null { 84 | const internals = _.internalsOf(this, 'Element', 'shadowRoot') 85 | 86 | return Object(internals.shadowInit).mode === 'open' ? internals.shadowRoot : null 87 | } 88 | 89 | get localName(): string { 90 | return _.internalsOf(this, 'Element', 'localName').localName 91 | } 92 | 93 | get nodeName(): string { 94 | return _.internalsOf(this, 'Element', 'nodeName').localName.toUpperCase() 95 | } 96 | 97 | get tagName(): string { 98 | return _.internalsOf(this, 'Element', 'tagName').localName.toUpperCase() 99 | } 100 | } 101 | 102 | export class HTMLElement extends Element {} 103 | 104 | export class HTMLBodyElement extends HTMLElement {} 105 | export class HTMLDivElement extends HTMLElement {} 106 | export class HTMLHeadElement extends HTMLElement {} 107 | export class HTMLHtmlElement extends HTMLElement {} 108 | export class HTMLSpanElement extends HTMLElement {} 109 | export class HTMLStyleElement extends HTMLElement {} 110 | export class HTMLTemplateElement extends HTMLElement {} 111 | export class HTMLUnknownElement extends HTMLElement {} 112 | 113 | _.assignStringTag(Element) 114 | _.assignStringTag(HTMLElement) 115 | _.assignStringTag(HTMLBodyElement) 116 | _.assignStringTag(HTMLDivElement) 117 | _.assignStringTag(HTMLHeadElement) 118 | _.assignStringTag(HTMLHtmlElement) 119 | _.assignStringTag(HTMLSpanElement) 120 | _.assignStringTag(HTMLStyleElement) 121 | _.assignStringTag(HTMLTemplateElement) 122 | _.assignStringTag(HTMLUnknownElement) 123 | 124 | // interfaces 125 | // ----------------------------------------------------------------------------- 126 | 127 | interface ElementConstructorInternals { 128 | document: Document 129 | } 130 | 131 | interface ElementInternals { 132 | attributes: { [name: string]: string } 133 | localName: string 134 | ownerDocument: Document 135 | shadowInit: ShadowRootInit | void 136 | shadowRoot: ShadowRoot | null 137 | } 138 | 139 | interface ShadowRootInit { 140 | mode: 'open' | 'closed' 141 | delegatesFocus: boolean 142 | } 143 | 144 | interface DocumentInternals { 145 | activeElement: HTMLElement, 146 | body: HTMLBodyElement 147 | documentElement: HTMLHtmlElement 148 | head: HTMLHeadElement 149 | 150 | constructorByName: Map 151 | nameByConstructor: Map 152 | } 153 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | import { assert, test } from '../run/test.setup.js' 2 | import { polyfill } from '../mod.js' 3 | 4 | test(() => { 5 | polyfill(globalThis) 6 | 7 | return [ 8 | { 9 | name: 'Globals exist', 10 | test() { 11 | const webAPIs = [ 'AbortController', 'AbortSignal', 'Blob', 'ByteLengthQueuingStrategy', 'CSSStyleSheet', 'CountQueuingStrategy', 'CustomElementRegistry', 'CustomEvent', 'DOMException', 'Document', 'DocumentFragment', 'Element', 'Event', 'EventTarget', 'File', 'FormData', 'HTMLDocument', 'HTMLElement', 'HTMLDivElement', 'HTMLHeadElement', 'HTMLHtmlElement', 'HTMLImageElement', 'HTMLStyleElement', 'HTMLTemplateElement', 'HTMLUnknownElement', 'Headers', 'IntersectionObserver', 'Image', 'MediaQueryList', 'MutationObserver', 'Node', 'ReadableByteStreamController', 'ReadableStream', 'ReadableStreamBYOBReader', 'ReadableStreamBYOBRequest', 'ReadableStreamDefaultController', 'ReadableStreamDefaultReader', 'Request', 'Response', 'ShadowRoot', 'StyleSheet', 'TransformStream', 'WritableStream', 'WritableStreamDefaultController', 'WritableStreamDefaultWriter', 'Window', 'cancelAnimationFrame', 'cancelIdleCallback', 'clearTimeout', 'fetch', 'requestAnimationFrame', 'requestIdleCallback', 'setTimeout' ] 12 | 13 | for (const name of webAPIs) { 14 | assert.equal(typeof globalThis[name], 'function') 15 | } 16 | }, 17 | }, 18 | { 19 | name: 'Constructs an Event', 20 | test() { 21 | const e = new Event('test') 22 | 23 | assert.equal(e.type, 'test') 24 | }, 25 | }, 26 | { 27 | name: 'Constructs an EventTarget', 28 | test() { 29 | const t = new EventTarget() 30 | }, 31 | }, 32 | { 33 | name: 'Dispatches an Event on an EventTarget', 34 | test() { 35 | const t = new EventTarget() 36 | 37 | let pass = false 38 | 39 | t.addEventListener('test', (event) => { 40 | pass = true 41 | }) 42 | 43 | const e = new Event('test') 44 | 45 | t.dispatchEvent(e) 46 | 47 | assert.equal(pass, true) 48 | }, 49 | }, 50 | { 51 | name: 'Classes extend as expected', 52 | test() { 53 | assert.equal(HTMLElement.prototype instanceof Element, true) 54 | assert.equal(Element.prototype instanceof Node, true) 55 | assert.equal(Node.prototype instanceof EventTarget, true) 56 | }, 57 | }, 58 | { 59 | name: 'DOM Methods have no effect', 60 | test() { 61 | const element = document.createElement('div') 62 | 63 | assert.equal(element.innerHTML, '') 64 | element.innerHTML = 'frozen' 65 | assert.equal(element.innerHTML, '') 66 | 67 | assert.equal(element.textContent, '') 68 | element.textContent = 'frozen' 69 | assert.equal(element.textContent, '') 70 | }, 71 | }, 72 | { 73 | name: 'globalThis.window === globalThis', 74 | test() { 75 | assert.equal(globalThis.window, globalThis) 76 | }, 77 | }, 78 | { 79 | name: 'Relative Indexing Method (String#at)', 80 | test() { 81 | assert.equal(typeof String.prototype.at, 'function') 82 | assert.equal(String.prototype.at.length, 1) 83 | 84 | const example = 'The quick brown fox jumps over the lazy dog.' 85 | 86 | assert.equal(example.at(2), 'e') 87 | assert.equal(example.at(-2), 'g') 88 | }, 89 | }, 90 | { 91 | name: 'Relative Indexing Method (Array#at)', 92 | test() { 93 | assert.equal(typeof Array.prototype.at, 'function') 94 | assert.equal(Array.prototype.at.length, 1) 95 | 96 | const example = [1, 3, 5, 7, 9] 97 | 98 | assert.equal(example.at(1), 3) 99 | assert.equal(example.at(-1), 9) 100 | }, 101 | }, 102 | { 103 | name: 'Relative Indexing Method (TypedArray#at)', 104 | test() { 105 | assert.equal(typeof Int8Array.prototype.at, 'function') 106 | assert.equal(Int8Array.prototype.at.length, 1) 107 | 108 | const example = new Int8Array([1, 3, 5, 7, 9]) 109 | 110 | assert.equal(example.at(1), 3) 111 | assert.equal(example.at(-1), 9) 112 | }, 113 | }, 114 | { 115 | name: 'Object.hasOwn', 116 | test() { 117 | assert.equal(typeof Object.hasOwn, 'function') 118 | assert.equal(Object.hasOwn.length, 2) 119 | 120 | const example = {} 121 | 122 | assert.equal(Object.hasOwn(example, 'prop'), false) 123 | 124 | example.prop = 'exists' 125 | 126 | assert.equal(Object.hasOwn(example, 'prop'), true) 127 | }, 128 | }, 129 | { 130 | name: 'Promise.any', 131 | test() { 132 | assert.equal(typeof Promise.any, 'function') 133 | assert.equal(Promise.any.length, 1) 134 | 135 | Promise.any([Promise.resolve(42), Promise.reject(-1), Promise.reject(Infinity)]).then( 136 | result => { 137 | assert.equal(result, 42) 138 | } 139 | ) 140 | }, 141 | }, 142 | { 143 | name: 'String#replaceAll', 144 | test() { 145 | assert.equal(typeof String.prototype.replaceAll, 'function') 146 | assert.equal(String.prototype.replaceAll.length, 2) 147 | 148 | const t1 = 'Of all the sorcerers in Harry Potter, Halo is my favorite sorcerer.' 149 | const t2 = t1.replaceAll('sorcerer', 'philosopher') 150 | const t3 = 'Of all the philosophers in Harry Potter, Halo is my favorite philosopher.' 151 | 152 | assert.equal(t2, t3) 153 | }, 154 | }, 155 | ] 156 | }) 157 | -------------------------------------------------------------------------------- /src/lib/CanvasRenderingContext2D.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLCanvasElement } from './HTMLCanvasElement' 2 | import type { OffscreenCanvas } from './OffscreenCanvas' 3 | 4 | import * as _ from './utils' 5 | import { ImageData } from './ImageData' 6 | 7 | export class CanvasRenderingContext2D { 8 | get canvas(): HTMLCanvasElement | OffscreenCanvas | null { 9 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'canvas').canvas 10 | } 11 | 12 | get direction(): 'ltr' | 'rtl' | 'inherit' { 13 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'direction').direction 14 | } 15 | 16 | get fillStyle(): string { 17 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'fillStyle').fillStyle 18 | } 19 | 20 | get filter(): string { 21 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'filter').filter 22 | } 23 | 24 | get globalAlpha(): number { 25 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'globalAlpha').globalAlpha 26 | } 27 | 28 | get globalCompositeOperation(): string { 29 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'globalCompositeOperation').globalCompositeOperation 30 | } 31 | 32 | get font(): string { 33 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'font').font 34 | } 35 | 36 | get imageSmoothingEnabled(): boolean { 37 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'imageSmoothingEnabled').imageSmoothingEnabled 38 | } 39 | 40 | get imageSmoothingQuality(): 'low' | 'medium' | 'high' { 41 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'imageSmoothingQuality').imageSmoothingQuality 42 | } 43 | 44 | get lineCap(): 'butt' | 'round' | 'square' { 45 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'lineCap').lineCap 46 | } 47 | 48 | get lineDashOffset(): number { 49 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'lineDashOffset').lineDashOffset 50 | } 51 | 52 | get lineJoin(): 'bevel' | 'round' | 'miter' { 53 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'lineJoin').lineJoin 54 | } 55 | 56 | get lineWidth(): number { 57 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'lineWidth').lineWidth 58 | } 59 | 60 | get miterLimit(): number { 61 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'miterLimit').miterLimit 62 | } 63 | 64 | get strokeStyle(): string { 65 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'strokeStyle').strokeStyle 66 | } 67 | 68 | get shadowOffsetX(): number { 69 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'shadowOffsetX').shadowOffsetX 70 | } 71 | 72 | get shadowOffsetY(): number { 73 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'shadowOffsetY').shadowOffsetY 74 | } 75 | 76 | get shadowBlur(): number { 77 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'shadowBlur').shadowBlur 78 | } 79 | 80 | get shadowColor(): string { 81 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'shadowColor').shadowColor 82 | } 83 | 84 | get textAlign(): 'left' | 'right' | 'center' | 'start' | 'end' { 85 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'textAlign').textAlign 86 | } 87 | 88 | get textBaseline(): 'top' | 'hanging' | 'middle' | 'alphabetic' | 'ideographic' | 'bottom' { 89 | return _.internalsOf(this, 'CanvasRenderingContext2D', 'textBaseline').textBaseline 90 | } 91 | 92 | arc() {} 93 | arcTo() {} 94 | beginPath() {} 95 | bezierCurveTo() {} 96 | clearRect() {} 97 | clip() {} 98 | closePath() {} 99 | 100 | createImageData(width: number, height: number): void 101 | createImageData(imagedata: ImageData): void 102 | 103 | createImageData(arg0: number | ImageData, arg1?: void | number) { 104 | /** Whether ImageData is provided. */ 105 | const hasData = _.__object_isPrototypeOf(ImageData.prototype, arg0) 106 | 107 | const w = hasData ? (arg0 as ImageData).width : arg0 as number 108 | const h = hasData ? (arg0 as ImageData).height : arg1 as number 109 | const d = hasData ? (arg0 as ImageData).data : new Uint8ClampedArray(w * h * 4) 110 | 111 | return new ImageData(d, w, h) 112 | } 113 | 114 | createLinearGradient() {} 115 | createPattern() {} 116 | createRadialGradient() {} 117 | drawFocusIfNeeded() {} 118 | drawImage() {} 119 | ellipse() {} 120 | fill() {} 121 | fillRect() {} 122 | fillText() {} 123 | getContextAttributes() {} 124 | getImageData() {} 125 | getLineDash() {} 126 | getTransform() {} 127 | isPointInPath() {} 128 | isPointInStroke() {} 129 | lineTo() {} 130 | measureText() {} 131 | moveTo() {} 132 | putImageData() {} 133 | quadraticCurveTo() {} 134 | rect() {} 135 | resetTransform() {} 136 | restore() {} 137 | rotate() {} 138 | save() {} 139 | scale() {} 140 | setLineDash() {} 141 | setTransform() {} 142 | stroke() {} 143 | strokeRect() {} 144 | strokeText() {} 145 | transform() {} 146 | translate() {} 147 | } 148 | 149 | _.assignStringTag(CanvasRenderingContext2D) 150 | 151 | export const __createCanvasRenderingContext2D = (canvas: EventTarget): CanvasRenderingContext2D => { 152 | const renderingContext2D = Object.create(CanvasRenderingContext2D.prototype) as CanvasRenderingContext2D 153 | 154 | _.INTERNALS.set(renderingContext2D, { 155 | canvas, 156 | direction: 'inherit', 157 | fillStyle: '#000', 158 | filter: 'none', 159 | font: '10px sans-serif', 160 | globalAlpha: 0, 161 | globalCompositeOperation: 'source-over', 162 | imageSmoothingEnabled: false, 163 | imageSmoothingQuality: 'high', 164 | lineCap: 'butt', 165 | lineDashOffset: 0.0, 166 | lineJoin: 'miter', 167 | lineWidth: 1.0, 168 | miterLimit: 10.0, 169 | shadowBlur: 0, 170 | shadowColor: '#000', 171 | shadowOffsetX: 0, 172 | shadowOffsetY: 0, 173 | strokeStyle: '#000', 174 | textAlign: 'start', 175 | textBaseline: 'alphabetic', 176 | }) 177 | 178 | return renderingContext2D 179 | } 180 | 181 | /** Returns whether the value is an instance of ImageData. */ 182 | const isImageData = (value: T) => (Object(value).data instanceof Uint8ClampedArray) as T extends ImageData ? true : false 183 | -------------------------------------------------------------------------------- /run/build.js: -------------------------------------------------------------------------------- 1 | import { rollup } from 'rollup' 2 | import { nodeResolve } from '@rollup/plugin-node-resolve' 3 | import { posix as path } from 'node:path' 4 | import { readFile as nodeReadFile, rename, rm, writeFile } from 'node:fs/promises' 5 | import { default as MagicString } from 'magic-string' 6 | import { default as alias } from '@rollup/plugin-alias' 7 | import { default as inject } from '@rollup/plugin-inject' 8 | import { default as typescript } from '@rollup/plugin-typescript' 9 | 10 | const readFileCache = Object.create(null) 11 | 12 | const readFile = (/** @type {string} */ id) => readFileCache[id] || (readFileCache[id] = nodeReadFile(id, 'utf8')) 13 | 14 | const pathToDOMException = path.resolve('src', 'lib', 'DOMException.js') 15 | const pathToEventTargetShim = path.resolve('node_modules', 'event-target-shim', 'index.mjs') 16 | const pathToStructuredClone = path.resolve('node_modules', 'node_modules', '@ungap', 'structured-clone', 'esm', 'index.js') 17 | 18 | const plugins = [ 19 | typescript({ 20 | tsconfig: './tsconfig.json', 21 | }), 22 | alias({ 23 | entries: [ 24 | { find: '@ungap/structured-clone', replacement: pathToStructuredClone }, 25 | { find: 'event-target-shim', replacement: pathToEventTargetShim }, 26 | { find: 'event-target-shim/dist/event-target-shim.js', replacement: pathToEventTargetShim }, 27 | { find: 'event-target-shim/dist/event-target-shim.umd.js', replacement: pathToEventTargetShim }, 28 | { find: 'node-domexception', replacement: pathToDOMException }, 29 | ], 30 | }), 31 | nodeResolve({ 32 | dedupe: [ 33 | 'net', 34 | 'node:net' 35 | ] 36 | }), 37 | inject({ 38 | // import { Promise as P } from 'es6-promise' 39 | // P: [ 'es6-promise', 'Promise' ], 40 | 41 | 'AbortController': [ 'abort-controller/dist/abort-controller.mjs', 'AbortController' ], 42 | 'Blob': [ 'fetch-blob/from.js', 'Blob' ], 43 | 'DOMException': [pathToDOMException, 'DOMException'], 44 | 'Document': [ './Document', 'Document' ], 45 | 'Element': [ './Element', 'Element' ], 46 | 'Event': [ 'event-target-shim', 'Event' ], 47 | 'EventTarget': [ 'event-target-shim', 'EventTarget' ], 48 | 'defineEventAttribute': [ 'event-target-shim', 'defineEventAttribute' ], 49 | 'HTMLElement': ['./Element', 'HTMLElement'], 50 | 'HTMLImageElement': ['./Element', 'HTMLImageElement'], 51 | 'HTMLUnknownElement': ['./Element', 'HTMLUnknownElement'], 52 | 'MediaQueryList': [ './MediaQueryList', 'MediaQueryList' ], 53 | 'Node': [ './Node', 'Node' ], 54 | 'ReadableStream': [ 'web-streams-polyfill/dist/ponyfill.es6.mjs', 'ReadableStream' ], 55 | 'ShadowRoot': [ './Node', 'ShadowRoot' ], 56 | 'Window': [ './Window', 'Window' ], 57 | 'globalThis.ReadableStream': [ 'web-streams-polyfill/dist/ponyfill.es6.mjs', 'ReadableStream' ], 58 | }), 59 | { 60 | async load(id) { 61 | const pathToEsm = id 62 | const pathToMap = `${pathToEsm}.map` 63 | 64 | const code = await readFile(pathToEsm, 'utf8') 65 | 66 | const indexes = [] 67 | 68 | const replacements = [ 69 | // remove unused imports 70 | [ /(^|\n)import\s+[^']+'node:(buffer|fs|path|worker_threads)'/g, `` ], 71 | [ /const \{ stat \} = fs/g, `` ], 72 | 73 | // remove unused polyfill utils 74 | [ /\nif \(\s*typeof Global[\W\w]+?\n\}/g, `` ], 75 | [ /\nif \(\s*typeof window[\W\w]+?\n\}/g, `` ], 76 | [ /\nif \(!globalThis\.ReadableStream\) \{[\W\w]+?\n\}/g, `` ], 77 | [ /\nif \(typeof SymbolPolyfill[\W\w]+?\n\}/g, `` ], 78 | 79 | // remove unused polyfills 80 | [ /\nconst globals = getGlobals\(\);/g, `` ], 81 | [ /\nconst queueMicrotask = [\W\w]+?\n\}\)\(\);/g, ``], 82 | [ /\nconst NativeDOMException =[^;]+;/g, `` ], 83 | [ /\nconst SymbolPolyfill\s*=[^;]+;/g, '\nconst SymbolPolyfill = Symbol;'], 84 | [ /\n(const|let) DOMException[^;]*;/g, `let DOMException$1=DOMException` ], 85 | [ /\nconst DOMException = globalThis.DOMException[\W\w]+?\}\)\(\)/g, `` ], 86 | [ /\nimport DOMException from 'node-domexception'/g, `` ], 87 | 88 | // use shared AbortController methods 89 | [ / new DOMException\$1/g, `new DOMException` ], 90 | [ / from 'net'/g, `from 'node:net'` ], 91 | [ / throw createInvalidStateError/g, `throw new DOMException` ], 92 | [ /= createAbortController/g, `= new AbortController` ], 93 | [ /\nconst queueMicrotask = [\W\w]+?\n\}\)\(\)\;/g, `` ], 94 | 95 | // remove Body.prototype.buffer deprecation notice 96 | [ /\nBody\.prototype\.buffer[^\n]+/g, `` ], 97 | 98 | // remove Body.prototype.data deprecation notice 99 | [ /\n data: \{get: deprecate[\W\w]+?\)\}/g, `` ], 100 | ] 101 | 102 | for (const [replacee, replacer] of replacements) { 103 | replacee.index = 0 104 | 105 | let replaced = null 106 | 107 | while ((replaced = replacee.exec(code)) !== null) { 108 | const leadIndex = replaced.index 109 | const tailIndex = replaced.index + replaced[0].length 110 | 111 | indexes.unshift([ leadIndex, tailIndex, replacer ]) 112 | } 113 | } 114 | 115 | if (indexes.length) { 116 | const magicString = new MagicString(code) 117 | 118 | indexes.sort( 119 | ([leadOfA], [leadOfB]) => leadOfA - leadOfB 120 | ) 121 | 122 | for (const [leadIndex, tailindex, replacer] of indexes) { 123 | magicString.overwrite(leadIndex, tailindex, replacer) 124 | } 125 | 126 | const magicMap = magicString.generateMap({ source: pathToEsm, file: pathToMap, includeContent: true }) 127 | 128 | const modifiedEsm = magicString.toString() 129 | const modifiedMap = magicMap.toString() 130 | 131 | return { code: modifiedEsm, map: modifiedMap } 132 | } 133 | }, 134 | }, 135 | ] 136 | 137 | async function build() { 138 | const configs = [ 139 | { 140 | inputOptions: { 141 | input: 'src/polyfill.ts', 142 | plugins: plugins, 143 | onwarn(warning, warn) { 144 | if (warning.code !== 'UNRESOLVED_IMPORT') warn(warning) 145 | }, 146 | }, 147 | outputOptions: { 148 | inlineDynamicImports: true, 149 | file: 'mod.js', 150 | format: 'esm', 151 | sourcemap: true, 152 | }, 153 | }, 154 | ] 155 | 156 | for (const config of configs) { 157 | const bundle = await rollup(config.inputOptions) 158 | 159 | // or write the bundle to disk 160 | await bundle.write(config.outputOptions) 161 | 162 | // closes the bundle 163 | await bundle.close() 164 | 165 | // delete the lib directory 166 | await rm('lib', { force: true, recursive: true }) 167 | await rm('exclusions.d.ts', { force: true, recursive: true }) 168 | await rm('exclusions.d.ts.map', { force: true, recursive: true }) 169 | await rm('inheritence.d.ts', { force: true, recursive: true }) 170 | await rm('inheritence.d.ts.map', { force: true, recursive: true }) 171 | await rm('polyfill.d.ts.map', { force: true, recursive: true }) 172 | await rm('polyfill.js.map', { force: true, recursive: true }) 173 | await rm('polyfill.js', { force: true, recursive: true }) 174 | await rm('ponyfill.d.ts', { force: true, recursive: true }) 175 | await rm('ponyfill.d.ts.map', { force: true, recursive: true }) 176 | await rm('ponyfill.js.map', { force: true, recursive: true }) 177 | await rm('ponyfill.js', { force: true, recursive: true }) 178 | 179 | await rename('polyfill.d.ts', 'mod.d.ts') 180 | 181 | const modDTS = await readFile('./mod.d.ts') 182 | 183 | writeFile('mod.d.ts', modDTS.replace('\n//# sourceMappingURL=polyfill.d.ts.map', '').replace('ponyfill.js', 'mod.js')) 184 | writeFile('apply.js', `import { polyfill } from './mod.js'\n\nexport * from './mod.js'\n\npolyfill(globalThis)\n`) 185 | } 186 | } 187 | 188 | build() 189 | -------------------------------------------------------------------------------- /src/polyfill.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbortController, 3 | AbortSignal, 4 | Blob, 5 | ByteLengthQueuingStrategy, 6 | CanvasRenderingContext2D, 7 | CharacterData, 8 | Comment, 9 | CountQueuingStrategy, 10 | CSSStyleSheet, 11 | CustomElementRegistry, 12 | CustomEvent, 13 | DOMException, 14 | Document, 15 | DocumentFragment, 16 | Element, 17 | Event, 18 | EventTarget, 19 | File, 20 | FormData, 21 | HTMLDocument, 22 | HTMLElement, 23 | HTMLBodyElement, 24 | HTMLCanvasElement, 25 | HTMLDivElement, 26 | HTMLHeadElement, 27 | HTMLHtmlElement, 28 | HTMLImageElement, 29 | HTMLSpanElement, 30 | HTMLStyleElement, 31 | HTMLTemplateElement, 32 | HTMLUnknownElement, 33 | Headers, 34 | IntersectionObserver, 35 | Image, 36 | ImageData, 37 | MediaQueryList, 38 | MutationObserver, 39 | Node, 40 | NodeFilter, 41 | NodeIterator, 42 | OffscreenCanvas, 43 | ReadableByteStreamController, 44 | ReadableStream, 45 | ReadableStreamBYOBReader, 46 | ReadableStreamBYOBRequest, 47 | ReadableStreamDefaultController, 48 | ReadableStreamDefaultReader, 49 | Request, 50 | ResizeObserver, 51 | Response, 52 | ShadowRoot, 53 | Storage, 54 | StyleSheet, 55 | Text, 56 | TransformStream, 57 | TreeWalker, 58 | URLPattern, 59 | WritableStream, 60 | WritableStreamDefaultController, 61 | WritableStreamDefaultWriter, 62 | Window, 63 | 64 | alert, 65 | atob, 66 | btoa, 67 | cancelAnimationFrame, 68 | cancelIdleCallback, 69 | clearTimeout, 70 | fetch, 71 | requestAnimationFrame, 72 | requestIdleCallback, 73 | setTimeout, 74 | structuredClone, 75 | 76 | initCustomElementRegistry, 77 | initDocument, 78 | initMediaQueryList, 79 | initObject, 80 | initPromise, 81 | initRelativeIndexingMethod, 82 | initStorage, 83 | initString, 84 | initWindow, 85 | } from './ponyfill' 86 | 87 | import * as _ from './lib/utils' 88 | 89 | import { exclusions } from './exclusions' 90 | import { inheritence } from './inheritence' 91 | 92 | export { 93 | AbortController, 94 | AbortSignal, 95 | Blob, 96 | ByteLengthQueuingStrategy, 97 | CanvasRenderingContext2D, 98 | CharacterData, 99 | Comment, 100 | CountQueuingStrategy, 101 | CSSStyleSheet, 102 | CustomElementRegistry, 103 | CustomEvent, 104 | DOMException, 105 | Document, 106 | DocumentFragment, 107 | Element, 108 | Event, 109 | EventTarget, 110 | File, 111 | FormData, 112 | HTMLDocument, 113 | HTMLElement, 114 | HTMLBodyElement, 115 | HTMLCanvasElement, 116 | HTMLDivElement, 117 | HTMLHeadElement, 118 | HTMLHtmlElement, 119 | HTMLImageElement, 120 | HTMLSpanElement, 121 | HTMLStyleElement, 122 | HTMLTemplateElement, 123 | HTMLUnknownElement, 124 | Headers, 125 | IntersectionObserver, 126 | Image, 127 | ImageData, 128 | MediaQueryList, 129 | MutationObserver, 130 | Node, 131 | NodeFilter, 132 | NodeIterator, 133 | OffscreenCanvas, 134 | ReadableByteStreamController, 135 | ReadableStream, 136 | ReadableStreamBYOBReader, 137 | ReadableStreamBYOBRequest, 138 | ReadableStreamDefaultController, 139 | ReadableStreamDefaultReader, 140 | Request, 141 | ResizeObserver, 142 | Response, 143 | ShadowRoot, 144 | StyleSheet, 145 | Text, 146 | TransformStream, 147 | TreeWalker, 148 | URLPattern, 149 | WritableStream, 150 | WritableStreamDefaultController, 151 | WritableStreamDefaultWriter, 152 | Window, 153 | 154 | alert, 155 | atob, 156 | btoa, 157 | cancelAnimationFrame, 158 | cancelIdleCallback, 159 | clearTimeout, 160 | fetch, 161 | requestAnimationFrame, 162 | requestIdleCallback, 163 | setTimeout, 164 | structuredClone, 165 | } from './ponyfill.js' 166 | 167 | export { pathToPosix } from './lib/utils' 168 | 169 | export const polyfill = (target: any, options?: PolyfillOptions) => { 170 | target = Object(target) 171 | 172 | let pseudo = _.assign(_.create(null) as any, { 173 | AbortController, 174 | AbortSignal, 175 | Blob, 176 | ByteLengthQueuingStrategy, 177 | CanvasRenderingContext2D, 178 | CharacterData, 179 | Comment, 180 | CountQueuingStrategy, 181 | CSSStyleSheet, 182 | CustomElementRegistry, 183 | CustomEvent, 184 | Document, 185 | DocumentFragment, 186 | DOMException, 187 | Element, 188 | Event, 189 | EventTarget, 190 | File, 191 | FormData, 192 | HTMLDocument, 193 | HTMLElement, 194 | HTMLBodyElement, 195 | HTMLCanvasElement, 196 | HTMLDivElement, 197 | HTMLHeadElement, 198 | HTMLHtmlElement, 199 | HTMLImageElement, 200 | HTMLSpanElement, 201 | HTMLStyleElement, 202 | HTMLTemplateElement, 203 | HTMLUnknownElement, 204 | Headers, 205 | IntersectionObserver, 206 | Image, 207 | ImageData, 208 | MediaQueryList, 209 | MutationObserver, 210 | Node, 211 | NodeFilter, 212 | NodeIterator, 213 | OffscreenCanvas, 214 | ReadableByteStreamController, 215 | ReadableStream, 216 | ReadableStreamBYOBReader, 217 | ReadableStreamBYOBRequest, 218 | ReadableStreamDefaultController, 219 | ReadableStreamDefaultReader, 220 | Request, 221 | ResizeObserver, 222 | Response, 223 | ShadowRoot, 224 | Storage, 225 | StyleSheet, 226 | Text, 227 | TransformStream, 228 | TreeWalker, 229 | URLPattern, 230 | WritableStream, 231 | WritableStreamDefaultController, 232 | WritableStreamDefaultWriter, 233 | Window, 234 | 235 | alert, 236 | atob, 237 | btoa, 238 | cancelAnimationFrame, 239 | cancelIdleCallback, 240 | clearTimeout, 241 | fetch, 242 | requestAnimationFrame, 243 | requestIdleCallback, 244 | setTimeout, 245 | structuredClone, 246 | }) 247 | 248 | _.INTERNALS.set(target, pseudo) 249 | 250 | pseudo = new Proxy(pseudo, { 251 | get(pseudo, name) { 252 | return target[name] || globalThis[name] || pseudo[name] 253 | }, 254 | }) 255 | 256 | // initialize exclude options 257 | const excludeOptions = new Set( 258 | typeof Object(options).exclude === 'string' 259 | ? String(Object(options).exclude).trim().split(/\s+/) 260 | : Array.isArray(Object(options).exclude) 261 | ? Object(options).exclude.reduce( 262 | (array: string[], entry: unknown) => array.splice(array.length, 0, ...(typeof entry === 'string' ? entry.trim().split(/\s+/) : [])) && array, 263 | [] 264 | ) 265 | : [] 266 | ) as Set 267 | 268 | // expand exclude options using exclusion shorthands 269 | for (const excludeOption of excludeOptions) { 270 | if (excludeOption in exclusions) { 271 | for (const exclusion of exclusions[excludeOption as keyof typeof exclusions]) { 272 | excludeOptions.add(exclusion) 273 | } 274 | } 275 | } 276 | 277 | // ensure WebAPIs correctly inherit other WebAPIs 278 | for (const name in pseudo) { 279 | // skip WebAPIs that are excluded 280 | if (excludeOptions.has(name)) continue 281 | 282 | // skip WebAPIs that do not extend other WebAPIs 283 | if (!Object.hasOwnProperty.call(inheritence, name)) continue 284 | 285 | const Class = pseudo[name] 286 | const Super = pseudo[inheritence[name as keyof typeof inheritence]] 287 | 288 | // skip WebAPIs that are not available 289 | if (!Class || !Super) continue 290 | 291 | // skip WebAPIs that are already inherited correctly 292 | if (Object.getPrototypeOf(Class.prototype) === Super.prototype) continue 293 | 294 | // define WebAPIs inheritence 295 | Object.setPrototypeOf(Class.prototype, Super.prototype) 296 | } 297 | 298 | // apply each WebAPI 299 | for (const name in pseudo) { 300 | // skip WebAPIs that are excluded 301 | if (excludeOptions.has(name)) continue 302 | 303 | // skip WebAPIs that are built-in 304 | if (Object.hasOwnProperty.call(target, name)) continue 305 | 306 | // define WebAPIs on the target 307 | Object.defineProperty(target, name, { configurable: true, enumerable: true, writable: true, value: pseudo[name as keyof typeof pseudo] }) 308 | } 309 | 310 | initDocument(target, excludeOptions, pseudo) 311 | initCustomElementRegistry(target, excludeOptions, pseudo) 312 | initMediaQueryList(target, excludeOptions, pseudo) 313 | 314 | initObject(target, excludeOptions) 315 | initPromise(target, excludeOptions) 316 | initRelativeIndexingMethod(target, excludeOptions) 317 | initStorage(target, excludeOptions) 318 | initString(target, excludeOptions) 319 | initWindow(target, excludeOptions) 320 | 321 | return target 322 | } 323 | 324 | polyfill.internals = (target: any, name: string) => { 325 | const pseudo = _.INTERNALS.get(target) 326 | 327 | const init = { 328 | CustomElementRegistry: initCustomElementRegistry, 329 | Document: initDocument, 330 | MediaQueryList: initMediaQueryList, 331 | Object: initObject, 332 | Promise: initPromise, 333 | RelativeIndexingMethod: initRelativeIndexingMethod, 334 | Storage: initStorage, 335 | String: initString, 336 | Window: initWindow, 337 | } 338 | 339 | init[name as keyof typeof init](target, new Set(), pseudo) 340 | 341 | return target 342 | } 343 | 344 | declare const globalThis: any 345 | 346 | interface PolyfillOptions { 347 | exclude?: string | string[] 348 | override?: Record 349 | } 350 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebAPI 2 | 3 | **WebAPI** lets you use Web APIs in Node v12, v14, and v16. 4 | 5 | ```shell 6 | npm install @astropub/webapi 7 | ``` 8 | 9 | ```js 10 | import { polyfill } from '@astropub/webapi' 11 | 12 | polyfill(globalThis) 13 | 14 | const t = new EventTarget() 15 | const e = new CustomEvent('hello') 16 | 17 | t.addEventListener('hello', console.log) 18 | 19 | t.dispatchEvent(e) // logs `e` event from `t` 20 | ``` 21 | 22 | These APIs are combined from popular open source projects and configured to share implementation details. This allows their behavior to match browser expectations as well as reduce their combined memory footprint. 23 | 24 | ## Features 25 | 26 | - [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) 27 | - [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) 28 | - [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) 29 | - [ByteLengthQueuingStrategy](https://developer.mozilla.org/en-US/docs/Web/API/ByteLengthQueuingStrategy) 30 | - [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) 31 | - [CSSStyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet) 32 | - [CountQueuingStrategy](https://developer.mozilla.org/en-US/docs/Web/API/CountQueuingStrategy) 33 | - [CustomElementRegistry](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry) 34 | - [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) 35 | - [DOMException](https://developer.mozilla.org/en-US/docs/Web/API/DOMException) 36 | - [Document](https://developer.mozilla.org/en-US/docs/Web/API/Document) 37 | - [DocumentFragment](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) 38 | - [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) 39 | - [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) 40 | - [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) 41 | - [File](https://developer.mozilla.org/en-US/docs/Web/API/File) 42 | - [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) 43 | - [HTMLDocument](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDocument) 44 | - [HTMLElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) 45 | - [HTMLBodyElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLBodyElement) 46 | - [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) 47 | - [HTMLDivElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement) 48 | - [HTMLHeadElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLHeadElement) 49 | - [HTMLHtmlElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLHtmlElement) 50 | - [HTMLImageElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement) 51 | - [HTMLSpanElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSpanElement) 52 | - [HTMLStyleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) 53 | - [HTMLTemplateElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLTemplateElement) 54 | - [HTMLUnknownElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLUnknownElement) 55 | - [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) 56 | - [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) 57 | - [Image](https://developer.mozilla.org/en-US/docs/Web/API/Image) 58 | - [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) 59 | - [MediaQueryList](https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList) 60 | - [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) 61 | - [Node](https://developer.mozilla.org/en-US/docs/Web/API/Node) 62 | - [NodeIterator](https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator) 63 | - [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) 64 | - [ReadableByteStreamController](https://developer.mozilla.org/en-US/docs/Web/API/ReadableByteStreamController) 65 | - [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) 66 | - [ReadableStreamBYOBReader](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader) 67 | - [ReadableStreamBYOBRequest](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBRequest) 68 | - [ReadableStreamDefaultController](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultController) 69 | - [ReadableStreamDefaultReader](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader) 70 | - [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) 71 | - [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) 72 | - [ShadowRoot](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) 73 | - [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) 74 | - [StyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/StyleSheet) 75 | - [TransformStream](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream) 76 | - [TreeWalker](https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker) 77 | - [WritableStream](https://developer.mozilla.org/en-US/docs/Web/API/WritableStream) 78 | - [WritableStreamDefaultController](https://developer.mozilla.org/en-US/docs/Web/API/WritableStreamDefaultController) 79 | - [WritableStreamDefaultWriter](https://developer.mozilla.org/en-US/docs/Web/API/WritableStreamDefaultWriter) 80 | - [Window](https://developer.mozilla.org/en-US/docs/Web/API/Window) 81 | - [atob](https://developer.mozilla.org/en-US/docs/Web/API/atob) 82 | - [btoa](https://developer.mozilla.org/en-US/docs/Web/API/btoa) 83 | - [cancelAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/cancelAnimationFrame) 84 | - [cancelIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/cancelIdleCallback) 85 | - [clearTimeout](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout) 86 | - [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) 87 | - [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/localStorage) 88 | - [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/requestAnimationFrame) 89 | - [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/requestIdleCallback) 90 | - [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) 91 | - [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) 92 | - [Object.hasOwn](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn) 93 | - [Promise.any](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any) 94 | - [Array.prototype.at](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at) 95 | - [String.prototype.at](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at) 96 | - [String.prototype.replaceAll](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll) 97 | - [TypedArray.prototype.at](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/at) 98 | - [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) 99 | 100 | ## Usage 101 | 102 | You can use WebAPIs as individual exports. 103 | 104 | ```js 105 | import { AbortController, Blob, Event, EventTarget, File, fetch, Response } from '@astropub/webapi' 106 | ``` 107 | 108 | You can apply WebAPIs to an object, like `globalThis`. 109 | 110 | ```js 111 | import { polyfill } from '@astropub/webapi' 112 | 113 | polyfill(globalThis) 114 | ``` 115 | 116 | ## Polyfill Options 117 | 118 | The `exclude` option receives a list of WebAPIs to exclude from polyfilling. 119 | 120 | ```js 121 | polyfill(globalThis, { 122 | // disables polyfills for setTimeout clearTimeout 123 | exclude: 'setTimeout clearTimeout', 124 | }) 125 | ``` 126 | 127 | The `exclude` option accepts shorthands to exclude multiple polyfills. These shorthands end with the plus sign (`+`). 128 | 129 | ```js 130 | polyfill(globalThis, { 131 | // disables polyfills for setTimeout clearTimeout 132 | exclude: 'Timeout+', 133 | }) 134 | ``` 135 | 136 | ```js 137 | polyfill(globalThis, { 138 | // disables polyfills for Node, Window, Document, HTMLElement, etc. 139 | exclude: 'Node+', 140 | }) 141 | ``` 142 | 143 | ```js 144 | polyfill(globalThis, { 145 | // disables polyfills for Event, EventTarget, Node, Window, Document, HTMLElement, etc. 146 | exclude: 'Event+', 147 | }) 148 | ``` 149 | 150 | | Shorthand | Excludes | 151 | |:-------------- |:-------- | 152 | | `Blob+` | `Blob`, `File` | 153 | | `Document+` | `Document`, `HTMLDocument` | 154 | | `Element+` | `Element`, and exclusions from `HTMLElement+` | 155 | | `Event+` | `Event`, `CustomEvent`, `EventTarget`, `AbortSignal`, `MediaQueryList`, `Window`, and exclusions from `Node+` | 156 | | `EventTarget+` | `Event`, `CustomEvent`, `EventTarget`, `AbortSignal`, `MediaQueryList`, `Window`, and exclusions from `Node+` | 157 | | `HTMLElement+` | `CustomElementsRegistry`, `HTMLElement`, `HTMLBodyElement`, `HTMLCanvasElement`, `HTMLDivElement`, `HTMLHeadElement`, `HTMLHtmlElement`, `HTMLImageElement`, `HTMLStyleElement`, `HTMLTemplateElement`, `HTMLUnknownElement`, `Image` | 158 | | `Node+` | `Node`, `DocumentFragment`, `ShadowRoot`, `Document`, `HTMLDocument`, and exclusions from `Element+` | 159 | | `StyleSheet+` | `StyleSheet`, `CSSStyleSheet` | 160 | 161 | --- 162 | 163 | 164 | 165 | ## License 166 | 167 | Code original to this project is licensed under the CC0-1.0 License. 168 | 169 | Code from [abort-controller](https://www.npmjs.com/package/abort-controller) is licensed under the MIT License (MIT), Copyright Toru Nagashima. 170 | 171 | Code from [event-target-shim](https://www.npmjs.com/package/event-target-shim) is licensed under the MIT License (MIT), Copyright Toru Nagashima. 172 | 173 | Code from [fetch-blob](https://www.npmjs.com/package/fetch-blob) is licensed under the MIT License (MIT), Copyright Jimmy Wärting. 174 | 175 | Code from [formdata-polyfill](https://www.npmjs.com/package/formdata-polyfill) is licensed under the MIT License (MIT), Copyright Jimmy Wärting. 176 | 177 | Code from [node-fetch](https://www.npmjs.com/package/node-fetch) is licensed under the MIT License (MIT), Copyright Node Fetch Team. 178 | 179 | Code from [web-streams-polyfill](https://www.npmjs.com/package/web-streams-polyfill) is licensed under the MIT License (MIT), Copyright Mattias Buelens and Diwank Singh Tomer. 180 | --------------------------------------------------------------------------------