Add cross-domain SX navigation with OOB swap
Enable instant cross-subdomain navigation (blog → market, etc.) via sx-get instead of full page reloads. The server prepends missing component definitions to OOB responses so the client can render components from other domains. - sexp.js: send SX-Components header, add credentials for cross-origin fetches to .rose-ash.com/.localhost, process sexp scripts in response before OOB swap - helpers.py: add components_for_request() to diff client/server component sets, update sexp_response() to prepend missing defs - factory.py: add SX-Components to CORS allowed headers, add Access-Control-Allow-Methods - fragments/routes.py: switch nav items from ~blog-nav-item-plain to ~blog-nav-item-link (sx-get enabled) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -278,6 +278,46 @@ def sexp_call(component_name: str, **kwargs: Any) -> str:
|
||||
return "(" + " ".join(parts) + ")"
|
||||
|
||||
|
||||
def components_for_request() -> str:
|
||||
"""Return defcomp source for components the client doesn't have yet.
|
||||
|
||||
Reads the ``SX-Components`` header (comma-separated component names
|
||||
like ``~card,~nav-item``) and returns only the definitions the client
|
||||
is missing. If the header is absent, returns all component defs.
|
||||
"""
|
||||
from quart import request
|
||||
from .jinja_bridge import client_components_tag, _COMPONENT_ENV
|
||||
from .types import Component
|
||||
from .parser import serialize
|
||||
|
||||
loaded_raw = request.headers.get("SX-Components", "")
|
||||
if not loaded_raw:
|
||||
# Client has nothing — send all
|
||||
tag = client_components_tag()
|
||||
if not tag:
|
||||
return ""
|
||||
start = tag.find(">") + 1
|
||||
end = tag.rfind("</script>")
|
||||
return tag[start:end] if start > 0 and end > start else ""
|
||||
|
||||
loaded = set(loaded_raw.split(","))
|
||||
parts = []
|
||||
for key, val in _COMPONENT_ENV.items():
|
||||
if not isinstance(val, Component):
|
||||
continue
|
||||
# Skip components the client already has
|
||||
if f"~{val.name}" in loaded or val.name in loaded:
|
||||
continue
|
||||
# Reconstruct defcomp source
|
||||
param_strs = ["&key"] + list(val.params)
|
||||
if val.has_children:
|
||||
param_strs.extend(["&rest", "children"])
|
||||
params_sexp = "(" + " ".join(param_strs) + ")"
|
||||
body_sexp = serialize(val.body, pretty=True)
|
||||
parts.append(f"(defcomp ~{val.name} {params_sexp} {body_sexp})")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def sexp_response(source_or_component: str, status: int = 200,
|
||||
headers: dict | None = None, **kwargs: Any):
|
||||
"""Return an s-expression wire-format response.
|
||||
@@ -289,13 +329,26 @@ def sexp_response(source_or_component: str, status: int = 200,
|
||||
Or with a component name + kwargs (builds the sexp call)::
|
||||
|
||||
return sexp_response("test-row", nodeid="foo", outcome="passed")
|
||||
|
||||
For SX requests, missing component definitions are prepended as a
|
||||
``<script type="text/sexp" data-components>`` block so the client
|
||||
can process them before rendering OOB content.
|
||||
"""
|
||||
from quart import Response
|
||||
from quart import request, Response
|
||||
if kwargs:
|
||||
source = sexp_call(source_or_component, **kwargs)
|
||||
else:
|
||||
source = source_or_component
|
||||
resp = Response(source, status=status, content_type="text/sexp")
|
||||
|
||||
body = source
|
||||
# For SX requests, prepend missing component definitions
|
||||
if request.headers.get("SX-Request"):
|
||||
comp_defs = components_for_request()
|
||||
if comp_defs:
|
||||
body = (f'<script type="text/sexp" data-components>'
|
||||
f'{comp_defs}</script>\n{body}')
|
||||
|
||||
resp = Response(body, status=status, content_type="text/sexp")
|
||||
if headers:
|
||||
for k, v in headers.items():
|
||||
resp.headers[k] = v
|
||||
|
||||
Reference in New Issue
Block a user