Make Plasmate produce correct SOMs for JS-rendered pages (React, Vue, Angular, dynamic content).
After this, fix Puppeteer compatibility so browser.newPage() works end-to-end.
- V8 integrated via
rusty_v8, persistent context per page - DOM shim with ~200 lines of JS providing
document,window,console, timers, fetch/XHR stubs - Script extraction from HTML (inline + external with
--fetch-external) - Mutations captured in
__plasmate_mutationsarray document.writeandappendChildmutations injected back into HTML before SOM compile- Timer draining (short
setTimeoutcallbacks executed) - 20/20 real sites passing script execution (284/738 scripts across 20 sites)
- DOM shim doesn't build a real tree -
document.createElementcreates detachedPlasElementobjects butquerySelector/getElementByIdalways returnnull. JS that queries the DOM after creating elements gets nothing back. - No HTML parsing of the source into the shim tree - The shim starts with empty
<html><head><body>. It doesn't reflect the actual page structure. Scripts that dodocument.getElementById('app')ordocument.querySelector('.content')fail. - Mutations don't actually modify the HTML properly - The pipeline re-executes scripts in a fresh context to collect mutations, then does string insertion before
</body>. This misses: DOM node removal, attribute changes, innerHTML replacement, textContent updates on existing nodes. - No async execution model -
fetch()returns empty stubs. React/Vue hydration callsfetch()for data, gets nothing, renders empty. Real JS-rendered pages need network responses to produce content. - SOM is compiled from original HTML, not from the mutated DOM - Even when mutations are captured, the SOM compiler parses the original (or lightly patched) HTML string, not a proper post-JS DOM tree.
Option A: Build a full DOM in Rust, expose it to V8 via bindings
- Most correct approach (what real browsers do)
- Massive effort: need Rust DOM tree + V8 C++ bindings for every DOM API
- 10,000+ lines minimum, weeks of work
Option B: Build a richer DOM tree in JS, serialize back to HTML after execution
- The DOM shim becomes a real (but minimal) DOM implementation in JS
- Parse the source HTML into the JS DOM tree before running page scripts
- After all scripts run, serialize the JS DOM tree back to HTML
- Feed that HTML to the existing SOM compiler
- Much faster to build, leverages existing SOM compiler
- Trade-off: JS DOM won't be 100% spec-compliant, but covers 80-90% of real-world patterns
Decision: Option B. Ship fast, iterate. The JS DOM tree in the shim needs to be good enough for React/Vue/Angular hydration and common dynamic content patterns, not a full W3C DOM implementation.
Replace the current PlasElement/shim with a proper DOM tree implementation in JS:
- Node types: Element, Text, Comment, DocumentFragment with proper
nodeType,parentNode,childNodes,firstChild,lastChild,nextSibling,previousSibling - Tree operations:
appendChild,removeChild,insertBefore,replaceChild,cloneNode(deep)- all maintaining parent/child/sibling pointers - Query methods:
getElementById,querySelector,querySelectorAllwith basic CSS selector support (tag, #id, .class, [attr], combinators) - innerHTML/outerHTML: Parse HTML strings into DOM nodes (mini HTML parser in JS), serialize DOM subtree back to HTML string
- Element specifics:
classList,style.cssText,dataset, form elementvalue/checked/selected - Events:
DOMContentLoaded,load,readystatechangefire at correct lifecycle points - Serialize:
document.documentElement.outerHTMLproduces the full post-JS HTML
Before running any page scripts:
- Parse the fetched HTML into the JS DOM tree (using the innerHTML parser from Phase 1)
- Set
document.title,document.head,document.bodyfrom parsed tree - Set
window.locationfrom the page URL - Inject
<script>elements are already extracted - just need the tree to reflect the rest of the page structure
Replace the empty fetch stub with one that actually makes HTTP requests:
- Expose a Rust function to V8 that does synchronous HTTP fetch (via
reqwest::blockingortokio::runtime::Handle::block_on) - JS
fetch()calls this Rust function, gets real response body - XHR gets the same treatment
- This is critical for React apps that fetch data during hydration/render
Update pipeline.rs:
- After all scripts execute + timers drain, serialize the DOM tree:
document.documentElement.outerHTML - Pass the serialized HTML (which now includes JS-created elements, modified attributes, updated text) to the SOM compiler
- The SOM compiler doesn't need to change - it already handles HTML
Fix the CDP layer for multi-target routing:
- The
setAutoAttach/runIfWaitingForDebuggerinfinite loop - root cause is thatattachedToTargetevents trigger moresetAutoAttachcalls which trigger more events - Each
Target.createTargetneeds a truly independent session with its own page state Page.navigateon a child target should use that target's session, not the parent's- Frame tree needs to be per-target, with proper
executionContextCreatedevents per frame
src/js/runtime.rs- New DOM shim (DOM_SHIM const), Rust-side fetch bridgesrc/js/pipeline.rs- Serialize JS DOM -> HTML -> SOM compilersrc/js/extract.rs- May need to preserve non-script HTML structure for bootstrapsrc/cdp/handler.rs- Fix multi-target session routingsrc/cdp/session.rs- Per-target state isolationsrc/cdp/domains.rs- FixsetAutoAttachevent logic
- Static HTML (no JS) - SOM unchanged
document.write()- content appears in SOMdocument.createElement+appendChild- new elements in SOMinnerHTMLreplacement - updated content in SOMgetElementById+ modify - changes reflected in SOM- React-style hydration pattern (create elements, set textContent from data)
- Timer-based rendering (
setTimeout(render, 0)) DOMContentLoadedhandler that modifies DOM
browser.newPage()creates a working pagepage.goto(url)navigates and returnspage.content()returns HTMLpage.evaluate()runs JSpage.$eval()queries DOM- Multiple pages don't interfere
plasmate fetch https://react-site.comproduces a SOM with the rendered content, not just the empty<div id="root"></div>- Puppeteer smoke test passes:
browser.newPage()->page.goto()->page.content()returns rendered HTML - No regression on existing 184 tests
- Throughput stays under 50ms/page for JS-light pages (acceptable to go to ~200ms for JS-heavy pages that need execution)