You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Nothing checks that an icon: literal reaching a record-reading lucide resolver is a live icons key — four hand-copied resolvers, four local pins, no gate over the population #5633
Split out of #5622 (suggestion 2), where triage fenced it out of the repair deliberately: "That is a new gate over a new population, not a repair, and folding it in would turn a five-name fix into a gate-design card carrying its own evidence burden." Filed unassigned so it can be triaged on its own terms.
The class
lucide retires a spelling by dropping it from the runtime icons record while keeping it as a deprecated named export. A retired name therefore still imports, still type-checks, and still renders wherever it is used as a component — and resolves to null wherever it is used as a string, because the string lookups read that record. Nothing goes red in either direction.
This has now been repaired twice, in two different packages, by two different cards:
Both cards added a local resolvability pin. Neither could add one over the population, because there is no population — just four hand-copied toPascalCase + one-entry iconNameMap ({ Home: 'House' }) implementations, each with its own set of call sites:
The two surfaces disagree about aliases, so the same icon: 'edit' renders correctly in the sidebar and renders nothing in an action bar.
What the gate would have to decide
Recorded because they are the design questions, not implementation detail — this is not a "just add a check" card:
What is the population? Every icon: string literal in packages/** / apps/** is 53 distinct names across 109 sites, and Two more retired lucide spellings reach the icons-record resolver — edit in DetailView's mobile Edit action, smile as the icon renderer's own default — and only one of the four resolver copies is pinned #5622's scan found 15 that are not record keys of which only 4 were defects. The other 11 are false positives of three distinct kinds — a differently-typed icon field holding a Tailwind class (text-amber-500 and eight siblings in app-shell/…/previews/AppNavCanvas.tsx), deliberate negative controls (not-a-real-icon), an authorable-keys fixture that is never resolved (chevron), and ComponentRegistry/schema type names that merely collide with lucide spellings (area-chart, bar-chart, line-chart, pie-chart, scatter-chart). A gate that cannot separate "reaches a record-reading resolver" from "is a string that looks like an icon name" is a gate that gets suppressed.
Which record does a given site read? The answer differs per resolver (table above), and a gate that checks the wrong one is worse than none: it would bless edit because the dynamic surface carries it.
Or is the real fix upstream of the gate — collapsing the four hand-copied resolvers into one shared helper, so there is a single place a check can attach and a single iconNameMap to maintain? That would make the gate small; it is also a bigger change than the gate.
⛔ Not the fix
Routing everything through the alias-tolerant getIcon is not the answer and should be ruled out up front: it silently substitutes the Database glyph for anything it cannot resolve, so it trades a no-icon failure for a wrong-icon failure. Recorded in #5622's body and re-stated by triage there.
Prior art to reuse
The membership-assertion shape is settled — four pins now use it (packages/plugin-view/src/__tests__/ViewSwitcher.test.tsx from #5586; plugin-detail, plugin-list and components pins from #5622). Each reads the map or registry entry, applies the consumer's own toPascalCase + iconNameMap, and asserts Object.prototype.hasOwnProperty.call(icons, key) — with a control that a name lucide keeps only as a deprecated export is rejected. A repo-level gate would generalise exactly that predicate.
Related: #5586, #5622, #5631 (ui:icon reading the SDUI identity key name as an icon name — a different defect on the same renderer).
Split out of #5622 (suggestion 2), where triage fenced it out of the repair deliberately: "That is a new gate over a new population, not a repair, and folding it in would turn a five-name fix into a gate-design card carrying its own evidence burden." Filed unassigned so it can be triaged on its own terms.
The class
lucide retires a spelling by dropping it from the runtime
iconsrecord while keeping it as a deprecated named export. A retired name therefore still imports, still type-checks, and still renders wherever it is used as a component — and resolves tonullwherever it is used as a string, because the string lookups read that record. Nothing goes red in either direction.This has now been repaired twice, in two different packages, by two different cards:
ViewSwitcherrenders NO icon forchartandganttviews: both icon names were dropped from lucide'siconsrecord #5586 —chart/ganttrendered no icon inplugin-view'sViewSwitcher.icons-record resolver —editin DetailView's mobile Edit action,smileas theiconrenderer's own default — and only one of the four resolver copies is pinned #5622 —editonDetailView's mobile Edit action,smileas theui:iconrenderer's own declared default, plus three component aliases inplugin-listand two retired fixture spellings inapp-shell.Both cards added a local resolvability pin. Neither could add one over the population, because there is no population — just four hand-copied
toPascalCase+ one-entryiconNameMap({ Home: 'House' }) implementations, each with its own set of call sites:packages/components/src/renderers/action/resolve-icon.tsiconsrecordpackages/components/src/renderers/basic/icon.tsxiconsrecordpackages/plugin-list/src/components/TabBar.tsxiconsrecordpackages/plugin-view/src/ViewSwitcher.tsxiconsrecordpackages/app-shell/src/utils/getIcon.ts,packages/layout/src/NavigationRenderer.tsxlucide-react/dynamic.mjs'siconNamesThe two surfaces disagree about aliases, so the same
icon: 'edit'renders correctly in the sidebar and renders nothing in an action bar.What the gate would have to decide
Recorded because they are the design questions, not implementation detail — this is not a "just add a check" card:
icon:string literal inpackages/**/apps/**is 53 distinct names across 109 sites, and Two more retired lucide spellings reach theicons-record resolver —editin DetailView's mobile Edit action,smileas theiconrenderer's own default — and only one of the four resolver copies is pinned #5622's scan found 15 that are not record keys of which only 4 were defects. The other 11 are false positives of three distinct kinds — a differently-typediconfield holding a Tailwind class (text-amber-500and eight siblings inapp-shell/…/previews/AppNavCanvas.tsx), deliberate negative controls (not-a-real-icon), an authorable-keys fixture that is never resolved (chevron), and ComponentRegistry/schema type names that merely collide with lucide spellings (area-chart,bar-chart,line-chart,pie-chart,scatter-chart). A gate that cannot separate "reaches a record-reading resolver" from "is a string that looks like an icon name" is a gate that gets suppressed.editbecause the dynamic surface carries it.icons-record resolver —editin DetailView's mobile Edit action,smileas theiconrenderer's own default — and only one of the four resolver copies is pinned #5622 measured thatGrid === Grid3x3,BarChart3 === ChartColumn,Edit === SquarePen,Smile === FaceSlightlySmiling,CheckCircle === CircleCheckBigandXCircle === CircleXare alltrue— a retired component alias is the very same object under a dead name, renders perfectly, and only record membership can see it. Both prior cards had to pin identifiers as well as strings.iconNameMapto maintain? That would make the gate small; it is also a bigger change than the gate.⛔ Not the fix
Routing everything through the alias-tolerant
getIconis not the answer and should be ruled out up front: it silently substitutes theDatabaseglyph for anything it cannot resolve, so it trades a no-icon failure for a wrong-icon failure. Recorded in #5622's body and re-stated by triage there.Prior art to reuse
The membership-assertion shape is settled — four pins now use it (
packages/plugin-view/src/__tests__/ViewSwitcher.test.tsxfrom #5586;plugin-detail,plugin-listandcomponentspins from #5622). Each reads the map or registry entry, applies the consumer's owntoPascalCase+iconNameMap, and assertsObject.prototype.hasOwnProperty.call(icons, key)— with a control that a name lucide keeps only as a deprecated export is rejected. A repo-level gate would generalise exactly that predicate.Related: #5586, #5622, #5631 (
ui:iconreading the SDUI identity keynameas an icon name — a different defect on the same renderer).