diff --git a/.gitignore b/.gitignore index c6acfc0997..a570ed353f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ **/.DS_Store **/*.pyc +assets/external/* dist/* examples/ .idea diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 733859ad4b..94559c3467 100644 --- a/reflex/constants/base.py +++ b/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. diff --git a/reflex/constants/base.pyi b/reflex/constants/base.pyi index 90804a080e..2fbeafd792 100644 --- a/reflex/constants/base.pyi +++ b/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"]) diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index 6972fdfe0d..b4ebc10862 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -8,6 +8,7 @@ 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 @@ ) _x = SimpleNamespace( + asset=asset, client_state=ClientStateVar.create, hooks=hooks, layout=layout, diff --git a/reflex/experimental/assets.py b/reflex/experimental/assets.py new file mode 100644 index 0000000000..7363901541 --- /dev/null +++ b/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 diff --git a/reflex/experimental/client_state.py b/reflex/experimental/client_state.py index 93405b29fb..9282c47214 100644 --- a/reflex/experimental/client_state.py +++ b/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 @@ def set(self) -> 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. diff --git a/reflex/experimental/hooks.py b/reflex/experimental/hooks.py index 9040abd4d6..5706b18d63 100644 --- a/reflex/experimental/hooks.py +++ b/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: diff --git a/tests/experimental/custom_script.js b/tests/experimental/custom_script.js new file mode 100644 index 0000000000..81bae31366 --- /dev/null +++ b/tests/experimental/custom_script.js @@ -0,0 +1 @@ +const test = "inside custom_script.js"; \ No newline at end of file diff --git a/tests/experimental/test_assets.py b/tests/experimental/test_assets.py new file mode 100644 index 0000000000..8037bcc75e --- /dev/null +++ b/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()