From 928d2b2927f7d50c555589f8222123b18daba8a4 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 12 Sep 2026 18:48:09 +0530 Subject: [PATCH 1/7] fix: restore bundled libraries for backend workers --- news/7096.bugfix.md | 1 + .../src/reflex_base/constants/base.py | 2 ++ reflex/compiler/compiler.py | 3 +++ reflex/compiler/utils.py | 26 +++++++++++++++++++ tests/units/compiler/test_compiler_utils.py | 24 +++++++++++++++++ 5 files changed, 56 insertions(+) create mode 100644 news/7096.bugfix.md diff --git a/news/7096.bugfix.md b/news/7096.bugfix.md new file mode 100644 index 00000000000..ba337a34007 --- /dev/null +++ b/news/7096.bugfix.md @@ -0,0 +1 @@ +Persist bundled-library metadata for backend-only workers so state hydration can serialize values that reference libraries included in the frontend build. diff --git a/packages/reflex-base/src/reflex_base/constants/base.py b/packages/reflex-base/src/reflex_base/constants/base.py index b5c9079517e..02fcea0f133 100644 --- a/packages/reflex-base/src/reflex_base/constants/base.py +++ b/packages/reflex-base/src/reflex_base/constants/base.py @@ -56,6 +56,8 @@ class Dirs(SimpleNamespace): APP_COMPONENTS = "app_components" # The name of the env json file. ENV_JSON = "env.json" + # The name of the compiled bundled-library registry. + BUNDLED_LIBRARIES = "bundled_libraries.json" # The name of the reflex json file. REFLEX_JSON = "reflex.json" # The name of the postcss config file. diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 23534ea9726..fa56b24b993 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -1241,6 +1241,7 @@ def compile_app( logger.debug(f"BE Evaluating stateful page: {route}") app._compile_page(route, save_page=False) if app._state is not None: + utils._restore_bundled_libraries() utils._compile_initial_state(app._state) app._add_optional_endpoints() return False @@ -1261,6 +1262,7 @@ def compile_app( app._write_stateful_pages_marker() if app._state is not None: + utils._restore_bundled_libraries() utils._compile_initial_state(app._state) app._add_optional_endpoints() return False @@ -1470,6 +1472,7 @@ def add_save_task( component_imports=all_imports, ) ) + compile_results.append(utils._compile_bundled_libraries()) progress.advance(task) compile_results.append(compile_app_root(app_root, hydrate_fallback_export)) diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index a0c5865b5b6..3884365477e 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -273,6 +273,32 @@ def serialize_initial_value(value: Any) -> Any: ) +def _compile_bundled_libraries() -> tuple[str, str]: + """Return the bundled-library registry as a frontend build artifact. + + Returns: + The output path and serialized registry. + """ + bundled_libraries = RegistrationContext.ensure_context().bundled_libraries + return constants.Dirs.BUNDLED_LIBRARIES, format.json_dumps(bundled_libraries) + + +def _restore_bundled_libraries() -> None: + """Restore the registry emitted by the most recent frontend compile.""" + path = get_web_dir() / constants.Dirs.BUNDLED_LIBRARIES + try: + bundled_libraries = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return + if not isinstance(bundled_libraries, list) or not all( + isinstance(library, str) for library in bundled_libraries + ): + return + RegistrationContext.ensure_context().bundled_libraries[:] = list( + dict.fromkeys(bundled_libraries) + ) + + def _compile_client_storage_field( field: Field, ) -> ( diff --git a/tests/units/compiler/test_compiler_utils.py b/tests/units/compiler/test_compiler_utils.py index 8055295b332..18a3571d0dc 100644 --- a/tests/units/compiler/test_compiler_utils.py +++ b/tests/units/compiler/test_compiler_utils.py @@ -7,9 +7,11 @@ from reflex_components_core.base.script import Script from reflex_components_core.el.elements.metadata import Link +from reflex.compiler import utils from reflex.compiler.utils import compile_state, create_document_root from reflex.compiler.utils import write_file as compiler_write_file from reflex.constants.state import FIELD_MARKER +from reflex_base.registry import RegistrationContext from reflex.state import State from reflex.utils.path_ops import write_file from reflex.vars.base import computed_var @@ -20,6 +22,28 @@ def test_write_file_reexport() -> None: assert compiler_write_file is write_file +def test_bundled_libraries_artifact_round_trip( + tmp_path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Backend-only workers can restore the registry from the frontend build.""" + monkeypatch.setattr(utils, "get_web_dir", lambda: tmp_path) + with RegistrationContext() as context: + context.bundled_libraries.append("@radix-ui/themes") + output_path, output = utils._compile_bundled_libraries() + (tmp_path / output_path).write_text(output, encoding="utf-8") + context.bundled_libraries[:] = ["react"] + + utils._restore_bundled_libraries() + + assert context.bundled_libraries == [ + "react", + "@emotion/react", + "$/utils/context", + "$/utils/state", + "@radix-ui/themes", + ] + + def test_document_preloads_the_global_stylesheet(): """Render-blocking CSS should be discoverable alongside early resource hints.""" head = create_document_root().children[0] From 8ba0d062a8bdf8710e4ba6877153dce4c3a93775 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 12 Sep 2026 19:08:54 +0530 Subject: [PATCH 2/7] chore: add reflex-base release note --- packages/reflex-base/news/7096.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/reflex-base/news/7096.bugfix.md diff --git a/packages/reflex-base/news/7096.bugfix.md b/packages/reflex-base/news/7096.bugfix.md new file mode 100644 index 00000000000..9a78cd12ad0 --- /dev/null +++ b/packages/reflex-base/news/7096.bugfix.md @@ -0,0 +1 @@ +Persist the bundled-library registry used by backend-only workers when serializing state hydration data. From bfa4e315dedd6d15095afcb306e159b23e75fd59 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 12 Sep 2026 19:20:20 +0530 Subject: [PATCH 3/7] style: satisfy pre-commit checks --- reflex/compiler/compiler.py | 8 ++++---- tests/units/compiler/test_compiler_utils.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index fa56b24b993..487b3845963 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -1465,14 +1465,14 @@ def add_save_task( compile_results.append(result) progress.advance(task) - compile_results.append( + compile_results.extend([ compile_contexts( app._state, radix_themes_plugin.get_theme(), component_imports=all_imports, - ) - ) - compile_results.append(utils._compile_bundled_libraries()) + ), + utils._compile_bundled_libraries(), + ]) progress.advance(task) compile_results.append(compile_app_root(app_root, hydrate_fallback_export)) diff --git a/tests/units/compiler/test_compiler_utils.py b/tests/units/compiler/test_compiler_utils.py index 18a3571d0dc..f84d8fed0a5 100644 --- a/tests/units/compiler/test_compiler_utils.py +++ b/tests/units/compiler/test_compiler_utils.py @@ -3,6 +3,7 @@ import asyncio import pytest +from reflex_base.registry import RegistrationContext from reflex_components_core.base.fragment import Fragment from reflex_components_core.base.script import Script from reflex_components_core.el.elements.metadata import Link @@ -11,7 +12,6 @@ from reflex.compiler.utils import compile_state, create_document_root from reflex.compiler.utils import write_file as compiler_write_file from reflex.constants.state import FIELD_MARKER -from reflex_base.registry import RegistrationContext from reflex.state import State from reflex.utils.path_ops import write_file from reflex.vars.base import computed_var From 760fe5979e19b379244abfec8887821fd093ad76 Mon Sep 17 00:00:00 2001 From: Farhan Date: Mon, 14 Sep 2026 23:23:26 +0500 Subject: [PATCH 4/7] fix: preserve backend bundle registrations --- reflex/compiler/utils.py | 7 ++-- tests/units/compiler/test_compiler.py | 39 +++++++++++++++++++-- tests/units/compiler/test_compiler_utils.py | 27 ++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 3884365477e..b5d2beb2f74 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -288,14 +288,15 @@ def _restore_bundled_libraries() -> None: path = get_web_dir() / constants.Dirs.BUNDLED_LIBRARIES try: bundled_libraries = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): + except (OSError, UnicodeDecodeError, json.JSONDecodeError): return if not isinstance(bundled_libraries, list) or not all( isinstance(library, str) for library in bundled_libraries ): return - RegistrationContext.ensure_context().bundled_libraries[:] = list( - dict.fromkeys(bundled_libraries) + context = RegistrationContext.ensure_context() + context.bundled_libraries[:] = list( + dict.fromkeys([*bundled_libraries, *context.bundled_libraries]) ) diff --git a/tests/units/compiler/test_compiler.py b/tests/units/compiler/test_compiler.py index 922c728437b..bef4d7740c1 100644 --- a/tests/units/compiler/test_compiler.py +++ b/tests/units/compiler/test_compiler.py @@ -28,6 +28,7 @@ import reflex as rx from reflex.compiler import compiler, utils from reflex.state import BaseState +from reflex.utils import prerequisites @pytest.mark.parametrize( @@ -874,9 +875,7 @@ def test_compile_registers_plugin_routes_on_backend_early_return( mocker.patch.object(app, "_should_compile", return_value=False) compile_page = mocker.patch.object(app, "_compile_page") mocker.patch.object(app, "_add_optional_endpoints") - mocker.patch.object( - compiler.prerequisites, "get_backend_dir", return_value=tmp_path - ) + mocker.patch.object(prerequisites, "get_backend_dir", return_value=tmp_path) mocker.patch.object(compiler, "get_config", return_value=config) if with_stateful_marker: @@ -892,6 +891,40 @@ def test_compile_registers_plugin_routes_on_backend_early_return( compile_page.assert_not_called() +@pytest.mark.usefixtures("clean_registration_context") +def test_backend_compile_restores_registry_before_initial_state_serialization( + tmp_path: Path, mocker: MockerFixture +): + """Backend-only compilation restores frontend metadata before serializing state.""" + + class BackendState(rx.State): + """State used to exercise backend-only compilation.""" + + app = rx.App(_state=BackendState) + mocker.patch.object(app, "_apply_decorated_pages") + mocker.patch.object(app, "_should_compile", return_value=False) + mocker.patch.object(app, "_add_optional_endpoints") + mocker.patch.object(prerequisites, "get_backend_dir", return_value=tmp_path) + mocker.patch.object( + compiler, "get_config", return_value=rx.Config(app_name="testing", plugins=[]) + ) + calls = [] + restore = mocker.patch.object( + utils, "_restore_bundled_libraries", side_effect=lambda: calls.append("restore") + ) + compile_initial_state = mocker.patch.object( + utils, + "_compile_initial_state", + side_effect=lambda _state: calls.append("initial_state"), + ) + + assert compiler.compile_app(app, use_rich=False) is False + + assert restore.call_args_list == [mocker.call()] + assert compile_initial_state.call_args_list == [mocker.call(BackendState)] + assert calls == ["restore", "initial_state"] + + def test_register_plugin_routes_exposes_app_type_not_mutable_app(): """The hook can validate the app class without bypassing staged writes.""" observed: dict[str, object] = {} diff --git a/tests/units/compiler/test_compiler_utils.py b/tests/units/compiler/test_compiler_utils.py index f84d8fed0a5..f7e982024a2 100644 --- a/tests/units/compiler/test_compiler_utils.py +++ b/tests/units/compiler/test_compiler_utils.py @@ -44,6 +44,33 @@ def test_bundled_libraries_artifact_round_trip( ] +def test_restore_bundled_libraries_preserves_page_registrations( + tmp_path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Restoring frontend metadata retains libraries discovered by page evaluation.""" + monkeypatch.setattr(utils, "get_web_dir", lambda: tmp_path) + (tmp_path / utils.constants.Dirs.BUNDLED_LIBRARIES).write_text( + '["@radix-ui/themes"]', encoding="utf-8" + ) + with RegistrationContext() as context: + context.bundled_libraries.append("page-library") + + utils._restore_bundled_libraries() + + assert "@radix-ui/themes" in context.bundled_libraries + assert "page-library" in context.bundled_libraries + + +def test_restore_bundled_libraries_ignores_invalid_utf8( + tmp_path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Malformed registry artifacts do not interrupt backend-only startup.""" + monkeypatch.setattr(utils, "get_web_dir", lambda: tmp_path) + (tmp_path / utils.constants.Dirs.BUNDLED_LIBRARIES).write_bytes(b"\xff") + + utils._restore_bundled_libraries() + + def test_document_preloads_the_global_stylesheet(): """Render-blocking CSS should be discoverable alongside early resource hints.""" head = create_document_root().children[0] From 29d48d7d3d2b1958f75a413bc233ef37738df20d Mon Sep 17 00:00:00 2001 From: Farhan Date: Mon, 14 Sep 2026 23:38:26 +0500 Subject: [PATCH 5/7] test: cover bundled library compiler wiring --- tests/units/compiler/test_compiler.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/units/compiler/test_compiler.py b/tests/units/compiler/test_compiler.py index bef4d7740c1..5b02be0ad80 100644 --- a/tests/units/compiler/test_compiler.py +++ b/tests/units/compiler/test_compiler.py @@ -925,6 +925,31 @@ class BackendState(rx.State): assert calls == ["restore", "initial_state"] +@pytest.mark.usefixtures("clean_registration_context") +def test_frontend_compile_emits_bundled_library_registry(mocker: MockerFixture): + """Frontend compilation emits the registry consumed by backend-only workers.""" + app = rx.App() + + def index(): + """Render an empty page. + + Returns: + The empty page. + """ + return rx.el.div() + + app.add_page(index) + emitted_registry = mocker.patch.object( + utils, + "_compile_bundled_libraries", + return_value=(constants.Dirs.BUNDLED_LIBRARIES, "[]"), + ) + + assert compiler.compile_app(app, dry_run=True, use_rich=False) is True + + emitted_registry.assert_called_once_with() + + def test_register_plugin_routes_exposes_app_type_not_mutable_app(): """The hook can validate the app class without bypassing staged writes.""" observed: dict[str, object] = {} From 1b9918ea683da643277ae784db7ee67f13cfc065 Mon Sep 17 00:00:00 2001 From: Farhan Date: Mon, 14 Sep 2026 23:44:41 +0500 Subject: [PATCH 6/7] fix: store bundle registry with backend artifacts --- .../reflex-base/src/reflex_base/constants/base.py | 8 ++++---- tests/units/compiler/test_compiler_utils.py | 14 +++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/constants/base.py b/packages/reflex-base/src/reflex_base/constants/base.py index 02fcea0f133..9387ec6d9c9 100644 --- a/packages/reflex-base/src/reflex_base/constants/base.py +++ b/packages/reflex-base/src/reflex_base/constants/base.py @@ -54,18 +54,18 @@ class Dirs(SimpleNamespace): # module, kept separate from other ``.web`` output so a mirrored module # path can't collide with framework files (e.g. ``app/``, ``utils/``). APP_COMPONENTS = "app_components" + # Where compilation artifacts for the backend are stored. + BACKEND = "backend" # The name of the env json file. ENV_JSON = "env.json" - # The name of the compiled bundled-library registry. - BUNDLED_LIBRARIES = "bundled_libraries.json" + # The compiled bundled-library registry consumed by backend-only workers. + BUNDLED_LIBRARIES = BACKEND + "/bundled_libraries.json" # The name of the reflex json file. REFLEX_JSON = "reflex.json" # The name of the postcss config file. POSTCSS_JS = "postcss.config.js" # The name of the states directory. STATES = ".states" - # Where compilation artifacts for the backend are stored. - BACKEND = "backend" # JSON-encoded list of page routes that need to be evaluated on the backend. STATEFUL_PAGES = "stateful_pages.json" # Marker file indicating that upload component was used in the frontend. diff --git a/tests/units/compiler/test_compiler_utils.py b/tests/units/compiler/test_compiler_utils.py index f7e982024a2..7818d78fcde 100644 --- a/tests/units/compiler/test_compiler_utils.py +++ b/tests/units/compiler/test_compiler_utils.py @@ -30,7 +30,9 @@ def test_bundled_libraries_artifact_round_trip( with RegistrationContext() as context: context.bundled_libraries.append("@radix-ui/themes") output_path, output = utils._compile_bundled_libraries() - (tmp_path / output_path).write_text(output, encoding="utf-8") + artifact_path = tmp_path / output_path + artifact_path.parent.mkdir() + artifact_path.write_text(output, encoding="utf-8") context.bundled_libraries[:] = ["react"] utils._restore_bundled_libraries() @@ -49,9 +51,9 @@ def test_restore_bundled_libraries_preserves_page_registrations( ) -> None: """Restoring frontend metadata retains libraries discovered by page evaluation.""" monkeypatch.setattr(utils, "get_web_dir", lambda: tmp_path) - (tmp_path / utils.constants.Dirs.BUNDLED_LIBRARIES).write_text( - '["@radix-ui/themes"]', encoding="utf-8" - ) + artifact_path = tmp_path / utils.constants.Dirs.BUNDLED_LIBRARIES + artifact_path.parent.mkdir() + artifact_path.write_text('["@radix-ui/themes"]', encoding="utf-8") with RegistrationContext() as context: context.bundled_libraries.append("page-library") @@ -66,7 +68,9 @@ def test_restore_bundled_libraries_ignores_invalid_utf8( ) -> None: """Malformed registry artifacts do not interrupt backend-only startup.""" monkeypatch.setattr(utils, "get_web_dir", lambda: tmp_path) - (tmp_path / utils.constants.Dirs.BUNDLED_LIBRARIES).write_bytes(b"\xff") + artifact_path = tmp_path / utils.constants.Dirs.BUNDLED_LIBRARIES + artifact_path.parent.mkdir() + artifact_path.write_bytes(b"\xff") utils._restore_bundled_libraries() From ad0f7fcad5fe84235a0fb2c56cf4a08522e47764 Mon Sep 17 00:00:00 2001 From: Farhan Date: Tue, 15 Sep 2026 00:05:12 +0500 Subject: [PATCH 7/7] test: cover invalid bundle registry artifacts --- tests/units/compiler/test_compiler_utils.py | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/units/compiler/test_compiler_utils.py b/tests/units/compiler/test_compiler_utils.py index 7818d78fcde..879dafb18b3 100644 --- a/tests/units/compiler/test_compiler_utils.py +++ b/tests/units/compiler/test_compiler_utils.py @@ -75,6 +75,35 @@ def test_restore_bundled_libraries_ignores_invalid_utf8( utils._restore_bundled_libraries() +@pytest.mark.parametrize( + "contents", + ["{", '"@radix-ui/themes"', '["@radix-ui/themes", 1]'], +) +def test_restore_bundled_libraries_ignores_invalid_json( + tmp_path, monkeypatch: pytest.MonkeyPatch, contents: str +) -> None: + """Malformed registry data does not change backend registrations.""" + monkeypatch.setattr(utils, "get_web_dir", lambda: tmp_path) + artifact_path = tmp_path / utils.constants.Dirs.BUNDLED_LIBRARIES + artifact_path.parent.mkdir() + artifact_path.write_text(contents, encoding="utf-8") + with RegistrationContext() as context: + original = list(context.bundled_libraries) + + utils._restore_bundled_libraries() + + assert context.bundled_libraries == original + + +def test_restore_bundled_libraries_ignores_missing_artifact( + tmp_path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A missing frontend artifact does not interrupt backend-only startup.""" + monkeypatch.setattr(utils, "get_web_dir", lambda: tmp_path) + + utils._restore_bundled_libraries() + + def test_document_preloads_the_global_stylesheet(): """Render-blocking CSS should be discoverable alongside early resource hints.""" head = create_document_root().children[0]