@miaskiewicz/turbo-dom 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miaskiewicz/turbo-dom",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Faster, more spec-correct DOM for test runners — native html5ever (Rust/WASM) parser + lazy copy-on-write DOM. A drop-in-style alternative to jsdom/happy-dom for vitest & jest.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -1,7 +1,7 @@
1
1
  // Live collections. Each reads its backing array on every access, so they
2
2
  // reflect mutations immediately — the exact place happy-dom bleeds liveness bugs.
3
3
 
4
- function makeLive(getArray, extra = {}) {
4
+ function makeLive(getArray, extra = {}, tag = 'NodeList') {
5
5
  // Plain-object target: it has no non-configurable own keys, so ownKeys can
6
6
  // report indices + length without tripping the proxy invariant (a function
7
7
  // target carries a non-configurable `prototype`, which made Object.keys(coll)
@@ -21,7 +21,7 @@ function makeLive(getArray, extra = {}) {
21
21
  if (key === 'entries') return () => getArray().entries();
22
22
  if (key === 'keys') return () => getArray().keys();
23
23
  if (key === 'values') return () => getArray()[Symbol.iterator]();
24
- if (key === 'toString') return () => '[object NodeList]';
24
+ if (key === 'toString') return () => `[object ${tag}]`;
25
25
  if (key in extra) return extra[key](getArray());
26
26
  return undefined;
27
27
  }
@@ -64,3 +64,17 @@ export function liveHTMLCollection(getArray) {
64
64
  arr.find((el) => el.getAttribute('id') === name || el.getAttribute('name') === name) ?? null,
65
65
  });
66
66
  }
67
+
68
+ // Live NamedNodeMap for Element.attributes. length/indexing read getArray() on
69
+ // every access, so React 19's `while (attrs.length) removeAttributeNode(attrs[0])`
70
+ // loop sees the map shrink and terminates (a snapshot array never would).
71
+ export function liveNamedNodeMap(el, getArray) {
72
+ return makeLive(getArray, {
73
+ getNamedItem: (arr) => (name) => arr.find((a) => a.name === name) ?? null,
74
+ getNamedItemNS: (arr) => (_ns, name) => arr.find((a) => a.name === name) ?? null,
75
+ setNamedItem: () => (attr) => el.setAttributeNode(attr),
76
+ setNamedItemNS: () => (attr) => el.setAttributeNode(attr),
77
+ removeNamedItem: () => (name) => el.removeAttributeNode({ name }),
78
+ removeNamedItemNS: () => (_ns, name) => el.removeAttributeNode({ name }),
79
+ }, 'NamedNodeMap');
80
+ }
@@ -11,7 +11,7 @@ import {
11
11
  EventTarget, Event, CustomEvent,
12
12
  UIEvent, MouseEvent, KeyboardEvent, FocusEvent,
13
13
  } from './events.mjs';
14
- import { liveNodeList, liveHTMLCollection } from './collections.mjs';
14
+ import { liveNodeList, liveHTMLCollection, liveNamedNodeMap } from './collections.mjs';
15
15
  import { matchesSelector, querySelector as qsel, querySelectorAll as qselAll } from './selectors.mjs';
16
16
  import { serializeInner, serializeOuter } from './html-serialize.mjs';
17
17
  import { CSSStyleSheet, styleSheetList } from './cssom.mjs';
@@ -443,10 +443,15 @@ export class Element extends Node {
443
443
  this.removeAttribute(name); return false;
444
444
  }
445
445
  get attributes() {
446
- return (this.__attrs ?? (this.__attrs = this.__buildAttrs())).map((a) => ({
447
- name: a.name, localName: a.name, value: a.value, prefix: a.prefix || null,
448
- namespaceURI: a.prefix === 'xlink' ? 'http://www.w3.org/1999/xlink' : null,
449
- }));
446
+ // Live NamedNodeMap (memoized for identity): length/indexing read __attrs on
447
+ // every access, so a captured reference shrinks as attributes are removed —
448
+ // React 19's releaseSingletonInstance `while (attrs.length)` loop terminates.
449
+ return this.__attributesMap ?? (this.__attributesMap = liveNamedNodeMap(this, () =>
450
+ (this.__attrs ?? (this.__attrs = this.__buildAttrs())).map((a) => ({
451
+ name: a.name, localName: a.name, value: a.value, prefix: a.prefix || null,
452
+ namespaceURI: a.prefix === 'xlink' ? 'http://www.w3.org/1999/xlink' : null,
453
+ ownerElement: this,
454
+ }))));
450
455
  }
451
456
 
452
457
  get id() { return this.getAttribute('id') || ''; }
@@ -846,9 +851,10 @@ export class Element extends Node {
846
851
  removeAttributeNS(_ns, name) { this.removeAttribute(name); }
847
852
  getAttributeNode(name) { const a = (this.__attrs ?? (this.__attrs = this.__buildAttrs())).find((x) => x.name === name); return a ? { name: a.name, value: a.value, ownerElement: this } : null; }
848
853
  getAttributeNodeNS(_ns, name) { return this.getAttributeNode(name); }
849
- // Attr-node methods are thin shims over the name-based API (no real Attr/NamedNodeMap).
850
- // React 19's releaseSingletonInstance reads node.attributes (a detached array copy) then
851
- // removeAttributeNode(attr)s each — safe because the iterated copy isn't __attrs.
854
+ // Attr-node methods are thin shims over the name-based API (no real Attr class).
855
+ // node.attributes is a LIVE NamedNodeMap (see the getter), so React 19's
856
+ // releaseSingletonInstance `while (attrs.length) removeAttributeNode(attrs[0])`
857
+ // shrinks the map each pass and terminates — not a snapshot that loops forever.
852
858
  setAttributeNode(attr) { const prev = this.getAttributeNode(attr.name); this.setAttribute(attr.name, attr.value); return prev; }
853
859
  setAttributeNodeNS(attr) { return this.setAttributeNode(attr); }
854
860
  removeAttributeNode(attr) { const removed = this.getAttributeNode(attr && attr.name); if (attr && attr.name != null) this.removeAttribute(attr.name); return removed; }