Преглед на файлове

Copy/update assets on compile (#4765)

* Add path_ops.update_directory_tree:

Copy missing and newer files from src to dest

* add console.timing context

Log debug messages with timing for different processes.

* Update assets tree as app._compile step.

If the assets change between hot reload, then update them before reloading (in
case a CSS file was added or something).

* Add timing for other app._compile events

* Only copy assets if assets exist

* Fix docstring for update_directory_tree
Masen Furer преди 3 месеца
родител
ревизия
70920a64be
променени са 3 файла, в които са добавени 94 реда и са изтрити 21 реда
  1. 45 21
      reflex/app.py
  2. 19 0
      reflex/utils/console.py
  3. 30 0
      reflex/utils/path_ops.py

+ 45 - 21
reflex/app.py

@@ -99,7 +99,15 @@ from reflex.state import (
     _substate_key,
     _substate_key,
     code_uses_state_contexts,
     code_uses_state_contexts,
 )
 )
-from reflex.utils import codespaces, console, exceptions, format, prerequisites, types
+from reflex.utils import (
+    codespaces,
+    console,
+    exceptions,
+    format,
+    path_ops,
+    prerequisites,
+    types,
+)
 from reflex.utils.exec import is_prod_mode, is_testing_env
 from reflex.utils.exec import is_prod_mode, is_testing_env
 from reflex.utils.imports import ImportVar
 from reflex.utils.imports import ImportVar
 
 
@@ -991,9 +999,10 @@ class App(MiddlewareMixin, LifespanMixin):
         should_compile = self._should_compile()
         should_compile = self._should_compile()
 
 
         if not should_compile:
         if not should_compile:
-            for route in self._unevaluated_pages:
-                console.debug(f"Evaluating page: {route}")
-                self._compile_page(route, save_page=should_compile)
+            with console.timing("Evaluate Pages (Backend)"):
+                for route in self._unevaluated_pages:
+                    console.debug(f"Evaluating page: {route}")
+                    self._compile_page(route, save_page=should_compile)
 
 
             # Add the optional endpoints (_upload)
             # Add the optional endpoints (_upload)
             self._add_optional_endpoints()
             self._add_optional_endpoints()
@@ -1019,10 +1028,11 @@ class App(MiddlewareMixin, LifespanMixin):
             + adhoc_steps_without_executor,
             + adhoc_steps_without_executor,
         )
         )
 
 
-        for route in self._unevaluated_pages:
-            console.debug(f"Evaluating page: {route}")
-            self._compile_page(route, save_page=should_compile)
-            progress.advance(task)
+        with console.timing("Evaluate Pages (Frontend)"):
+            for route in self._unevaluated_pages:
+                console.debug(f"Evaluating page: {route}")
+                self._compile_page(route, save_page=should_compile)
+                progress.advance(task)
 
 
         # Add the optional endpoints (_upload)
         # Add the optional endpoints (_upload)
         self._add_optional_endpoints()
         self._add_optional_endpoints()
@@ -1057,13 +1067,13 @@ class App(MiddlewareMixin, LifespanMixin):
             custom_components |= component._get_all_custom_components()
             custom_components |= component._get_all_custom_components()
 
 
         # Perform auto-memoization of stateful components.
         # Perform auto-memoization of stateful components.
-        (
-            stateful_components_path,
-            stateful_components_code,
-            page_components,
-        ) = compiler.compile_stateful_components(self._pages.values())
-
-        progress.advance(task)
+        with console.timing("Auto-memoize StatefulComponents"):
+            (
+                stateful_components_path,
+                stateful_components_code,
+                page_components,
+            ) = compiler.compile_stateful_components(self._pages.values())
+            progress.advance(task)
 
 
         # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
         # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
         if code_uses_state_contexts(stateful_components_code) and self._state is None:
         if code_uses_state_contexts(stateful_components_code) and self._state is None:
@@ -1086,6 +1096,17 @@ class App(MiddlewareMixin, LifespanMixin):
 
 
         progress.advance(task)
         progress.advance(task)
 
 
+        # Copy the assets.
+        assets_src = Path.cwd() / constants.Dirs.APP_ASSETS
+        if assets_src.is_dir():
+            with console.timing("Copy assets"):
+                path_ops.update_directory_tree(
+                    src=assets_src,
+                    dest=(
+                        Path.cwd() / prerequisites.get_web_dir() / constants.Dirs.PUBLIC
+                    ),
+                )
+
         # Use a forking process pool, if possible.  Much faster, especially for large sites.
         # Use a forking process pool, if possible.  Much faster, especially for large sites.
         # Fallback to ThreadPoolExecutor as something that will always work.
         # Fallback to ThreadPoolExecutor as something that will always work.
         executor = None
         executor = None
