pulse-framework 0.1.44__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 (80) hide show
  1. pulse/__init__.py +10 -24
  2. pulse/app.py +3 -25
  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/queries/client.py +7 -7
  40. pulse/queries/effect.py +16 -0
  41. pulse/queries/infinite_query.py +138 -29
  42. pulse/queries/mutation.py +1 -15
  43. pulse/queries/protocol.py +136 -0
  44. pulse/queries/query.py +610 -174
  45. pulse/queries/store.py +11 -14
  46. pulse/react_component.py +167 -14
  47. pulse/reactive.py +19 -1
  48. pulse/reactive_extensions.py +5 -5
  49. pulse/render_session.py +185 -59
  50. pulse/renderer.py +80 -158
  51. pulse/routing.py +1 -18
  52. pulse/transpiler/__init__.py +131 -0
  53. pulse/transpiler/builtins.py +731 -0
  54. pulse/transpiler/constants.py +110 -0
  55. pulse/transpiler/context.py +26 -0
  56. pulse/transpiler/errors.py +2 -0
  57. pulse/transpiler/function.py +250 -0
  58. pulse/transpiler/ids.py +16 -0
  59. pulse/transpiler/imports.py +409 -0
  60. pulse/transpiler/js_module.py +274 -0
  61. pulse/transpiler/modules/__init__.py +30 -0
  62. pulse/transpiler/modules/asyncio.py +38 -0
  63. pulse/transpiler/modules/json.py +20 -0
  64. pulse/transpiler/modules/math.py +320 -0
  65. pulse/transpiler/modules/re.py +466 -0
  66. pulse/transpiler/modules/tags.py +268 -0
  67. pulse/transpiler/modules/typing.py +59 -0
  68. pulse/transpiler/nodes.py +1216 -0
  69. pulse/transpiler/py_module.py +119 -0
  70. pulse/transpiler/transpiler.py +938 -0
  71. pulse/transpiler/utils.py +4 -0
  72. pulse/types/event_handler.py +3 -2
  73. pulse/vdom.py +212 -13
  74. {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/METADATA +1 -1
  75. pulse_framework-0.1.47.dist-info/RECORD +119 -0
  76. pulse/codegen/imports.py +0 -204
  77. pulse/css.py +0 -155
  78. pulse_framework-0.1.44.dist-info/RECORD +0 -79
  79. {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/WHEEL +0 -0
  80. {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,409 @@
1
+ """Unified JS import system for javascript_v2."""
2
+
3
+ import inspect
4
+ from collections.abc import Callable, Sequence
5
+ from pathlib import Path
6
+ from typing import (
7
+ ClassVar,
8
+ TypeAlias,
9
+ TypeVar,
10
+ TypeVarTuple,
11
+ overload,
12
+ override,
13
+ )
14
+
15
+ from pulse.transpiler.context import is_interpreted_mode
16
+ from pulse.transpiler.ids import generate_id
17
+ from pulse.transpiler.nodes import JSExpr
18
+
19
+ T = TypeVar("T")
20
+ Args = TypeVarTuple("Args")
21
+ R = TypeVar("R")
22
+
23
+ # Registry key: (name, src, is_default)
24
+ # - Named imports: (name, src, False)
25
+ # - Default/side-effect imports: ("", src, is_default) - dedupe by src only
26
+ _ImportKey: TypeAlias = tuple[str, str, bool]
27
+ _REGISTRY: dict[_ImportKey, "Import"] = {}
28
+
29
+
30
+ def _caller_file(depth: int = 2) -> Path:
31
+ """Get the file path of the caller.
32
+
33
+ Args:
34
+ depth: How many frames to go back (2 = caller of the function that calls this)
35
+ """
36
+ frame = inspect.currentframe()
37
+ try:
38
+ # Walk up the call stack
39
+ for _ in range(depth):
40
+ if frame is None:
41
+ raise RuntimeError("Cannot determine caller frame")
42
+ frame = frame.f_back
43
+ if frame is None:
44
+ raise RuntimeError("Cannot determine caller frame")
45
+ return Path(frame.f_code.co_filename).resolve()
46
+ finally:
47
+ del frame
48
+
49
+
50
+ def _is_local_css_path(path: str) -> bool:
51
+ """Check if a CSS path refers to a local file vs a package import.
52
+
53
+ Local paths:
54
+ - Start with './' or '../' (relative)
55
+ - Start with '/' (absolute)
56
+ - Don't start with '@' (scoped npm package)
57
+ - Don't look like a bare module specifier
58
+
59
+ Package imports:
60
+ - Start with '@' (e.g., '@mantine/core/styles.css')
61
+ - Bare specifiers (e.g., 'some-package/styles.css')
62
+ """
63
+ # Relative paths are local
64
+ if path.startswith("./") or path.startswith("../"):
65
+ return True
66
+ # Absolute paths are local
67
+ if path.startswith("/"):
68
+ return True
69
+ # Scoped packages
70
+ if path.startswith("@"):
71
+ return False
72
+ # If it contains no slashes, it could be a local file in current dir
73
+ # But without './' prefix, treat bare names as package imports for safety
74
+ # Exception: if it ends with .css and doesn't look like a package
75
+ if "/" not in path and path.endswith(".css"):
76
+ # Ambiguous - could be local or package. Require explicit ./
77
+ return False
78
+ # Everything else (bare specifiers with paths) are package imports
79
+ return False
80
+
81
+
82
+ class Import(JSExpr):
83
+ """Universal import descriptor.
84
+
85
+ Import identity is determined by (name, src, is_default):
86
+ - Named imports: unique by (name, src)
87
+ - Default imports: unique by src (name is the local binding)
88
+ - Side-effect imports: unique by src (name is empty)
89
+
90
+ When two Import objects reference the same underlying import, they share
91
+ the same ID, allowing multiple Import objects to target different properties
92
+ of the same import.
93
+
94
+ Examples:
95
+ # Named import: import { foo } from "./module"
96
+ foo = Import("foo", "./module")
97
+
98
+ # Default import: import React from "react"
99
+ React = Import("React", "react", is_default=True)
100
+
101
+ # Type-only import: import type { Foo } from "./types"
102
+ Foo = Import("Foo", "./types", is_type_only=True)
103
+
104
+ # Side-effect import: import "./styles.css"
105
+ Import.side_effect("./styles.css")
106
+
107
+ # CSS module import with class access
108
+ styles = Import.css_module("./styles.module.css", relative=True)
109
+ styles.container # Returns JSMember for 'container' class
110
+ """
111
+
112
+ __slots__ = ( # pyright: ignore[reportUnannotatedClassAttribute]
113
+ "name",
114
+ "src",
115
+ "is_default",
116
+ "is_namespace",
117
+ "is_type_only",
118
+ "before",
119
+ "id",
120
+ "source_path",
121
+ )
122
+
123
+ is_primary: ClassVar[bool] = True
124
+
125
+ name: str
126
+ src: str
127
+ is_default: bool
128
+ is_namespace: bool
129
+ is_type_only: bool
130
+ before: tuple[str, ...]
131
+ id: str
132
+ source_path: Path | None # For local CSS files that need to be copied
133
+
134
+ def __init__(
135
+ self,
136
+ name: str,
137
+ src: str,
138
+ *,
139
+ is_default: bool = False,
140
+ is_namespace: bool = False,
141
+ is_type_only: bool = False,
142
+ before: Sequence[str] = (),
143
+ source_path: Path | None = None,
144
+ ) -> None:
145
+ self.name = name
146
+ self.src = src
147
+ self.is_default = is_default
148
+ self.is_namespace = is_namespace
149
+ self.source_path = source_path
150
+
151
+ before_tuple = tuple(before)
152
+
153
+ # Dedupe key: for default/side-effect/namespace imports, only src matters
154
+ key: _ImportKey = (
155
+ ("", src, is_default or is_namespace)
156
+ if (is_default or is_namespace or name == "")
157
+ else (name, src, False)
158
+ )
159
+
160
+ if key in _REGISTRY:
161
+ existing = _REGISTRY[key]
162
+
163
+ # Merge: type-only + regular = regular
164
+ if existing.is_type_only and not is_type_only:
165
+ existing.is_type_only = False
166
+
167
+ # Merge: union of before constraints
168
+ if before_tuple:
169
+ merged_before = set(existing.before) | set(before_tuple)
170
+ existing.before = tuple(sorted(merged_before))
171
+
172
+ # Reuse ID and merged values
173
+ self.id = existing.id
174
+ self.is_type_only = existing.is_type_only
175
+ self.before = existing.before
176
+ else:
177
+ # New import
178
+ self.id = generate_id()
179
+ self.is_type_only = is_type_only
180
+ self.before = before_tuple
181
+ _REGISTRY[key] = self
182
+
183
+ @classmethod
184
+ def default(
185
+ cls,
186
+ name: str,
187
+ src: str,
188
+ *,
189
+ is_type_only: bool = False,
190
+ before: Sequence[str] = (),
191
+ ) -> "Import":
192
+ """Create a default import."""
193
+ return cls(
194
+ name,
195
+ src,
196
+ is_default=True,
197
+ is_type_only=is_type_only,
198
+ before=before,
199
+ )
200
+
201
+ @classmethod
202
+ def named(
203
+ cls,
204
+ name: str,
205
+ src: str,
206
+ *,
207
+ is_type_only: bool = False,
208
+ before: Sequence[str] = (),
209
+ ) -> "Import":
210
+ """Create a named import."""
211
+ return cls(
212
+ name,
213
+ src,
214
+ is_default=False,
215
+ is_type_only=is_type_only,
216
+ before=before,
217
+ )
218
+
219
+ @classmethod
220
+ def namespace(
221
+ cls,
222
+ name: str,
223
+ src: str,
224
+ *,
225
+ before: Sequence[str] = (),
226
+ ) -> "Import":
227
+ """Create a namespace import: import * as name from src."""
228
+ return cls(
229
+ name,
230
+ src,
231
+ is_namespace=True,
232
+ before=before,
233
+ )
234
+
235
+ @classmethod
236
+ def type_(
237
+ cls,
238
+ name: str,
239
+ src: str,
240
+ *,
241
+ is_default: bool = False,
242
+ before: Sequence[str] = (),
243
+ ) -> "Import":
244
+ """Create a type-only import."""
245
+ return cls(name, src, is_default=is_default, is_type_only=True, before=before)
246
+
247
+ @property
248
+ def is_side_effect(self) -> bool:
249
+ """True if this is a side-effect only import (no bindings)."""
250
+ return self.name == "" and not self.is_default
251
+
252
+ @property
253
+ def js_name(self) -> str:
254
+ """Unique JS identifier for this import."""
255
+ return f"{self.name}_{self.id}"
256
+
257
+ @override
258
+ def emit(self) -> str:
259
+ """Emit JS code for this import.
260
+
261
+ In normal mode: returns the unique JS name (e.g., "Button_1")
262
+ In interpreted mode: returns a get_object call (e.g., "get_object('Button_1')")
263
+ """
264
+ base = self.js_name
265
+ if is_interpreted_mode():
266
+ return f"get_object('{base}')"
267
+ return base
268
+
269
+ @override
270
+ def __repr__(self) -> str:
271
+ parts = [f"name={self.name!r}", f"src={self.src!r}"]
272
+ if self.is_default:
273
+ parts.append("is_default=True")
274
+ if self.is_namespace:
275
+ parts.append("is_namespace=True")
276
+ if self.is_type_only:
277
+ parts.append("is_type_only=True")
278
+ if self.source_path:
279
+ parts.append(f"source_path={self.source_path!r}")
280
+ return f"Import({', '.join(parts)})"
281
+
282
+
283
+ class CssImport(Import):
284
+ """Import for CSS files (both local files and npm packages).
285
+
286
+ For local files, tracks the source path and provides a generated filename
287
+ for the output directory. For npm packages, acts as a regular import.
288
+
289
+ Args:
290
+ path: Path to CSS file. Can be:
291
+ - Package path (e.g., "@mantine/core/styles.css")
292
+ - Relative path with relative=True (e.g., "./global.css")
293
+ - Absolute path (e.g., "/path/to/styles.css")
294
+ module: If True, import as a CSS module (default export for class access).
295
+ If False, import for side effects only (global styles).
296
+ relative: If True, resolve path relative to the caller's file.
297
+ before: List of import sources that should come after this import.
298
+
299
+ Examples:
300
+ # Side-effect CSS import (global styles)
301
+ CssImport("@mantine/core/styles.css")
302
+
303
+ # CSS module for class access
304
+ styles = CssImport("./styles.module.css", module=True, relative=True)
305
+ styles.container # Returns JSMember for 'container' class
306
+
307
+ # Local CSS file (will be copied during codegen)
308
+ CssImport("./global.css", relative=True)
309
+ """
310
+
311
+ def __init__(
312
+ self,
313
+ path: str,
314
+ *,
315
+ module: bool = False,
316
+ relative: bool = False,
317
+ before: Sequence[str] = (),
318
+ ) -> None:
319
+ source_path: Path | None = None
320
+ import_src = path
321
+
322
+ if relative:
323
+ # Resolve relative to caller's file (depth=2: _caller_file -> __init__ -> caller)
324
+ caller = _caller_file(depth=2)
325
+ source_path = (caller.parent / Path(path)).resolve()
326
+ if not source_path.exists():
327
+ kind = "CSS module" if module else "CSS file"
328
+ raise FileNotFoundError(
329
+ f"{kind} '{path}' not found relative to {caller.parent}"
330
+ )
331
+ import_src = str(source_path)
332
+ elif _is_local_css_path(path):
333
+ # Absolute local path
334
+ source_path = Path(path).resolve()
335
+ if not source_path.exists():
336
+ kind = "CSS module" if module else "CSS file"
337
+ raise FileNotFoundError(f"{kind} '{path}' not found")
338
+ import_src = str(source_path)
339
+
340
+ # CSS modules are default imports with "css" name prefix
341
+ # Side-effect imports have empty name and is_default=False
342
+ name = "css" if module else ""
343
+ is_default = module
344
+
345
+ super().__init__(
346
+ name,
347
+ import_src,
348
+ is_default=is_default,
349
+ is_type_only=False,
350
+ before=before,
351
+ source_path=source_path,
352
+ )
353
+
354
+ @property
355
+ def is_local(self) -> bool:
356
+ """True if this is a local CSS file (not an npm package)."""
357
+ return self.source_path is not None
358
+
359
+ @property
360
+ def generated_filename(self) -> str | None:
361
+ """Generated filename for local CSS files, or None for package imports."""
362
+ if self.source_path is None:
363
+ return None
364
+ if self.source_path.name.endswith(".module.css"):
365
+ return f"css_{self.id}.module.css"
366
+ return f"css_{self.id}.css"
367
+
368
+
369
+ def registered_imports() -> list[Import]:
370
+ """Get all registered imports."""
371
+ return list(_REGISTRY.values())
372
+
373
+
374
+ def clear_import_registry() -> None:
375
+ """Clear the import registry."""
376
+ _REGISTRY.clear()
377
+
378
+
379
+ # =============================================================================
380
+ # js_import decorator/function
381
+ # =============================================================================
382
+
383
+
384
+ @overload
385
+ def import_js(
386
+ name: str, src: str, *, is_default: bool = False
387
+ ) -> Callable[[Callable[[*Args], R]], Callable[[*Args], R]]:
388
+ "Import a JS function for use in `@javascript` functions"
389
+ ...
390
+
391
+
392
+ @overload
393
+ def import_js(name: str, src: str, type_: type[T], *, is_default: bool = False) -> T:
394
+ "Import a JS value for use in `@javascript` functions"
395
+ ...
396
+
397
+
398
+ def import_js(
399
+ name: str, src: str, type_: type[T] | None = None, *, is_default: bool = False
400
+ ) -> T | Callable[[Callable[[*Args], R]], Callable[[*Args], R]]:
401
+ imp = Import.default(name, src) if is_default else Import.named(name, src)
402
+
403
+ if type_ is not None:
404
+ return imp # pyright: ignore[reportReturnType]
405
+
406
+ def decorator(fn: Callable[[*Args], R]) -> Callable[[*Args], R]:
407
+ return imp # pyright: ignore[reportReturnType]
408
+
409
+ return decorator
@@ -0,0 +1,274 @@
1
+ """Core infrastructure for JavaScript module bindings.
2
+
3
+ JS modules are Python modules that map to JavaScript modules/builtins.
4
+ Registration is done by calling register_js_module() from within the module itself.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import inspect
10
+ import sys
11
+ from collections.abc import Callable
12
+ from dataclasses import dataclass, field
13
+ from types import FunctionType, ModuleType
14
+ from typing import Any, ClassVar, Literal, TypeVar, override
15
+
16
+ from pulse.transpiler.errors import JSCompilationError
17
+ from pulse.transpiler.imports import Import
18
+ from pulse.transpiler.nodes import JSExpr, JSIdentifier, JSMember, JSNew
19
+
20
+ # Track functions marked as constructors (by id, since we delete them)
21
+ CONSTRUCTORS: set[int] = set()
22
+
23
+ F = TypeVar("F", bound=Callable[..., object])
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class JsModule:
28
+ """Configuration for a JavaScript module binding.
29
+
30
+ Attributes:
31
+ name: The JavaScript identifier (e.g., "Math", "lodash")
32
+ src: Import source path. None for builtins.
33
+ kind: Import kind - "default" or "namespace"
34
+ values: How attribute access is expressed:
35
+ - "member": Access as property (e.g., React.useState)
36
+ - "named_import": Each attribute is a named import (e.g., import { useState } from "react")
37
+ constructors: Set of names that are constructors (emit with 'new')
38
+ global_scope: If True, members are registered in global scope. Module imports are disallowed.
39
+ """
40
+
41
+ name: str
42
+ src: str | None = None
43
+ kind: Literal["default", "namespace"] = "namespace"
44
+ values: Literal["member", "named_import"] = "named_import"
45
+ constructors: frozenset[str] = field(default_factory=frozenset)
46
+ global_scope: bool = False
47
+
48
+ @property
49
+ def is_builtin(self) -> bool:
50
+ return self.src is None
51
+
52
+ def to_js_expr(self) -> JSIdentifier | Import:
53
+ """Generate the appropriate JSExpr for this module.
54
+
55
+ Returns JSIdentifier for builtins, Import for external modules.
56
+
57
+ Raises JSCompilationError if global_scope=True (module imports are disallowed).
58
+ """
59
+ if self.global_scope:
60
+ module_name_lower = self.name.lower()
61
+ msg = (
62
+ f"Cannot import module '{self.name}' directly. "
63
+ + f"Use 'from pulse.js.{module_name_lower} import ...' instead."
64
+ )
65
+ raise JSCompilationError(msg)
66
+
67
+ if self.src is None:
68
+ return JSIdentifier(self.name)
69
+
70
+ if self.kind == "default":
71
+ return Import.default(self.name, self.src)
72
+ return Import.namespace(self.name, self.src)
73
+
74
+ def get_value(self, name: str) -> JSMember | JSConstructor | JSIdentifier | Import:
75
+ """Get a member of this module as a JS expression.
76
+
77
+ For global_scope modules: returns JSIdentifier(name) directly (e.g., Set -> Set)
78
+ For builtins: returns JSMember (e.g., Math.sin), or JSIdentifier if name
79
+ matches the module name (e.g., Set -> Set, not Set.Set)
80
+ For external modules with "member" style: returns JSMember (e.g., React.useState)
81
+ For external modules with "named_import" style: returns a named Import (e.g., import { useState } from "react")
82
+
83
+ If name is in constructors, wraps the result in JSConstructor.
84
+ """
85
+ expr: JSMember | JSIdentifier | Import
86
+ if self.global_scope:
87
+ # Global scope: members are just identifiers, not members of a module
88
+ expr = JSIdentifier(name)
89
+ elif self.src is None:
90
+ # Builtins: use identifier when name matches module name (Set.Set -> Set)
91
+ if name == self.name:
92
+ expr = JSIdentifier(name)
93
+ else:
94
+ expr = JSMember(JSIdentifier(self.name), name)
95
+ elif self.values == "named_import":
96
+ expr = Import.named(name, self.src)
97
+ else:
98
+ expr = JSMember(self.to_js_expr(), name)
99
+
100
+ if name in self.constructors:
101
+ return JSConstructor(expr)
102
+ return expr
103
+
104
+
105
+ @dataclass
106
+ class JSConstructor(JSExpr):
107
+ """Wrapper that emits constructor calls with 'new' keyword.
108
+
109
+ When this expression is called, it produces JSNew instead of JSCall.
110
+ """
111
+
112
+ ctor: JSExpr
113
+ is_primary: ClassVar[bool] = True
114
+
115
+ @override
116
+ def emit(self) -> str:
117
+ return self.ctor.emit()
118
+
119
+ @override
120
+ def emit_call(self, args: list[Any], kwargs: dict[str, Any]) -> JSExpr:
121
+ if kwargs:
122
+ raise JSCompilationError(
123
+ "Keyword arguments not supported in constructor call"
124
+ )
125
+ return JSNew(self.ctor, [JSExpr.of(a) for a in args])
126
+
127
+
128
+ # Registry: Python module -> JsModule config
129
+ JS_MODULES: dict[ModuleType, JsModule] = {}
130
+
131
+
132
+ def register_js_module(
133
+ *,
134
+ name: str,
135
+ src: str | None = None,
136
+ kind: Literal["default", "namespace"] = "namespace",
137
+ values: Literal["member", "named_import"] = "named_import",
138
+ global_scope: bool = False,
139
+ ) -> None:
140
+ """Register the calling Python module as a JavaScript module binding.
141
+
142
+ Must be called from within the module being registered. The module is
143
+ automatically detected from the call stack.
144
+
145
+ This function:
146
+ 1. Creates a JsModule config and adds it to JS_MODULES
147
+ 2. Sets up __getattr__ on the module for dynamic attribute access
148
+
149
+ Args:
150
+ name: The JavaScript identifier (e.g., "Math")
151
+ src: Import source path. None for builtins.
152
+ kind: Import kind - "default" or "namespace"
153
+ values: How attribute access works:
154
+ - "member": Access as property (e.g., Math.sin, React.useState)
155
+ - "named_import": Each attribute is a named import (e.g., import { useState } from "react")
156
+ global_scope: If True, members are registered in global scope. Module imports
157
+ (e.g., `import pulse.js.set as Set`) are disallowed. Members are transpiled
158
+ as direct identifiers (e.g., `Set` -> `Set`, not `Set.Set`).
159
+
160
+ Example (inside pulse/js/math.py):
161
+ register_js_module(name="Math") # builtin
162
+
163
+ Example (inside pulse/js/react.py):
164
+ register_js_module(name="React", src="react") # namespace + named imports (default)
165
+
166
+ Example (inside pulse/js/set.py):
167
+ register_js_module(name="Set", global_scope=True) # global scope builtin
168
+ """
169
+ # Get the calling module from the stack frame
170
+ frame = inspect.currentframe()
171
+ assert frame is not None and frame.f_back is not None
172
+ module_name = frame.f_back.f_globals["__name__"]
173
+ module = sys.modules[module_name]
174
+
175
+ # Collect constructor names before deleting functions/classes
176
+ # Classes are automatically treated as constructors
177
+ # Only items defined in this module are considered (not imported ones)
178
+ # Track locally defined names so __getattr__ can distinguish them from imported ones
179
+
180
+ def is_defined_in_module(obj: Any) -> bool:
181
+ """Check if an object is defined in the current module (not imported)."""
182
+ return hasattr(obj, "__module__") and obj.__module__ == module_name
183
+
184
+ ctor_names: set[str] = set()
185
+ locally_defined_names: set[str] = set()
186
+
187
+ for attr_name in list(vars(module)):
188
+ # Skip special module attributes
189
+ if attr_name in (
190
+ "__name__",
191
+ "__file__",
192
+ "__doc__",
193
+ "__package__",
194
+ "__path__",
195
+ "__cached__",
196
+ "__loader__",
197
+ "__spec__",
198
+ ):
199
+ continue
200
+ attr = getattr(module, attr_name)
201
+ # Check if this is an imported name (has __module__ that doesn't match)
202
+ is_imported = hasattr(attr, "__module__") and attr.__module__ != module_name
203
+ if isinstance(attr, FunctionType):
204
+ # Functions without __module__ are assumed to be locally defined (stub functions)
205
+ if not is_imported and (
206
+ not hasattr(attr, "__module__") or is_defined_in_module(attr)
207
+ ):
208
+ # Only track non-underscore-prefixed names for exports
209
+ if not attr_name.startswith("_"):
210
+ locally_defined_names.add(attr_name)
211
+ if id(attr) in CONSTRUCTORS:
212
+ ctor_names.add(attr_name)
213
+ delattr(module, attr_name)
214
+ else:
215
+ # Delete imported functions (including underscore-prefixed)
216
+ delattr(module, attr_name)
217
+ elif inspect.isclass(attr):
218
+ # Only consider classes defined in this module (not imported)
219
+ if not is_imported and is_defined_in_module(attr):
220
+ # Only track non-underscore-prefixed names for exports
221
+ if not attr_name.startswith("_"):
222
+ locally_defined_names.add(attr_name)
223
+ ctor_names.add(attr_name)
224
+ delattr(module, attr_name)
225
+ else:
226
+ # Delete imported classes (including underscore-prefixed)
227
+ delattr(module, attr_name)
228
+ elif is_imported:
229
+ # Delete all imported objects (TypeVar, Generic, etc.) including underscore-prefixed
230
+ delattr(module, attr_name)
231
+ else:
232
+ # For objects without __module__, assume they're locally defined
233
+ # (constants, etc.) - but constants are usually just annotations
234
+ # so they won't be in vars(module) anyway
235
+ if not attr_name.startswith("_"):
236
+ locally_defined_names.add(attr_name)
237
+ delattr(module, attr_name)
238
+
239
+ # Collect constant names from annotations (constants are just type annotations)
240
+ # before clearing them
241
+ constant_names: set[str] = set()
242
+ if hasattr(module, "__annotations__"):
243
+ for ann_name in module.__annotations__:
244
+ if not ann_name.startswith("_") and ann_name not in locally_defined_names:
245
+ constant_names.add(ann_name)
246
+ # Clear annotations (they're just for IDE hints, not runtime values)
247
+ module.__annotations__.clear()
248
+
249
+ js_module = JsModule(
250
+ name=name,
251
+ src=src,
252
+ kind=kind,
253
+ values=values,
254
+ constructors=frozenset(ctor_names),
255
+ global_scope=global_scope,
256
+ )
257
+ JS_MODULES[module] = js_module
258
+
259
+ # Include constants in locally_defined_names so they're accessible via __getattr__
260
+ locally_defined_names.update(constant_names)
261
+
262
+ # Set up __getattr__ - all attribute access now goes through here
263
+ # Capture locally_defined_names in closure
264
+ _defined_names = locally_defined_names
265
+
266
+ def __getattr__(name: str) -> JSMember | JSConstructor | JSIdentifier | Import:
267
+ if name.startswith("_"):
268
+ raise AttributeError(name)
269
+ # Only allow access to locally defined names (functions, classes, constants)
270
+ if name not in _defined_names:
271
+ raise AttributeError(name)
272
+ return js_module.get_value(name)
273
+
274
+ module.__getattr__ = __getattr__ # type: ignore[method-assign]
@@ -0,0 +1,30 @@
1
+ """Central registration point for all module transpilers.
2
+
3
+ This module registers all built-in Python and JavaScript modules for transpilation.
4
+ Import this module to ensure all transpilers are available.
5
+ """
6
+
7
+ import asyncio as asyncio_builtin
8
+ import json as json_builtin
9
+ import math as math_builtin
10
+ import re as re_builtin
11
+ import typing as typing_builtin
12
+
13
+ import pulse.html.tags as pulse_tags
14
+ from pulse.transpiler.modules.asyncio import PyAsyncio
15
+ from pulse.transpiler.modules.json import PyJson
16
+ from pulse.transpiler.modules.math import PyMath
17
+ from pulse.transpiler.modules.re import PyRe
18
+ from pulse.transpiler.modules.tags import PyTags
19
+ from pulse.transpiler.modules.typing import PyTyping
20
+ from pulse.transpiler.py_module import register_module
21
+
22
+ # Register built-in Python modules
23
+ register_module(asyncio_builtin, PyAsyncio)
24
+ register_module(json_builtin, PyJson)
25
+ register_module(math_builtin, PyMath)
26
+ register_module(re_builtin, PyRe)
27
+ register_module(typing_builtin, PyTyping)
28
+
29
+ # Register Pulse HTML tags for JSX transpilation
30
+ register_module(pulse_tags, PyTags)