Skip to content
Open
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 news/7016.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Explicit `bundle_library()` registrations now survive frontend compilation instead of being discarded when compiler plugin dependencies are collected.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
1 change: 1 addition & 0 deletions packages/reflex-base/news/7016.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dynamic components can now use subpath imports from a bundled frontend library, including deep default imports such as static Lucide icons.
94 changes: 88 additions & 6 deletions packages/reflex-base/src/reflex_base/components/dynamic.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,16 @@ def get_cdn_url(lib: str) -> str:

def reset_bundled_libraries() -> None:
"""Reset the bundled library registry to its default values."""
bundled = RegistrationContext.ensure_context().bundled_libraries
bundled[:] = _default_bundled_libraries()
context = RegistrationContext.ensure_context()
context.bundled_libraries[:] = _default_bundled_libraries()
context._explicit_bundled_libraries.clear()


def _reset_bundled_libraries_for_compile() -> None:
"""Reset derived libraries while preserving explicit registrations."""
context = RegistrationContext.ensure_context()
context.bundled_libraries[:] = _default_bundled_libraries()
context.bundled_libraries.extend(context._explicit_bundled_libraries)


def bundle_library(component: Union["Component", str]):
Expand All @@ -84,14 +92,42 @@ def bundle_library(component: Union["Component", str]):
Raises:
DynamicComponentMissingLibraryError: Raised when a dynamic component is missing a library.
"""
bundled = RegistrationContext.ensure_context().bundled_libraries
_bundle_library(component, explicit=True)


def _bundle_library(
component: Union["Component", str], *, explicit: bool = False
) -> None:
"""Register a library for the current compile.

Args:
component: The component or library to bundle.
explicit: Whether this is an application-level registration that should
survive compiler resets.

