فهرست منبع

do things in a slightly more solid way

Khaleel Al-Adhami 1 روز پیش
والد
کامیت
6f6a35413a
4فایلهای تغییر یافته به همراه99 افزوده شده و 43 حذف شده
  1. 61 21
      reflex/app.py
  2. 18 2
      reflex/compiler/utils.py
  3. 3 2
      reflex/plugins/base.py
  4. 17 18
      reflex/plugins/tailwind_v3.py

+ 61 - 21
reflex/app.py

@@ -1166,6 +1166,7 @@ class App(MiddlewareMixin, LifespanMixin):
 
         Raises:
             ReflexRuntimeError: When any page uses state, but no rx.State subclass is defined.
+            FileNotFoundError: When a plugin requires a file that does not exist.
         """
         from reflex.utils.exceptions import ReflexRuntimeError
 
@@ -1373,17 +1374,6 @@ class App(MiddlewareMixin, LifespanMixin):
                     ),
                 )
 
-        for plugin in config.plugins:
-            for static_file_path, content in plugin.get_static_assets():
-                resulting_path = (
-                    Path.cwd() / prerequisites.get_web_dir() / static_file_path
-                )
-                resulting_path.parent.mkdir(parents=True, exist_ok=True)
-                if isinstance(content, str):
-                    resulting_path.write_text(content)
-                else:
-                    resulting_path.write_bytes(content)
-
         executor = ExecutorType.get_executor_from_environment()
 
         for route, component in zip(self._pages, page_components, strict=True):
@@ -1391,11 +1381,17 @@ class App(MiddlewareMixin, LifespanMixin):
 
         ExecutorSafeFunctions.STATE = self._state
 
+        modify_files_tasks: list[tuple[str, str, Callable[[str], str]]] = []
+
         with console.timing("Compile to Javascript"), executor as executor:
-            result_futures: list[concurrent.futures.Future[tuple[str, str] | None]] = []
+            result_futures: list[
+                concurrent.futures.Future[
+                    list[tuple[str, str]] | tuple[str, str] | None
+                ]
+            ] = []
 
             def _submit_work(
-                fn: Callable[P, tuple[str, str] | None],
+                fn: Callable[P, list[tuple[str, str]] | tuple[str, str] | None],
                 *args: P.args,
                 **kwargs: P.kwargs,
             ):
@@ -1417,14 +1413,22 @@ class App(MiddlewareMixin, LifespanMixin):
             _submit_work(compile_theme, self.style)
 
             for plugin in config.plugins:
-                plugin.pre_compile(add_task=_submit_work)
+                plugin.pre_compile(
+                    add_save_task=_submit_work,
+                    add_modify_task=(
+                        lambda *args, plugin=plugin: modify_files_tasks.append(
+                            (plugin.__class__.__name__, *args)
+                        )
+                    ),
+                )
 
             # Wait for all compilation tasks to complete.
-            compile_results.extend(
-                result
-                for future in concurrent.futures.as_completed(result_futures)
-                if (result := future.result()) is not None
-            )
+            for future in concurrent.futures.as_completed(result_futures):
+                if (result := future.result()) is not None:
+                    if isinstance(result, list):
+                        compile_results.extend(result)
+                    else:
+                        compile_results.append(result)
 
         app_root = self._app_root(app_wrappers=app_wrappers)
 
@@ -1491,9 +1495,45 @@ class App(MiddlewareMixin, LifespanMixin):
                     # Remove pages that are no longer in the app.
                     p.unlink()
 
+        output_mapping: dict[Path, str] = {}
+        for output_path, code in compile_results:
+            path = Path(output_path).absolute()
+            if path in output_mapping:
+                console.warn(
+                    f"Path {path} has two different outputs. The first one will be used."
+                )
+            else:
+                output_mapping[path] = code
+
+        for plugin in config.plugins:
+            for static_file_path, content in plugin.get_static_assets():
+                path = compiler_utils.resolve_path_of_web_dir(static_file_path)
+                if path in output_mapping:
+                    console.warn(
+                        f"Plugin {plugin.__class__.__name__} is trying to write to {path} but it already exists. The plugin file will be ignored."
+                    )
+                else:
+                    output_mapping[path] = (
+                        content.decode("utf-8")
+                        if isinstance(content, bytes)
+                        else content
+                    )
+
+        for plugin_name, file_path, modify_fn in modify_files_tasks:
+            path = compiler_utils.resolve_path_of_web_dir(file_path)
+            file_content = output_mapping.get(path)
+            if file_content is None:
+                if path.exists():
+                    file_content = path.read_text()
+                else:
+                    raise FileNotFoundError(
+                        f"Plugin {plugin_name} is trying to modify {path} but it does not exist."
+                    )
+            output_mapping[path] = modify_fn(file_content)
+
         with console.timing("Write to Disk"):
-            for output_path, code in compile_results:
-                compiler_utils.write_page(output_path, code)
+            for output_path, code in output_mapping.items():
+                compiler_utils.write_file(output_path, code)
 
     def _write_stateful_pages_marker(self):
         """Write list of routes that create dynamic states for the backend to use later."""

+ 18 - 2
reflex/compiler/utils.py

@@ -500,7 +500,23 @@ def add_meta(
     return page
 
 
-def write_page(path: str | Path, code: str):
+def resolve_path_of_web_dir(path: str | Path) -> Path:
+    """Get the path under the web directory.
+
+    Args:
+        path: The path to get. It can be a relative or absolute path.
+
+    Returns:
+        The path under the web directory.
+    """
+    path = Path(path)
+    web_dir = get_web_dir()
+    if path.is_relative_to(web_dir):
+        return path.absolute()
+    return (web_dir / path).absolute()
+
+
+def write_file(path: str | Path, code: str):
     """Write the given code to the given path.
 
     Args:
@@ -508,7 +524,7 @@ def write_page(path: str | Path, code: str):
         code: The code to write.
     """
     path = Path(path)
-    path_ops.mkdir(path.parent)
+    path.parent.mkdir(parents=True, exist_ok=True)
     if path.exists() and path.read_text(encoding="utf-8") == code:
         return
     path.write_text(code, encoding="utf-8")

+ 3 - 2
reflex/plugins/base.py

@@ -19,7 +19,7 @@ class AddTaskProtcol(Protocol):
 
     def __call__(
         self,
-        task: Callable[P, tuple[str, str] | None],
+        task: Callable[P, list[tuple[str, str]] | tuple[str, str] | None],
         /,
         *args: P.args,
         **kwargs: P.kwargs,
@@ -36,7 +36,8 @@ class AddTaskProtcol(Protocol):
 class PreCompileContext(CommonContext):
     """Context for pre-compile hooks."""
 
-    add_task: AddTaskProtcol
+    add_save_task: AddTaskProtcol
+    add_modify_task: Callable[[str, Callable[[str], str]], None]
 
 
 class Plugin:

+ 17 - 18
reflex/plugins/tailwind_v3.py

@@ -3,6 +3,7 @@
 from pathlib import Path
 from types import SimpleNamespace
 
+from reflex.constants.base import Dirs
 from reflex.plugins.base import Plugin as PluginBase
 from reflex.utils.decorator import once
 
@@ -37,7 +38,7 @@ def tailwind_config_js_template():
     """
     from reflex.compiler.templates import from_string
 
-    source = """
+    source = r"""
 {# Helper macro to render JS objects and arrays #}
 {% macro render_js(val, indent=2, level=0) -%}
 {%- set space = ' ' * (indent * level) -%}
@@ -153,23 +154,21 @@ def _index_of_element_that_startswith(lines: list[str], prefix: str) -> int | No
     )
 
 
-def add_tailwind_to_postcss_config():
-    """Add tailwind to the postcss config."""
-    from reflex.constants import Dirs
-    from reflex.utils.prerequisites import get_web_dir
+def add_tailwind_to_postcss_config(postcss_file_content: str) -> str:
+    """Add tailwind to the postcss config.
 
-    postcss_file = get_web_dir() / Dirs.POSTCSS_JS
-    if not postcss_file.exists():
-        print(  # noqa: T201
-            f"Could not find {Dirs.POSTCSS_JS}. "
-            "Please make sure the file exists and is valid."
-        )
-        return
+    Args:
+        postcss_file_content: The content of the postcss config file.
+
+    Returns:
+        The modified postcss config file content.
+    """
+    from reflex.constants import Dirs
 
-    postcss_file_lines = postcss_file.read_text().splitlines()
+    postcss_file_lines = postcss_file_content.splitlines()
 
     if _index_of_element_that_startswith(postcss_file_lines, "tailwindcss") is not None:
-        return
+        return postcss_file_content
 
     line_with_postcss_plugins = _index_of_element_that_startswith(
         postcss_file_lines, "plugins"
@@ -179,7 +178,7 @@ def add_tailwind_to_postcss_config():
             f"Could not find line with 'plugins' in {Dirs.POSTCSS_JS}. "
             "Please make sure the file exists and is valid."
         )
-        return
+        return postcss_file_content
 
     postcss_import_line = _index_of_element_that_startswith(
         postcss_file_lines, '"postcss-import"'
@@ -188,7 +187,7 @@ def add_tailwind_to_postcss_config():
         (postcss_import_line or line_with_postcss_plugins) + 1, "tailwindcss: {},"
     )
 
-    return str(postcss_file), "\n".join(postcss_file_lines)
+    return "\n".join(postcss_file_lines)
 
 
 class Plugin(PluginBase):
@@ -244,5 +243,5 @@ class Plugin(PluginBase):
         config = get_config().tailwind or {}
 
         config["content"] = config.get("content", Constants.CONTENT)
-        context["add_task"](compile_tailwind, config)
-        context["add_task"](add_tailwind_to_postcss_config)
+        context["add_save_task"](compile_tailwind, config)
+        context["add_modify_task"](Dirs.POSTCSS_JS, add_tailwind_to_postcss_config)