Skip to content

fix(components): flex declares the containment it renders - #6762

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6740-flex-is-container
Aug 29, 2026
Merged

fix(components): flex declares the containment it renders#6762
os-sales merged 1 commit into
mainfrom
claude/issue-6740-flex-is-container

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes #6740

flex has always rendered schema.children, but its registration omitted isContainer while grid, card, container and stack — same directory, same ui namespace — all declare it. The render path never reads the flag, so nothing was broken at runtime; the flag's consumers are elsewhere, and the gap made them contradict the renderer.

Reproduced first, with the card's own control

Base b76ca6764, the card's re-check verbatim:

git grep -c isContainer -- packages/components/src/renderers/layout/flex.tsx   ->  no output, exit 1  (zero matches)
git grep -c isContainer -- packages/components/src/renderers/layout/grid.tsx   ->  grid.tsx:1, exit 0

That zero is a reading only because the positive control returns non-zero in the same corpus and under the same command shape.

The defect measured through the mechanism, inside this repo

@object-ui/sdui-parser is already a dependency of @object-ui/components, so the downstream path is constructible here — no NOT MEASURED fallback was needed. The manifest was built exactly the way the app builds it (getKnownTypes() + getMeta() fed to manifestFromConfigs, mirroring renderers/layout/page.tsx:462) and a node carrying children put through validateTree.

On base b76ca6764:

node diagnostics
flex with children ["not-a-container"]
grid with children []
card with children []
container with children []

