The conformance board (tests/afi/test_capability_parity.py) is now fully green: 90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The gaps the prose table used to launder as "Django-only" / "out of scope" are wired, against the pinned-spec model (single-authored spec, byte-identical conformance across languages) — never per-language reimplementation. FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest), WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes (SQLAlchemy projection, same declaration surface as django-readers), Forms (Pydantic schema/validate/submit). Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth= enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT mint+verify and cache-key derivation byte-pinned to the Python reference (cache_keys_pin, token_pin, invalidate_header_pin). TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS backend can feed the codegen — the largest gap), multipart upload, session-init, WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms. Verified in the merged tree: core 25, fastapi 74, django 353/21-skip, mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
3.3 KiB
Python
81 lines
3.3 KiB
Python
"""
|
|
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'<div id="mizan-root">{result.html}</div>'
|
|
f"<script>window.__MIZAN_SSR_DATA__={hydration_json}</script>"
|
|
)
|
|
|
|
def render(self, template_name: str, props: dict[str, Any] | None = None, status_code: int = 200) -> HTMLResponse:
|
|
"""Render the component and return a FastAPI `HTMLResponse`."""
|
|
return HTMLResponse(self.render_to_string(template_name, props), status_code=status_code)
|
|
|
|
def shutdown(self) -> None:
|
|
"""Stop the underlying Bun subprocess."""
|
|
self._bridge.shutdown()
|