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

@@ -27,7 +27,7 @@ from __future__ import annotations
import contextvars
from typing import Any
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
from .types import Component, Island, Keyword, Lambda, Macro, NIL, Symbol
from .evaluator import _eval as _raw_eval, _call_component as _raw_call_component, _expand_macro, _trampoline
def _eval(expr, env):
@@ -411,6 +411,64 @@ def _render_component(comp: Component, args: list, env: dict[str, Any]) -> str:
return _render(comp.body, local)
def _render_island(island: Island, args: list, env: dict[str, Any]) -> str:
"""Render an island as static HTML with hydration attributes.
Produces: <div data-sx-island="name" data-sx-state='{"k":"v",...}'>body HTML</div>
The client hydrates this into a reactive island.
"""
import json as _json
kwargs: dict[str, Any] = {}
children: list[Any] = []
i = 0
while i < len(args):
arg = args[i]
if isinstance(arg, Keyword) and i + 1 < len(args):
kwargs[arg.name] = _eval(args[i + 1], env)
i += 2
else:
children.append(arg)
i += 1
local = dict(island.closure)
local.update(env)
for p in island.params:
if p in kwargs:
local[p] = kwargs[p]
else:
local[p] = NIL
if island.has_children:
local["children"] = _RawHTML("".join(_render(c, env) for c in children))
body_html = _render(island.body, local)
# Serialize state for hydration — only keyword args
state = {}
for k, v in kwargs.items():
if isinstance(v, (str, int, float, bool)):
state[k] = v
elif v is NIL or v is None:
state[k] = None
elif isinstance(v, list):
state[k] = v
elif isinstance(v, dict):
state[k] = v
else:
state[k] = str(v)
state_json = _escape_attr(_json.dumps(state, separators=(",", ":"))) if state else ""
island_name = _escape_attr(island.name)
parts = [f'<div data-sx-island="{island_name}"']
if state_json:
parts.append(f' data-sx-state="{state_json}"')
parts.append(">")
parts.append(body_html)
parts.append("</div>")
return "".join(parts)
def _render_list(expr: list, env: dict[str, Any]) -> str:
"""Render a list expression — could be an HTML element, special form,
component call, or data list."""
@@ -464,9 +522,11 @@ def _render_list(expr: list, env: dict[str, Any]) -> str:
if name in HTML_TAGS:
return _render_element(name, expr[1:], env)
# --- Component (~prefix) → render-aware component call ------------
# --- Component/Island (~prefix) → render-aware call ----------------
if name.startswith("~"):
val = env.get(name)
if isinstance(val, Island):
return _render_island(val, expr[1:], env)
if isinstance(val, Component):
return _render_component(val, expr[1:], env)
# Fall through to evaluation