From a8c2a6a402d22cf11a724251fa04b626f3589563 Mon Sep 17 00:00:00 2001 From: Sanath Date: Mon, 19 Oct 2026 21:18:57 +0530 Subject: [PATCH 1/2] fix: expose deep Lucide imports through import_var --- .../news/+deep-import-metadata.bugfix.md | 1 + .../src/reflex_components_lucide/icon.py | 37 ++++------ pyi_hashes.json | 2 +- tests/integration/test_dynamic_components.py | 12 ++++ tests/units/components/lucide/test_icon.py | 72 ++++++++++++++++++- 5 files changed, 100 insertions(+), 24 deletions(-) create mode 100644 packages/reflex-components-lucide/news/+deep-import-metadata.bugfix.md diff --git a/packages/reflex-components-lucide/news/+deep-import-metadata.bugfix.md b/packages/reflex-components-lucide/news/+deep-import-metadata.bugfix.md new file mode 100644 index 00000000000..f92e112c6a9 --- /dev/null +++ b/packages/reflex-components-lucide/news/+deep-import-metadata.bugfix.md @@ -0,0 +1 @@ +Preserve imports required by icon props and expose the correct Lucide module through `import_var`. diff --git a/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py b/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py index 9a206baec82..cad926e46ba 100644 --- a/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py +++ b/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py @@ -1,5 +1,6 @@ """Lucide Icon component.""" +import dataclasses import logging from reflex_base.components.component import Component, field @@ -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 @@ -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): @@ -129,14 +122,14 @@ 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, package_path="/dynamic.mjs") LUCIDE_ICON_LIST = [ diff --git a/pyi_hashes.json b/pyi_hashes.json index 40fba0d5829..e82e6aaed94 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -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", diff --git a/tests/integration/test_dynamic_components.py b/tests/integration/test_dynamic_components.py index 9061dad9ab7..b90ae422182 100644 --- a/tests/integration/test_dynamic_components.py +++ b/tests/integration/test_dynamic_components.py @@ -243,6 +243,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") ) @@ -266,6 +275,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() diff --git a/tests/units/components/lucide/test_icon.py b/tests/units/components/lucide/test_icon.py index d813e43ae8c..9161cca63df 100644 --- a/tests/units/components/lucide/test_icon.py +++ b/tests/units/components/lucide/test_icon.py @@ -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, @@ -86,6 +87,75 @@ 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 + + +def test_dynamic_icon_import_var(): + """Expose the named dynamic module import through the component API.""" + assert DynamicIcon.create(name=Var("icon_name").to(str)).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 = icon._get_imports() + 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") From 0215ea166dce197170b5f7db23be0a0a0f7244cf Mon Sep 17 00:00:00 2001 From: Sanath Date: Mon, 19 Oct 2026 21:54:50 +0530 Subject: [PATCH 2/2] fix: preserve named dynamic Lucide imports --- .../src/reflex_components_lucide/icon.py | 4 +++- tests/integration/test_dynamic_components.py | 13 ++++++---- tests/units/components/lucide/test_icon.py | 24 +++++++++++++++---- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py b/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py index cad926e46ba..71cd3b8831f 100644 --- a/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py +++ b/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py @@ -129,7 +129,9 @@ def import_var(self) -> ImportVar: Returns: The named dynamic module import. """ - return dataclasses.replace(super().import_var, package_path="/dynamic.mjs") + return dataclasses.replace( + super().import_var, is_default=False, package_path="/dynamic.mjs" + ) LUCIDE_ICON_LIST = [ diff --git a/tests/integration/test_dynamic_components.py b/tests/integration/test_dynamic_components.py index b90ae422182..acbee4cc3d5 100644 --- a/tests/integration/test_dynamic_components.py +++ b/tests/integration/test_dynamic_components.py @@ -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 @@ -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 diff --git a/tests/units/components/lucide/test_icon.py b/tests/units/components/lucide/test_icon.py index 9161cca63df..033f42bd6c2 100644 --- a/tests/units/components/lucide/test_icon.py +++ b/tests/units/components/lucide/test_icon.py @@ -129,11 +129,22 @@ def test_static_icon_import_var(tag: str, expected: ImportVar): assert Icon.create(tag).import_var == expected -def test_dynamic_icon_import_var(): - """Expose the named dynamic module import through the component API.""" - assert DynamicIcon.create(name=Var("icon_name").to(str)).import_var == ImportVar( - "DynamicIcon", package_path="/dynamic.mjs" +@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]) @@ -150,7 +161,10 @@ def test_icon_keeps_var_imports(reactive: bool): _var_data=VarData(imports={LUCIDE_LIBRARY: [required]}), ) icon = Icon.create(Var("icon_name").to(str) if reactive else "apple", size=size) - first = icon._get_imports() + 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