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
1 change: 1 addition & 0 deletions packages/reflex-base/news/6628.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pyi_generator no longer includes underscore-prefixed props in generated .pyi files.
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ def _extract_class_props_as_ast_nodes(
or name in all_props
or name in event_triggers
or get_origin(value) is ClassVar
or name.startswith("_")
):
continue
all_props.add(name)
Expand Down
1 change: 1 addition & 0 deletions packages/reflex-components-lucide/news/6628.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`rx.icon` with a static name now emits a deep per-icon import (`import LucideWifiOff from "lucide-react/dist/esm/icons/wifi-off.mjs"`) instead of a named import from the `lucide-react` barrel. The barrel import was harmless on its own, but once an app's module graph also contained `lucide-react/dynamic.mjs` (pulled in by dynamic `Var`-tagged icons, and by the connection overlay mounted on every page), esbuild's dev dep-optimizer rewrote the barrel to statically import every icon chunk — making each page load fetch the entire ~1700-icon library. Deep imports load only the icons actually used. The dynamic `Var` path still resolves through `lucide-react/dynamic.mjs` and is unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class Icon(LucideIconComponent):

size: Var[int] = field(doc="The size of the icon in pixels.")

# Deep-import subpath into lucide-react for this icon (e.g.
# "/dist/esm/icons/wifi-off.mjs"). Set during ``create``; not a rendered prop.
_import_path: str = field(default="")

@classmethod
def create(cls, *children, **props) -> Component:
"""Initialize the Icon component.
Expand Down Expand Up @@ -83,8 +87,35 @@ def create(cls, *children, **props) -> Component:

props["tag"] = LUCIDE_ICON_MAPPING_OVERRIDE.get(tag, format.to_title_case(tag))
props["alias"] = f"Lucide{props['tag']}"
file_name = LUCIDE_ICON_FILENAME_OVERRIDE.get(tag, format.to_kebab_case(tag))
props["_import_path"] = f"/dist/esm/icons/{file_name}.mjs"
return super().create(**props)

def _get_imports(self):
"""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
the whole library into the dev module graph once ``DynamicIcon`` (which
imports ``lucide-react/dynamic.mjs``) is also present, flooding the page
with a request per icon. A deep default import from the icon's own
module avoids this and is lucide's recommended form.

