Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default {
plugins: {
"postcss-import": {},
autoprefixer: {},
autoprefixer: process.env.REFLEX_NO_AUTOPREFIXER ? false : {},
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
},
};
4 changes: 4 additions & 0 deletions packages/reflex-base/src/reflex_base/compiler/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ def vite_config_template(
force_full_reload: bool,
experimental_hmr: bool,
sourcemap: bool | Literal["inline", "hidden"],
minify: bool = True,
allowed_hosts: bool | list[str] = False,
):
"""Template for vite.config.js.
Expand All @@ -553,6 +554,7 @@ def vite_config_template(
force_full_reload: Whether to force a full reload on changes.
experimental_hmr: Whether to enable experimental HMR features.
sourcemap: The sourcemap configuration.
minify: Whether to minify the build output.
allowed_hosts: Allow all hosts (True), specific hosts (list of strings), or only localhost (False).

Returns:
Expand Down Expand Up @@ -611,6 +613,8 @@ def vite_config_template(
safariCacheBustPlugin(),
].concat({"[fullReload()]" if force_full_reload else "[]"}),
build: {{
minify: {"true" if minify else "false"},
cssMinify: {"true" if minify else "false"},
sourcemap: {"true" if sourcemap is True else "false" if sourcemap is False else repr(sourcemap)},
rollupOptions: {{
onwarn(warning, warn) {{
Expand Down
1 change: 1 addition & 0 deletions packages/reflex-base/src/reflex_base/constants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class Env(str, Enum):
"""The environment modes."""

DEV = "dev"
DEV_BUILD = "dev-build"
PROD = "prod"


Expand Down
7 changes: 7 additions & 0 deletions packages/reflex-base/src/reflex_base/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,13 @@ class EnvironmentVariables:
# Whether to generate sourcemaps for the frontend.
VITE_SOURCEMAP: EnvVar[Literal[False, True, "inline", "hidden"]] = env_var(False) # noqa: RUF038

# Whether to minify the frontend build output. Disabled by dev-build mode for readable bundles.
VITE_MINIFY: EnvVar[bool] = env_var(True)

# Read by the generated postcss.config.js to skip autoprefixer. Set by dev-build
# mode to speed up rebuilds (vendor prefixes are unnecessary for local dev).
REFLEX_NO_AUTOPREFIXER: EnvVar[bool] = env_var(False, internal=True)

# Whether to enable SSR for the frontend.
REFLEX_SSR: EnvVar[bool] = env_var(True)

Expand Down
15 changes: 14 additions & 1 deletion reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,11 +710,24 @@ def __call__(self) -> ASGIApp:
# rx.asset(shared=True) symlink re-creation doesn't trigger further reloads.
remove_stale_external_asset_symlinks()

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

# In dev-build mode the frontend is served as a mounted static bundle rather
# than by the Vite dev server, so each hot reload must re-run the frontend
# build against the freshly compiled output.
if (
trigger == "hot_reload"
and environment.REFLEX_ENV_MODE.get() == constants.Env.DEV_BUILD
and environment.REFLEX_MOUNT_FRONTEND_COMPILED_APP.get()
):
from reflex.utils import build

build.build()
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

config = get_config()

for plugin in config.plugins:
Expand Down
79 changes: 73 additions & 6 deletions reflex/reflex.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,52 @@ def _run_dev(
exec.kill(exec.frontend_process.pid)


def _run_dev_build(running_mode: constants.RunningMode, port: int, host: str):
"""Run the app in dev-build mode.

