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/__init__.py
CHANGED
|
@@ -51,23 +51,6 @@ from pulse.context import PulseContext as PulseContext
|
|
|
51
51
|
# Cookies
|
|
52
52
|
from pulse.cookies import Cookie as Cookie
|
|
53
53
|
from pulse.cookies import SetCookie as SetCookie
|
|
54
|
-
from pulse.css import (
|
|
55
|
-
CssImport as CssImport,
|
|
56
|
-
)
|
|
57
|
-
from pulse.css import (
|
|
58
|
-
CssModule as CssModule,
|
|
59
|
-
)
|
|
60
|
-
from pulse.css import (
|
|
61
|
-
CssReference as CssReference,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
# CSS
|
|
65
|
-
from pulse.css import (
|
|
66
|
-
css as css,
|
|
67
|
-
)
|
|
68
|
-
from pulse.css import (
|
|
69
|
-
css_module as css_module,
|
|
70
|
-
)
|
|
71
54
|
|
|
72
55
|
# Decorators
|
|
73
56
|
from pulse.decorators import computed as computed
|
|
@@ -96,12 +79,6 @@ from pulse.form import (
|
|
|
96
79
|
from pulse.helpers import (
|
|
97
80
|
CSSProperties as CSSProperties,
|
|
98
81
|
)
|
|
99
|
-
from pulse.helpers import (
|
|
100
|
-
JsFunction as JsFunction,
|
|
101
|
-
)
|
|
102
|
-
from pulse.helpers import (
|
|
103
|
-
JsObject as JsObject,
|
|
104
|
-
)
|
|
105
82
|
from pulse.helpers import (
|
|
106
83
|
later as later,
|
|
107
84
|
)
|
|
@@ -1431,12 +1408,16 @@ from pulse.reactive_extensions import (
|
|
|
1431
1408
|
from pulse.reactive_extensions import (
|
|
1432
1409
|
unwrap as unwrap,
|
|
1433
1410
|
)
|
|
1411
|
+
|
|
1412
|
+
# JavaScript execution
|
|
1413
|
+
from pulse.render_session import JsExecError as JsExecError
|
|
1434
1414
|
from pulse.render_session import (
|
|
1435
1415
|
RenderSession as RenderSession,
|
|
1436
1416
|
)
|
|
1437
1417
|
from pulse.render_session import (
|
|
1438
1418
|
RouteMount as RouteMount,
|
|
1439
1419
|
)
|
|
1420
|
+
from pulse.render_session import run_js as run_js
|
|
1440
1421
|
|
|
1441
1422
|
# Request
|
|
1442
1423
|
from pulse.request import PulseRequest as PulseRequest
|
|
@@ -1450,6 +1431,11 @@ from pulse.serializer import serialize as serialize
|
|
|
1450
1431
|
|
|
1451
1432
|
# State and routing
|
|
1452
1433
|
from pulse.state import State as State
|
|
1434
|
+
from pulse.transpiler.function import JsFunction as JsFunction
|
|
1435
|
+
from pulse.transpiler.function import javascript as javascript
|
|
1436
|
+
from pulse.transpiler.imports import CssImport as CssImport
|
|
1437
|
+
from pulse.transpiler.imports import Import as Import
|
|
1438
|
+
from pulse.transpiler.imports import import_js as import_js
|
|
1453
1439
|
|
|
1454
1440
|
# Types
|
|
1455
1441
|
from pulse.types.event_handler import (
|
pulse/app.py
CHANGED
|
@@ -31,12 +31,6 @@ from pulse.cookies import (
|
|
|
31
31
|
cors_options,
|
|
32
32
|
session_cookie,
|
|
33
33
|
)
|
|
34
|
-
from pulse.css import (
|
|
35
|
-
CssImport,
|
|
36
|
-
CssModule,
|
|
37
|
-
registered_css_imports,
|
|
38
|
-
registered_css_modules,
|
|
39
|
-
)
|
|
40
34
|
from pulse.env import (
|
|
41
35
|
ENV_PULSE_HOST,
|
|
42
36
|
ENV_PULSE_PORT,
|
|
@@ -216,8 +210,6 @@ class App:
|
|
|
216
210
|
|
|
217
211
|
# Auto-add React components to all routes
|
|
218
212
|
add_react_components(all_routes, registered_react_components())
|
|
219
|
-
add_css_modules(all_routes, registered_css_modules())
|
|
220
|
-
add_css_imports(all_routes, registered_css_imports())
|
|
221
213
|
# RouteTree filters routes based on dev flag and environment during construction
|
|
222
214
|
self.routes = RouteTree(all_routes)
|
|
223
215
|
self.not_found = not_found
|
|
@@ -584,7 +576,10 @@ class App:
|
|
|
584
576
|
+ "Use 'pulse run' CLI command or set the environment variable."
|
|
585
577
|
)
|
|
586
578
|
|
|
587
|
-
proxy_handler = ReactProxy(
|
|
579
|
+
proxy_handler = ReactProxy(
|
|
580
|
+
react_server_address=react_server_address,
|
|
581
|
+
server_address=server_address,
|
|
582
|
+
)
|
|
588
583
|
|
|
589
584
|
@self.fastapi.api_route(
|
|
590
585
|
"/{path:path}",
|
|
@@ -752,6 +747,8 @@ class App:
|
|
|
752
747
|
render.channels.remove_route(msg["path"])
|
|
753
748
|
elif msg["type"] == "api_result":
|
|
754
749
|
render.handle_api_result(dict(msg))
|
|
750
|
+
elif msg["type"] == "js_result":
|
|
751
|
+
render.handle_js_result(dict(msg))
|
|
755
752
|
else:
|
|
756
753
|
logger.warning("Unknown message type received: %s", msg)
|
|
757
754
|
return Ok()
|
|
@@ -1002,19 +999,3 @@ def add_react_components(
|
|
|
1002
999
|
route.components = components
|
|
1003
1000
|
if route.children:
|
|
1004
1001
|
add_react_components(route.children, components)
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
def add_css_modules(routes: Sequence[Route | Layout], modules: list[CssModule]):
|
|
1008
|
-
for route in routes:
|
|
1009
|
-
if route.css_modules is None:
|
|
1010
|
-
route.css_modules = modules
|
|
1011
|
-
if route.children:
|
|
1012
|
-
add_css_modules(route.children, modules)
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
def add_css_imports(routes: Sequence[Route | Layout], imports: list[CssImport]):
|
|
1016
|
-
for route in routes:
|
|
1017
|
-
if route.css_imports is None:
|
|
1018
|
-
route.css_imports = imports
|
|
1019
|
-
if route.children:
|
|
1020
|
-
add_css_imports(route.children, imports)
|
pulse/cli/processes.py
CHANGED
|
@@ -257,6 +257,7 @@ def _write_tagged_line(name: str, message: str, colors: Mapping[str, str]) -> No
|
|
|
257
257
|
if (
|
|
258
258
|
"Network: use --host to expose" in clean_message
|
|
259
259
|
or "press h + enter to show help" in clean_message
|
|
260
|
+
or "➜ Local:" in clean_message
|
|
260
261
|
):
|
|
261
262
|
return
|
|
262
263
|
|
pulse/codegen/codegen.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import os
|
|
3
2
|
from collections.abc import Sequence
|
|
4
3
|
from dataclasses import dataclass
|
|
5
4
|
from pathlib import Path
|
|
@@ -7,15 +6,14 @@ from typing import TYPE_CHECKING
|
|
|
7
6
|
|
|
8
7
|
from pulse.cli.helpers import ensure_gitignore_has
|
|
9
8
|
from pulse.codegen.templates.layout import LAYOUT_TEMPLATE
|
|
10
|
-
from pulse.codegen.templates.route import
|
|
9
|
+
from pulse.codegen.templates.route import generate_route
|
|
11
10
|
from pulse.codegen.templates.routes_ts import (
|
|
12
11
|
ROUTES_CONFIG_TEMPLATE,
|
|
13
12
|
ROUTES_RUNTIME_TEMPLATE,
|
|
14
13
|
)
|
|
15
|
-
from pulse.codegen.utils import NameRegistry
|
|
16
|
-
from pulse.css import CssImport, CssModule
|
|
17
14
|
from pulse.env import env
|
|
18
15
|
from pulse.routing import Layout, Route, RouteTree
|
|
16
|
+
from pulse.transpiler.imports import registered_imports
|
|
19
17
|
|
|
20
18
|
if TYPE_CHECKING:
|
|
21
19
|
from pulse.app import ConnectionStatusConfig
|
|
@@ -97,15 +95,13 @@ def write_file_if_changed(path: Path, content: str) -> Path:
|
|
|
97
95
|
class Codegen:
|
|
98
96
|
cfg: CodegenConfig
|
|
99
97
|
routes: RouteTree
|
|
100
|
-
_css_name_registry: NameRegistry
|
|
101
98
|
|
|
102
99
|
def __init__(self, routes: RouteTree, config: CodegenConfig) -> None:
|
|
103
100
|
self.cfg = config
|
|
104
101
|
self.routes = routes
|
|
105
|
-
self.
|
|
106
|
-
|
|
107
|
-
self.
|
|
108
|
-
self._css_import_dest: dict[str, Path | str] = {}
|
|
102
|
+
self._copied_css_files: set[Path] = set()
|
|
103
|
+
# Maps source path -> destination path for CSS files
|
|
104
|
+
self._css_dest_paths: dict[str, Path] = {}
|
|
109
105
|
|
|
110
106
|
@property
|
|
111
107
|
def output_folder(self):
|
|
@@ -121,10 +117,12 @@ class Codegen:
|
|
|
121
117
|
# Ensure generated files are gitignored
|
|
122
118
|
ensure_gitignore_has(self.cfg.web_root, f"app/{self.cfg.pulse_dir}/")
|
|
123
119
|
|
|
124
|
-
self.
|
|
125
|
-
self.
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
self._copied_css_files = set()
|
|
121
|
+
self._css_dest_paths = {}
|
|
122
|
+
|
|
123
|
+
# Copy all registered CSS files to the output css directory
|
|
124
|
+
self._copy_css_files()
|
|
125
|
+
|
|
128
126
|
# Keep track of all generated files
|
|
129
127
|
generated_files = set(
|
|
130
128
|
[
|
|
@@ -142,7 +140,7 @@ class Codegen:
|
|
|
142
140
|
),
|
|
143
141
|
]
|
|
144
142
|
)
|
|
145
|
-
generated_files.update(self.
|
|
143
|
+
generated_files.update(self._copied_css_files)
|
|
146
144
|
|
|
147
145
|
# Clean up any remaining files that are not part of the generated files
|
|
148
146
|
for path in self.output_folder.rglob("*"):
|
|
@@ -153,6 +151,32 @@ class Codegen:
|
|
|
153
151
|
except Exception as e:
|
|
154
152
|
logger.warning(f"Could not remove stale file {path}: {e}")
|
|
155
153
|
|
|
154
|
+
def _copy_css_files(self) -> None:
|
|
155
|
+
"""Copy all registered local CSS files to the output css directory."""
|
|
156
|
+
from pulse.transpiler.imports import CssImport
|
|
157
|
+
|
|
158
|
+
css_dir = self.output_folder / "css"
|
|
159
|
+
|
|
160
|
+
for imp in registered_imports():
|
|
161
|
+
if not isinstance(imp, CssImport) or not imp.is_local:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
# Local CssImport has source_path and generated_filename set
|
|
165
|
+
source_path = imp.source_path
|
|
166
|
+
generated_filename = imp.generated_filename
|
|
167
|
+
assert source_path is not None and generated_filename is not None
|
|
168
|
+
|
|
169
|
+
if not source_path.exists():
|
|
170
|
+
logger.warning(f"CSS file not found: {source_path}")
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
dest_path = css_dir / generated_filename
|
|
174
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
content = source_path.read_text()
|
|
176
|
+
write_file_if_changed(dest_path, content)
|
|
177
|
+
self._copied_css_files.add(dest_path)
|
|
178
|
+
self._css_dest_paths[str(source_path)] = dest_path
|
|
179
|
+
|
|
156
180
|
def generate_layout_tsx(
|
|
157
181
|
self,
|
|
158
182
|
server_address: str,
|
|
@@ -232,29 +256,11 @@ class Codegen:
|
|
|
232
256
|
else:
|
|
233
257
|
output_path = self.output_folder / "routes" / route.file_path()
|
|
234
258
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
css_imports: list[CssModuleImport] = []
|
|
241
|
-
for module in css_modules:
|
|
242
|
-
import_path = self._prepare_css_module(module, target_dir)
|
|
243
|
-
css_imports.append({"id": module.id, "import_path": import_path})
|
|
244
|
-
|
|
245
|
-
css_side_effect_specs: list[str] = []
|
|
246
|
-
for css_import in css_side_effects:
|
|
247
|
-
spec = self._prepare_css_import(css_import, target_dir)
|
|
248
|
-
css_side_effect_specs.append(spec)
|
|
249
|
-
|
|
250
|
-
content = render_route(
|
|
251
|
-
route=route,
|
|
252
|
-
components=components,
|
|
253
|
-
css_modules=css_imports,
|
|
254
|
-
css_imports=css_side_effect_specs,
|
|
255
|
-
js_functions=[],
|
|
256
|
-
external_js=[],
|
|
257
|
-
reserved_names=None,
|
|
259
|
+
content = generate_route(
|
|
260
|
+
path=route.unique_path(),
|
|
261
|
+
components=list(route.components) if route.components else None,
|
|
262
|
+
route_file_path=output_path,
|
|
263
|
+
css_dir=self.output_folder / "css",
|
|
258
264
|
)
|
|
259
265
|
return write_file_if_changed(output_path, content)
|
|
260
266
|
|
|
@@ -304,54 +310,3 @@ class Codegen:
|
|
|
304
310
|
out.append(f"{ind} ,")
|
|
305
311
|
out.append(f"{ind}]")
|
|
306
312
|
return "\n".join(out)
|
|
307
|
-
|
|
308
|
-
def _copy_css_source(self, source_path: Path) -> Path:
|
|
309
|
-
name = source_path.name
|
|
310
|
-
if name.endswith(".module.css"):
|
|
311
|
-
suffix = ".module.css"
|
|
312
|
-
base_name = name[: -len(suffix)] or "style"
|
|
313
|
-
else:
|
|
314
|
-
suffix = source_path.suffix or ".css"
|
|
315
|
-
base_name = source_path.stem or "style"
|
|
316
|
-
|
|
317
|
-
unique_name = self._css_name_registry.register(base_name)
|
|
318
|
-
dest_filename = f"{unique_name}{suffix}"
|
|
319
|
-
dest_path = self.output_folder / "css" / dest_filename
|
|
320
|
-
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
321
|
-
content = source_path.read_text()
|
|
322
|
-
write_file_if_changed(dest_path, content)
|
|
323
|
-
self._copied_css_modules.add(dest_path)
|
|
324
|
-
return dest_path
|
|
325
|
-
|
|
326
|
-
def _copy_css_module(self, module: CssModule) -> Path:
|
|
327
|
-
return self._copy_css_source(module.source_path)
|
|
328
|
-
|
|
329
|
-
def _prepare_css_import(self, css_import: CssImport, target_dir: Path) -> str:
|
|
330
|
-
existing = self._css_import_dest.get(css_import.id)
|
|
331
|
-
if existing is None:
|
|
332
|
-
if css_import.source_path is not None:
|
|
333
|
-
dest_path = self._copy_css_source(css_import.source_path)
|
|
334
|
-
existing = dest_path
|
|
335
|
-
else:
|
|
336
|
-
existing = css_import.specifier or ""
|
|
337
|
-
self._css_import_dest[css_import.id] = existing
|
|
338
|
-
|
|
339
|
-
value = self._css_import_dest[css_import.id]
|
|
340
|
-
if isinstance(value, Path):
|
|
341
|
-
rel_path = os.path.relpath(value, target_dir)
|
|
342
|
-
rel_posix = Path(rel_path).as_posix()
|
|
343
|
-
if not rel_posix.startswith("."):
|
|
344
|
-
rel_posix = f"./{rel_posix}"
|
|
345
|
-
return rel_posix
|
|
346
|
-
return value
|
|
347
|
-
|
|
348
|
-
def _prepare_css_module(self, module: CssModule, target_dir: Path) -> str:
|
|
349
|
-
dest = self._css_module_dest.get(module.id)
|
|
350
|
-
if dest is None:
|
|
351
|
-
dest = self._copy_css_module(module)
|
|
352
|
-
self._css_module_dest[module.id] = dest
|
|
353
|
-
rel_path = os.path.relpath(dest, target_dir)
|
|
354
|
-
rel_posix = Path(rel_path).as_posix()
|
|
355
|
-
if not rel_posix.startswith("."):
|
|
356
|
-
rel_posix = f"./{rel_posix}"
|
|
357
|
-
return rel_posix
|
pulse/codegen/js.py
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# Placeholders for the WIP JS compilation feature
|
|
2
|
+
# NOTE: This module is deprecated. Use pulse.transpiler instead.
|
|
2
3
|
|
|
3
4
|
from collections.abc import Callable
|
|
4
5
|
from typing import Generic, TypeVar, TypeVarTuple
|
|
5
6
|
|
|
6
|
-
from pulse.
|
|
7
|
+
from pulse.transpiler.imports import Import
|
|
7
8
|
|
|
8
9
|
Args = TypeVarTuple("Args")
|
|
9
10
|
R = TypeVar("R")
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class JsFunction(Generic[*Args, R]):
|
|
13
|
-
"A transpiled JS function"
|
|
14
|
+
"A transpiled JS function (deprecated - use pulse.transpiler.function.JsFunction)"
|
|
14
15
|
|
|
15
16
|
name: str
|
|
16
17
|
hint: Callable[[*Args], R]
|
|
@@ -26,10 +27,12 @@ class JsFunction(Generic[*Args, R]):
|
|
|
26
27
|
def __call__(self, *args: *Args) -> R: ...
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
class ExternalJsFunction(Generic[*Args, R]
|
|
30
|
-
"An imported JS function"
|
|
30
|
+
class ExternalJsFunction(Generic[*Args, R]):
|
|
31
|
+
"An imported JS function (deprecated - use pulse.transpiler.imports.Import)"
|
|
31
32
|
|
|
33
|
+
import_: Import
|
|
32
34
|
hint: Callable[[*Args], R]
|
|
35
|
+
_prop: str | None
|
|
33
36
|
|
|
34
37
|
def __init__(
|
|
35
38
|
self,
|
|
@@ -40,7 +43,34 @@ class ExternalJsFunction(Generic[*Args, R], Imported):
|
|
|
40
43
|
is_default: bool,
|
|
41
44
|
hint: Callable[[*Args], R],
|
|
42
45
|
) -> None:
|
|
43
|
-
|
|
46
|
+
if is_default:
|
|
47
|
+
self.import_ = Import.default(name, src)
|
|
48
|
+
else:
|
|
49
|
+
self.import_ = Import.named(name, src)
|
|
50
|
+
self._prop = prop
|
|
44
51
|
self.hint = hint
|
|
45
52
|
|
|
53
|
+
@property
|
|
54
|
+
def name(self) -> str:
|
|
55
|
+
return self.import_.name
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def src(self) -> str:
|
|
59
|
+
return self.import_.src
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_default(self) -> bool:
|
|
63
|
+
return self.import_.is_default
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def prop(self) -> str | None:
|
|
67
|
+
return self._prop
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def expr(self) -> str:
|
|
71
|
+
base = self.import_.js_name
|
|
72
|
+
if self._prop:
|
|
73
|
+
return f"{base}.{self._prop}"
|
|
74
|
+
return base
|
|
75
|
+
|
|
46
76
|
def __call__(self, *args: *Args) -> R: ...
|