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
12 changes: 12 additions & 0 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,18 @@ def __call__(self) -> ASGIApp:

asgi_app = self._api

if environment.REFLEX_MOUNT_FRONTEND_COMPILED_APP.get():
asgi_app.mount(
"/" + config.frontend_path.strip("/"),
StaticFiles(
directory=prerequisites.get_web_dir()
/ constants.Dirs.STATIC
/ config.frontend_path.strip("/"),
html=True,
),
name="frontend",
)

if self.api_transformer is not None:
api_transformers: Sequence[Starlette | Callable[[ASGIApp], ASGIApp]] = (
[self.api_transformer]
Expand Down
3 changes: 3 additions & 0 deletions reflex/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,9 @@ class EnvironmentVariables:
# Whether to enable SSR for the frontend.
REFLEX_SSR: EnvVar[bool] = env_var(True)

# Whether to mount the compiled frontend app in the backend server in production.
REFLEX_MOUNT_FRONTEND_COMPILED_APP: EnvVar[bool] = env_var(False, internal=True)


environment = EnvironmentVariables()

Expand Down
84 changes: 61 additions & 23 deletions reflex/reflex.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import atexit
from importlib.util import find_spec
from pathlib import Path
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -132,8 +131,11 @@ def _run(
frontend_port: int | None = None,
backend_port: int | None = None,
backend_host: str | None = None,
single_port: bool = False,
):
"""Run the app in the given directory."""
import atexit

from reflex.utils import build, exec, prerequisites, processes

config = get_config()
Expand Down Expand Up @@ -173,7 +175,9 @@ def _run(
auto_increment=auto_increment_frontend,
)

if backend:
if single_port:
backend_port = frontend_port
elif backend:
auto_increment_backend = not bool(backend_port or config.backend_port)

backend_port = processes.handle_port(
Expand Down Expand Up @@ -223,23 +227,23 @@ def _run(
if not return_result:
raise SystemExit(1)

if env != constants.Env.PROD and env != constants.Env.DEV:
msg = f"Invalid env: {env}. Must be DEV or PROD."
raise ValueError(msg)

# Get the frontend and backend commands, based on the environment.
setup_frontend = frontend_cmd = backend_cmd = None
if env == constants.Env.DEV:
setup_frontend, frontend_cmd, backend_cmd = (
build.setup_frontend,
exec.run_frontend,
exec.run_backend,
)
if env == constants.Env.PROD:
elif env == constants.Env.PROD:
setup_frontend, frontend_cmd, backend_cmd = (
build.setup_frontend_prod,
exec.run_frontend_prod,
exec.run_backend_prod,
)
if not setup_frontend or not frontend_cmd or not backend_cmd:
msg = f"Invalid env: {env}. Must be DEV or PROD."
raise ValueError(msg)

# Post a telemetry event.
telemetry.send(f"run-{env.value}")
Expand All @@ -251,7 +255,7 @@ def _run(
commands = []

# Run the frontend on a separate thread.
if frontend:
if frontend and not single_port:
setup_frontend(Path.cwd())
commands.append((frontend_cmd, Path.cwd(), frontend_port, backend))

Expand All @@ -267,21 +271,30 @@ def _run(
)
)

# Start the frontend and backend.
with processes.run_concurrently_context(*commands):
# In dev mode, run the backend on the main thread.
if backend and backend_port and env == constants.Env.DEV:
backend_cmd(
backend_host,
int(backend_port),
config.loglevel.subprocess_level(),
frontend,
)
# The windows uvicorn bug workaround
# https://github.com/reflex-dev/reflex/issues/2335
if constants.IS_WINDOWS and exec.frontend_process:
# Sends SIGTERM in windows
exec.kill(exec.frontend_process.pid)
if single_port:
setup_frontend(Path.cwd())
backend_function, *args = commands[0]
exec.notify_app_running()
exec.notify_frontend(
f"http://0.0.0.0:{get_config().frontend_port}", backend_present=True
)
backend_function(*args, mount_frontend_compiled_app=True)
else:
# Start the frontend and backend.
with processes.run_concurrently_context(*commands):
# In dev mode, run the backend on the main thread.
if backend and backend_port and env == constants.Env.DEV:
backend_cmd(
backend_host,
int(backend_port),
config.loglevel.subprocess_level(),
frontend,
)
# The windows uvicorn bug workaround
# https://github.com/reflex-dev/reflex/issues/2335
if constants.IS_WINDOWS and exec.frontend_process:
# Sends SIGTERM in windows
exec.kill(exec.frontend_process.pid)


@cli.command()
Expand Down Expand Up @@ -322,19 +335,43 @@ def _run(
"--backend-host",
help="Specify the backend host.",
)
@click.option(
"--single-port",
is_flag=True,
help="Run both frontend and backend on the same port.",
default=False,
)
def run(
env: LITERAL_ENV,
frontend_only: bool,
backend_only: bool,
frontend_port: int | None,
backend_port: int | None,
backend_host: str | None,
single_port: bool,
):
"""Run the app in the current directory."""
if frontend_only and backend_only:
console.error("Cannot use both --frontend-only and --backend-only options.")
raise click.exceptions.Exit(1)

if single_port:
if env != constants.Env.PROD.value:
console.error("--single-port can only be used with --env=PROD.")
raise click.exceptions.Exit(1)
if frontend_only or backend_only:
console.error(
"Cannot use --single-port with --frontend-only or --backend-only options."
)
raise click.exceptions.Exit(1)
if backend_port and frontend_port and backend_port != frontend_port:
console.error(
"When using --single-port, --backend-port and --frontend-port must be the same."
)
raise click.exceptions.Exit(1)
elif frontend_port and backend_port and frontend_port == backend_port:
single_port = True

config = get_config()

frontend_port = frontend_port or config.frontend_port
Expand All @@ -352,6 +389,7 @@ def run(
frontend_port,
backend_port,
backend_host,
single_port,
)


Expand Down
27 changes: 23 additions & 4 deletions reflex/utils/exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,18 @@ def kill(proc_pid: int):
process.kill()


def notify_frontend(url: str, backend_present: bool):
"""Output a string notifying where the frontend is running.

Args:
url: The URL where the frontend is running.
backend_present: Whether the backend is present.
"""
console.print(
f"App running at: [bold green]{url.rstrip('/')}/[/bold green]{' (Frontend-only mode)' if not backend_present else ''}"
)


def notify_backend():
"""Output a string notifying where the backend is running."""
console.print(
Expand Down Expand Up @@ -210,9 +222,7 @@ def run_process_and_launch_url(
if get_config().frontend_path != "":
url = urljoin(url, get_config().frontend_path)

console.print(
f"App running at: [bold green]{url}[/bold green]{' (Frontend-only mode)' if not backend_present else ''}"
)
notify_frontend(url, backend_present)
if backend_present:
notify_backend()
first_run = False
Expand Down Expand Up @@ -249,6 +259,11 @@ def run_frontend(root: Path, port: str, backend_present: bool = True):
)


def notify_app_running():
"""Notify that the app is running."""
console.rule("[bold green]App Running")


def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
"""Run the frontend.

Expand All @@ -264,7 +279,7 @@ def run_frontend_prod(root: Path, port: str, backend_present: bool = True):
# validate dependencies before run
js_runtimes.validate_frontend_dependencies(init=False)
# Run the frontend in production mode.
console.rule("[bold green]App Running")
notify_app_running()
run_process_and_launch_url(
[*js_runtimes.get_js_package_executor(raise_on_none=True)[0], "run", "prod"],
backend_present,
Expand Down Expand Up @@ -552,6 +567,7 @@ def run_backend_prod(
port: int,
loglevel: constants.LogLevel = constants.LogLevel.ERROR,
frontend_present: bool = False,
mount_frontend_compiled_app: bool = False,
):
"""Run the backend.

Expand All @@ -560,10 +576,13 @@ def run_backend_prod(
port: The app port
loglevel: The log level.
frontend_present: Whether the frontend is present.
mount_frontend_compiled_app: Whether to mount the compiled frontend app with the backend.
"""
if not frontend_present:
notify_backend()

environment.REFLEX_MOUNT_FRONTEND_COMPILED_APP.set(mount_frontend_compiled_app)

if should_use_granian():
run_granian_backend_prod(host, port, loglevel)
else:
Expand Down