Island guard in aser expansion + page helper IO tests (13 tests)
Fix: islands (defisland) pass component? check but must NEVER be expanded server-side — they use browser-only reactive primitives (signal, deref, computed). Added (not (island? comp)) guard in adapter-sx.sx aser component dispatch. New test file: shared/sx/tests/test_ocaml_helpers.py - TestHelperInjection: 5 tests — helper IO proxy, 2-arg calls, aser/aser_slot with helpers, undefined helper error - TestHelperIOPerformance: 2 tests — 20 sequential IO round-trips complete in <5s, aser_slot with 5 helpers in <3s - TestAserSlotClientAffinity: 6 tests — island exclusion, client affinity exclusion, server affinity expansion, auto affinity behavior in aser vs aser_slot eval_sx_url stays on bridge.aser() (server-affinity only) for now. Switching to aser_slot requires fixing the double-aser issue in _render_to_sx where content gets re-parsed and re-asered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-19T11:12:01Z";
|
var SX_VERSION = "2026-03-19T12:40:33Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -2679,7 +2679,7 @@ PRIMITIVES["aser"] = aser;
|
|||||||
return (isSxTruthy((name == "<>")) ? aserFragment(args, env) : (isSxTruthy((name == "raw!")) ? aserCall("raw!", args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
|
return (isSxTruthy((name == "<>")) ? aserFragment(args, env) : (isSxTruthy((name == "raw!")) ? aserCall("raw!", args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() {
|
||||||
var comp = (isSxTruthy(envHas(env, name)) ? envGet(env, name) : NIL);
|
var comp = (isSxTruthy(envHas(env, name)) ? envGet(env, name) : NIL);
|
||||||
var expandAll = (isSxTruthy(envHas(env, "expand-components?")) ? expandComponents_p() : false);
|
var expandAll = (isSxTruthy(envHas(env, "expand-components?")) ? expandComponents_p() : false);
|
||||||
return (isSxTruthy((isSxTruthy(comp) && isMacro(comp))) ? aser(expandMacro(comp, args, env), env) : (isSxTruthy((isSxTruthy(comp) && isSxTruthy(isComponent(comp)) && isSxTruthy(sxOr(expandAll, (componentAffinity(comp) == "server"))) && !isSxTruthy((componentAffinity(comp) == "client")))) ? aserExpandComponent(comp, args, env) : aserCall(name, args, env)));
|
return (isSxTruthy((isSxTruthy(comp) && isMacro(comp))) ? aser(expandMacro(comp, args, env), env) : (isSxTruthy((isSxTruthy(comp) && isSxTruthy(isComponent(comp)) && isSxTruthy(!isSxTruthy(isIsland(comp))) && isSxTruthy(sxOr(expandAll, (componentAffinity(comp) == "server"))) && !isSxTruthy((componentAffinity(comp) == "client")))) ? aserExpandComponent(comp, args, env) : aserCall(name, args, env)));
|
||||||
})() : (isSxTruthy((name == "lake")) ? aserCall(name, args, env) : (isSxTruthy((name == "marsh")) ? aserCall(name, args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() {
|
})() : (isSxTruthy((name == "lake")) ? aserCall(name, args, env) : (isSxTruthy((name == "marsh")) ? aserCall(name, args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() {
|
||||||
var f = trampoline(evalExpr(head, env));
|
var f = trampoline(evalExpr(head, env));
|
||||||
var evaledArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args);
|
var evaledArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args);
|
||||||
|
|||||||
355
shared/sx/tests/test_ocaml_helpers.py
Normal file
355
shared/sx/tests/test_ocaml_helpers.py
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
"""Tests for OCaml kernel ↔ Python page helper IO bridge.
|
||||||
|
|
||||||
|
Verifies that:
|
||||||
|
1. Helper injection registers functions in the OCaml kernel
|
||||||
|
2. The kernel can call helpers via (io-request "helper" ...)
|
||||||
|
3. aser_slot expands components that use helpers
|
||||||
|
4. Caching eliminates redundant IO round-trips
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
pytest shared/sx/tests/test_ocaml_helpers.py -v
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
|
||||||
|
if _project_root not in sys.path:
|
||||||
|
sys.path.insert(0, _project_root)
|
||||||
|
|
||||||
|
from shared.sx.ocaml_bridge import OcamlBridge, OcamlBridgeError, _DEFAULT_BIN, _escape
|
||||||
|
|
||||||
|
|
||||||
|
def _skip_if_no_binary():
|
||||||
|
bin_path = os.path.abspath(_DEFAULT_BIN)
|
||||||
|
if not os.path.isfile(bin_path):
|
||||||
|
raise unittest.SkipTest(
|
||||||
|
f"OCaml binary not found at {bin_path}. "
|
||||||
|
f"Build with: cd hosts/ocaml && eval $(opam env) && dune build"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHelperInjection(unittest.IsolatedAsyncioTestCase):
|
||||||
|
"""Test that page helpers can be injected into the OCaml kernel."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
_skip_if_no_binary()
|
||||||
|
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.bridge = OcamlBridge()
|
||||||
|
await self.bridge.start()
|
||||||
|
# Load spec + adapter (needed for aser)
|
||||||
|
spec_dir = os.path.join(_project_root, "spec")
|
||||||
|
web_dir = os.path.join(_project_root, "web")
|
||||||
|
for f in ["parser.sx", "render.sx"]:
|
||||||
|
path = os.path.join(spec_dir, f)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load "{_escape(path)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
adapter = os.path.join(web_dir, "adapter-sx.sx")
|
||||||
|
if os.path.isfile(adapter):
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load "{_escape(adapter)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
async def asyncTearDown(self):
|
||||||
|
await self.bridge.stop()
|
||||||
|
|
||||||
|
async def _inject_test_helper(self, name: str, nargs: int):
|
||||||
|
"""Inject a single helper proxy into the kernel."""
|
||||||
|
param_names = " ".join(chr(97 + i) for i in range(nargs))
|
||||||
|
arg_list = " ".join(chr(97 + i) for i in range(nargs))
|
||||||
|
sx_def = f'(define {name} (fn ({param_names}) (helper "{name}" {arg_list})))'
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(sx_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
async def test_helper_call_returns_value(self):
|
||||||
|
"""Injected helper can be called and returns IO result."""
|
||||||
|
# The "helper" native binding is already in the kernel.
|
||||||
|
# Define a test helper that calls (helper "json-encode" value)
|
||||||
|
await self._inject_test_helper("json-encode", 1)
|
||||||
|
|
||||||
|
# Call it via eval — should yield io-request, Python dispatches
|
||||||
|
result = await self.bridge.eval(
|
||||||
|
'(json-encode "hello")',
|
||||||
|
ctx={"_helper_service": "sx"}
|
||||||
|
)
|
||||||
|
self.assertIn("hello", result)
|
||||||
|
|
||||||
|
async def test_helper_with_two_args(self):
|
||||||
|
"""Helper with 2 args works (e.g. highlight pattern)."""
|
||||||
|
# Define a 2-arg test helper via the generic helper binding
|
||||||
|
sx_def = '(define test-two-args (fn (a b) (helper "json-encode" (str a ":" b))))'
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(sx_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
result = await self.bridge.eval(
|
||||||
|
'(test-two-args "hello" "world")',
|
||||||
|
ctx={"_helper_service": "sx"}
|
||||||
|
)
|
||||||
|
self.assertIn("hello:world", result)
|
||||||
|
|
||||||
|
async def test_undefined_helper_errors(self):
|
||||||
|
"""Calling an uninjected helper raises an error."""
|
||||||
|
with self.assertRaises(OcamlBridgeError) as cm:
|
||||||
|
await self.bridge.eval('(nonexistent-helper "arg")')
|
||||||
|
self.assertIn("Undefined symbol", str(cm.exception))
|
||||||
|
|
||||||
|
async def test_helper_in_aser(self):
|
||||||
|
"""Helper works inside aser — result inlined in SX output."""
|
||||||
|
await self._inject_test_helper("json-encode", 1)
|
||||||
|
|
||||||
|
# aser a component-like expression that calls the helper
|
||||||
|
result = await self.bridge.aser(
|
||||||
|
'(div :class "test" (json-encode "hello"))',
|
||||||
|
ctx={"_helper_service": "sx"}
|
||||||
|
)
|
||||||
|
# The aser should evaluate json-encode and inline the result
|
||||||
|
self.assertIn("div", result)
|
||||||
|
self.assertIn("hello", result)
|
||||||
|
|
||||||
|
async def test_helper_in_aser_slot_component(self):
|
||||||
|
"""aser_slot expands component containing helper call."""
|
||||||
|
await self._inject_test_helper("json-encode", 1)
|
||||||
|
|
||||||
|
# Define a component that calls the helper
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(
|
||||||
|
'(load-source "(defcomp ~test/code-display (&key code) '
|
||||||
|
'(pre (code code)))")'
|
||||||
|
)
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
# aser_slot should expand the component, evaluating the body
|
||||||
|
result = await self.bridge.aser_slot(
|
||||||
|
'(~test/code-display :code (json-encode "test-value"))',
|
||||||
|
ctx={"_helper_service": "sx"}
|
||||||
|
)
|
||||||
|
# Should contain expanded HTML tags, not component call
|
||||||
|
self.assertIn("pre", result)
|
||||||
|
self.assertIn("test-value", result)
|
||||||
|
# Should NOT contain the component call
|
||||||
|
self.assertNotIn("~test/code-display", result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHelperIOPerformance(unittest.IsolatedAsyncioTestCase):
|
||||||
|
"""Test that helper IO round-trips are fast enough for production."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
_skip_if_no_binary()
|
||||||
|
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.bridge = OcamlBridge()
|
||||||
|
await self.bridge.start()
|
||||||
|
spec_dir = os.path.join(_project_root, "spec")
|
||||||
|
web_dir = os.path.join(_project_root, "web")
|
||||||
|
for f in ["parser.sx", "render.sx"]:
|
||||||
|
path = os.path.join(spec_dir, f)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load "{_escape(path)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
adapter = os.path.join(web_dir, "adapter-sx.sx")
|
||||||
|
if os.path.isfile(adapter):
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load "{_escape(adapter)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
async def asyncTearDown(self):
|
||||||
|
await self.bridge.stop()
|
||||||
|
|
||||||
|
async def test_sequential_helper_calls_timing(self):
|
||||||
|
"""Measure round-trip time for sequential helper calls."""
|
||||||
|
# Inject json-encode as a fast helper
|
||||||
|
param_names = "a"
|
||||||
|
sx_def = '(define json-encode (fn (a) (helper "json-encode" a)))'
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(sx_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
# Time 20 sequential calls (simulating a page with 20 highlight calls)
|
||||||
|
n_calls = 20
|
||||||
|
calls = " ".join(f'(json-encode "{i}")' for i in range(n_calls))
|
||||||
|
expr = f'(list {calls})'
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
result = await self.bridge.eval(expr, ctx={"_helper_service": "sx"})
|
||||||
|
elapsed = time.monotonic() - start
|
||||||
|
|
||||||
|
# Should complete in under 5 seconds (generous for 20 IO round-trips)
|
||||||
|
self.assertLess(elapsed, 5.0,
|
||||||
|
f"20 helper IO round-trips took {elapsed:.1f}s (target: <5s)")
|
||||||
|
|
||||||
|
async def test_aser_slot_with_many_helper_calls(self):
|
||||||
|
"""aser_slot with multiple helper calls completes in reasonable time."""
|
||||||
|
sx_def = '(define json-encode (fn (a) (helper "json-encode" a)))'
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(sx_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
# Define a component with multiple helper calls
|
||||||
|
comp_def = (
|
||||||
|
'(defcomp ~test/multi-helper (&key)'
|
||||||
|
' (div'
|
||||||
|
' (p (json-encode "a"))'
|
||||||
|
' (p (json-encode "b"))'
|
||||||
|
' (p (json-encode "c"))'
|
||||||
|
' (p (json-encode "d"))'
|
||||||
|
' (p (json-encode "e"))))'
|
||||||
|
)
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(comp_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
result = await self.bridge.aser_slot(
|
||||||
|
'(~test/multi-helper)',
|
||||||
|
ctx={"_helper_service": "sx"}
|
||||||
|
)
|
||||||
|
elapsed = time.monotonic() - start
|
||||||
|
|
||||||
|
self.assertIn("div", result)
|
||||||
|
self.assertLess(elapsed, 3.0,
|
||||||
|
f"aser_slot with 5 helpers took {elapsed:.1f}s (target: <3s)")
|
||||||
|
|
||||||
|
|
||||||
|
class TestAserSlotClientAffinity(unittest.IsolatedAsyncioTestCase):
|
||||||
|
"""Test that :affinity :client components are NOT expanded by aser_slot."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
_skip_if_no_binary()
|
||||||
|
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.bridge = OcamlBridge()
|
||||||
|
await self.bridge.start()
|
||||||
|
spec_dir = os.path.join(_project_root, "spec")
|
||||||
|
web_dir = os.path.join(_project_root, "web")
|
||||||
|
for f in ["parser.sx", "render.sx"]:
|
||||||
|
path = os.path.join(spec_dir, f)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load "{_escape(path)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
adapter = os.path.join(web_dir, "adapter-sx.sx")
|
||||||
|
if os.path.isfile(adapter):
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load "{_escape(adapter)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
async def asyncTearDown(self):
|
||||||
|
await self.bridge.stop()
|
||||||
|
|
||||||
|
async def test_client_affinity_not_expanded(self):
|
||||||
|
"""Components with :affinity :client stay as calls in aser_slot."""
|
||||||
|
comp_def = (
|
||||||
|
'(defcomp ~test/client-only () :affinity :client'
|
||||||
|
' (div "browser-only-content"))'
|
||||||
|
)
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(comp_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
result = await self.bridge.aser_slot('(~test/client-only)')
|
||||||
|
# Should remain as a component call, NOT expanded
|
||||||
|
self.assertIn("~test/client-only", result)
|
||||||
|
self.assertNotIn("browser-only-content", result)
|
||||||
|
|
||||||
|
async def test_server_affinity_expanded(self):
|
||||||
|
"""Components with :affinity :server are expanded by regular aser."""
|
||||||
|
comp_def = (
|
||||||
|
'(defcomp ~test/server-only (&key label) :affinity :server'
|
||||||
|
' (div :class "server" label))'
|
||||||
|
)
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(comp_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
result = await self.bridge.aser(
|
||||||
|
'(~test/server-only :label "hello")')
|
||||||
|
# Should be expanded — div with class, not component call
|
||||||
|
self.assertIn("div", result)
|
||||||
|
self.assertIn("server", result)
|
||||||
|
self.assertNotIn("~test/server-only", result)
|
||||||
|
|
||||||
|
async def test_auto_affinity_not_expanded_by_aser(self):
|
||||||
|
"""Default affinity components are NOT expanded by regular aser."""
|
||||||
|
comp_def = (
|
||||||
|
'(defcomp ~test/auto-comp (&key label)'
|
||||||
|
' (div "auto-content" label))'
|
||||||
|
)
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(comp_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
result = await self.bridge.aser(
|
||||||
|
'(~test/auto-comp :label "hi")')
|
||||||
|
# Should remain as component call
|
||||||
|
self.assertIn("~test/auto-comp", result)
|
||||||
|
|
||||||
|
async def test_island_not_expanded_by_aser_slot(self):
|
||||||
|
"""Islands are NEVER expanded server-side, even with expand-all."""
|
||||||
|
island_def = (
|
||||||
|
'(defisland ~test/reactive-isle (&key label)'
|
||||||
|
' (div (deref (signal 0)) label))'
|
||||||
|
)
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(island_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
result = await self.bridge.aser_slot(
|
||||||
|
'(~test/reactive-isle :label "hello")')
|
||||||
|
# Island should be serialized as a call, NOT expanded
|
||||||
|
self.assertIn("~test/reactive-isle", result)
|
||||||
|
# Body content (deref, signal) should NOT appear
|
||||||
|
self.assertNotIn("deref", result)
|
||||||
|
self.assertNotIn("signal", result)
|
||||||
|
|
||||||
|
async def test_island_preserved_inside_expanded_component(self):
|
||||||
|
"""Island calls survive inside aser_slot-expanded components."""
|
||||||
|
src = (
|
||||||
|
'(defisland ~test/inner-isle (&key v) (span (deref (signal v))))'
|
||||||
|
'(defcomp ~test/outer-comp (&key title)'
|
||||||
|
' (div (h1 title) (~test/inner-isle :v 42)))'
|
||||||
|
)
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(src)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
result = await self.bridge.aser_slot(
|
||||||
|
'(~test/outer-comp :title "Test")')
|
||||||
|
# Outer component expanded
|
||||||
|
self.assertNotIn("~test/outer-comp", result)
|
||||||
|
self.assertIn("div", result)
|
||||||
|
self.assertIn("Test", result)
|
||||||
|
# Inner island preserved as call
|
||||||
|
self.assertIn("~test/inner-isle", result)
|
||||||
|
|
||||||
|
async def test_auto_affinity_expanded_by_aser_slot(self):
|
||||||
|
"""Default affinity components ARE expanded by aser_slot."""
|
||||||
|
comp_def = (
|
||||||
|
'(defcomp ~test/auto-comp2 (&key label)'
|
||||||
|
' (div "expanded" label))'
|
||||||
|
)
|
||||||
|
async with self.bridge._lock:
|
||||||
|
self.bridge._send(f'(load-source "{_escape(comp_def)}")')
|
||||||
|
await self.bridge._read_until_ok(ctx=None)
|
||||||
|
|
||||||
|
result = await self.bridge.aser_slot(
|
||||||
|
'(~test/auto-comp2 :label "hi")')
|
||||||
|
# Should be expanded
|
||||||
|
self.assertIn("div", result)
|
||||||
|
self.assertIn("expanded", result)
|
||||||
|
self.assertNotIn("~test/auto-comp2", result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -87,6 +87,7 @@
|
|||||||
(and comp (macro? comp))
|
(and comp (macro? comp))
|
||||||
(aser (expand-macro comp args env) env)
|
(aser (expand-macro comp args env) env)
|
||||||
(and comp (component? comp)
|
(and comp (component? comp)
|
||||||
|
(not (island? comp))
|
||||||
(or expand-all
|
(or expand-all
|
||||||
(= (component-affinity comp) "server"))
|
(= (component-affinity comp) "server"))
|
||||||
;; :affinity :client components are never expanded
|
;; :affinity :client components are never expanded
|
||||||
|
|||||||
Reference in New Issue
Block a user