Wire reactive islands end-to-end: live interactive demos on the demo page

- Rebuild sx-browser.js with signals spec module (was missing entirely)
- Register signal functions (signal, deref, effect, computed, etc.) as
  PRIMITIVES so runtime-evaluated SX code in island bodies can call them
- Add reactive deref detection in adapter-dom.sx: (deref sig) in island
  scope creates reactive-text node instead of static text
- Add Island SSR support in html.py (_render_island with data-sx-island)
- Add Island bundling in jinja_bridge.py (defisland defs sent to client)
- Update deps.py to track Island dependencies alongside Component
- Add defisland to _ASER_FORMS in async_eval.py
- Add clear-interval platform primitive (was missing)
- Create four live demo islands: counter, temperature, imperative, stopwatch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 11:57:58 +00:00
parent 50a184faf2
commit 9a0173419a
9 changed files with 855 additions and 220 deletions

View File

@@ -25,7 +25,7 @@ import hashlib
import os
from typing import Any
from .types import NIL, Component, Keyword, Macro, Symbol
from .types import NIL, Component, Island, Keyword, Macro, Symbol
from .parser import parse
import os as _os
if _os.environ.get("SX_USE_REF") == "1":
@@ -64,7 +64,14 @@ def _compute_component_hash() -> None:
parts = []
for key in sorted(_COMPONENT_ENV):
val = _COMPONENT_ENV[key]
if isinstance(val, Component):
if isinstance(val, Island):
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body)
parts.append(f"(defisland ~{val.name} {params_sx} {body_sx})")
elif isinstance(val, Component):
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
@@ -198,7 +205,7 @@ def register_components(sx_source: str) -> None:
# Slightly over-counts per component but safe and avoids re-scanning at request time.
all_classes: set[str] | None = None
for key, val in _COMPONENT_ENV.items():
if key not in existing and isinstance(val, Component):
if key not in existing and isinstance(val, (Component, Island)):
if all_classes is None:
all_classes = scan_classes_from_sx(sx_source)
val.css_classes = set(all_classes)
@@ -307,7 +314,16 @@ def client_components_tag(*names: str) -> str:
from .parser import serialize
parts = []
for key, val in _COMPONENT_ENV.items():
if isinstance(val, Component):
if isinstance(val, Island):
if names and val.name not in names and key.lstrip("~") not in names:
continue
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body, pretty=True)
parts.append(f"(defisland ~{val.name} {params_sx} {body_sx})")
elif isinstance(val, Component):
if names and val.name not in names and key.lstrip("~") not in names:
continue
# Reconstruct defcomp source from the Component object
@@ -365,7 +381,15 @@ def components_for_page(page_sx: str, service: str | None = None) -> tuple[str,
# Also include macros — they're needed for client-side expansion
parts = []
for key, val in _COMPONENT_ENV.items():
if isinstance(val, Component):
if isinstance(val, Island):
if f"~{val.name}" in needed or key in needed:
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body, pretty=True)
parts.append(f"(defisland ~{val.name} {params_sx} {body_sx})")
elif isinstance(val, Component):
if f"~{val.name}" in needed or key in needed:
param_strs = ["&key"] + list(val.params)
if val.has_children:
@@ -412,7 +436,7 @@ def css_classes_for_page(page_sx: str, service: str | None = None) -> set[str]:
classes: set[str] = set()
for key, val in _COMPONENT_ENV.items():
if isinstance(val, Component):
if isinstance(val, (Component, Island)):
if (f"~{val.name}" in needed or key in needed) and val.css_classes:
classes.update(val.css_classes)