pulse-framework 0.1.51__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.
- pulse/__init__.py +542 -562
- pulse/_examples.py +29 -0
- pulse/app.py +0 -14
- pulse/cli/cmd.py +96 -80
- pulse/cli/dependencies.py +10 -41
- pulse/cli/folder_lock.py +3 -3
- pulse/cli/helpers.py +40 -67
- pulse/cli/logging.py +102 -0
- pulse/cli/packages.py +16 -0
- pulse/cli/processes.py +40 -23
- pulse/codegen/codegen.py +70 -35
- pulse/codegen/js.py +2 -4
- pulse/codegen/templates/route.py +94 -146
- pulse/component.py +115 -0
- pulse/components/for_.py +1 -1
- pulse/components/if_.py +1 -1
- pulse/components/react_router.py +16 -22
- pulse/{html → dom}/events.py +1 -1
- pulse/{html → dom}/props.py +6 -6
- pulse/{html → dom}/tags.py +11 -11
- pulse/dom/tags.pyi +480 -0
- pulse/form.py +7 -6
- pulse/hooks/init.py +1 -13
- pulse/js/__init__.py +37 -41
- pulse/js/__init__.pyi +22 -2
- pulse/js/_types.py +5 -3
- pulse/js/array.py +121 -38
- pulse/js/console.py +9 -9
- pulse/js/date.py +22 -19
- pulse/js/document.py +8 -4
- pulse/js/error.py +12 -14
- pulse/js/json.py +4 -3
- pulse/js/map.py +17 -7
- pulse/js/math.py +2 -2
- pulse/js/navigator.py +4 -4
- pulse/js/number.py +8 -8
- pulse/js/object.py +9 -13
- pulse/js/promise.py +25 -9
- pulse/js/regexp.py +6 -6
- pulse/js/set.py +20 -8
- pulse/js/string.py +7 -7
- pulse/js/weakmap.py +6 -6
- pulse/js/weakset.py +6 -6
- pulse/js/window.py +17 -14
- pulse/messages.py +1 -4
- pulse/react_component.py +3 -1001
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- pulse/transpiler/__init__.py +84 -114
- pulse/transpiler/builtins.py +661 -343
- pulse/transpiler/errors.py +78 -2
- pulse/transpiler/function.py +463 -133
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +230 -325
- pulse/transpiler/js_module.py +218 -209
- pulse/transpiler/modules/__init__.py +16 -13
- pulse/transpiler/modules/asyncio.py +45 -26
- pulse/transpiler/modules/json.py +12 -8
- pulse/transpiler/modules/math.py +161 -216
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +231 -0
- pulse/transpiler/modules/typing.py +33 -28
- pulse/transpiler/nodes.py +1607 -923
- pulse/transpiler/py_module.py +118 -95
- pulse/transpiler/react_component.py +51 -0
- pulse/transpiler/transpiler.py +593 -437
- pulse/transpiler/vdom.py +255 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/METADATA +1 -1
- pulse_framework-0.1.52.dist-info/RECORD +120 -0
- pulse/html/tags.pyi +0 -470
- pulse/transpiler/constants.py +0 -110
- pulse/transpiler/context.py +0 -26
- pulse/transpiler/ids.py +0 -16
- pulse/transpiler/modules/re.py +0 -466
- pulse/transpiler/modules/tags.py +0 -268
- pulse/transpiler/utils.py +0 -4
- pulse/vdom.py +0 -599
- pulse_framework-0.1.51.dist-info/RECORD +0 -119
- /pulse/{html → dom}/__init__.py +0 -0
- /pulse/{html → dom}/elements.py +0 -0
- /pulse/{html → dom}/svg.py +0 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
pulse/codegen/templates/route.py
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
"""Route code generation using
|
|
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
|
-
|
|
16
|
-
|
|
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.
|
|
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.
|
|
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 "{
|
|
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 "{
|
|
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 "{
|
|
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(
|
|
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 "{
|
|
96
|
+
lines.append(f'import "{import_src}";')
|
|
75
97
|
|
|
76
98
|
return "\n".join(lines)
|
|
77
99
|
|
|
78
100
|
|
|
79
|
-
def _generate_imports_section(
|
|
80
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
204
|
-
lines: list[str] = []
|
|
206
|
+
"""Generate the unified registry from all registered entities.
|
|
205
207
|
|
|
206
|
-
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
242
|
-
for
|
|
243
|
-
lines.append(f' "{
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
#
|
|
265
|
-
|
|
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
|
-
#
|
|
295
|
-
|
|
248
|
+
# Add core Pulse imports
|
|
249
|
+
pulse_view_import = Import("PulseView", "pulse-ui-client")
|
|
296
250
|
|
|
297
|
-
#
|
|
298
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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(
|
|
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
|
-
|
|
328
|
-
|
|
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
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.
|
|
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)
|
pulse/components/react_router.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from typing import Literal, TypedDict, Unpack
|
|
2
2
|
|
|
3
|
-
from pulse.
|
|
4
|
-
from pulse.
|
|
5
|
-
from pulse.
|
|
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:
|
|
18
|
+
*children: Node,
|
|
17
19
|
key: str | None = None,
|
|
18
20
|
to: str,
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
pulse/{html → dom}/events.py
RENAMED
pulse/{html → dom}/props.py
RENAMED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
76
|
-
ClassName = str |
|
|
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):
|
pulse/{html → dom}/tags.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Any, ParamSpec
|
|
2
2
|
|
|
3
|
-
from pulse.
|
|
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:
|
|
21
|
-
"""Create a
|
|
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
|
-
|
|
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) ->
|
|
44
|
-
"""Create a self-closing
|
|
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
|
|
49
|
+
return Element(
|
|
49
50
|
tag=name,
|
|
50
51
|
key=key,
|
|
51
|
-
props=props,
|
|
52
|
-
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("
|
|
174
|
+
fragment = define_tag("")
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
# SVG tags
|