Skip to content

finding: the DOM-leak sweep never reaches packages/components/src/renderers/** — four layout/basic renderers still carry the unfixed spread #5574

Description

@os-sales

Observation-class finding, recorded while implementing #4787 (PR #5573). Not fixed there — that card's file surface was grid.tsx alone, and packages/components is a large shared package.

The gap

packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx is the measurement gate for the #3291 / #4425 whitelist migration. Its target list is four plugin packages (plugin-charts, plugin-calendar, plugin-chatbot, plugin-dashboard). It does not sweep the packages/components renderers at all.

That is why ui:grid leaked unobserved: #4008's ratchet pins only className, and nothing else looked at the rendered attributes of a packages/components renderer. The leak was found by hand during #4011 / PR #4785, not by a gate.

The renderers still carrying it

The same bare-spread shape grid.tsx had — destructure data-obj-id / data-obj-type / style, spread the rest onto a host element — is present in at least these, all on origin/main at ac73c24b0:

  • packages/components/src/renderers/layout/flex.tsx (flex.tsx:63-76)
  • packages/components/src/renderers/layout/stack.tsx (stack.tsx:65-77)
  • packages/components/src/renderers/layout/container.tsx (container.tsx:78-90)
  • packages/components/src/renderers/basic/text.tsx (text.tsx:18-22)

Listed as candidates to measure, not asserted as defects — the shape is the same, but whether a given node's keys actually reach an element was verified only for grid. That verification is cheap and is exactly what the sweep does.

For reference, the measured grid reading before PR #5573, on a canary node — eight illegitimate attributes. (Tags are written with a space after < and before >, the same workaround #3291's body uses: GitHub's body sanitizer eats angle-bracket-delimited runs even inside fenced code blocks, and it emptied this block on the first revision of this issue.)

< div class="grid grid-cols-4 sm:grid-cols-2 md:grid-cols-2 gap-4 authored-class"
columns="4" gap="4" mdcolumns="2" smcolumns="2" id="grid-node" name="grid_node"
props="[object Object]" zzcanary="leak" data-testid="g" aria-label="A grid"
role="region" colorvariant="x" data-obj-id="grid-node" data-obj-type="grid" >

Why this is the pin worth having, rather than a warning-as-error pin

#4787 also asked whether React's unknown-attribute warning should be pinned as an error so the class stays closed. PR #5573 answers that in full; the short version is no, because such a pin is blind to most of the defect:

  1. React does not warn for unknown all-lowercase attributes at all — it passes them through by design and stringifies object values. columns, gap, name, props produce zero output. Measured: 8 leaked attributes, 3 console.error calls.
  2. The warning that does fire is the camelCase one, and React's own suggested remedy ("spell it lowercase") silences the warning while keeping the leak.
  3. Vitest 4's reporter defaults to silent: 'passed-only', so console output from a passing test is discarded — the warning can only be seen in a run that is already red for another reason.
  4. React latches the warning per prop name per module instance, so it fires only on the first render carrying a key. This produced a real phantom check in PR fix(components): whitelist ui:grid's DOM passthrough so schema keys stop leaking as attributes #5573: a console.error case written against a shared canary passed against code leaking ten attributes, because earlier cases consumed the latch.

The technique that actually closes the class reads the DOM, not the console — which is what the sweep already does, and it catches lowercase and camelCase alike.

Suggested shape

Extend the existing sweep's target list to the registry-reachable renderers in packages/components, deleting rows from its ledger as each renderer converges on toDomProps — the same per-package migration pattern #4425 phase 2 already runs. The dependency-direction argument in that file's docblock still holds (app-shell may import components).

Sizing note: packages/components registers far more renderers than the four plugin packages did, so the first run should be expected to record a ledger, not to come back clean.

References


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

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

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions