Skip to content

Commit 8ef6fb5

Browse files
committed
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
1 parent 4b5c59c commit 8ef6fb5

File tree

3 files changed

+94
-21
lines changed

3 files changed

+94
-21
lines changed

reflex/app.py

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,15 @@
9999
_substate_key,
100100
code_uses_state_contexts,
101101
)
102-
from reflex.utils import codespaces, console, exceptions, format, prerequisites, types
102+
from reflex.utils import (
103+
codespaces,
104+
console,
105+
exceptions,
106+
format,
107+
path_ops,
108+
prerequisites,
109+
types,
110+
)
103111
from reflex.utils.exec import is_prod_mode, is_testing_env
104112
from reflex.utils.imports import ImportVar
105113

@@ -974,9 +982,10 @@ def get_compilation_time() -> str:
974982
should_compile = self._should_compile()
975983

976984
if not should_compile:
977-
for route in self._unevaluated_pages:
978-
console.debug(f"Evaluating page: {route}")
979-
self._compile_page(route, save_page=should_compile)
985+
with console.timing("Evaluate Pages (Backend)"):
986+
for route in self._unevaluated_pages:
987+
console.debug(f"Evaluating page: {route}")
988+
self._compile_page(route, save_page=should_compile)
980989

981990
# Add the optional endpoints (_upload)
982991
self._add_optional_endpoints()
@@ -1002,10 +1011,11 @@ def get_compilation_time() -> str:
10021011
+ adhoc_steps_without_executor,
10031012
)
10041013

1005-
for route in self._unevaluated_pages:
1006-
console.debug(f"Evaluating page: {route}")
1007-
self._compile_page(route, save_page=should_compile)
1008-
progress.advance(task)
1014+
with console.timing("Evaluate Pages (Frontend)"):
1015+
for route in self._unevaluated_pages:
1016+
console.debug(f"Evaluating page: {route}")
1017+
self._compile_page(route, save_page=should_compile)
1018+
progress.advance(task)
10091019

10101020
# Add the optional endpoints (_upload)
10111021
self._add_optional_endpoints()
@@ -1040,13 +1050,13 @@ def get_compilation_time() -> str:
10401050
custom_components |= component._get_all_custom_components()
10411051

10421052
# Perform auto-memoization of stateful components.
1043-
(
1044-
stateful_components_path,
1045-
stateful_components_code,
1046-
page_components,
1047-
) = compiler.compile_stateful_components(self._pages.values())
1048-
1049-
progress.advance(task)
1053+
with console.timing("Auto-memoize StatefulComponents"):
1054+
(
1055+
stateful_components_path,
1056+
stateful_components_code,
1057+
page_components,
1058+
) = compiler.compile_stateful_components(self._pages.values())
1059+
progress.advance(task)
10501060

10511061
# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
10521062
if code_uses_state_contexts(stateful_components_code) and self._state is None:
@@ -1069,6 +1079,17 @@ def get_compilation_time() -> str:
10691079

10701080
progress.advance(task)
10711081

1082+
# Copy the assets.
1083+
assets_src = Path.cwd() / constants.Dirs.APP_ASSETS
1084+
if assets_src.is_dir():
1085+
with console.timing("Copy assets"):
1086+
path_ops.update_directory_tree(
1087+
src=assets_src,
1088+
dest=(
1089+
Path.cwd() / prerequisites.get_web_dir() / constants.Dirs.PUBLIC
1090+
),
1091+
)
1092+
10721093
# Use a forking process pool, if possible. Much faster, especially for large sites.
10731094
# Fallback to ThreadPoolExecutor as something that will always work.
10741095
executor = None
@@ -1121,9 +1142,10 @@ def _submit_work(fn: Callable, *args, **kwargs):
11211142
_submit_work(compiler.remove_tailwind_from_postcss)
11221143

11231144
# Wait for all compilation tasks to complete.
1124-
for future in concurrent.futures.as_completed(result_futures):
1125-
compile_results.append(future.result())
1126-
progress.advance(task)
1145+
with console.timing("Compile to Javascript"):
1146+
for future in concurrent.futures.as_completed(result_futures):
1147+
compile_results.append(future.result())
1148+
progress.advance(task)
11271149

11281150
app_root = self._app_root(app_wrappers=app_wrappers)
11291151

@@ -1158,7 +1180,8 @@ def _submit_work(fn: Callable, *args, **kwargs):
11581180
progress.stop()
11591181

11601182
# Install frontend packages.
1161-
self._get_frontend_packages(all_imports)
1183+
with console.timing("Install Frontend Packages"):
1184+
self._get_frontend_packages(all_imports)
11621185

11631186
# Setup the next.config.js
11641187
transpile_packages = [
@@ -1184,8 +1207,9 @@ def _submit_work(fn: Callable, *args, **kwargs):
11841207
# Remove pages that are no longer in the app.
11851208
p.unlink()
11861209

1187-
for output_path, code in compile_results:
1188-
compiler_utils.write_page(output_path, code)
1210+
with console.timing("Write to Disk"):
1211+
for output_path, code in compile_results:
1212+
compiler_utils.write_page(output_path, code)
11891213

11901214
@contextlib.asynccontextmanager
11911215
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:

reflex/utils/console.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from __future__ import annotations
44

5+
import contextlib
56
import inspect
67
import shutil
8+
import time
79
from pathlib import Path
810
from types import FrameType
911

@@ -317,3 +319,20 @@ def status(*args, **kwargs):
317319
A new status.
318320
"""
319321
return _console.status(*args, **kwargs)
322+
323+
324+
@contextlib.contextmanager
325+
def timing(msg: str):
326+
"""Create a context manager to time a block of code.
327+
328+
Args:
329+
msg: The message to display.
330+
331+
Yields:
332+
None.
333+
"""
334+
start = time.time()
335+
try:
336+
yield
337+
finally:
338+
debug(f"[white]\\[timing] {msg}: {time.time() - start:.2f}s[/white]")

reflex/utils/path_ops.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,33 @@ def find_replace(directory: str | Path, find: str, replace: str):
245245
text = filepath.read_text(encoding="utf-8")
246246
text = re.sub(find, replace, text)
247247
filepath.write_text(text, encoding="utf-8")
248+
249+
250+
def update_directory_tree(src: Path, dest: Path):
251+
"""Recursively copies a directory tree from src to dest.
252+
Only copies files if the destination file is missing or modified earlier than the source file.
253+
254+
Args:
255+
src: Source directory
256+
dest: Destination directory
257+
258+
Raises:
259+
ValueError: If the source is not a directory
260+
"""
261+
if not src.is_dir():
262+
raise ValueError(f"Source {src} is not a directory")
263+
264+
# Ensure the destination directory exists
265+
dest.mkdir(parents=True, exist_ok=True)
266+
267+
for item in src.iterdir():
268+
dest_item = dest / item.name
269+
270+
if item.is_dir():
271+
# Recursively copy subdirectories
272+
update_directory_tree(item, dest_item)
273+
elif item.is_file() and (
274+
not dest_item.exists() or item.stat().st_mtime > dest_item.stat().st_mtime
275+
):
276+
# Copy file if it doesn't exist in the destination or is older than the source
277+
shutil.copy2(item, dest_item)

0 commit comments

Comments
 (0)