Like dev mode, but instead of running the Vite dev server it serves a freshly
built (un-minified) frontend bundle mounted into the backend on a single port.
The backend still hot reloads, and each reload re-runs the frontend build
against the newly compiled output, so a manual browser refresh shows changes.
"""
import atexit

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

config = get_config()

config._set_persistent(frontend_port=port, backend_port=port)

# Mount the compiled frontend into the dev backend so no Vite server is needed.
environment.REFLEX_MOUNT_FRONTEND_COMPILED_APP.set(
running_mode.has_frontend() and running_mode.has_backend()
)

if running_mode.has_frontend():
# Compile the app and produce the initial frontend build.
_compile_app()
build.setup_frontend_prod(Path.cwd())

# Post a telemetry event.
telemetry.send("run-dev")
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

# Display custom message when there is a keyboard interrupt.
atexit.register(processes.atexit_handler)

exec.notify_app_running()
exec.notify_frontend(
f"http://{host}:{port}",
backend_present=running_mode.has_backend(),
)

if running_mode.has_backend():
exec.run_backend(
host, port, config.loglevel.subprocess_level(), running_mode.has_frontend()
)
else:
exec.run_frontend_prod(host, port)


def _run_prod(running_mode: constants.RunningMode, port: int, host: str):
import atexit

Expand Down Expand Up @@ -278,12 +324,14 @@ def _run(
console.error("Cannot specify --backend-port when not running backend.")
raise SystemExit(1)
if (
env == constants.Env.PROD
env in (constants.Env.PROD, constants.Env.DEV_BUILD)
and frontend_port
and backend_port
and frontend_port != backend_port
):
console.error("In production, frontend and backend must run on the same port.")
console.error(
f"In {env.value} mode, frontend and backend must run on the same port."
)
raise SystemExit(1)

config = get_config()
Expand All @@ -293,6 +341,17 @@ def _run(
# Set env mode in the environment
environment.REFLEX_ENV_MODE.set(env)

# Dev-build serves a real (but readable) frontend bundle: disable JS/CSS
# minification by default for readable output and to speed up rebuilds.
# Sourcemaps are left off (the default) since un-minified output is already
# debuggable, and autoprefixer is skipped (vendor prefixes are unnecessary for
# local dev). All remain overridable via the corresponding env vars.
if env == constants.Env.DEV_BUILD:
if not environment.VITE_MINIFY.is_set():
environment.VITE_MINIFY.set(False)
if not environment.REFLEX_NO_AUTOPREFIXER.is_set():
environment.REFLEX_NO_AUTOPREFIXER.set(True)

# Show system info
exec.output_system_info()

Expand Down Expand Up @@ -373,7 +432,10 @@ def _run(
auto_increment=requested_port is None,
)

_run_prod(running_mode, port, backend_host)
if env == constants.Env.DEV_BUILD:
_run_dev_build(running_mode, port, backend_host)
else:
_run_prod(running_mode, port, backend_host)


@cli.command()
Expand All @@ -382,7 +444,10 @@ def _run(
"--env",
type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
default=constants.Env.DEV.value,
help="The environment to run the app in.",
help=(
"The environment to run the app in. 'dev-build' hot reloads like 'dev' but "
"serves a freshly built, un-minified frontend bundle instead of the Vite dev server."
),
)
@click.option(
"--frontend-only",
Expand Down Expand Up @@ -464,7 +529,7 @@ def run(
running_mode = prerequisites.check_running_mode(frontend_only, backend_only)

_run(
env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
env=constants.Env(env),
running_mode=running_mode,
frontend_port=frontend_port,
backend_port=backend_port,
Expand Down Expand Up @@ -538,7 +603,9 @@ def compile(dry: bool, rich: bool):
)
@click.option(
"--env",
type=click.Choice([e.value for e in constants.Env], case_sensitive=False),
type=click.Choice(
[constants.Env.DEV.value, constants.Env.PROD.value], case_sensitive=False
),
default=constants.Env.PROD.value,
help="The environment to export the app in.",
)
Expand Down
1 change: 1 addition & 0 deletions reflex/utils/frontend_skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ def _compile_vite_config(config: Config):
force_full_reload=environment.VITE_FORCE_FULL_RELOAD.get(),
experimental_hmr=environment.VITE_EXPERIMENTAL_HMR.get(),
sourcemap=environment.VITE_SOURCEMAP.get(),
minify=environment.VITE_MINIFY.get(),
allowed_hosts=config.vite_allowed_hosts,
)

Expand Down
51 changes: 51 additions & 0 deletions tests/units/utils/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -815,3 +815,54 @@ def test_is_prod_mode() -> None:
assert utils_exec.is_prod_mode()
environment.REFLEX_ENV_MODE.set(None)
assert not utils_exec.is_prod_mode()


def test_dev_build_env_is_not_prod_mode() -> None:
"""dev-build is a development mode, so is_prod_mode must stay False."""
environment.REFLEX_ENV_MODE.set(constants.Env.DEV_BUILD)
try:
assert not utils_exec.is_prod_mode()
finally:
environment.REFLEX_ENV_MODE.set(None)


@pytest.mark.parametrize(
("value", "expected"),
[
("dev", constants.Env.DEV),
("dev-build", constants.Env.DEV_BUILD),
("prod", constants.Env.PROD),
],
)
def test_env_enum_roundtrip(value: str, expected: constants.Env) -> None:
"""Each env string maps to the matching Env member (used by the run CLI)."""
assert constants.Env(value) is expected


@pytest.mark.parametrize("minify", [True, False])
def test_vite_config_template_minify(minify: bool) -> None:
"""The vite config template emits the requested build.minify value."""
from reflex.compiler import templates as compiler_templates

config = compiler_templates.vite_config_template(
base="/",
hmr=True,
force_full_reload=False,
experimental_hmr=False,
sourcemap=False,
minify=minify,
)
expected = "true" if minify else "false"
assert f"minify: {expected}," in config
# CSS minification follows the JS minify flag.
assert f"cssMinify: {expected}," in config


@pytest.mark.parametrize("minify", [True, False])
def test_compile_vite_config_reads_minify_env(
minify: bool, monkeypatch: pytest.MonkeyPatch
) -> None:
"""_compile_vite_config threads the VITE_MINIFY env var into the template."""
monkeypatch.setenv(environment.VITE_MINIFY.name, "true" if minify else "false")
config = frontend_skeleton._compile_vite_config(prerequisites.get_config())
assert f"minify: {'true' if minify else 'false'}," in config
Loading