pulse-framework 0.1.57__tar.gz → 0.1.59__tar.gz

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 (126) hide show
  1. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/PKG-INFO +5 -3
  2. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/pyproject.toml +5 -3
  3. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/app.py +0 -2
  4. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/cmd.py +7 -2
  5. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/codegen/codegen.py +21 -8
  6. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/components/react_router.py +2 -4
  7. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/react.py +1 -1
  8. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/react_component.py +26 -6
  9. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/render_session.py +0 -1
  10. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/dynamic_import.py +1 -1
  11. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/imports.py +30 -14
  12. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/js_module.py +3 -2
  13. pulse_framework-0.1.57/src/pulse/codegen/js.py +0 -74
  14. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/README.md +0 -0
  15. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/__init__.py +0 -0
  16. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/_examples.py +0 -0
  17. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/channel.py +0 -0
  18. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/__init__.py +0 -0
  19. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/dependencies.py +0 -0
  20. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/folder_lock.py +0 -0
  21. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/helpers.py +0 -0
  22. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/logging.py +0 -0
  23. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/models.py +0 -0
  24. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/packages.py +0 -0
  25. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/processes.py +0 -0
  26. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/secrets.py +0 -0
  27. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cli/uvicorn_log_config.py +0 -0
  28. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/code_analysis.py +0 -0
  29. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/codegen/__init__.py +0 -0
  30. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/codegen/templates/__init__.py +0 -0
  31. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/codegen/templates/layout.py +0 -0
  32. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/codegen/templates/route.py +0 -0
  33. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/codegen/templates/routes_ts.py +0 -0
  34. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/codegen/utils.py +0 -0
  35. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/component.py +0 -0
  36. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/components/__init__.py +0 -0
  37. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/components/for_.py +0 -0
  38. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/components/if_.py +0 -0
  39. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/context.py +0 -0
  40. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/cookies.py +0 -0
  41. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/decorators.py +0 -0
  42. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/dom/__init__.py +0 -0
  43. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/dom/elements.py +0 -0
  44. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/dom/events.py +0 -0
  45. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/dom/props.py +0 -0
  46. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/dom/svg.py +0 -0
  47. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/dom/tags.py +0 -0
  48. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/dom/tags.pyi +0 -0
  49. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/env.py +0 -0
  50. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/form.py +0 -0
  51. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/helpers.py +0 -0
  52. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/hooks/__init__.py +0 -0
  53. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/hooks/core.py +0 -0
  54. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/hooks/effects.py +0 -0
  55. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/hooks/init.py +0 -0
  56. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/hooks/runtime.py +0 -0
  57. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/hooks/setup.py +0 -0
  58. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/hooks/stable.py +0 -0
  59. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/hooks/state.py +0 -0
  60. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/__init__.py +0 -0
  61. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/__init__.pyi +0 -0
  62. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/_types.py +0 -0
  63. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/array.py +0 -0
  64. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/console.py +0 -0
  65. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/date.py +0 -0
  66. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/document.py +0 -0
  67. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/error.py +0 -0
  68. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/json.py +0 -0
  69. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/map.py +0 -0
  70. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/math.py +0 -0
  71. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/navigator.py +0 -0
  72. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/number.py +0 -0
  73. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/obj.py +0 -0
  74. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/object.py +0 -0
  75. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/promise.py +0 -0
  76. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/pulse.py +0 -0
  77. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/regexp.py +0 -0
  78. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/set.py +0 -0
  79. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/string.py +0 -0
  80. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/weakmap.py +0 -0
  81. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/weakset.py +0 -0
  82. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/js/window.py +0 -0
  83. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/messages.py +0 -0
  84. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/middleware.py +0 -0
  85. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/plugin.py +0 -0
  86. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/proxy.py +0 -0
  87. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/py.typed +0 -0
  88. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/queries/__init__.py +0 -0
  89. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/queries/client.py +0 -0
  90. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/queries/common.py +0 -0
  91. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/queries/effect.py +0 -0
  92. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/queries/infinite_query.py +0 -0
  93. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/queries/mutation.py +0 -0
  94. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/queries/protocol.py +0 -0
  95. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/queries/query.py +0 -0
  96. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/queries/store.py +0 -0
  97. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/reactive.py +0 -0
  98. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/reactive_extensions.py +0 -0
  99. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/renderer.py +0 -0
  100. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/request.py +0 -0
  101. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/routing.py +0 -0
  102. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/serializer.py +0 -0
  103. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/state.py +0 -0
  104. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/test_helpers.py +0 -0
  105. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/__init__.py +0 -0
  106. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/assets.py +0 -0
  107. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/builtins.py +0 -0
  108. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/emit_context.py +0 -0
  109. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/errors.py +0 -0
  110. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/function.py +0 -0
  111. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/id.py +0 -0
  112. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/modules/__init__.py +0 -0
  113. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/modules/asyncio.py +0 -0
  114. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/modules/json.py +0 -0
  115. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/modules/math.py +0 -0
  116. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
  117. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
  118. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/modules/typing.py +0 -0
  119. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/nodes.py +0 -0
  120. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/py_module.py +0 -0
  121. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/transpiler.py +0 -0
  122. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/transpiler/vdom.py +0 -0
  123. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/types/__init__.py +0 -0
  124. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/types/event_handler.py +0 -0
  125. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/user_session.py +0 -0
  126. {pulse_framework-0.1.57 → pulse_framework-0.1.59}/src/pulse/version.py +0 -0
