pulse-framework 0.1.62__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. pulse/__init__.py +1493 -0
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +1086 -0
  4. pulse/channel.py +607 -0
  5. pulse/cli/__init__.py +0 -0
  6. pulse/cli/cmd.py +575 -0
  7. pulse/cli/dependencies.py +181 -0
  8. pulse/cli/folder_lock.py +134 -0
  9. pulse/cli/helpers.py +271 -0
  10. pulse/cli/logging.py +102 -0
  11. pulse/cli/models.py +35 -0
  12. pulse/cli/packages.py +262 -0
  13. pulse/cli/processes.py +292 -0
  14. pulse/cli/secrets.py +39 -0
  15. pulse/cli/uvicorn_log_config.py +87 -0
  16. pulse/code_analysis.py +38 -0
  17. pulse/codegen/__init__.py +0 -0
  18. pulse/codegen/codegen.py +359 -0
  19. pulse/codegen/templates/__init__.py +0 -0
  20. pulse/codegen/templates/layout.py +106 -0
  21. pulse/codegen/templates/route.py +345 -0
  22. pulse/codegen/templates/routes_ts.py +42 -0
  23. pulse/codegen/utils.py +20 -0
  24. pulse/component.py +237 -0
  25. pulse/components/__init__.py +0 -0
  26. pulse/components/for_.py +83 -0
  27. pulse/components/if_.py +86 -0
  28. pulse/components/react_router.py +94 -0
  29. pulse/context.py +108 -0
  30. pulse/cookies.py +322 -0
  31. pulse/decorators.py +344 -0
  32. pulse/dom/__init__.py +0 -0
  33. pulse/dom/elements.py +1024 -0
  34. pulse/dom/events.py +445 -0
  35. pulse/dom/props.py +1250 -0
  36. pulse/dom/svg.py +0 -0
  37. pulse/dom/tags.py +328 -0
  38. pulse/dom/tags.pyi +480 -0
  39. pulse/env.py +178 -0
  40. pulse/form.py +538 -0
  41. pulse/helpers.py +541 -0
  42. pulse/hooks/__init__.py +0 -0
  43. pulse/hooks/core.py +452 -0
  44. pulse/hooks/effects.py +88 -0
  45. pulse/hooks/init.py +668 -0
  46. pulse/hooks/runtime.py +464 -0
  47. pulse/hooks/setup.py +254 -0
  48. pulse/hooks/stable.py +138 -0
  49. pulse/hooks/state.py +192 -0
  50. pulse/js/__init__.py +125 -0
  51. pulse/js/__init__.pyi +115 -0
  52. pulse/js/_types.py +299 -0
  53. pulse/js/array.py +339 -0
  54. pulse/js/console.py +50 -0
  55. pulse/js/date.py +119 -0
  56. pulse/js/document.py +145 -0
  57. pulse/js/error.py +140 -0
  58. pulse/js/json.py +66 -0
  59. pulse/js/map.py +97 -0
  60. pulse/js/math.py +69 -0
  61. pulse/js/navigator.py +79 -0
  62. pulse/js/number.py +57 -0
  63. pulse/js/obj.py +81 -0
  64. pulse/js/object.py +172 -0
  65. pulse/js/promise.py +172 -0
  66. pulse/js/pulse.py +115 -0
  67. pulse/js/react.py +495 -0
  68. pulse/js/regexp.py +57 -0
  69. pulse/js/set.py +124 -0
  70. pulse/js/string.py +38 -0
  71. pulse/js/weakmap.py +53 -0
  72. pulse/js/weakset.py +48 -0
  73. pulse/js/window.py +205 -0
  74. pulse/messages.py +202 -0
  75. pulse/middleware.py +471 -0
  76. pulse/plugin.py +96 -0
  77. pulse/proxy.py +242 -0
  78. pulse/py.typed +0 -0
  79. pulse/queries/__init__.py +0 -0
  80. pulse/queries/client.py +609 -0
  81. pulse/queries/common.py +101 -0
  82. pulse/queries/effect.py +55 -0
  83. pulse/queries/infinite_query.py +1418 -0
  84. pulse/queries/mutation.py +295 -0
  85. pulse/queries/protocol.py +136 -0
  86. pulse/queries/query.py +1314 -0
  87. pulse/queries/store.py +120 -0
  88. pulse/react_component.py +88 -0
  89. pulse/reactive.py +1208 -0
  90. pulse/reactive_extensions.py +1172 -0
  91. pulse/render_session.py +768 -0
  92. pulse/renderer.py +584 -0
  93. pulse/request.py +205 -0
  94. pulse/routing.py +598 -0
  95. pulse/serializer.py +279 -0
  96. pulse/state.py +556 -0
  97. pulse/test_helpers.py +15 -0
  98. pulse/transpiler/__init__.py +111 -0
  99. pulse/transpiler/assets.py +81 -0
  100. pulse/transpiler/builtins.py +1029 -0
  101. pulse/transpiler/dynamic_import.py +130 -0
  102. pulse/transpiler/emit_context.py +49 -0
  103. pulse/transpiler/errors.py +96 -0
  104. pulse/transpiler/function.py +611 -0
  105. pulse/transpiler/id.py +18 -0
  106. pulse/transpiler/imports.py +341 -0
  107. pulse/transpiler/js_module.py +336 -0
  108. pulse/transpiler/modules/__init__.py +33 -0
  109. pulse/transpiler/modules/asyncio.py +57 -0
  110. pulse/transpiler/modules/json.py +24 -0
  111. pulse/transpiler/modules/math.py +265 -0
  112. pulse/transpiler/modules/pulse/__init__.py +5 -0
  113. pulse/transpiler/modules/pulse/tags.py +250 -0
  114. pulse/transpiler/modules/typing.py +63 -0
  115. pulse/transpiler/nodes.py +1987 -0
  116. pulse/transpiler/py_module.py +135 -0
  117. pulse/transpiler/transpiler.py +1100 -0
  118. pulse/transpiler/vdom.py +256 -0
  119. pulse/types/__init__.py +0 -0
  120. pulse/types/event_handler.py +50 -0
  121. pulse/user_session.py +386 -0
  122. pulse/version.py +69 -0
  123. pulse_framework-0.1.62.dist-info/METADATA +198 -0
  124. pulse_framework-0.1.62.dist-info/RECORD +126 -0
  125. pulse_framework-0.1.62.dist-info/WHEEL +4 -0
  126. pulse_framework-0.1.62.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1987 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import datetime as dt