Raises:
DynamicComponentMissingLibraryError: Raised when a dynamic component is missing a library.
"""
context = RegistrationContext.ensure_context()
bundled = context.bundled_libraries
if isinstance(component, str):
bundled.append(format_library_name(component))
library = format_library_name(component)
bundled.append(library)
if explicit:
context._explicit_bundled_libraries.append(library)
return
if component.library is None:
msg = "Component must have a library to bundle."
raise DynamicComponentMissingLibraryError(msg)
bundled.append(format_library_name(component.library))
library = format_library_name(component.library)
bundled.append(library)
if explicit:
context._explicit_bundled_libraries.append(library)


_BUNDLED_IMPORT_LIB = "lib"
_BUNDLED_IMPORT_DEFAULT = "default"
_BUNDLED_IMPORT_REST = "rest"


def load_dynamic_serializer():
Expand Down Expand Up @@ -138,6 +174,7 @@ def make_component(component: Component) -> str:
compiler._apply_common_imports(component_imports)

imports = {}
bundled_subpath_imports: set[str] = set()
for lib, names in component_imports.items():
formatted_lib_name = format_library_name(lib)
if (
Expand All @@ -148,9 +185,46 @@ def make_component(component: Component) -> str:
imports[get_cdn_url(lib)] = names
else:
imports[lib] = names
if formatted_lib_name in libs_in_window:
for name in names:
if name.package_path in {"/", ""}:
continue
import_path = formatted_lib_name + name.package_path
_bundle_library(import_path)
bundled_subpath_imports.add(import_path)

compiled_imports = utils.compile_imports(imports)
bundled_subpath_rewrites = {}
for module in compiled_imports:
if module[_BUNDLED_IMPORT_LIB] not in bundled_subpath_imports:
continue

window_library = f"window.__reflex['{module[_BUNDLED_IMPORT_LIB]}']"
statements = []
if module[_BUNDLED_IMPORT_DEFAULT]:
statements.append(
f"const {module[_BUNDLED_IMPORT_DEFAULT]} = "
f"{window_library}.default"
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.

named_imports = []
for imported_name in module[_BUNDLED_IMPORT_REST]:
if imported_name.startswith("* as "):
Comment thread
greptile-apps[bot] marked this conversation as resolved.
statements.append(
f"const {imported_name.removeprefix('* as ')} = {window_library}"
)
else:
named_imports.append(imported_name.replace(" as ", ": "))
if named_imports:
statements.append(
f"const {{{','.join(named_imports)}}} = {window_library}"
)

if statements:
bundled_subpath_rewrites[module["lib"]] = "\n".join(statements)

module_code_lines = templates.dynamic_components_module_template(
imports=utils.compile_imports(imports),
imports=compiled_imports,
memoized_code="\n".join(rendered_components),
).splitlines()

Expand All @@ -166,6 +240,14 @@ def make_component(component: Component) -> str:
+ "]"
)
else:
subpath_rewritten = False
for import_path, replacement in bundled_subpath_rewrites.items():
if line.endswith(f'from "{import_path}"'):
module_code_lines[ix] = replacement
subpath_rewritten = True
break
if subpath_rewritten:
continue
for lib in libs_in_window:
if f'from "{lib}"' in line:
module_code_lines[ix] = (
Expand Down
5 changes: 5 additions & 0 deletions packages/reflex-base/src/reflex_base/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class RegistrationContext(BaseContext):
default_factory=_default_bundled_libraries,
repr=False,
)
_explicit_bundled_libraries: list[str] = dataclasses.field(
default_factory=list,
repr=False,
)
_app: App | None = dataclasses.field(default=None, repr=False)

@property
Expand Down Expand Up @@ -140,6 +144,7 @@ def fork(self) -> Self:
},
decorated_pages=list(self.decorated_pages),
bundled_libraries=list(self.bundled_libraries),
_explicit_bundled_libraries=list(self._explicit_bundled_libraries),
)

def _set_config(self, config: Config) -> None:
Expand Down
1 change: 1 addition & 0 deletions packages/reflex-components-radix/news/7016.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Radix Themes auto-enablement now avoids retaining stale frontend library registrations across recompiles.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import dataclasses
from typing import TYPE_CHECKING, Any

import reflex_base.components.dynamic as dynamic_components
from reflex_base.components.component import BaseComponent, Component
from reflex_base.components.dynamic import bundle_library
from reflex_base.plugins.base import Plugin
from reflex_base.utils import console

Expand All @@ -23,6 +23,14 @@
_REMOVAL_VERSION = "1.0"


def _bundle_library_for_compile(library: str) -> None:
"""Register a compile-derived library across supported reflex-base versions."""
bundle_library = getattr(
dynamic_components, "_bundle_library", dynamic_components.bundle_library
)
bundle_library(library)


@dataclasses.dataclass
class RadixThemesPlugin(Plugin):
"""Opt-in plugin for Radix Themes assets and app-level wrapping."""
Expand Down Expand Up @@ -67,7 +75,7 @@ def enter_component(
return

self.enabled = True
bundle_library(RADIX_THEMES_PACKAGE)
_bundle_library_for_compile(RADIX_THEMES_PACKAGE)
if not self._explicit and not self._app_theme_warning_emitted:
console.deprecate(
feature_name="Implicit Radix Themes enablement",
Expand Down
55 changes: 44 additions & 11 deletions reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,42 @@ def _normalize_library_name(lib: str) -> str:
"""
if lib == "react":
return "React"
return lib.replace("$/", "").replace("@", "").replace("/", "_").replace("-", "_")
return (
lib
.replace("$/", "")
.replace("@", "")
.replace("/", "_")
.replace("-", "_")
.replace(".", "_")
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
)


def _get_window_libraries() -> list[tuple[str, str]]:
"""Build unique aliases for libraries exposed through ``window.__reflex``.

Returns:
Library aliases paired with their original package names.
"""
used_aliases: set[str] = set()
seen_libraries: set[str] = set()
window_libraries: list[tuple[str, str]] = []

for library in RegistrationContext.ensure_context().bundled_libraries:
if library in seen_libraries:
continue
seen_libraries.add(library)

base_alias = _normalize_library_name(library)
alias = base_alias
suffix = 2
while alias in used_aliases:
alias = f"{base_alias}_{suffix}"
suffix += 1

used_aliases.add(alias)
window_libraries.append((alias, library))

return window_libraries


