Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Preserve imports required by icon props and expose the correct Lucide module through `import_var`.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Lucide Icon component."""

import dataclasses
import logging

from reflex_base.components.component import Component, field
Expand Down Expand Up @@ -95,7 +96,8 @@ def create(cls, *children, **props) -> Component:
props["_import_path"] = f"/dist/esm/icons/{file_name}.mjs"
return super().create(**props)

def _get_imports(self):
@property
def import_var(self) -> ImportVar:
"""Emit a deep per-icon import instead of a lucide-react barrel import.

Importing a static icon by name from the ``lucide-react`` barrel pulls
Expand All @@ -105,20 +107,11 @@ def _get_imports(self):
module avoids this and is lucide's recommended form.

Returns:
The imports needed by the component.
The deep default import for the icon.
"""
imports_ = super()._get_imports()
if self.library and self._import_path:
imports_.pop(self.library, None)
imports_[LUCIDE_LIBRARY] = [
ImportVar(
self.tag,
is_default=True,
alias=self.alias,
package_path=self._import_path,
)
]
return imports_
return dataclasses.replace(
super().import_var, is_default=True, package_path=self._import_path
)


class DynamicIcon(LucideIconComponent):
Expand All @@ -129,14 +122,16 @@ class DynamicIcon(LucideIconComponent):
name: Var[str]
size: Var[int]

def _get_imports(self):
imports_ = super()._get_imports()
if self.library:
imports_.pop(self.library)
imports_[LUCIDE_LIBRARY] = [
ImportVar("DynamicIcon", package_path="/dynamic.mjs")
]
return imports_
@property
def import_var(self) -> ImportVar:
"""Import the dynamic icon from Lucide's dynamic module.

Returns:
The named dynamic module import.
"""
return dataclasses.replace(
super().import_var, is_default=False, package_path="/dynamic.mjs"
)


LUCIDE_ICON_LIST = [
Expand Down
2 changes: 1 addition & 1 deletion pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"packages/reflex-components-core/src/reflex_components_core/react_router/dom.pyi": "7617635157b816f70354e2db98b38642",
"packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.pyi": "8aedc1629118cd3c0075340a5a0c3356",
"packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "2ce1c076ecf5c2fa4945b4abdbf2f91d",
"packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "1e331a3d6420b97e5b1ce7f63ad53de8",
"packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "0bbc5e33f2c2175c3ddcbf1b08dc8923",
"packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "6759daf6e242e209d9298045c7b4456a",
"packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "13e39f8d2b46bfd76c50cb04062da8ff",
"packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "f411341e59a3065fee964747584f9fcf",
Expand Down
25 changes: 20 additions & 5 deletions tests/integration/test_dynamic_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ def icon_component(self) -> rx.Component:
"""
return rx.hstack(
rx.icon("apple", id="dynamic-icon"),
rx.icon(DynamicComponentsState.icon_name, id="dynamic-named-icon"),
rx.icon(
DynamicComponentsState.icon_name,
id="dynamic-named-icon",
is_default=True,
),
)

@rx.var
Expand Down Expand Up @@ -195,11 +199,10 @@ def driver(dynamic_components: AppHarness):
"""
driver = dynamic_components.frontend()
try:
token_input = AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "token")
)
# wait for the backend connection to send the token
token = dynamic_components.poll_for_value(token_input)
token = AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "token").get_attribute("value")
)
assert token is not None

yield driver
Expand Down Expand Up @@ -243,6 +246,15 @@ def test_dynamic_components(driver, dynamic_components: AppHarness):
lambda icon_id=icon_id: driver.find_element(By.ID, icon_id)
)

bundled = driver.execute_script("return Object.keys(window.__reflex)")
assert "lucide-react" not in bundled
assert {
"lucide-react/dist/esm/icons/apple.mjs",
"lucide-react/dist/esm/icons/banana.mjs",
"lucide-react/dist/esm/icons/tag.mjs",
"lucide-react/dynamic.mjs",
} <= set(bundled)

count = AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "count")
)
Expand All @@ -266,6 +278,9 @@ def test_dynamic_components(driver, dynamic_components: AppHarness):
AppHarness.poll_for_or_raise_timeout(
lambda: driver.find_element(By.ID, "delayed-icon")
)
assert "lucide-react" not in driver.execute_script(
"return Object.keys(window.__reflex)"
)
driver.find_element(By.ID, "delayed-increment").click()
AppHarness.expect(lambda: driver.find_element(By.ID, "delayed-count").text == "1")
driver.find_element(By.ID, "delayed-decrement").click()
Expand Down
86 changes: 85 additions & 1 deletion tests/units/components/lucide/test_icon.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest
from reflex_base.utils import format
from reflex_base.vars.base import Var
from reflex_base.utils.imports import ImportVar
from reflex_base.vars.base import Var, VarData
from reflex_components_lucide.icon import (
LUCIDE_ICON_FILENAME_OVERRIDE,
LUCIDE_ICON_LIST,
Expand Down Expand Up @@ -86,6 +87,89 @@ def test_dynamic_icon_uses_dynamic_module():
assert not import_var.is_default


@pytest.mark.parametrize(
("tag", "expected"),
[
(
"wifi_off",
ImportVar(
"WifiOff",
is_default=True,
alias="LucideWifiOff",
package_path="/dist/esm/icons/wifi-off.mjs",
),
),
(
"fingerprint",
ImportVar(
"FingerprintPattern",
is_default=True,
alias="LucideFingerprintPattern",
package_path="/dist/esm/icons/fingerprint-pattern.mjs",
),
),
(
"grid_2x_2",
ImportVar(
"Grid2X2",
is_default=True,
alias="LucideGrid2X2",
package_path="/dist/esm/icons/grid-2x2.mjs",
),
),
],
)
def test_static_icon_import_var(tag: str, expected: ImportVar):
"""Expose the actual default module import through the component API.

Args:
tag: The static icon name.
expected: Its deep import, including aliases and file-name overrides.
"""
assert Icon.create(tag).import_var == expected


@pytest.mark.parametrize("is_default", [False, True])
@pytest.mark.parametrize("reactive", [False, True])
def test_dynamic_icon_import_var(is_default: bool, reactive: bool):
"""Expose the named dynamic module import through the component API.

Args:
is_default: Whether the caller requests a default import.
reactive: Whether to construct the icon through the reactive factory.
"""
name = Var("icon_name").to(str)
icon = (
Icon.create(name, is_default=is_default)
if reactive
else DynamicIcon.create(name=name, is_default=is_default)
)
assert icon.import_var == ImportVar("DynamicIcon", package_path="/dynamic.mjs")


@pytest.mark.parametrize("reactive", [False, True])
def test_icon_keeps_var_imports(reactive: bool):
"""Assembling icon imports retains other Lucide imports required by props.

Args:
reactive: Whether to render a dynamic icon.
"""
required = ImportVar("icons", alias="lucideIcons")
size = Var(
"Object.keys(lucideIcons).length",
_var_type=int,
_var_data=VarData(imports={LUCIDE_LIBRARY: [required]}),
)
icon = Icon.create(Var("icon_name").to(str) if reactive else "apple", size=size)
first = {
library: list(import_vars)
for library, import_vars in icon._get_imports().items()
}
assert required in first[LUCIDE_LIBRARY]
assert icon.import_var in first[LUCIDE_LIBRARY]
assert icon._get_imports() == first


def test_static_icon_import_idempotent():
"""Repeated import computation (cache hits) yields the same deep import."""
icon = Icon.create("wifi_off")
Expand Down
Loading