diff --git a/crates/obscura-browser/src/page.rs b/crates/obscura-browser/src/page.rs index 30ef124..e79f7a7 100644 --- a/crates/obscura-browser/src/page.rs +++ b/crates/obscura-browser/src/page.rs @@ -679,6 +679,22 @@ impl Page { None => return, }; + // HTML scripts have an "already started" flag. Mark every + // parser-discovered script before running page code so React/Next + // hydration can move or hoist those nodes without appendChild + // executing them a second time. + if let Some(js) = &mut self.js { + let ids = all_scripts + .iter() + .map(|script| script.nid.to_string()) + .collect::>() + .join(","); + let _ = js.execute_script( + "", + &format!("globalThis.__markParserScripts([{}]);", ids), + ); + } + let mut regular = Vec::new(); let mut deferred = Vec::new(); let mut async_scripts = Vec::new(); diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index b15060d..7146e97 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -16,7 +16,7 @@ '__obscura_objects', '__obscura_oid', '__obscura_ua', '__obscura_platform', '__obscura_ua_platform', '__obscura_ua_platform_version', '__obscura_stealth', '__obscura_markTrusted', - '__obscura_hw', '__obscura_mem', + '__obscura_hw', '__obscura_mem', '__markParserScripts', '__documentReadyState__', '__currentUrl', // internal helpers (var-declared throughout the file) '__processDynScriptQueue', '_decodeDataScriptUrl', '_markNative', '_fpRand', '_fpNoise', @@ -45,7 +45,7 @@ // the later `globalThis.X = X` assignments only update the value. 'Node', 'Element', 'Document', 'DocumentFragment', 'DocumentType', 'Text', 'Comment', 'CDATASection', 'ProcessingInstruction', 'CharacterData', - 'CSSStyleDeclaration', 'DOMTokenList', 'Screen', 'NetworkInformation', + 'CSSStyleDeclaration', 'DOMTokenList', 'NamedNodeMap', 'Screen', 'NetworkInformation', 'MessageChannel', 'MessagePort', 'CustomElementRegistry', 'XMLHttpRequestEventTarget', 'HTMLMediaElement', 'HTMLVideoElement', 'HTMLAudioElement', 'WebGL2RenderingContext', @@ -227,6 +227,14 @@ function _decodeDataScriptUrl(url) { } return new TextDecoder().decode(new Uint8Array(bytes)); } +// A script element executes at most once. Parser-discovered scripts are marked +// before page execution starts, and dynamically inserted scripts are marked +// when they are queued. Framework hydration may move existing