Source code for litestar_vite.fragments._jinja

"""Jinja2 template integration for rendering Vite component fragments."""

import functools
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Literal, cast

import anyio
import markupsafe
from litestar.handlers import HTTPRouteHandler
from litestar.response import Template
from litestar.response.base import ASGIResponse

if TYPE_CHECKING:
    from collections.abc import Awaitable, Callable

    from litestar import Litestar
    from litestar.types import ASGIApp, Receive, Scope, Send

__all__ = ("render_fragment", "vite_fragment")


def _resolve_vite_plugin(context: Mapping[str, Any]) -> Any:
    """Resolve VitePlugin instance from template context.

    Args:
        context: The template context.

    Returns:
        The registered VitePlugin instance, or None.
    """
    from litestar_vite.plugin import VitePlugin

    request = context.get("request")
    if request is None:
        return None
    app = getattr(request, "app", None)
    if app is None:
        return None
    try:
        return app.plugins.get(VitePlugin)
    except (KeyError, AttributeError):
        pass
    try:
        return app.plugins.get("VitePlugin")
    except (KeyError, AttributeError):
        return None


[docs] def vite_fragment( context: Mapping[str, Any], /, component: str, props: dict[str, Any] | None = None, mode: Literal["static", "island"] = "static", **kwargs: Any, ) -> markupsafe.Markup: """Render a component fragment within a Jinja2 template. Merges explicit props with keyword arguments passed to the template callable. Example in Jinja template: {{ vite_fragment("components/UserProfile.vue", user=user) }} {{ vite_fragment("components/Counter.tsx", props={"initial": 0}, mode="island") }} Args: context: The Jinja2 template context containing the request. component: Relative path to the component entrypoint. props: Optional dictionary of component props. mode: Rendering mode ('static' for HTML only or 'island' for interactive). **kwargs: Additional props passed as keyword arguments. Returns: Markup-safe rendered HTML string with scoped styles. """ vite_plugin = _resolve_vite_plugin(context) if vite_plugin is None: return markupsafe.Markup("") combined_props = {**(props or {}), **kwargs} if hasattr(vite_plugin, "render_fragment_sync"): rendered = vite_plugin.render_fragment_sync(component, props=combined_props, mode=mode) elif hasattr(vite_plugin, "fragment_engine"): rendered = vite_plugin.fragment_engine.render_fragment_sync(component, props=combined_props, mode=mode) else: rendered = "" return markupsafe.Markup(rendered)
render_fragment = vite_fragment class _DeferredTemplateResponse(ASGIResponse): """Hold a synchronous template render until its converter can await it.""" __slots__ = ("_render_template",) def __init__(self, render_template: "Callable[[], ASGIResponse]") -> None: super().__init__() self._render_template = render_template async def render_async(self) -> ASGIResponse: return await anyio.to_thread.run_sync(self._render_template) async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: response = await self.render_async() await response(scope, receive, send) class _AsyncFragmentTemplate(Template): """Allow the response converter to render Jinja in an AnyIO worker thread.""" __slots__ = () def to_asgi_response(self, *args: Any, **kwargs: Any) -> ASGIResponse: return _DeferredTemplateResponse( functools.partial(super().to_asgi_response, *args, **kwargs) # pyright: ignore[reportUnknownMemberType] ) def _wrap_template_converter(handler: HTTPRouteHandler) -> None: """Adapt Litestar's synchronous Template conversion at its async boundary. The response converter runs inside Litestar's dependency cleanup scope, after the handler and its after_request hook and before caching/compression. A middleware or deferred ASGI render would run too late. Keep the private converter mapping access here so this integration has one compatibility seam. """ original = cast("Callable[..., Awaitable[ASGIApp]]", handler.get_response_handler(is_response_type_data=True)) if getattr(original, "_vite_fragment_template_wrapped", False): return @functools.wraps(original) async def convert(data: Any, **kwargs: Any) -> "ASGIApp": if type(data) is Template: template = _AsyncFragmentTemplate( template_name=data.template_name, template_str=data.template_str, context=data.context, background=data.background, cookies=data.cookies, encoding=data.encoding, headers=data.headers, media_type=data.media_type, status_code=data.status_code or 200, ) template.status_code = data.status_code template.response_type_encoders = data.response_type_encoders data = template response = await original(data=data, **kwargs) if isinstance(response, _DeferredTemplateResponse): return await response.render_async() return response convert._vite_fragment_template_wrapped = True # type: ignore[attr-defined] handler._response_handler_mapping["response_type_handler"] = convert # pyright: ignore[reportPrivateUsage] def wrap_fragment_template_handlers(app: "Litestar") -> None: """Enable asynchronous fragment rendering for standard Template responses.""" for route in app.routes: for handler in getattr(route, "route_handlers", ()): if isinstance(handler, HTTPRouteHandler): _wrap_template_converter(handler)