Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/reflex-base/src/reflex_base/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,10 @@ class EnvironmentVariables:
# If this env var is set to "yes", App.compile will be a no-op
REFLEX_SKIP_COMPILE: EnvVar[bool] = env_var(False, internal=True)

# Inherited by uvicorn/granian reload workers so the backend can distinguish
# dev reload-capable worker boots from other backend starts. Never set in prod.
REFLEX_DEV_BACKEND_RELOAD_ACTIVE: EnvVar[bool] = env_var(False, internal=True)

# Whether to run app harness tests in headless mode.
APP_HARNESS_HEADLESS: EnvVar[bool] = env_var(False)

Expand Down
2 changes: 1 addition & 1 deletion pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,5 @@
"packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "2c5fadcc014056f041cd4d916137d9e7",
"reflex/__init__.pyi": "3a9bb8544cbc338ffaf0a5927d9156df",
"reflex/components/__init__.pyi": "f39a2af77f438fa243c58c965f19d42e",
"reflex/experimental/memo.pyi": "82d8699470071df80886a4a6ba8dccfe"
"reflex/experimental/memo.pyi": "d09629b81bf0df6153b131ac0ee10bd7"
}
58 changes: 44 additions & 14 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin
from reflex.compiler import compiler
from reflex.compiler.compiler import readable_name_from_component
from reflex.istate.data import RouterData
from reflex.istate.manager import StateManager, StateModificationContext
from reflex.istate.manager.token import BaseStateToken
from reflex.page import DECORATED_PAGES
Expand All @@ -78,21 +79,24 @@
replace_brackets_with_keywords,
verify_route_validity,
)
from reflex.state import (
BaseState,
RouterData,
State,
StateUpdate,
all_base_state_classes,
from reflex.state import BaseState, State, StateUpdate, all_base_state_classes
from reflex.utils import (
codespaces,
exceptions,
format,
js_runtimes,
prerequisites,
telemetry_accounting,
)
from reflex.utils import codespaces, exceptions, format, js_runtimes, prerequisites
from reflex.utils.exec import (
get_backend_compile_trigger,
get_compile_context,
is_prod_mode,
is_testing_env,
should_prerender_routes,
)
from reflex.utils.misc import run_in_thread
from reflex.utils.telemetry_context import CompileTrigger, TelemetryContext
from reflex.utils.token_manager import RedisTokenManager, TokenManager

if sys.version_info < (3, 13):
Expand Down Expand Up @@ -662,7 +666,10 @@ def __call__(self) -> ASGIApp:
# rx.asset(shared=True) symlink re-creation doesn't trigger further reloads.
remove_stale_external_asset_symlinks()

self._compile(prerender_routes=should_prerender_routes())
self._compile(
prerender_routes=should_prerender_routes(),
trigger=get_backend_compile_trigger(),
)

config = get_config()

Expand Down Expand Up @@ -1167,24 +1174,47 @@ def _compile(
prerender_routes: bool = False,
dry_run: bool = False,
use_rich: bool = True,
trigger: CompileTrigger | None = None,
):
"""Compile the app and output it to the pages folder.

Args:
prerender_routes: Whether to prerender the routes.
dry_run: Whether to compile the app without saving it.
use_rich: Whether to use rich progress bars.
trigger: Label identifying what initiated this compile. Recorded
on the ``compile`` telemetry event.

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.
"""
compiler.compile_app(
self,
prerender_routes=prerender_routes,
dry_run=dry_run,
use_rich=use_rich,
)
ctx = TelemetryContext.start(trigger=trigger)
if ctx is None:
compiler.compile_app(
self,
prerender_routes=prerender_routes,
dry_run=dry_run,
use_rich=use_rich,
)
return

with ctx:
did_real_compile = False
try:
did_real_compile = compiler.compile_app(
self,
prerender_routes=prerender_routes,
dry_run=dry_run,
use_rich=use_rich,
)
except Exception as exc:
ctx.set_exception(exc)
did_real_compile = True
raise
finally:
if did_real_compile:
telemetry_accounting.record_compile(self, ctx)

def _write_stateful_pages_marker(self):
"""Write list of routes that create dynamic states for the backend to use later."""
Expand Down
17 changes: 12 additions & 5 deletions reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -993,8 +993,13 @@ def compile_app(
prerender_routes: bool = False,
dry_run: bool = False,
use_rich: bool = True,
) -> None:
"""Compile an app using the compiler plugin pipeline."""
) -> bool:
"""Compile an app using the compiler plugin pipeline.

Returns:
``True`` when a real frontend compile ran, ``False`` when the call
short-circuited (backend-only paths that only re-evaluate pages).
"""
from reflex_base.components.dynamic import bundle_library, reset_bundled_libraries
from reflex_base.utils.exceptions import ReflexRuntimeError

