|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import hashlib |
| 4 | +import re |
| 5 | +from dataclasses import dataclass |
| 6 | +from datetime import datetime, timezone |
| 7 | +from email.utils import formatdate |
| 8 | +from pathlib import Path |
| 9 | + |
| 10 | +from typing_extensions import Unpack |
| 11 | + |
| 12 | +from reactpy import html |
| 13 | +from reactpy.asgi.executors.standalone import ReactPy, ReactPyApp |
| 14 | +from reactpy.asgi.middleware import ReactPyMiddleware |
| 15 | +from reactpy.asgi.utils import vdom_head_to_html |
| 16 | +from reactpy.pyscript.utils import pyscript_component_html, pyscript_setup_html |
| 17 | +from reactpy.types import ( |
| 18 | + ReactPyConfig, |
| 19 | + VdomDict, |
| 20 | +) |
| 21 | + |
| 22 | + |
| 23 | +class ReactPyCSR(ReactPy): |
| 24 | + def __init__( |
| 25 | + self, |
| 26 | + *component_paths: str | Path, |
| 27 | + extra_py: tuple[str, ...] = (), |
| 28 | + extra_js: dict | str = "", |
| 29 | + pyscript_config: dict | str = "", |
| 30 | + root_name: str = "root", |
| 31 | + initial: str | VdomDict = "", |
| 32 | + http_headers: dict[str, str] | None = None, |
| 33 | + html_head: VdomDict | None = None, |
| 34 | + html_lang: str = "en", |
| 35 | + **settings: Unpack[ReactPyConfig], |
| 36 | + ) -> None: |
| 37 | + """Variant of ReactPy's standalone that only performs Client-Side Rendering (CSR). |
| 38 | +
|
| 39 | + Parameters: |
| 40 | + ... |
| 41 | + """ |
| 42 | + ReactPyMiddleware.__init__( |
| 43 | + self, app=ReactPyAppCSR(self), root_components=[], **settings |
| 44 | + ) |
| 45 | + if not component_paths: |
| 46 | + raise ValueError("At least one component file path must be provided.") |
| 47 | + self.component_paths = tuple(str(path) for path in component_paths) |
| 48 | + self.extra_py = extra_py |
| 49 | + self.extra_js = extra_js |
| 50 | + self.pyscript_config = pyscript_config |
| 51 | + self.root_name = root_name |
| 52 | + self.initial = initial |
| 53 | + self.extra_headers = http_headers or {} |
| 54 | + self.dispatcher_pattern = re.compile(f"^{self.dispatcher_path}?") |
| 55 | + self.html_head = html_head or html.head() |
| 56 | + self.html_lang = html_lang |
| 57 | + |
| 58 | + |
| 59 | +@dataclass |
| 60 | +class ReactPyAppCSR(ReactPyApp): |
| 61 | + """ReactPy's standalone ASGI application for Client-Side Rendering (CSR).""" |
| 62 | + |
| 63 | + parent: ReactPyCSR |
| 64 | + _index_html = "" |
| 65 | + _etag = "" |
| 66 | + _last_modified = "" |
| 67 | + |
| 68 | + def render_index_html(self) -> None: |
| 69 | + """Process the index.html and store the results in this class.""" |
| 70 | + head_content = vdom_head_to_html(self.parent.html_head) |
| 71 | + pyscript_setup = pyscript_setup_html( |
| 72 | + extra_py=self.parent.extra_py, |
| 73 | + extra_js=self.parent.extra_js, |
| 74 | + config=self.parent.pyscript_config, |
| 75 | + ) |
| 76 | + pyscript_component = pyscript_component_html( |
| 77 | + file_paths=self.parent.component_paths, |
| 78 | + initial=self.parent.initial, |
| 79 | + root=self.parent.root_name, |
| 80 | + ) |
| 81 | + head_content.replace("</head>", f"{pyscript_setup}</head>") |
| 82 | + |
| 83 | + self._index_html = ( |
| 84 | + "<!doctype html>" |
| 85 | + f'<html lang="{self.parent.html_lang}">' |
| 86 | + f"{head_content}" |
| 87 | + "<body>" |
| 88 | + f"{pyscript_component}" |
| 89 | + "</body>" |
| 90 | + "</html>" |
| 91 | + ) |
| 92 | + self._etag = f'"{hashlib.md5(self._index_html.encode(), usedforsecurity=False).hexdigest()}"' |
| 93 | + self._last_modified = formatdate( |
| 94 | + datetime.now(tz=timezone.utc).timestamp(), usegmt=True |
| 95 | + ) |
0 commit comments