Returns:
The imports needed by the component.
"""
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_


class DynamicIcon(LucideIconComponent):
"""A DynamicIcon component."""
Expand Down Expand Up @@ -1834,3 +1865,17 @@ def _get_imports(self):
"grid_3x2": "Grid3x2Icon",
"fingerprint": "FingerprintPattern",
}

# For deep per-icon imports, the kebab-case icon name must match Lucide's own
# file name under dist/esm/icons. These are the entries where the default
# kebab conversion doesn't produce the correct file name (verified against the
# full LUCIDE_ICON_LIST): the digit-x-digit names gain a stray hyphen, and
# "fingerprint" is an alias of the "fingerprint-pattern" module.
LUCIDE_ICON_FILENAME_OVERRIDE = {
"fingerprint": "fingerprint-pattern",
"grid_2x_2": "grid-2x2",
"grid_2x_2_check": "grid-2x2-check",
"grid_2x_2_plus": "grid-2x2-plus",
"grid_2x_2_x": "grid-2x2-x",
"grid_3x_3": "grid-3x3",
}
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": "1074a512195ae23d479c4a2d553954e1",
"packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.pyi": "8e379fa038c7c6c0672639eb5902934d",
"packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "d2dc211d707c402eb24678a4cba945f7",
"packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "b692058e40b15da293fbf463ad300a83",
"packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "d16d77881afaae71578177db4d479c13",
"packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "e04f22f5d3d2b5dfd99f9fbedb2b4f3d",
"packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "d6a02e447dfd3c91bba84bcd02722aed",
"packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "91e956633778c6992f04940c69ff7140",
Expand Down
84 changes: 84 additions & 0 deletions tests/units/components/lucide/test_icon.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import pytest
from reflex_base.utils import format
from reflex_base.vars.base import Var
from reflex_components_lucide.icon import (
LUCIDE_ICON_FILENAME_OVERRIDE,
LUCIDE_ICON_LIST,
LUCIDE_ICON_MAPPING_OVERRIDE,
LUCIDE_LIBRARY,
DynamicIcon,
Icon,
)

Expand All @@ -15,6 +19,86 @@ def test_icon(tag):
)


def _lucide_imports(component):
return [
var
for lib, vars_ in component._get_imports().items()
if "lucide-react" in lib
for var in vars_
]


def test_static_icon_deep_import():
"""Static string icons import from a deep per-icon module, not the barrel."""
icon = Icon.create("wifi_off")
(import_var,) = _lucide_imports(icon)
assert import_var.package_path == "/dist/esm/icons/wifi-off.mjs"
assert import_var.is_default
assert import_var.alias == "LucideWifiOff"
# The barrel import (package_path "/") must not be emitted.
assert import_var.package_path != "/"


@pytest.mark.parametrize("tag", LUCIDE_ICON_LIST)
def test_static_icon_import_path(tag):
"""Every icon maps to a deep import using its kebab-case file name."""
icon = Icon.create(tag)
(import_var,) = _lucide_imports(icon)
file_name = LUCIDE_ICON_FILENAME_OVERRIDE.get(tag, format.to_kebab_case(tag))
assert import_var.package_path == f"/dist/esm/icons/{file_name}.mjs"
assert import_var.is_default


# Literal expected paths — independent of LUCIDE_ICON_FILENAME_OVERRIDE and the
# kebab helper — so a typo in an override value (the hand-maintained, drift-prone
# part) is actually caught instead of being mirrored on both sides of the assert.
# The override file names were verified to exist under lucide-react's
# dist/esm/icons; the plain entries spot-check the default kebab conversion.
@pytest.mark.parametrize(
("tag", "expected_path"),
[
("fingerprint", "/dist/esm/icons/fingerprint-pattern.mjs"),
("grid_2x_2", "/dist/esm/icons/grid-2x2.mjs"),
("grid_2x_2_check", "/dist/esm/icons/grid-2x2-check.mjs"),
("grid_2x_2_plus", "/dist/esm/icons/grid-2x2-plus.mjs"),
("grid_2x_2_x", "/dist/esm/icons/grid-2x2-x.mjs"),
("grid_3x_3", "/dist/esm/icons/grid-3x3.mjs"),
# grid_3x2 is NOT an override — its kebab file name already matches.
("grid_3x2", "/dist/esm/icons/grid-3x2.mjs"),
("wifi_off", "/dist/esm/icons/wifi-off.mjs"),
("circle_help", "/dist/esm/icons/circle-help.mjs"),
("activity", "/dist/esm/icons/activity.mjs"),
],
)
def test_static_icon_import_path_literal(tag, expected_path):
"""Spot-check concrete import paths, especially the file-name overrides."""
(import_var,) = _lucide_imports(Icon.create(tag))
assert import_var.package_path == expected_path


def test_dynamic_icon_uses_dynamic_module():
"""Var-tagged icons still resolve through lucide-react/dynamic.mjs."""
icon = Icon.create(Var("state_icon").to(str))
Comment thread
adhami3310 marked this conversation as resolved.
assert isinstance(icon, DynamicIcon)
(import_var,) = _lucide_imports(icon)
assert import_var.tag == "DynamicIcon"
assert import_var.package_path == "/dynamic.mjs"
assert not import_var.is_default


def test_static_icon_import_idempotent():
"""Repeated import computation (cache hits) yields the same deep import."""
icon = Icon.create("wifi_off")
first = _lucide_imports(icon)
second = _lucide_imports(icon)
assert first == second
assert all(var.package_path == "/dist/esm/icons/wifi-off.mjs" for var in second)


def test_lucide_library_constant():
assert LUCIDE_LIBRARY.startswith("lucide-react")


def test_icon_missing_tag():
with pytest.raises(AttributeError):
_ = Icon.create()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ class StrictComponent(Component):
def create(
cls,
*children,
_internal_counter: int | None = None,
visible_prop: Var[str] | str | None = None,
size: Var[int] | int | None = None,
style: Sequence[Mapping[str, Any]]
Expand Down Expand Up @@ -52,7 +51,6 @@ class StrictComponent(Component):

Args:
*children: The children of the component.
_internal_counter: no description
visible_prop: A prop visible in the stub.
size: The size of the component.
style: The style of the component.
Expand Down
Loading