""" SSR render path — FastAPI adapter surface over the shared Bun bridge. The SSR subprocess lifecycle and JSON-RPC wire protocol live in `mizan_core.ssr.SSRBridge` (framework-agnostic). FastAPI has no template-engine backend, so instead of Django's `MizanTemplates` veneer this exposes an `SSRRenderer` whose `.render(...)` calls the same bridge — `renderToString` runs in the persistent Bun worker — and returns an `HTMLResponse` with the rendered markup plus the hydration payload the client reads on mount. Usage: from mizan_fastapi.ssr import SSRRenderer ssr = SSRRenderer(worker="path/to/mizan-ssr/src/worker.tsx", dirs=["frontend"]) @app.get("/profile/{user_id}") async def profile(user_id: int): return ssr.render("components/Profile.tsx", {"user_id": user_id}) `render` resolves the template name to an absolute file path against `dirs` (parity with Django's `DIRS`), then renders the component's default export. The hydration wrapping matches the Django backend byte-for-byte so the same client bundle hydrates either server. """ from __future__ import annotations import json import os from typing import Any from fastapi.responses import HTMLResponse from mizan_core.ssr import SSRBridge class SSRRenderer: """Render React `.tsx`/`.jsx` files via the shared Bun SSR bridge. One renderer owns one persistent `SSRBridge`. Thread-safe (the bridge serializes worker I/O); a single renderer can be shared across the app. """ def __init__(self, worker: str, dirs: list[str] | None = None, timeout: float = 5.0) -> None: self._dirs = list(dirs or []) self._bridge = SSRBridge(worker_path=worker, timeout=timeout) def _resolve(self, template_name: str) -> str: """Resolve a template name to an absolute file path against `dirs`. An already-absolute, existing path is used directly; otherwise each `dirs` entry is tried in order (parity with Django's `DIRS` resolution). """ if os.path.isabs(template_name) and os.path.isfile(template_name): return template_name for dir_path in self._dirs: candidate = os.path.join(dir_path, template_name) if os.path.isfile(candidate): return os.path.abspath(candidate) raise FileNotFoundError( f"SSR component '{template_name}' not found in dirs={self._dirs!r}" ) def render_to_string(self, template_name: str, props: dict[str, Any] | None = None) -> str: """Render the component to an HTML string (markup + hydration script).""" props = dict(props or {}) result = self._bridge.render(self._resolve(template_name), props) hydration_json = json.dumps(props, sort_keys=True, default=str) return ( f'