pulse-framework 0.1.51__py3-none-any.whl → 0.1.52__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 (84) hide show
  1. pulse/__init__.py +542 -562
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +0 -14
  4. pulse/cli/cmd.py +96 -80
  5. pulse/cli/dependencies.py +10 -41
  6. pulse/cli/folder_lock.py +3 -3
  7. pulse/cli/helpers.py +40 -67
  8. pulse/cli/logging.py +102 -0
  9. pulse/cli/packages.py +16 -0
  10. pulse/cli/processes.py +40 -23
  11. pulse/codegen/codegen.py +70 -35
  12. pulse/codegen/js.py +2 -4
  13. pulse/codegen/templates/route.py +94 -146
  14. pulse/component.py +115 -0
  15. pulse/components/for_.py +1 -1
  16. pulse/components/if_.py +1 -1
  17. pulse/components/react_router.py +16 -22
  18. pulse/{html → dom}/events.py +1 -1
  19. pulse/{html → dom}/props.py +6 -6
  20. pulse/{html → dom}/tags.py +11 -11
  21. pulse/dom/tags.pyi +480 -0
  22. pulse/form.py +7 -6
  23. pulse/hooks/init.py +1 -13
  24. pulse/js/__init__.py +37 -41
  25. pulse/js/__init__.pyi +22 -2
  26. pulse/js/_types.py +5 -3
  27. pulse/js/array.py +121 -38
  28. pulse/js/console.py +9 -9
  29. pulse/js/date.py +22 -19
  30. pulse/js/document.py +8 -4
  31. pulse/js/error.py +12 -14
  32. pulse/js/json.py +4 -3
  33. pulse/js/map.py +17 -7
  34. pulse/js/math.py +2 -2
  35. pulse/js/navigator.py +4 -4
  36. pulse/js/number.py +8 -8
  37. pulse/js/object.py +9 -13
  38. pulse/js/promise.py +25 -9
  39. pulse/js/regexp.py +6 -6
  40. pulse/js/set.py +20 -8
  41. pulse/js/string.py +7 -7
  42. pulse/js/weakmap.py +6 -6
  43. pulse/js/weakset.py +6 -6
  44. pulse/js/window.py +17 -14
  45. pulse/messages.py +1 -4
  46. pulse/react_component.py +3 -1001
  47. pulse/render_session.py +74 -66
  48. pulse/renderer.py +311 -238
  49. pulse/routing.py +1 -10
  50. pulse/transpiler/__init__.py +84 -114
  51. pulse/transpiler/builtins.py +661 -343
  52. pulse/transpiler/errors.py +78 -2
  53. pulse/transpiler/function.py +463 -133
  54. pulse/transpiler/id.py +18 -0
  55. pulse/transpiler/imports.py +230 -325
  56. pulse/transpiler/js_module.py +218 -209
  57. pulse/transpiler/modules/__init__.py +16 -13
  58. pulse/transpiler/modules/asyncio.py +45 -26
  59. pulse/transpiler/modules/json.py +12 -8
  60. pulse/transpiler/modules/math.py +161 -216
  61. pulse/transpiler/modules/pulse/__init__.py +5 -0
  62. pulse/transpiler/modules/pulse/tags.py +231 -0
  63. pulse/transpiler/modules/typing.py +33 -28
  64. pulse/transpiler/nodes.py +1607 -923
  65. pulse/transpiler/py_module.py +118 -95
  66. pulse/transpiler/react_component.py +51 -0
  67. pulse/transpiler/transpiler.py +593 -437
  68. pulse/transpiler/vdom.py +255 -0
  69. {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/METADATA +1 -1
  70. pulse_framework-0.1.52.dist-info/RECORD +120 -0
  71. pulse/html/tags.pyi +0 -470
  72. pulse/transpiler/constants.py +0 -110
  73. pulse/transpiler/context.py +0 -26
  74. pulse/transpiler/ids.py +0 -16
  75. pulse/transpiler/modules/re.py +0 -466
  76. pulse/transpiler/modules/tags.py +0 -268
  77. pulse/transpiler/utils.py +0 -4
  78. pulse/vdom.py +0 -599
  79. pulse_framework-0.1.51.dist-info/RECORD +0 -119
  80. /pulse/{html → dom}/__init__.py +0 -0
  81. /pulse/{html → dom}/elements.py +0 -0
  82. /pulse/{html → dom}/svg.py +0 -0
  83. {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
  84. {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
@@ -1,414 +1,319 @@
1
- """Unified JS import system for javascript_v2."""
1
+ """Import with auto-registration for transpiler."""
2
+
3
+ from __future__ import annotations
2
4
 
3
5
  import inspect
4
- from collections.abc import Callable, Sequence
6
+ from dataclasses import dataclass
5
7
  from pathlib import Path
6
8
  from typing import (
7
- ClassVar,
9
+ Any,
10
+ ParamSpec,
8
11
  TypeAlias,
9
12
  TypeVar,
10
- TypeVarTuple,
11
- overload,
12
13
  override,
13
14
  )
15
+ from typing import Literal as Lit
14
16
 
15
- from pulse.transpiler.context import is_interpreted_mode
16
- from pulse.transpiler.ids import generate_id
17
- from pulse.transpiler.nodes import JSExpr
17
+ from pulse.cli.packages import pick_more_specific
18
+ from pulse.transpiler.id import next_id
19
+ from pulse.transpiler.nodes import Call, Expr, to_js_identifier
20
+ from pulse.transpiler.vdom import VDOMNode
18
21
 
19
- T = TypeVar("T")
20
- Args = TypeVarTuple("Args")
21
- R = TypeVar("R")
22
+ _P = ParamSpec("_P")
23
+ _R = TypeVar("_R")
22
24
 
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"] = {}
25
+ ImportKind: TypeAlias = Lit["named", "default", "namespace", "side_effect"]
28
26
 
27
+ # JS-like extensions to try when resolving imports without extension (ESM convention)
28
+ _JS_EXTENSIONS = (".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts")
29
29
 
30
- def _caller_file(depth: int = 2) -> Path:
31
- """Get the file path of the caller.
32
30
 
33
- Args:
34
- depth: How many frames to go back (2 = caller of the function that calls this)
35
- """
31
+ def caller_file(depth: int = 2) -> Path:
32
+ """Get the file path of the caller."""
36
33
  frame = inspect.currentframe()
37
34
  try:
38
- # Walk up the call stack
39
35
  for _ in range(depth):
40
36
  if frame is None:
41
- raise RuntimeError("Cannot determine caller frame")
37
+ raise RuntimeError("Could not determine caller file")
42
38
  frame = frame.f_back
43
39
  if frame is None:
44
- raise RuntimeError("Cannot determine caller frame")
45
- return Path(frame.f_code.co_filename).resolve()
40
+ raise RuntimeError("Could not determine caller file")
41
+ return Path(frame.f_code.co_filename)
46
42
  finally:
47
43
  del frame
48
44
 
49
45
 
50
- def _is_local_css_path(path: str) -> bool:
51
- """Check if a CSS path refers to a local file vs a package import.
46
+ def is_relative_path(path: str) -> bool:
47
+ """Check if path is a relative import (starts with ./ or ../)."""
48
+ return path.startswith("./") or path.startswith("../")
49
+
50
+
51
+ def is_absolute_path(path: str) -> bool:
52
+ """Check if path is an absolute filesystem path."""
53
+ return path.startswith("/")
54
+
55
+
56
+ def is_local_path(path: str) -> bool:
57
+ """Check if path is a local file path (relative or absolute)."""
58
+ return is_relative_path(path) or is_absolute_path(path)
59
+
60
+
61
+ def resolve_js_file(base_path: Path) -> Path | None:
62
+ """Resolve a JS-like import path to an actual file.
63
+
64
+ Follows ESM resolution order:
65
+ 1. Exact path (if has extension)
66
+ 2. Try JS extensions: .ts, .tsx, .js, .jsx, .mjs, .mts
67
+ 3. Try /index with each extension
68
+
69
+ Returns None if no file is found.
70
+ """
71
+ # If path already has an extension that exists, use it
72
+ if base_path.suffix and base_path.exists():
73
+ return base_path
74
+
75
+ # If no extension, try JS-like extensions
76
+ if not base_path.suffix:
77
+ for ext in _JS_EXTENSIONS:
78
+ candidate = base_path.with_suffix(ext)
79
+ if candidate.exists():
80
+ return candidate
81
+
82
+ # Try /index with each extension
83
+ for ext in _JS_EXTENSIONS:
84
+ candidate = base_path / f"index{ext}"
85
+ if candidate.exists():
86
+ return candidate
87
+
88
+ return None
89
+
52
90
 
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
91
+ def resolve_local_path(path: str, caller: Path | None = None) -> Path | None:
92
+ """Resolve a local import path to an actual file.
58
93
 
59
- Package imports:
60
- - Start with '@' (e.g., '@mantine/core/styles.css')
61
- - Bare specifiers (e.g., 'some-package/styles.css')
94
+ For relative paths, resolves relative to caller.
95
+ For absolute paths, uses the path directly.
96
+
97
+ For paths without extensions, tries JS-like resolution.
98
+ Falls back to the raw resolved path if the file doesn't exist
99
+ (might be a generated file or future file).
100
+
101
+ Returns None only for non-local paths or relative paths without a caller.
62
102
  """
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.
103
+ if is_relative_path(path):
104
+ if caller is None:
105
+ return None
106
+ base_path = (caller.parent / Path(path)).resolve()
107
+ elif is_absolute_path(path):
108
+ base_path = Path(path).resolve()
109
+ else:
110
+ return None
111
+
112
+ # If the path has an extension, return it (even if it doesn't exist)
113
+ if base_path.suffix:
114
+ return base_path
115
+
116
+ # Try JS-like resolution for existing files
117
+ resolved = resolve_js_file(base_path)
118
+ if resolved is not None:
119
+ return resolved
120
+
121
+ # Fallback: return the base path even if the file doesn't exist
122
+ return base_path
123
+
124
+
125
+ # Registry key depends on kind:
126
+ # - named: (name, src, "named")
127
+ # - default/namespace/side_effect: ("", src, kind) - only one per src
128
+ _ImportKey: TypeAlias = tuple[str, str, str]
129
+ _IMPORT_REGISTRY: dict[_ImportKey, "Import"] = {}
130
+
131
+
132
+ def get_registered_imports() -> list["Import"]:
133
+ """Get all registered imports."""
134
+ return list(_IMPORT_REGISTRY.values())
135
+
136
+
137
+ def clear_import_registry() -> None:
138
+ """Clear the import registry."""
139
+ _IMPORT_REGISTRY.clear()
140
+
141
+
142
+ @dataclass(slots=True, init=False)
143
+ class Import(Expr):
144
+ """JS import that auto-registers and dedupes.
145
+
146
+ An Expr that emits as its unique identifier (e.g., useState_1).
147
+ Overrides transpile_call for JSX component behavior and transpile_getattr for
148
+ member access.
93
149
 
94
150
  Examples:
95
- # Named import: import { foo } from "./module"
96
- foo = Import("foo", "./module")
151
+ # Named import: import { useState } from "react"
152
+ useState = Import("useState", "react")
97
153
 
98
154
  # Default import: import React from "react"
99
- React = Import("React", "react", is_default=True)
155
+ React = Import("React", "react", kind="default")
100
156
 
101
- # Type-only import: import type { Foo } from "./types"
102
- Foo = Import("Foo", "./types", is_type_only=True)
157
+ # Namespace import: import * as utils from "./utils"
158
+ utils = Import("utils", "./utils", kind="namespace")
103
159
 
104
160
  # Side-effect import: import "./styles.css"
105
- Import.side_effect("./styles.css")
161
+ Import("", "./styles.css", kind="side_effect")
106
162
 
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
- """
163
+ # Type-only import: import type { Props } from "./types"
164
+ Props = Import("Props", "./types", is_type=True)
111
165
 
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
- )
166
+ # JSX component import - wrap in Jsx() to create elements
167
+ Button = Jsx(Import("Button", "@mantine/core"))
168
+ # Button("Click me", disabled=True) -> <Button_1 disabled={true}>Click me</Button_1>
122
169
 
123
- is_primary: ClassVar[bool] = True
170
+ # Local file imports (relative or absolute paths)
171
+ Import("", "./styles.css", kind="side_effect") # Local CSS
172
+ utils = Import("utils", "./utils", kind="namespace") # Local JS (resolves extension)
173
+ config = Import("config", "/absolute/path/config", kind="default") # Absolute path
174
+ """
124
175
 
125
176
  name: str
126
177
  src: str
127
- is_default: bool
128
- is_namespace: bool
129
- is_type_only: bool
178
+ kind: ImportKind
179
+ is_type: bool
130
180
  before: tuple[str, ...]
131
181
  id: str
132
- source_path: Path | None # For local CSS files that need to be copied
182
+ version: str | None = None
183
+ source_path: Path | None = (
184
+ None # Resolved local file path (for copying during codegen)
185
+ )
133
186
 
134
187
  def __init__(
135
188
  self,
136
189
  name: str,
137
190
  src: str,
138
191
  *,
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,
192
+ kind: ImportKind | None = None,
193
+ is_type: bool = False,
194
+ version: str | None = None,
195
+ before: tuple[str, ...] | list[str] = (),
196
+ _caller_depth: int = 2,
144
197
  ) -> None:
198
+ # Auto-resolve local paths (relative or absolute) to actual files
199
+ source_path: Path | None = None
200
+ import_src = src
201
+
202
+ if is_local_path(src):
203
+ # Resolve to actual file (handles JS extension resolution)
204
+ caller = caller_file(depth=_caller_depth) if is_relative_path(src) else None
205
+ resolved = resolve_local_path(src, caller)
206
+ if resolved is not None:
207
+ source_path = resolved
208
+ import_src = str(resolved)
209
+
210
+ # Default kind to "named" if not specified
211
+ if kind is None:
212
+ kind = "named"
213
+
145
214
  self.name = name
146
- self.src = src
147
- self.is_default = is_default
148
- self.is_namespace = is_namespace
215
+ self.src = import_src
216
+ self.kind = kind
217
+ self.version = version
149
218
  self.source_path = source_path
150
219
 
151
- before_tuple = tuple(before)
220
+ before_tuple = tuple(before) if isinstance(before, list) else before
152
221
 
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
- )
222
+ # Dedupe key: for named imports use (name, src, "named")
223
+ # For default/namespace/side_effect, only one per src: ("", src, kind)
224
+ if kind == "named":
225
+ key: _ImportKey = (name, import_src, "named")
226
+ else:
227
+ key = ("", import_src, kind)
159
228
 
160
- if key in _REGISTRY:
161
- existing = _REGISTRY[key]
229
+ if key in _IMPORT_REGISTRY:
230
+ existing = _IMPORT_REGISTRY[key]
162
231
 
163
232
  # Merge: type-only + regular = regular
164
- if existing.is_type_only and not is_type_only:
165
- existing.is_type_only = False
233
+ if existing.is_type and not is_type:
234
+ existing.is_type = False
166
235
 
167
236
  # Merge: union of before constraints
168
237
  if before_tuple:
169
238
  merged_before = set(existing.before) | set(before_tuple)
170
239
  existing.before = tuple(sorted(merged_before))
171
240
 
241
+ # Merge: version
242
+ existing.version = pick_more_specific(existing.version, version)
243
+
172
244
  # Reuse ID and merged values
173
245
  self.id = existing.id
174
- self.is_type_only = existing.is_type_only
246
+ self.is_type = existing.is_type
175
247
  self.before = existing.before
248
+ self.version = existing.version
176
249
  else:
177
250
  # New import
178
- self.id = generate_id()
179
- self.is_type_only = is_type_only
251
+ self.id = next_id()
252
+ self.is_type = is_type
180
253
  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
254
+ _IMPORT_REGISTRY[key] = self
251
255
 
252
256
  @property
253
257
  def js_name(self) -> str:
254
258
  """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
- Automatically set to True if path ends with ".module.css".
297
- relative: If True, resolve path relative to the caller's file.
298
- before: List of import sources that should come after this import.
299
-
300
- Examples:
301
- # Side-effect CSS import (global styles)
302
- CssImport("@mantine/core/styles.css")
303
-
304
- # CSS module for class access (module=True auto-detected from .module.css)
305
- styles = CssImport("./styles.module.css", relative=True)
306
- styles.container # Returns JSMember for 'container' class
307
-
308
- # Local CSS file (will be copied during codegen)
309
- CssImport("./global.css", relative=True)
310
- """
311
-
312
- def __init__(
313
- self,
314
- path: str,
315
- *,
316
- module: bool = False,
317
- relative: bool = False,
318
- before: Sequence[str] = (),
319
- ) -> None:
320
- # Auto-detect CSS modules based on filename
321
- if path.endswith(".module.css"):
322
- module = True
323
-
324
- source_path: Path | None = None
325
- import_src = path
326
-
327
- if relative:
328
- # Resolve relative to caller's file (depth=2: _caller_file -> __init__ -> caller)
329
- caller = _caller_file(depth=2)
330
- source_path = (caller.parent / Path(path)).resolve()
331
- if not source_path.exists():
332
- kind = "CSS module" if module else "CSS file"
333
- raise FileNotFoundError(
334
- f"{kind} '{path}' not found relative to {caller.parent}"
335
- )
336
- import_src = str(source_path)
337
- elif _is_local_css_path(path):
338
- # Absolute local path
339
- source_path = Path(path).resolve()
340
- if not source_path.exists():
341
- kind = "CSS module" if module else "CSS file"
342
- raise FileNotFoundError(f"{kind} '{path}' not found")
343
- import_src = str(source_path)
344
-
345
- # CSS modules are default imports with "css" name prefix
346
- # Side-effect imports have empty name and is_default=False
347
- name = "css" if module else ""
348
- is_default = module
349
-
350
- super().__init__(
351
- name,
352
- import_src,
353
- is_default=is_default,
354
- is_type_only=False,
355
- before=before,
356
- source_path=source_path,
357
- )
259
+ return f"{to_js_identifier(self.name)}_{self.id}"
358
260
 
359
261
  @property
360
262
  def is_local(self) -> bool:
361
- """True if this is a local CSS file (not an npm package)."""
263
+ """Check if this is a local file import (has resolved source_path)."""
362
264
  return self.source_path is not None
363
265
 
266
+ # Convenience properties for kind checks
364
267
  @property
365
- def generated_filename(self) -> str | None:
366
- """Generated filename for local CSS files, or None for package imports."""
367
- if self.source_path is None:
368
- return None
369
- if self.source_path.name.endswith(".module.css"):
370
- return f"css_{self.id}.module.css"
371
- return f"css_{self.id}.css"
372
-
373
-
374
- def registered_imports() -> list[Import]:
375
- """Get all registered imports."""
376
- return list(_REGISTRY.values())
377
-
378
-
379
- def clear_import_registry() -> None:
380
- """Clear the import registry."""
381
- _REGISTRY.clear()
268
+ def is_default(self) -> bool:
269
+ return self.kind == "default"
382
270
 
271
+ @property
272
+ def is_namespace(self) -> bool:
273
+ return self.kind == "namespace"
383
274
 
384
- # =============================================================================
385
- # js_import decorator/function
386
- # =============================================================================
275
+ @property
276
+ def is_side_effect(self) -> bool:
277
+ return self.kind == "side_effect"
387
278
 
279
+ def asset_filename(self) -> str:
280
+ """Get the filename for this import when copied to assets folder.
388
281
 
389
- @overload
390
- def import_js(
391
- name: str, src: str, *, is_default: bool = False
392
- ) -> Callable[[Callable[[*Args], R]], Callable[[*Args], R]]:
393
- "Import a JS function for use in `@javascript` functions"
394
- ...
282
+ Uses the import ID for uniqueness and preserves the original extension.
283
+ For JS-like files, uses the resolved extension.
284
+ """
285
+ if self.source_path is None:
286
+ raise ValueError("Cannot get asset filename for non-local import")
287
+ stem = self.source_path.stem
288
+ suffix = self.source_path.suffix
289
+ return f"{stem}_{self.id}{suffix}"
395
290
 
291
+ # -------------------------------------------------------------------------
292
+ # Expr.emit: outputs the unique identifier
293
+ # -------------------------------------------------------------------------
396
294
 
397
- @overload
398
- def import_js(name: str, src: str, type_: type[T], *, is_default: bool = False) -> T:
399
- "Import a JS value for use in `@javascript` functions"
400
- ...
295
+ @override
296
+ def emit(self, out: list[str]) -> None:
297
+ """Emit this import as its unique JS identifier."""
298
+ out.append(self.js_name)
401
299
 
300
+ @override
301
+ def render(self) -> VDOMNode:
302
+ """Render as a registry reference."""
303
+ return {"t": "ref", "key": self.id}
402
304
 
403
- def import_js(
404
- name: str, src: str, type_: type[T] | None = None, *, is_default: bool = False
405
- ) -> T | Callable[[Callable[[*Args], R]], Callable[[*Args], R]]:
406
- imp = Import.default(name, src) if is_default else Import.named(name, src)
305
+ # -------------------------------------------------------------------------
306
+ # Python dunder methods: allow natural syntax in @javascript functions
307
+ # -------------------------------------------------------------------------
407
308
 
408
- if type_ is not None:
409
- return imp # pyright: ignore[reportReturnType]
309
+ # Overloads for __call__:
310
+ # 1. Decorator usage: @Import(...) def fn(...) -> returns fn's type
311
+ # 2. Expression usage: Import(...)(...) -> returns Call
410
312
 
411
- def decorator(fn: Callable[[*Args], R]) -> Callable[[*Args], R]:
412
- return imp # pyright: ignore[reportReturnType]
313
+ @override
314
+ def __call__(self, *args: Any, **kwargs: Any) -> "Call":
315
+ """Allow calling Import objects in Python code.
413
316
 
414
- return decorator
317
+ Returns a Call expression.
318
+ """
319
+ return Expr.__call__(self, *args, **kwargs)