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
This commit is contained in:
Masen Furer 2025-02-07 14:59:22 -08:00 committed by GitHub
parent c17cda3e95
commit 70920a64be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 94 additions and 21 deletions

View File

@ -99,7 +99,15 @@ from reflex.state import (
_substate_key,
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.imports import ImportVar
@ -991,9 +999,10 @@ class App(MiddlewareMixin, LifespanMixin):
should_compile = self._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)
self._add_optional_endpoints()
@ -1019,10 +1028,11 @@ class App(MiddlewareMixin, LifespanMixin):
+ 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)
self._add_optional_endpoints()
@ -1057,13 +1067,13 @@ class App(MiddlewareMixin, LifespanMixin):
custom_components |= component._get_all_custom_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.
if code_uses_state_contexts(stateful_components_code) and self._state is None:
@ -1086,6 +1096,17 @@ class App(MiddlewareMixin, LifespanMixin):
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.
# Fallback to ThreadPoolExecutor as something that will always work.
executor = None
@ -1138,9 +1159,10 @@ class App(MiddlewareMixin, LifespanMixin):
_submit_work(compiler.remove_tailwind_from_postcss)
# 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)
@ -1175,7 +1197,8 @@ class App(MiddlewareMixin, LifespanMixin):
progress.stop()
# 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
transpile_packages = [
@ -1201,8 +1224,9 @@ class App(MiddlewareMixin, LifespanMixin):
# Remove pages that are no longer in the app.
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
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:

View File

@ -2,8 +2,10 @@
from __future__ import annotations
import contextlib
import inspect
import shutil
import time
from pathlib import Path
from types import FrameType
@ -317,3 +319,20 @@ def status(*args, **kwargs):
A new status.
"""
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]")

View File

@ -260,3 +260,33 @@ def find_replace(directory: str | Path, find: str, replace: str):
text = filepath.read_text(encoding="utf-8")
text = re.sub(find, replace, text)
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)