Skip to content

fix(components): route flex/stack/container/text through toDomProps (#5574) - #5839

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5574-components-renderer-dom-leaks
Aug 23, 2026
Merged

fix(components): route flex/stack/container/text through toDomProps (#5574)#5839
os-zhuang merged 2 commits into
mainfrom
claude/issue-5574-components-renderer-dom-leaks

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#5574

Routes flex, stack, container and text through toDomProps — the whitelist PR #5573 landed on grid.tsx — and deletes their four rows from the DOM-leak sweep's ledger, which is the only way the gate goes green again.

This is the burn-down half of #5574. The measurement half landed in PR #5629 (the sweep widening plus the 119-row arrival ledger); the post-merge reading in comment 5379776530 is what said the card was measured but not delivered.

The reading, re-taken rather than trusted

Every examples/schema-catalog node of these types rendered through the real SchemaRenderer, attributes read off the node's own host element:

attributebeforeafterattributebeforeafter
text[content]5220container[padding]140
flex[align]1980container[maxwidth]60
flex[gap]1930flex[direction]50
stack[gap]1530stack[align]40
flex[justify]980text[value]10
TOTAL11940

grid read 0 across its 26 nodes in both runs — the control the dispatch asked to be kept. A fixed renderer reads clean while its unfixed siblings read dirty, so the instrument is demonstrably not blind.

Two deltas from the numbers in comment 5379776530, both explained rather than waved at:

  • flex[gap] is 193, not 184. PR Author the nine catalog space-x-* flex nodes as gap, and ratchet the tenth out #5826 re-authored nine space-x-* flex nodes as gap after that reading was taken. Renderer-side nothing moved; this is the "grows with the examples" property that comment already called out. Every other layout figure reproduces exactly.
  • text leaks too, and it is the largest single reading in the family — 523 attributes. The card body named text as a candidate; the post-merge measurement did not cover it. The same probe shows it leaking, so it is fixed here with the count stated. All but one of those attributes are content — the key the renderer renders as its children, duplicated into the markup as an attribute.

text also carries the phantom-clean caveat in miniature: of its 699 nodes, 176 rendered no element at all (the bare-fragment return, taken when a node has neither designer id nor className). Those 176 were never evidence of safety. Recorded in the source and asserted, not hidden.

Reverse-verification, on a real commit

Fix committed first (3ed962a73), then the four renderer sources — and only those — reverted to origin/main, mutation confirmed on disk (toDomProps count 4→0 per file, the bare-spread marker 'data-obj-type': dataObjType back, git diff HEAD --stat naming exactly 4 files), both instruments re-run:

× every catalog node of these five types renders, and none leaks
AssertionError: expected [ …(1194) ] to deeply equal []
× ui:container AssertionError: expected 'ui:container leaked 14 non-DOM attrib…' to be ''
× ui:flex AssertionError: expected 'ui:flex leaked 14 non-DOM attribute(s…' to be ''
× ui:stack AssertionError: expected 'ui:stack leaked 14 non-DOM attribute(…' to be ''
× ui:text AssertionError: expected 'ui:text leaked 14 non-DOM attribute(s…' to be ''
Tests 5 failed | 197 passed (202)

Restored (the script carries a trap … EXIT INT TERM, so a foreground-cap kill mid-mutation could not leave the tree mutated for later measurements), git diff HEAD --stat empty, re-run: 202 passed.

Legs that do NOT discriminate, named rather than counted as evidence:

  • The grid leg. It reads 0 in every configuration of this change, so on its own it can never fail and proves nothing. It earns its place only jointly — as the reading that stays clean in the same run where its siblings flip.
  • The judge self-check. Passes identically before and after; it guards a different failure (an attribute reader that reports nothing), not this one.
  • The NODE_CENSUS assertion. Identical across the mutation — the same nodes render either way. It guards the blind-walk failure.

What landed, and what deliberately did not

Renderers — the grid.tsx treatment, not a second approach: destructure style (forwarded by name, the #4435 route, as the designer's sizing channel), spread toDomProps(hostProps), let data-obj-id / data-obj-type arrive through the open data-* family instead of by hand. Every leaked key was already being consumed off schema to build the class list, so nothing an author writes renders differently — the markup loses attributes that never had meaning and keeps the styling that did.

The sweep's ledger — four rows deleted, not edited. The gate's exact-set-equality-in-both-directions is what forced it: the fix cannot go green while the rows stand. The docblock keeps the arrival reading (119) in prose and records the burn-down; the assertions carry current truth (115).

The case that pinned the four as leaking is inverted, not deleted. It now asserts all five converged renderers are ABSENT from the ledger and still SWEPT. The sweep already fails if one regresses; what this adds is that the regression cannot be made green by putting the row back — the one repair the ledger's shape invites, and the one that turns a measurement into an allowlist entry.

The sweep's target list is NOT widened here — PR #5629 already did that, so the gap the card names is closed. Stated because the dispatch asked either way.

No renderer outside the measured set was touched. 115 rows remain, in the seven other measured shapes; they are the PM seat's burn-down parent to sequence.

Verification

Union re-run after the final commit, at 64ca95fe5, working tree clean, every exit code captured before any pipe:

gateverdict line
pnpm exec vitest run (repo root) — sweep + examples/schema-catalog + packages/componentsTest Files 192 passed (192) / Tests 3469 passed (3469)
type-check — components, app-shell, example-schema-catalogDone ×3, exit 0
lint — same three packagesDone ×3, exit 0 (0 errors; 906 / 2598 pre-existing warnings, none on a changed line)
node scripts/check-changeset-presence.mjs✅ 5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-no-major.mjs✅ No changeset declares a major bump
node scripts/check-control-bytes.mjs✅ OK (scanned 4857 tracked text file(s); skipped 85 binary)

Vitest always from the repo root, never pnpm --filter (#3378's guard). Every heavy command through the container's shared verify lock. The dependency closure was built first — pnpm --filter "PKG^..." build, with PKG standing in for the package name because GitHub's body sanitizer eats angle-bracket-delimited runs, which it did to the first revision of this line. Without that build, type-check reports TS2307: Cannot find module '@object-ui/core' on 25 untouched files and reads like a broken diff.

Lint narrowing, declared: repo-scope pnpm lint is CI's run and was not taken. What replaces it is package-scope eslint . on all three affected packages — a config-derived population, a provable superset of this diff at package granularity — plus a 6-file targeted run reporting files linted: 6, errors: 0 from --format json. eslint.config.js configures no project / projectService, so linting is not type-aware and this diff cannot move the verdict on any file it does not touch.


Generated by Claude Code

…#5574)
The four renderers objectui#5574 named as candidates all leaked, and the leak
was their OWN declared props: `align`, `gap`, `justify`, `direction`, `padding`,
`maxWidth`, `content`, `value` were consumed off `schema` to build the class
list and then spread onto the host element a second time, as attributes HTML
does not define. Converged on `toDomProps`, the whitelist `grid.tsx` was
converged on by objectui#4787 / PR #5573.
Measured across `examples/schema-catalog` through the real `SchemaRenderer`:
1194 illegitimate attributes over 1141 nodes before, 0 after, with `grid`'s 26
nodes reading 0 in both runs as the control that says the probe is not blind.
The four ledger rows in the DOM-leak sweep are DELETED rather than edited — the
gate's two-way expiry makes the fix fail until they go — and the case that
pinned them as leaking is inverted to pin that no row may re-absorb this family.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
…shape (#5574)
`<div align="start" gap={4}>` does not type-check, which is the point: the
attributes this file measures reached real elements through an untyped spread,
never through JSX attributes. The self-check now spreads a `Record<string,
unknown>` — the defect's own shape — instead of hand-writing attributes the
compiler would have caught anyway.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3227.2 KB3990.2 KB
Main entry chunk (gzip)152.7 KB350 KB
Entry fileindex-HE7rjYeh.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.13KB3.77KB
app-shell (runtime-config.js)12.80KB4.47KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)503.68KB113.91KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)186.86KB44.21KB
plugin-dashboard (index.js)128.44KB32.96KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.34KB60.99KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.80KB27.20KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.51KB20.72KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants

@os-zhuang@claude