pulse-framework 0.1.46__py3-none-any.whl → 0.1.48__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 +9 -23
- pulse/app.py +6 -25
- pulse/cli/processes.py +1 -0
- pulse/codegen/codegen.py +43 -88
- pulse/codegen/js.py +35 -5
- pulse/codegen/templates/route.py +341 -254
- pulse/form.py +1 -1
- pulse/helpers.py +51 -27
- pulse/hooks/core.py +2 -2
- pulse/hooks/effects.py +1 -1
- pulse/hooks/init.py +2 -1
- pulse/hooks/setup.py +1 -1
- pulse/hooks/stable.py +2 -2
- pulse/hooks/states.py +2 -2
- pulse/html/props.py +3 -2
- pulse/html/tags.py +135 -0
- pulse/html/tags.pyi +4 -0
- pulse/js/__init__.py +110 -0
- pulse/js/__init__.pyi +95 -0
- pulse/js/_types.py +297 -0
- pulse/js/array.py +253 -0
- pulse/js/console.py +47 -0
- pulse/js/date.py +113 -0
- pulse/js/document.py +138 -0
- pulse/js/error.py +139 -0
- pulse/js/json.py +62 -0
- pulse/js/map.py +84 -0
- pulse/js/math.py +66 -0
- pulse/js/navigator.py +76 -0
- pulse/js/number.py +54 -0
- pulse/js/object.py +173 -0
- pulse/js/promise.py +150 -0
- pulse/js/regexp.py +54 -0
- pulse/js/set.py +109 -0
- pulse/js/string.py +35 -0
- pulse/js/weakmap.py +50 -0
- pulse/js/weakset.py +45 -0
- pulse/js/window.py +199 -0
- pulse/messages.py +22 -3
- pulse/proxy.py +21 -8
- pulse/react_component.py +167 -14
- pulse/reactive_extensions.py +5 -5
- pulse/render_session.py +144 -34
- pulse/renderer.py +80 -115
- pulse/routing.py +1 -18
- pulse/transpiler/__init__.py +131 -0
- pulse/transpiler/builtins.py +731 -0
- pulse/transpiler/constants.py +110 -0
- pulse/transpiler/context.py +26 -0
- pulse/transpiler/errors.py +2 -0
- pulse/transpiler/function.py +250 -0
- pulse/transpiler/ids.py +16 -0
- pulse/transpiler/imports.py +409 -0
- pulse/transpiler/js_module.py +274 -0
- pulse/transpiler/modules/__init__.py +30 -0
- pulse/transpiler/modules/asyncio.py +38 -0
- pulse/transpiler/modules/json.py +20 -0
- pulse/transpiler/modules/math.py +320 -0
- pulse/transpiler/modules/re.py +466 -0
- pulse/transpiler/modules/tags.py +268 -0
- pulse/transpiler/modules/typing.py +59 -0
- pulse/transpiler/nodes.py +1216 -0
- pulse/transpiler/py_module.py +119 -0
- pulse/transpiler/transpiler.py +938 -0
- pulse/transpiler/utils.py +4 -0
- pulse/vdom.py +112 -6
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/METADATA +1 -1
- pulse_framework-0.1.48.dist-info/RECORD +119 -0
- pulse/codegen/imports.py +0 -204
- pulse/css.py +0 -155
- pulse_framework-0.1.46.dist-info/RECORD +0 -80
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.48.dist-info}/entry_points.txt +0 -0
pulse/codegen/templates/route.py
CHANGED
|
@@ -1,266 +1,353 @@
|
|
|
1
|
-
|
|
2
|
-
from typing import TypedDict, TypeVarTuple
|
|
1
|
+
"""Route code generation using the javascript_v2 import system."""
|
|
3
2
|
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
from pulse.codegen.imports import Imports, ImportStatement
|
|
7
|
-
from pulse.codegen.js import ExternalJsFunction, JsFunction
|
|
8
|
-
from pulse.codegen.utils import NameRegistry
|
|
9
|
-
from pulse.react_component import ReactComponent
|
|
10
|
-
from pulse.routing import Layout, Route
|
|
11
|
-
|
|
12
|
-
Args = TypeVarTuple("Args")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class ComponentInfo(TypedDict):
|
|
16
|
-
key: str
|
|
17
|
-
expr: str
|
|
18
|
-
src: str
|
|
19
|
-
name: str
|
|
20
|
-
default: bool
|
|
21
|
-
lazy: bool
|
|
22
|
-
dynamic: str
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class CssModuleImport(TypedDict):
|
|
26
|
-
id: str
|
|
27
|
-
import_path: str
|
|
3
|
+
from __future__ import annotations
|
|
28
4
|
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
29
8
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
|
|
14
|
+
|
|
15
|
+
def _generate_import_statement(src: str, imports: list[Import]) -> str:
|
|
16
|
+
"""Generate import statement(s) for a source module."""
|
|
17
|
+
default_imports: list[Import] = []
|
|
18
|
+
namespace_imports: list[Import] = []
|
|
19
|
+
named_imports: list[Import] = []
|
|
20
|
+
type_imports: list[Import] = []
|
|
21
|
+
has_side_effect = False
|
|
22
|
+
|
|
23
|
+
for imp in imports:
|
|
24
|
+
if imp.is_side_effect:
|
|
25
|
+
has_side_effect = True
|
|
26
|
+
elif imp.is_namespace:
|
|
27
|
+
namespace_imports.append(imp)
|
|
28
|
+
elif imp.is_default:
|
|
29
|
+
if imp.is_type_only:
|
|
30
|
+
type_imports.append(imp)
|
|
31
|
+
else:
|
|
32
|
+
default_imports.append(imp)
|
|
33
|
+
else:
|
|
34
|
+
if imp.is_type_only:
|
|
35
|
+
type_imports.append(imp)
|
|
36
|
+
else:
|
|
37
|
+
named_imports.append(imp)
|
|
38
|
+
|
|
39
|
+
lines: list[str] = []
|
|
40
|
+
|
|
41
|
+
# Namespace import (only one allowed per source)
|
|
42
|
+
if namespace_imports:
|
|
43
|
+
imp = namespace_imports[0]
|
|
44
|
+
lines.append(f'import * as {imp.js_name} from "{src}";')
|
|
45
|
+
|
|
46
|
+
# Default import (only one allowed per source)
|
|
47
|
+
if default_imports:
|
|
48
|
+
imp = default_imports[0]
|
|
49
|
+
lines.append(f'import {imp.js_name} from "{src}";')
|
|
50
|
+
|
|
51
|
+
# Named imports
|
|
52
|
+
if named_imports:
|
|
53
|
+
members = [f"{imp.name} as {imp.js_name}" for imp in named_imports]
|
|
54
|
+
lines.append(f'import {{ {", ".join(members)} }} from "{src}";')
|
|
55
|
+
|
|
56
|
+
# Type imports
|
|
57
|
+
if type_imports:
|
|
58
|
+
type_members: list[str] = []
|
|
59
|
+
for imp in type_imports:
|
|
60
|
+
if imp.is_default:
|
|
61
|
+
type_members.append(f"default as {imp.js_name}")
|
|
62
|
+
else:
|
|
63
|
+
type_members.append(f"{imp.name} as {imp.js_name}")
|
|
64
|
+
lines.append(f'import type {{ {", ".join(type_members)} }} from "{src}";')
|
|
65
|
+
|
|
66
|
+
# Side-effect only import (only if no other imports)
|
|
67
|
+
if (
|
|
68
|
+
has_side_effect
|
|
69
|
+
and not default_imports
|
|
70
|
+
and not namespace_imports
|
|
71
|
+
and not named_imports
|
|
72
|
+
and not type_imports
|
|
73
|
+
):
|
|
74
|
+
lines.append(f'import "{src}";')
|
|
75
|
+
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _generate_imports_section(imports: Sequence[Import]) -> str:
|
|
80
|
+
"""Generate the full imports section with deduplication and topological ordering."""
|
|
81
|
+
if not imports:
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
# Deduplicate imports by ID
|
|
85
|
+
seen_ids: set[str] = set()
|
|
86
|
+
unique_imports: list[Import] = []
|
|
87
|
+
for imp in imports:
|
|
88
|
+
if imp.id not in seen_ids:
|
|
89
|
+
seen_ids.add(imp.id)
|
|
90
|
+
unique_imports.append(imp)
|
|
91
|
+
|
|
92
|
+
# Group by source
|
|
93
|
+
grouped: dict[str, list[Import]] = {}
|
|
94
|
+
for imp in unique_imports:
|
|
95
|
+
if imp.src not in grouped:
|
|
96
|
+
grouped[imp.src] = []
|
|
97
|
+
grouped[imp.src].append(imp)
|
|
98
|
+
|
|
99
|
+
# Topological sort using Import.before constraints (Kahn's algorithm)
|
|
100
|
+
keys = list(grouped.keys())
|
|
101
|
+
if not keys:
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
index = {k: i for i, k in enumerate(keys)} # for stability
|
|
105
|
+
indegree: dict[str, int] = {k: 0 for k in keys}
|
|
106
|
+
adj: dict[str, list[str]] = {k: [] for k in keys}
|
|
107
|
+
|
|
108
|
+
for src, src_imports in grouped.items():
|
|
109
|
+
for imp in src_imports:
|
|
110
|
+
for before_src in imp.before:
|
|
111
|
+
if before_src in adj:
|
|
112
|
+
adj[src].append(before_src)
|
|
113
|
+
indegree[before_src] += 1
|
|
114
|
+
|
|
115
|
+
queue = [k for k, d in indegree.items() if d == 0]
|
|
116
|
+
queue.sort(key=lambda k: index[k])
|
|
117
|
+
ordered: list[str] = []
|
|
118
|
+
|
|
119
|
+
while queue:
|
|
120
|
+
u = queue.pop(0)
|
|
121
|
+
ordered.append(u)
|
|
122
|
+
for v in adj[u]:
|
|
123
|
+
indegree[v] -= 1
|
|
124
|
+
if indegree[v] == 0:
|
|
125
|
+
queue.append(v)
|
|
126
|
+
queue.sort(key=lambda k: index[k])
|
|
127
|
+
|
|
128
|
+
# Fall back to insertion order if cycle detected
|
|
129
|
+
if len(ordered) != len(keys):
|
|
130
|
+
ordered = keys
|
|
131
|
+
|
|
132
|
+
lines: list[str] = []
|
|
133
|
+
for src in ordered:
|
|
134
|
+
stmt = _generate_import_statement(src, grouped[src])
|
|
135
|
+
if stmt:
|
|
136
|
+
lines.append(stmt)
|
|
137
|
+
|
|
138
|
+
return "\n".join(lines)
|
|
139
|
+
|
|
140
|
+
|
|
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:
|
|
172
|
+
"""Generate the constants section."""
|
|
173
|
+
if not constants:
|
|
174
|
+
return ""
|
|
175
|
+
|
|
176
|
+
lines: list[str] = ["// Constants"]
|
|
177
|
+
for const in constants:
|
|
178
|
+
js_value = const.expr.emit()
|
|
179
|
+
lines.append(f"const {const.js_name} = {js_value};")
|
|
180
|
+
|
|
181
|
+
return "\n".join(lines)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _generate_functions_section(functions: Sequence[AnyJsFunction]) -> str:
|
|
185
|
+
"""Generate the functions section with actual transpiled code."""
|
|
186
|
+
if not functions:
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
lines: list[str] = ["// Functions"]
|
|
190
|
+
for fn in functions:
|
|
191
|
+
js_code = fn.transpile()
|
|
192
|
+
lines.append(js_code)
|
|
193
|
+
|
|
194
|
+
return "\n".join(lines)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
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
|
+
) -> str:
|
|
203
|
+
"""Generate the unified registry containing all imports for runtime lookup."""
|
|
204
|
+
lines: list[str] = []
|
|
205
|
+
|
|
206
|
+
# Unified Registry - contains all imports that need to be looked up at runtime
|
|
207
|
+
lines.append("// Unified Registry")
|
|
208
|
+
lines.append("const __registry = {")
|
|
209
|
+
|
|
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
|
+
)
|
|
240
|
+
|
|
241
|
+
# Add transpiled functions to the registry
|
|
242
|
+
for fn in functions or []:
|
|
243
|
+
lines.append(f' "{fn.js_name}": {fn.js_name},')
|
|
244
|
+
|
|
245
|
+
lines.append("};")
|
|
246
|
+
|
|
247
|
+
return "\n".join(lines)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def generate_route(
|
|
251
|
+
path: str,
|
|
252
|
+
components: Sequence[ReactComponent[...]] | None = None,
|
|
253
|
+
route_file_path: Path | None = None,
|
|
254
|
+
css_dir: Path | None = None,
|
|
255
|
+
) -> str:
|
|
256
|
+
"""Generate a route file with all imports and components.
|
|
38
257
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
258
|
+
Args:
|
|
259
|
+
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)
|
|
43
263
|
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
self._imports.add_statement(stmt)
|
|
118
|
-
|
|
119
|
-
def add_external_js(self, fns: Sequence[ExternalJsFunction[*Args, object]]) -> None:
|
|
120
|
-
for fn in fns:
|
|
121
|
-
self._imports.import_(fn.src, fn.name, is_default=True)
|
|
122
|
-
# TODO: update fn in case of aliasing
|
|
123
|
-
|
|
124
|
-
def reserve_js_function_names(
|
|
125
|
-
self, js_functions: Sequence[JsFunction[*Args, object]]
|
|
126
|
-
) -> None:
|
|
127
|
-
for j in js_functions:
|
|
128
|
-
self._js_local_names[j.name] = self.names.register(j.name)
|
|
129
|
-
# TODO: update fn in case of aliasing
|
|
130
|
-
|
|
131
|
-
def context(self) -> dict[str, object]:
|
|
132
|
-
# Deterministic order of import sources with ordering constraints
|
|
133
|
-
import_sources = self._imports.ordered_sources()
|
|
134
|
-
return {
|
|
135
|
-
"import_sources": import_sources,
|
|
136
|
-
"components_ctx": list(self.components_by_key.values()),
|
|
137
|
-
"local_js_names": self._js_local_names,
|
|
138
|
-
"needs_render_lazy": self.needs_render_lazy,
|
|
139
|
-
"css_modules_ctx": list(self._css_modules.values()),
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def dynamic_selector(comp: "ReactComponent[...]"):
|
|
144
|
-
# Dynamic import mapping for lazy usage on the client
|
|
145
|
-
attr = "default" if comp.is_default else comp.name
|
|
146
|
-
prop_accessor = f".{comp.prop}" if comp.prop else ""
|
|
147
|
-
return f"({{ default: m.{attr}{prop_accessor} }})"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# Constants and functions defined in the template below. We need to avoid name conflicts with imports
|
|
151
|
-
RESERVED_NAMES = [
|
|
152
|
-
"externalComponents",
|
|
153
|
-
"path",
|
|
154
|
-
"RouteComponent",
|
|
155
|
-
"hasAnyHeaders",
|
|
156
|
-
"headers",
|
|
157
|
-
"HeadersArgs",
|
|
158
|
-
"PulseView",
|
|
159
|
-
"ComponentRegistry",
|
|
160
|
-
"RenderLazy",
|
|
161
|
-
"cssModules",
|
|
162
|
-
]
|
|
163
|
-
|
|
164
|
-
TEMPLATE = Template(
|
|
165
|
-
"""import { PulseView, type ComponentRegistry${", " + "RenderLazy" if needs_render_lazy else ""} } from "pulse-ui-client";
|
|
166
|
-
import type { HeadersArgs } from "react-router";
|
|
167
|
-
|
|
168
|
-
% if import_sources:
|
|
169
|
-
// Component and helper imports
|
|
170
|
-
% for import_source in import_sources:
|
|
171
|
-
% if import_source.default_import:
|
|
172
|
-
import ${import_source.default_import} from "${import_source.src}";
|
|
173
|
-
% endif
|
|
174
|
-
% if import_source.values:
|
|
175
|
-
import { ${', '.join([f"{v.name}{f' as {v.alias}' if v.alias else ''}" for v in import_source.values])} } from "${import_source.src}";
|
|
176
|
-
% endif
|
|
177
|
-
% if import_source.types:
|
|
178
|
-
import type { ${', '.join([f"{t.name}{f' as {t.alias}' if t.alias else ''}" for t in import_source.types])} } from "${import_source.src}";
|
|
179
|
-
% endif
|
|
180
|
-
% if import_source.side_effect and (not import_source.default_import) and (not import_source.values) and (not import_source.types):
|
|
181
|
-
import "${import_source.src}";
|
|
182
|
-
% endif
|
|
183
|
-
% endfor
|
|
184
|
-
% endif
|
|
185
|
-
|
|
186
|
-
// Component registry
|
|
187
|
-
% if css_modules_ctx:
|
|
188
|
-
const cssModules = {
|
|
189
|
-
% for mod in css_modules_ctx:
|
|
190
|
-
"${mod['id']}": ${mod['identifier']},
|
|
191
|
-
% endfor
|
|
192
|
-
};
|
|
193
|
-
% else:
|
|
194
|
-
const cssModules = {};
|
|
195
|
-
% endif
|
|
196
|
-
|
|
197
|
-
% if components_ctx:
|
|
198
|
-
const externalComponents: ComponentRegistry = {
|
|
199
|
-
% for c in components_ctx:
|
|
200
|
-
% if c['lazy']:
|
|
201
|
-
"${c['key']}": RenderLazy(() => import("${c['src']}").then((m) => ${c['dynamic']})),
|
|
202
|
-
% else:
|
|
203
|
-
"${c['key']}": ${c['expr']},
|
|
204
|
-
% endif
|
|
205
|
-
% endfor
|
|
206
|
-
};
|
|
207
|
-
% else:
|
|
208
|
-
// No components needed for this route
|
|
209
|
-
const externalComponents: ComponentRegistry = {};
|
|
210
|
-
% endif
|
|
211
|
-
|
|
212
|
-
const path = "${route.unique_path()}";
|
|
213
|
-
|
|
214
|
-
export default function RouteComponent() {
|
|
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())
|
|
293
|
+
|
|
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]
|
|
296
|
+
|
|
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
|
|
300
|
+
|
|
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()
|
|
309
|
+
|
|
310
|
+
# Generate output sections
|
|
311
|
+
output_parts: list[str] = []
|
|
312
|
+
|
|
313
|
+
imports_section = _generate_imports_section(all_imports)
|
|
314
|
+
if imports_section:
|
|
315
|
+
output_parts.append(imports_section)
|
|
316
|
+
|
|
317
|
+
output_parts.append("")
|
|
318
|
+
|
|
319
|
+
if constants:
|
|
320
|
+
output_parts.append(_generate_constants_section(constants))
|
|
321
|
+
output_parts.append("")
|
|
322
|
+
|
|
323
|
+
if funcs:
|
|
324
|
+
output_parts.append(_generate_functions_section(funcs))
|
|
325
|
+
output_parts.append("")
|
|
326
|
+
|
|
327
|
+
output_parts.append(
|
|
328
|
+
_generate_registry_section(all_imports, lazy_components, prop_components, funcs)
|
|
329
|
+
)
|
|
330
|
+
output_parts.append("")
|
|
331
|
+
|
|
332
|
+
# Route component
|
|
333
|
+
pulse_view_js = pulse_view_import.js_name
|
|
334
|
+
output_parts.append(f'''const path = "{path}";
|
|
335
|
+
|
|
336
|
+
export default function RouteComponent() {{
|
|
215
337
|
return (
|
|
216
|
-
<
|
|
338
|
+
<{pulse_view_js} key={{path}} registry={{__registry}} path={{path}} />
|
|
217
339
|
);
|
|
218
|
-
}
|
|
340
|
+
}}''')
|
|
341
|
+
output_parts.append("")
|
|
219
342
|
|
|
220
|
-
|
|
221
|
-
|
|
343
|
+
# Headers function
|
|
344
|
+
output_parts.append("""// Action and loader headers are not returned automatically
|
|
345
|
+
function hasAnyHeaders(headers) {
|
|
222
346
|
return [...headers].length > 0;
|
|
223
347
|
}
|
|
224
348
|
|
|
225
|
-
export function headers({
|
|
226
|
-
actionHeaders
|
|
227
|
-
|
|
228
|
-
}: HeadersArgs) {
|
|
229
|
-
return hasAnyHeaders(actionHeaders)
|
|
230
|
-
? actionHeaders
|
|
231
|
-
: loaderHeaders;
|
|
232
|
-
}
|
|
233
|
-
"""
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
# Back-compat alias
|
|
237
|
-
ROUTE_TEMPLATE = TEMPLATE
|
|
349
|
+
export function headers({ actionHeaders, loaderHeaders }) {
|
|
350
|
+
return hasAnyHeaders(actionHeaders) ? actionHeaders : loaderHeaders;
|
|
351
|
+
}""")
|
|
238
352
|
|
|
239
|
-
|
|
240
|
-
def render_route(
|
|
241
|
-
*,
|
|
242
|
-
route: Route | Layout,
|
|
243
|
-
components: Sequence[ReactComponent[...]] | None = None,
|
|
244
|
-
css_modules: Sequence[CssModuleImport] | None = None,
|
|
245
|
-
css_imports: Sequence[str] | None = None,
|
|
246
|
-
js_functions: Sequence[JsFunction[*Args, object]] | None = None,
|
|
247
|
-
external_js: Sequence[ExternalJsFunction[*Args, object]] | None = None,
|
|
248
|
-
reserved_names: Iterable[str] | None = None,
|
|
249
|
-
) -> str:
|
|
250
|
-
comps = list(components or [])
|
|
251
|
-
|
|
252
|
-
jt = RouteTemplate(reserved_names=reserved_names)
|
|
253
|
-
jt.add_components(comps)
|
|
254
|
-
modules = list(css_modules or [])
|
|
255
|
-
if modules:
|
|
256
|
-
jt.add_css_modules(modules)
|
|
257
|
-
imports = list(css_imports or [])
|
|
258
|
-
if imports:
|
|
259
|
-
jt.add_css_imports(imports)
|
|
260
|
-
if external_js:
|
|
261
|
-
jt.add_external_js(list(external_js))
|
|
262
|
-
if js_functions:
|
|
263
|
-
jt.reserve_js_function_names(list(js_functions))
|
|
264
|
-
|
|
265
|
-
ctx = jt.context() | {"route": route}
|
|
266
|
-
return str(TEMPLATE.render_unicode(**ctx))
|
|
353
|
+
return "\n".join(output_parts)
|
pulse/form.py
CHANGED
|
@@ -278,7 +278,7 @@ class ManualForm(Disposable):
|
|
|
278
278
|
|
|
279
279
|
|
|
280
280
|
class FormStorage(HookState):
|
|
281
|
-
__slots__
|
|
281
|
+
__slots__ = ("forms", "prev_forms", "render_mark") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
282
282
|
render_mark: int
|
|
283
283
|
|
|
284
284
|
def __init__(self) -> None:
|