Expand All @@ -1012,7 +1017,7 @@ def compile_app(
console.debug(f"BE Evaluating stateful page: {route}")
app._compile_page(route, save_page=False)
app._add_optional_endpoints()
return
return False

if constants.Page404.SLUG not in app._unevaluated_pages:
app.add_page(route=constants.Page404.SLUG)
Expand All @@ -1028,7 +1033,7 @@ def compile_app(

app._write_stateful_pages_marker()
app._add_optional_endpoints()
return
return False

progress = (
Progress(
Expand Down Expand Up @@ -1222,7 +1227,7 @@ def add_save_task(
progress.stop()

if dry_run:
return
return True

with console.timing("Install Frontend Packages"):
app._get_frontend_packages(all_imports)
Expand Down Expand Up @@ -1277,3 +1282,5 @@ def add_save_task(
with console.timing("Write to Disk"):
for output_path, code in output_mapping.items():
utils.write_file(output_path, code)

return True
9 changes: 8 additions & 1 deletion reflex/experimental/memo.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import Callable
from copy import copy
from functools import cache, update_wrapper
from typing import Any, get_args, get_origin, get_type_hints
from typing import Any, ClassVar, get_args, get_origin, get_type_hints

from reflex_base import constants
from reflex_base.components.component import Component
Expand Down Expand Up @@ -94,6 +94,12 @@ class ExperimentalMemoComponent(Component):
library = f"$/{constants.Dirs.COMPONENTS_PATH}"
_memoization_mode = MemoizationMode(disposition=MemoizationDisposition.NEVER)

# The user-authored component class this wrapper stands in for. Populated
# on the dynamic subclass by ``_get_experimental_memo_component_class`` so
# introspection (e.g. compile telemetry) can recover the underlying type
# without parsing the wrapper's auto-generated class name.
_wrapped_component_type: ClassVar[type[Component] | None] = None

def _validate_component_children(self, children: list[Component]) -> None:
"""Skip direct parent/child validation for memo wrapper instances.

Expand Down Expand Up @@ -176,6 +182,7 @@ def _get_experimental_memo_component_class(
# Per-file import paths give Vite distinct module boundaries per
# memo, enabling actual code-split by page.
"library": f"$/{constants.Dirs.COMPONENTS_PATH}/{export_name}",
"_wrapped_component_type": wrapped_component_type,
}
if (
wrapped_component_type._get_app_wrap_components
Expand Down
3 changes: 2 additions & 1 deletion reflex/reflex.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def _compile_app(*, avoid_dirty_check: bool = True):
kwargs = {
"check_if_schema_up_to_date": True,
"prerender_routes": exec.should_prerender_routes(),
"trigger": "initial",
}

# Granian fails if the app is already imported.
Expand Down Expand Up @@ -485,7 +486,7 @@ def compile(dry: bool, rich: bool):
_init(name=get_config().app_name)
get_config(reload=True)
starting_time = time.monotonic()
prerequisites.get_compiled_app(dry_run=dry, use_rich=rich)
prerequisites.get_compiled_app(dry_run=dry, use_rich=rich, trigger="cli_compile")
elapsed_time = time.monotonic() - starting_time
console.success(f"App compiled successfully in {elapsed_time:.3f} seconds.")

Expand Down
49 changes: 49 additions & 0 deletions reflex/utils/exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import contextlib
import hashlib
import importlib.util
import json
Expand All @@ -25,10 +26,52 @@
from reflex.utils import path_ops
from reflex.utils.misc import get_module_path
from reflex.utils.prerequisites import get_web_dir
from reflex.utils.telemetry_context import CompileTrigger

# For uvicorn windows bug fix (#2335)
frontend_process = None

DEV_BACKEND_RELOAD_MARKER = ".reflex_dev_backend_started"


def get_dev_backend_reload_marker() -> Path:
"""Get the marker path for dev backend reload-capable worker starts.

Returns:
The path to the reload marker.
"""
return get_web_dir() / DEV_BACKEND_RELOAD_MARKER


def reset_dev_backend_reload_marker() -> None:
"""Remove the reload marker at the start of a fresh dev backend session."""
with contextlib.suppress(OSError):
get_dev_backend_reload_marker().unlink(missing_ok=True)


def get_backend_compile_trigger() -> CompileTrigger:
"""Determine the compile trigger and claim the dev backend reload marker.

Atomically creates the marker so a failed first compile is still treated
as the first worker boot: the next worker (after the user fixes the
error) will see the marker and report ``hot_reload``. If the marker
cannot be created (e.g. permission error, missing parent dir), falls
back to ``backend_startup``.

Returns:
``"backend_startup"`` for non-dev startups and the first dev
reload-capable worker boot, ``"hot_reload"`` for subsequent boots.
"""
if not environment.REFLEX_DEV_BACKEND_RELOAD_ACTIVE.get():
return "backend_startup"
try:
os.close(os.open(get_dev_backend_reload_marker(), os.O_CREAT | os.O_EXCL))
except FileExistsError:
return "hot_reload"
except OSError:
pass
return "backend_startup"


def get_package_json_and_hash(package_json_path: Path) -> tuple[PackageJson, str]:
"""Get the content of package.json and its hash.
Expand Down Expand Up @@ -537,6 +580,9 @@ def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel):
"""
import uvicorn

reset_dev_backend_reload_marker()
environment.REFLEX_DEV_BACKEND_RELOAD_ACTIVE.set(True)

uvicorn.run(
app=f"{get_app_instance()}",
factory=True,
Expand Down Expand Up @@ -588,6 +634,9 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
from granian.server import Server as Granian
from reflex_base.environment import _load_dotenv_from_env

reset_dev_backend_reload_marker()
environment.REFLEX_DEV_BACKEND_RELOAD_ACTIVE.set(True)

granian_app = Granian(
target=get_app_instance_from_file(),
factory=True,
Expand Down
4 changes: 3 additions & 1 deletion reflex/utils/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ def export(

if frontend:
# Ensure module can be imported and app.compile() is called.
prerequisites.get_compiled_app(prerender_routes=prerender_routes)
prerequisites.get_compiled_app(
prerender_routes=prerender_routes, trigger="export"
)
# Set up .web directory and install frontend dependencies.
build.setup_frontend(Path.cwd())

Expand Down
13 changes: 12 additions & 1 deletion reflex/utils/prerequisites.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from redis.asyncio import Redis

from reflex.app import App
from reflex.utils.telemetry_context import CompileTrigger


class AppInfo(NamedTuple):
Expand Down Expand Up @@ -251,6 +252,7 @@ def get_compiled_app(
dry_run: bool = False,
check_if_schema_up_to_date: bool = False,
use_rich: bool = True,
trigger: CompileTrigger | None = None,
) -> ModuleType:
"""Get the app module based on the default config after first compiling it.

Expand All @@ -260,14 +262,20 @@ def get_compiled_app(
dry_run: If True, do not write the compiled app to disk.
check_if_schema_up_to_date: If True, check if the schema is up to date.
use_rich: Whether to use rich progress bars.
trigger: Optional label forwarded to ``App._compile`` for telemetry.

Returns:
The compiled app based on the default config.
"""
app, app_module = get_and_validate_app(
reload=reload, check_if_schema_up_to_date=check_if_schema_up_to_date
)
app._compile(prerender_routes=prerender_routes, dry_run=dry_run, use_rich=use_rich)
app._compile(
prerender_routes=prerender_routes,
dry_run=dry_run,
use_rich=use_rich,
trigger=trigger,
)
return app_module


Expand Down Expand Up @@ -335,13 +343,15 @@ def compile_or_validate_app(
compile: bool = False,
check_if_schema_up_to_date: bool = False,
prerender_routes: bool = False,
trigger: CompileTrigger | None = None,
) -> bool:
"""Compile or validate the app module based on the default config.

Args:
compile: Whether to compile the app.
check_if_schema_up_to_date: If True, check if the schema is up to date.
prerender_routes: Whether to prerender routes.
trigger: Optional label forwarded to ``App._compile`` for telemetry.

Returns:
True if the app was successfully compiled or validated, False otherwise.
Expand All @@ -351,6 +361,7 @@ def compile_or_validate_app(
get_compiled_app(
check_if_schema_up_to_date=check_if_schema_up_to_date,
prerender_routes=prerender_routes,
trigger=trigger,
)
else:
get_and_validate_app(check_if_schema_up_to_date=check_if_schema_up_to_date)
Expand Down
Loading
Loading