#39)
Adds content negotiation to the Litestar adapter so plain htmx (hx-post/
hx-swap, and the SSE extension) can drive components without the
proprietary component-client.js: when a request carries the HX-Request
header, component_endpoint and stream_component_endpoint now return the
rendered HTML fragment directly instead of the JSON envelope; the JSON
envelope remains the default for backward compatibility.
Also fixes docs/site-pages/litestar-guide.html, which referenced a
nonexistent Jinja2Renderer instead of the real JinjaxRenderer(catalog)
API, and adds jinjax to the litestar extra in pyproject.toml so
JinjaxRenderer is actually importable for Litestar consumers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pi1LT1PQ8qo9GcyLeDLiut
Summary
Closes #39. Two related gaps in the Litestar adapter reported while integrating this framework into a Litestar+JinjaX app with a stateful htmx-swapped component.
Scope items
docs/site-pages/litestar-guide.html(lines ~625, ~884, ~946) referenced a nonexistentJinja2Rendererimported fromcomponent_framework.adapters.jinjax_renderer. Replaced all three occurrences with the realJinjaxRenderer(catalog)API (constructed from ajinjax.Cataloginstance, matching the working pattern already used inexamples/fastapi_example.py). The third occurrence (inside thetests/test_litestar_adapter.pycode sample) had an unused/dead import — removed, since the real test file usesMockRendererand never imported this at all.adapters/litestar.py'scomponent_endpointandstream_component_endpointpreviously always returned the JSON envelope ({"html", "state", "component_id", "slots"}), which breaks a plainhx-swap(raw JSON would splat into the DOM).HX-Requestheader content negotiation.component_endpoint: when the request carriesHX-Request, returnsResponse(content=result["html"], media_type="text/html")— the rendered fragment, ready forhx-swap. Without the header, behavior is byte-for-byte unchanged (JSON envelope, default).stream_component_endpoint: whenHX-Requestis present, each SSE frame is now formatted as raw HTML lines (data: <line>per htmx's SSE-extension convention) instead of a JSON-wrapped frame, sosse-swapworks directly. Without the header, frames remain the existing JSON-per-frame format (regression-covered).jinjaxadded to thelitestarextra inpyproject.toml(litestar = ["litestar>=2.0", "jinja2>=3.1", "jinjax>=0.41"]) soJinjaxRendereris actually importable once youpip install component-framework[litestar].Tests
TDD red→green. Added to the existing (previously untracked-by-justfile)
tests/test_litestar_adapter.pyandtests/test_litestar_sse.py:TestHtmxContentNegotiation: HX-Request → HTML fragment (mount + event dispatch); no header → JSON envelope unchanged.TestHtmxStreamContentNegotiation: HX-Request → HTML-per-frame SSE; no header → JSON-per-frame SSE unchanged.All three new/failing assertions were confirmed to fail for the right reason (plain JSON returned regardless of header) before the implementation was added.
Verification
ruff check .— all checks passedruff format --check .— 82 files already formattedpytest tests/— 482 passed (full suite, not just new tests)ty check src/— exit 0; 17 pre-existing warn-level diagnostics unrelated to this change (django_websocket.py, composition.py, form.py, testing.py — none inlitestar.py)Out of scope (noted, not touched)
While reading
docs/site-pages/litestar-guide.htmlI noticed it also referencescomponent_stream_endpointand importscomponent_websocket_endpointfromcomponent_framework.adapters.litestar(the real names/locations arestream_component_endpointinadapters/litestar.py, andcomponent_websocket_endpointactually lives inadapters/litestar_websocket.py, notadapters/litestar.py). This is a separate doc-accuracy issue from the one filed here — flagging as a follow-up rather than folding it into this diff.🤖 Generated with Claude Code
https://claude.ai/code/session_01Pi1LT1PQ8qo9GcyLeDLiut