Skip to content

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

Description

@os-sales

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:

resolver reads
packages/components/src/renderers/action/resolve-icon.ts icons record
packages/components/src/renderers/basic/icon.tsx icons record
packages/plugin-list/src/components/TabBar.tsx icons record
packages/plugin-view/src/ViewSwitcher.tsx icons record
packages/app-shell/src/utils/getIcon.ts, packages/layout/src/NavigationRenderer.tsx lucide-react/dynamic.mjs's iconNames

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:

  1. 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.
  2. 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.
  3. Does it also cover imported COMPONENTS, not just strings? 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 measured that Grid === Grid3x3, BarChart3 === ChartColumn, Edit === SquarePen, Smile === FaceSlightlySmiling, CheckCircle === CircleCheckBig and XCircle === CircleX are all true — 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.
  4. 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).

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:queue

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions