pulse-framework 0.1.50__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.
- pulse/__init__.py +542 -562
- pulse/_examples.py +29 -0
- pulse/app.py +0 -14
- pulse/cli/cmd.py +96 -80
- pulse/cli/dependencies.py +10 -41
- pulse/cli/folder_lock.py +3 -3
- pulse/cli/helpers.py +40 -67
- pulse/cli/logging.py +102 -0
- pulse/cli/packages.py +16 -0
- pulse/cli/processes.py +40 -23
- pulse/codegen/codegen.py +70 -35
- pulse/codegen/js.py +2 -4
- pulse/codegen/templates/route.py +94 -146
- pulse/component.py +115 -0
- pulse/components/for_.py +1 -1
- pulse/components/if_.py +1 -1
- pulse/components/react_router.py +16 -22
- pulse/{html → dom}/events.py +1 -1
- pulse/{html → dom}/props.py +6 -6
- pulse/{html → dom}/tags.py +11 -11
- pulse/dom/tags.pyi +480 -0
- pulse/form.py +7 -6
- pulse/hooks/init.py +1 -13
- pulse/js/__init__.py +37 -41
- pulse/js/__init__.pyi +22 -2
- pulse/js/_types.py +5 -3
- pulse/js/array.py +121 -38
- pulse/js/console.py +9 -9
- pulse/js/date.py +22 -19
- pulse/js/document.py +8 -4
- pulse/js/error.py +12 -14
- pulse/js/json.py +4 -3
- pulse/js/map.py +17 -7
- pulse/js/math.py +2 -2
- pulse/js/navigator.py +4 -4
- pulse/js/number.py +8 -8
- pulse/js/object.py +9 -13
- pulse/js/promise.py +25 -9
- pulse/js/regexp.py +6 -6
- pulse/js/set.py +20 -8
- pulse/js/string.py +7 -7
- pulse/js/weakmap.py +6 -6
- pulse/js/weakset.py +6 -6
- pulse/js/window.py +17 -14
- pulse/messages.py +1 -4
- pulse/react_component.py +3 -999
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- pulse/serializer.py +11 -1
- pulse/transpiler/__init__.py +84 -114
- pulse/transpiler/builtins.py +661 -343
- pulse/transpiler/errors.py +78 -2
- pulse/transpiler/function.py +463 -133
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +230 -325
- pulse/transpiler/js_module.py +218 -209
- pulse/transpiler/modules/__init__.py +16 -13
- pulse/transpiler/modules/asyncio.py +45 -26
- pulse/transpiler/modules/json.py +12 -8
- pulse/transpiler/modules/math.py +161 -216
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +231 -0
- pulse/transpiler/modules/typing.py +33 -28
- pulse/transpiler/nodes.py +1607 -923
- pulse/transpiler/py_module.py +118 -95
- pulse/transpiler/react_component.py +51 -0
- pulse/transpiler/transpiler.py +593 -437
- pulse/transpiler/vdom.py +255 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/METADATA +1 -1
- pulse_framework-0.1.52.dist-info/RECORD +120 -0
- pulse/html/tags.pyi +0 -470
- pulse/transpiler/constants.py +0 -110
- pulse/transpiler/context.py +0 -26
- pulse/transpiler/ids.py +0 -16
- pulse/transpiler/modules/re.py +0 -466
- pulse/transpiler/modules/tags.py +0 -268
- pulse/transpiler/utils.py +0 -4
- pulse/vdom.py +0 -667
- pulse_framework-0.1.50.dist-info/RECORD +0 -119
- /pulse/{html → dom}/__init__.py +0 -0
- /pulse/{html → dom}/elements.py +0 -0
- /pulse/{html → dom}/svg.py +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.50.dist-info → pulse_framework-0.1.52.dist-info}/entry_points.txt +0 -0
pulse/transpiler/imports.py
CHANGED
|
@@ -1,414 +1,319 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Import with auto-registration for transpiler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
import inspect
|
|
4
|
-
from
|
|
6
|
+
from dataclasses import dataclass
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from typing import (
|
|
7
|
-
|
|
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.
|
|
16
|
-
from pulse.transpiler.
|
|
17
|
-
from pulse.transpiler.nodes import
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
R = TypeVar("R")
|
|
22
|
+
_P = ParamSpec("_P")
|
|
23
|
+
_R = TypeVar("_R")
|
|
22
24
|
|
|
23
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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("
|
|
37
|
+
raise RuntimeError("Could not determine caller file")
|
|
42
38
|
frame = frame.f_back
|
|
43
39
|
if frame is None:
|
|
44
|
-
raise RuntimeError("
|
|
45
|
-
return Path(frame.f_code.co_filename)
|
|
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
|
|
51
|
-
"""Check if
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# If
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 {
|
|
96
|
-
|
|
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",
|
|
155
|
+
React = Import("React", "react", kind="default")
|
|
100
156
|
|
|
101
|
-
#
|
|
102
|
-
|
|
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
|
|
161
|
+
Import("", "./styles.css", kind="side_effect")
|
|
106
162
|
|
|
107
|
-
#
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
"
|
|
114
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
is_type_only: bool
|
|
178
|
+
kind: ImportKind
|
|
179
|
+
is_type: bool
|
|
130
180
|
before: tuple[str, ...]
|
|
131
181
|
id: str
|
|
132
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
before:
|
|
143
|
-
|
|
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 =
|
|
147
|
-
self.
|
|
148
|
-
self.
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
161
|
-
existing =
|
|
229
|
+
if key in _IMPORT_REGISTRY:
|
|
230
|
+
existing = _IMPORT_REGISTRY[key]
|
|
162
231
|
|
|
163
232
|
# Merge: type-only + regular = regular
|
|
164
|
-
if existing.
|
|
165
|
-
existing.
|
|
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.
|
|
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 =
|
|
179
|
-
self.
|
|
251
|
+
self.id = next_id()
|
|
252
|
+
self.is_type = is_type
|
|
180
253
|
self.before = before_tuple
|
|
181
|
-
|
|
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
|
-
"""
|
|
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
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
@
|
|
398
|
-
def
|
|
399
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
412
|
-
|
|
313
|
+
@override
|
|
314
|
+
def __call__(self, *args: Any, **kwargs: Any) -> "Call":
|
|
315
|
+
"""Allow calling Import objects in Python code.
|
|
413
316
|
|
|
414
|
-
|
|
317
|
+
Returns a Call expression.
|
|
318
|
+
"""
|
|
319
|
+
return Expr.__call__(self, *args, **kwargs)
|