Skip to content
Closed
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
1 change: 1 addition & 0 deletions packages/reflex-base/news/6561.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed recharts charts crashing at runtime with a `TypeError` and an infinite route-reload loop. recharts default-imports CommonJS `es-toolkit/compat/*` shims, whose interop helper could be bundled into a chunk that loaded after a lazy route chunk; the generated `vite.config.js` now redirects those imports to the pure-ESM `es-toolkit/compat` barrel.
27 changes: 27 additions & 0 deletions packages/reflex-base/src/reflex_base/compiler/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,10 +575,32 @@ def vite_config_template(
}};
}}

function esToolkitCompatEsm() {{
// recharts default-imports CJS shims like `es-toolkit/compat/get`; the CJS interop
// helper can land in a chunk that runs before it is initialized, crashing any page
// with a chart (#6561). Redirect to the pure-ESM compat barrel. Registered for both
// the prod build (plugins) and the dev prebundle (optimizeDeps.rolldownOptions).
const VIRTUAL = "\0es-toolkit-compat:";
return {{
name: "vite-plugin-es-toolkit-compat-esm",
enforce: "pre",
resolveId(source) {{
const match = /^es-toolkit\/compat\/([A-Za-z0-9_]+)$/.exec(source);
return match ? VIRTUAL + match[1] : null;
}},
load(id) {{
if (!id.startsWith(VIRTUAL)) return null;
const name = id.slice(VIRTUAL.length);
return "export {{ " + name + " as default }} from \"es-toolkit/compat\";";
}},
}};
}}

export default defineConfig((config) => ({{
base: "{base}",
plugins: [
alwaysUseReactDomServerNode(),
esToolkitCompatEsm(),
reactRouter(),
safariCacheBustPlugin(),
].concat({"[fullReload()]" if force_full_reload else "[]"}),
Expand All @@ -602,6 +624,11 @@ def vite_config_template(
}},
}},
}},
optimizeDeps: {{
rolldownOptions: {{
plugins: [esToolkitCompatEsm()],
}},
}},
experimental: {{
enableNativePlugin: false,
hmr: {"true" if experimental_hmr else "false"},
Expand Down
85 changes: 85 additions & 0 deletions tests/integration/tests_playwright/test_recharts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Integration test that recharts charts render at runtime.

Regression coverage for https://github.com/reflex-dev/reflex/issues/6561: any page
rendering a ``rx.recharts`` chart crashed with ``TypeError`` and an infinite React
Router route-reload loop. recharts default-imports CommonJS ``es-toolkit/compat/*``
shims from its core utils; the bundler emitted the CJS interop helper into a chunk
that loaded after the lazy route chunk. The bug only surfaced at runtime in a real
(esp. production / Rolldown) build, which this test exercises via ``app_harness_env``.
"""

from collections.abc import Generator

import pytest
from playwright.sync_api import Page, expect

from reflex.testing import AppHarness


def RechartsApp():
"""App with an area chart on the index route (the issue's reproduction)."""
import reflex as rx

@rx.page("/")
def index():
return rx.box(
rx.heading("recharts"),
rx.recharts.area_chart(
rx.recharts.area(data_key="value"),
rx.recharts.x_axis(data_key="name"),
data=[
{"name": "A", "value": 1},
{"name": "B", "value": 3},
{"name": "C", "value": 2},
],
width=500,
height=300,
),
id="page",
)

app = rx.App() # noqa: F841


@pytest.fixture(scope="module")
def recharts_app(
app_harness_env: type[AppHarness],
tmp_path_factory: pytest.TempPathFactory,
) -> Generator[AppHarness, None, None]:
"""Start RechartsApp in both dev and prod mode.

Args:
app_harness_env: AppHarness (dev) or AppHarnessProd (prod), from conftest.
tmp_path_factory: pytest fixture for creating temporary directories.

Yields:
Running AppHarness instance.
"""
name = f"recharts_{app_harness_env.__name__.lower()}"
with app_harness_env.create(
root=tmp_path_factory.mktemp(name),
app_name=name,
app_source=RechartsApp,
) as harness:
assert harness.app_instance is not None, "app is not running"
yield harness


def test_recharts_renders_without_error(recharts_app: AppHarness, page: Page):
"""The chart's SVG renders and the page raises no JS error or reload loop.

Args:
recharts_app: AppHarness running the recharts test app.
page: Playwright page.
"""
assert recharts_app.frontend_url is not None

page_errors: list[str] = []
page.on("pageerror", lambda exc: page_errors.append(str(exc)))

page.goto(recharts_app.frontend_url)

# Under #6561 the route module fails to load and React Router reloads forever,
# so the chart never appears and this assertion times out.
expect(page.locator(".recharts-surface")).to_be_visible()
assert not page_errors, f"Frontend raised unexpected errors: {page_errors}"
Loading