@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.
|
|
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 () =>
|
|
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
|
+
}
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
850
|
-
//
|
|
851
|
-
// removeAttributeNode(
|
|
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; }
|
|
Binary file
|