Found while widening the DOM-leak sweep in #5574 (PR #5629). Out of that card's scope — it touches no renderer source — so recorded here.
What happens
packages/components/src/renderers/basic/icon.tsx resolves its glyph from schema.name:
const iconName = toPascalCase(schema.name);
const mappedIconName = iconNameMap[iconName] || iconName;
const Icon = (icons as any)[mappedIconName];
if (!Icon) {
console.warn(...);
return null;
}
But name is not this renderer's private prop — it is the SDUI identity key every authored node carries, alongside id. So a perfectly ordinary node like
{ type: 'ui:icon', id: 'save_icon', name: 'save_icon' }
resolves SaveIcon, misses, and returns null. Nothing renders. There is no error boundary, no placeholder, no visible failure — just an absent icon and one console.warn, which Vitest 4 discards for a passing test (silent: 'passed-only') and which nobody reads in a browser either.
How it was measured
The sweep renders every registry-reachable widget through the real SchemaRenderer path with a canary node that sets name: 'canary_node' — an ordinary authored identity, nothing exotic. ui:icon was one of 12 targets that rendered zero elements and therefore read as clean, because an empty scan reports no leaks. It only started rendering once the target was given schemaExtras: { name: 'check' } to override the identity key, and once it did it turned out to leak 14 attributes (it is in PR #5629's ledger under BARE_SPREAD_ON_SVG).
So the collision was invisible in two independent ways at once: silent at runtime, and clean-looking to a gate.
Why this is a contract question, not a one-line fix
The tempting fix — read schema.icon ?? schema.name in the consumer — is the tolerant-consumer shape #5574's family is being migrated away from, and it would make name mean two different things depending on whether the lookup happens to hit. The real question is which key names an icon on an icon node, answered once in the spec rather than per renderer:
icon is the icon key; name is identity, always. Consistent with every other renderer and with action:*, which already reads schema.icon. Breaks any authored content relying on name, which is why it needs a look at real metadata rather than a guess.
ui:icon keeps name, and the identity key is documented as unavailable on this one node type — cheap, but it is a carve-out in the authoring model that an author cannot discover before it silently renders nothing.
Either way the failure mode should stop being silent: an unresolvable icon should render a visible placeholder or be refused at publish time, not return null.
Related but distinct: #5622 and #5586 both concern retired lucide spellings reaching this same icons-record resolver. Those are about which names resolve; this is about which schema key is consulted in the first place. A fix for either does not close the other.
References
Found while widening the DOM-leak sweep in #5574 (PR #5629). Out of that card's scope — it touches no renderer source — so recorded here.
What happens
packages/components/src/renderers/basic/icon.tsxresolves its glyph fromschema.name:But
nameis not this renderer's private prop — it is the SDUI identity key every authored node carries, alongsideid. So a perfectly ordinary node likeresolves
SaveIcon, misses, and returnsnull. Nothing renders. There is no error boundary, no placeholder, no visible failure — just an absent icon and oneconsole.warn, which Vitest 4 discards for a passing test (silent: 'passed-only') and which nobody reads in a browser either.How it was measured
The sweep renders every registry-reachable widget through the real
SchemaRendererpath with a canary node that setsname: 'canary_node'— an ordinary authored identity, nothing exotic.ui:iconwas one of 12 targets that rendered zero elements and therefore read as clean, because an empty scan reports no leaks. It only started rendering once the target was givenschemaExtras: { name: 'check' }to override the identity key, and once it did it turned out to leak 14 attributes (it is in PR #5629's ledger underBARE_SPREAD_ON_SVG).So the collision was invisible in two independent ways at once: silent at runtime, and clean-looking to a gate.
Why this is a contract question, not a one-line fix
The tempting fix — read
schema.icon ?? schema.namein the consumer — is the tolerant-consumer shape #5574's family is being migrated away from, and it would makenamemean two different things depending on whether the lookup happens to hit. The real question is which key names an icon on an icon node, answered once in the spec rather than per renderer:iconis the icon key;nameis identity, always. Consistent with every other renderer and withaction:*, which already readsschema.icon. Breaks any authored content relying onname, which is why it needs a look at real metadata rather than a guess.ui:iconkeepsname, and the identity key is documented as unavailable on this one node type — cheap, but it is a carve-out in the authoring model that an author cannot discover before it silently renders nothing.Either way the failure mode should stop being silent: an unresolvable icon should render a visible placeholder or be refused at publish time, not return
null.Related but distinct: #5622 and #5586 both concern retired lucide spellings reaching this same
icons-record resolver. Those are about which names resolve; this is about which schema key is consulted in the first place. A fix for either does not close the other.References
packages/components/src/renderers/basic/icon.tsxpackages/components/src/renderers/**— four layout/basic renderers still carry the unfixed spread #5574 / PR test(app-shell): widen the DOM-leak sweep topackages/componentsrenderers, with the first run's ledger #5629 — where it surfaced, and the sweep'sschemaExtrasworkaroundicons-record resolver —editin DetailView's mobile Edit action,smileas theiconrenderer's own default — and only one of the four resolver copies is pinned #5622,ViewSwitcherrenders NO icon forchartandganttviews: both icon names were dropped from lucide'siconsrecord #5586 — the lucide-spelling family on the same resolver