abulvenz преди 11 месеца
родител
ревизия
6c6eaaa55f

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 **/.DS_Store
 **/*.pyc
+assets/external/*
 dist/*
 examples/
 .idea

+ 2 - 0
reflex/constants/base.py

@@ -21,6 +21,8 @@ class Dirs(SimpleNamespace):
     WEB = ".web"
     # The name of the assets directory.
     APP_ASSETS = "assets"
+    # The name of the assets directory for external ressource (a subfolder of APP_ASSETS).
+    EXTERNAL_APP_ASSETS = "external"
     # The name of the utils file.
     UTILS = "utils"
     # The name of the output static directory.

+ 5 - 0
reflex/constants/base.pyi

@@ -15,10 +15,15 @@ from types import SimpleNamespace
 from platformdirs import PlatformDirs
 
 IS_WINDOWS = platform.system() == "Windows"
+IS_WINDOWS_BUN_SUPPORTED_MACHINE = IS_WINDOWS and platform.machine() in [
+    "AMD64",
+    "x86_64",
+]
 
 class Dirs(SimpleNamespace):
     WEB = ".web"
     APP_ASSETS = "assets"
+    EXTERNAL_APP_ASSETS = "external"
     UTILS = "utils"
     STATIC = "_static"
     STATE_PATH = "/".join([UTILS, "state"])

+ 2 - 0
reflex/experimental/__init__.py

@@ -8,6 +8,7 @@ from reflex.components.sonner.toast import toast as toast
 
 from ..utils.console import warn
 from . import hooks as hooks
+from .assets import asset as asset
 from .client_state import ClientStateVar as ClientStateVar
 from .layout import layout as layout
 from .misc import run_in_thread as run_in_thread
@@ -17,6 +18,7 @@ warn(
 )
 
 _x = SimpleNamespace(
+    asset=asset,
     client_state=ClientStateVar.create,
     hooks=hooks,
     layout=layout,

+ 56 - 0
reflex/experimental/assets.py

@@ -0,0 +1,56 @@
+"""Helper functions for adding assets to the app."""
+import inspect
+from pathlib import Path
+from typing import Optional
+
+from reflex import constants
+
+
+def asset(relative_filename: str, subfolder: Optional[str] = None) -> str:
+    """Add an asset to the app.
+    Place the file next to your including python file.
+    Copies the file to the app's external assets directory.
+
+    Example:
+    ```python
+    rx.script(src=rx._x.asset("my_custom_javascript.js"))
+    rx.image(src=rx._x.asset("test_image.png","subfolder"))
+    ```
+
+    Args:
+        relative_filename: The relative filename of the asset.
+        subfolder: The directory to place the asset in.
+
+    Raises:
+        FileNotFoundError: If the file does not exist.
+
+    Returns:
+        The relative URL to the copied asset.
+    """
+    # Determine the file by which the asset is exposed.
+    calling_file = inspect.stack()[1].filename
+    module = inspect.getmodule(inspect.stack()[1][0])
+    assert module is not None
+    caller_module_path = module.__name__.replace(".", "/")
+
+    subfolder = f"{caller_module_path}/{subfolder}" if subfolder else caller_module_path
+
+    src_file = Path(calling_file).parent / relative_filename
+
+    assets = constants.Dirs.APP_ASSETS
+    external = constants.Dirs.EXTERNAL_APP_ASSETS
+
+    if not src_file.exists():
+        raise FileNotFoundError(f"File not found: {src_file}")
+
+    # Create the asset folder in the currently compiling app.
+    asset_folder = Path.cwd() / assets / external / subfolder
+    asset_folder.mkdir(parents=True, exist_ok=True)
+
+    dst_file = asset_folder / relative_filename
+
+    if not dst_file.exists():
+        dst_file.symlink_to(src_file)
+
+    asset_url = f"/{external}/{subfolder}/{relative_filename}"
+    return asset_url

+ 4 - 2
reflex/experimental/client_state.py

@@ -2,7 +2,7 @@
 
 import dataclasses
 import sys
-from typing import Any, Callable, Optional, Type
+from typing import Any, Callable, Optional, Type, Union
 
 from reflex import constants
 from reflex.event import EventChain, EventHandler, EventSpec, call_script
@@ -171,7 +171,9 @@ class ClientStateVar(Var):
             )
         )
 
-    def retrieve(self, callback: EventHandler | Callable | None = None) -> EventSpec:
+    def retrieve(
+        self, callback: Union[EventHandler, Callable, None] = None
+    ) -> EventSpec:
         """Pass the value of the client state variable to a backend EventHandler.
 
         The event handler must `yield` or `return` the EventSpec to trigger the event.

+ 8 - 6
reflex/experimental/hooks.py

@@ -1,10 +1,12 @@
 """Add standard Hooks wrapper for React."""
 
+from typing import Optional, Union
+
 from reflex.utils.imports import ImportVar
 from reflex.vars import Var, VarData
 
 
-def _add_react_import(v: Var | None, tags: str | list):
+def _add_react_import(v: Optional[Var], tags: Union[str, list]):
     if v is None:
         return
 
@@ -16,7 +18,7 @@ def _add_react_import(v: Var | None, tags: str | list):
     )
 
 
-def const(name, value) -> Var | None:
+def const(name, value) -> Optional[Var]:
     """Create a constant Var.
 
     Args:
@@ -31,7 +33,7 @@ def const(name, value) -> Var | None:
     return Var.create(f"const {name} = {value}")
 
 
-def useCallback(func, deps) -> Var | None:
+def useCallback(func, deps) -> Optional[Var]:
     """Create a useCallback hook with a function and dependencies.
 
     Args:
@@ -49,7 +51,7 @@ def useCallback(func, deps) -> Var | None:
     return v
 
 
-def useContext(context) -> Var | None:
+def useContext(context) -> Optional[Var]:
     """Create a useContext hook with a context.
 
     Args:
@@ -63,7 +65,7 @@ def useContext(context) -> Var | None:
     return v
 
 
-def useRef(default) -> Var | None:
+def useRef(default) -> Optional[Var]:
     """Create a useRef hook with a default value.
 
     Args:
@@ -77,7 +79,7 @@ def useRef(default) -> Var | None:
     return v
 
 
-def useState(var_name, default=None) -> Var | None:
+def useState(var_name, default=None) -> Optional[Var]:
     """Create a useState hook with a variable name and setter name.
 
     Args:

+ 1 - 0
tests/experimental/custom_script.js

@@ -0,0 +1 @@
+const test = "inside custom_script.js";

+ 36 - 0
tests/experimental/test_assets.py

@@ -0,0 +1,36 @@
+import shutil
+from pathlib import Path
+
+import pytest
+
+import reflex as rx
+
+
+def test_asset():
+    # Test the asset function.
+
+    # The asset function copies a file to the app's external assets directory.
+    asset = rx._x.asset("custom_script.js", "subfolder")
+    assert asset == "/external/test_assets/subfolder/custom_script.js"
+    result_file = Path(
+        Path.cwd(), "assets/external/test_assets/subfolder/custom_script.js"
+    )
+    assert result_file.exists()
+
+    # Running a second time should not raise an error.
+    asset = rx._x.asset("custom_script.js", "subfolder")
+
+    # Test the asset function without a subfolder.
+    asset = rx._x.asset("custom_script.js")
+    assert asset == "/external/test_assets/custom_script.js"
+    result_file = Path(Path.cwd(), "assets/external/test_assets/custom_script.js")
+    assert result_file.exists()
+
+    # clean up
+    shutil.rmtree(Path.cwd() / "assets/external")
+
+    with pytest.raises(FileNotFoundError):
+        asset = rx._x.asset("non_existent_file.js")
+
+    # Nothing is done to assets when file does not exist.
+    assert not Path(Path.cwd() / "assets/external").exists()