Fix defisland→Component bug in jinja_bridge; add island reactivity test

jinja_bridge.py was creating Component objects for both defcomp AND
defisland forms. Islands need Island objects so the serializer emits
defisland (not defcomp) in the client component bundle. Without this,
client-side islands don't get data-sx-island attributes, hydration
fails, and all reactive signals (colour cycling, stepper) stop working.

Add Playwright test: islands hydrate, stepper buttons update count,
reactive colour cycling works on click.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 15:48:19 +00:00
parent 7434de53a6
commit e887c0d978
2 changed files with 61 additions and 3 deletions

View File

@@ -396,6 +396,40 @@ def load_handler_dir(directory: str, service_name: str) -> None:
_load(directory, service_name)
def _parse_defcomp_params(param_form: list) -> tuple[list[str], bool]:
"""Extract keyword param names and has_children from a defcomp param list.
Handles: (&key p1 p2 &rest children), (&key (p1 :as type) &rest children),
(p1 p2), () etc.
Returns (param_names, has_children).
"""
if not isinstance(param_form, list):
return [], False
params: list[str] = []
has_children = False
in_key = False
i = 0
while i < len(param_form):
item = param_form[i]
if isinstance(item, Symbol):
sname = item.name
if sname == "&key":
in_key = True
elif sname == "&rest":
has_children = True
i += 1 # skip the rest-param name (e.g. 'children')
else:
params.append(sname)
elif isinstance(item, list):
# Typed param: (name :as type)
if item and isinstance(item[0], Symbol):
params.append(item[0].name)
i += 1
return params, has_children
def register_components(sx_source: str, *, _defer_postprocess: bool = False) -> None:
"""Parse and evaluate s-expression component definitions into the
shared environment.
@@ -410,7 +444,7 @@ def register_components(sx_source: str, *, _defer_postprocess: bool = False) ->
existing = set(_COMPONENT_ENV.keys())
# Evaluate definitions — OCaml kernel handles everything.
# Python-side component registry is populated minimally for CSS/deps.
# Python-side component registry is populated with parsed params for CSS/deps.
exprs = parse_all(sx_source)
for expr in exprs:
if (isinstance(expr, list) and expr and isinstance(expr[0], Symbol)
@@ -420,9 +454,11 @@ def register_components(sx_source: str, *, _defer_postprocess: bool = False) ->
name_sym = expr[1] if len(expr) > 1 else None
name = name_sym.name if hasattr(name_sym, 'name') else str(name_sym) if name_sym else None
if name and expr[0].name in ("defcomp", "defisland"):
_COMPONENT_ENV[name] = Component(
params, has_children = _parse_defcomp_params(expr[2] if len(expr) > 3 else [])
cls = Island if expr[0].name == "defisland" else Component
_COMPONENT_ENV[name] = cls(
name=name.lstrip("~"),
params=[], has_children=False,
params=params, has_children=has_children,
body=expr[-1], closure={},
)