With this change, flex returns [] and the three controls are unchanged. This is the same warning objectstack measured downstream: its three shipped examples/app-showcase html pages drew 232 diagnostics, and every one of the 32 warnings was not-a-container on flex, with flex their only source. The 200 errors are a separate matter (objectstack#12924 owns it) and are untouched here.

The form is copied from the siblings, not invented

grid, card, container and stack all place isContainer: true in the registration's third argument, after defaultProps. This change matches that placement and value.

Where they disagree, and the call made. All four ui-namespace siblings pair isContainer with resizable: true and a resizeConstraints block. The page:* containers in containers.tsx declare isContainer: true on its own, with neither. So the flag is independent of designer resize affordances, and only isContainer is added here — minting a resize affordance for flex would be a behaviour change this card did not measure.

Reviewer attention: a second consumer, beyond the diagnostic

The card asked that anything altering behaviour past not-a-container be surfaced rather than absorbed. One thing does, and it is measured rather than reasoned about.

renderers/layout/react-page.tsx:77 builds the JSX scope of every kind:'react' page with if (!tag || cfg.isContainer) continue;, reading the predicate off getPublicConfigs(). While flex omitted the flag it was the one layout primitive of the five still injected there.

Measured by rendering real kind:'react' page source through SchemaRenderer:

identifier base b76ca6764 with this change
Flex resolves not defined
Grid not defined not defined
Card not defined not defined
Container not defined not defined
Stack not defined not defined
Button, Badge, Text, Image resolve resolve

The leaf-block row is the direction control: without it, "containers are dropped" would be indistinguishable from "everything is dropped".

Two things make this the correcting direction rather than a regression, but a reviewer should still rule on it:

  1. content/docs/guide/react-pages.md has documented this behaviour all along and names this component first — "Layout containers are deliberately not injected. The scope builder skips every container ... so flex, grid, card and friends have no injected wrapper." The prose predates the 2026-08-21 rewrite that added the code citation; only flex did not obey it.
  2. The behaviour being removed is itself silently broken. The injected wrapper is ({ children: _children, ...props }) — it discards children. Measured on base: a react page writing this component around a child rendered div data-obj-type="flex" class="flex flex-row ..." with the child gone. After the change the author gets the page-level error panel naming the identifier, and that doc page's stated remedy is real HTML with an inline style object.

So the change moves a react page from silently swallowing content to failing loudly with a documented fix. It is still a user-visible change on a published package, which is why it is called out here rather than left in the diff.

The pin covers the family, not just the one that was missing

The defect's shape is "three declare it and one does not". A pin covering only flex would let the next registration rot the same way and stay green while it did, so packages/components/src/__tests__/layout-containers-declare-containment.test.tsx asserts over all four, through validateTree rather than against the literal property — the property is only interesting because a mechanism reads it.

It carries three controls, each closing a way the pin could pass while measuring nothing:

  • reachability — no unknown-component in the result, and the probe node really has children (the containment branch is guarded by node.children?.length);
  • livenessbadge, a genuine leaf, must still draw not-a-container, so deleting the check or defaulting the flag on turns the file red;
  • direction — the four leaf blocks must stay non-containers on the public tier, so the react-page assertion is about containers and not about everything.

A fourth test renders flex through the real SchemaRenderer and asserts its children still arrive, which is the scope guard for "nothing else changes".

Ablation

Against the committed implementation e44f941ba, with the restore trapped on EXIT INT TERM using absolute paths.

  • Mutation confirmed on disk before anything was run: anchor count 1 -> 0, and blob 9cd1720a0... -> f5994b334... (a differing hash, not an exit code).
  • Pin on the mutated tree: 2 failed, 9 passed, exit 1. The two reds are exactly the two consumers — "flex with children draws no not-a-container" (expected [ { severity: 'warning', ... } ] to deeply equal []) and "flex reports as a container on the public tier" (expected undefined to be true). The three sibling cases stayed green, since they legitimately declare the flag.
  • Restore proven, not assumed: git checkout HEAD -- against the absolute path (naming HEAD, so the mutated index cannot be the source), then git diff HEAD empty and restored blob 9cd1720a0... equal to that path's HEAD blob hash.
  • Pin on the restored tree: 11 passed, exit 0.

No rebuild step is involved and none is owed: vitest.config.mts aliases every @object-ui/* specifier to packages/*/src, and the pin reaches flex.tsx by relative import, so the mutated source is what ran. Nothing resolved through dist.

Gates

All run on e44f941ba with a clean tree, every exit code captured by redirect-then-capture and each result quoted from the tool's own verdict line.

gate result
pnpm exec vitest run packages/components/ packages/core/src/registry/ examples/schema-catalog/ Test Files 228 passed (228) / Tests 3875 passed (3875), exit 0
console public-contract suites (5 files reading getPublicConfigs / PUBLIC_BLOCKS) Test Files 5 passed (5) / Tests 152 passed (152), exit 0
pnpm --filter @object-ui/components type-check exit 0
node scripts/check-changeset-presence.mjs exit 0, "1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"
control-byte self-scan over the changed files no matches
eslint . in packages/components (this package's own lint gate, which is what turbo run lint invokes for it) exit 0 — 422 files, 0 errors, 938 pre-existing warnings

The type-check number is qualified rather than quoted bare: packages/components/tsconfig.json excludes src/__tests__, so tsc --noEmit alone says nothing about the new pin. The chained tsc -p tsconfig.test.json is what covers it, and --listFiles was used to confirm the file is actually in that program rather than assumed to be.

The repo-wide pnpm lint is turbo run lint, which fans out to each package's own lint. Only this package's run was taken, and that is a measurement rather than a skip, on three pieces of evidence: the population is eslint's own (422 files, counted from --format json, not from a guess about what counts); both changed files are inside that population and were linted (the test file clean, flex.tsx carrying one pre-existing no-explicit-any warning at line 15 column 94 — the original registration signature, far from the line 146 edit); and eslint.config.js configures no projectService and no parserOptions.project, so type-aware linting is off and each file's verdict is a function of its own source alone — this diff cannot move a verdict in a package it does not touch.

CI owns the full farm and runs it regardless.


Generated by Claude Code

`flex` has always rendered `schema.children`, but its registration omitted
`isContainer` while `grid`, `card`, `container` and `stack` all declare it.
The render path never reads the flag, so nothing was broken at runtime; its
consumers are elsewhere, and the gap made them contradict the renderer.

Measured through the mechanism rather than the property: the manifest built
the way the app builds it, with a `flex` node carrying children put through
`validateTree`, returned ["not-a-container"] while grid/card/container under
the identical probe returned [] — the control that makes the reading real.

Pinned over the family, not over `flex` alone: the defect's shape was "three
declare it and one does not", so a pin covering only the missing one would let
the next registration rot the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 48 chunks) 3180.0 KB 3222.7 KB
Main entry chunk (gzip) 148.2 KB 350 KB
Entry file index-D1Tv6WNW.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 511.68KB 116.30KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 240.41KB 60.56KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.89KB 12.91KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.44KB 34.48KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.43KB 62.45KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.96KB 32.63KB
plugin-gantt (index.js) 165.20KB 40.37KB
plugin-grid (index.js) 201.53KB 54.54KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 113.11KB 27.58KB
plugin-map (index.js) 20.17KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 26.98KB 7.78KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 69.47KB 23.06KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

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

Projects

None yet

2 participants