@@ -1,16 +1,18 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.57
3
+ Version: 0.1.59
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
5
  Requires-Dist: websockets>=12.0
6
- Requires-Dist: fastapi>=0.104.0
6
+ Requires-Dist: fastapi>=0.128.0
7
7
  Requires-Dist: uvicorn>=0.24.0
8
8
  Requires-Dist: mako>=1.3.10
9
9
  Requires-Dist: typer>=0.16.0
10
- Requires-Dist: python-socketio>=5.13.0
10
+ Requires-Dist: python-socketio>=5.16.0
11
11
  Requires-Dist: rich>=13.7.1
12
12
  Requires-Dist: python-multipart>=0.0.20
13
13
  Requires-Dist: python-dateutil>=2.9.0.post0
14
+ Requires-Dist: starlette>=0.50.0,<0.51.0
15
+ Requires-Dist: urllib3>=2.6.3
14
16
  Requires-Dist: watchfiles>=1.1.0
15
17
  Requires-Dist: httpx>=0.28.1
16
18
  Requires-Python: >=3.11
@@ -1,19 +1,21 @@
1
1
  [project]
2
2
  name = "pulse-framework"
3
- version = "0.1.57"
3
+ version = "0.1.59"
4
4
  description = "Pulse - Full-stack framework for building real-time React applications in Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
7
7
  dependencies = [
8
8
  "websockets>=12.0",
9
- "fastapi>=0.104.0",
9
+ "fastapi>=0.128.0",
10
10
  "uvicorn>=0.24.0",
11
11
  "mako>=1.3.10",
12
12
  "typer>=0.16.0",
13
- "python-socketio>=5.13.0",
13
+ "python-socketio>=5.16.0",
14
14
  "rich>=13.7.1",
15
15
  "python-multipart>=0.0.20",
16
16
  "python-dateutil>=2.9.0.post0",
17
+ "starlette>=0.50.0,<0.51.0",
18
+ "urllib3>=2.6.3",
17
19
  "watchfiles>=1.1.0",
18
20
  "httpx>=0.28.1",
19
21
  ]
@@ -823,8 +823,6 @@ class App:
823
823
  async def _handle_pulse_message(
824
824
  self, render: RenderSession, session: UserSession, msg: ClientPulseMessage
825
825
  ) -> None:
826
- print(f"[MSG] {msg}")
827
-
828
826
  async def _next() -> Ok[None]:
829
827
  if msg["type"] == "attach":
830
828
  render.attach(msg["path"], msg["routeInfo"])
@@ -398,6 +398,7 @@ def build_uvicorn_command(
398
398
  on_ready: Callable[[], None] | None = None,
399
399
  plain: bool = False,
400
400
  ) -> CommandSpec:
401
+ cwd = app_ctx.server_cwd or app_ctx.app_dir or Path.cwd()
401
402
  app_import = f"{app_ctx.module_name}:{app_ctx.app_var}.asgi_factory"
