diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..9210db8 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "privatePackages": false, + "ignore": [] +} diff --git a/.changeset/dom-mutations-and-events.md b/.changeset/dom-mutations-and-events.md new file mode 100644 index 0000000..1e5adac --- /dev/null +++ b/.changeset/dom-mutations-and-events.md @@ -0,0 +1,5 @@ +--- +'@clack/ui': minor +--- + +Add DOM tree navigation and mutation APIs, text and attribute updates, and native event dispatch with bubbling. Support cancellation, propagation control, listener updates during dispatch, and nested events for framework integrations. diff --git a/package.json b/package.json index 7b83d8e..19d36f7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ }, "devDependencies": { "@bomb.sh/tools": "^0.8.0", + "@changesets/cli": "^2.30.0", "@codspeed/vitest-plugin": "^6.1.0", "@termless/core": "^0.10.2", "@termless/test": "^0.10.2", diff --git a/packages/ui/src/dom.test.ts b/packages/ui/src/dom.test.ts new file mode 100644 index 0000000..9711f8d --- /dev/null +++ b/packages/ui/src/dom.test.ts @@ -0,0 +1,1317 @@ +import { describe, expect, it, vi } from 'vitest'; +import { Document, Event, EventTarget, type Node, type Text } from './dom.ts'; + +describe('DOM connectivity', () => { + it('preserves a subtree through connection, disconnection, and reconnection', () => { + const notify = vi.fn(); + const document = new Document(notify); + const root = document.documentElement; + const branch = document.createElement('div'); + const span = document.createElement('span'); + const text = document.createTextNode('initial'); + + expect(root.isConnected).toBe(true); + expect(branch.isConnected).toBe(false); + expect(span.isConnected).toBe(false); + expect(text.isConnected).toBe(false); + expect(branch.parentNode).toBeNull(); + + branch.appendChild(span); + span.appendChild(text); + text.data = 'prepared'; + branch.setAttribute('data-state', 'prepared'); + + expect(span.parentNode).toBe(branch); + expect(text.parentNode).toBe(span); + expect(branch.isConnected).toBe(false); + expect(span.isConnected).toBe(false); + expect(text.isConnected).toBe(false); + expect(text.data).toBe('prepared'); + expect(branch.attributes[0]).toMatchObject({ name: 'data-state', value: 'prepared' }); + expect(notify).not.toHaveBeenCalled(); + + root.appendChild(branch); + + expect(root.childNodes).toHaveLength(1); + expect(root.childNodes[0]).toBe(branch); + expect(branch.parentNode).toBe(root); + expect(branch.isConnected).toBe(true); + expect(span.isConnected).toBe(true); + expect(text.isConnected).toBe(true); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + text.data = 'connected'; + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + branch.setAttribute('data-state', 'connected'); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + const extra = document.createTextNode('!'); + span.appendChild(extra); + expect(extra.isConnected).toBe(true); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + root.removeChild(branch); + + expect(root.childNodes).toHaveLength(0); + expect(branch.parentNode).toBeNull(); + expect(span.parentNode).toBe(branch); + expect(text.parentNode).toBe(span); + expect(branch.isConnected).toBe(false); + expect(span.isConnected).toBe(false); + expect(text.isConnected).toBe(false); + expect(extra.isConnected).toBe(false); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + text.data = 'detached'; + branch.removeAttribute('data-state'); + span.removeChild(extra); + + expect(extra.parentNode).toBeNull(); + expect(span.childNodes).toHaveLength(1); + expect(span.childNodes[0]).toBe(text); + expect(text.data).toBe('detached'); + expect(branch.attributes).toHaveLength(0); + expect(notify).not.toHaveBeenCalled(); + + root.appendChild(branch); + + expect(root.childNodes).toHaveLength(1); + expect(root.childNodes[0]).toBe(branch); + expect(branch.parentNode).toBe(root); + expect(branch.isConnected).toBe(true); + expect(span.isConnected).toBe(true); + expect(text.isConnected).toBe(true); + expect(extra.isConnected).toBe(false); + expect(text.data).toBe('detached'); + expect(branch.attributes).toHaveLength(0); + expect(notify).toHaveBeenCalled(); + }); + + it('moves a subtree between detached parents without notifying', () => { + const notify = vi.fn(); + const document = new Document(notify); + const source = document.createElement('div'); + const destination = document.createElement('div'); + const branch = document.createElement('span'); + const text = document.createTextNode('before'); + branch.appendChild(text); + source.appendChild(branch); + notify.mockClear(); + + destination.appendChild(branch); + + expect(source.childNodes).toHaveLength(0); + expect(destination.childNodes).toHaveLength(1); + expect(destination.childNodes[0]).toBe(branch); + expect(branch.parentNode).toBe(destination); + expect(branch.childNodes).toHaveLength(1); + expect(branch.childNodes[0]).toBe(text); + expect(text.parentNode).toBe(branch); + expect(source.isConnected).toBe(false); + expect(destination.isConnected).toBe(false); + expect(branch.isConnected).toBe(false); + expect(text.isConnected).toBe(false); + expect(notify).not.toHaveBeenCalled(); + + text.data = 'after'; + expect(notify).not.toHaveBeenCalled(); + + branch.setAttribute('data-state', 'after'); + expect(notify).not.toHaveBeenCalled(); + }); + + it('connects a detached subtree and starts notifying when moved into a connected parent', () => { + const notify = vi.fn(); + const document = new Document(notify); + const source = document.createElement('div'); + const destination = document.createElement('div'); + const branch = document.createElement('span'); + const text = document.createTextNode('before'); + branch.appendChild(text); + source.appendChild(branch); + document.documentElement.appendChild(destination); + notify.mockClear(); + + destination.appendChild(branch); + + expect(source.childNodes).toHaveLength(0); + expect(destination.childNodes).toHaveLength(1); + expect(destination.childNodes[0]).toBe(branch); + expect(branch.parentNode).toBe(destination); + expect(branch.childNodes).toHaveLength(1); + expect(branch.childNodes[0]).toBe(text); + expect(text.parentNode).toBe(branch); + expect(source.isConnected).toBe(false); + expect(destination.isConnected).toBe(true); + expect(branch.isConnected).toBe(true); + expect(text.isConnected).toBe(true); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + text.data = 'after'; + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + branch.setAttribute('data-state', 'after'); + expect(notify).toHaveBeenCalled(); + }); + + it('notifies removal and stops notifying when a subtree moves into a detached parent', () => { + const notify = vi.fn(); + const document = new Document(notify); + const source = document.createElement('div'); + const destination = document.createElement('div'); + const branch = document.createElement('span'); + const text = document.createTextNode('before'); + branch.appendChild(text); + source.appendChild(branch); + document.documentElement.appendChild(source); + notify.mockClear(); + + destination.appendChild(branch); + + expect(source.childNodes).toHaveLength(0); + expect(destination.childNodes).toHaveLength(1); + expect(destination.childNodes[0]).toBe(branch); + expect(branch.parentNode).toBe(destination); + expect(branch.childNodes).toHaveLength(1); + expect(branch.childNodes[0]).toBe(text); + expect(text.parentNode).toBe(branch); + expect(source.isConnected).toBe(true); + expect(destination.isConnected).toBe(false); + expect(branch.isConnected).toBe(false); + expect(text.isConnected).toBe(false); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + text.data = 'after'; + expect(notify).not.toHaveBeenCalled(); + + branch.setAttribute('data-state', 'after'); + expect(notify).not.toHaveBeenCalled(); + }); + + it('keeps a subtree connected and notifying when moved between connected parents', () => { + const notify = vi.fn(); + const document = new Document(notify); + const source = document.createElement('div'); + const destination = document.createElement('div'); + const branch = document.createElement('span'); + const text = document.createTextNode('before'); + branch.appendChild(text); + source.appendChild(branch); + document.documentElement.appendChild(source); + document.documentElement.appendChild(destination); + notify.mockClear(); + + destination.appendChild(branch); + + expect(source.childNodes).toHaveLength(0); + expect(destination.childNodes).toHaveLength(1); + expect(destination.childNodes[0]).toBe(branch); + expect(branch.parentNode).toBe(destination); + expect(branch.childNodes).toHaveLength(1); + expect(branch.childNodes[0]).toBe(text); + expect(text.parentNode).toBe(branch); + expect(source.isConnected).toBe(true); + expect(destination.isConnected).toBe(true); + expect(branch.isConnected).toBe(true); + expect(text.isConnected).toBe(true); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + text.data = 'after'; + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + branch.setAttribute('data-state', 'after'); + expect(notify).toHaveBeenCalled(); + }); + + it('moves an existing child to the end without duplicating it', () => { + const notify = vi.fn(); + const document = new Document(notify); + const root = document.documentElement; + const first = document.createTextNode('first'); + const second = document.createTextNode('second'); + root.appendChild(first); + root.appendChild(second); + notify.mockClear(); + + root.appendChild(first); + + expect(root.childNodes).toHaveLength(2); + expect(root.childNodes[0]).toBe(second); + expect(root.childNodes[1]).toBe(first); + expect(first.parentNode).toBe(root); + expect(second.parentNode).toBe(root); + expect(first.isConnected).toBe(true); + expect(second.isConnected).toBe(true); + expect(notify).toHaveBeenCalled(); + }); + + it('rejects cycles and removal from the wrong parent without changing the tree', () => { + const notify = vi.fn(); + const document = new Document(notify); + const root = document.documentElement; + const parent = document.createElement('div'); + const other = document.createElement('div'); + const child = document.createElement('span'); + parent.appendChild(child); + root.appendChild(parent); + root.appendChild(other); + notify.mockClear(); + + for (const [mutate, name] of [ + [() => child.appendChild(parent), 'HierarchyRequestError'], + [() => parent.appendChild(parent), 'HierarchyRequestError'], + [() => other.removeChild(child), 'NotFoundError'], + ] as const) { + expect(mutate).toThrow(expect.objectContaining({ constructor: DOMException, name })); + + expect(root.childNodes).toHaveLength(2); + expect(root.childNodes[0]).toBe(parent); + expect(root.childNodes[1]).toBe(other); + expect(parent.parentNode).toBe(root); + expect(other.parentNode).toBe(root); + expect(parent.childNodes).toHaveLength(1); + expect(parent.childNodes[0]).toBe(child); + expect(child.parentNode).toBe(parent); + expect(child.childNodes).toHaveLength(0); + expect(other.childNodes).toHaveLength(0); + expect(notify).not.toHaveBeenCalled(); + } + }); +}); + +describe('createElementNS', () => { + it('creates HTML elements and text with the metadata used to match existing nodes', () => { + const document = new Document(vi.fn()); + const namespace = 'http://www.w3.org/1999/xhtml'; + const div = document.createElementNS(namespace, 'div'); + const span = document.createElement('span'); + const text = document.createTextNode('hello'); + + expect(document.documentElement.ownerDocument).toBe(document); + expect(document.documentElement.namespaceURI).toBe(namespace); + expect(document.documentElement.nodeType).toBe(1); + expect(div.ownerDocument).toBe(document); + expect(div.namespaceURI).toBe(namespace); + expect(div.localName).toBe('div'); + expect(div.nodeName).toBe('DIV'); + expect(div.nodeType).toBe(1); + expect(div.parentNode).toBeNull(); + expect(div.isConnected).toBe(false); + expect(span.namespaceURI).toBe(namespace); + expect(span.localName).toBe('span'); + expect(text.ownerDocument).toBe(document); + expect(text.nodeName).toBe('#text'); + expect(text.nodeType).toBe(3); + expect(text.firstChild).toBeNull(); + expect(text.nextSibling).toBeNull(); + }); +}); + +describe('createTextNode / data', () => { + it('coerces numeric text to strings and updates or clears it without replacing the node', () => { + const notify = vi.fn(); + const document = new Document(notify); + // Preact passes numbers directly from JavaScript, despite the DOM's string signature. + const text: Text = Reflect.apply(document.createTextNode, document, [42]); + + expect(text.data).toBe('42'); + expect(notify).not.toHaveBeenCalled(); + document.documentElement.appendChild(text); + notify.mockClear(); + + Reflect.set(text, 'data', 43n); + + expect(text.data).toBe('43'); + expect(document.documentElement.childNodes[0]).toBe(text); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + text.data = '43'; + expect(notify).not.toHaveBeenCalled(); + + text.data = ''; + expect(text.data).toBe(''); + expect(document.documentElement.childNodes).toHaveLength(1); + expect(document.documentElement.childNodes[0]).toBe(text); + expect(notify).toHaveBeenCalled(); + }); +}); + +describe('insertBefore', () => { + it('inserts before an anchor and appends with a null anchor', () => { + const notify = vi.fn(); + const document = new Document(notify); + const parent = document.createElement('div'); + const first = document.createTextNode('first'); + const middle = document.createElement('span'); + const last = document.createTextNode('last'); + + expect(parent.firstChild).toBeNull(); + expect(parent.insertBefore(last, null)).toBe(last); + expect(parent.insertBefore(first, last)).toBe(first); + expect(parent.insertBefore(middle, last)).toBe(middle); + + expect(parent.childNodes).toHaveLength(3); + expect(parent.childNodes[0]).toBe(first); + expect(parent.childNodes[1]).toBe(middle); + expect(parent.childNodes[2]).toBe(last); + expect(parent.firstChild).toBe(first); + expect(first.nextSibling).toBe(middle); + expect(middle.nextSibling).toBe(last); + expect(last.nextSibling).toBeNull(); + expect(first.parentNode).toBe(parent); + expect(middle.parentNode).toBe(parent); + expect(last.parentNode).toBe(parent); + expect(parent.children).toHaveLength(1); + expect(parent.children[0]).toBe(middle); + expect(notify).not.toHaveBeenCalled(); + }); + + it('reorders existing children in both directions without duplicating them', () => { + const notify = vi.fn(); + const document = new Document(notify); + const parent = document.documentElement; + const first = document.createElement('span'); + const middle = document.createTextNode('middle'); + const last = document.createElement('span'); + parent.appendChild(first); + parent.appendChild(middle); + parent.appendChild(last); + notify.mockClear(); + + parent.insertBefore(last, first); + + expect(parent.childNodes).toHaveLength(3); + expect(parent.firstChild).toBe(last); + expect(last.nextSibling).toBe(first); + expect(first.nextSibling).toBe(middle); + expect(middle.nextSibling).toBeNull(); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + parent.insertBefore(last, middle); + + expect(parent.childNodes).toHaveLength(3); + expect(parent.firstChild).toBe(first); + expect(first.nextSibling).toBe(last); + expect(last.nextSibling).toBe(middle); + expect(middle.nextSibling).toBeNull(); + expect(notify).toHaveBeenCalled(); + + parent.insertBefore(last, null); + parent.insertBefore(first, first); + + expect(parent.childNodes).toHaveLength(3); + expect(parent.childNodes[0]).toBe(first); + expect(parent.childNodes[1]).toBe(middle); + expect(parent.childNodes[2]).toBe(last); + expect(parent.firstChild).toBe(first); + expect(first.nextSibling).toBe(middle); + expect(middle.nextSibling).toBe(last); + expect(last.nextSibling).toBeNull(); + expect(first.parentNode).toBe(parent); + expect(middle.parentNode).toBe(parent); + expect(last.parentNode).toBe(parent); + }); + + it('connects and disconnects a subtree when inserting it into another parent', () => { + const notify = vi.fn(); + const document = new Document(notify); + const detached = document.createElement('div'); + const root = document.documentElement; + const branch = document.createElement('span'); + const text = document.createTextNode('before'); + const anchor = document.createTextNode('anchor'); + branch.appendChild(text); + detached.appendChild(branch); + root.appendChild(anchor); + notify.mockClear(); + + root.insertBefore(branch, anchor); + + expect(detached.childNodes).toHaveLength(0); + expect(detached.firstChild).toBeNull(); + expect(root.childNodes).toHaveLength(2); + expect(root.firstChild).toBe(branch); + expect(branch.nextSibling).toBe(anchor); + expect(branch.parentNode).toBe(root); + expect(branch.childNodes[0]).toBe(text); + expect(text.parentNode).toBe(branch); + expect(branch.isConnected).toBe(true); + expect(text.isConnected).toBe(true); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + text.data = 'connected'; + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + detached.insertBefore(branch, null); + + expect(root.childNodes).toHaveLength(1); + expect(root.firstChild).toBe(anchor); + expect(detached.childNodes).toHaveLength(1); + expect(detached.firstChild).toBe(branch); + expect(branch.parentNode).toBe(detached); + expect(branch.nextSibling).toBeNull(); + expect(branch.childNodes[0]).toBe(text); + expect(text.parentNode).toBe(branch); + expect(branch.isConnected).toBe(false); + expect(text.isConnected).toBe(false); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + text.data = 'detached'; + expect(notify).not.toHaveBeenCalled(); + }); + + it('rejects invalid insertion anchors and cycles before detaching any nodes', () => { + const notify = vi.fn(); + const document = new Document(notify); + const root = document.documentElement; + const parent = document.createElement('div'); + const other = document.createElement('div'); + const child = document.createElement('span'); + parent.appendChild(child); + root.appendChild(parent); + root.appendChild(other); + notify.mockClear(); + + expect(parent.insertBefore).toBeTypeOf('function'); + expect(() => other.insertBefore(child, parent)).toThrow( + expect.objectContaining({ constructor: DOMException, name: 'NotFoundError' }), + ); + expect(parent.childNodes).toHaveLength(1); + expect(parent.childNodes[0]).toBe(child); + expect(child.parentNode).toBe(parent); + expect(other.childNodes).toHaveLength(0); + expect(notify).not.toHaveBeenCalled(); + + expect(() => child.insertBefore(parent, null)).toThrow(); + expect(root.childNodes).toHaveLength(2); + expect(root.childNodes[0]).toBe(parent); + expect(root.childNodes[1]).toBe(other); + expect(parent.parentNode).toBe(root); + expect(parent.childNodes[0]).toBe(child); + expect(child.parentNode).toBe(parent); + expect(child.childNodes).toHaveLength(0); + expect(notify).not.toHaveBeenCalled(); + + expect(() => parent.insertBefore(parent, child)).toThrow(); + expect(root.childNodes[0]).toBe(parent); + expect(parent.parentNode).toBe(root); + expect(parent.childNodes).toHaveLength(1); + expect(parent.childNodes[0]).toBe(child); + expect(child.parentNode).toBe(parent); + expect(notify).not.toHaveBeenCalled(); + }); +}); + +describe('remove', () => { + it('removes elements and text, updates sibling links, and ignores repeated removal', () => { + const notify = vi.fn(); + const document = new Document(notify); + const root = document.documentElement; + const branch = document.createElement('span'); + const nested = document.createTextNode('nested'); + const sibling = document.createTextNode('sibling'); + branch.appendChild(nested); + root.appendChild(branch); + root.appendChild(sibling); + notify.mockClear(); + + branch.remove(); + + expect(root.childNodes).toHaveLength(1); + expect(root.firstChild).toBe(sibling); + expect(branch.parentNode).toBeNull(); + expect(branch.nextSibling).toBeNull(); + expect(branch.firstChild).toBe(nested); + expect(nested.parentNode).toBe(branch); + expect(branch.isConnected).toBe(false); + expect(nested.isConnected).toBe(false); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + branch.remove(); + expect(notify).not.toHaveBeenCalled(); + + sibling.remove(); + + expect(root.childNodes).toHaveLength(0); + expect(root.firstChild).toBeNull(); + expect(sibling.parentNode).toBeNull(); + expect(sibling.nextSibling).toBeNull(); + expect(sibling.isConnected).toBe(false); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + sibling.remove(); + expect(notify).not.toHaveBeenCalled(); + }); +}); + +describe('setAttribute / removeAttribute', () => { + it('stores string attributes for existing-node matching and updates them without duplicates', () => { + const notify = vi.fn(); + const document = new Document(notify); + const element = document.createElement('span'); + + expect(element.attributes).toHaveLength(0); + element.setAttribute('title', 'before'); + // Attribute values arrive from JavaScript as numbers and booleans too. + Reflect.apply(element.setAttribute, element, ['data-count', 42]); + Reflect.apply(element.setAttribute, element, ['aria-hidden', false]); + + expect(element.attributes).toHaveLength(3); + expect(element.attributes[0]).toMatchObject({ name: 'title', value: 'before' }); + expect(element.attributes[1]).toMatchObject({ name: 'data-count', value: '42' }); + expect(element.attributes[2]).toMatchObject({ name: 'aria-hidden', value: 'false' }); + expect(notify).not.toHaveBeenCalled(); + + document.documentElement.appendChild(element); + notify.mockClear(); + element.setAttribute('title', 'after'); + + expect(element.attributes).toHaveLength(3); + expect(element.attributes[0]).toMatchObject({ name: 'title', value: 'after' }); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + element.removeAttribute('data-count'); + + expect(element.attributes).toHaveLength(2); + expect(element.attributes[0]).toMatchObject({ name: 'title', value: 'after' }); + expect(element.attributes[1]).toMatchObject({ name: 'aria-hidden', value: 'false' }); + expect(notify).toHaveBeenCalled(); + + notify.mockClear(); + element.removeAttribute('data-count'); + expect(element.attributes).toHaveLength(2); + expect(notify).not.toHaveBeenCalled(); + + document.documentElement.removeChild(element); + notify.mockClear(); + element.removeAttribute('title'); + element.removeAttribute('aria-hidden'); + expect(element.attributes).toHaveLength(0); + expect(notify).not.toHaveBeenCalled(); + }); +}); + +describe('events', () => { + describe('Event', () => { + it('starts with no dispatch state and allows framework bookkeeping', () => { + expect(Event).toBeTypeOf('function'); + expect(Event).toBe(globalThis.Event); + const event = new Event('click'); + + expect(event).toBeInstanceOf(globalThis.Event); + expect(event.type).toBe('click'); + expect(event.bubbles).toBe(false); + expect(event.cancelable).toBe(false); + expect(event.defaultPrevented).toBe(false); + expect(event.target).toBeNull(); + expect(event.currentTarget).toBeNull(); + expect(event.eventPhase).toBe(Event.NONE); + expect(Event.NONE).toBe(0); + expect(Event.CAPTURING_PHASE).toBe(1); + expect(Event.AT_TARGET).toBe(2); + expect(Event.BUBBLING_PHASE).toBe(3); + + const dispatched = Symbol('dispatched'); + expect(Reflect.set(event, dispatched, 1)).toBe(true); + expect(Reflect.get(event, dispatched)).toBe(1); + }); + }); + + describe('native events and listener options', () => { + it('dispatches native events and custom events with their identity and data intact', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const target = document.createElement('span'); + root.appendChild(target); + expect(target).toBeInstanceOf(globalThis.EventTarget); + const calls: unknown[][] = []; + const details: unknown[] = []; + function record(this: Node, event: Event): void { + calls.push([this, event, event.target, event.currentTarget, event.eventPhase]); + if (event instanceof globalThis.CustomEvent) { + details.push(event.detail); + } + } + target.addEventListener('native', record); + root.addEventListener('native', record); + const detail = { value: 'original data' }; + const events = [ + new globalThis.Event('native', { bubbles: true }), + new globalThis.CustomEvent('native', { bubbles: true, detail }), + ]; + + for (const event of events) { + calls.length = 0; + expect(target.dispatchEvent(event)).toBe(true); + expect(calls).toEqual([ + [target, event, target, target, Event.AT_TARGET], + [root, event, target, root, Event.BUBBLING_PHASE], + ]); + expect(calls.every((call) => call[1] === event)).toBe(true); + expect(event.target).toBe(target); + expect(event.currentTarget).toBeNull(); + expect(event.eventPhase).toBe(Event.NONE); + } + expect(details).toEqual([detail, detail]); + expect(details.every((received) => received === detail)).toBe(true); + }); + + it('removes once listeners before invoking them so nested events do not invoke them again', () => { + const document = new Document(vi.fn()); + const target = document.createElement('span'); + const outer = new globalThis.Event('click'); + const inner = new globalThis.Event('click'); + const once = vi.fn(() => target.dispatchEvent(inner)); + const regular = vi.fn(); + const options: Parameters[2] = Object.create({ + capture: false, + once: true, + }); + target.addEventListener('click', once, options); + target.addEventListener('click', regular); + + target.dispatchEvent(outer); + + expect(once).toHaveBeenCalledExactlyOnceWith(outer); + expect(regular.mock.calls).toEqual([[inner], [outer]]); + target.dispatchEvent(outer); + expect(once).toHaveBeenCalledTimes(1); + expect(regular.mock.calls).toEqual([[inner], [outer], [outer]]); + }); + + it('skips already aborted signals and removes listeners on abort', () => { + const document = new Document(vi.fn()); + const target = document.createElement('span'); + const controller = new AbortController(); + const aborted = vi.fn(); + const listener = vi.fn(); + target.addEventListener('click', aborted, { signal: AbortSignal.abort() }); + target.addEventListener('click', listener, { signal: controller.signal }); + + target.dispatchEvent(new globalThis.Event('click')); + expect(aborted).not.toHaveBeenCalled(); + expect(listener).toHaveBeenCalledTimes(1); + + controller.abort(); + target.dispatchEvent(new globalThis.Event('click')); + expect(listener).toHaveBeenCalledTimes(1); + }); + + it('skips a listener aborted by an earlier listener during dispatch', () => { + const document = new Document(vi.fn()); + const target = document.createElement('span'); + const controller = new AbortController(); + const listener = vi.fn(); + target.addEventListener('click', () => controller.abort()); + target.addEventListener('click', listener, { signal: controller.signal }); + + target.dispatchEvent(new globalThis.Event('click')); + + expect(listener).not.toHaveBeenCalled(); + }); + + it('prevents passive listeners from canceling while allowing subsequent ordinary listeners to cancel', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const target = document.createElement('span'); + root.appendChild(target); + const prevented: boolean[] = []; + target.addEventListener( + 'click', + (event) => { + event.preventDefault(); + event.returnValue = false; + }, + { passive: true }, + ); + root.addEventListener('click', (event) => prevented.push(event.defaultPrevented)); + const passive = new globalThis.Event('click', { bubbles: true, cancelable: true }); + + expect(target.dispatchEvent(passive)).toBe(true); + expect(passive.defaultPrevented).toBe(false); + + target.addEventListener('click', (event) => event.preventDefault()); + const active = new globalThis.Event('click', { bubbles: true, cancelable: true }); + expect(target.dispatchEvent(active)).toBe(false); + expect(active.defaultPrevented).toBe(true); + expect(prevented).toEqual([false, true]); + }); + + it('uses listener object identity and resolves handleEvent with the object as this on each dispatch', () => { + const document = new Document(vi.fn()); + const target = document.createElement('span'); + const first = vi.fn(); + const second = vi.fn(); + const listener = { handleEvent: first }; + target.addEventListener('click', listener); + target.addEventListener('click', listener); + const initial = new globalThis.Event('click'); + + target.dispatchEvent(initial); + expect(first).toHaveBeenCalledExactlyOnceWith(initial); + expect(first.mock.contexts[0]).toBe(listener); + + listener.handleEvent = second; + const updated = new globalThis.Event('click'); + target.dispatchEvent(updated); + expect(first).toHaveBeenCalledTimes(1); + expect(second).toHaveBeenCalledExactlyOnceWith(updated); + expect(second.mock.contexts[0]).toBe(listener); + + target.removeEventListener('click', listener); + target.dispatchEvent(new globalThis.Event('click')); + expect(second).toHaveBeenCalledTimes(1); + }); + + it('retains native propagation flags when reusing a stopped event', () => { + for (const method of ['stopPropagation', 'stopImmediatePropagation'] as const) { + const document = new Document(vi.fn()); + const root = document.documentElement; + const target = document.createElement('span'); + root.appendChild(target); + const listener = vi.fn(); + const ancestor = vi.fn(); + target.addEventListener('click', (event) => event[method](), { once: true }); + target.addEventListener('click', listener); + root.addEventListener('click', ancestor); + const event = new globalThis.Event('click', { bubbles: true }); + + target.dispatchEvent(event); + + expect(event.cancelBubble).toBe(true); + expect(event.stopPropagation).toBe(globalThis.Event.prototype.stopPropagation); + expect(event.stopImmediatePropagation).toBe( + globalThis.Event.prototype.stopImmediatePropagation, + ); + expect(event.preventDefault).toBe(globalThis.Event.prototype.preventDefault); + expect(event.composedPath).toBe(globalThis.Event.prototype.composedPath); + listener.mockClear(); + + expect(target.dispatchEvent(event)).toBe(true); + expect(listener).toHaveBeenCalledTimes(method === 'stopPropagation' ? 1 : 0); + expect(ancestor).not.toHaveBeenCalled(); + expect(event.currentTarget).toBeNull(); + expect(event.eventPhase).toBe(Event.NONE); + } + }); + + it('rejects native redispatch from an active listener', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const target = document.createElement('span'); + root.appendChild(target); + const event = new globalThis.Event('click', { bubbles: true }); + const nativeTarget = new globalThis.EventTarget(); + const nativeListener = vi.fn(); + nativeTarget.addEventListener('click', nativeListener); + let redispatchError: unknown; + const calls: unknown[][] = []; + target.addEventListener('click', (received) => { + try { + nativeTarget.dispatchEvent(received); + } catch (error) { + redispatchError = error; + } + calls.push(['first', received.target, received.currentTarget, received.eventPhase]); + }); + target.addEventListener('click', (received) => { + calls.push(['second', received.target, received.currentTarget, received.eventPhase]); + }); + root.addEventListener('click', (received) => { + calls.push(['ancestor', received.target, received.currentTarget, received.eventPhase]); + }); + + expect(target.dispatchEvent(event)).toBe(true); + + expect(redispatchError).toBeInstanceOf(Error); + expect(nativeListener).not.toHaveBeenCalled(); + expect(calls).toEqual([ + ['first', target, target, Event.AT_TARGET], + ['second', target, target, Event.AT_TARGET], + ['ancestor', target, root, Event.BUBBLING_PHASE], + ]); + expect(event.target).toBe(target); + expect(event.currentTarget).toBeNull(); + expect(event.eventPhase).toBe(Event.NONE); + }); + }); + + describe('addEventListener / removeEventListener', () => { + it('calls a stable listener with the element as this as its handler changes', () => { + const document = new Document(vi.fn()); + const first = vi.fn(); + const second = vi.fn(); + const element = Object.assign(document.createElement('span'), { handler: first }); + const listener = vi.fn(function (this: typeof element, event: Event): void { + this.handler(event); + }); + element.addEventListener('click', listener, false); + element.addEventListener('click', listener, false); + const initial = new Event('click'); + element.dispatchEvent(initial); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener.mock.contexts[0]).toBe(element); + expect(first).toHaveBeenCalledExactlyOnceWith(initial); + + element.handler = second; + const updated = new Event('click'); + element.dispatchEvent(updated); + + expect(first).toHaveBeenCalledTimes(1); + expect(second).toHaveBeenCalledExactlyOnceWith(updated); + + element.removeEventListener('click', listener, false); + element.dispatchEvent(new Event('click')); + + expect(listener).toHaveBeenCalledTimes(2); + expect(second).toHaveBeenCalledTimes(1); + }); + + it('inherits the native listener methods unchanged', () => { + const document = new Document(vi.fn()); + const element = document.createElement('span'); + + expect(EventTarget).toBe(globalThis.EventTarget); + expect(element.addEventListener).toBe(globalThis.EventTarget.prototype.addEventListener); + expect(element.removeEventListener).toBe( + globalThis.EventTarget.prototype.removeEventListener, + ); + }); + + it('uses native capture option registration and removal without a capture traversal', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const element = document.createElement('span'); + root.appendChild(element); + const calls: string[] = []; + const listener = () => calls.push('target'); + element.addEventListener('click', listener, true); + element.addEventListener('click', listener, { capture: true }); + element.addEventListener('click', listener); + root.addEventListener('click', () => calls.push('root'), { capture: true }); + + element.dispatchEvent(new Event('click')); + expect(calls).toEqual(['target', 'target']); + + calls.length = 0; + element.dispatchEvent(new Event('click', { bubbles: true })); + expect(calls).toEqual(['target', 'target', 'root']); + + calls.length = 0; + element.removeEventListener('click', listener, { capture: true }); + element.dispatchEvent(new Event('click', { bubbles: true })); + expect(calls).toEqual(['target', 'root']); + }); + + it('scopes listeners by node and event type without requesting a render', () => { + const notify = vi.fn(); + const document = new Document(notify); + const first = document.createElement('span'); + const second = document.createElement('span'); + document.documentElement.appendChild(first); + document.documentElement.appendChild(second); + notify.mockClear(); + const listener = vi.fn(); + first.addEventListener('click', listener); + first.addEventListener('keydown', listener); + second.addEventListener('click', listener); + + first.removeEventListener('click', listener); + first.removeEventListener('click', listener); + first.removeEventListener('missing', listener); + first.dispatchEvent(new Event('click')); + expect(listener).not.toHaveBeenCalled(); + + const keydown = new Event('keydown'); + first.dispatchEvent(keydown); + expect(listener).toHaveBeenCalledExactlyOnceWith(keydown); + + listener.mockClear(); + const click = new Event('click'); + second.dispatchEvent(click); + expect(listener).toHaveBeenCalledExactlyOnceWith(click); + expect(notify).not.toHaveBeenCalled(); + }); + }); + + describe('dispatchEvent', () => { + it('sets the target and clears transient state even when there are no listeners', () => { + const document = new Document(vi.fn()); + const target = document.createElement('span'); + const event = new Event('click', { bubbles: true, cancelable: true }); + + expect(target.dispatchEvent(event)).toBe(true); + + expect(event.bubbles).toBe(true); + expect(event.cancelable).toBe(true); + expect(event.target).toBe(target); + expect(event.currentTarget).toBeNull(); + expect(event.eventPhase).toBe(Event.NONE); + expect(event.defaultPrevented).toBe(false); + }); + + it('invokes target listeners and bubbles through ancestors to the document in order', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const parent = document.createElement('div'); + const target = document.createElement('span'); + root.appendChild(parent); + parent.appendChild(target); + const event = new Event('click', { bubbles: true }); + const calls: string[] = []; + const phases: number[] = []; + const contexts: boolean[] = []; + const targets: boolean[] = []; + const events: boolean[] = []; + function record(label: string, currentTarget: Node): (this: Node, received: Event) => void { + return function (this: Node, received: Event): void { + calls.push(label); + phases.push(received.eventPhase); + contexts.push(this === currentTarget && received.currentTarget === currentTarget); + targets.push(received.target === target); + events.push(received === event); + }; + } + target.addEventListener('click', record('target first', target)); + target.addEventListener('click', record('target second', target)); + parent.addEventListener('click', record('parent bubble', parent)); + root.addEventListener('click', record('root bubble', root)); + document.addEventListener('click', record('document bubble', document)); + + expect(target.dispatchEvent(event)).toBe(true); + + expect(calls).toEqual([ + 'target first', + 'target second', + 'parent bubble', + 'root bubble', + 'document bubble', + ]); + expect(phases).toEqual([ + Event.AT_TARGET, + Event.AT_TARGET, + Event.BUBBLING_PHASE, + Event.BUBBLING_PHASE, + Event.BUBBLING_PHASE, + ]); + expect(contexts.every(Boolean)).toBe(true); + expect(targets.every(Boolean)).toBe(true); + expect(events.every(Boolean)).toBe(true); + expect(event.target).toBe(target); + expect(event.currentTarget).toBeNull(); + expect(event.eventPhase).toBe(Event.NONE); + }); + + it('invokes only target listeners for non-bubbling events', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const target = document.createElement('span'); + root.appendChild(target); + const calls: string[] = []; + root.addEventListener('click', () => calls.push('root bubble')); + target.addEventListener('click', () => calls.push('target')); + + target.dispatchEvent(new Event('click')); + + expect(calls).toEqual(['target']); + }); + + it('preserves listeners and dispatches within a detached subtree without rendering', () => { + const notify = vi.fn(); + const document = new Document(notify); + const parent = document.createElement('div'); + const target = document.createElement('span'); + parent.appendChild(target); + document.documentElement.appendChild(parent); + const calls: string[] = []; + document.addEventListener('click', () => calls.push('document')); + parent.addEventListener('click', () => calls.push('parent bubble')); + target.addEventListener('click', () => calls.push('target')); + parent.remove(); + notify.mockClear(); + + target.dispatchEvent(new Event('click', { bubbles: true })); + + expect(target.isConnected).toBe(false); + expect(calls).toEqual(['target', 'parent bubble']); + expect(notify).not.toHaveBeenCalled(); + }); + + it('keeps the original ancestor path when a listener reparents the target', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const original = document.createElement('div'); + const destination = document.createElement('div'); + const target = document.createElement('span'); + root.appendChild(original); + root.appendChild(destination); + original.appendChild(target); + const calls: string[] = []; + target.addEventListener('click', () => { + calls.push('target'); + destination.appendChild(target); + }); + original.addEventListener('click', () => calls.push('original bubble')); + destination.addEventListener('click', () => calls.push('destination bubble')); + root.addEventListener('click', () => calls.push('root bubble')); + + target.dispatchEvent(new Event('click', { bubbles: true })); + + expect(target.parentNode).toBe(destination); + expect(calls).toEqual(['target', 'original bubble', 'root bubble']); + + calls.length = 0; + target.dispatchEvent(new Event('click', { bubbles: true })); + expect(calls).toEqual(['target', 'destination bubble', 'root bubble']); + }); + + it('skips a removed listener even if the same callback is registered again', () => { + const document = new Document(vi.fn()); + const target = document.createElement('span'); + const calls: string[] = []; + const later = (): number => calls.push('later'); + const replace = (): void => { + calls.push('replace'); + target.removeEventListener('click', later); + target.addEventListener('click', later); + }; + target.addEventListener('click', replace); + target.addEventListener('click', later); + + target.dispatchEvent(new Event('click')); + expect(calls).toEqual(['replace']); + + target.removeEventListener('click', replace); + calls.length = 0; + target.dispatchEvent(new Event('click')); + expect(calls).toEqual(['later']); + }); + + it('invokes listeners added to an upcoming ancestor', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const target = document.createElement('span'); + root.appendChild(target); + const calls: string[] = []; + const later = (): number => calls.push('later'); + const added = (): void => { + calls.push('added'); + root.addEventListener('click', later); + }; + target.addEventListener('click', () => { + calls.push('target'); + root.addEventListener('click', added); + }); + + target.dispatchEvent(new Event('click', { bubbles: true })); + expect(calls).toEqual(['target', 'added']); + + calls.length = 0; + target.dispatchEvent(new Event('click', { bubbles: true })); + expect(calls).toEqual(['target', 'added', 'later']); + }); + + it('supports nested events, rejects active-event redispatch, and permits later reuse', () => { + const document = new Document(vi.fn()); + const target = document.createElement('span'); + const outer = new Event('outer'); + const inner = new Event('inner'); + const calls: string[] = []; + const states: boolean[] = []; + let redispatchError: unknown; + let redispatching = false; + target.addEventListener('inner', (event: Event) => { + calls.push('inner'); + states.push(event.currentTarget === target && outer.currentTarget === target); + }); + target.addEventListener('outer', (event: Event) => { + if (redispatching) { + calls.push('unexpected redispatch'); + return; + } + calls.push('outer start'); + redispatching = true; + try { + target.dispatchEvent(event); + } catch (error) { + redispatchError = error; + } finally { + redispatching = false; + } + target.dispatchEvent(inner); + states.push(event.currentTarget === target && event.eventPhase === Event.AT_TARGET); + states.push(inner.currentTarget === null && inner.eventPhase === Event.NONE); + calls.push('outer end'); + }); + + target.dispatchEvent(outer); + + expect(redispatchError).toBeInstanceOf(DOMException); + expect(redispatchError).toMatchObject({ name: 'InvalidStateError' }); + expect(calls).toEqual(['outer start', 'inner', 'outer end']); + expect(states).toEqual([true, true, true]); + expect(outer.currentTarget).toBeNull(); + expect(outer.eventPhase).toBe(Event.NONE); + + calls.length = 0; + states.length = 0; + redispatchError = undefined; + target.dispatchEvent(outer); + expect(redispatchError).toBeInstanceOf(DOMException); + expect(redispatchError).toMatchObject({ name: 'InvalidStateError' }); + expect(calls).toEqual(['outer start', 'inner', 'outer end']); + expect(states).toEqual([true, true, true]); + }); + }); + + describe('stopPropagation', () => { + it('finishes target listeners without bubbling or canceling the event', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const target = document.createElement('span'); + root.appendChild(target); + const calls: string[] = []; + target.addEventListener('click', (event: Event) => { + calls.push('first'); + event.stopPropagation(); + }); + target.addEventListener('click', () => calls.push('second')); + root.addEventListener('click', () => calls.push('root bubble')); + const event = new Event('click', { bubbles: true, cancelable: true }); + + expect(target.dispatchEvent(event)).toBe(true); + + expect(calls).toEqual(['first', 'second']); + expect(event.defaultPrevented).toBe(false); + }); + + it('finishes the current ancestor bubble listeners without visiting further ancestors', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const parent = document.createElement('div'); + const target = document.createElement('span'); + root.appendChild(parent); + parent.appendChild(target); + const calls: string[] = []; + target.addEventListener('click', () => calls.push('target')); + parent.addEventListener('click', (event: Event) => { + calls.push('first parent bubble'); + event.stopPropagation(); + }); + parent.addEventListener('click', () => calls.push('second parent bubble')); + root.addEventListener('click', () => calls.push('root bubble')); + document.addEventListener('click', () => calls.push('document bubble')); + const event = new Event('click', { bubbles: true, cancelable: true }); + + expect(target.dispatchEvent(event)).toBe(true); + + expect(calls).toEqual(['target', 'first parent bubble', 'second parent bubble']); + expect(event.defaultPrevented).toBe(false); + expect(event.currentTarget).toBeNull(); + expect(event.eventPhase).toBe(Event.NONE); + }); + }); + + describe('stopImmediatePropagation', () => { + it('stops remaining ancestor bubble listeners and further ancestors', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const parent = document.createElement('div'); + const target = document.createElement('span'); + root.appendChild(parent); + parent.appendChild(target); + const calls: string[] = []; + target.addEventListener('click', () => calls.push('target')); + parent.addEventListener('click', (event: Event) => { + calls.push('first parent bubble'); + event.stopImmediatePropagation(); + }); + parent.addEventListener('click', () => calls.push('second parent bubble')); + root.addEventListener('click', () => calls.push('root bubble')); + document.addEventListener('click', () => calls.push('document bubble')); + const event = new Event('click', { bubbles: true, cancelable: true }); + + expect(target.dispatchEvent(event)).toBe(true); + + expect(calls).toEqual(['target', 'first parent bubble']); + expect(event.defaultPrevented).toBe(false); + expect(event.currentTarget).toBeNull(); + expect(event.eventPhase).toBe(Event.NONE); + }); + + it('stops remaining listeners and ancestors and retains native stop flags on reuse', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const target = document.createElement('span'); + root.appendChild(target); + const calls: string[] = []; + const stop = (event: Event): void => { + calls.push('stop'); + event.stopImmediatePropagation(); + }; + target.addEventListener('click', stop); + target.addEventListener('click', () => calls.push('target')); + root.addEventListener('click', () => calls.push('root bubble')); + const event = new Event('click', { bubbles: true, cancelable: true }); + + expect(target.dispatchEvent(event)).toBe(true); + expect(calls).toEqual(['stop']); + expect(event.defaultPrevented).toBe(false); + expect(event.currentTarget).toBeNull(); + expect(event.eventPhase).toBe(Event.NONE); + + target.removeEventListener('click', stop); + calls.length = 0; + expect(target.dispatchEvent(event)).toBe(true); + expect(calls).toEqual([]); + + target.dispatchEvent(new Event('click', { bubbles: true, cancelable: true })); + expect(calls).toEqual(['target', 'root bubble']); + }); + }); + + describe('preventDefault', () => { + it('cancels only cancelable events without stopping propagation', () => { + const document = new Document(vi.fn()); + const root = document.documentElement; + const target = document.createElement('span'); + root.appendChild(target); + const prevented: boolean[] = []; + const prevent = (event: Event): void => event.preventDefault(); + target.addEventListener('click', prevent); + root.addEventListener('click', (event: Event) => prevented.push(event.defaultPrevented)); + const ordinary = new Event('click', { bubbles: true }); + const cancelable = new Event('click', { bubbles: true, cancelable: true }); + + expect(target.dispatchEvent(ordinary)).toBe(true); + expect(ordinary.defaultPrevented).toBe(false); + expect(target.dispatchEvent(cancelable)).toBe(false); + expect(cancelable.defaultPrevented).toBe(true); + expect(prevented).toEqual([false, true]); + + target.removeEventListener('click', prevent); + expect(target.dispatchEvent(cancelable)).toBe(false); + expect(cancelable.defaultPrevented).toBe(true); + }); + }); +}); diff --git a/packages/ui/src/dom.ts b/packages/ui/src/dom.ts index e56606e..7343d6f 100644 --- a/packages/ui/src/dom.ts +++ b/packages/ui/src/dom.ts @@ -5,71 +5,335 @@ export interface Elements { document: Element; } -export class Document { - readonly documentElement: Element; - readonly notify: () => void; +export const ELEMENT_NODE = 1; +export const ATTRIBUTE_NODE = 2; +export const TEXT_NODE = 3; +export const DOCUMENT_NODE = 9; - constructor(notify: () => void) { - this.notify = notify; - this.documentElement = new Element('document', notify); +export type NodeType = + | typeof ELEMENT_NODE + | typeof ATTRIBUTE_NODE + | typeof TEXT_NODE + | typeof DOCUMENT_NODE; + +export interface EventListener { + (this: Target, event: Event): void; +} + +export const EventTarget = globalThis.EventTarget; +export type EventTarget = globalThis.EventTarget; + +export abstract class Node extends EventTarget { + parentNode: Node | null; + readonly ownerDocument: Owner; + readonly nodeName: string; + abstract readonly nodeType: NodeType; + + readonly #children: Node[] = []; + + constructor(nodeName: string, owner: Owner) { + super(); + this.nodeName = nodeName; + this.ownerDocument = owner; + this.parentNode = null; } - createElement(tagName: keyof Elements) { - return new Element(tagName, this.notify); + get namespaceURI() { + return 'http://www.w3.org/1999/xhtml'; } - createTextNode(data: string): Text { - return new Text(data, this.notify); + get isConnected(): boolean { + return this.parentNode ? this.parentNode.isConnected : false; } -} -export class Node { - readonly nodeName: string; - readonly childNodes: Node[]; + get childNodes(): ReadonlyArray { + return this.#children; + } - protected readonly notify: () => void; + get firstChild(): Node | null { + return this.#children[0] ?? null; + } - constructor(nodeName: string, notify: Node['notify']) { - this.notify = notify; - this.nodeName = nodeName; - this.childNodes = []; + get nextSibling(): Node | null { + if (this.parentNode) { + const index = this.parentNode.childNodes.indexOf(this); + return this.parentNode.childNodes[index + 1] ?? null; + } + return null; + } + + remove() { + this.parentNode?.removeChild(this); + return this; + } + + insertBefore(node: Node, anchor: Node | null): Node { + if (anchor && anchor.parentNode !== this) { + throw new DOMException('Anchor is not a child of this node', 'NotFoundError'); + } + + if (node === this) { + throw new DOMException('cannot make a Node a child of itself', 'HierarchyRequestError'); + } + + if (ancestorOf(this, node)) { + throw new DOMException( + 'cannot add an ancestor Node to its descendant', + 'HierarchyRequestError', + ); + } + if (node === anchor) { + return node; + } + if (node.parentNode) { + if (node.parentNode === this) { + const currentIndex = this.#children.indexOf(node); + this.#children.splice(currentIndex, 1); + } else { + node.parentNode.removeChild(node); + } + } + const anchorIndex = anchor ? this.#children.indexOf(anchor) : this.#children.length; + this.#children.splice(anchorIndex, 0, node); + + node.parentNode = this; + this.notify(); + + return node; } appendChild(node: Node): Node { - this.childNodes.push(node); + return this.insertBefore(node, null); + } + + removeChild(node: Node): Node { + const currentIndex = this.#children.indexOf(node); + if (currentIndex < 0) { + throw new DOMException( + `cannot remove ${node.nodeName} from ${this.nodeName}. It is not a child`, + 'NotFoundError', + ); + } + + this.#children.splice(currentIndex, 1); + node.parentNode = null; + this.notify(); return node; } + + override dispatchEvent(event: Event): boolean { + if (event.eventPhase !== Event.NONE) { + throw new DOMException('Cannot redispatch an active event', 'InvalidStateError'); + } + + const path: Node[] = [this]; + for (let current = this.parentNode; current; current = current.parentNode) { + path.push(current); + } + + // Native dispatch retargets on each node. Keep the tree's original target + // and expose its current node and phase while each native dispatch runs. + Object.defineProperty(event, 'target', { configurable: true, value: this }); + const originals = Object.getOwnPropertyDescriptors(event); + + try { + Object.defineProperty(event, 'composedPath', { + value: () => [...path], + configurable: true, + }); + const end = event.bubbles ? path.length : 1; + for (let i = 0; i < end; i++) { + Object.defineProperties(event, { + currentTarget: { value: path[i], configurable: true }, + eventPhase: { + value: i === 0 ? Event.AT_TARGET : Event.BUBBLING_PHASE, + configurable: true, + }, + }); + super.dispatchEvent.call(path[i], event); + if (event.cancelBubble) { + break; + } + } + return !event.defaultPrevented; + } finally { + for (const name of ['currentTarget', 'eventPhase', 'composedPath']) { + const original = originals[name]; + if (original) { + Object.defineProperty(event, name, original); + } else { + Reflect.deleteProperty(event, name); + } + } + } + } + + protected notify(): void { + if (this.isConnected) { + this.ownerDocument?.notify(); + } + } +} + +export class Document extends Node { + readonly documentElement: Element; + override readonly notify: () => void; + + constructor(notify: () => void) { + super('#document', null); + this.documentElement = this.createElement('document'); + this.notify = () => {}; + this.appendChild(this.documentElement); + this.notify = notify; + } + + get nodeType(): NodeType { + return DOCUMENT_NODE; + } + + override get isConnected() { + return true; + } + + createElement(tagName: K): Elements[K]; + createElement(tagName: string): Element; + createElement(tagName: string): Element { + return new Element(tagName, this); + } + + createElementNS(namespace: string, tagName: string): Element { + if (namespace !== 'http://www.w3.org/1999/xhtml') { + throw new DOMException('only HTML namespace is supported', 'NotSupportedError'); + } + return this.createElement(tagName); + } + + createAttribute(name: string): Attr { + return new Attr(name, this); + } + + createTextNode(data: string): Text { + return new Text(String(data), this); + } } -export class Element extends Node { +export class Element extends Node { readonly tagName: string; - constructor(tagName: keyof Elements, notify: Node['notify']) { - super(tagName.toUpperCase(), notify); + #attributes = new Map(); + + constructor(tagName: string, document: Document) { + super(tagName.toUpperCase(), document); this.tagName = tagName; } + get nodeType(): NodeType { + return ELEMENT_NODE; + } + + get localName(): string { + return this.tagName; + } + get children(): Element[] { return this.childNodes.filter((node) => node instanceof Element); } + + get attributes(): Attr[] { + return [...this.#attributes.values()]; + } + + setAttribute(name: string, value: string): void { + let attr = this.#attributes.get(name); + if (!attr) { + attr = this.ownerDocument.createAttribute(name); + attr.ownerElement = this; + this.#attributes.set(name, attr); + } + if (value !== attr.value) { + attr.value = value; + this.notify(); + } + } + + removeAttribute(name: string) { + const attr = this.#attributes.get(name); + if (attr) { + this.#attributes.delete(name); + attr.ownerElement = null; + this.notify(); + } + } } -export class Text extends Node { +export class Text extends Node { #data: string; - constructor(data: string, notify: Node['notify']) { - super('#text', notify); + constructor(data: string, document: Document) { + super('#text', document); this.#data = data; } + get nodeType(): NodeType { + return TEXT_NODE; + } + get data() { return this.#data; } set data(data: string) { - if (data !== this.data) { - this.#data = data; + const value = String(data); + if (value !== this.data) { + this.#data = value; this.notify(); } } } + +export class Attr extends Node { + readonly name: string; + ownerElement: Element | null = null; + #value = ''; + + constructor(name: string, owner: Document) { + super(name, owner); + this.name = name; + } + + get nodeType(): NodeType { + return ATTRIBUTE_NODE; + } + + get value(): string { + return this.#value; + } + + set value(value: string) { + this.#value = String(value); + } +} + +export interface EventOptions { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; +} + +// Node exposes these constants at runtime, but its type declarations omit them. +export const Event = globalThis.Event as typeof globalThis.Event & { + readonly NONE: 0; + readonly CAPTURING_PHASE: 1; + readonly AT_TARGET: 2; + readonly BUBBLING_PHASE: 3; +}; +export type Event = globalThis.Event; + +function ancestorOf(node: Node, candidate: Node) { + for (let current: Node | null = node; current; current = current.parentNode) { + if (current.parentNode === candidate) { + return true; + } + } + return false; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac918e5..4a9e09c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@bomb.sh/tools': specifier: ^0.8.0 version: 0.8.0(@types/node@22.20.0)(oxc-resolver@11.20.0)(unrun@0.2.39)(vite@8.0.16(@types/node@22.20.0)(jiti@2.7.0)(yaml@2.9.1)) + '@changesets/cli': + specifier: ^2.30.0 + version: 2.31.1(@types/node@22.20.0) '@codspeed/vitest-plugin': specifier: ^6.1.0 version: 6.1.0(tinybench@6.2.2)(vite@8.0.16(@types/node@22.20.0)(jiti@2.7.0)(yaml@2.9.1))(vitest@5.0.5(@types/node@22.20.0)(vite@8.0.16(@types/node@22.20.0)(jiti@2.7.0)(yaml@2.9.1))) @@ -44,6 +47,10 @@ importers: packages: + '@babel/runtime@7.29.10': + resolution: {integrity: sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==} + engines: {node: '>=6.9.0'} + '@bomb.sh/args@0.3.1': resolution: {integrity: sha512-CwxKrfgcorUPP6KfYD59aRdBYWBTsfsxT+GmoLVnKo5Tmyoqbpo0UNcjngRMyU+6tiPbd18RuIYxhgAn44wU/Q==} @@ -55,6 +62,61 @@ packages: resolution: {integrity: sha512-ZOfMnVMhlik9CtSv5NJV4409T6SitS9GC93YZrbq1Be1nmbv83gf5wQHdOc+ic9QOb2kgpgAXKrMV6WiHLSTPA==} engines: {node: '>= 22'} + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.31.1': + resolution: {integrity: sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==} + hasBin: true + + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@codspeed/core@6.1.0': resolution: {integrity: sha512-lxFkexeuf6iOWUZmgCeuGSVgk0fKBbtnCVd3w3UWi3627/4t55FhA046LhFLzedCQUi6183s99DnM3/6TRjzHQ==} @@ -90,6 +152,15 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -100,6 +171,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@napi-rs/canvas-android-arm64@1.0.11': resolution: {integrity: sha512-dtJS4FQEfH9OwSYLtMrEnj7T8DzBbJc/97LI/OkiWOYpW/iW5t6R/gcxVrUXS8l45gCVpwMufgucRe/pe0ISqA==} engines: {node: '>= 10'} @@ -176,6 +253,18 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@oxc-parser/binding-android-arm-eabi@0.133.0': resolution: {integrity: sha512-l/44caGse+VpnY9gx0yvvc5QnnG3yG1FO3KZgYvNL1GZrfK86zIwAOgGEVlxDyRymzrU/KHiblPFpevKOmJmUA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1022,6 +1111,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@22.20.0': resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} @@ -1226,6 +1318,24 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1236,6 +1346,14 @@ packages: axios@1.21.0: resolution: {integrity: sha512-YCzXMB53Q9PZBsh7yN7wtapit4WepwcHS2fP8QhGsBxSz5YtgYobV7GGlWlAJi+RxfU77GQTQi2DSLUma42EYA==} + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + cac@7.1.0: resolution: {integrity: sha512-LMDU/U2yA4IPIY8KT0NsNPWEYZvAMkbgyspxJ6btJaYV+WMDOHAZCmB0lsTObiHaRS1V/ztKAl7+TSkTqXyeoA==} engines: {node: '>=20.19.0'} @@ -1248,10 +1366,17 @@ packages: resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} engines: {node: '>=18'} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1268,10 +1393,18 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dts-resolver@3.0.0: resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} engines: {node: ^22.18.0 || >=24.0.0} @@ -1289,6 +1422,10 @@ packages: resolution: {integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==} engines: {node: '>=14'} + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1308,6 +1445,11 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1315,6 +1457,16 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + fd-package-json@2.0.0: resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} @@ -1327,6 +1479,14 @@ packages: picomatch: optional: true + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + follow-redirects@1.16.2: resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==} engines: {node: '>=4.0'} @@ -1345,6 +1505,14 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1374,10 +1542,21 @@ packages: gifenc@1.0.3: resolution: {integrity: sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1397,14 +1576,60 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + human-id@4.2.2: + resolution: {integrity: sha512-Z+8unD6em/MJwyrkWQ2M3wygRGI1+MztbutiNwk1DCuEPXOZRWGjtcMPZYP1exY9iNZMhd7ZOSMQSo9WYtiQRw==} + hasBin: true + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + import-without-cache@0.4.1: resolution: {integrity: sha512-vXoV9PjKHEednCUu01e98TkImxy67e3BJXbTIOmIq4Hyzw3IAwYRcuPkfeTzJzwyyts4kjuA73x+pWJLc4f86A==} engines: {node: ^22.18.0 || >=24.0.0} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} + hasBin: true + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + knip@6.16.1: resolution: {integrity: sha512-TKMn1rxgH6h9vXR9Y0B+Cq7AdPTr9EI02IwoT65NzqYUkvoDQAaJ/aPybiFpAhZ1px6cNYYwXf86iHkBgzCo9w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1480,6 +1705,13 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + magic-string@1.4.3: resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} @@ -1487,6 +1719,14 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1521,6 +1761,9 @@ packages: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + oxc-parser@0.133.0: resolution: {integrity: sha512-661RSx+ZcjBmjBYid+Fpp/2F5EbtildpeoZh5HdgnGs+jZ03nqQEQW8yGkt4BGyOC3OMPDQQRl8M5kqD2/g6jw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1557,19 +1800,62 @@ packages: vite-plus: optional: true + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.7: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + playwright-core@1.64.1: resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==} engines: {node: '>=20'} @@ -1584,6 +1870,11 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -1593,12 +1884,30 @@ packages: engines: {node: '>=18'} hasBin: true + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rolldown-plugin-dts@0.28.8: resolution: {integrity: sha512-FVhfqxorCWcc20BVT5eVRfRjzLLl6mgzjcMR4gSBf0K4oQMXT0qoHhhzfXy0L7y83fQy7Ug5ZDNk90xM3/LiCQ==} engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} @@ -1636,10 +1945,37 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + smol-toml@1.6.1: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} @@ -1648,6 +1984,12 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stack-trace@1.0.0-pre2: resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} engines: {node: '>=16'} @@ -1655,10 +1997,22 @@ packages: std-env@4.3.0: resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + tinybench@6.2.2: resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} engines: {node: '>=20.0.0'} @@ -1675,6 +2029,10 @@ packages: resolution: {integrity: sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==} engines: {node: ^20.0.0 || >=22.0.0} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1731,6 +2089,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + unrun@0.2.39: resolution: {integrity: sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg==} engines: {node: '>=20.19.0'} @@ -1841,6 +2203,11 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@3.2.1: resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} engines: {node: '>=20.11'} @@ -1872,6 +2239,8 @@ packages: snapshots: + '@babel/runtime@7.29.10': {} + '@bomb.sh/args@0.3.1': {} '@bomb.sh/tools@0.8.0(@types/node@22.20.0)(oxc-resolver@11.20.0)(unrun@0.2.39)(vite@8.0.16(@types/node@22.20.0)(jiti@2.7.0)(yaml@2.9.1))': @@ -1922,6 +2291,149 @@ snapshots: '@bomb.sh/tty@0.10.0': {} + '@changesets/apply-release-plan@7.1.1': + dependencies: + '@changesets/config': 3.1.4 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.8.5 + + '@changesets/assemble-release-plan@6.0.10': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.8.5 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.31.1(@types/node@22.20.0)': + dependencies: + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.4 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@22.20.0) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.8.5 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.4': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.4': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.8.5 + + '@changesets/get-release-plan@4.0.16': + dependencies: + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.3.2 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.2.2 + prettier: 2.8.8 + '@codspeed/core@6.1.0': dependencies: axios: 1.21.0 @@ -1972,6 +2484,13 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inquirer/external-editor@1.0.3(@types/node@22.20.0)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 22.20.0 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.6.0': {} @@ -1981,6 +2500,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.10 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.10 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + '@napi-rs/canvas-android-arm64@1.0.11': optional: true @@ -2036,6 +2571,18 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + '@oxc-parser/binding-android-arm-eabi@0.133.0': optional: true @@ -2525,6 +3072,8 @@ snapshots: '@types/estree@1.0.9': {} + '@types/node@12.20.55': {} + '@types/node@22.20.0': dependencies: undici-types: 6.21.0 @@ -2644,6 +3193,18 @@ snapshots: transitivePeerDependencies: - supports-color + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + assertion-error@2.0.1: {} asynckit@0.4.0: {} @@ -2658,6 +3219,14 @@ snapshots: - debug - supports-color + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + cac@7.1.0: {} call-bind-apply-helpers@1.0.2: @@ -2667,10 +3236,18 @@ snapshots: chai@6.3.0: {} + chardet@2.2.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + debug@4.4.3: dependencies: ms: 2.1.3 @@ -2679,8 +3256,14 @@ snapshots: delayed-stream@1.0.0: {} + detect-indent@6.1.0: {} + detect-libc@2.1.2: {} + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + dts-resolver@3.0.0(oxc-resolver@11.20.0): optionalDependencies: oxc-resolver: 11.20.0 @@ -2693,6 +3276,11 @@ snapshots: empathic@2.1.0: {} + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -2710,12 +3298,28 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + esprima@4.0.1: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 expect-type@1.4.0: {} + extendable-error@0.1.7: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + fd-package-json@2.0.0: dependencies: walk-up-path: 4.0.0 @@ -2724,6 +3328,15 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + follow-redirects@1.16.2: {} form-data@4.0.6: @@ -2738,6 +3351,18 @@ snapshots: dependencies: fd-package-json: 2.0.0 + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fsevents@2.3.3: optional: true @@ -2775,8 +3400,23 @@ snapshots: gifenc@1.0.3: optional: true + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -2796,10 +3436,47 @@ snapshots: transitivePeerDependencies: - supports-color + human-id@4.2.2: {} + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + import-without-cache@0.4.1: {} + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + jiti@2.7.0: {} + js-yaml@3.15.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + knip@6.16.1: dependencies: fdir: 6.5.0(picomatch@4.0.7) @@ -2865,12 +3542,25 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + magic-string@1.4.3: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 math-intrinsics@1.1.0: {} + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.52.0: {} mime-types@2.1.35: @@ -2893,6 +3583,8 @@ snapshots: obug@3.0.0: {} + outdent@0.5.0: {} + oxc-parser@0.133.0: dependencies: '@oxc-project/types': 0.133.0 @@ -2986,15 +3678,45 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.89.0 '@oxlint/binding-win32-x64-msvc': 1.89.0 + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + package-manager-detector@1.6.0: {} pako@1.0.11: optional: true + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.7: {} + pify@4.0.1: {} + playwright-core@1.64.1: optional: true @@ -3009,6 +3731,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prettier@2.8.8: {} + proxy-from-env@2.1.0: {} publint@0.3.21: @@ -3018,10 +3742,25 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 + quansync@0.2.11: {} + quansync@1.0.0: {} + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.15.2 + pify: 4.0.1 + strip-bom: 3.0.0 + + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + reusify@1.1.0: {} + rolldown-plugin-dts@0.28.8(oxc-resolver@11.20.0)(rolldown@1.2.15)(typescript@7.0.2): dependencies: dts-resolver: 3.0.0(oxc-resolver@11.20.0) @@ -3100,20 +3839,53 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.15 '@rolldown/binding-win32-x64-msvc': 1.2.15 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + sade@1.8.1: dependencies: mri: 1.2.0 + safer-buffer@2.1.2: {} + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + smol-toml@1.6.1: {} source-map-js@1.2.1: {} + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + stack-trace@1.0.0-pre2: {} std-env@4.3.0: {} + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + strip-json-comments@5.0.3: {} + term-size@2.2.1: {} + tinybench@6.2.2: {} tinyexec@1.3.1: {} @@ -3125,6 +3897,10 @@ snapshots: tinypool@2.2.0: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + tree-kill@1.2.2: {} tsdown@0.23.1(oxc-resolver@11.20.0)(publint@0.3.21)(typescript@7.0.2)(unrun@0.2.39): @@ -3189,6 +3965,8 @@ snapshots: undici-types@6.21.0: {} + universalify@0.1.2: {} + unrun@0.2.39: dependencies: rolldown: 1.0.0-rc.17 @@ -3241,6 +4019,10 @@ snapshots: walk-up-path@4.0.0: {} + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@3.2.1: {} yaml@2.9.1: {}