@@ -1138,9 +1159,10 @@ class App(MiddlewareMixin, LifespanMixin):
                 _submit_work(compiler.remove_tailwind_from_postcss)
                 _submit_work(compiler.remove_tailwind_from_postcss)
 
 
             # Wait for all compilation tasks to complete.
             # Wait for all compilation tasks to complete.
-            for future in concurrent.futures.as_completed(result_futures):
-                compile_results.append(future.result())
-                progress.advance(task)
+            with console.timing("Compile to Javascript"):
+                for future in concurrent.futures.as_completed(result_futures):
+                    compile_results.append(future.result())
+                    progress.advance(task)
 
 
         app_root = self._app_root(app_wrappers=app_wrappers)
         app_root = self._app_root(app_wrappers=app_wrappers)
 
 
@@ -1175,7 +1197,8 @@ class App(MiddlewareMixin, LifespanMixin):
         progress.stop()
         progress.stop()
 
 
         # Install frontend packages.
         # Install frontend packages.
-        self._get_frontend_packages(all_imports)
+        with console.timing("Install Frontend Packages"):
+            self._get_frontend_packages(all_imports)
 
 
         # Setup the next.config.js
         # Setup the next.config.js
         transpile_packages = [
         transpile_packages = [
@@ -1201,8 +1224,9 @@ class App(MiddlewareMixin, LifespanMixin):
                     # Remove pages that are no longer in the app.
                     # Remove pages that are no longer in the app.
                     p.unlink()
                     p.unlink()
 
 
-        for output_path, code in compile_results:
-            compiler_utils.write_page(output_path, code)
+        with console.timing("Write to Disk"):
+            for output_path, code in compile_results:
+                compiler_utils.write_page(output_path, code)
 
 
     @contextlib.asynccontextmanager
     @contextlib.asynccontextmanager
     async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
     async def modify_state(self, token: str) -> AsyncIterator[BaseState]:

+ 19 - 0
reflex/utils/console.py

@@ -2,8 +2,10 @@
 
 
 from __future__ import annotations
 from __future__ import annotations
 
 
+import contextlib
 import inspect
 import inspect
 import shutil
 import shutil
+import time
 from pathlib import Path
 from pathlib import Path
 from types import FrameType
 from types import FrameType
 
 
@@ -317,3 +319,20 @@ def status(*args, **kwargs):
         A new status.
         A new status.
     """
     """
     return _console.status(*args, **kwargs)
     return _console.status(*args, **kwargs)
+
+
+@contextlib.contextmanager
+def timing(msg: str):
+    """Create a context manager to time a block of code.
+
+    Args:
+        msg: The message to display.
+
+    Yields:
+        None.
+    """
+    start = time.time()
+    try:
+        yield
+    finally:
+        debug(f"[white]\\[timing] {msg}: {time.time() - start:.2f}s[/white]")

+ 30 - 0
reflex/utils/path_ops.py

@@ -260,3 +260,33 @@ def find_replace(directory: str | Path, find: str, replace: str):
             text = filepath.read_text(encoding="utf-8")
             text = filepath.read_text(encoding="utf-8")
             text = re.sub(find, replace, text)
             text = re.sub(find, replace, text)
             filepath.write_text(text, encoding="utf-8")
             filepath.write_text(text, encoding="utf-8")
+
+
+def update_directory_tree(src: Path, dest: Path):
+    """Recursively copies a directory tree from src to dest.
+    Only copies files if the destination file is missing or modified earlier than the source file.
+
+    Args:
+        src: Source directory
+        dest: Destination directory
+
+    Raises:
+        ValueError: If the source is not a directory
+    """
+    if not src.is_dir():
+        raise ValueError(f"Source {src} is not a directory")
+
+    # Ensure the destination directory exists
+    dest.mkdir(parents=True, exist_ok=True)
+
+    for item in src.iterdir():
+        dest_item = dest / item.name
+
+        if item.is_dir():
+            # Recursively copy subdirectories
+            update_directory_tree(item, dest_item)
+        elif item.is_file() and (
+            not dest_item.exists() or item.stat().st_mtime > dest_item.stat().st_mtime
+        ):
+            # Copy file if it doesn't exist in the destination or is older than the source
+            shutil.copy2(item, dest_item)