fix(js,dom): add Storage global, CharacterData ChildNode methods, fix remove/isConnected (#176)

fix(dom): add missing DOM/JS shims for modern frameworks

Closes #163.

Adds missing DOM and JS shims required by modern frameworks (Svelte 5, 
Vue, Lit) and consent-management SDKs to execute without errors:

- Define the `Storage` global constructor to resolve `ReferenceError`s 
  and fix `Storage.prototype` access.
- Apply standard ChildNode methods (`before`, `after`, `replaceWith`, 
  `remove`) to `CharacterData.prototype`. This prevents `TypeError`s when 
  frameworks call these on Text or Comment nodes during hydration.
- Update `Element.prototype.isConnected` to perform a genuine parent-walk 
  rather than returning a hardcoded `true`.
- Fix `Element.prototype.remove` to properly clean up the internal ID 
  index so removed elements are no longer queryable.
- Remove outdated no-op stubs on the `Element` class that were shadowing 
  the proper polyfill implementations.
This commit is contained in:
Ben Younes authored and GitHub committed 2026-05-24 17:36:19 +00:00
1 parent 10810e250c
commit 183c444a56
3 files changed
+67 -4

No files matched your search

+34
View File
@@ -322,6 +322,40 @@ impl DomTree {
}
}
/// Detach a node from its parent AND remove it (and all descendants)
/// from the id-index so that `getElementById` no longer returns them.
/// Unlike `remove()`, this does NOT free the nodes — the JS side may
/// still hold references to the wrappers.
pub fn remove_child(&self, node_id: NodeId) {
// Collect all id attribute values in the subtree. We snapshot them
// before detaching so `get_attribute` can still see the tree.
let ids_to_remove: Vec<String> = {
let descendants = self.descendants(node_id);
let inner = self.inner.borrow();
let mut ids: Vec<String> = Vec::new();
if let Some(Some(node)) = inner.nodes.get(node_id.index()) {
if let Some(id_val) = node.get_attribute("id") {
ids.push(id_val.to_string());
}
}
for desc_id in &descendants {
if let Some(Some(node)) = inner.nodes.get(desc_id.index()) {
if let Some(id_val) = node.get_attribute("id") {
ids.push(id_val.to_string());
}
}
}
ids
};
self.detach(node_id);
let mut inner = self.inner.borrow_mut();
for id_str in &ids_to_remove {
inner.id_index.remove(id_str);
}
}
pub fn remove(&self, node_id: NodeId) {
self.detach(node_id);
let descendants = self.descendants(node_id);
+32 -3
View File
@@ -758,8 +758,15 @@ class Element extends Node {
};
}
getAnimations() { return []; }
get isConnected() { return true; }
after() {} before() {} remove() { if (this.parentNode) this.parentNode.removeChild(this); }
get isConnected() {
var node = this;
while (node) {
if (node.nodeType === 9) return true;
node = node.parentNode;
}
return false;
}
remove() { if (this.parentNode) this.parentNode.removeChild(this); }
append(...nodes) { for (const n of nodes) { if (typeof n === "string") this.appendChild(document.createTextNode(n)); else this.appendChild(n); } }
prepend() {}
}
@@ -1678,6 +1685,15 @@ if (!Element.prototype.after) {
_markNative(Element.prototype.after);
}
// ChildNode mixin: also mix before/after/replaceWith/remove into
// CharacterData.prototype (covers Text, Comment, ProcessingInstruction).
// These are the same implementations as Element.prototype — frameworks
// (Svelte 5, Vue, Lit) anchor on Comment/Text nodes and call these methods.
if (!CharacterData.prototype.before) CharacterData.prototype.before = Element.prototype.before;
if (!CharacterData.prototype.after) CharacterData.prototype.after = Element.prototype.after;
if (!CharacterData.prototype.replaceWith) CharacterData.prototype.replaceWith = Element.prototype.replaceWith;
if (!CharacterData.prototype.remove) CharacterData.prototype.remove = Element.prototype.remove;
if (!('isConnected' in Node.prototype)) {
Object.defineProperty(Node.prototype, 'isConnected', {
get() {
@@ -2054,7 +2070,15 @@ globalThis.crypto = globalThis.crypto || { getRandomValues(arr) { for(let i=0;i<
globalThis.structuredClone = globalThis.structuredClone || ((v) => JSON.parse(JSON.stringify(v)));
globalThis.reportError = globalThis.reportError || ((e) => console.error(e));
const _mkStore = () => { const s={}; return { getItem:k=>s[k]??null, setItem:(k,v)=>{s[k]=String(v);}, removeItem:k=>{delete s[k];}, clear:()=>{for(const k in s)delete s[k];}, get length(){return Object.keys(s).length;}, key:i=>Object.keys(s)[i]??null }; };
globalThis.Storage = function Storage() {};
Storage.prototype.getItem = function(k) { return (this._data && this._data[k]) ?? null; };
Storage.prototype.setItem = function(k, v) { if (this._data) this._data[k] = String(v); };
Storage.prototype.removeItem = function(k) { if (this._data) delete this._data[k]; };
Storage.prototype.clear = function() { if (this._data) for (var k in this._data) delete this._data[k]; };
Object.defineProperty(Storage.prototype, 'length', { get: function() { return this._data ? Object.keys(this._data).length : 0; } });
Storage.prototype.key = function(i) { return this._data ? Object.keys(this._data)[i] ?? null : null; };
const _mkStore = () => { var s = Object.create(Storage.prototype); s._data = {}; return s; };
globalThis.localStorage = _mkStore();
globalThis.sessionStorage = _mkStore();
@@ -2146,16 +2170,21 @@ globalThis.Range = class Range { setStart(){} setEnd(){} collapse(){} selectNode
Element.prototype.cloneNode, Element.prototype.attachShadow,
Element.prototype.insertAdjacentHTML, Element.prototype.scrollIntoView,
Element.prototype.append, Element.prototype.remove,
Element.prototype.before, Element.prototype.after, Element.prototype.replaceWith,
Element.prototype.getContext, Element.prototype.toDataURL, Element.prototype.toBlob,
Node.prototype.appendChild, Node.prototype.removeChild,
Node.prototype.replaceChild, Node.prototype.insertBefore,
Node.prototype.contains, Node.prototype.hasChildNodes, Node.prototype.cloneNode,
CharacterData.prototype.before, CharacterData.prototype.after,
CharacterData.prototype.replaceWith, CharacterData.prototype.remove,
Document.prototype.getElementById, Document.prototype.querySelector,
Document.prototype.querySelectorAll, Document.prototype.getElementsByTagName,
Document.prototype.createElement, Document.prototype.createElementNS,
Document.prototype.createTextNode, Document.prototype.createComment,
Document.prototype.createDocumentFragment, Document.prototype.createEvent,
Document.prototype.hasFocus,
Storage, Storage.prototype.getItem, Storage.prototype.setItem,
Storage.prototype.removeItem, Storage.prototype.clear, Storage.prototype.key,
Notification, Notification.requestPermission,
window.chrome?.csi, window.chrome?.loadTimes,
MutationObserver, ResizeObserver, IntersectionObserver, PerformanceObserver,
+1 -1
View File
@@ -193,7 +193,7 @@ fn op_dom(state: &OpState, #[string] cmd: String, #[string] arg1: String, #[stri
}
"remove_child" => {
let child = arg1.parse::<u32>().unwrap_or(0);
dom.detach(NodeId::new(child));
dom.remove_child(NodeId::new(child));
"true".into()
}
"insert_before" => {