fix: emit deep per-icon imports for static lucide icons#6628
Conversation
Static-string `rx.icon` tags compiled to a barrel import
`import { WifiOff } from "lucide-react"`. Once the module graph also
contains `lucide-react/dynamic.mjs` (used by DynamicIcon for Var tags),
esbuild's dep-optimizer emits a per-icon chunk for the dynamic-import map
and rewrites the barrel to statically import every one of them. Since
DefaultOverlayComponents (mounted on every page) barrel-imports WifiOff,
loading any page in dev fired ~1700 requests — the whole icon library.
Emit deep default imports instead, lucide's recommended form:
`import LucideWifiOff from "lucide-react/dist/esm/icons/wifi-off.mjs"`.
lucide-react has no exports map, so the path targets the kebab-case file
directly; the .mjs extension is required for Node/Bun ESM resolution.
The dynamic Var -> DynamicIcon path is left unchanged.
A 6-entry file-name override covers the icons whose kebab name doesn't
match lucide's file (fingerprint, grid_2x_2*, grid_3x_3), verified
against the full icon list.
Fixes #6627
ENG-9721
Merging this PR will not alter performance
Comparing Footnotes
|
WifiOffPulse extends Icon, so it inherits the new _import_path field — regenerate its .pyi hash, which I missed. Move the news fragment from news/6627 (issue number, root) to packages/reflex-components-lucide/news/6628 (PR number, affected package) so the per-package towncrier check finds it.
Greptile SummaryThis PR fixes the ~1700 dev-mode requests-per-page-load regression caused by esbuild's dep-optimizer expanding the
Confidence Score: 5/5Safe to merge — the change is narrowly scoped to import path rewriting, the compiler already handles per-subpath grouping, and the dynamic icon path is untouched. The core change rewrites a single well-understood mechanism (barrel import to deep per-icon default import) that already has an established pattern in DynamicIcon. The compiler's compile_imports groups ImportVar entries by package_path, so multiple icons on a page each receive their own import statement without conflict. The 6-entry LUCIDE_ICON_FILENAME_OVERRIDE is verified against the full icon list and spot-checked with independent literal assertions in the test suite. No files require special attention. Important Files Changed
Reviews (2): Last reviewed commit: "add news for pyi_generator change" | Re-trigger Greptile |
AccordionIcon(Icon) inherits the new _import_path field, so its generated .pyi hash changed too. This was the remaining drift causing pre-commit's update-pyi-files hook to fail. AccordionIcon and WifiOffPulse are the only two Icon subclasses; both stubs are now regenerated.
Address Greptile review on #6628: - test_static_icon_import_path was circular for override entries (built the expected path from the same LUCIDE_ICON_FILENAME_OVERRIDE the code reads). Add test_static_icon_import_path_literal with hardcoded paths, independent of the override dict, so a typo in an override value fails. - Complete the dangling 'doesn't' clause in the override-map comment.
These are "private" and should not form part of the IDE/type hinting documentation. They are still accepted and won't raise any typing errors due to all components _also_ taking `**props`
Problem
Static-string
rx.icontags (e.g.rx.icon("wifi_off")) compile to a barrel import:On its own this is fine (Vite tree-shakes it). But once the module graph also contains
lucide-react/dynamic.mjs— whichrx.iconuses for dynamic / state-Vartags viaDynamicIcon— esbuild's dep-optimizer emits a per-icon chunk for the dynamic-import map and rewrites the barrel to statically import every one of them. SinceDefaultOverlayComponents(the connection banner, mounted on every page) barrel-importsWifiOff, loading any page in dev fired ~1700 requests — the whole icon library.Confirmed empirically: barrel alone = 1 request,
DynamicIconalone = 1 request, both together = 1715.Fixes #6627.
Fix
Emit deep per-icon default imports (lucide's recommended form) for the static path:
lucide-reactships noexportsmap, so the path targets the kebab-case file directly; the.mjsextension is required for the integration harness's Node/Bun ESM resolver (extensionless only resolves under Vite's prod bundler).Var→DynamicIcon(lucide-react/dynamic.mjs) path is unchanged — it needs the runtime map and doesn't flood.DefaultOverlayComponents/ the connection banner (WifiOffPulse) goes through the sameIconcodegen, so it's fixed automatically.fingerprint→fingerprint-pattern, and thegrid_2x_2*/grid_3x_3digit-x-digit names), verified exhaustively against the full 1721-icon list.Testing
dynamic.mjs, idempotency across cache hits.tests/integration/test_icon.pypasses in dev + prod.tests/units/components+tests/units/compilersuites pass; ruff + pyright clean;.pyistubs regenerated.