Skip to main content

Code Generator

moon_spa.codegen · moon_spa.scope · moon_spa.trace

These three modules form the Python-to-JavaScript compiler that turns Component.setup(self, props) output into a setup() JS function.

Pipeline

load_python_scope(python_block) → dict

moon_spa.scope

Executes the <python> block in a full Python environment (all built-ins available). Adds:

  • Component base class
  • StateField dataclass
  • Tracing-aware overrides for int, float, str, bool

resolve_component_callables(scope)

Returns (context_fn, client_fn) from a Component subclass implementing setup(self, props). Legacy context()/client() and top-level setup styles are rejected.

Tracing proxies (moon_spa.trace)

When setup-derived operations are normalized, traced objects are used to preserve expression references and operation semantics.

This allows expressions like self.add("count", props.initialCount + 1) to be converted to a JavaScript expression rather than evamoonted eagerly in Python.

Generated setup() structure

function setup({ useState, props, componentName }) {
// 1. Resolve and coerce props
const resolvedProps = Object.assign({}, { name: "default" }, props || {});

// 2. useState hooks
const [__state_0, __set_state_0] = useState(0);

// 3. Actions object + server bridge
const actions = {
increment: function () { __serverCall("action", "increment"); },
reset: function () { __set_state_0(0); },
};

// 4. __callAction dispatcher
function __callAction(actionName, event) { ... }

// 5. Lifecycle hooks
const lifecycle = {
created: function () {},
mounted: function () { __callAction("init"); },
updated: function () {},
unmounted: function () {},
};

// 6. Exposed state + return
const state = { count: __state_0 };
return { props: resolvedProps, state, actions, lifecycle };
}

Operation mapping

Computed variables (setup().datapy namespace)

setup(self, props) can return a mapping, or omit return and let the server infer the spec. The data section is evamoonted server-side and injected into render context as py. It is not compiled to JavaScript by itself; it is hydrated through props patches when returned by server callables.

# Python
class Card(Component):
def setup(self, props):
price = props.get("price", 0)
props = {**props}
state = {}
data = {
"display": f"${price:.2f}",
"is_cheap": price < 10,
}

# Optional: The server can infer this return automatically.
return {
"props": props,
"state": state,
"data": data,
"actions": {},
"lifecycle": {},
}
Render context passed to template:
{
"props": { "price": 7 },
"state": { ... },
"py": { "display": "$7.00", "is_cheap": True }
}

Template access:

<p>{{ py.display }}</p> <!-- explicit namespace -->
<p l-if="is_cheap">On sale!</p> <!-- flat / shorthand -->

py values are baked into the server-rendered HTML. They do not appear in the generated setup() function.


State (setup().stateuseState hooks)

Each state field in setup().state compiles to a useState hook. The initial value expression depends on how the field is declared:

Python declarationInternal specGenerated JS
"count": 0{"default": 0}useState(0)
"name": "moon"{"default": "moon"}useState("moon")
StateField("n", default=0, cast="int"){"default": 0, "cast": "int"}useState(0)
StateField("q", from_prop="qty", default=1, cast="int"){"from_prop": "qty", "default": 1, "cast": "int"}useState(Number(resolvedProps["qty"] ?? 1))

Example — full Python → JS compilation:

# Python
class Counter(Component):
def setup(self, props):
props = {**props}
state = {
"count": 0,
"label": "start",
}
data = {}

return {
"props": props,
"state": state,
"data": data,
"actions": {},
"lifecycle": {},
}
// Generated setup() (excerpt)
const [__state_0, __set_state_0] = useState(0); // count
const [__state_1, __set_state_1] = useState("start"); // label

const state = { count: __state_0, label: __state_1 };

Actions (setup().actions)

Actions are defined in setup().actions as operation mappings or callables.

PythonInternal operation dictGenerated JS action body
{"op":"add","state":"count","value":1}{"op":"add","state":"count","value":1}__set_state_0(function(v){return v+1;});
{"op":"sub","state":"count","value":1}{"op":"sub","state":"count","value":1}__set_state_0(function(v){return v-1;});
{"op":"set","state":"flag","value":True}{"op":"set","state":"flag","value":True}__set_state_1(function(){return true;});
{"op":"toggle","state":"flag"}{"op":"toggle","state":"flag"}__set_state_1(function(v){return !v;});
callable{"op":"server_call","kind":"action","name":"..."}__serverCall("action", "...");

Full example:

# Python
class Counter(Component):
def setup(self, props):
return {
"props": props,
"state": {"count": 0},
"data": {},
"actions": {
"increment": {"op": "add", "state": "count", "value": 1},
"reset": {"op": "set", "state": "count", "value": 0},
},
"lifecycle": {},
}
// Generated setup() (excerpt)
const [__state_0, __set_state_0] = useState(0);

const actions = {
increment: function () {
__set_state_0(function (value) { return value + 1; });
},
reset: function () {
__set_state_0(function () { return 0; });
},
};

Callable actions are executed through the server bridge and generate state/props patches. If an action mutates data, updated data keys are automatically included in the props patch even when the action returns None.

Latest behavior:

  • Action callables can update data directly (for example data["pypi"] = refreshed) without returning a mapping.
  • The server emits those data keys through props patching automatically.
# Python
class Counter(Component):
def setup(self, props):
state = {"count": 0}
data = {"limit": 10}

def safe_increment():
if state["count"] < 10:
state["count"] += 1
data["limit"] = 10

return {
"props": props,
"state": state,
"data": data,
"actions": {"safe_increment": safe_increment},
"lifecycle": {},
}

Tracing records:

# Internal operation list produced by _TraceState
[
{
"op": "add",
"state": "count",
"value": 1,
"cond": {"left": "count", "op": "<", "right": 10}
}
]

Generated JS:

safe_increment: function () {
if (__state_0 < 10) {
__set_state_0(function (value) { return value + 1; });
}
},

Conditional operators captured by _TraceState

State variables are named by enumeration order, not by field name: the first state field is __state_0, the second __state_1, and so on. The field name (x) is only used as a lookup key — it never appears in the JS variable name.

Python comparisoncond.opJS guard (where __state_0 is the variable for x)
self.state.x < n"<"if (__state_0 < n)
self.state.x <= n"<="if (__state_0 <= n)
self.state.x > n">"if (__state_0 > n)
self.state.x >= n">="if (__state_0 >= n)
self.state.x == n"=="if (__state_0 == n)
self.state.x != n"!="if (__state_0 != n)

Multi-step traced method

def restart(self):
self.state.score = 0 # → {"op":"set","state":"score","value":0}
self.state.lives = 3 # → {"op":"set","state":"lives","value":3}
self.state.game_over = False # → {"op":"set","state":"game_over","value":False}
restart: function () {
__set_state_0(function () { return 0; });
__set_state_1(function () { return 3; });
__set_state_2(function () { return false; });
},