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..71cd3b8831f 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,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 = [ 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..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 @@ -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") ) @@ -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() diff --git a/tests/units/components/lucide/test_icon.py b/tests/units/components/lucide/test_icon.py index d813e43ae8c..033f42bd6c2 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,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")