402
403
  args: list[str] = [
403
404
  sys.executable,
@@ -418,6 +419,12 @@ def build_uvicorn_command(
418
419
  args.extend(["--reload-dir", str(app_dir)])
419
420
  if web_root.exists():
420
421
  args.extend(["--reload-dir", str(web_root)])
422
+ pulse_dir = str(app_ctx.app.codegen.cfg.pulse_dir)
423
+ pulse_app_dir = web_root / "app" / pulse_dir
424
+ rel_path = Path(os.path.relpath(pulse_app_dir, cwd))
425
+ if not rel_path.is_absolute():
426
+ args.extend(["--reload-exclude", str(rel_path)])
427
+ args.extend(["--reload-exclude", str(rel_path / "**")])
421
428
 
422
429
  if app_ctx.app.env == "prod":
423
430
  args.extend(production_flags())
@@ -450,8 +457,6 @@ def build_uvicorn_command(
450
457
  if dev_secret:
451
458
  command_env[ENV_PULSE_SECRET] = dev_secret
452
459
 
453
- cwd = app_ctx.server_cwd or app_ctx.app_dir or Path.cwd()
454
-
455
460
  # Apply custom log config to filter noisy requests (dev/ci only)
456
461
  if app_ctx.app.env != "prod" and not verbose:
457
462
  import json
@@ -1,5 +1,4 @@
1
1
  import logging
2
- import shutil
3
2
  from collections.abc import Sequence
4
3
  from dataclasses import dataclass
5
4
  from pathlib import Path
@@ -107,20 +106,26 @@ class CodegenConfig:
107
106
  return self.web_root / "app" / self.pulse_dir
108
107
 
109
108
 
110
- def write_file_if_changed(path: Path, content: str) -> Path:
109
+ def write_file_if_changed(path: Path, content: str | bytes) -> Path:
111
110
  """Write content to file only if it has changed."""
112
111
  if path.exists():
113
112
  try:
114
- current_content = path.read_text()
113
+ if isinstance(content, bytes):
114
+ current_content = path.read_bytes()
115
+ else:
116
+ current_content = path.read_text()
115
117
  if current_content == content:
116
118
  return path # Skip writing, content is the same
117
- except Exception:
118
- logging.warning(f"Can't read file {path.absolute()}")
119
+ except Exception as exc:
120
+ logging.warning("Can't read file %s: %s", path.absolute(), exc)
119
121
  # If we can't read the file for any reason, just write it
120
122
  pass
121
123
 
122
124
  path.parent.mkdir(exist_ok=True, parents=True)
123
- path.write_text(content)
125
+ if isinstance(content, bytes):
126
+ path.write_bytes(content)
127
+ else:
128
+ path.write_text(content)
124
129
  return path
125
130
 
126
131
 
@@ -202,9 +207,17 @@ class Codegen:
202
207
 
203
208
  # Copy file if source exists
204
209
  if asset.source_path.exists():
205
- shutil.copy2(asset.source_path, dest_path)
206
210
  self._copied_files.add(dest_path)
207
- logger.debug(f"Copied {asset.source_path} -> {dest_path}")
211
+ try:
212
+ content = asset.source_path.read_bytes()
213
+ except OSError as exc:
214
+ logger.warning(
215
+ "Can't read asset %s: %s",
216
+ asset.source_path,
217
+ exc,
218
+ )
219
+ continue
220
+ write_file_if_changed(dest_path, content)
208
221
 
209
222
  def generate_layout_tsx(
210
223
  self,
@@ -19,8 +19,7 @@ class LinkPath(TypedDict):
19
19
  hash: str
20
20
 
21
21
 
22
- # @react_component(Import("Link", "react-router", version="^7"))
23
- @react_component(Import("Link", "react-router"))
22
+ @react_component(Import("Link", "react-router", version="^7"))
24
23
  def Link(
25
24
  *children: Node,
26
25
  key: str | None = None,
@@ -69,8 +68,7 @@ def Link(
69
68
  ...
70
69
 
71
70
 
72
- # @react_component(Import("Outlet", "react-router", version="^7"))
73
- @react_component(Import("Outlet", "react-router"))
71
+ @react_component(Import("Outlet", "react-router", version="^7"))
74
72
  def Outlet(key: str | None = None) -> None:
75
73
  """Renders the matched child route's element.
76
74
 
@@ -384,7 +384,7 @@ class _LazyComponentFactory(_Expr):
384
384
 
385
385
  ```python
386
386
  # At definition time (Python executes this)
387
- LazyChart = lazy(Import("Chart", "./Chart", lazy=True))
387
+ LazyChart = lazy(Import("./Chart", lazy=True))
388
388
 
389
389
  # As reference in transpiled code
390
390
  @javascript
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from collections.abc import Callable
6
6
  from typing import Any, ParamSpec, overload
7
7
 
8
+ from pulse.js.react import lazy as react_lazy
8
9
  from pulse.transpiler.imports import Import
9
10
  from pulse.transpiler.nodes import Element, Expr, Jsx, Node
10
11
 
@@ -19,9 +20,20 @@ def default_signature(
19
20
  class ReactComponent(Jsx):
20
21
  """JSX wrapper for React components with runtime call support."""
21
22
 
22
- def __init__(self, expr: Expr) -> None:
23
+ def __init__(self, expr_or_src: Expr | str, *, lazy: bool = False) -> None:
24
+ if isinstance(expr_or_src, str):
25
+ if lazy:
26
+ expr: Expr = react_lazy(Import(expr_or_src, lazy=True))
27
+ else:
28
+ expr = Import(expr_or_src)
29
+ else:
30
+ if lazy:
31
+ raise TypeError(
32
+ "ReactComponent lazy only supported with a source string"
33
+ )
34
+ expr = expr_or_src
23
35
  if not isinstance(expr, Expr):
24
- raise TypeError("ReactComponent expects an Expr")
36
+ raise TypeError("ReactComponent expects an Expr or source string")
25
37
  if isinstance(expr, Jsx):
26
38
  expr = expr.expr
27
39
  super().__init__(expr)
@@ -35,7 +47,10 @@ def react_component(
35
47
 
36
48
  @overload
37
49
  def react_component(
38
- expr_or_name: str, src: str, *, lazy: bool = False
50
+ expr_or_name: str,
51
+ src: str | None = None,
52
+ *,
53
+ lazy: bool = False,
39
54
  ) -> Callable[[Callable[P, Any]], Callable[P, Element]]: ...
40
55
 
41
56
 
@@ -50,12 +65,17 @@ def react_component(
50
65
  if src is not None:
51
66
  raise TypeError("react_component expects (expr) or (name, src)")
52
67
  if lazy:
53
- raise TypeError("react_component lazy only supported with (name, src)")
68
+ raise TypeError("react_component lazy only supported with string inputs")
54
69
  component = ReactComponent(expr_or_name)
55
70
  elif isinstance(expr_or_name, str):
56
71
  if src is None:
57
- raise TypeError("react_component expects (name, src)")
58
- component = ReactComponent(Import(expr_or_name, src, lazy=lazy))
72
+ component = ReactComponent(expr_or_name, lazy=lazy)
73
+ else:
74
+ imp = Import(expr_or_name, src, lazy=lazy)
75
+ if lazy:
76
+ component = ReactComponent(react_lazy(imp))
77
+ else:
78
+ component = ReactComponent(imp)
59
79
  else:
60
80
  raise TypeError("react_component expects an Expr or (name, src)")
61
81
 
@@ -457,7 +457,6 @@ class RenderSession:
457
457
  def detach(self, path: str, *, timeout: float | None = None):
458
458
  """Client no longer wants updates. Queue briefly, then dispose."""
459
459
  path = ensure_absolute_path(path)
460
- print(f"Detaching '{path}'")
461
460
  mount = self.route_mounts.get(path)
462
461
  if not mount:
463
462
  return
@@ -11,7 +11,7 @@ For lazy-loaded React components, use Import(lazy=True) with React.lazy:
11
11
  from pulse.js.react import React, lazy
12
12
 
13
13
  # Low-level: Import(lazy=True) creates a factory, wrap with React.lazy
14
- factory = Import("Chart", "./Chart", kind="default", lazy=True)
14
+ factory = Import("./Chart", lazy=True)
15
15
  LazyChart = Jsx(React.lazy(factory))
16
16
 
17
17
  # High-level: lazy() helper combines both
@@ -154,13 +154,13 @@ class Import(Expr):
154
154
  useState = Import("useState", "react")
155
155
 
156
156
  # Default import: import React from "react"
157
- React = Import("React", "react", kind="default")
157
+ React = Import("react")
158
158
 
159
- # Namespace import: import * as utils from "./utils"
160
- utils = Import("utils", "./utils", kind="namespace")
159
+ # Namespace import: import * as React from "react"
160
+ React = Import("*", "react")
161
161
 
162
162
  # Side-effect import: import "./styles.css"
163
- Import("", "./styles.css", kind="side_effect")
163
+ Import("./styles.css", side_effect=True)
164
164
 
165
165
  # Type-only import: import type { Props } from "./types"
166
166
  Props = Import("Props", "./types", is_type=True)
@@ -170,12 +170,12 @@ class Import(Expr):
170
170
  # Button("Click me", disabled=True) -> <Button_1 disabled={true}>Click me</Button_1>
171
171
 
172
172
  # Local file imports (relative or absolute paths)
173
- Import("", "./styles.css", kind="side_effect") # Local CSS
174
- utils = Import("utils", "./utils", kind="namespace") # Local JS (resolves extension)
175
- config = Import("config", "/absolute/path/config", kind="default") # Absolute path
173
+ Import("./styles.css", side_effect=True) # Local CSS
174
+ utils = Import("*", "./utils") # Local JS namespace (resolves extension)
175
+ config = Import("/absolute/path/config") # Absolute path default import
176
176
 
177
177
  # Lazy import (generates factory for code-splitting)
178
- Chart = Import("Chart", "./Chart", kind="default", lazy=True)
178
+ Chart = Import("./Chart", lazy=True)
179
179
  # Generates: const Chart_1 = () => import("./Chart")
180
180
  """
181
181
 
@@ -194,15 +194,35 @@ class Import(Expr):
194
194
  def __init__(
195
195
  self,
196
196
  name: str,
197
- src: str,
197
+ src: str | None = None,
198
198
  *,
199
- kind: ImportKind | None = None,
199
+ side_effect: bool = False,
200
200
  is_type: bool = False,
201
201
  lazy: bool = False,
202
202
  version: str | None = None,
203
203
  before: tuple[str, ...] | list[str] = (),
204
204
  _caller_depth: int = 2,
205
205
  ) -> None:
206
+ if src is None:
207
+ if name == "*":
208
+ raise TypeError("Import('*') requires a source")
209
+ src = name
210
+ if side_effect:
211
+ name = ""
212
+ kind: ImportKind = "side_effect"
213
+ else:
214
+ kind = "default"
215
+ else:
216
+ if side_effect:
217
+ raise TypeError("side_effect imports cannot specify a name")
218
+ if name == "*":
219
+ name = src
220
+ kind = "namespace"
221
+ else:
222
+ if not name:
223
+ raise TypeError("Import(name, src) requires a non-empty name")
224
+ kind = "named"
225
+
206
226
  # Validate: lazy imports cannot be type-only
207
227
  if lazy and is_type:
208
228
  raise TranspileError("Import cannot be both lazy and type-only")
@@ -220,10 +240,6 @@ class Import(Expr):
220
240
  asset = register_local_asset(resolved)
221
241
  import_src = str(resolved)
222
242
 
223
- # Default kind to "named" if not specified
224
- if kind is None:
225
- kind = "named"
226
-
227
243
  self.name = name
228
244
  self.src = import_src
229
245
  self.kind = kind
@@ -160,8 +160,9 @@ class JsModule(Expr):
160
160
  if self.src is None:
161
161
  return Identifier(self.name)
162
162
 
163
- import_kind = "default" if self.kind == "default" else "named"
164
- return Import(self.name, self.src, kind=import_kind)
163
+ if self.kind == "default":
164
+ return Import(self.src)
165
+ return Import("*", self.src)
165
166
 
166
167
  def get_value(self, name: str) -> Member | Class | Jsx | Identifier | Import:
167
168
  """Get a member of this module as an expression.
@@ -1,74 +0,0 @@
1
- # Placeholders for the WIP JS compilation feature
2
- # NOTE: This module is deprecated. Use pulse.transpiler instead.
3
-
4
- from collections.abc import Callable
5
- from typing import Generic, TypeVar, TypeVarTuple
6
-
7
- from pulse.transpiler.imports import Import
8
-
9
- Args = TypeVarTuple("Args")
10
- R = TypeVar("R")
11
-
12
-
13
- class JsFunction(Generic[*Args, R]):
14
- "A transpiled JS function (deprecated - use pulse.transpiler.function.JsFunction)"
15
-
16
- name: str
17
- hint: Callable[[*Args], R]
18
-
19
- def __init__(
20
- self,
21
- name: str,
22
- hint: Callable[[*Args], R],
23
- ) -> None:
24
- self.name = name
25
- self.hint = hint
26
-
27
- def __call__(self, *args: *Args) -> R: ...
28
-
29
-
30
- class ExternalJsFunction(Generic[*Args, R]):
31
- "An imported JS function (deprecated - use pulse.transpiler.imports.Import)"
32
-
33
- import_: Import
34
- hint: Callable[[*Args], R]
35
- _prop: str | None
36
-
37
- def __init__(
38
- self,
39
- name: str,
40
- src: str,
41
- *,
42
- prop: str | None = None,
43
- is_default: bool,
44
- hint: Callable[[*Args], R],
45
- ) -> None:
46
- kind = "default" if is_default else "named"
47
- self.import_ = Import(name, src, kind=kind)
48
- self._prop = prop
49
- self.hint = hint
50
-
51
- @property
52
- def name(self) -> str:
53
- return self.import_.name
54
-
55
- @property
56
- def src(self) -> str:
57
- return self.import_.src
58
-
59
- @property
60
- def is_default(self) -> bool:
61
- return self.import_.is_default
62
-
63
- @property
64
- def prop(self) -> str | None:
65
- return self._prop
66
-
67
- @property
68
- def expr(self) -> str:
69
- base = self.import_.js_name
70
- if self._prop:
71
- return f"{base}.{self._prop}"
72
- return base
73
-
74
- def __call__(self, *args: *Args) -> R: ...