pulse-framework 0.1.46__py3-none-any.whl → 0.1.47__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 (71) hide show
  1. pulse/__init__.py +9 -23
  2. pulse/app.py +2 -24
  3. pulse/codegen/codegen.py +43 -88
  4. pulse/codegen/js.py +35 -5
  5. pulse/codegen/templates/route.py +341 -254
  6. pulse/form.py +1 -1
  7. pulse/helpers.py +40 -8
  8. pulse/hooks/core.py +2 -2
  9. pulse/hooks/effects.py +1 -1
  10. pulse/hooks/init.py +2 -1
  11. pulse/hooks/setup.py +1 -1
  12. pulse/hooks/stable.py +2 -2
  13. pulse/hooks/states.py +2 -2
  14. pulse/html/props.py +3 -2
  15. pulse/html/tags.py +135 -0
  16. pulse/html/tags.pyi +4 -0
  17. pulse/js/__init__.py +110 -0
  18. pulse/js/__init__.pyi +95 -0
  19. pulse/js/_types.py +297 -0
  20. pulse/js/array.py +253 -0
  21. pulse/js/console.py +47 -0
  22. pulse/js/date.py +113 -0
  23. pulse/js/document.py +138 -0
  24. pulse/js/error.py +139 -0
  25. pulse/js/json.py +62 -0
  26. pulse/js/map.py +84 -0
  27. pulse/js/math.py +66 -0
  28. pulse/js/navigator.py +76 -0
  29. pulse/js/number.py +54 -0
  30. pulse/js/object.py +173 -0
  31. pulse/js/promise.py +150 -0
  32. pulse/js/regexp.py +54 -0
  33. pulse/js/set.py +109 -0
  34. pulse/js/string.py +35 -0
  35. pulse/js/weakmap.py +50 -0
  36. pulse/js/weakset.py +45 -0
  37. pulse/js/window.py +199 -0
  38. pulse/messages.py +22 -3
  39. pulse/react_component.py +167 -14
  40. pulse/reactive_extensions.py +5 -5
  41. pulse/render_session.py +144 -34
  42. pulse/renderer.py +80 -115
  43. pulse/routing.py +1 -18
  44. pulse/transpiler/__init__.py +131 -0
  45. pulse/transpiler/builtins.py +731 -0
  46. pulse/transpiler/constants.py +110 -0
  47. pulse/transpiler/context.py +26 -0
  48. pulse/transpiler/errors.py +2 -0
  49. pulse/transpiler/function.py +250 -0
  50. pulse/transpiler/ids.py +16 -0
  51. pulse/transpiler/imports.py +409 -0
  52. pulse/transpiler/js_module.py +274 -0
  53. pulse/transpiler/modules/__init__.py +30 -0
  54. pulse/transpiler/modules/asyncio.py +38 -0
  55. pulse/transpiler/modules/json.py +20 -0
  56. pulse/transpiler/modules/math.py +320 -0
  57. pulse/transpiler/modules/re.py +466 -0
  58. pulse/transpiler/modules/tags.py +268 -0
  59. pulse/transpiler/modules/typing.py +59 -0
  60. pulse/transpiler/nodes.py +1216 -0
  61. pulse/transpiler/py_module.py +119 -0
  62. pulse/transpiler/transpiler.py +938 -0
  63. pulse/transpiler/utils.py +4 -0
  64. pulse/vdom.py +112 -6
  65. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.47.dist-info}/METADATA +1 -1
  66. pulse_framework-0.1.47.dist-info/RECORD +119 -0
  67. pulse/codegen/imports.py +0 -204
  68. pulse/css.py +0 -155
  69. pulse_framework-0.1.46.dist-info/RECORD +0 -80
  70. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.47.dist-info}/WHEEL +0 -0
  71. {pulse_framework-0.1.46.dist-info → pulse_framework-0.1.47.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
@@ -752,6 +744,8 @@ class App:
752
744
  render.channels.remove_route(msg["path"])
753
745
  elif msg["type"] == "api_result":
754
746
  render.handle_api_result(dict(msg))
747
+ elif msg["type"] == "js_result":
748
+ render.handle_js_result(dict(msg))
755
749
  else:
756
750
  logger.warning("Unknown message type received: %s", msg)
757
751
  return Ok()
@@ -1002,19 +996,3 @@ def add_react_components(
1002
996
  route.components = components
1003
997
  if route.children:
1004
998
  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/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 CssModuleImport, render_route
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._css_module_dest: dict[str, Path] = {}
106
- self._copied_css_modules: set[Path] = set()
107
- self._css_name_registry = NameRegistry()
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._copied_css_modules = set()
125
- self._css_module_dest = {}
126
- self._css_name_registry = NameRegistry()
127
- self._css_import_dest = {}
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._copied_css_modules)
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
- components = route.components or []
236
- css_modules = route.css_modules or []
237
- css_side_effects = route.css_imports or []
238
-
239
- target_dir = output_path.parent
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.codegen.imports import Imported
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], Imported):
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
- super().__init__(name, src, is_default=is_default, prop=prop)
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: ...