Files
ditto.site/.opencode/scripts/dump-rendered.py
T
2026-06-29 15:11:48 -07:00

119 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""
dump-rendered.py — Open a URL (typically the local dev server) and dump its DOM
using the same __CLONE_DUMP_COMPUTED__ walker the capture pipeline uses for the
source. This produces a directly-comparable structural snapshot the validate
agent can diff against the captured original.
Usage:
dump-rendered.py --url http://localhost:3000/ --output workspace/rendered/1280-step-00.json \
--viewport 1280 --scroll-y 0
Dependencies: playwright.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from pathlib import Path
try:
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
except ImportError:
print(json.dumps({"error": "Missing dependencies. pip install playwright && playwright install chromium"}), file=sys.stderr)
sys.exit(1)
def load_init_hooks(hooks_dir: Path) -> list[str]:
sources: list[str] = []
# We only need computed-styles + section-scan for rendered-DOM purposes.
for name in ("bootstrap.js", "computed-styles.js", "section-scan.js"):
p = hooks_dir / name
if p.exists():
sources.append(p.read_text())
return sources
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--url", required=True)
parser.add_argument("--output", required=True, help="Path to write the rendered DOM JSON")
parser.add_argument("--viewport", type=int, default=1280)
parser.add_argument("--viewport-height", type=int, default=None, help="Default: 16:9 of viewport width")
parser.add_argument("--scroll-y", type=int, default=0)
parser.add_argument("--reduce-motion", action="store_true")
parser.add_argument("--settle-ms", type=int, default=800)
args = parser.parse_args()
output = Path(args.output).resolve()
output.parent.mkdir(parents=True, exist_ok=True)
height = args.viewport_height or round(args.viewport * 9 / 16)
script_dir = Path(__file__).resolve().parent
init_sources = load_init_hooks(script_dir / "init-hooks")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=["--disable-blink-features=AutomationControlled"])
ctx = browser.new_context(viewport={"width": args.viewport, "height": height})
for src in init_sources:
ctx.add_init_script(src)
page = ctx.new_page()
try:
page.goto(args.url, wait_until="networkidle", timeout=30_000)
except PWTimeout:
page.goto(args.url, wait_until="domcontentloaded", timeout=30_000)
if args.reduce_motion:
try:
page.evaluate("document.documentElement.classList.add('reduce-motion')")
except Exception:
pass
try:
page.evaluate("() => document.fonts.ready")
except Exception:
pass
try:
page.wait_for_function("window.__CLONE_READY__ === true", timeout=5_000)
except Exception:
pass
if args.scroll_y:
page.evaluate(f"window.scrollTo({{ top: {args.scroll_y}, behavior: 'instant' }})")
time.sleep(args.settle_ms / 1000.0)
snapshot = page.evaluate(
"() => (typeof window.__CLONE_DUMP_COMPUTED__ === 'function') ? window.__CLONE_DUMP_COMPUTED__() : null"
)
sections = page.evaluate(
"() => (typeof window.__CLONE_LIST_SECTIONS__ === 'function') ? window.__CLONE_LIST_SECTIONS__() : []"
) or []
if snapshot is None:
raise SystemExit("dump-rendered: __CLONE_DUMP_COMPUTED__ unavailable — init hooks failed to load")
output.write_text(json.dumps(snapshot, separators=(",", ":")))
sections_path = output.with_name(output.stem + "-sections.json")
sections_path.write_text(json.dumps(sections, indent=2))
page.close()
ctx.close()
browser.close()
print(json.dumps({
"status": "ok",
"output": str(output),
"sections_path": str(sections_path),
"section_count": len(sections),
}))
if __name__ == "__main__":
main()