Reactive island preservation across server-driven morphs
Islands survive hypermedia swaps: morph-node skips hydrated data-sx-island elements when the same island exists in new content. dispose-islands-in skips hydrated islands to prevent premature cleanup. - @client directive: .sx files marked ;; @client send define forms to browser - CSSX client-side: cssxgroup renamed (no hyphen) to avoid isRenderExpr matching it as a custom element — was producing [object HTMLElement] - Island wrappers: div→span to avoid block-in-inline HTML parse breakage - ~sx-header is now a defisland with inline reactive colour cycling - bootstrap_js.py defaults output to shared/static/scripts/sx-browser.js - Deleted stale sx-ref.js (sx-browser.js is the canonical browser build) - Hegelian Synthesis essay: dialectic of hypertext and reactivity - component-source helper handles Island types for docs pretty-printing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,11 @@ _COMPONENT_ENV: dict[str, Any] = {}
|
||||
# client-side localStorage caching.
|
||||
_COMPONENT_HASH: str = ""
|
||||
|
||||
# Raw source of .sx files marked with ;; @client — sent to the browser
|
||||
# alongside component definitions so define forms (functions, data) are
|
||||
# available for client-side evaluation (e.g. cssx colour/spacing functions).
|
||||
_CLIENT_LIBRARY_SOURCES: list[str] = []
|
||||
|
||||
|
||||
def get_component_env() -> dict[str, Any]:
|
||||
"""Return the shared component environment."""
|
||||
@@ -61,7 +66,7 @@ def _compute_component_hash() -> None:
|
||||
"""Recompute _COMPONENT_HASH from all registered Component and Macro definitions."""
|
||||
global _COMPONENT_HASH
|
||||
from .parser import serialize
|
||||
parts = []
|
||||
parts = list(_CLIENT_LIBRARY_SOURCES)
|
||||
for key in sorted(_COMPONENT_ENV):
|
||||
val = _COMPONENT_ENV[key]
|
||||
if isinstance(val, Island):
|
||||
@@ -96,6 +101,8 @@ def load_sx_dir(directory: str) -> None:
|
||||
"""Load all .sx files from a directory and register components.
|
||||
|
||||
Skips boundary.sx — those are parsed separately by the boundary validator.
|
||||
Files starting with ``;; @client`` have their source stored for delivery
|
||||
to the browser (so ``define`` forms are available client-side).
|
||||
"""
|
||||
for filepath in sorted(
|
||||
glob.glob(os.path.join(directory, "**", "*.sx"), recursive=True)
|
||||
@@ -103,7 +110,17 @@ def load_sx_dir(directory: str) -> None:
|
||||
if os.path.basename(filepath) == "boundary.sx":
|
||||
continue
|
||||
with open(filepath, encoding="utf-8") as f:
|
||||
register_components(f.read())
|
||||
source = f.read()
|
||||
if source.lstrip().startswith(";; @client"):
|
||||
# Parse and re-serialize to normalize syntax sugar.
|
||||
# The Python parser accepts ' for quote but the bootstrapped
|
||||
# client parser uses #' — re-serializing emits (quote x).
|
||||
from .parser import parse_all, serialize
|
||||
exprs = parse_all(source)
|
||||
_CLIENT_LIBRARY_SOURCES.append(
|
||||
"\n".join(serialize(e) for e in exprs)
|
||||
)
|
||||
register_components(source)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -150,6 +167,7 @@ def reload_if_changed() -> None:
|
||||
_logger.info("Changed: %s", fp)
|
||||
t0 = time.monotonic()
|
||||
_COMPONENT_ENV.clear()
|
||||
_CLIENT_LIBRARY_SOURCES.clear()
|
||||
# Reload SX libraries first (e.g. z3.sx) so reader macros resolve
|
||||
for cb in _reload_callbacks:
|
||||
cb()
|
||||
@@ -368,9 +386,10 @@ def client_components_tag(*names: str) -> str:
|
||||
params_sx = "(" + " ".join(param_strs) + ")"
|
||||
body_sx = serialize(val.body, pretty=True)
|
||||
parts.append(f"(defmacro {val.name} {params_sx} {body_sx})")
|
||||
if not parts:
|
||||
if not parts and not _CLIENT_LIBRARY_SOURCES:
|
||||
return ""
|
||||
source = "\n".join(parts)
|
||||
all_parts = list(_CLIENT_LIBRARY_SOURCES) + parts
|
||||
source = "\n".join(all_parts)
|
||||
return f'<script type="text/sx" data-components>{source}</script>'
|
||||
|
||||
|
||||
@@ -437,10 +456,12 @@ def components_for_page(page_sx: str, service: str | None = None) -> tuple[str,
|
||||
body_sx = serialize(val.body, pretty=True)
|
||||
parts.append(f"(defmacro {val.name} {params_sx} {body_sx})")
|
||||
|
||||
if not parts:
|
||||
if not parts and not _CLIENT_LIBRARY_SOURCES:
|
||||
return "", ""
|
||||
|
||||
source = "\n".join(parts)
|
||||
# Prepend client library sources (define forms) before component defs
|
||||
all_parts = list(_CLIENT_LIBRARY_SOURCES) + parts
|
||||
source = "\n".join(all_parts)
|
||||
digest = hashlib.sha256(source.encode()).hexdigest()[:12]
|
||||
return source, digest
|
||||
|
||||
|
||||
Reference in New Issue
Block a user