Skip to content

Commit 4e7b239

Browse files
authored
Fix outstanding frontend_path issues (#6328)
* Plumb frontend_path into vite.config.js `base` option * also initialize vite.config.js during compile, in case user has set a different runtime frontend path * remove assetsDir workaround (base handles this case) * Add test_frontend_path and make sure APIs respect it Add `prepend_frontend_path` to `rx.Config` to make it easy to handle the contract in current and future APIs. Use `prepend_frontend_path` in: * rx.get_upload_url * rx.asset * vite config * react router config * consolidate boilerplate logic in test_frontend_path * Always run playwright tests in separate CI job playwright uses a session scope fixture which creates an event loop for the session, so subsequent tests using pytest_asyncio fixture cannot start their own loop and fail * fix frontend path for shared assets include updated test case
1 parent c3f684c commit 4e7b239

File tree

11 files changed

+529
-25
lines changed

11 files changed

+529
-25
lines changed

.github/workflows/integration_app_harness.yml

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,46 @@ jobs:
5353
python-version: ${{ matrix.python-version }}
5454
run-uv-sync: true
5555

56+
- name: Run app harness tests
57+
env:
58+
REFLEX_REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
59+
run: uv run pytest tests/integration --ignore=tests/integration/tests_playwright --reruns 3 -v --maxfail=5 --splits 2 --group ${{matrix.split_index}}
60+
61+
# Playwright tests run in a separate job because the pytest-playwright plugin
62+
# keeps an asyncio event loop running on the main thread for the entire
63+
# session, which is incompatible with pytest-asyncio tests.
64+
integration-app-harness-playwright:
65+
timeout-minutes: 30
66+
strategy:
67+
matrix:
68+
state_manager: ["redis", "memory"]
69+
python-version: ["3.11", "3.12", "3.13", "3.14"]
70+
fail-fast: false
71+
runs-on: ubuntu-22.04
72+
services:
73+
redis:
74+
image: ${{ matrix.state_manager == 'redis' && 'redis' || '' }}
75+
options: >-
76+
--health-cmd "redis-cli ping"
77+
--health-interval 10s
78+
--health-timeout 5s
79+
--health-retries 5
80+
ports:
81+
- 6379:6379
82+
steps:
83+
- uses: actions/checkout@v4
84+
with:
85+
fetch-tags: true
86+
fetch-depth: 0
87+
- uses: ./.github/actions/setup_build_env
88+
with:
89+
python-version: ${{ matrix.python-version }}
90+
run-uv-sync: true
91+
5692
- name: Install playwright
5793
run: uv run playwright install chromium --only-shell
5894

59-
- name: Run app harness tests
95+
- name: Run playwright tests
6096
env:
6197
REFLEX_REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
62-
run: uv run pytest tests/integration --reruns 3 -v --maxfail=5 --splits 2 --group ${{matrix.split_index}}
98+
run: uv run pytest tests/integration/tests_playwright --reruns 3 -v --maxfail=5

packages/reflex-base/src/reflex_base/compiler/templates.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ def vite_config_template(
506506
"""Template for vite.config.js.
507507
508508
Args:
509-
base: The base path for the Vite config.
509+
base: The base path for the Vite config (for handling frontend_path config).
510510
hmr: Whether to enable hot module replacement.
511511
force_full_reload: Whether to force a full reload on changes.
512512
experimental_hmr: Whether to enable experimental HMR features.
@@ -562,13 +562,13 @@ def vite_config_template(
562562
}}
563563
564564
export default defineConfig((config) => ({{
565+
base: "{base}",
565566
plugins: [
566567
alwaysUseReactDomServerNode(),
567568
reactRouter(),
568569
safariCacheBustPlugin(),
569570
].concat({"[fullReload()]" if force_full_reload else "[]"}),
570571
build: {{
571-
assetsDir: "{base}assets".slice(1),
572572
sourcemap: {"true" if sourcemap is True else "false" if sourcemap is False else repr(sourcemap)},
573573
rollupOptions: {{
574574
onwarn(warning, warn) {{

packages/reflex-base/src/reflex_base/config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,19 @@ def json(self) -> str:
476476

477477
return json.dumps(self, default=serialize)
478478

479+
def prepend_frontend_path(self, path: str) -> str:
480+
"""Prepend the frontend path to a given path.
481+
482+
Args:
483+
path: The path to prepend the frontend path to.
484+
485+
Returns:
486+
The path with the frontend path prepended if it begins with a slash, otherwise the original path.
487+
"""
488+
if self.frontend_path and path.startswith("/"):
489+
return f"/{self.frontend_path.strip('/')}{path}"
490+
return path
491+
479492
@property
480493
def app_module(self) -> ModuleType | None:
481494
"""Return the app module if `app_module_import` is set.

reflex/app.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,7 @@ def get_compilation_time() -> str:
12201220
)
12211221

12221222
# try to be somewhat accurate - but still not 100%
1223-
adhoc_steps_without_executor = 7
1223+
adhoc_steps_without_executor = 8
12241224
fixed_pages_within_executor = 4
12251225
plugin_count = len(config.plugins)
12261226
progress.start()
@@ -1278,6 +1278,13 @@ def get_compilation_time() -> str:
12781278

12791279
progress.advance(task)
12801280

1281+
# Reinitialize vite config in case runtime options have changed.
1282+
compile_results.append((
1283+
constants.ReactRouter.VITE_CONFIG_FILE,
1284+
frontend_skeleton._compile_vite_config(config),
1285+
))
1286+
progress.advance(task)
1287+
12811288
# Track imports found.
12821289
all_imports = {}
12831290

reflex/assets.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pathlib import Path
55

66
from reflex_base import constants
7+
from reflex_base.config import get_config
78
from reflex_base.environment import EnvironmentVariables
89

910

@@ -92,7 +93,7 @@ def asset(
9293
if not backend_only and not src_file_local.exists():
9394
msg = f"File not found: {src_file_local}"
9495
raise FileNotFoundError(msg)
95-
return f"/{path}"
96+
return get_config().prepend_frontend_path(f"/{path}")
9697

9798
# Shared asset handling
9899
# Determine the file by which the asset is exposed.
@@ -128,4 +129,4 @@ def asset(
128129
dst_file.unlink()
129130
dst_file.symlink_to(src_file_shared)
130131

131-
return f"/{external}/{subfolder}/{path}"
132+
return get_config().prepend_frontend_path(f"/{external}/{subfolder}/{path}")

reflex/testing.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -896,18 +896,19 @@ def _run_frontend(self):
896896
/ reflex.utils.prerequisites.get_web_dir()
897897
/ reflex.constants.Dirs.STATIC
898898
)
899-
error_page_map = {
900-
404: web_root / "404.html",
901-
}
899+
config = reflex.config.get_config()
902900
with Subdir404TCPServer(
903901
("", 0),
904902
SimpleHTTPRequestHandlerCustomErrors,
905903
root=web_root,
906-
error_page_map=error_page_map,
904+
error_page_map={
905+
404: web_root / config.prepend_frontend_path("/404.html").lstrip("/"),
906+
},
907907
) as self.frontend_server:
908+
frontend_path = config.frontend_path.strip("/")
908909
self.frontend_url = "http://localhost:{1}".format(
909910
*self.frontend_server.socket.getsockname()
910-
)
911+
) + (f"/{frontend_path}/" if frontend_path else "/")
911912
self.frontend_server.serve_forever()
912913

913914
def _start_frontend(self):

reflex/utils/build.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def build():
241241
config = get_config()
242242

243243
if frontend_path := config.frontend_path.strip("/"):
244+
# Create a subdirectory that matches the configured frontend_path.
244245
frontend_path = PosixPath(frontend_path)
245246
first_part = frontend_path.parts[0]
246247
for child in list((wdir / constants.Dirs.STATIC).iterdir()):

reflex/utils/exec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def get_frontend_mount():
280280
config = get_config()
281281

282282
return Mount(
283-
"/" + config.frontend_path.strip("/"),
283+
config.prepend_frontend_path("/"),
284284
app=StaticFiles(
285285
directory=prerequisites.get_web_dir()
286286
/ constants.Dirs.STATIC

reflex/utils/frontend_skeleton.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,8 @@ def update_react_router_config(prerender_routes: bool = False):
205205

206206

207207
def _update_react_router_config(config: Config, prerender_routes: bool = False):
208-
basename = "/" + (config.frontend_path or "").strip("/")
209-
if not basename.endswith("/"):
210-
basename += "/"
211-
212208
react_router_config = {
213-
"basename": basename,
209+
"basename": config.prepend_frontend_path("/"),
214210
"future": {
215211
"unstable_optimizeDeps": True,
216212
},
@@ -244,11 +240,8 @@ def initialize_package_json():
244240

245241
def _compile_vite_config(config: Config):
246242
# base must have exactly one trailing slash
247-
base = "/"
248-
if frontend_path := config.frontend_path.strip("/"):
249-
base += frontend_path + "/"
250243
return templates.vite_config_template(
251-
base=base,
244+
base=config.prepend_frontend_path("/"),
252245
hmr=environment.VITE_HMR.get(),
253246
force_full_reload=environment.VITE_FORCE_FULL_RELOAD.get(),
254247
experimental_hmr=environment.VITE_EXPERIMENTAL_HMR.get(),

0 commit comments

Comments
 (0)