pulse-framework 0.1.50__py3-none-any.whl → 0.1.52__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 (85) hide show
  1. pulse/__init__.py +542 -562
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +0 -14
  4. pulse/cli/cmd.py +96 -80
  5. pulse/cli/dependencies.py +10 -41
  6. pulse/cli/folder_lock.py +3 -3
  7. pulse/cli/helpers.py +40 -67
  8. pulse/cli/logging.py +102 -0
  9. pulse/cli/packages.py +16 -0
  10. pulse/cli/processes.py +40 -23
  11. pulse/codegen/codegen.py +70 -35
  12. pulse/codegen/js.py +2 -4
  13. pulse/codegen/templates/route.py +94 -146
  14. pulse/component.py +115 -0
  15. pulse/components/for_.py +1 -1
  16. pulse/components/if_.py +1 -1
  17. pulse/components/react_router.py +16 -22
  18. pulse/{html → dom}/events.py +1 -1
  19. pulse/{html → dom}/props.py +6 -6
  20. pulse/{html → dom}/tags.py +11 -11
  21. pulse/dom/tags.pyi +480 -0
  22. pulse/form.py +7 -6
  23. pulse/hooks/init.py +1 -13
  24. pulse/js/__init__.py +37 -41
  25. pulse/js/__init__.pyi +22 -2
  26. pulse/js/_types.py +5 -3
  27. pulse/js/array.py +121 -38
  28. pulse/js/console.py +9 -9
  29. pulse/js/date.py +22 -19
  30. pulse/js/document.py +8 -4
  31. pulse/js/error.py +12 -14
  32. pulse/js/json.py +4 -3
  33. pulse/js/map.py +17 -7
  34. pulse/js/math.py +2 -2
  35. pulse/js/navigator.py +4 -4
  36. pulse/js/number.py +8 -8
  37. pulse/js/object.py +9 -13
  38. pulse/js/promise.py +25 -9
  39. pulse/js/regexp.py +6 -6
  40. pulse/js/set.py +20 -8
  41. pulse/js/string.py +7 -7
  42. pulse/js/weakmap.py +6 -6
  43. pulse/js/weakset.py +6 -6
  44. pulse/js/window.py +17 -14
  45. pulse/messages.py +1 -4
  46. pulse/react_component.py +3 -999
  47. pulse/render_session.py +74 -66
  48. pulse/renderer.py +311 -238
  49. pulse/routing.py +1 -10
  50. pulse/serializer.py +11 -1
  51. pulse/transpiler/__init__.py +84 -114
  52. pulse/transpiler/builtins.py +661 -343
  53. pulse/transpiler/errors.py +78 -2
  54. pulse/transpiler/function.py +463 -133
  55. pulse/transpiler/id.py +18 -0
  56. pulse/transpiler/imports.py +230 -325
  57. pulse/transpiler/js_module.py +218 -209
  58. pulse/transpiler/modules/__init__.py +16 -13
  59. pulse/transpiler/modules/asyncio.py +45 -26
  60. pulse/transpiler/modules/json.py +12 -8
  61. pulse/transpiler/modules/math.py +161 -216
  62. pulse/transpiler/modules/pulse/__init__.py +5 -0
  63. pulse/transpiler/modules/pulse/tags.py +231 -0
  64. pulse/transpiler/modules/typing.py +33 -28
  65. pulse/transpiler/nodes.py +1607 -923
  66. pulse/transpiler/py_module.py +118 -95
  67. pulse/transpiler/react_component.py +51 -0
  68. pulse/transpiler/transpiler.py +593 -437
  69. pulse/transpiler/vdom.py +255 -0
  70. {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/METADATA +1 -1
  71. pulse_framework-0.1.52.dist-info/RECORD +120 -0
  72. pulse/html/tags.pyi +0 -470
  73. pulse/transpiler/constants.py +0 -110
  74. pulse/transpiler/context.py +0 -26
  75. pulse/transpiler/ids.py +0 -16
  76. pulse/transpiler/modules/re.py +0 -466
  77. pulse/transpiler/modules/tags.py +0 -268
  78. pulse/transpiler/utils.py +0 -4
  79. pulse/vdom.py +0 -667
  80. pulse_framework-0.1.50.dist-info/RECORD +0 -119
  81. /pulse/{html → dom}/__init__.py +0 -0
  82. /pulse/{html → dom}/elements.py +0 -0
  83. /pulse/{html → dom}/svg.py +0 -0
  84. {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
  85. {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
@@ -1,19 +1,34 @@
1
- """Route code generation using the javascript_v2 import system."""
1
+ """Route code generation using transpiler."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import os
6
5
  from collections.abc import Sequence
7
- from pathlib import Path
8
-
9
- from pulse.react_component import ReactComponent
10
- from pulse.transpiler.constants import JsConstant
11
- from pulse.transpiler.function import AnyJsFunction, JsFunction, registered_functions
12
- from pulse.transpiler.imports import Import, registered_imports
13
6
 
7
+ from pulse.transpiler import (
8
+ Constant,
9
+ Import,
10
+ collect_function_graph,
11
+ emit,
12
+ get_registered_imports,
13
+ registered_functions,
14
+ )
15
+ from pulse.transpiler.function import AnyJsFunction
16
+
17
+
18
+ def _generate_import_statement(
19
+ src: str,
20
+ imports: list[Import],
21
+ asset_filenames: dict[str, str] | None = None,
22
+ asset_prefix: str = "../assets/",
23
+ ) -> str:
24
+ """Generate import statement(s) for a source module.
14
25
 
15
- def _generate_import_statement(src: str, imports: list[Import]) -> str:
16
- """Generate import statement(s) for a source module."""
26
+ Args:
27
+ src: The original source path (may be remapped for local imports)
28
+ imports: List of Import objects for this source
29
+ asset_filenames: Mapping of original source paths to asset filenames
30
+ asset_prefix: Relative path prefix from route file to assets folder
31
+ """
17
32
  default_imports: list[Import] = []
18
33
  namespace_imports: list[Import] = []
19
34
  named_imports: list[Import] = []
@@ -26,32 +41,37 @@ def _generate_import_statement(src: str, imports: list[Import]) -> str:
26
41
  elif imp.is_namespace:
27
42
  namespace_imports.append(imp)
28
43
  elif imp.is_default:
29
- if imp.is_type_only:
44
+ if imp.is_type:
30
45
  type_imports.append(imp)
31
46
  else:
32
47
  default_imports.append(imp)
33
48
  else:
34
- if imp.is_type_only:
49
+ if imp.is_type:
35
50
  type_imports.append(imp)
36
51
  else:
37
52
  named_imports.append(imp)
38
53
 
54
+ # Remap source path if this is a local import
55
+ import_src = src
56
+ if asset_filenames and src in asset_filenames:
57
+ import_src = asset_prefix + asset_filenames[src]
58
+
39
59
  lines: list[str] = []
40
60
 
41
61
  # Namespace import (only one allowed per source)
42
62
  if namespace_imports:
43
63
  imp = namespace_imports[0]
44
- lines.append(f'import * as {imp.js_name} from "{src}";')
64
+ lines.append(f'import * as {imp.js_name} from "{import_src}";')
45
65
 
46
66
  # Default import (only one allowed per source)
47
67
  if default_imports:
48
68
  imp = default_imports[0]
49
- lines.append(f'import {imp.js_name} from "{src}";')
69
+ lines.append(f'import {imp.js_name} from "{import_src}";')
50
70
 
51
71
  # Named imports
52
72
  if named_imports:
53
73
  members = [f"{imp.name} as {imp.js_name}" for imp in named_imports]
54
- lines.append(f'import {{ {", ".join(members)} }} from "{src}";')
74
+ lines.append(f'import {{ {", ".join(members)} }} from "{import_src}";')
55
75
 
56
76
  # Type imports
57
77
  if type_imports:
@@ -61,7 +81,9 @@ def _generate_import_statement(src: str, imports: list[Import]) -> str:
61
81
  type_members.append(f"default as {imp.js_name}")
62
82
  else:
63
83
  type_members.append(f"{imp.name} as {imp.js_name}")
64
- lines.append(f'import type {{ {", ".join(type_members)} }} from "{src}";')
84
+ lines.append(
85
+ f'import type {{ {", ".join(type_members)} }} from "{import_src}";'
86
+ )
65
87
 
66
88
  # Side-effect only import (only if no other imports)
67
89
  if (
@@ -71,13 +93,23 @@ def _generate_import_statement(src: str, imports: list[Import]) -> str:
71
93
  and not named_imports
72
94
  and not type_imports
73
95
  ):
74
- lines.append(f'import "{src}";')
96
+ lines.append(f'import "{import_src}";')
75
97
 
76
98
  return "\n".join(lines)
77
99
 
78
100
 
79
- def _generate_imports_section(imports: Sequence[Import]) -> str:
80
- """Generate the full imports section with deduplication and topological ordering."""
101
+ def _generate_imports_section(
102
+ imports: Sequence[Import],
103
+ asset_filenames: dict[str, str] | None = None,
104
+ asset_prefix: str = "../assets/",
105
+ ) -> str:
106
+ """Generate the full imports section with deduplication and topological ordering.
107
+
108
+ Args:
109
+ imports: List of Import objects to generate
110
+ asset_filenames: Mapping of original source paths to asset filenames
111
+ asset_prefix: Relative path prefix from route file to assets folder
112
+ """
81
113
  if not imports:
82
114
  return ""
83
115
 
@@ -131,51 +163,23 @@ def _generate_imports_section(imports: Sequence[Import]) -> str:
131
163
 
132
164
  lines: list[str] = []
133
165
  for src in ordered:
134
- stmt = _generate_import_statement(src, grouped[src])
166
+ stmt = _generate_import_statement(
167
+ src, grouped[src], asset_filenames, asset_prefix
168
+ )
135
169
  if stmt:
136
170
  lines.append(stmt)
137
171
 
138
172
  return "\n".join(lines)
139
173
 
140
174
 
141
- def _collect_function_graph(
142
- functions: Sequence[AnyJsFunction],
143
- ) -> tuple[list[JsConstant], list[AnyJsFunction]]:
144
- """Collect all constants and functions in dependency order (depth-first)."""
145
- seen_funcs: set[str] = set()
146
- seen_consts: set[str] = set()
147
- all_funcs: list[AnyJsFunction] = []
148
- all_consts: list[JsConstant] = []
149
-
150
- def walk(fn: AnyJsFunction) -> None:
151
- if fn.id in seen_funcs:
152
- return
153
- seen_funcs.add(fn.id)
154
-
155
- for dep in fn.deps.values():
156
- if isinstance(dep, JsFunction):
157
- walk(dep) # pyright: ignore[reportUnknownArgumentType]
158
- elif isinstance(dep, JsConstant):
159
- if dep.id not in seen_consts:
160
- seen_consts.add(dep.id)
161
- all_consts.append(dep)
162
-
163
- all_funcs.append(fn)
164
-
165
- for fn in functions:
166
- walk(fn)
167
-
168
- return all_consts, all_funcs
169
-
170
-
171
- def _generate_constants_section(constants: Sequence[JsConstant]) -> str:
175
+ def _generate_constants_section(constants: Sequence[Constant]) -> str:
172
176
  """Generate the constants section."""
173
177
  if not constants:
174
178
  return ""
175
179
 
176
180
  lines: list[str] = ["// Constants"]
177
181
  for const in constants:
178
- js_value = const.expr.emit()
182
+ js_value = emit(const.expr)
179
183
  lines.append(f"const {const.js_name} = {js_value};")
180
184
 
181
185
  return "\n".join(lines)
@@ -188,59 +192,38 @@ def _generate_functions_section(functions: Sequence[AnyJsFunction]) -> str:
188
192
 
189
193
  lines: list[str] = ["// Functions"]
190
194
  for fn in functions:
191
- js_code = fn.transpile()
195
+ js_code = emit(fn.transpile())
192
196
  lines.append(js_code)
193
197
 
194
198
  return "\n".join(lines)
195
199
 
196
200
 
197
201
  def _generate_registry_section(
198
- all_imports: Sequence[Import],
199
- lazy_components: Sequence[tuple[ReactComponent[...], Import]] | None = None,
200
- prop_components: Sequence[ReactComponent[...]] | None = None,
201
- functions: Sequence[AnyJsFunction] | None = None,
202
+ imports: Sequence[Import],
203
+ constants: Sequence[Constant],
204
+ functions: Sequence[AnyJsFunction],
202
205
  ) -> str:
203
- """Generate the unified registry containing all imports for runtime lookup."""
204
- lines: list[str] = []
206
+ """Generate the unified registry from all registered entities.
205
207
 
206
- # Unified Registry - contains all imports that need to be looked up at runtime
208
+ The registry contains all values that need to be looked up at runtime,
209
+ keyed by their unique ID.
210
+ """
211
+ lines: list[str] = []
207
212
  lines.append("// Unified Registry")
208
213
  lines.append("const __registry = {")
209
214
 
210
- # Add non-type, non-side-effect imports to the registry
211
- seen_js_names: set[str] = set()
212
- for imp in all_imports:
213
- if imp.is_side_effect or imp.is_type_only:
214
- continue
215
- if imp.js_name in seen_js_names:
216
- continue
217
- seen_js_names.add(imp.js_name)
218
- lines.append(f' "{imp.js_name}": {imp.js_name},')
219
-
220
- # Add components with prop access (e.g., AppShell.Header)
221
- # These need separate registry entries because the lookup key includes the prop
222
- for comp in prop_components or []:
223
- if comp.prop:
224
- # Key is "ImportName_123.PropName", value is ImportName_123.PropName
225
- key = f"{comp.import_.js_name}.{comp.prop}"
226
- lines.append(f' "{key}": {key},')
227
-
228
- # Add lazy components with RenderLazy wrapper
229
- for comp, render_lazy_imp in lazy_components or []:
230
- attr = "default" if comp.is_default else comp.name
231
- prop_accessor = f".{comp.prop}" if comp.prop else ""
232
- dynamic = f"({{ default: m.{attr}{prop_accessor} }})"
233
- # Key includes prop if present (e.g., "AppShell_123.Header")
234
- key = comp.import_.js_name
235
- if comp.prop:
236
- key = f"{key}.{comp.prop}"
237
- lines.append(
238
- f' "{key}": {render_lazy_imp.js_name}(() => import("{comp.src}").then((m) => {dynamic})),'
239
- )
215
+ # Add imports
216
+ for imp in imports:
217
+ if not imp.is_side_effect:
218
+ lines.append(f' "{imp.id}": {imp.js_name},')
240
219
 
241
- # Add transpiled functions to the registry
242
- for fn in functions or []:
243
- lines.append(f' "{fn.js_name}": {fn.js_name},')
220
+ # Add constants
221
+ for const in constants:
222
+ lines.append(f' "{const.id}": {const.js_name},')
223
+
224
+ # Add functions
225
+ for fn in functions:
226
+ lines.append(f' "{fn.id}": {fn.js_name},')
244
227
 
245
228
  lines.append("};")
246
229
 
@@ -249,68 +232,34 @@ def _generate_registry_section(
249
232
 
250
233
  def generate_route(
251
234
  path: str,
252
- components: Sequence[ReactComponent[...]] | None = None,
253
- route_file_path: Path | None = None,
254
- css_dir: Path | None = None,
235
+ asset_filenames: dict[str, str] | None = None,
236
+ asset_prefix: str = "../assets/",
255
237
  ) -> str:
256
- """Generate a route file with all imports and components.
238
+ """Generate a route file with all registered imports, functions, and components.
257
239
 
258
240
  Args:
259
241
  path: The route path (e.g., "/users/:id")
260
- components: React components used in the route
261
- route_file_path: Path where the route file will be written (for computing relative imports)
262
- css_dir: Path to the CSS output directory (for computing relative CSS imports)
242
+ asset_filenames: Mapping of original source paths to asset filenames
243
+ asset_prefix: Relative path prefix from route file to assets folder
263
244
  """
264
- # Collect lazy component import IDs to exclude from registered_imports
265
- lazy_import_ids: set[str] = set()
266
- for comp in components or []:
267
- if comp.lazy:
268
- lazy_import_ids.add(comp.import_.id)
269
-
270
- # Add core Pulse imports - store references to use their js_name later
271
- pulse_view_import = Import.named("PulseView", "pulse-ui-client")
272
-
273
- # Check if we need RenderLazy
274
- render_lazy_import: Import | None = None
275
- if any(c.lazy for c in (components or [])):
276
- render_lazy_import = Import.named("RenderLazy", "pulse-ui-client")
277
-
278
- # Process components: add non-lazy imports and collect metadata
279
- prop_components: list[ReactComponent[...]] = []
280
- lazy_components: list[tuple[ReactComponent[...], Import]] = []
281
- for comp in components or []:
282
- if comp.lazy:
283
- if render_lazy_import is not None:
284
- lazy_components.append((comp, render_lazy_import))
285
- else:
286
- # Force registration by accessing the import
287
- _ = comp.import_
288
- if comp.prop:
289
- prop_components.append(comp)
290
-
291
- # Collect function graph (constants + functions in dependency order)
292
- constants, funcs = _collect_function_graph(registered_functions())
245
+ # Note: Lazy component support is not yet implemented.
246
+ # Components now register via the unified registry.
293
247
 
294
- # Get all registered imports, excluding lazy ones
295
- all_imports = [imp for imp in registered_imports() if imp.id not in lazy_import_ids]
248
+ # Add core Pulse imports
249
+ pulse_view_import = Import("PulseView", "pulse-ui-client")
296
250
 
297
- # Update src for local CSS imports to use relative paths from the route file
298
- if route_file_path is not None and css_dir is not None:
299
- from pulse.transpiler.imports import CssImport
251
+ # Collect function graph (constants + functions in dependency order)
252
+ constants, funcs = collect_function_graph(registered_functions())
300
253
 
301
- route_dir = route_file_path.parent
302
- for imp in all_imports:
303
- if isinstance(imp, CssImport) and imp.is_local:
304
- generated_filename = imp.generated_filename
305
- assert generated_filename is not None
306
- css_file_path = css_dir / generated_filename
307
- rel_path = Path(os.path.relpath(css_file_path, route_dir))
308
- imp.src = rel_path.as_posix()
254
+ # Get all registered imports
255
+ all_imports = list(get_registered_imports())
309
256
 
310
257
  # Generate output sections
311
258
  output_parts: list[str] = []
312
259
 
313
- imports_section = _generate_imports_section(all_imports)
260
+ imports_section = _generate_imports_section(
261
+ all_imports, asset_filenames, asset_prefix
262
+ )
314
263
  if imports_section:
315
264
  output_parts.append(imports_section)
316
265
 
@@ -324,9 +273,8 @@ def generate_route(
324
273
  output_parts.append(_generate_functions_section(funcs))
325
274
  output_parts.append("")
326
275
 
327
- output_parts.append(
328
- _generate_registry_section(all_imports, lazy_components, prop_components, funcs)
329
- )
276
+ # Generate the unified registry including all imports, constants and functions
277
+ output_parts.append(_generate_registry_section(all_imports, constants, funcs))
330
278
  output_parts.append("")
331
279
 
332
280
  # Route component
pulse/component.py ADDED
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from inspect import Parameter, signature
5
+ from typing import Any, Generic, ParamSpec, TypeVar, overload, override
6
+
7
+ from pulse.hooks.init import rewrite_init_blocks
8
+ from pulse.transpiler.nodes import (
9
+ Children,
10
+ Node,
11
+ Primitive,
12
+ PulseNode,
13
+ flatten_children,
14
+ )
15
+ from pulse.transpiler.nodes import Element as Element
16
+ from pulse.transpiler.vdom import VDOMNode
17
+
18
+ P = ParamSpec("P")
19
+ _T = TypeVar("_T")
20
+
21
+
22
+ class Component(Generic[P]):
23
+ fn: Callable[P, Any]
24
+ name: str
25
+ _takes_children: bool
26
+
27
+ def __init__(self, fn: Callable[P, Any], name: str | None = None) -> None:
28
+ self.fn = rewrite_init_blocks(fn)
29
+ self.name = name or _infer_component_name(fn)
30
+ self._takes_children = _takes_children(fn)
31
+
32
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> PulseNode:
33
+ key = kwargs.get("key")
34
+ if key is not None and not isinstance(key, str):
35
+ raise ValueError("key must be a string or None")
36
+
37
+ if self._takes_children and args:
38
+ flattened = flatten_children(
39
+ args, # pyright: ignore[reportArgumentType]
40
+ parent_name=f"<{self.name}>",
41
+ warn_stacklevel=4,
42
+ )
43
+ args = tuple(flattened) # pyright: ignore[reportAssignmentType]
44
+
45
+ return PulseNode(fn=self.fn, args=args, kwargs=kwargs, key=key, name=self.name)
46
+
47
+ @override
48
+ def __repr__(self) -> str:
49
+ return f"Component(name={self.name!r}, fn={_callable_qualname(self.fn)!r})"
50
+
51
+ @override
52
+ def __str__(self) -> str:
53
+ return self.name
54
+
55
+
56
+ @overload
57
+ def component(fn: Callable[P, Any]) -> Component[P]: ...
58
+
59
+
60
+ @overload
61
+ def component(
62
+ fn: None = None, *, name: str | None = None
63
+ ) -> Callable[[Callable[P, Any]], Component[P]]: ...
64
+
65
+
66
+ # The explicit return type is necessary for the type checker to be happy
67
+ def component(
68
+ fn: Callable[P, Any] | None = None, *, name: str | None = None
69
+ ) -> Component[P] | Callable[[Callable[P, Any]], Component[P]]:
70
+ def decorator(fn: Callable[P, Any]) -> Component[P]:
71
+ return Component(fn, name)
72
+
73
+ if fn is not None:
74
+ return decorator(fn)
75
+ return decorator
76
+
77
+
78
+ def _takes_children(fn: Callable[..., Any]) -> bool:
79
+ try:
80
+ sig = signature(fn)
81
+ except (ValueError, TypeError):
82
+ return False
83
+ for p in sig.parameters.values():
84
+ if p.kind == Parameter.VAR_POSITIONAL and p.name == "children":
85
+ return True
86
+ return False
87
+
88
+
89
+ # ----------------------------------------------------------------------------
90
+ # Formatting helpers
91
+ # ----------------------------------------------------------------------------
92
+
93
+
94
+ def _infer_component_name(fn: Callable[..., Any]) -> str:
95
+ name = getattr(fn, "__name__", None)
96
+ if name:
97
+ return name
98
+ return "Component"
99
+
100
+
101
+ def _callable_qualname(fn: Callable[..., Any]) -> str:
102
+ mod = getattr(fn, "__module__", "<unknown>")
103
+ qname = getattr(fn, "__qualname__", getattr(fn, "__name__", "<callable>"))
104
+ return f"{mod}.{qname}"
105
+
106
+
107
+ __all__ = [
108
+ "Node",
109
+ "Children",
110
+ "Component",
111
+ "Element",
112
+ "Primitive",
113
+ "VDOMNode",
114
+ "component",
115
+ ]
pulse/components/for_.py CHANGED
@@ -2,7 +2,7 @@ from collections.abc import Callable, Iterable
2
2
  from inspect import Parameter, signature
3
3
  from typing import TypeVar, overload
4
4
 
5
- from pulse.vdom import Element
5
+ from pulse.transpiler.nodes import Element
6
6
 
7
7
  T = TypeVar("T")
8
8
 
pulse/components/if_.py CHANGED
@@ -2,7 +2,7 @@ from collections.abc import Iterable
2
2
  from typing import Any, TypeVar
3
3
 
4
4
  from pulse.reactive import Computed, Signal
5
- from pulse.vdom import Element
5
+ from pulse.transpiler.nodes import Element
6
6
 
7
7
  T1 = TypeVar("T1", bound=Element | Iterable[Element])
8
8
  T2 = TypeVar("T2", bound=Element | Iterable[Element] | None)
@@ -1,8 +1,9 @@
1
1
  from typing import Literal, TypedDict, Unpack
2
2
 
3
- from pulse.html.props import HTMLAnchorProps
4
- from pulse.react_component import DEFAULT, react_component
5
- from pulse.vdom import Child
3
+ from pulse.dom.props import HTMLAnchorProps
4
+ from pulse.transpiler import Import
5
+ from pulse.transpiler.nodes import Node
6
+ from pulse.transpiler.react_component import react_component
6
7
 
7
8
 
8
9
  class LinkPath(TypedDict):
@@ -11,33 +12,26 @@ class LinkPath(TypedDict):
11
12
  hash: str
12
13
 
13
14
 
14
- @react_component("Link", "react-router", version="^7")
15
+ # @react_component(Import("Link", "react-router", version="^7"))
16
+ @react_component(Import("Link", "react-router"))
15
17
  def Link(
16
- *children: Child,
18
+ *children: Node,
17
19
  key: str | None = None,
18
20
  to: str,
19
- # Default: render
20
- discover: Literal["render", "none"] = DEFAULT,
21
- # The React Router default is 'none' to match the behavior of regular links,
22
- # but 'intent' is more desirable in general
21
+ discover: Literal["render", "none"] | None = None,
23
22
  prefetch: Literal["none", "intent", "render", "viewport"] = "intent",
24
- # Default: False
25
- preventScrollReset: bool = DEFAULT,
26
- # Default: 'route'
27
- relative: Literal["route", "path"] = DEFAULT,
28
- # Default: False
29
- reloadDocument: bool = DEFAULT,
30
- # Default: False
31
- replace: bool = DEFAULT,
32
- # Default: undefined
33
- state: dict[str, object] = DEFAULT,
34
- # Default: False
35
- viewTransition: bool = DEFAULT,
23
+ preventScrollReset: bool | None = None,
24
+ relative: Literal["route", "path"] | None = None,
25
+ reloadDocument: bool | None = None,
26
+ replace: bool | None = None,
27
+ state: dict[str, object] | None = None,
28
+ viewTransition: bool | None = None,
36
29
  **props: Unpack[HTMLAnchorProps],
37
30
  ): ...
38
31
 
39
32
 
40
- @react_component("Outlet", "react-router", version="^7")
33
+ # @react_component(Import("Outlet", "react-router", version="^7"))
34
+ @react_component(Import("Outlet", "react-router"))
41
35
  def Outlet(key: str | None = None): ...
42
36
 
43
37
 
@@ -13,7 +13,7 @@ from typing import (
13
13
  TypeVar,
14
14
  )
15
15
 
16
- from pulse.html.elements import (
16
+ from pulse.dom.elements import (
17
17
  HTMLDialogElement,
18
18
  HTMLElement,
19
19
  HTMLInputElement,
@@ -2,8 +2,7 @@
2
2
  # NOT the same thing as the properties in `elements.py` (but very similar)
3
3
  from typing import Any, Literal, TypedDict
4
4
 
5
- from pulse.helpers import CSSProperties
6
- from pulse.html.elements import (
5
+ from pulse.dom.elements import (
7
6
  GenericHTMLElement,
8
7
  HTMLAnchorElement,
9
8
  HTMLAreaElement,
@@ -60,7 +59,7 @@ from pulse.html.elements import (
60
59
  HTMLTrackElement,
61
60
  HTMLUListElement,
62
61
  )
63
- from pulse.html.events import (
62
+ from pulse.dom.events import (
64
63
  DialogDOMEvents,
65
64
  DOMEvents,
66
65
  InputDOMEvents,
@@ -68,12 +67,13 @@ from pulse.html.events import (
68
67
  TElement,
69
68
  TextAreaDOMEvents,
70
69
  )
71
- from pulse.transpiler.nodes import JSExpr
70
+ from pulse.helpers import CSSProperties
71
+ from pulse.transpiler.nodes import Expr
72
72
 
73
73
  Booleanish = Literal[True, False, "true", "false"]
74
74
  CrossOrigin = Literal["anonymous", "use-credentials", ""] | None
75
- # ClassName can be a string or any JSExpr (e.g., JSMember from CssModule.classname)
76
- ClassName = str | JSExpr
75
+ # ClassName can be a string or any Expr (e.g., Member from CssModule.classname)
76
+ ClassName = str | Expr
77
77
 
78
78
 
79
79
  class BaseHTMLProps(TypedDict, total=False):
@@ -1,6 +1,6 @@
1
1
  from typing import Any, ParamSpec
2
2
 
3
- from pulse.vdom import Element, Node
3
+ from pulse.transpiler.nodes import Element, Node
4
4
 
5
5
  P = ParamSpec("P")
6
6
 
@@ -17,12 +17,13 @@ def define_tag(name: str, default_props: dict[str, Any] | None = None):
17
17
  A function that creates UITreeNode instances
18
18
  """
19
19
 
20
- def create_element(*children: Element, **props: Any) -> Node:
21
- """Create a UITreeNode for this tag."""
20
+ def create_element(*children: Node, **props: Any) -> Element:
21
+ """Create a UI element for this tag."""
22
22
  if default_props:
23
23
  props = default_props | props
24
24
  key = props.pop("key", None)
25
- return Node(tag=name, key=key, props=props, children=children)
25
+ child_list = list(children) if children else None
26
+ return Element(tag=name, key=key, props=props or None, children=child_list)
26
27
 
27
28
  return create_element
28
29
 
@@ -40,17 +41,16 @@ def define_self_closing_tag(name: str, default_props: dict[str, Any] | None = No
40
41
  """
41
42
  default_props = default_props
42
43
 
43
- def create_element(**props: Any) -> Node:
44
- """Create a self-closing UITreeNode for this tag."""
44
+ def create_element(**props: Any) -> Element:
45
+ """Create a self-closing UI element for this tag."""
45
46
  if default_props:
46
47
  props = default_props | props
47
48
  key = props.pop("key", None)
48
- return Node(
49
+ return Element(
49
50
  tag=name,
50
51
  key=key,
51
- props=props,
52
- children=(), # Self-closing tags never have children
53
- allow_children=False,
52
+ props=props or None,
53
+ children=None,
54
54
  )
55
55
 
56
56
  return create_element
@@ -171,7 +171,7 @@ track = define_self_closing_tag("track")
171
171
  wbr = define_self_closing_tag("wbr")
172
172
 
173
173
  # React fragment
174
- fragment = define_tag("$$fragment")
174
+ fragment = define_tag("")
175
175
 
176
176
 
177
177
  # SVG tags