From 183c444a56d9845c4d0617e906c8c2bf716f1a12 Mon Sep 17 00:00:00 2001 From: Ben Younes Date: Sun, 24 May 2026 19:36:19 +0200 Subject: [PATCH] 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. --- crates/obscura-dom/src/tree.rs | 34 ++++++++++++++++++++++++++++++ crates/obscura-js/js/bootstrap.js | 35 ++++++++++++++++++++++++++++--- crates/obscura-js/src/ops.rs | 2 +- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/crates/obscura-dom/src/tree.rs b/crates/obscura-dom/src/tree.rs index 2dd0f56..0b93abb 100644 --- a/crates/obscura-dom/src/tree.rs +++ b/crates/obscura-dom/src/tree.rs @@ -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 = { + let descendants = self.descendants(node_id); + let inner = self.inner.borrow(); + let mut ids: Vec = 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); diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index 0b19fcb..2d85610 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -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, diff --git a/crates/obscura-js/src/ops.rs b/crates/obscura-js/src/ops.rs index 73af2cc..c3e807f 100644 --- a/crates/obscura-js/src/ops.rs +++ b/crates/obscura-js/src/ops.rs @@ -193,7 +193,7 @@ fn op_dom(state: &OpState, #[string] cmd: String, #[string] arg1: String, #[stri } "remove_child" => { let child = arg1.parse::().unwrap_or(0); - dom.detach(NodeId::new(child)); + dom.remove_child(NodeId::new(child)); "true".into() } "insert_before" => {