def _compile_app(
Expand All @@ -146,12 +181,7 @@ def _compile_app(
Returns:
The compiled app.
"""
window_libraries = [
(_normalize_library_name(name), name)
for name in RegistrationContext.ensure_context().bundled_libraries
]

window_libraries_deduped = list(dict.fromkeys(window_libraries))
window_libraries = _get_window_libraries()

app_root_imports = app_root._get_all_imports()
_apply_common_imports(app_root_imports)
Expand All @@ -160,7 +190,7 @@ def _compile_app(
imports=utils.compile_imports(app_root_imports),
custom_codes=app_root._get_all_custom_code(),
hooks=app_root._get_all_hooks(),
window_libraries=window_libraries_deduped,
window_libraries=window_libraries,
render=app_root.render(),
dynamic_imports=app_root._get_all_dynamic_imports(),
hydrate_fallback_export=hydrate_fallback_export,
Expand Down Expand Up @@ -1164,7 +1194,10 @@ def compile_app(
``True`` when a real frontend compile ran, ``False`` when the call
short-circuited (backend-only paths that only re-evaluate pages).
"""
from reflex_base.components.dynamic import bundle_library, reset_bundled_libraries
from reflex_base.components.dynamic import (
_bundle_library,
_reset_bundled_libraries_for_compile,
)
from reflex_base.utils.exceptions import ReflexRuntimeError

app._apply_decorated_pages()
Expand Down Expand Up @@ -1206,14 +1239,14 @@ def compile_app(
app,
config.plugins,
)
reset_bundled_libraries()
_reset_bundled_libraries_for_compile()
# Drop cached memo wrapper classes so each compile recomputes a memo's
# ``library`` from the current module layout (handles a module flipping to
# a package across hot reloads).
reset_memo_component_classes()
for plugin in compiler_plugins:
for dependency in plugin.get_frontend_dependencies():
bundle_library(dependency)
_bundle_library(dependency)
base_total = (len(app._unevaluated_pages) * 2) + fixed_steps + len(config.plugins)
progress.start()
task = progress.add_task("Compiling:", total=base_total)
Expand Down
17 changes: 17 additions & 0 deletions tests/integration/test_dynamic_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
def DynamicComponents():
"""App with var operations."""
import reflex as rx
from reflex.components.dynamic import bundle_library

bundle_library("lucide-react")

class DynamicComponentsState(rx.State):
value: int = 10
Expand Down Expand Up @@ -84,6 +87,15 @@ def counter_component(self) -> rx.Component:
),
)

@rx.var
def icon_component(self) -> rx.Component:
"""Get a dynamic component with a bundled-library subpath import.

Returns:
A Lucide icon component.
"""
return rx.icon("apple", id="dynamic-icon")

app = rx.App()

def factorial(n: int) -> int:
Expand All @@ -97,6 +109,7 @@ def index():
DynamicComponentsState.client_token_component,
DynamicComponentsState.button,
DynamicComponentsState.counter_component,
DynamicComponentsState.icon_component,
rx.text(
DynamicComponentsState._evaluate(
lambda state: factorial(state.value), of_type=int
Expand Down Expand Up @@ -176,6 +189,10 @@ def test_dynamic_components(driver, dynamic_components: AppHarness):
)
assert factorial.text == "3628800"

assert AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "dynamic-icon")
)

count = AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "count")
)
Expand Down
49 changes: 49 additions & 0 deletions tests/units/compiler/test_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,55 @@ def test_compile_app_root_includes_radix_window_library_when_bundled():
reset_bundled_libraries()


def test_compile_app_preserves_user_bundled_libraries(
tmp_path: Path, mocker: MockerFixture
):
"""Frontend compilation should retain explicit bundle registrations."""

class StopCompile(Exception):
"""Stop after bundled libraries are prepared."""

with RegistrationContext() as registration_context:
bundle_library("lucide-react")
registration_context.bundled_libraries.append("stale-plugin")
app = rx.App()
config = rx.Config(app_name="testing", plugins=[])

mocker.patch.object(app, "_apply_decorated_pages")
mocker.patch.object(app, "_should_compile", return_value=True)
mocker.patch.object(compiler, "get_config", return_value=config)
mocker.patch.object(
compiler.prerequisites, "get_backend_dir", return_value=tmp_path
)
mocker.patch.object(compiler, "_register_plugin_routes")
mocker.patch.object(
compiler,
"_resolve_radix_themes_plugin",
return_value=([], mocker.Mock()),
)
mocker.patch.object(compiler, "CompileContext", side_effect=StopCompile)

with pytest.raises(StopCompile):
compiler.compile_app(app, dry_run=True, use_rich=False)

assert "lucide-react" in registration_context.bundled_libraries
assert "stale-plugin" not in registration_context.bundled_libraries


def test_compile_app_root_uses_unique_window_library_aliases():
"""Bundled library aliases should remain unique after normalization."""
with RegistrationContext():
bundle_library("foo.bar")
bundle_library("foo_bar")

_, code = compiler.compile_app_root(rx.el.div("hello"))

assert 'import * as foo_bar from "foo.bar";' in code
assert 'import * as foo_bar_2 from "foo_bar";' in code
assert '"foo.bar": foo_bar' in code
assert '"foo_bar": foo_bar_2' in code


def test_compile_contexts_has_default_color_mode_context():
"""ColorModeContext should have a safe fallback value without Radix."""
_, code = compiler.compile_contexts(None, None)
Expand Down
Loading
Loading