5
+ import string
6
+ import warnings
7
+ from abc import ABC, abstractmethod
8
+ from collections.abc import Callable, Iterable, Sequence
9
+ from dataclasses import dataclass, field
10
+ from inspect import isfunction, signature
11
+ from typing import (
12
+ TYPE_CHECKING,
13
+ Any,
14
+ Generic,
15
+ Protocol,
16
+ TypeAlias,
17
+ TypeVar,
18
+ cast,
19
+ overload,
20
+ override,
21
+ )
22
+ from typing import Literal as Lit
23
+
24
+ from pulse.env import env
25
+ from pulse.transpiler.errors import TranspileError
26
+ from pulse.transpiler.vdom import VDOMExpr, VDOMNode, VDOMPrimitive
27
+
28
+ if TYPE_CHECKING:
29
+ from pulse.transpiler.transpiler import Transpiler
30
+
31
+ _T = TypeVar("_T")
32
+ Primitive: TypeAlias = bool | int | float | str | dt.datetime | None
33
+
34
+ _JS_IDENTIFIER_START = set(string.ascii_letters + "_")
35
+ _JS_IDENTIFIER_CONTINUE = set(string.ascii_letters + string.digits + "_")
36
+
37
+
38
+ def to_js_identifier(name: str) -> str:
39
+ """Normalize a string to a JS-compatible identifier."""
40
+ if not name:
41
+ return "_"
42
+ out: list[str] = []
43
+ for ch in name:
44
+ out.append(ch if ch in _JS_IDENTIFIER_CONTINUE else "_")
45
+ if not out or out[0] not in _JS_IDENTIFIER_START:
46
+ out.insert(0, "_")
47
+ return "".join(out)
48
+
49
+
50
+ # =============================================================================
51
+ # Global registries
52
+ # =============================================================================
53
+
54
+ # Global registry: id(value) -> Expr
55
+ # Used by Expr.of() to resolve registered Python values
56
+ EXPR_REGISTRY: dict[int, "Expr"] = {}
57
+
58
+
59
+ # =============================================================================
60
+ # Base classes
61
+ # =============================================================================
62
+ class Expr(ABC):
63
+ """Base class for expression nodes.
64
+
65
+ Provides hooks for custom transpilation behavior:
66
+ - transpile_call: customize behavior when called as a function
67
+ - transpile_getattr: customize attribute access
68
+ - transpile_subscript: customize subscript access
69
+
70
+ And serialization for client-side rendering:
71
+ - render: serialize to dict for client renderer (stub for now)
72
+ """
73
+
74
+ __slots__: tuple[str, ...] = ()
75
+
76
+ @abstractmethod
77
+ def emit(self, out: list[str]) -> None:
78
+ """Emit this expression as JavaScript/JSX code into the output buffer."""
79
+
80
+ def precedence(self) -> int:
81
+ """Operator precedence (higher = binds tighter). Default: primary (20)."""
82
+ return 20
83
+
84
+ # -------------------------------------------------------------------------
85
+ # Transpilation hooks (override to customize behavior)
86
+ # -------------------------------------------------------------------------
87
+
88
+ def transpile_call(
89
+ self,
90
+ args: list[ast.expr],
91
+ keywords: list[ast.keyword],
92
+ ctx: Transpiler,
93
+ ) -> Expr:
94
+ """Called when this expression is used as a function: expr(args).
95
+
96
+ Override to customize call behavior.
97
+ Default emits a Call expression with args transpiled.
98
+
99
+ Args and keywords are raw Python AST nodes (not yet transpiled).
100
+ Use ctx.emit_expr() to convert them to Expr as needed.
101
+ Keywords with kw.arg=None are **spread syntax.
102
+ """
103
+ if keywords:
104
+ has_spread = any(kw.arg is None for kw in keywords)
105
+ if has_spread:
106
+ raise TranspileError("Spread (**expr) not supported in this call")
107
+ raise TranspileError("Keyword arguments not supported in call")
108
+ return Call(self, [ctx.emit_expr(a) for a in args])
109
+
110
+ def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
111
+ """Called when an attribute is accessed: expr.attr.
112
+
113
+ Override to customize attribute access.
114
+ Default returns Member(self, attr).
115
+ """
116
+ return Member(self, attr)
117
+
118
+ def transpile_subscript(self, key: ast.expr, ctx: Transpiler) -> Expr:
119
+ """Called when subscripted: expr[key].
120
+
121
+ Override to customize subscript behavior.
122
+ Default returns Subscript(self, emitted_key).
123
+ """
124
+ if isinstance(key, ast.Tuple):
125
+ raise TranspileError(
126
+ "Multiple indices not supported in subscript", node=key
127
+ )
128
+ return Subscript(self, ctx.emit_expr(key))
129
+
130
+ # -------------------------------------------------------------------------
131
+ # Serialization for client-side rendering
132
+ # -------------------------------------------------------------------------
133
+
134
+ @abstractmethod
135
+ def render(self) -> VDOMPrimitive | VDOMExpr:
136
+ """Serialize this expression node for client-side rendering.
137
+
138
+ Returns a VDOMNode (primitive or dict) that can be JSON-serialized and
139
+ evaluated on the client. Override in each concrete Expr subclass.
140
+
141
+ Raises TypeError for nodes that cannot be serialized (e.g., Transformer).
142
+ """
143
+
144
+ # -------------------------------------------------------------------------
145
+ # Python dunder methods for natural syntax in @javascript functions
146
+ # These return Expr nodes that represent the operations at transpile time.
147
+ # -------------------------------------------------------------------------
148
+
149
+ def __call__(self, *args: object, **kwargs: object) -> "Call":
150
+ """Allow calling Expr objects in Python code.
151
+
152
+ Returns a Call expression. Subclasses may override to return more
153
+ specific types (e.g., Element for JSX components).
154
+ """
155
+ return Call(self, [Expr.of(a) for a in args])
156
+
157
+ def __getitem__(self, key: object) -> "Subscript":
158
+ """Allow subscript access on Expr objects in Python code.
159
+
160
+ Returns a Subscript expression for type checking.
161
+ """
162
+ return Subscript(self, Expr.of(key))
163
+
164
+ def __getattr__(self, attr: str) -> "Member":
165
+ """Allow attribute access on Expr objects in Python code.
166
+
167
+ Returns a Member expression for type checking.
168
+ """
169
+ return Member(self, attr)
170
+
171
+ def __add__(self, other: object) -> "Binary":
172
+ """Allow + operator on Expr objects."""
173
+ return Binary(self, "+", Expr.of(other))
174
+
175
+ def __sub__(self, other: object) -> "Binary":
176
+ """Allow - operator on Expr objects."""
177
+ return Binary(self, "-", Expr.of(other))
178
+
179
+ def __mul__(self, other: object) -> "Binary":
180
+ """Allow * operator on Expr objects."""
181
+ return Binary(self, "*", Expr.of(other))
182
+
183
+ def __truediv__(self, other: object) -> "Binary":
184
+ """Allow / operator on Expr objects."""
185
+ return Binary(self, "/", Expr.of(other))
186
+
187
+ def __mod__(self, other: object) -> "Binary":
188
+ """Allow % operator on Expr objects."""
189
+ return Binary(self, "%", Expr.of(other))
190
+
191
+ def __and__(self, other: object) -> "Binary":
192
+ """Allow & operator on Expr objects (maps to &&)."""
193
+ return Binary(self, "&&", Expr.of(other))
194
+
195
+ def __or__(self, other: object) -> "Binary":
196
+ """Allow | operator on Expr objects (maps to ||)."""
197
+ return Binary(self, "||", Expr.of(other))
198
+
199
+ def __neg__(self) -> "Unary":
200
+ """Allow unary - operator on Expr objects."""
201
+ return Unary("-", self)
202
+
203
+ def __pos__(self) -> "Unary":
204
+ """Allow unary + operator on Expr objects."""
205
+ return Unary("+", self)
206
+
207
+ def __invert__(self) -> "Unary":
208
+ """Allow ~ operator on Expr objects (maps to !)."""
209
+ return Unary("!", self)
210
+
211
+ # -------------------------------------------------------------------------
212
+ # Type casting and wrapper methods
213
+ # -------------------------------------------------------------------------
214
+
215
+ def as_(self, typ_: "_T | type[_T]") -> "_T":
216
+ """Cast this expression to a type or use as a decorator.
217
+
218
+ Usage as decorator:
219
+ @Import(...).as_
220
+ def fn(): ...
221
+
222
+ Usage for type casting:
223
+ clsx = Import(...).as_(Callable[[str, ...], str])
224
+
225
+ If typ_ is a user-defined callable (function or lambda),
226
+ wraps the expression in a Signature node that stores the callable's
227
+ signature for type introspection.
228
+ """
229
+ # Only wrap for user-defined functions (lambdas, def functions)
230
+ # Skip for types (str, int, etc.) used as type annotations
231
+ if isfunction(typ_):
232
+ try:
233
+ sig = signature(typ_)
234
+ return cast("_T", Signature(self, sig))
235
+ except (ValueError, TypeError):
236
+ # Signature not available (e.g., for built-ins), return self
237
+ pass
238
+
239
+ return cast("_T", self)
240
+
241
+ def jsx(self) -> "Jsx":
242
+ """Wrap this expression as a JSX component.
243
+
244
+ When called in transpiled code, produces Element(tag=self, ...).
245
+ """
246
+ return Jsx(self)
247
+
248
+ # -------------------------------------------------------------------------
249
+ # Registry for Python value -> Expr mapping
250
+ # -------------------------------------------------------------------------
251
+
252
+ @staticmethod
253
+ def of(value: Any) -> Expr:
254
+ """Convert a Python value to an Expr.
255
+
256
+ Resolution order:
257
+ 1. Already an Expr: returned as-is
258
+ 2. Registered in EXPR_REGISTRY: return the registered expr
259
+ 3. Primitives: str/int/float -> Literal, bool -> Literal, None -> Literal(None)
260
+ 4. Collections: list/tuple -> Array, dict -> Object (recursively converted)
261
+ 5. set -> Call(Identifier("Set"), [Array(...)])
262
+
263
+ Raises TypeError for unconvertible values.
264
+ """
265
+ # Already an Expr
266
+ if isinstance(value, Expr):
267
+ return value
268
+
269
+ # Check registry (for modules, functions, etc.)
270
+ if (expr := EXPR_REGISTRY.get(id(value))) is not None:
271
+ return expr
272
+
273
+ # Primitives - must check bool before int since bool is subclass of int
274
+ if isinstance(value, bool):
275
+ return Literal(value)
276
+ if isinstance(value, (int, float, str)):
277
+ return Literal(value)
278
+ if value is None:
279
+ return Literal(None)
280
+
281
+ # Collections
282
+ if isinstance(value, (list, tuple)):
283
+ return Array([Expr.of(v) for v in value])
284
+ if isinstance(value, dict):
285
+ props = [(str(k), Expr.of(v)) for k, v in value.items()] # pyright: ignore[reportUnknownArgumentType]
286
+ return Object(props)
287
+ if isinstance(value, set):
288
+ # new Set([...])
289
+ return New(Identifier("Set"), [Array([Expr.of(v) for v in value])])
290
+
291
+ raise TypeError(f"Cannot convert {type(value).__name__} to Expr")
292
+
293
+ @staticmethod
294
+ def register(value: Any, expr: Expr | Callable[..., Expr]) -> None:
295
+ """Register a Python value for conversion via Expr.of().
296
+
297
+ Args:
298
+ value: The Python object to register (function, constant, etc.)
299
+ expr: Either an Expr or a Callable[..., Expr] (will be wrapped in Transformer)
300
+ """
301
+ if callable(expr) and not isinstance(expr, Expr):
302
+ expr = Transformer(expr)
303
+ EXPR_REGISTRY[id(value)] = expr
304
+
305
+
306
+ class Stmt(ABC):
307
+ """Base class for statement nodes."""
308
+
309
+ __slots__: tuple[str, ...] = ()
310
+
311
+ @abstractmethod
312
+ def emit(self, out: list[str]) -> None:
313
+ """Emit this statement as JavaScript code into the output buffer."""
314
+
315
+
316
+ # =============================================================================
317
+ # Data Nodes
318
+ # =============================================================================
319
+
320
+
321
+ class ExprWrapper(Expr):
322
+ """Base class for Expr wrappers that delegate to an underlying expression.
323
+
324
+ Subclasses must define an `expr` attribute (via __slots__ or dataclass).
325
+ All Expr methods delegate to self.expr by default. Override specific
326
+ methods to customize behavior.
327
+ """
328
+
329
+ __slots__: tuple[str, ...] = ("expr",)
330
+ expr: Expr
331
+
332
+ @override
333
+ def emit(self, out: list[str]) -> None:
334
+ self.expr.emit(out)
335
+
336
+ @override
337
+ def render(self) -> VDOMPrimitive | VDOMExpr:
338
+ return self.expr.render()
339
+
340
+ @override
341
+ def precedence(self) -> int:
342
+ return self.expr.precedence()
343
+
344
+ @override
345
+ def transpile_call(
346
+ self,
347
+ args: list[ast.expr],
348
+ keywords: list[ast.keyword],
349
+ ctx: Transpiler,
350
+ ) -> Expr:
351
+ return self.expr.transpile_call(args, keywords, ctx)
352
+
353
+ @override
354
+ def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
355
+ return self.expr.transpile_getattr(attr, ctx)
356
+
357
+ @override
358
+ def transpile_subscript(self, key: ast.expr, ctx: Transpiler) -> Expr:
359
+ return self.expr.transpile_subscript(key, ctx)
360
+
361
+ @override
362
+ def __call__(self, *args: object, **kwargs: object) -> Expr: # pyright: ignore[reportIncompatibleMethodOverride]
363
+ return self.expr(*args, **kwargs)
364
+
365
+ @override
366
+ def __getitem__(self, key: object) -> Expr: # pyright: ignore[reportIncompatibleMethodOverride]
367
+ return self.expr[key]
368
+
369
+ @override
370
+ def __getattr__(self, attr: str) -> Expr: # pyright: ignore[reportIncompatibleMethodOverride]
371
+ return getattr(self.expr, attr)
372
+
373
+
374
+ @dataclass(slots=True, init=False)
375
+ class Jsx(ExprWrapper):
376
+ """JSX wrapper that makes any Expr callable as a component.
377
+
378
+ When called in transpiled code, produces Element(tag=expr, ...).
379
+ This enables patterns like `Jsx(Member(AppShell, "Header"))` to emit
380
+ `<AppShell.Header ... />`.
381
+
382
+ Example:
383
+ app_shell = Import("AppShell", "@mantine/core")
384
+ Header = Jsx(Member(app_shell, "Header"))
385
+ # In @javascript:
386
+ # Header(height=60) -> <AppShell_1.Header height={60} />
387
+ """
388
+
389
+ expr: Expr
390
+ id: str
391
+
392
+ def __init__(self, expr: Expr) -> None:
393
+ from pulse.transpiler.id import next_id
394
+
395
+ self.expr = expr
396
+ self.id = next_id()
397
+
398
+ @override
399
+ def transpile_call(
400
+ self,
401
+ args: list[ast.expr],
402
+ keywords: list[ast.keyword],
403
+ ctx: "Transpiler",
404
+ ) -> Expr:
405
+ """Transpile a call to this JSX wrapper into an Element.
406
+
407
+ Positional args become children, keyword args become props.
408
+ The `key` kwarg is extracted specially. Spread (**expr) is supported.
409
+ """
410
+ children: list[Node] = [ctx.emit_expr(a) for a in args]
411
+
412
+ props: list[tuple[str, Prop] | Spread] = []
413
+ key: str | Expr | None = None
414
+ for kw in keywords:
415
+ if kw.arg is None:
416
+ # **spread syntax
417
+ props.append(spread_dict(ctx.emit_expr(kw.value)))
418
+ else:
419
+ k = kw.arg
420
+ v = ctx.emit_expr(kw.value)
421
+ if k == "key":
422
+ # Accept any expression as key for transpilation
423
+ if isinstance(v, Literal) and isinstance(v.value, str):
424
+ key = v.value # Optimize string literals
425
+ else:
426
+ key = v # Keep as expression
427
+ else:
428
+ props.append((k, v))
429
+
430
+ return Element(
431
+ tag=self.expr,
432
+ props=props if props else None,
433
+ children=children if children else None,
434
+ key=key,
435
+ )
436
+
437
+ @override
438
+ def __call__(self, *args: Any, **kwargs: Any) -> Element:
439
+ """Allow calling Jsx in Python code.
440
+
441
+ Supports two usage patterns:
442
+ 1. Decorator: @Jsx(expr) def Component(...): ...
443
+ 2. Call: Jsx(expr)(props, children) -> Element
444
+ """
445
+
446
+ # Normal call: build Element
447
+ props: dict[str, Any] = {}
448
+ key: str | None = None
449
+ children: list[Node] = list(args)
450
+
451
+ for k, v in kwargs.items():
452
+ if k == "key":
453
+ if v is None:
454
+ continue
455
+ if not isinstance(v, str):
456
+ raise ValueError("key must be a string")
457
+ key = v
458
+ else:
459
+ props[k] = v
460
+
461
+ return Element(
462
+ tag=self.expr,
463
+ props=props if props else None,
464
+ children=children if children else None,
465
+ key=key,
466
+ )
467
+
468
+
469
+ @dataclass(slots=True)
470
+ class Signature(ExprWrapper):
471
+ """Wraps an Expr with signature information for type checking.
472
+
473
+ When you call expr.as_(callable_type), this creates a Signature wrapper
474
+ that stores the callable's signature for introspection, while delegating
475
+ all other behavior to the wrapped expression.
476
+
477
+ Example:
478
+ button = Import("Button", "@mantine/core")
479
+ typed_button = Signature(button, signature_of_callable)
480
+ """
481
+
482
+ expr: Expr
483
+ sig: Any # inspect.Signature, but use Any for type compatibility
484
+
485
+
486
+ @dataclass(slots=True)
487
+ class Value(Expr):
488
+ """Wraps a non-primitive Python value for pass-through serialization.
489
+
490
+ Use cases:
491
+ - Complex prop values: options={"a": 1, "b": 2}
492
+ - Server-computed data passed to client components
493
+ - Any value that doesn't need expression semantics
494
+ """
495
+
496
+ value: Any
497
+
498
+ @override
499
+ def emit(self, out: list[str]) -> None:
500
+ _emit_value(self.value, out)
501
+
502
+ @override
503
+ def render(self) -> VDOMExpr:
504
+ raise TypeError(
505
+ "Value cannot be rendered as VDOMExpr; unwrap with .value instead"
506
+ )
507
+
508
+
509
+ class Element(Expr):
510
+ """A React element: built-in tag, fragment, or client component.
511
+
512
+ Tag conventions:
513
+ - "" (empty string): Fragment
514
+ - "div", "span", etc.: HTML element
515
+ - "$$ComponentId": Client component (registered in JS registry)
516
+ - Expr (Import, Member, etc.): Direct component reference for transpilation
517
+
518
+ Props can be either:
519
+ - tuple[str, Prop]: key-value pair
520
+ - Spread: spread expression (...expr)
521
+ """
522
+
523
+ __slots__: tuple[str, ...] = ("tag", "props", "children", "key")
524
+
525
+ tag: str | Expr
526
+ props: Sequence[tuple[str, Prop] | Spread] | dict[str, Any] | None
527
+ children: Sequence[Node] | None
528
+ key: str | Expr | None
529
+
530
+ def __init__(
531
+ self,
532
+ tag: str | Expr,
533
+ props: Sequence[tuple[str, Prop] | Spread] | dict[str, Any] | None = None,
534
+ children: Sequence[Node] | None = None,
535
+ key: str | Expr | None = None,
536
+ ) -> None:
537
+ self.tag = tag
538
+ self.props = props
539
+ if children is None:
540
+ self.children = None
541
+ else:
542
+ if isinstance(tag, str):
543
+ parent_name = tag[2:] if tag.startswith("$$") else tag
544
+ else:
545
+ tag_out: list[str] = []
546
+ tag.emit(tag_out)
547
+ parent_name = "".join(tag_out)
548
+ self.children = flatten_children(
549
+ children,
550
+ parent_name=parent_name,
551
+ warn_stacklevel=5,
552
+ )
553
+ self.key = key
554
+
555
+ def _emit_key(self, out: list[str]) -> None:
556
+ """Emit key prop (string or expression)."""
557
+ if self.key is None:
558
+ return
559
+ if isinstance(self.key, str):
560
+ out.append('key="')
561
+ out.append(_escape_jsx_attr(self.key))
562
+ out.append('"')
563
+ else:
564
+ # Expression key: key={expr}
565
+ out.append("key={")
566
+ self.key.emit(out)
567
+ out.append("}")
568
+
569
+ @override
570
+ def emit(self, out: list[str]) -> None:
571
+ # Fragment (only for string tags)
572
+ if self.tag == "":
573
+ if self.key is not None:
574
+ # Fragment with key needs explicit Fragment component
575
+ out.append("<Fragment ")
576
+ self._emit_key(out)
577
+ out.append(">")
578
+ for c in self.children or []:
579
+ _emit_jsx_child(c, out)
580
+ out.append("</Fragment>")
581
+ else:
582
+ out.append("<>")
583
+ for c in self.children or []:
584
+ _emit_jsx_child(c, out)
585
+ out.append("</>")
586
+ return
587
+
588
+ # Resolve tag - either emit Expr or use string (strip $$ prefix)
589
+ tag_out: list[str] = []
590
+ if isinstance(self.tag, Expr):
591
+ self.tag.emit(tag_out)
592
+ else:
593
+ tag_out.append(self.tag[2:] if self.tag.startswith("$$") else self.tag)
594
+ tag_str = "".join(tag_out)
595
+
596
+ # Build props into a separate buffer to check if empty
597
+ props_out: list[str] = []
598
+ if self.key is not None:
599
+ self._emit_key(props_out)
600
+ if self.props:
601
+ # Handle both dict (from render path) and sequence (from transpilation)
602
+ # Dict case: items() yields tuple[str, Any], never Spread
603
+ # Sequence case: already list[tuple[str, Prop] | Spread]
604
+ props_iter: Iterable[tuple[str, Any]] | Sequence[tuple[str, Prop] | Spread]
605
+ if isinstance(self.props, dict):
606
+ props_iter = self.props.items()
607
+ else:
608
+ props_iter = self.props
609
+ for prop in props_iter:
610
+ if props_out:
611
+ props_out.append(" ")
612
+ if isinstance(prop, Spread):
613
+ props_out.append("{...")
614
+ prop.expr.emit(props_out)
615
+ props_out.append("}")
616
+ else:
617
+ name, value = prop
618
+ _emit_jsx_prop(name, value, props_out)
619
+
620
+ # Build children into a separate buffer to check if empty
621
+ children_out: list[str] = []
622
+ for c in self.children or []:
623
+ _emit_jsx_child(c, children_out)
624
+
625
+ # Self-closing if no children
626
+ if not children_out:
627
+ out.append("<")
628
+ out.append(tag_str)
629
+ if props_out:
630
+ out.append(" ")
631
+ out.extend(props_out)
632
+ out.append(" />")
633
+ return
634
+
635
+ # Open tag
636
+ out.append("<")
637
+ out.append(tag_str)
638
+ if props_out:
639
+ out.append(" ")
640
+ out.extend(props_out)
641
+ out.append(">")
642
+ # Children
643
+ out.extend(children_out)
644
+ # Close tag
645
+ out.append("</")
646
+ out.append(tag_str)
647
+ out.append(">")
648
+
649
+ @override
650
+ def transpile_subscript(self, key: ast.expr, ctx: Transpiler) -> Expr:
651
+ """Transpile subscript as adding children to this element.
652
+
653
+ Handles both single children and tuple of children.
654
+ """
655
+ if self.children:
656
+ raise TranspileError(
657
+ f"Element '{self.tag}' already has children; cannot add more via subscript"
658
+ )
659
+
660
+ # Convert key to list of children
661
+ if isinstance(key, ast.Tuple):
662
+ children = [ctx.emit_expr(e) for e in key.elts]
663
+ else:
664
+ children = [ctx.emit_expr(key)]
665
+
666
+ return Element(
667
+ tag=self.tag,
668
+ props=self.props,
669
+ children=children,
670
+ key=self.key,
671
+ )
672
+
673
+ @override
674
+ def __getitem__(self, key: Any) -> Element: # pyright: ignore[reportIncompatibleMethodOverride]
675
+ """Return new Element with children set via subscript.
676
+
677
+ Raises if this element already has children.
678
+ Accepts a single child or a Sequence of children.
679
+ """
680
+ if self.children:
681
+ raise ValueError(
682
+ f"Element '{self.tag}' already has children; cannot add more via subscript"
683
+ )
684
+
685
+ # Convert key to sequence of children
686
+ if isinstance(key, (list, tuple)):
687
+ children = list(cast(list[Any] | tuple[Any, ...], key))
688
+ else:
689
+ children = [key]
690
+
691
+ return Element(
692
+ tag=self.tag,
693
+ props=self.props,
694
+ children=children,
695
+ key=self.key,
696
+ )
697
+
698
+ def with_children(self, children: Sequence[Node]) -> Element:
699
+ """Return new Element with children set.
700
+
701
+ Raises if this element already has children.
702
+ """
703
+ if self.children:
704
+ raise ValueError(
705
+ f"Element '{self.tag}' already has children; cannot add more via subscript"
706
+ )
707
+ return Element(
708
+ tag=self.tag,
709
+ props=self.props,
710
+ children=list(children),
711
+ key=self.key,
712
+ )
713
+
714
+ def props_dict(self) -> dict[str, Any]:
715
+ """Convert props to dict for rendering.
716
+
717
+ Raises TypeError if props contain Spread (only valid in transpilation).
718
+ """
719
+ if not self.props:
720
+ return {}
721
+ # Already a dict (from renderer reconciliation)
722
+ if isinstance(self.props, dict):
723
+ return self.props
724
+ # Sequence of (key, value) pairs or Spread
725
+ result: dict[str, Any] = {}
726
+ for prop in self.props:
727
+ if isinstance(prop, Spread):
728
+ raise TypeError(
729
+ "Element with spread props cannot be rendered; spread is only valid during transpilation"
730
+ )
731
+ k, v = prop
732
+ result[k] = v
733
+ return result
734
+
735
+ @override
736
+ def render(self):
737
+ """Element rendering is handled by Renderer.render_node(), not render().
738
+
739
+ This method validates render-time constraints and raises TypeError
740
+ because Element produces VDOMElement, not VDOMExpr.
741
+ """
742
+ # Validate key is string or numeric (not arbitrary Expr) during rendering
743
+ if self.key is not None and not isinstance(self.key, (str, int)):
744
+ raise TypeError(
745
+ f"Element key must be a string or int for rendering, got {type(self.key).__name__}. "
746
+ + "Expression keys are only valid during transpilation (emit)."
747
+ )
748
+ raise TypeError(
749
+ "Element cannot be rendered as VDOMExpr; use Renderer.render_node() instead"
750
+ )
751
+
752
+
753
+ @dataclass(slots=True)
754
+ class PulseNode:
755
+ """A Pulse server-side component instance.
756
+
757
+ During rendering, PulseNode is called and replaced by its returned tree.
758
+ Can only appear in VDOM context (render path), never in transpiled code.
759
+ """
760
+
761
+ fn: Any # Callable[..., Node]
762
+ args: tuple[Any, ...] = ()
763
+ kwargs: dict[str, Any] = field(default_factory=dict)
764
+ key: str | None = None
765
+ name: str | None = None # Optional component name for debug messages.
766
+ # Renderer state (mutable, set during render)
767
+ hooks: Any = None # HookContext
768
+ contents: Node | None = None
769
+
770
+ def emit(self, out: list[str]) -> None:
771
+ fn_name = getattr(self.fn, "__name__", "unknown")
772
+ raise TypeError(
773
+ f"Cannot transpile PulseNode '{fn_name}'. "
774
+ + "Server components must be rendered, not transpiled."
775
+ )
776
+
777
+ def __getitem__(self, children_arg: "Node | tuple[Node, ...]"):
778
+ if self.args:
779
+ raise ValueError(
780
+ "PulseNode already received positional args; pass children in the call or via brackets, not both."
781
+ )
782
+ if not isinstance(children_arg, tuple):
783
+ children_arg = (children_arg,)
784
+ parent_name = self.name
785
+ if parent_name is None:
786
+ parent_name = getattr(self.fn, "__name__", "Component")
787
+ flat = flatten_children(
788
+ children_arg,
789
+ parent_name=parent_name,
790
+ warn_stacklevel=5,
791
+ )
792
+ return PulseNode(
793
+ fn=self.fn,
794
+ args=tuple(flat),
795
+ kwargs=self.kwargs,
796
+ key=self.key,
797
+ name=self.name,
798
+ )
799
+
800
+
801
+ # =============================================================================
802
+ # Children normalization helpers
803
+ # =============================================================================
804
+ def flatten_children(
805
+ children: Sequence[Node | Iterable[Node]],
806
+ *,
807
+ parent_name: str,
808
+ warn_stacklevel: int = 5,
809
+ ) -> list[Node]:
810
+ if env.pulse_env == "dev":
811
+ return _flatten_children_dev(
812
+ children, parent_name=parent_name, warn_stacklevel=warn_stacklevel
813
+ )
814
+ return _flatten_children_prod(children)
815
+
816
+
817
+ def _flatten_children_prod(children: Sequence[Node | Iterable[Node]]) -> list[Node]:
818
+ flat: list[Node] = []
819
+
820
+ def visit(item: Node | Iterable[Node]) -> None:
821
+ if isinstance(item, dict):
822
+ raise TypeError("Dict is not a valid child")
823
+ if isinstance(item, Iterable) and not isinstance(item, str):
824
+ for sub in item:
825
+ visit(sub)
826
+ else:
827
+ flat.append(item)
828
+
829
+ for child in children:
830
+ visit(child)
831
+
832
+ return flat
833
+
834
+
835
+ def _flatten_children_dev(
836
+ children: Sequence[Node | Iterable[Node]],
837
+ *,
838
+ parent_name: str,
839
+ warn_stacklevel: int = 5,
840
+ ) -> list[Node]:
841
+ flat: list[Node] = []
842
+ seen_keys: set[str] = set()
843
+
844
+ def visit(item: Node | Iterable[Node]) -> None:
845
+ if isinstance(item, dict):
846
+ raise TypeError("Dict is not a valid child")
847
+ if isinstance(item, Iterable) and not isinstance(item, str):
848
+ missing_key = False
849
+ for sub in item:
850
+ if isinstance(sub, PulseNode) and sub.key is None:
851
+ missing_key = True
852
+ if isinstance(sub, Element) and _normalize_key(sub.key) is None:
853
+ missing_key = True
854
+ visit(sub) # type: ignore[arg-type]
855
+ if missing_key:
856
+ clean_name = clean_element_name(parent_name)
857
+ warnings.warn(
858
+ (
859
+ f"[Pulse] Iterable children of {clean_name} contain elements without 'key'. "
860
+ "Add a stable 'key' to each element inside iterables to improve reconciliation."
861
+ ),
862
+ stacklevel=warn_stacklevel,
863
+ )
864
+ else:
865
+ if isinstance(item, PulseNode) and item.key is not None:
866
+ if item.key in seen_keys:
867
+ clean_name = clean_element_name(parent_name)
868
+ raise ValueError(
869
+ f"[Pulse] Duplicate key '{item.key}' found among children of {clean_name}. "
870
+ + "Keys must be unique per sibling set."
871
+ )
872
+ seen_keys.add(item.key)
873
+ if isinstance(item, Element):
874
+ key = _normalize_key(item.key)
875
+ if key is not None:
876
+ if key in seen_keys:
877
+ clean_name = clean_element_name(parent_name)
878
+ raise ValueError(
879
+ f"[Pulse] Duplicate key '{key}' found among children of {clean_name}. "
880
+ + "Keys must be unique per sibling set."
881
+ )
882
+ seen_keys.add(key)
883
+ flat.append(item)
884
+
885
+ for child in children:
886
+ visit(child)
887
+
888
+ return flat
889
+
890
+
891
+ def clean_element_name(parent_name: str) -> str:
892
+ if parent_name.startswith("<") and parent_name.endswith(">"):
893
+ return parent_name
894
+ return f"<{parent_name}>"
895
+
896
+
897
+ def _normalize_key(key: object | None) -> str | None:
898
+ if isinstance(key, Literal):
899
+ return key.value if isinstance(key.value, str) else None
900
+ return key if isinstance(key, str) else None
901
+
902
+
903
+ # =============================================================================
904
+ # Expression Nodes
905
+ # =============================================================================
906
+
907
+
908
+ @dataclass(slots=True)
909
+ class Identifier(Expr):
910
+ """JS identifier: x, foo, myFunc"""
911
+
912
+ name: str
913
+
914
+ @override
915
+ def emit(self, out: list[str]) -> None:
916
+ out.append(self.name)
917
+
918
+ @override
919
+ def render(self) -> VDOMExpr:
920
+ return {"t": "id", "name": self.name}
921
+
922
+
923
+ @dataclass(slots=True)
924
+ class Literal(Expr):
925
+ """JS literal: 42, "hello", true, null"""
926
+
927
+ value: int | float | str | bool | None
928
+
929
+ @override
930
+ def emit(self, out: list[str]) -> None:
931
+ if self.value is None:
932
+ out.append("null")
933
+ elif isinstance(self.value, bool):
934
+ out.append("true" if self.value else "false")
935
+ elif isinstance(self.value, str):
936
+ out.append('"')
937
+ out.append(_escape_string(self.value))
938
+ out.append('"')
939
+ else:
940
+ out.append(str(self.value))
941
+
942
+ @override
943
+ def render(self) -> VDOMPrimitive:
944
+ return self.value
945
+
946
+
947
+ class Undefined(Expr):
948
+ """JS undefined literal.
949
+
950
+ Use Undefined() for JS `undefined`. Literal(None) emits `null`.
951
+ This is a singleton-like class with no fields.
952
+ """
953
+
954
+ __slots__: tuple[str, ...] = ()
955
+
956
+ @override
957
+ def emit(self, out: list[str]) -> None:
958
+ out.append("undefined")
959
+
960
+ @override
961
+ def render(self) -> VDOMExpr:
962
+ return {"t": "undef"}
963
+
964
+
965
+ # Singleton instance for convenience
966
+ UNDEFINED = Undefined()
967
+
968
+
969
+ @dataclass(slots=True)
970
+ class Array(Expr):
971
+ """JS array: [a, b, c]"""
972
+
973
+ elements: Sequence[Expr]
974
+
975
+ @override
976
+ def emit(self, out: list[str]) -> None:
977
+ out.append("[")
978
+ for i, e in enumerate(self.elements):
979
+ if i > 0:
980
+ out.append(", ")
981
+ e.emit(out)
982
+ out.append("]")
983
+
984
+ @override
985
+ def render(self) -> VDOMExpr:
986
+ return {"t": "array", "items": [e.render() for e in self.elements]}
987
+
988
+
989
+ @dataclass(slots=True)
990
+ class Object(Expr):
991
+ """JS object: { key: value, ...spread }
992
+
993
+ Props can be either:
994
+ - tuple[str, Expr]: key-value pair
995
+ - Spread: spread expression (...expr)
996
+ """
997
+
998
+ props: Sequence[tuple[str, Expr] | Spread]
999
+
1000
+ @override
1001
+ def emit(self, out: list[str]) -> None:
1002
+ out.append("{")
1003
+ for i, prop in enumerate(self.props):
1004
+ if i > 0:
1005
+ out.append(", ")
1006
+ if isinstance(prop, Spread):
1007
+ prop.emit(out)
1008
+ else:
1009
+ k, v = prop
1010
+ out.append('"')
1011
+ out.append(_escape_string(k))
1012
+ out.append('": ')
1013
+ v.emit(out)
1014
+ out.append("}")
1015
+
1016
+ @override
1017
+ def render(self) -> VDOMExpr:
1018
+ rendered_props: dict[str, VDOMNode] = {}
1019
+ for prop in self.props:
1020
+ if isinstance(prop, Spread):
1021
+ raise TypeError("Object spread cannot be rendered to VDOM")
1022
+ k, v = prop
1023
+ rendered_props[k] = v.render()
1024
+ return {"t": "object", "props": rendered_props}
1025
+
1026
+
1027
+ @dataclass(slots=True)
1028
+ class Member(Expr):
1029
+ """JS member access: obj.prop"""
1030
+
1031
+ obj: Expr
1032
+ prop: str
1033
+
1034
+ @override
1035
+ def emit(self, out: list[str]) -> None:
1036
+ _emit_primary(self.obj, out)
1037
+ out.append(".")
1038
+ out.append(self.prop)
1039
+
1040
+ @override
1041
+ def render(self) -> VDOMExpr:
1042
+ return {"t": "member", "obj": self.obj.render(), "prop": self.prop}
1043
+
1044
+
1045
+ @dataclass(slots=True)
1046
+ class Subscript(Expr):
1047
+ """JS subscript access: obj[key]"""
1048
+
1049
+ obj: Expr
1050
+ key: Expr
1051
+
1052
+ @override
1053
+ def emit(self, out: list[str]) -> None:
1054
+ _emit_primary(self.obj, out)
1055
+ out.append("[")
1056
+ self.key.emit(out)
1057
+ out.append("]")
1058
+
1059
+ @override
1060
+ def render(self) -> VDOMExpr:
1061
+ return {"t": "sub", "obj": self.obj.render(), "key": self.key.render()}
1062
+
1063
+
1064
+ @dataclass(slots=True)
1065
+ class Call(Expr):
1066
+ """JS function call: fn(args)"""
1067
+
1068
+ callee: Expr
1069
+ args: Sequence[Expr]
1070
+
1071
+ @override
1072
+ def emit(self, out: list[str]) -> None:
1073
+ _emit_primary(self.callee, out)
1074
+ out.append("(")
1075
+ for i, a in enumerate(self.args):
1076
+ if i > 0:
1077
+ out.append(", ")
1078
+ a.emit(out)
1079
+ out.append(")")
1080
+
1081
+ @override
1082
+ def render(self) -> VDOMExpr:
1083
+ return {
1084
+ "t": "call",
1085
+ "callee": self.callee.render(),
1086
+ "args": [a.render() for a in self.args],
1087
+ }
1088
+
1089
+
1090
+ @dataclass(slots=True)
1091
+ class Unary(Expr):
1092
+ """JS unary expression: -x, !x, typeof x"""
1093
+
1094
+ op: str
1095
+ operand: Expr
1096
+
1097
+ @override
1098
+ def precedence(self) -> int:
1099
+ op = self.op
1100
+ tag = "+u" if op == "+" else ("-u" if op == "-" else op)
1101
+ return _PRECEDENCE.get(tag, 17)
1102
+
1103
+ @override
1104
+ def emit(self, out: list[str]) -> None:
1105
+ if self.op in {"typeof", "await", "void", "delete"}:
1106
+ out.append(self.op)
1107
+ out.append(" ")
1108
+ else:
1109
+ out.append(self.op)
1110
+ _emit_paren(self.operand, self.op, "unary", out)
1111
+
1112
+ @override
1113
+ def render(self) -> VDOMExpr:
1114
+ if self.op == "await":
1115
+ raise TypeError("await is not supported in VDOM expressions")
1116
+ return {"t": "unary", "op": self.op, "arg": self.operand.render()}
1117
+
1118
+
1119
+ @dataclass(slots=True)
1120
+ class Binary(Expr):
1121
+ """JS binary expression: x + y, a && b"""
1122
+
1123
+ left: Expr
1124
+ op: str
1125
+ right: Expr
1126
+
1127
+ @override
1128
+ def precedence(self) -> int:
1129
+ return _PRECEDENCE.get(self.op, 0)
1130
+
1131
+ @override
1132
+ def emit(self, out: list[str]) -> None:
1133
+ # Special: ** with unary +/- on left needs parens
1134
+ force_left = (
1135
+ self.op == "**"
1136
+ and isinstance(self.left, Unary)
1137
+ and self.left.op in {"-", "+"}
1138
+ )
1139
+ if force_left:
1140
+ out.append("(")
1141
+ self.left.emit(out)
1142
+ out.append(")")
1143
+ else:
1144
+ _emit_paren(self.left, self.op, "left", out)
1145
+ out.append(" ")
1146
+ out.append(self.op)
1147
+ out.append(" ")
1148
+ _emit_paren(self.right, self.op, "right", out)
1149
+
1150
+ @override
1151
+ def render(self) -> VDOMExpr:
1152
+ return {
1153
+ "t": "binary",
1154
+ "op": self.op,
1155
+ "left": self.left.render(),
1156
+ "right": self.right.render(),
1157
+ }
1158
+
1159
+
1160
+ @dataclass(slots=True)
1161
+ class Ternary(Expr):
1162
+ """JS ternary expression: cond ? a : b"""
1163
+
1164
+ cond: Expr
1165
+ then: Expr
1166
+ else_: Expr
1167
+
1168
+ @override
1169
+ def precedence(self) -> int:
1170
+ return _PRECEDENCE["?:"]
1171
+
1172
+ @override
1173
+ def emit(self, out: list[str]) -> None:
1174
+ self.cond.emit(out)
1175
+ out.append(" ? ")
1176
+ self.then.emit(out)
1177
+ out.append(" : ")
1178
+ self.else_.emit(out)
1179
+
1180
+ @override
1181
+ def render(self) -> VDOMExpr:
1182
+ return {
1183
+ "t": "ternary",
1184
+ "cond": self.cond.render(),
1185
+ "then": self.then.render(),
1186
+ "else_": self.else_.render(),
1187
+ }
1188
+
1189
+
1190
+ @dataclass(slots=True)
1191
+ class Arrow(Expr):
1192
+ """JS arrow function: (x) => expr or (x) => { ... }
1193
+
1194
+ body can be:
1195
+ - Expr: expression body, emits as `() => expr`
1196
+ - Sequence[Stmt]: statement body, emits as `() => { stmt1; stmt2; }`
1197
+ """
1198
+
1199
+ params: Sequence[str]
1200
+ body: Expr | Sequence[Stmt]
1201
+
1202
+ @override
1203
+ def precedence(self) -> int:
1204
+ # Arrow functions have very low precedence (assignment level)
1205
+ # This ensures they get wrapped in parens when used as callee in Call
1206
+ return 3
1207
+
1208
+ @override
1209
+ def emit(self, out: list[str]) -> None:
1210
+ if len(self.params) == 1:
1211
+ out.append(self.params[0])
1212
+ else:
1213
+ out.append("(")
1214
+ out.append(", ".join(self.params))
1215
+ out.append(")")
1216
+ out.append(" => ")
1217
+ if isinstance(self.body, Expr):
1218
+ self.body.emit(out)
1219
+ else:
1220
+ out.append("{ ")
1221
+ for stmt in self.body:
1222
+ stmt.emit(out)
1223
+ out.append(" ")
1224
+ out.append("}")
1225
+
1226
+ @override
1227
+ def render(self) -> VDOMExpr:
1228
+ if not isinstance(self.body, Expr):
1229
+ raise TypeError("Arrow with statement body cannot be rendered as VDOMExpr")
1230
+ return {"t": "arrow", "params": list(self.params), "body": self.body.render()}
1231
+
1232
+
1233
+ @dataclass(slots=True)
1234
+ class Template(Expr):
1235
+ """JS template literal: `hello ${name}`
1236
+
1237
+ Parts alternate: [str, Expr, str, Expr, str, ...]
1238
+ Always starts and ends with a string (may be empty).
1239
+ """
1240
+
1241
+ parts: Sequence[str | Expr] # alternating, starting with str
1242
+
1243
+ @override
1244
+ def emit(self, out: list[str]) -> None:
1245
+ out.append("`")
1246
+ for p in self.parts:
1247
+ if isinstance(p, str):
1248
+ out.append(_escape_template(p))
1249
+ else:
1250
+ out.append("${")
1251
+ p.emit(out)
1252
+ out.append("}")
1253
+ out.append("`")
1254
+
1255
+ @override
1256
+ def render(self) -> VDOMExpr:
1257
+ rendered_parts: list[str | VDOMNode] = []
1258
+ for p in self.parts:
1259
+ if isinstance(p, str):
1260
+ rendered_parts.append(p)
1261
+ else:
1262
+ rendered_parts.append(p.render())
1263
+ return {"t": "template", "parts": rendered_parts}
1264
+
1265
+
1266
+ @dataclass(slots=True)
1267
+ class Spread(Expr):
1268
+ """JS spread: ...expr"""
1269
+
1270
+ expr: Expr
1271
+
1272
+ @override
1273
+ def emit(self, out: list[str]) -> None:
1274
+ out.append("...")
1275
+ self.expr.emit(out)
1276
+
1277
+ @override
1278
+ def render(self) -> VDOMExpr:
1279
+ raise TypeError("Spread cannot be rendered as VDOMExpr directly")
1280
+
1281
+
1282
+ def spread_dict(expr: Expr) -> Spread:
1283
+ """Wrap a spread expression with Map-to-object conversion.
1284
+
1285
+ Python dicts transpile to Map, which has no enumerable own properties.
1286
+ This wraps the spread with an IIFE that converts Map to object:
1287
+ (...expr) -> ...($s => $s instanceof Map ? Object.fromEntries($s) : $s)(expr)
1288
+
1289
+ The IIFE ensures expr is evaluated only once.
1290
+ """
1291
+ s = Identifier("$s")
1292
+ is_map = Binary(s, "instanceof", Identifier("Map"))
1293
+ as_obj = Call(Member(Identifier("Object"), "fromEntries"), [s])
1294
+ return Spread(Call(Arrow(["$s"], Ternary(is_map, as_obj, s)), [expr]))
1295
+
1296
+
1297
+ @dataclass(slots=True)
1298
+ class New(Expr):
1299
+ """JS new expression: new Ctor(args)"""
1300
+
1301
+ ctor: Expr
1302
+ args: Sequence[Expr]
1303
+
1304
+ @override
1305
+ def emit(self, out: list[str]) -> None:
1306
+ out.append("new ")
1307
+ self.ctor.emit(out)
1308
+ out.append("(")
1309
+ for i, a in enumerate(self.args):
1310
+ if i > 0:
1311
+ out.append(", ")
1312
+ a.emit(out)
1313
+ out.append(")")
1314
+
1315
+ @override
1316
+ def render(self) -> VDOMExpr:
1317
+ return {
1318
+ "t": "new",
1319
+ "ctor": self.ctor.render(),
1320
+ "args": [a.render() for a in self.args],
1321
+ }
1322
+
1323
+
1324
+ class TransformerFn(Protocol):
1325
+ def __call__(self, *args: Any, ctx: Transpiler, **kwargs: Any) -> Expr: ...
1326
+
1327
+
1328
+ _F = TypeVar("_F", bound=TransformerFn)
1329
+
1330
+
1331
+ @dataclass(slots=True)
1332
+ class Transformer(Expr, Generic[_F]):
1333
+ """Expr that wraps a function transforming args to Expr output.
1334
+
1335
+ Used for Python->JS transpilation of functions, builtins, and module attrs.
1336
+ The wrapped function receives args/kwargs and ctx, and returns an Expr.
1337
+
1338
+ Example:
1339
+ emit_len = Transformer(lambda x, ctx: Member(ctx.emit_expr(x), "length"), name="len")
1340
+ # When called: emit_len.transpile_call([some_ast], {}, ctx) -> Member(some_expr, "length")
1341
+ """
1342
+
1343
+ fn: _F
1344
+ name: str = "" # For error messages
1345
+
1346
+ @override
1347
+ def emit(self, out: list[str]) -> None:
1348
+ label = self.name or "Transformer"
1349
+ raise TypeError(f"{label} cannot be emitted directly - must be called")
1350
+
1351
+ @override
1352
+ def transpile_call(
1353
+ self,
1354
+ args: list[ast.expr],
1355
+ keywords: list[ast.keyword],
1356
+ ctx: Transpiler,
1357
+ ) -> Expr:
1358
+ # Convert keywords to dict, reject spreads
1359
+ kwargs: dict[str, ast.expr] = {}
1360
+ for kw in keywords:
1361
+ if kw.arg is None:
1362
+ label = self.name or "Function"
1363
+ raise TranspileError(f"{label} does not support **spread")
1364
+ kwargs[kw.arg] = kw.value
1365
+ if kwargs:
1366
+ return self.fn(*args, ctx=ctx, **kwargs)
1367
+ return self.fn(*args, ctx=ctx)
1368
+
1369
+ @override
1370
+ def transpile_getattr(self, attr: str, ctx: Transpiler) -> Expr:
1371
+ label = self.name or "Transformer"
1372
+ raise TypeError(f"{label} cannot have attributes")
1373
+
1374
+ @override
1375
+ def transpile_subscript(self, key: ast.expr, ctx: Transpiler) -> Expr:
1376
+ label = self.name or "Transformer"
1377
+ raise TypeError(f"{label} cannot be subscripted")
1378
+
1379
+ @override
1380
+ def render(self) -> VDOMExpr:
1381
+ label = self.name or "Transformer"
1382
+ raise TypeError(f"{label} cannot be rendered - must be called")
1383
+
1384
+
1385
+ @overload
1386
+ def transformer(arg: str) -> Callable[[_F], _F]: ...
1387
+
1388
+
1389
+ @overload
1390
+ def transformer(arg: _F) -> _F: ...
1391
+
1392
+
1393
+ def transformer(arg: str | _F) -> Callable[[_F], _F] | _F:
1394
+ """Decorator/helper for Transformer.
1395
+
1396
+ Usage:
1397
+ @transformer("len")
1398
+ def emit_len(x, *, ctx): ...
1399
+ or:
1400
+ emit_len = transformer(lambda x, *, ctx: ...)
1401
+
1402
+ Returns a Transformer, but the type signature lies and preserves
1403
+ the original function type.
1404
+ """
1405
+ if isinstance(arg, str):
1406
+
1407
+ def decorator(fn: _F) -> _F:
1408
+ return cast(_F, Transformer(fn, name=arg))
1409
+
1410
+ return decorator
1411
+ elif isfunction(arg):
1412
+ # Use empty name for lambdas, function name for named functions
1413
+ name = "" if arg.__name__ == "<lambda>" else arg.__name__
1414
+ return cast(_F, Transformer(arg, name=name))
1415
+ else:
1416
+ raise TypeError(
1417
+ "transformer expects a function or string (for decorator usage)"
1418
+ )
1419
+
1420
+
1421
+ # =============================================================================
1422
+ # Statement Nodes
1423
+ # =============================================================================
1424
+
1425
+
1426
+ @dataclass(slots=True)
1427
+ class Return(Stmt):
1428
+ """JS return statement: return expr;"""
1429
+
1430
+ value: Expr | None = None
1431
+
1432
+ @override
1433
+ def emit(self, out: list[str]) -> None:
1434
+ out.append("return")
1435
+ if self.value is not None:
1436
+ out.append(" ")
1437
+ self.value.emit(out)
1438
+ out.append(";")
1439
+
1440
+
1441
+ @dataclass(slots=True)
1442
+ class If(Stmt):
1443
+ """JS if statement: if (cond) { ... } else { ... }"""
1444
+
1445
+ cond: Expr
1446
+ then: Sequence[Stmt]
1447
+ else_: Sequence[Stmt] = ()
1448
+
1449
+ @override
1450
+ def emit(self, out: list[str]) -> None:
1451
+ out.append("if (")
1452
+ self.cond.emit(out)
1453
+ out.append(") {\n")
1454
+ for stmt in self.then:
1455
+ stmt.emit(out)
1456
+ out.append("\n")
1457
+ out.append("}")
1458
+ if self.else_:
1459
+ out.append(" else {\n")
1460
+ for stmt in self.else_:
1461
+ stmt.emit(out)
1462
+ out.append("\n")
1463
+ out.append("}")
1464
+
1465
+
1466
+ @dataclass(slots=True)
1467
+ class ForOf(Stmt):
1468
+ """JS for-of loop: for (const x of iter) { ... }
1469
+
1470
+ target can be a single name or array pattern for destructuring: [a, b]
1471
+ """
1472
+
1473
+ target: str
1474
+ iter: Expr
1475
+ body: Sequence[Stmt]
1476
+
1477
+ @override
1478
+ def emit(self, out: list[str]) -> None:
1479
+ out.append("for (const ")
1480
+ out.append(self.target)
1481
+ out.append(" of ")
1482
+ self.iter.emit(out)
1483
+ out.append(") {\n")
1484
+ for stmt in self.body:
1485
+ stmt.emit(out)
1486
+ out.append("\n")
1487
+ out.append("}")
1488
+
1489
+
1490
+ @dataclass(slots=True)
1491
+ class While(Stmt):
1492
+ """JS while loop: while (cond) { ... }"""
1493
+
1494
+ cond: Expr
1495
+ body: Sequence[Stmt]
1496
+
1497
+ @override
1498
+ def emit(self, out: list[str]) -> None:
1499
+ out.append("while (")
1500
+ self.cond.emit(out)
1501
+ out.append(") {\n")
1502
+ for stmt in self.body:
1503
+ stmt.emit(out)
1504
+ out.append("\n")
1505
+ out.append("}")
1506
+
1507
+
1508
+ @dataclass(slots=True)
1509
+ class Break(Stmt):
1510
+ """JS break statement."""
1511
+
1512
+ @override
1513
+ def emit(self, out: list[str]) -> None:
1514
+ out.append("break;")
1515
+
1516
+
1517
+ @dataclass(slots=True)
1518
+ class Continue(Stmt):
1519
+ """JS continue statement."""
1520
+
1521
+ @override
1522
+ def emit(self, out: list[str]) -> None:
1523
+ out.append("continue;")
1524
+
1525
+
1526
+ @dataclass(slots=True)
1527
+ class Assign(Stmt):
1528
+ """JS assignment: let x = expr; or x = expr; or x += expr;
1529
+
1530
+ declare: "let", "const", or None (reassignment)
1531
+ op: None for =, or "+", "-", etc. for augmented assignment
1532
+ """
1533
+
1534
+ target: str
1535
+ value: Expr
1536
+ declare: Lit["let", "const"] | None = None
1537
+ op: str | None = None # For augmented: +=, -=, etc.
1538
+
1539
+ @override
1540
+ def emit(self, out: list[str]) -> None:
1541
+ if self.declare:
1542
+ out.append(self.declare)
1543
+ out.append(" ")
1544
+ out.append(self.target)
1545
+ if self.op:
1546
+ out.append(" ")
1547
+ out.append(self.op)
1548
+ out.append("= ")
1549
+ else:
1550
+ out.append(" = ")
1551
+ self.value.emit(out)
1552
+ out.append(";")
1553
+
1554
+
1555
+ @dataclass(slots=True)
1556
+ class ExprStmt(Stmt):
1557
+ """JS expression statement: expr;"""
1558
+
1559
+ expr: Expr
1560
+
1561
+ @override
1562
+ def emit(self, out: list[str]) -> None:
1563
+ self.expr.emit(out)
1564
+ out.append(";")
1565
+
1566
+
1567
+ @dataclass(slots=True)
1568
+ class Block(Stmt):
1569
+ """JS block: { ... } - a sequence of statements."""
1570
+
1571
+ body: Sequence[Stmt]
1572
+
1573
+ @override
1574
+ def emit(self, out: list[str]) -> None:
1575
+ out.append("{\n")
1576
+ for stmt in self.body:
1577
+ stmt.emit(out)
1578
+ out.append("\n")
1579
+ out.append("}")
1580
+
1581
+
1582
+ @dataclass(slots=True)
1583
+ class StmtSequence(Stmt):
1584
+ """A sequence of statements without block braces.
1585
+
1586
+ Used for tuple unpacking where we need multiple statements
1587
+ but don't want to create a new scope.
1588
+ """
1589
+
1590
+ body: Sequence[Stmt]
1591
+
1592
+ @override
1593
+ def emit(self, out: list[str]) -> None:
1594
+ for i, stmt in enumerate(self.body):
1595
+ stmt.emit(out)
1596
+ if i < len(self.body) - 1:
1597
+ out.append("\n")
1598
+
1599
+
1600
+ @dataclass(slots=True)
1601
+ class Throw(Stmt):
1602
+ """JS throw statement: throw expr;"""
1603
+
1604
+ value: Expr
1605
+
1606
+ @override
1607
+ def emit(self, out: list[str]) -> None:
1608
+ out.append("throw ")
1609
+ self.value.emit(out)
1610
+ out.append(";")
1611
+
1612
+
1613
+ @dataclass(slots=True)
1614
+ class TryStmt(Stmt):
1615
+ """JS try/catch/finally statement.
1616
+
1617
+ try { body } catch (param) { handler } finally { finalizer }
1618
+ """
1619
+
1620
+ body: Sequence[Stmt]
1621
+ catch_param: str | None = None # None for bare except
1622
+ catch_body: Sequence[Stmt] | None = None
1623
+ finally_body: Sequence[Stmt] | None = None
1624
+
1625
+ @override
1626
+ def emit(self, out: list[str]) -> None:
1627
+ out.append("try {\n")
1628
+ for stmt in self.body:
1629
+ stmt.emit(out)
1630
+ out.append("\n")
1631
+ out.append("}")
1632
+
1633
+ if self.catch_body is not None:
1634
+ if self.catch_param:
1635
+ out.append(f" catch ({self.catch_param}) {{\n")
1636
+ else:
1637
+ out.append(" catch {\n")
1638
+ for stmt in self.catch_body:
1639
+ stmt.emit(out)
1640
+ out.append("\n")
1641
+ out.append("}")
1642
+
1643
+ if self.finally_body is not None:
1644
+ out.append(" finally {\n")
1645
+ for stmt in self.finally_body:
1646
+ stmt.emit(out)
1647
+ out.append("\n")
1648
+ out.append("}")
1649
+
1650
+
1651
+ @dataclass(slots=True)
1652
+ class Function(Expr):
1653
+ """JS function: function name(params) { ... } or async function ...
1654
+
1655
+ For statement-bodied functions. Use Arrow for expression-bodied.
1656
+ """
1657
+
1658
+ params: Sequence[str]
1659
+ body: Sequence[Stmt]
1660
+ name: str | None = None
1661
+ is_async: bool = False
1662
+
1663
+ @override
1664
+ def emit(self, out: list[str]) -> None:
1665
+ if self.is_async:
1666
+ out.append("async ")
1667
+ out.append("function")
1668
+ if self.name:
1669
+ out.append(" ")
1670
+ out.append(self.name)
1671
+ out.append("(")
1672
+ out.append(", ".join(self.params))
1673
+ out.append(") {\n")
1674
+ for stmt in self.body:
1675
+ stmt.emit(out)
1676
+ out.append("\n")
1677
+ out.append("}")
1678
+
1679
+ @override
1680
+ def render(self) -> VDOMExpr:
1681
+ raise TypeError("Function cannot be rendered as VDOMExpr")
1682
+
1683
+
1684
+ Node: TypeAlias = Primitive | Expr | PulseNode
1685
+ Child: TypeAlias = Node | Iterable[Node]
1686
+ Children: TypeAlias = Sequence[Child]
1687
+ Prop: TypeAlias = Primitive | Expr
1688
+
1689
+
1690
+ # =============================================================================
1691
+ # Emit logic
1692
+ # =============================================================================
1693
+
1694
+
1695
+ Emittable: TypeAlias = Expr | Stmt
1696
+
1697
+
1698
+ def emit(node: Emittable) -> str:
1699
+ """Emit an expression or statement as JavaScript/JSX code."""
1700
+ out: list[str] = []
1701
+ node.emit(out)
1702
+ return "".join(out)
1703
+
1704
+
1705
+ # Operator precedence table (higher = binds tighter)
1706
+ _PRECEDENCE: dict[str, int] = {
1707
+ # Primary
1708
+ ".": 20,
1709
+ "[]": 20,
1710
+ "()": 20,
1711
+ # Unary
1712
+ "!": 17,
1713
+ "+u": 17,
1714
+ "-u": 17,
1715
+ "typeof": 17,
1716
+ "await": 17,
1717
+ # Exponentiation (right-assoc)
1718
+ "**": 16,
1719
+ # Multiplicative
1720
+ "*": 15,
1721
+ "/": 15,
1722
+ "%": 15,
1723
+ # Additive
1724
+ "+": 14,
1725
+ "-": 14,
1726
+ # Relational
1727
+ "<": 12,
1728
+ "<=": 12,
1729
+ ">": 12,
1730
+ ">=": 12,
1731
+ "===": 12,
1732
+ "!==": 12,
1733
+ "instanceof": 12,
1734
+ "in": 12,
1735
+ # Logical
1736
+ "&&": 7,
1737
+ "||": 6,
1738
+ "??": 6,
1739
+ # Ternary
1740
+ "?:": 4,
1741
+ # Comma
1742
+ ",": 1,
1743
+ }
1744
+
1745
+ _RIGHT_ASSOC = {"**"}
1746
+
1747
+
1748
+ def _escape_string(s: str) -> str:
1749
+ """Escape for double-quoted JS string literals."""
1750
+ return (
1751
+ s.replace("\\", "\\\\")
1752
+ .replace('"', '\\"')
1753
+ .replace("\n", "\\n")
1754
+ .replace("\r", "\\r")
1755
+ .replace("\t", "\\t")
1756
+ .replace("\b", "\\b")
1757
+ .replace("\f", "\\f")
1758
+ .replace("\v", "\\v")
1759
+ .replace("\x00", "\\x00")
1760
+ .replace("\u2028", "\\u2028")
1761
+ .replace("\u2029", "\\u2029")
1762
+ )
1763
+
1764
+
1765
+ def _escape_template(s: str) -> str:
1766
+ """Escape for template literal strings."""
1767
+ return (
1768
+ s.replace("\\", "\\\\")
1769
+ .replace("`", "\\`")
1770
+ .replace("${", "\\${")
1771
+ .replace("\n", "\\n")
1772
+ .replace("\r", "\\r")
1773
+ .replace("\t", "\\t")
1774
+ .replace("\b", "\\b")
1775
+ .replace("\f", "\\f")
1776
+ .replace("\v", "\\v")
1777
+ .replace("\x00", "\\x00")
1778
+ .replace("\u2028", "\\u2028")
1779
+ .replace("\u2029", "\\u2029")
1780
+ )
1781
+
1782
+
1783
+ def _escape_jsx_text(s: str) -> str:
1784
+ """Escape text content for JSX."""
1785
+ return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
1786
+
1787
+
1788
+ def _escape_jsx_attr(s: str) -> str:
1789
+ """Escape attribute value for JSX."""
1790
+ return s.replace("&", "&amp;").replace('"', "&quot;")
1791
+
1792
+
1793
+ def _emit_paren(node: Expr, parent_op: str, side: str, out: list[str]) -> None:
1794
+ """Emit child with parens if needed for precedence."""
1795
+ # Ternary as child of binary always needs parens
1796
+ needs_parens = False
1797
+ if isinstance(node, Ternary):
1798
+ needs_parens = True
1799
+ else:
1800
+ child_prec = node.precedence()
1801
+ parent_prec = _PRECEDENCE.get(parent_op, 0)
1802
+ if child_prec < parent_prec:
1803
+ needs_parens = True
1804
+ elif child_prec == parent_prec and isinstance(node, Binary):
1805
+ # Handle associativity
1806
+ if parent_op in _RIGHT_ASSOC:
1807
+ needs_parens = side == "left"
1808
+ else:
1809
+ needs_parens = side == "right"
1810
+
1811
+ if needs_parens:
1812
+ out.append("(")
1813
+ node.emit(out)
1814
+ out.append(")")
1815
+ else:
1816
+ node.emit(out)
1817
+
1818
+
1819
+ def _emit_primary(node: Expr, out: list[str]) -> None:
1820
+ """Emit with parens if not primary precedence."""
1821
+ if node.precedence() < 20 or isinstance(node, Ternary):
1822
+ out.append("(")
1823
+ node.emit(out)
1824
+ out.append(")")
1825
+ else:
1826
+ node.emit(out)
1827
+
1828
+
1829
+ def _emit_value(value: Any, out: list[str]) -> None:
1830
+ """Emit a Python value as JavaScript literal."""
1831
+ if value is None:
1832
+ out.append("null")
1833
+ elif isinstance(value, bool):
1834
+ out.append("true" if value else "false")
1835
+ elif isinstance(value, str):
1836
+ out.append('"')
1837
+ out.append(_escape_string(value))
1838
+ out.append('"')
1839
+ elif isinstance(value, (int, float)):
1840
+ out.append(str(value))
1841
+ elif isinstance(value, dt.datetime):
1842
+ out.append("new Date(")
1843
+ out.append(str(int(value.timestamp() * 1000)))
1844
+ out.append(")")
1845
+ elif isinstance(value, list):
1846
+ out.append("[")
1847
+ for i, v in enumerate(value): # pyright: ignore[reportUnknownArgumentType]
1848
+ if i > 0:
1849
+ out.append(", ")
1850
+ _emit_value(v, out)
1851
+ out.append("]")
1852
+ elif isinstance(value, dict):
1853
+ out.append("{")
1854
+ for i, (k, v) in enumerate(value.items()): # pyright: ignore[reportUnknownArgumentType]
1855
+ if i > 0:
1856
+ out.append(", ")
1857
+ out.append('"')
1858
+ out.append(_escape_string(str(k))) # pyright: ignore[reportUnknownArgumentType]
1859
+ out.append('": ')
1860
+ _emit_value(v, out)
1861
+ out.append("}")
1862
+ elif isinstance(value, set):
1863
+ out.append("new Set([")
1864
+ for i, v in enumerate(value): # pyright: ignore[reportUnknownArgumentType]
1865
+ if i > 0:
1866
+ out.append(", ")
1867
+ _emit_value(v, out)
1868
+ out.append("])")
1869
+ else:
1870
+ raise TypeError(f"Cannot emit {type(value).__name__} as JavaScript")
1871
+
1872
+
1873
+ def _emit_jsx_prop(name: str, value: Prop, out: list[str]) -> None:
1874
+ """Emit a single JSX prop."""
1875
+ # Spread props
1876
+ if isinstance(value, Spread):
1877
+ out.append("{...")
1878
+ value.expr.emit(out)
1879
+ out.append("}")
1880
+ return
1881
+ # Expression nodes
1882
+ if isinstance(value, Expr):
1883
+ # String literals can use compact form
1884
+ if isinstance(value, Literal) and isinstance(value.value, str):
1885
+ out.append(name)
1886
+ out.append('="')
1887
+ out.append(_escape_jsx_attr(value.value))
1888
+ out.append('"')
1889
+ else:
1890
+ out.append(name)
1891
+ out.append("={")
1892
+ value.emit(out)
1893
+ out.append("}")
1894
+ return
1895
+ # Primitives
1896
+ if value is None:
1897
+ out.append(name)
1898
+ out.append("={null}")
1899
+ return
1900
+ if isinstance(value, bool):
1901
+ out.append(name)
1902
+ out.append("={true}" if value else "={false}")
1903
+ return
1904
+ if isinstance(value, str):
1905
+ out.append(name)
1906
+ out.append('="')
1907
+ out.append(_escape_jsx_attr(value))
1908
+ out.append('"')
1909
+ return
1910
+ if isinstance(value, (int, float)):
1911
+ out.append(name)
1912
+ out.append("={")
1913
+ out.append(str(value))
1914
+ out.append("}")
1915
+ return
1916
+ # Value
1917
+ if isinstance(value, Value):
1918
+ out.append(name)
1919
+ out.append("={")
1920
+ _emit_value(value.value, out)
1921
+ out.append("}")
1922
+ return
1923
+ # Nested Element (render prop)
1924
+ if isinstance(value, Element):
1925
+ out.append(name)
1926
+ out.append("={")
1927
+ value.emit(out)
1928
+ out.append("}")
1929
+ return
1930
+ # Callable - error
1931
+ if callable(value):
1932
+ raise TypeError("Cannot emit callable in transpile context")
1933
+ # Fallback for other data
1934
+ out.append(name)
1935
+ out.append("={")
1936
+ _emit_value(value, out)
1937
+ out.append("}")
1938
+
1939
+
1940
+ def _emit_jsx_child(child: Node, out: list[str]) -> None:
1941
+ """Emit a single JSX child."""
1942
+ # Primitives
1943
+ if child is None or isinstance(child, bool):
1944
+ return # React ignores None/bool
1945
+ if isinstance(child, str):
1946
+ out.append(_escape_jsx_text(child))
1947
+ return
1948
+ if isinstance(child, (int, float)):
1949
+ out.append("{")
1950
+ out.append(str(child))
1951
+ out.append("}")
1952
+ return
1953
+ if isinstance(child, dt.datetime):
1954
+ out.append("{")
1955
+ _emit_value(child, out)
1956
+ out.append("}")
1957
+ return
1958
+ # PulseNode - error
1959
+ if isinstance(child, PulseNode):
1960
+ fn_name = getattr(child.fn, "__name__", "unknown")
1961
+ raise TypeError(
1962
+ f"Cannot transpile PulseNode '{fn_name}'. "
1963
+ + "Server components must be rendered, not transpiled."
1964
+ )
1965
+ # Element - recurse
1966
+ if isinstance(child, Element):
1967
+ child.emit(out)
1968
+ return
1969
+ # Spread - emit as {expr} without the spread operator (arrays are already iterable in JSX)
1970
+ if isinstance(child, Spread):
1971
+ out.append("{")
1972
+ child.expr.emit(out)
1973
+ out.append("}")
1974
+ return
1975
+ # Expr
1976
+ if isinstance(child, Expr):
1977
+ out.append("{")
1978
+ child.emit(out)
1979
+ out.append("}")
1980
+ return
1981
+ # Value
1982
+ if isinstance(child, Value):
1983
+ out.append("{")
1984
+ _emit_value(child.value, out)
1985
+ out.append("}")
1986
+ return
1987
+ raise TypeError(f"Cannot emit {type(child).__name__} as JSX child")